Home

Awesome

<img src="https://github.com/zkwlx/DroidTelescope/blob/master/wiki/logo.png" width="50px" /> DroidTelescope(DT)

这是一套Android端线上应用性能监控框架,目前支持卡顿监控、内存泄露监控;后续还会增加更多监控对象。此项目参考自开源项目BlockCanaryEx

框架简介

架构图

<br>

使用效果

方法调用追踪(新功能)

可以通过接口 DroidTelescope.startMethodTracing 和 DroidTelescope.stopMethodTracing 追踪方法的调用栈的耗时, 例如想要监控 App 的启动耗时方法,可以在 App 中加入如下代码:

public class MyApplication extends Application {
    ...
    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        DroidTelescope.install();
        DroidTelescope.startMethodTracing();
    }
    ...
}
 
public class MainActivity extends Activity {
    ...
    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        String path = DroidTelescope.stopMethodTracing(this.getApplicationContext());
        // 打印日志路径。使用 SysTrace 时框架不会主动生成日志文件,由 Android 提供的 systrace.py 工具生成
        if (!TextUtils.isEmpty(path)) {
            Log.i("zkw", "加载完成:::>" + path);
        } else {
            Log.i("zkw", "Path is null, maybe use Systrace.");
        }
    }
    ...
}

使用框架自研模块追踪效果

之后按如下几部打开报告文件:

报告的效果和分析方法如下:

<br> 简单说明下两个时长的意义:

相比 Google 官方提供的检测方法,DroidTelescope 框架有如下优点:

使用 SysTrace 追踪效果

详细使用方式请参考官方文档,框架中使用 SysTrace 方式如下:

    public void init() {
        DroidTelescope.install(new MyConfig());
    }
    ...
    class MyConfig extends Config {
        @Override
        public boolean useSysTrace() {
            return true;
        }
    }
    ...

使用 SysTrace 后,DT 框架会在每个方法的开始和结束调用 Trace.beginSection(方法签名) 和 Trace.endSection(),并维护调用栈关系。 在开始追踪之前,运行命令:

systrace.py -o output.html --app=app进程名 app

结束后用浏览器打开 output.html,效果如下图所示。 <br>

卡顿监控

当发生卡顿时,框架会记录相关方法的调用时间和调用栈,并生成BlockInfo对象,使用框架提供的ConvertUtils工具将BlockInfo对象转换成JSON格式的日志,如下例子,每个字段的意义请看注释:

{
    "loop_wall_clock_time":319,//表示一次loop所消耗的时钟时长,单位是ms毫秒
    "loop_cpu_time":47,//表示一次loop所消耗的cpu时长,单位是ms毫秒
    "invoke_trace_array":[//表示一次loop记录的耗时方法调用关系
        {
            "method_signature":"plugin.gradle.my.SecondActivity.onCreate(android.os.Bundle)",
            "thread_id":1,
            "wall_clock_time":31.06,
            "cpu_time":20
        },
        {
            "method_signature":"plugin.gradle.my.BlankFragment.onCreateView(android.view.LayoutInflater,android.view.ViewGroup,android.os.Bundle)",
            "thread_id":1,
            "wall_clock_time":5.74,
            "cpu_time":5
        },
        {
            "method_signature":"plugin.gradle.my.SecondActivity.onResume()",//方法的签名
            "thread_id":1,//方法调用时所在线程id
            "wall_clock_time":257.77,//方法调用消耗的时钟时长,单位是ms毫秒
            "cpu_time":8,//方法调用消耗的cpu时长,单位是ms毫秒
            "invoke_trace":[//记录在当前方法中调用的其他子方法
                {
                    "method_signature":"plugin.gradle.my.dummy.DummyContent.<clinit>()",
                    "thread_id":1,
                    "wall_clock_time":6.62,
                    "cpu_time":6
                },
                {
                    "method_signature":"plugin.gradle.my.dummy.DummyContent.sleep()",
                    "thread_id":1,
                    "wall_clock_time":250.18,
                    "cpu_time":0
                }
            ]
        }
    ]
}

框架不会记录所有方法,只有当方法耗时超过阈值时(可以配置)记录,日志中所有的time单位都是ms毫秒。 触发这次卡顿的源码结构是这样的:

public class SecondActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.second_layout);

        FragmentManager fm = getSupportFragmentManager();
        FragmentTransaction tx = fm.beginTransaction();
        BlankFragment blankFragment = new BlankFragment();
        tx.add(R.id.id_content, blankFragment, "ONE");
        tx.commit();

        blankFragment.onLowMemory();
    }

    @Override
    protected void onResume() {
        super.onResume();
        DummyContent d = new DummyContent();
        d.sleep();
    }
    
}
public class DummyContent {
    public void sleep() {
        try {
            Thread.sleep(250);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

内存泄露监控

下面模拟一个内存泄露的环境:MainActivity启动SecondActivity,SecondActivity添加一个BlankFragment,BlankFragment会导致泄露,泄露代码如下:

public class BlankFragment extends Fragment {

    public static List<Activity> ins = new ArrayList<>();

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        ins.add(activity);
    }
}

当发生内存泄露时(目前只支持监控Activity和Fragment的引用泄露),会创建LeakInfo对象,使用框架提供的ConvertUtils工具将LeakInfo对象转换成Json格式,如下例子,每个字段的意义请看注释:

{
    "garbage_reference_list":[//怀疑是泄露对象的列表
        {
            //泄露对象的id,通过Object.toString()生成
            "objectId":"BlankFragment{1324e62}",
            //泄露对象的调用链,只记录Activity、Fragment之间的调用关系
            "object_create_chain":"plugin.gradle.my.MainActivity@2695ae7->plugin.gradle.my.SecondActivity@6cce780->BlankFragment{1324e62 #0 id=0x7f0b006f ONE}"
        },
        {
            "objectId":"plugin.gradle.my.SecondActivity@6cce780",
            "object_create_chain":"plugin.gradle.my.MainActivity@2695ae7->plugin.gradle.my.SecondActivity@6cce780"
        }
    ]
}

使用方法

(不知为何bintray一直不通过我的包,所以jcenter上还没有插件包,可以先使用本地编译,见谅) <br>框架会在编译期间注入代码,首先配置代码注入的插件,将repo目录复制到你自己项目的根目录,在项目app的build.gradle文件中加入如下代码:

buildscript {
    repositories {
        maven {
            url uri('../repo')
        }
        jcenter()
    }
    dependencies {
        classpath 'andr.perf.monitor:TelescopeInjector:0.9.0'
    }
}
apply plugin: 'telescope.injector'

然后项目添加对DroidTelescope库的依赖,可以直接使用项目目录下的DroidTelescope_v0.8.0_xxxxxx.jar包, 添加完后,大致是这个样子: <br>

然后再代码中配置监控框架,建议在自定义的Application.onCreate中配置,示例如下:

public class MyApplication extends Application {

    // 需要自定义配置项时,重写Config的相应方法
    private Config config = new AndrPerfMonitorConfig();

    // 设置监听器,当发生卡顿或者内存泄露时回调
    private DroidTelescope.BlockListener blockListener = new MyBlockListener();
    private DroidTelescope.LeakListener leakListener = new MyLeakListener();

    @Override
    public void onCreate() {
        super.onCreate();
        // 设置自定义配置
        DroidTelescope.install(config);
        // 设置监听器,当发生卡顿或者内存泄露时回调
        DroidTelescope.setBlockListener(blockListener);
        DroidTelescope.setLeakListener(leakListener);
    }

    //自定义配置类,本例没有自定义配置,所以没有重写任何方法
    private static class AndrPerfMonitorConfig extends Config {
        
    }

    //卡顿监听器,当发生卡顿时,使用框架提供的转换工具类将BlockInfo转换成Json,并保存到文件
    private static class MyBlockListener implements DroidTelescope.BlockListener {
        @Override
        public void onBlock(BlockInfo blockInfo) {
            JSONObject blockInfoJson = null;
            //使用框架提供的转换工具,将BlockInfo对象转换成Json格式
            try {
                blockInfoJson = ConvertUtils.convertBlockInfoToJson(blockInfo);
            } catch (JSONException e) {
                e.printStackTrace();
            }
            //可以将json数据上传服务器,或者保存到本地
            if (blockInfoJson != null) {
                FileUtils fileUtils = new FileUtils();
                String blockJson = blockInfoJson.toString();
                fileUtils.write2SDFromInput("", "block.txt", blockJson);
            }
        }
    }

    //泄露监听器,当发生内存泄露时,使用框架提供的转换工具类将LeakInfo转换成Json,并保存到文件
    private static class MyLeakListener implements DroidTelescope.LeakListener {
        @Override
        public void onLeak(LeakInfo leakInfo) {
            JSONObject leakInfoJson = null;
            //使用框架提供的转换工具,将LeakInfo对象转换成Json格式
            try {
                leakInfoJson = ConvertUtils.convertLeakInfoToJson(leakInfo);
            } catch (JSONException e) {
                e.printStackTrace();
            }
            //可以将json数据上传服务器,或者保存到本地
            if (leakInfoJson != null) {
                FileUtils fileUtils = new FileUtils();
                String leakJson = leakInfoJson.toString();
                fileUtils.write2SDFromInput("", "leak.txt", leakJson);
            }
        }
    }
    
}

对应用的性能影响测试


欢迎关注我的公众号 二叉树根子,在这里可以看到不曾见过的 Android 底层技术。

<img width="258" height="258" alt="公众号" src="wiki/binary tree root.jpg">

License

DroidTelescope使用的GPL3.0协议,详细请参考License