????我們產(chǎn)線的主流程頁(yè)面中有幾個(gè)比較復(fù)雜的頁(yè)面在版本迭代中流暢度頻繁出現(xiàn)反復(fù),經(jīng)常由于開發(fā)的不注意導(dǎo)致變卡,主要是對(duì)流暢度缺少必要的監(jiān)控和可持續(xù)的優(yōu)化手段,這個(gè)系列是對(duì)上半年實(shí)踐App流暢度監(jiān)控、優(yōu)化過(guò)程中的一點(diǎn)總結(jié),希望可以給需要的同學(xué)一點(diǎn)小參考。
當(dāng)然App內(nèi)存上的優(yōu)化,盡量減少內(nèi)存抖動(dòng)也能顯著提升流暢度,內(nèi)存優(yōu)化方面可以參考之前的文章:實(shí)踐App內(nèi)存優(yōu)化:如何有序地做內(nèi)存分析與優(yōu)化
整個(gè)系列將主要包括以下幾部分:
-
卡頓與View的繪制過(guò)程解析
這部分內(nèi)容比較多,主要是從源碼層面解析一下整個(gè)過(guò)程,也是我們后面做流暢度監(jiān)控與優(yōu)化的基礎(chǔ)
-
Debug階段如何對(duì)實(shí)時(shí)幀率進(jìn)行監(jiān)控和顯示
根據(jù)上面的原理,設(shè)計(jì)一個(gè)顯示實(shí)時(shí)幀率的工具,可以有效的在開發(fā)階段發(fā)現(xiàn)問(wèn)題
-
如何實(shí)現(xiàn)流暢度自動(dòng)化測(cè)試
實(shí)現(xiàn)一個(gè)流暢度UI自動(dòng)化測(cè)試,在上線前跑一下UI自動(dòng)化并生成流暢度報(bào)表郵件給相關(guān)人員
-
線上的用戶流暢度的監(jiān)控方案
實(shí)時(shí)反映真實(shí)用戶的流暢度體驗(yàn),線上龐大的數(shù)據(jù)可以敏感的反應(yīng)出版本迭代間流暢度的變化
-
實(shí)現(xiàn)一個(gè)方便排查高耗時(shí)方法的工具
利用自定義gradle plugin+ASM插樁實(shí)現(xiàn)快速而準(zhǔn)確的找出耗時(shí)的方法,進(jìn)行針對(duì)性的優(yōu)化
-
分享提升app流暢度的一些經(jīng)驗(yàn)
分享一些成本小收益高的提升流暢度的方案
?
?
工欲善其事必先利其器,今天首先分享一下在優(yōu)化頁(yè)面流暢度過(guò)程中自己實(shí)現(xiàn)的一個(gè)方便快速排查高耗時(shí)方法的工具:
MethodTraceMan
,畢竟保持主流程流暢,避免在主流程執(zhí)行高耗時(shí)方法永遠(yuǎn)是優(yōu)化卡頓最直接的手段,只要我們能快速方便的排查到高耗時(shí)的方法,就可以做針對(duì)性優(yōu)化。
實(shí)現(xiàn)一個(gè)方便排查高耗時(shí)方法的工具
????平常我們用來(lái)排查Android卡頓的比較熟悉的工具有TraceView
、systrace
等,一般分為兩種模式:instrument
和sample
。但是這些工具不管是哪種模式都有各自不足的地方,比如instruement模式,可以獲得所有函數(shù)的調(diào)用過(guò)程,信息比較豐富,但是會(huì)帶來(lái)極大的性能開銷,導(dǎo)致統(tǒng)計(jì)的耗時(shí)與實(shí)際不符
;而sample模式是通過(guò)采樣的方式進(jìn)行分析的,所以信息豐富度上就大打折扣
,像systrace就屬于sample型的,它只能監(jiān)控一些系統(tǒng)調(diào)用的耗時(shí)情況。
????除了上面說(shuō)的工具,著名的JackWharton也實(shí)現(xiàn)了一個(gè)可以打印出出方法耗時(shí)的工具hugo
,它是基于注解觸發(fā)的,在一個(gè)方法上加上特定注解即可打印出該方法的耗時(shí)等信息,但是如果我們想排查高耗時(shí)方法,顯然在所有方法上一個(gè)一個(gè)加注解太費(fèi)勁了。
?
那么我們?cè)谧隹D優(yōu)化的過(guò)程中需要一個(gè)什么樣的工具呢?
- 可以方便地統(tǒng)計(jì)所有方法的耗時(shí)
- 對(duì)性能影響微小,能準(zhǔn)確統(tǒng)計(jì)出方法的精確耗時(shí)
- 支持耗時(shí)篩選、線程篩選、方法名搜索等功能,能快速發(fā)現(xiàn)主線程高耗時(shí)方法
????要實(shí)現(xiàn)這樣一個(gè)工具,首先想到的就是通過(guò)插樁技術(shù)
來(lái)實(shí)現(xiàn),在編譯過(guò)程中對(duì)所有的方法進(jìn)行插樁,在方法進(jìn)入和方法結(jié)束的地方進(jìn)行打點(diǎn),就可以在對(duì)性能影響很小的方式下統(tǒng)計(jì)到每個(gè)方法的耗時(shí)。統(tǒng)計(jì)到每個(gè)方法的耗時(shí)數(shù)據(jù)后,我們?cè)賹?shí)現(xiàn)一個(gè)UI界面來(lái)展示這些數(shù)據(jù),并實(shí)現(xiàn)耗時(shí)篩選、線程篩選、方法名搜索等功能,這樣我們就可以快速的找到主線程高耗時(shí)的方法進(jìn)行針對(duì)性的優(yōu)化了。
1. 效果預(yù)覽
我們先來(lái)看下最終實(shí)現(xiàn)的效果預(yù)覽:
輸出所有的方法耗時(shí),高耗時(shí)方法以紅色預(yù)警,同時(shí)支持對(duì)耗時(shí)篩選,線程篩選,方法名搜索等,比如想篩出主線程耗時(shí)大于50ms的方法,就可以很方便的找出。
?
詳細(xì)的集成以及使用文檔詳見:MethodTraceMan
2. 技術(shù)選型
????插樁技術(shù)其實(shí)充斥在我們平常開發(fā)中的方方面面,可以幫助我們實(shí)現(xiàn)很多繁瑣復(fù)雜的功能,還可以幫助我們提高功能的穩(wěn)定性,比如ButterKnife、Protocol Buffers等都會(huì)在編譯時(shí)期生成代碼,當(dāng)然插樁技術(shù)也分很多種,比如ButterKnife是利用APT在編譯的開始階段對(duì)java文件進(jìn)行操作,而像AscpectJ、ASM等則是在java文件編譯為字節(jié)碼文件后,對(duì)字節(jié)碼進(jìn)行操作,當(dāng)然還有一些可以在字節(jié)碼文件被編譯為dex文件后對(duì)dex進(jìn)行操作的框架。
由于我們的需求是在編譯期對(duì)所有的方法的進(jìn)入和結(jié)束的地方插樁進(jìn)行耗時(shí)統(tǒng)計(jì),所以最終的技術(shù)選型鎖定在對(duì)字節(jié)碼文件的操作。那么我們來(lái)對(duì)比一下AspectJ和ASM兩種字節(jié)碼插樁的框架:
一. AspectJ
????AspectJ是老牌的字節(jié)碼處理框架了,其優(yōu)點(diǎn)就是使用簡(jiǎn)單上手容易,不需要了解字節(jié)碼相關(guān)知識(shí)也可以在項(xiàng)目中集成使用,只要指定簡(jiǎn)單的規(guī)則就可以完成對(duì)代碼的插樁,比如我們現(xiàn)在要實(shí)現(xiàn)對(duì)所有方法的進(jìn)入和退出時(shí)進(jìn)行插樁,十分簡(jiǎn)單,如下:
@Before("execution(* **(..))")
public void beforeMethod(JoinPoint joinPoint) {
//TODO 耗時(shí)統(tǒng)計(jì)
}
@After("execution(* **(..))")
public void afterMethod() {
//TODO 耗時(shí)統(tǒng)計(jì)
}
當(dāng)然相對(duì)于優(yōu)點(diǎn)來(lái)說(shuō),AspectJ的缺點(diǎn)是,由于其基于規(guī)則,所以其切入點(diǎn)相對(duì)固定,對(duì)于字節(jié)碼文件的操作自由度以及開發(fā)的掌控度就大打折扣。還有就是我們要實(shí)現(xiàn)的是對(duì)所有方法進(jìn)行插樁,所以代碼注入后的性能也是我們需要關(guān)注的一個(gè)重要的點(diǎn),我們希望只插入我們想插入的代碼,而AspectJ會(huì)額外生成一些包裝代碼,對(duì)性能以及包大小有一定影響。
二. ASM
????ASM是一個(gè)十分強(qiáng)大的字節(jié)碼處理框架,基本上可以實(shí)現(xiàn)任何對(duì)字節(jié)碼的操作,也就是自由度和開發(fā)的掌控度很高,但是其相對(duì)來(lái)說(shuō)比AspectJ上手難度要高,需要對(duì)Java字節(jié)碼有一定了解,不過(guò)ASM為我們提供了訪問(wèn)者模式來(lái)訪問(wèn)字節(jié)碼文件,這種模式下可以比較簡(jiǎn)單的做一些字節(jié)碼操作,實(shí)現(xiàn)一些功能。同時(shí)ASM可以精確的只注入我們想要注入的代碼,不會(huì)額外生成一些包裝代碼,所以性能上影響比較微小。
上面說(shuō)了很多,對(duì)于java字節(jié)碼,這里做一些簡(jiǎn)單的介紹:
java字節(jié)碼
我們都知道在java文件的通過(guò)javac編譯后會(huì)生成十六進(jìn)制的class文件,比如我們先編寫一個(gè)簡(jiǎn)單的Test.java文件:
public class Test {
private int m = 1;
public int add() {
int j = 2;
int k = m + j;
return k;
}
}
然后我們通過(guò) javac Test.java -g
來(lái)編譯為Test.class,用文本編輯器打開如下:
可以看到是一堆十六進(jìn)制數(shù),但是其實(shí)這一堆十六進(jìn)制數(shù)是按嚴(yán)格的結(jié)構(gòu)拼接在一起的,按順序分別是:魔數(shù)(cafe babe)、java版本號(hào)、常量池、訪問(wèn)權(quán)限標(biāo)志、當(dāng)前類索引、父類索引、接口索引、字段表、方法表、附加屬性等十個(gè)部分,這些部分以十六進(jìn)制的形式表達(dá)出來(lái)并緊湊的拼接在一起,就是上面看到的class字節(jié)碼文件。
當(dāng)然上面的十六進(jìn)制文件顯然不具備可閱讀性,所以我們可以通過(guò) javap -verbose Test
來(lái)反編譯,有興趣的可以自己試一試,就可以看到上面說(shuō)的十個(gè)部分,由于我們做字節(jié)碼插樁一般和方法表關(guān)聯(lián)比較大,所以我們下面著重看一下方法表
,下面是反編譯后的add()方法:
可以看到包括三部分:
-
Code
: 這里部分就是方法里的JVM指令操作碼,也是最重要的一部分,因?yàn)槲覀兎椒ɡ锏倪壿媽?shí)際上就是一條一條的指令操作碼來(lái)完成的。這里可以看到我們的add方法是通過(guò)9條指令操作碼完成的。當(dāng)然插樁重點(diǎn)操作的也是這一塊,只要能修改指令,也就能操控任何代碼了。 -
LineNumberTable
: 這個(gè)是表示行號(hào)表。是我們的java源碼與指令行的行號(hào)對(duì)應(yīng)。比如我們上面的add方法java源碼里總共有三行,也就是上圖中的line10、line11、line12,這三行對(duì)應(yīng)的JVM指令行數(shù)。有了這樣的對(duì)應(yīng)關(guān)系后,就可以實(shí)現(xiàn)比如Debug調(diào)試的功能,指令執(zhí)行的時(shí)候,我們就可以定位到該指令對(duì)應(yīng)的源碼所在的位置。 -
LocalVariableTable
:本地變量表,主要包括This和方法里的局部變量。從上圖可以看到add方法里有this、j、k三個(gè)局部變量。
由于JVM指令集是基于棧的,上面我們已經(jīng)了解到了add方法的邏輯編譯為class文件后變成了9個(gè)指令操作碼,下面我們簡(jiǎn)單看看這些指令操作碼是如何配合操作數(shù)棧+本地變量表+常量池
來(lái)執(zhí)行add方法的邏輯的:
按順序執(zhí)行9條指令操作碼:
- 0:把數(shù)字2入棧
- 1:將2賦值給本地變量表中的j
- 2、3:獲取常量池中的m入棧
- 6:將本地變量表中的j入棧
- 7、8:將m和j相加,然后賦值給本地變量表中的k
- 9、10:將本地變量表中的k入棧,并return
好的,關(guān)于java字節(jié)碼的暫時(shí)就簡(jiǎn)單介紹這些,主要是讓我們基本了解字節(jié)碼文件的結(jié)構(gòu),以及編譯后代碼時(shí)如何運(yùn)行的。而ASM可以通過(guò)操作指令碼來(lái)生成字節(jié)碼或者插樁,當(dāng)你可以利用ASM來(lái)接觸到字節(jié)碼,并且可以利用ASM的api來(lái)操控字節(jié)碼時(shí),就有很大的自由度來(lái)進(jìn)行各種字節(jié)碼的生成、修改、操作等等,也就能產(chǎn)生很強(qiáng)大的功能。
三、Gradle plugin + Transform
????上面對(duì)于插樁框架的選擇,我們通過(guò)對(duì)比最終選擇了ASM,但是ASM只負(fù)責(zé)操作字節(jié)碼,我們還需要通過(guò)自定義gradle plugin的形式來(lái)干預(yù)編譯過(guò)程,在編譯過(guò)程中獲取到所有的class文件和jar包,然后遍歷他們,利用ASM來(lái)修改字節(jié)碼,達(dá)到插樁的目的。
????那么干預(yù)編譯的過(guò)程,我們的第一個(gè)念頭可能就是,對(duì)class轉(zhuǎn)為dex的任務(wù)進(jìn)行hook
,在class轉(zhuǎn)為dex之前拿到所有的class文件,然后利用ASM對(duì)這些字節(jié)碼文件進(jìn)行插樁,然后再把處理過(guò)的字節(jié)碼文件作為transformClassesWithDex
任務(wù)的輸入即可。這種方案的好處是易于控制,我們明確的知道操作的字節(jié)碼文件是最終的字節(jié)碼,因?yàn)槲覀兪窃?code>transformClassesWithDex任務(wù)的前一刻拿到字節(jié)碼文件的。缺點(diǎn)就是,如果項(xiàng)目開啟了混淆,那么在transformClassesWithDex
任務(wù)的前一刻拿到的字節(jié)碼文件顯然是經(jīng)過(guò)了混淆了的,所以利用ASM操作字節(jié)碼的時(shí)候還需要mapping文件進(jìn)行配合才能找到正確的插樁點(diǎn),這一點(diǎn)比較麻煩。
????幸虧gradle還為我們提供了另一種干預(yù)編譯轉(zhuǎn)換過(guò)程的方法:Transform
.其實(shí)我們稍微翻一下gradle編譯過(guò)程的源碼,就會(huì)發(fā)現(xiàn)一些我們熟知的功能都是通過(guò)Transform來(lái)實(shí)現(xiàn)的。還有一點(diǎn),就是關(guān)于混淆的問(wèn)題,上面我們說(shuō)了如果通過(guò)hook transformClassesWithDex
任務(wù)的方式來(lái)實(shí)現(xiàn)插樁,開啟混淆的情況下會(huì)出現(xiàn)問(wèn)題,那么利用Transform的方式會(huì)不會(huì)有混淆的問(wèn)題呢?下面我們從gradle源碼上面找一下答案:
我們從com.android.build.gradle.internal.TaskManager類里的createCompileTask()
方法看起,顯然這是一個(gè)創(chuàng)建編譯任務(wù)的方法:
protected void createCompileTask(@NonNull VariantScope variantScope) {
//創(chuàng)建一個(gè)將java文件編譯為class文件的任務(wù)
JavaCompile javacTask = createJavacTask(variantScope);
addJavacClassesStream(variantScope);
setJavaCompilerTask(javacTask, variantScope);
//創(chuàng)建一些在編譯為class文件后執(zhí)行的額外任務(wù),比如一些Transform等
createPostCompilationTasks(variantScope);
}
接下來(lái)我們看看createPostCompilationTasks()
方法,這個(gè)方法比較長(zhǎng),下面只保留重要的幾個(gè)代碼:
public void createPostCompilationTasks(@NonNull final VariantScope variantScope) {
、、、、、、
TransformManager transformManager = variantScope.getTransformManager();
、、、、、
// ----- External Transforms 這個(gè)就是我們自定義注冊(cè)進(jìn)來(lái)的Transform-----
// apply all the external transforms.
List<Transform> customTransforms = extension.getTransforms();
List<List<Object>> customTransformsDependencies = extension.getTransformsDependencies();
、、、、、、
、、、、、、
// ----- Minify next 這個(gè)就是混淆代碼的Transform-----
CodeShrinker shrinker = maybeCreateJavaCodeShrinkerTransform(variantScope);
、、、、、、
、、、、、、
}
????其實(shí)這個(gè)方法里有很多其他Transform,這里都省略了,我們重點(diǎn)只看我們自定義注冊(cè)的Transform和混淆代碼的Transform,從上面的代碼上我們自定義的Transform是在混淆Transform之前添加進(jìn)TransformManager,所以執(zhí)行的時(shí)候我們自定義的Transform也會(huì)在混淆之前執(zhí)行的,也就是說(shuō)我們利用自定義Transform的方式對(duì)代碼進(jìn)行插樁是不受混淆影響的
。
所以我們最終確定的方案就是 Gradle plugin + Transform +ASM
的技術(shù)方案。下面我們正式說(shuō)說(shuō)利用該技術(shù)方案進(jìn)行具體實(shí)現(xiàn)。
3. 具體實(shí)現(xiàn)
這里具體實(shí)現(xiàn)只挑重點(diǎn)實(shí)現(xiàn)步驟講,詳細(xì)的可以看具體源碼,文章結(jié)尾提供了項(xiàng)目的github地址。
一、自定義gradle plugin
關(guān)于如何創(chuàng)建一個(gè)自定義gradle plugin的項(xiàng)目,這邊就不細(xì)說(shuō)了,可以網(wǎng)上搜索,或者直接看MethodTraceMan項(xiàng)目的源碼也行,自定義gradle plgin繼承自Plugin類,入口是apply方法,我們的apply方法里很簡(jiǎn)單,就是創(chuàng)建一個(gè)自定義擴(kuò)展配置,然后就是注冊(cè)一下我們自定義的Transform:
@Override
void apply(Project project) {
println '*****************MethodTraceMan Plugin apply*********************'
project.extensions.create("traceMan", TraceManConfig)
def android = project.extensions.getByType(AppExtension)
android.registerTransform(new TraceManTransform(project))
}
二、自定義Transform實(shí)現(xiàn)
這里我們創(chuàng)建了一個(gè)名叫traceMan
的擴(kuò)展,這樣我們可以再使用這個(gè)plugin的時(shí)候進(jìn)行一些配置,比如配置插樁的范圍,配置是否開啟插樁等,這樣我們就可以根據(jù)自己的需要來(lái)配置。
接下來(lái)我們看一下TraceManTransform
的實(shí)現(xiàn):
public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
println '[MethodTraceMan]: transform()'
def traceManConfig = project.traceMan
String output = traceManConfig.output
if (output == null || output.isEmpty()) {
traceManConfig.output = project.getBuildDir().getAbsolutePath() + File.separator + "traceman_output"
}
if (traceManConfig.open) {
//讀取配置
Config traceConfig = initConfig()
traceConfig.parseTraceConfigFile()
Collection<TransformInput> inputs = transformInvocation.inputs
TransformOutputProvider outputProvider = transformInvocation.outputProvider
if (outputProvider != null) {
outputProvider.deleteAll()
}
//遍歷,分為class文件變量和jar包的遍歷
inputs.each { TransformInput input ->
input.directoryInputs.each { DirectoryInput directoryInput ->
traceSrcFiles(directoryInput, outputProvider, traceConfig)
}
input.jarInputs.each { JarInput jarInput ->
traceJarFiles(jarInput, outputProvider, traceConfig)
}
}
}
}
三、利用ASM進(jìn)行插樁
接下來(lái)看看遍歷class文件后如何利用ASM的訪問(wèn)者模式進(jìn)行插樁:
static void traceSrcFiles(DirectoryInput directoryInput, TransformOutputProvider outputProvider, Config traceConfig) {
if (directoryInput.file.isDirectory()) {
directoryInput.file.eachFileRecurse { File file ->
def name = file.name
//根據(jù)配置的插樁范圍決定要對(duì)某個(gè)class文件進(jìn)行處理
if (traceConfig.isNeedTraceClass(name)) {
//利用ASM的api對(duì)class文件進(jìn)行訪問(wèn)
ClassReader classReader = new ClassReader(file.bytes)
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
ClassVisitor cv = new TraceClassVisitor(Opcodes.ASM5, classWriter, traceConfig)
classReader.accept(cv, EXPAND_FRAMES)
byte[] code = classWriter.toByteArray()
FileOutputStream fos = new FileOutputStream(
file.parentFile.absolutePath + File.separator + name)
fos.write(code)
fos.close()
}
}
}
//處理完輸出給下一任務(wù)作為輸入
def dest = outputProvider.getContentLocation(directoryInput.name,
directoryInput.contentTypes, directoryInput.scopes,
Format.DIRECTORY)
FileUtils.copyDirectory(directoryInput.file, dest)
}
可以看到,最終是TraceClassVisitor類里對(duì)class文件進(jìn)行處理的,我們看一下TraceClassVisitor
:
class TraceClassVisitor(api: Int, cv: ClassVisitor?, var traceConfig: Config) : ClassVisitor(api, cv) {
private var className: String? = null
private var isABSClass = false
private var isBeatClass = false
private var isConfigTraceClass = false
override fun visit(
version: Int,
access: Int,
name: String?,
signature: String?,
superName: String?,
interfaces: Array<out String>?
) {
super.visit(version, access, name, signature, superName, interfaces)
this.className = name
//抽象方法或者接口
if (access and Opcodes.ACC_ABSTRACT > 0 || access and Opcodes.ACC_INTERFACE > 0) {
this.isABSClass = true
}
//插樁代碼所屬類
val resultClassName = name?.replace(".", "/")
if (resultClassName == traceConfig.mBeatClass) {
this.isBeatClass = true
}
//是否是配置的需要插樁的類
name?.let { className ->
isConfigTraceClass = traceConfig.isConfigTraceClass(className)
}
}
override fun visitMethod(
access: Int,
name: String?,
desc: String?,
signature: String?,
exceptions: Array<out String>?
): MethodVisitor {
val isConstructor = MethodFilter.isConstructor(name)
//抽象方法、構(gòu)造方法、不是插樁范圍內(nèi)的方法,則不進(jìn)行插樁
return if (isABSClass || isBeatClass || !isConfigTraceClass || isConstructor) {
super.visitMethod(access, name, desc, signature, exceptions)
} else {
//TraceMethodVisitor中對(duì)方法進(jìn)行插樁
val mv = cv.visitMethod(access, name, desc, signature, exceptions)
TraceMethodVisitor(api, mv, access, name, desc, className, traceConfig)
}
}
}
再來(lái)看看TraceMethodVisitor
:
override fun onMethodEnter() {
super.onMethodEnter()
//利用ASM在方法進(jìn)入的時(shí)候 通過(guò)插入指令調(diào)用耗時(shí)統(tǒng)計(jì)的方法:start()
mv.visitLdcInsn(generatorMethodName())
mv.visitMethodInsn(INVOKESTATIC, traceConfig.mBeatClass, "start", "(Ljava/lang/String;)V", false)
}
override fun onMethodExit(opcode: Int) {
//利用ASM在方法進(jìn)入的時(shí)候 通過(guò)插入指令調(diào)用耗時(shí)統(tǒng)計(jì)的方法:end()
mv.visitLdcInsn(generatorMethodName())
mv.visitMethodInsn(INVOKESTATIC, traceConfig.mBeatClass, "end", "(Ljava/lang/String;)V", false)
}
這樣,我們就可以在所有配置的在插樁范圍內(nèi)的方法都在方法進(jìn)入的時(shí)候調(diào)用TraceMan.start()方法,在方法退出的時(shí)候調(diào)用TraceMan.end()方法進(jìn)行耗時(shí)統(tǒng)計(jì)。而TraceMan這個(gè)類也是可配置的,也就是你可以通過(guò)配置決定在方法進(jìn)入和退出的時(shí)候調(diào)用哪個(gè)類的哪個(gè)方法。
至于TraceMan.start()
和TraceMan.end()
是如何實(shí)現(xiàn)對(duì)一個(gè)方法的耗時(shí)統(tǒng)計(jì),如何輸出所有方法的耗時(shí),可以具體看源碼里TraceMan
類的具體實(shí)現(xiàn),這里就不具體展開了。
4. UI界面展示
????通過(guò)上面的方法插樁,以及耗時(shí)數(shù)據(jù)的處理,我們已經(jīng)可以獲取到所有方法的耗時(shí)統(tǒng)計(jì),那么為了這個(gè)工具的易用性,我們?cè)賮?lái)實(shí)現(xiàn)一個(gè)UI展示界面,可以讓方法的耗時(shí)數(shù)據(jù)可以實(shí)時(shí)的展示在瀏覽器上,并且支持耗時(shí)篩選、線程篩選、方法名搜索等功能。
????我們使用React
實(shí)現(xiàn)了一個(gè)UI展示界面,然后在手機(jī)上搭建了一個(gè)服務(wù)器,這樣在瀏覽器上就可以通過(guò)地址訪問(wèn)到這個(gè)UI展示界面,并且通過(guò)socket進(jìn)行數(shù)據(jù)傳輸,我們的插樁代碼產(chǎn)生方法耗時(shí)數(shù)據(jù),然后React實(shí)現(xiàn)的UI界面接收數(shù)據(jù)、消費(fèi)數(shù)據(jù)、展示數(shù)據(jù)。
????UI界面展示這部分的實(shí)現(xiàn)說(shuō)起來(lái)比較瑣碎,這里就不詳細(xì)展開了,感興趣的同學(xué)可以看看源碼。
該項(xiàng)目的源碼和詳細(xì)的集成以及使用方法,我在github上維護(hù)了詳細(xì)的文檔,歡迎提供意見: MethodTraceMan
5. 總結(jié)
????以上就是我們?cè)趦?yōu)化流暢度的過(guò)程中實(shí)現(xiàn)的一個(gè)協(xié)助我們快速解決問(wèn)題的工具,也簡(jiǎn)單分享了相關(guān)的技術(shù)知識(shí),希望對(duì)也為頁(yè)面流暢度苦惱的同學(xué)提供一點(diǎn)點(diǎn)想法。之后將分享其他的幾個(gè)部分,主要包括:Android View繪制原理
、幀率流暢度監(jiān)控
、幀率自動(dòng)化測(cè)試
、流暢度優(yōu)化實(shí)用技巧
等等。當(dāng)然對(duì)于卡頓以及流暢度的監(jiān)控及優(yōu)化還有很多需要做的工作,我們的主要目標(biāo)是希望從監(jiān)控到排查問(wèn)題工具再到卡頓解決形成一個(gè)閉環(huán)的方案,讓版本迭代間的流暢度問(wèn)題做到可控、可發(fā)現(xiàn)、易解決,這是我們努力的方向。