一個能讓你了解所有函數(shù)調(diào)用順序的Android庫
背景
當項目代碼量很大的時候,或者你作為一名新人要快速掌握代碼的時候,給函數(shù)打上log,來了解代碼執(zhí)行邏輯,這種方式會顯然成本太大,要改動項目編譯運行,NO!太耗時;或者你想debug的方式來給你想關(guān)注的幾個函數(shù),來了解代碼執(zhí)行邏輯,NO!因為你肯定會漏掉函數(shù);也許你可以固執(zhí)的給你寫的項目打滿log說這樣也行,但是你要知道你方法所調(diào)用的jdk的函數(shù)或者第三方aar或者jar再或者android sdk中的函數(shù)調(diào)用順序你怎么辦,還能打log嗎?顯然不行吧,來~這個項目給讓可以讓你以包名為過濾點過濾你想要知道所有函數(shù)調(diào)用順序。
效果奉上
動作簡介:首先點擊MainActivity的自定義MyTextView然后進入SecondActivity再點擊textview之后finish跳轉(zhuǎn)回MainActivity
下面是庫處理過所得到的函數(shù)調(diào)用順序order.txt文件(我這里屏蔽了jdk函數(shù),第三方庫函數(shù),以及android sdk中函數(shù),換句話說我就保留了我自己包名中的函數(shù)順序)
832 ent 67593 .....com.zjw.appmethodorder.MainActivity.onClick (Landroid/view/View;)VMainActivity.java 832 ent 99956 ..........com.zjw.appmethodorder.MainActivity.onPause ()VMainActivity.java 832 ent 99970 ...........com.zjw.appmethodorder.BaseActivity.onPause ()VBaseActivity.java 832 ent 100472 ............com.zjw.appmethodorder.BaseActivity.baseOnPause ()VBaseActivity.java 832 ent 128540 ........com.zjw.appmethodorder.SecondActivity.()VSecondActivity.java 832 ent 128562 .........com.zjw.appmethodorder.BaseActivity. ()VBaseActivity.java 832 ent 213911 ........com.zjw.appmethodorder.SecondActivity.onCreate (Landroid/os/Bundle;)VSecondActivity.java 832 ent 213928 .........com.zjw.appmethodorder.BaseActivity.onCreate (Landroid/os/Bundle;)VBaseActivity.java 832 ent 258414 ..........com.zjw.appmethodorder.BaseActivity.baseOnCreate ()VBaseActivity.java 832 ent 1440503 .........com.zjw.appmethodorder.SecondActivity.onResume ()VSecondActivity.java 832 ent 1440563 ..........com.zjw.appmethodorder.BaseActivity.onResume ()VBaseActivity.java 832 ent 1445675 ...........com.zjw.appmethodorder.BaseActivity.baseOnResume ()VBaseActivity.java 832 ent 2954291 .................com.zjw.appmethodorder.MyTextView.onWindowVisibilityChanged (I)VMyTextView.java 832 ent 3065664 ........com.zjw.appmethodorder.MainActivity.onStop ()VMainActivity.java 832 ent 3065701 .........com.zjw.appmethodorder.BaseActivity.onStop ()VBaseActivity.java 832 ent 3069155 ..........com.zjw.appmethodorder.BaseActivity.baseOnStop ()VBaseActivity.java 832 ent 3139519 .......com.zjw.appmethodorder.SecondActivity.click (Landroid/view/View;)VSecondActivity.java 832 ent 3146300 ........com.zjw.appmethodorder.SecondActivity.finish ()VSecondActivity.java 832 ent 3183478 ..........com.zjw.appmethodorder.SecondActivity.onPause ()VSecondActivity.java 832 ent 3183498 ...........com.zjw.appmethodorder.BaseActivity.onPause ()VBaseActivity.java 832 ent 3183843 ............com.zjw.appmethodorder.BaseActivity.baseOnPause ()VBaseActivity.java 832 ent 3209420 ........com.zjw.appmethodorder.MainActivity. ()VMainActivity.java 832 ent 3209438 .........com.zjw.appmethodorder.BaseActivity. ()VBaseActivity.java 832 ent 3283359 ........com.zjw.appmethodorder.MainActivity.onCreate (Landroid/os/Bundle;)VMainActivity.java 832 ent 3283378 .........com.zjw.appmethodorder.BaseActivity.onCreate (Landroid/os/Bundle;)VBaseActivity.java 832 ent 3330938 ..........com.zjw.appmethodorder.BaseActivity.baseOnCreate ()VBaseActivity.java 832 ent 4363295 .....................com.zjw.appmethodorder.MyTextView. (Landroid/content/Context;Landroid/util/AttributeSet;)VMyTextView.java 832 ent 4436094 ..................com.zjw.appmethodorder.MyTextView.onFinishInflate ()VMyTextView.java 832 ent 4449689 .........com.zjw.appmethodorder.MainActivity.initView ()VMainActivity.java 832 ent 4539427 .........com.zjw.appmethodorder.MainActivity.onResume ()VMainActivity.java 832 ent 4539467 ..........com.zjw.appmethodorder.BaseActivity.onResume ()VBaseActivity.java 832 ent 4543597 ...........com.zjw.appmethodorder.BaseActivity.baseOnResume ()VBaseActivity.java 832 ent 4917854 .................com.zjw.appmethodorder.MyTextView.onAttachedToWindow ()VMyTextView.java 832 ent 4918658 .................com.zjw.appmethodorder.MyTextView.onWindowVisibilityChanged (I)VMyTextView.java 832 ent 5090653 ...................................com.zjw.appmethodorder.MyTextView.onMeasure (II)VMyTextView.java 832 ent 5355203 ..................................com.zjw.appmethodorder.MyTextView.onMeasure (II)VMyTextView.java 832 ent 5456681 .......................................com.zjw.appmethodorder.MyTextView.onSizeChanged (IIII)VMyTextView.java 832 ent 5467577 ....................................com.zjw.appmethodorder.MyTextView.onLayout (ZIIII)VMyTextView.java 832 ent 5876623 ...........................................com.zjw.appmethodorder.MyTextView.onDraw (Landroid/graphics/Canvas;)VMyTextView.java 832 ent 6121967 ........com.zjw.appmethodorder.SecondActivity.onStop ()VSecondActivity.java 832 ent 6121986 .........com.zjw.appmethodorder.BaseActivity.onStop ()VBaseActivity.java 832 ent 6123689 ..........com.zjw.appmethodorder.BaseActivity.baseOnStop ()VBaseActivity.java 832 ent 6127522 ........com.zjw.appmethodorder.SecondActivity.onDestroy ()VSecondActivity.java 832 ent 6127679 .........com.zjw.appmethodorder.BaseActivity.onDestroy ()VBaseActivity.java 832 ent 6133301 ..........com.zjw.appmethodorder.BaseActivity.baseOnDestroy ()VBaseActivity.java
OK!發(fā)現(xiàn)是不是很炫酷啊,下面來介紹該庫的原理(求star!!!)
原理
本庫其實并沒有什么黑科技,本庫也沒有java代碼,核心就是2個build.gradle中的task。首先,原理就是基于android sdk中提供的工具----traceview,和dmtracedump。traceview會生成.trace文件,該文件記錄了函數(shù)調(diào)用順序,函數(shù)耗時,函數(shù)調(diào)用次數(shù)等等有用的信息。而dmtracedump 工具就是基于trace文件生成報告的工具,具體用法不細說。
dmtracedump 工具大家一般用的多的選項就是生成html報告,或者生成調(diào)用順序圖片(看起來很不直觀)。首先說說為什么要用traceview,和dmtracedump來作為得到函數(shù)調(diào)用順序的,因為這個工具既然能知道cpu執(zhí)行時間和調(diào)用次數(shù)以及函數(shù)調(diào)用樹(看出函數(shù)調(diào)用順序很費勁)比如android studio是這樣呈現(xiàn).trace文件的解析視圖的
或者這樣的
是網(wǎng)上找的,侵刪) 用上面這2個圖發(fā)現(xiàn)你要清晰知道函數(shù)調(diào)用看懂了才是見鬼了?;蛘呤褂胐mtracedump 工具解析生成的html長下面這樣(dmtracedump 生成圖片就不說了 生成出的圖片也根本看不出順序這個就略過了)
一開始我以為 Method 序列號有戲于是乎沖動的我把帶序號的東西內(nèi)容復制出來寫了一個腳本對他們進行排序代碼如下:
- 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/Testc/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+)(?=\\])";// 匹配[]中的數(shù)字
- 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));
- }
- }
結(jié)果發(fā)現(xiàn)過濾后的東西序列號是按順序的但是并不是代碼執(zhí)行的邏輯順序。我擦怎么辦,工具代碼也寫了,居然不是我想要的結(jié)果,好在花了一些時間發(fā)現(xiàn)dmtracedump -ho 選項,經(jīng)過研究發(fā)現(xiàn),這玩意輸出的內(nèi)容居然是按邏輯順序從上到下的,于是基于這一點我寫一個項目,其實核心就是2個task完成了了解所有函數(shù)調(diào)用順序的目的。
- //核心任務:在captures文件目錄下輸出 基于最新.trace文件的函數(shù)調(diào)用信息的txt版本
- //說明:dmtracedump 為 android sdk自帶工具,要執(zhí)行dmtracedump命令則需要先添加環(huán)境變量
- 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自帶工具,要執(zhí)行dmtracedump命令則需要先添加環(huán)境變量
- 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 任務其實也不需要 執(zhí)行找到 \captures 目錄找到 base_order.txt
- //用Notepad++ 使用正則 先過濾 帶 xit 的行 (我們只關(guān)注ent 行就行,ent代表進入執(zhí)行函數(shù) xit代表退出函數(shù))再過濾掉你不關(guān)心的包名
- // 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行 函數(shù)耗時計算方式: xit字符后 數(shù)值 減去 ent字符后的 數(shù)字 (差值就是耗時 單位:微妙)
- //注意:好像函數(shù)體中含Thread.sleep的計算不準確
- if (line.contains(filterPackageName)
- || line.contains(filterSignature)){
- orderTimeFile.append(line + "\n")
- }
- }
- }
- }
- }
- }
如何使用
講了一堆原理我們來說說這個庫怎么用吧。
- 下載utils.gradle到工程根目錄
- 修改根目錄下build.gradle,增加 apply from: rootProject.getRootDir().getAbsolutePath() + "/utils.gradle" 對utils.gradle的引用,具體可參考本工程根目錄下的build.gradle.
- 使用下面的詳細介紹生成trace文件
- 使用下面的命令生成堆棧文件 ./gradlew AppOutPutMethodOrder
- 上面命令文件內(nèi)容太多時,通過這個命令進行過濾包含需要過濾的字符串 ./gradlew AppFilterMethodOrder -P package_name=com.zjw.appmethodorder
注意:請先確保 anroid sdk 中的dmtracedump 工具加入在你的環(huán)境變量中(Mac同學因為task面板執(zhí)行的bug 需要把gradle添加到環(huán)境變量中)
首先編譯運行項目,然后點擊下圖的時鐘(這是使用工具打trace start 和 end)進行操作,可以參考上文所說的動作簡介(記住你操作想想你的生命周期函數(shù)調(diào)用順序,待會可以和生成的captures目錄下base_order.txt或者生成的order.txt中的函數(shù)順序做做對比)然后再點一次下圖那個時鐘。還有一種記錄trace start 和 end的方式就是在修改代碼,即使用android.os.Debug.startMethodTracing();和android.os.Debug.stopMethodTracing();
以上操作完成后即會在captures目錄生成
com.zjw.appmethodorder_2017.03.25_21.41.trace文件,android studio會默認打開一個可視化窗口
然后雙擊右側(cè)面板的 AppOutPutMethodOrder任務 (特別注意:用Mac的同學注意了,現(xiàn)在已知雙擊執(zhí)行task會輸出空文件,貌似是studio的BUG,可以使用 ./gradlew AppOutPutMethodOrder執(zhí)行該任務)如下圖
這一步完成就將在captures目錄生成和trace文件同名的txt文件(如示例com.zjw.appmethodorder_2017.03.25_21.41.trace),該文件包含所有函數(shù)執(zhí)行順序
等待任務執(zhí)行完成,再雙擊執(zhí)行AppFilterMethodOrder任務 (特別注意:用Mac的同學注意了,現(xiàn)在已知雙擊執(zhí)行task會輸出空文件,貌似是studio的BUG,可以使用 ./gradlew AppFilterMethodOrder -P package_name=com.zjw.appmethodorder執(zhí)行該任務)如下圖窗口
該任務目的就是過濾其他非相關(guān)包名,留下自己包名的函數(shù),任務執(zhí)行完成將在captures目錄生成和·trace文件同名+--filter.txt文件(例如示例AppMethodOrder\captures\com.zjw.appmethodorder_2017.03.25_21.41--filter.txt) 如下圖:
接下來打開trace文件同名+--filter.txt文件(例如示例AppMethodOrder\captures\com.zjw.appmethodorder_2017.03.25_21.41--filter.txt)就是上文效果中的那樣啦。
關(guān)于擴展和改造
這里改成你想要過濾的包名即可。
小工具
Windows 環(huán)境下 可使用tool文件夾下的Method-trace-analysis.jar 直接導入.trace文件,一鍵分析
執(zhí)行AppFilterMethodOrder 任務 新增后綴為--filterTime.txt 的文件,用于計算方法耗時
如上圖例:MainActivity.onPause方法執(zhí)行耗時為149231-148152 = 1079,最終耗時為 1079μs(微秒) 約為 1毫秒
1.生成帶xit 和 ent 的trace行 函數(shù)耗時計算方式: xit字符后 數(shù)值 減去 ent字符后的 數(shù)字 (差值就是耗時 單位:微妙)
2.注意:函數(shù)體中含Thread.sleep的計算不準確