AppMethodOrder

简介:一个能让你了解所有函数调用顺序的 Android 库(无需侵入式代码)
更多:作者   提 Bug   
标签:

背景:当项目代码量很大的时候,或者你作为一名新人要快速掌握代码的时候,给函数打上 log,来了解代码执行逻辑,这种方式会显然成本太大,要改动项目编译运行,NO!太耗时;或者你想 debug 的方式来给你想关注的几个函数,来了解代码执行逻辑,NO!因为你肯定会漏掉函数;也许你可以固执的给你写的项目打满 log 说这样也行,但是你要知道你方法所调用的 jdk 的函数或者第三方 aar 或者 jar 再或者 android sdk 中的函数调用顺序你怎么办,还能打 log 吗?显然不行吧,来~这个项目给让可以让你以包名为过滤点过滤你想要知道所有函数调用顺序。

提醒:本文以及相关库是本人原创,转载请标注原文链接。

项目地址:https://github.com/zjw-swun/AppMethodOrder 欢迎 star

作者列表(排名按代码贡献时间顺序):二精-霁雪清虹,xingstarx,大精-wing,pighead4u ,Tesla ,lijunjie,三精-虹猫

特别鸣谢 : xingstarx 同学 ,提供兼容 mac 和 linux 的task AppOutPutMethodOrder代码
他的 github 地址:https://github.com/xingstarx/
大精-wing 同学,提供兼容不同版本 traceview 的task AppFilterMethodOrder代码
他的 github 地址:https://github.com/githubwing
pighead4u同学,提供的task AppOutPutMethodOrder task AppFilterMethodOrdergroovy 语法重构
他的 github 地址,https://github.com/pighead4u

3.gif

动作简介:首先点击 MainActivity 的自定义 MyTextView 然后进入 SecondActivity 再点击 textview 之后 finish 跳转回 MainActivity
下面是库处理过所得到的函数调用顺序order.txt文件(我这里屏蔽了 jdk 函数,第三方库函数,以及 android sdk 中函数,换句话说我就保留了我自己包名中的函数顺序)

832 ent     67593 .....com.zjw.appmethodorder.MainActivity.onClick (Landroid/view/View;)V    MainActivity.java
832 ent     99956 ..........com.zjw.appmethodorder.MainActivity.onPause ()V    MainActivity.java
832 ent     99970 ...........com.zjw.appmethodorder.BaseActivity.onPause ()V    BaseActivity.java
832 ent    100472 ............com.zjw.appmethodorder.BaseActivity.baseOnPause ()V    BaseActivity.java
832 ent    128540 ........com.zjw.appmethodorder.SecondActivity.<init> ()V    SecondActivity.java
832 ent    128562 .........com.zjw.appmethodorder.BaseActivity.<init> ()V    BaseActivity.java
832 ent    213911 ........com.zjw.appmethodorder.SecondActivity.onCreate (Landroid/os/Bundle;)V    SecondActivity.java
832 ent    213928 .........com.zjw.appmethodorder.BaseActivity.onCreate (Landroid/os/Bundle;)V    BaseActivity.java
832 ent    258414 ..........com.zjw.appmethodorder.BaseActivity.baseOnCreate ()V    BaseActivity.java
832 ent   1440503 .........com.zjw.appmethodorder.SecondActivity.onResume ()V    SecondActivity.java
832 ent   1440563 ..........com.zjw.appmethodorder.BaseActivity.onResume ()V    BaseActivity.java
832 ent   1445675 ...........com.zjw.appmethodorder.BaseActivity.baseOnResume ()V    BaseActivity.java
832 ent   2954291 .................com.zjw.appmethodorder.MyTextView.onWindowVisibilityChanged (I)V    MyTextView.java
832 ent   3065664 ........com.zjw.appmethodorder.MainActivity.onStop ()V    MainActivity.java
832 ent   3065701 .........com.zjw.appmethodorder.BaseActivity.onStop ()V    BaseActivity.java
832 ent   3069155 ..........com.zjw.appmethodorder.BaseActivity.baseOnStop ()V    BaseActivity.java
832 ent   3139519 .......com.zjw.appmethodorder.SecondActivity.click (Landroid/view/View;)V    SecondActivity.java
832 ent   3146300 ........com.zjw.appmethodorder.SecondActivity.finish ()V    SecondActivity.java
832 ent   3183478 ..........com.zjw.appmethodorder.SecondActivity.onPause ()V    SecondActivity.java
832 ent   3183498 ...........com.zjw.appmethodorder.BaseActivity.onPause ()V    BaseActivity.java
832 ent   3183843 ............com.zjw.appmethodorder.BaseActivity.baseOnPause ()V    BaseActivity.java
832 ent   3209420 ........com.zjw.appmethodorder.MainActivity.<init> ()V    MainActivity.java
832 ent   3209438 .........com.zjw.appmethodorder.BaseActivity.<init> ()V    BaseActivity.java
832 ent   3283359 ........com.zjw.appmethodorder.MainActivity.onCreate (Landroid/os/Bundle;)V    MainActivity.java
832 ent   3283378 .........com.zjw.appmethodorder.BaseActivity.onCreate (Landroid/os/Bundle;)V    BaseActivity.java
832 ent   3330938 ..........com.zjw.appmethodorder.BaseActivity.baseOnCreate ()V    BaseActivity.java
832 ent   4363295 .....................com.zjw.appmethodorder.MyTextView.<init> (Landroid/content/Context;Landroid/util/AttributeSet;)V    MyTextView.java
832 ent   4436094 ..................com.zjw.appmethodorder.MyTextView.onFinishInflate ()V    MyTextView.java
832 ent   4449689 .........com.zjw.appmethodorder.MainActivity.initView ()V    MainActivity.java
832 ent   4539427 .........com.zjw.appmethodorder.MainActivity.onResume ()V    MainActivity.java
832 ent   4539467 ..........com.zjw.appmethodorder.BaseActivity.onResume ()V    BaseActivity.java
832 ent   4543597 ...........com.zjw.appmethodorder.BaseActivity.baseOnResume ()V    BaseActivity.java
832 ent   4917854 .................com.zjw.appmethodorder.MyTextView.onAttachedToWindow ()V    MyTextView.java
832 ent   4918658 .................com.zjw.appmethodorder.MyTextView.onWindowVisibilityChanged (I)V    MyTextView.java
832 ent   5090653 ...................................com.zjw.appmethodorder.MyTextView.onMeasure (II)V    MyTextView.java
832 ent   5355203 ..................................com.zjw.appmethodorder.MyTextView.onMeasure (II)V    MyTextView.java
832 ent   5456681 .......................................com.zjw.appmethodorder.MyTextView.onSizeChanged (IIII)V    MyTextView.java
832 ent   5467577 ....................................com.zjw.appmethodorder.MyTextView.onLayout (ZIIII)V    MyTextView.java
832 ent   5876623 ...........................................com.zjw.appmethodorder.MyTextView.onDraw (Landroid/graphics/Canvas;)V    MyTextView.java
832 ent   6121967 ........com.zjw.appmethodorder.SecondActivity.onStop ()V    SecondActivity.java
832 ent   6121986 .........com.zjw.appmethodorder.BaseActivity.onStop ()V    BaseActivity.java
832 ent   6123689 ..........com.zjw.appmethodorder.BaseActivity.baseOnStop ()V    BaseActivity.java
832 ent   6127522 ........com.zjw.appmethodorder.SecondActivity.onDestroy ()V    SecondActivity.java
832 ent   6127679 .........com.zjw.appmethodorder.BaseActivity.onDestroy ()V    BaseActivity.java
832 ent   6133301 ..........com.zjw.appmethodorder.BaseActivity.baseOnDestroy ()V    BaseActivity.java

OK!发现是不是很炫酷啊,下面来介绍该库的原理(求 star!!!)

2 原理

本库其实并没有什么黑科技,本库也没有 java 代码,核心就是 2 个 build.gradle 中的 task。首先,原理就是基于 android sdk 中提供的工具----traceview,和 dmtracedump。traceview 会生成.trace 文件,该文件记录了函数调用顺序,函数耗时,函数调用次数等等有用的信息。而 dmtracedump 工具就是基于 trace 文件生成报告的工具,具体用法不细说。dmtracedump 工具大家一般用的多的选项就是生成 html 报告,或者生成调用顺序图片(看起来很不直观)。首先说说为什么要用 traceview,和 dmtracedump 来作为得到函数调用顺序的,因为这个工具既然能知道 cpu 执行时间和调用次数以及函数调用树(看出函数调用顺序很费劲)比如 android studio 是这样呈现.trace 文件的解析视图的

QQ 图片 20170326000536.png 或者这样的

QQ 图片 20170326000715.png (上面这张图是网上找的,侵删) 用上面这 2 个图发现你要清晰知道函数调用看懂了才是见鬼了。或者使用 dmtracedump 工具解析生成的 html 长下面这样(dmtracedump 生成图片就不说了 生成出的图片也根本看不出顺序这个就略过了)

QQ 图片 20170326001217.png 一开始我以为 Method 序列号有戏于是乎冲动的我把带序号的东西内容复制出来写了一个脚本对他们进行排序代码如下:

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Sort implements Comparable<Sort> {

    static String uri = "D:/Application/eclipse/javaworkspace/Test/src/JB/1.text";
    String str = "";
    String content = "";

    public Sort(String str,String content) {
        super();
        this.str = str;
        this.content = content;
    }

    public static void main(String[] args) throws IOException {
        // TODO Auto-generated method stub

        ArrayList<Sort> list = new ArrayList<>();

        BufferedReader in = new BufferedReader(new FileReader(uri));
        String a = "";

        while ((a = in.readLine()) != null) {
            //System.out.println(Long.valueOf(getIndexStr(a)));
            if(a.contains("com.zjw.appmethodorder")){
            list.add(new Sort(getIndexStr(a),a));
            }
        }
        Collections.sort(list);
        for (Sort sort : list) {
            System.out.println(sort.content);
        }
    }

    public static String getIndexStr(String str) {
        String regEx = "(?<=\\[)(\\S+)(?=\\])";// 匹配[]中的数字
        Pattern p = Pattern.compile(regEx);
        Matcher m = p.matcher(str.trim());
        while (m.find()) {
            return m.group().trim();
        }
        return "";
    }

    @Override
    public int compareTo(Sort o) {
        // TODO Auto-generated method stub
        //return 0;
        return (int) (Long.valueOf(str) - Long.valueOf(o.str));
    }

}

结果发现过滤后的东西序列号是按顺序的但是并不是代码执行的逻辑顺序。我擦怎么办,工具代码也写了,居然不是我想要的结果,好在花了一些时间发现 dmtracedump -ho 选项,经过研究发现,这玩意输出的内容居然是按逻辑顺序从上到下的,于是基于这一点我写一个项目,其实核心就是 2 个 task 完成了了解所有函数调用顺序的目的。

//核心任务:在 captures 文件目录下输出 基于最新.trace 文件的函数调用信息的 txt 版本
//说明:dmtracedump 为 android sdk 自带工具,要执行 dmtracedump 命令则需要先添加环境变量
task AppOutPutMethodOrder() {
    doLast {
        def capturesDirPath = project.getProjectDir().getParentFile().path + File.separator + "captures";
        def capturesDir = new File(capturesDirPath);
        capturesDir.traverse {
            if (it.isFile() && it.name.endsWith(".trace")) {
                def orderName = it.name.replace("trace", "txt")
                def orderFile = new File(capturesDirPath, orderName)
                orderFile.write("")
                def dmtracedumpDir = getDmtraceDumpDir();
                //说明:dmtracedump 为 android sdk 自带工具,要执行 dmtracedump 命令则需要先添加环境变量
                def baseComand = dmtracedumpDir + "dmtracedump  -ho " + it.absolutePath + " >> " + orderFile.absolutePath
                println baseComand
                String osNameMatch = System.getProperty("os.name").toLowerCase();
                if (osNameMatch.contains("windows")) {
                    ("cmd /c start  /b " + baseComand).execute()
                } else {
                    ["bash", "-c", baseComand].execute()
                }
            }
        }
    }
}

/**
 * read the sdk dir from local.properties
 * eg :
 *  sdk.dir = /home/env/sdk
 *  so:
 *   dmtracedump.dir = /home/env/sdk/platform-tools
 *
 * @return the dir which dmtracedump tools exists
 */
def getDmtraceDumpDir() {
    def rootDir = project.rootDir
    def localProperties = new File(rootDir, "local.properties")
    def sdkDir = null;
    if (localProperties.exists()) {
        Properties properties = new Properties()
        localProperties.withInputStream { instr ->
            properties.load(instr)
        }
        sdkDir = properties.getProperty('sdk.dir')
    }
    if (sdkDir == null || !(new File(sdkDir).exists())) {
        sdkDir = android.getSdkDirectory().getAbsolutePath()
    }
    if (sdkDir == null || !(new File(sdkDir).exists())) {
        sdkDir = android.plugin.getSdkFolder().getAbsolutePath()
    }
    def dmtraceDumpToolDir = sdkDir + File.separator + "platform-tools" + File.separator
    if (new File(dmtraceDumpToolDir).exists()) {
        return dmtraceDumpToolDir;
    }
    return ""
}


//这里 AppFilterMethodOrder 任务其实也不需要 执行找到 \captures 目录找到 base_order.txt
//用 Notepad++ 使用正则 先过滤 带 xit 的行 (我们只关注 ent 行就行,ent 代表进入执行函数 xit 代表退出函数)再过滤掉你不关心的包名
// Notepad++ 中过滤将会使用到的命令行如下
//^.*xit.*$ //去除掉 含有 xit 字符串的行  然后替换为空
// ^((?!XXX).)*$  //去除不包含 XXX 的行  然后替换为空
//^\s+   //合并空行  然后替换为空

task AppFilterMethodOrder() {
    doLast {
        //TODO 替换为你想要过滤的包名
        def filterPackageName = "com.zjw.appmethodorder"
        if (project.hasProperty("package_name")) {
            filterPackageName = project.getProperty("package_name")
        }
        //处理包名
        def filterSignature = filterPackageName.replaceAll("[.]", "/")

        def capturesDirPath = project.getProjectDir().getParentFile().path + File.separator + "captures";
        def capturesDir = new File(capturesDirPath);

        capturesDir.traverse {
            if (it.isFile() && it.name.endsWith(".txt") && !it.name.contains("--filter")) {
                def orderName = it.name.replace(".txt", "--filter.txt")
                def orderTimeName = it.name.replace(".txt", "--timefilter.txt")
                def orderFile = new File(capturesDirPath, orderName)
                orderFile.write("")
                def orderTimeFile = new File(capturesDirPath, orderTimeName)
                orderTimeFile.write("")
                it.eachLine { line ->

                    if (line.contains(" ent ")
                        //兼容不同版本 traceview 有的是方法包名有的是方法签名
                    && (line.contains(filterPackageName)
                    || line.contains(filterSignature))
                    ) {
                        orderFile.append(line + "\n")
                    }

                    //生成带 xit 和 ent 的 trace 行 函数耗时计算方式: xit 字符后 数值 减去 ent 字符后的 数字 (差值就是耗时 单位:微妙)
                    //注意:好像函数体中含 Thread.sleep 的计算不准确
                    if (line.contains(filterPackageName)
                            || line.contains(filterSignature)){
                        orderTimeFile.append(line + "\n")
                    }

                }
            }
        }


    }
}

3.如何使用

讲了一堆原理我们来说说这个库怎么用吧。

  • 下载 utils.gradle 到工程根目录
  • 修改根目录下 build.gradle,增加 apply from: rootProject.getRootDir().getAbsolutePath() + "/utils.gradle" 对 utils.gradle 的引用,具体可参考本工程根目录下的 build.gradle.
  • 使用下面的详细介绍生成 trace 文件
  • 使用下面的命令生成堆栈文件 ./gradlew AppOutPutMethodOrder
  • 上面命令文件内容太多时,通过这个命令进行过滤包含需要过滤的字符串 ./gradlew AppFilterMethodOrder -P package_name=com.zjw.appmethodorder

注意:请先确保 anroid sdk 中的 dmtracedump 工具加入在你的环境变量中(Mac 同学因为 task 面板执行的 bug 需要把 gradle 添加到环境变量中)

首先编译运行项目,然后点击下图的时钟(这是使用工具打 trace start 和 end)进行操作,可以参考上文所说的动作简介(记住你操作想想你的生命周期函数调用顺序,待会可以和生成的 captures 目录下 base_order.txt 或者生成的 order.txt 中的函数顺序做做对比)然后再点一次下图那个时钟。还有一种记录 trace start 和 end 的方式就是在修改代码,即使用android.os.Debug.startMethodTracing();android.os.Debug.stopMethodTracing(); QQ 图片 20170326003311.png 以上操作完成后即会在 captures 目录生成com.zjw.appmethodorder_2017.03.25_21.41.trace文件,android studio 会默认打开一个可视化窗口

QQ 图片 20170326003945.png

然后双击右侧面板的 AppOutPutMethodOrder任务 (特别注意:用 Mac 的同学注意了,现在已知双击执行 task 会输出空文件,貌似是 studio 的 BUG,可以使用 ./gradlew AppOutPutMethodOrder执行该任务)如下图

QQ 图片 20170327232840.png

这一步完成就将在captures目录生成和 trace 文件同名的 txt 文件(如示例com.zjw.appmethodorder_2017.03.25_21.41.trace),该文件包含所有函数执行顺序

等待任务执行完成,再双击执行AppFilterMethodOrder任务 (特别注意:用 Mac 的同学注意了,现在已知双击执行 task 会输出空文件,貌似是 studio 的 BUG,可以使用 ./gradlew AppFilterMethodOrder -P package_name=com.zjw.appmethodorder执行该任务)如下图窗口

QQ 图片 20170326004734.png 该任务目的就是过滤其他非相关包名,留下自己包名的函数,任务执行完成将在captures目录生成和·trace文件同名+--filter.txt文件(例如示例AppMethodOrder\captures\com.zjw.appmethodorder_2017.03.25_21.41--filter.txt) 如下图:

QQ 图片 20170327233401.png

接下来打开trace文件同名+--filter.txt文件(例如示例AppMethodOrder\captures\com.zjw.appmethodorder_2017.03.25_21.41--filter.txt)就是上文效果中的那样啦。

4.关于扩展和改造

QQ 图片 20170327233843.png

这里改成你想要过滤的包名即可。

5.小工具

  • Windows 环境下 可使用 tool 文件夹下的Method-trace-analysis.jar 直接导入.trace 文件,一键分析

6.执行 AppFilterMethodOrder 任务 新增后缀为--filterTime.txt 的文件,用于计算方法耗时

QQ 图片 20170329005447.png

如上图例:MainActivity.onPause方法执行耗时为149231-148152 = 1079,最终耗时为 1079μs(微秒) 约为 1 毫秒

1.生成带 xit 和 ent 的 trace 行 函数耗时计算方式: xit 字符后 数值 减去 ent 字符后的 数字 (差值就是耗时 单位:微妙)
2.注意:函数体中含 Thread.sleep 的计算不准确

7.新增将文件转换成 JSON 对象的工具

file2json.png 复制 file2json.gradle 到 app 路径下,给 build.gradle 中添加

apply from: 'file2json.gradle'

在文件中修改参数,默认的是解析后缀为"--filter.txt",地址在 captures 文件夹下

MAC 用户可以在 terminal 中使用

./gradlew file2json

在文件夹中可以看到.json 后缀的文件。

json 格式如下

{
    "methodName":"com.xxx.sample.MainActivity$2.onClick",
    "threadUID":19903,
    "useTime(us)":7973,
    "level":2,
    "params":{
        Landroid/view/View
    },
    "return":"V",
    "nextFunction":
            {
                "methodName":"com.xxx.sample.Fun.<init>",
                "threadUID":19903,
                "useTime(us)":33,
                "level":3,
                "params":{

                },
                "return":"V",
                "nextFunction":null
            },

            {
                "methodName":"com.xxx.sample.MainActivity$Callback.<init>",
                "threadUID":19903,
                "useTime(us)":20,
                "level":3,
                "params":{
                    Lcom/xxx/sample/MainActivity
                },
                "return":"V",
                "nextFunction":null
            },
}

注意:文件最后会多一个','

TODO

  • 解决 params 数组为空不显示 null 的问题(如图)
  • 调用链可能不准确的问题
  • 静态页面分析工具
Android 开发经验分享
用了一年多的理财推荐
Android 开发经验分享
用了一年多的理财推荐