前言
做項(xiàng)目?jī)?yōu)化時(shí),我們通常會(huì)先打印出方法的執(zhí)行時(shí)間,再根據(jù)方法的耗時(shí)情況對(duì)其進(jìn)行優(yōu)化。代碼如下:
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
//...
long endTime = System.currentTimeMillis();
System.out.println("程序運(yùn)行時(shí)間: " + (endTime - startTime) + "ms");
}
如果是一兩個(gè)方法我們手動(dòng)插入代碼沒(méi)有問(wèn)題,但是整個(gè)項(xiàng)目的方法何其多,都要我們手動(dòng)去插入的話,估計(jì)能把C、V兩鍵扣廢掉。那么有沒(méi)有一種優(yōu)雅的方式實(shí)現(xiàn)耗時(shí)打印呢?當(dāng)然有的,這就是今天要介紹的主角 ASM (字節(jié)碼插樁)。
有同學(xué)到這里可能就會(huì)問(wèn),我不會(huì)寫ASM代碼該怎么辦呢?
悄悄的跟你說(shuō),其實(shí)我也不會(huì)寫ASM代碼。
那這會(huì)影響到我們的開(kāi)發(fā)嗎?
當(dāng)然不會(huì)了,如果有影響就不會(huì)有這篇文章了。
ASM Bytecode Viewer
ASM Bytecode Viewer是一款能 查看字節(jié)碼 和 生成ASM代碼 的插件,是幫助我們學(xué)習(xí)ASM的利器,剩下就是對(duì)ASM的熟悉和使用可以說(shuō)是so easy。
- 在Android Studio中搜索 ASM Bytecode Viewer Support Kotlin 找到并安裝。
- 代碼右鍵 ASM Bytecode Viewer 便能自動(dòng)生成ASM插樁代碼。
ASM 和 ASM Bytecode Viewer 我在之前的文章 最通俗易懂的字節(jié)碼插樁實(shí)戰(zhàn)(Gradle + ASM)—— 自動(dòng)埋點(diǎn) 已經(jīng)介紹過(guò)了,有不了解的同學(xué)可以翻看一下。具體使用方法我會(huì)在后面的編碼階段詳細(xì)介紹。
實(shí)戰(zhàn)
至此我們已經(jīng)做了大量的準(zhǔn)備工作,現(xiàn)在就正式進(jìn)入實(shí)戰(zhàn)環(huán)節(jié)。
首先創(chuàng)建一個(gè)module作為插件開(kāi)發(fā),再刪除掉多余的文件,然后創(chuàng)建groovy目錄供代碼編寫……
PS:由于gradle插件開(kāi)發(fā)并不是我們今天的任務(wù),這里就不過(guò)多的展開(kāi)說(shuō)明了,具體代碼可在 github 上查看,module目錄結(jié)構(gòu)如下:
1、StatisticPlugin
我們本次編寫的插件,在apply 方法的注冊(cè) MethodTimerTransform,并讀取 build.gradle 里面配置信息。
class StatisticPlugin implements Plugin<Project> {
public static List<MethodTimerEntity> METHOD_TIMER_LIST
@Override
void apply(Project project) {
def android = project.extensions.findByType(AppExtension)
// 注冊(cè)Transform
android.registerTransform(new MethodTimerTransform())
// 獲取gradle里面配置的埋點(diǎn)信息
def statisticExtension = project.extensions.create('statistic', StatisticExtension)
project.afterEvaluate {
// 獲取方法計(jì)時(shí)信息,將其保存在METHOD_TIMER_LIST方便調(diào)用
METHOD_TIMER_LIST = new ArrayList<>()
def methodTimer = statisticExtension.getMethodTimer()
if (methodTimer != null) {
methodTimer.each { Map<String, Object> map ->
MethodTimerEntity entity = new MethodTimerEntity()
if (map.containsKey("time")) {
entity.time = map.get("time")
}
if (map.containsKey("owner")) {
entity.owner = map.get("owner")
}
METHOD_TIMER_LIST.add(entity)
}
}
}
}
}
2、MethodTimerTransform
通過(guò)transform 方法的 Collection<TransformInput> inputs 對(duì) .class文件遍歷拿到所有方法。
class MethodTimerTransform extends Transform {
...省略中間非關(guān)鍵代碼,詳細(xì)請(qǐng)到github中查看...
/**
*
* @param context
* @param inputs 有兩種類型,一種是目錄,一種是 jar 包,要分開(kāi)遍歷
* @param outputProvider 輸出路徑
*/
@Override
void transform(
@NonNull Context context,
@NonNull Collection<TransformInput> inputs,
@NonNull Collection<TransformInput> referencedInputs,
@Nullable TransformOutputProvider outputProvider,
boolean isIncremental
) throws IOException, TransformException, InterruptedException {
if (!incremental) {
//不是增量更新刪除所有的outputProvider
outputProvider.deleteAll()
}
inputs.each { TransformInput input ->
//遍歷目錄
input.directoryInputs.each { DirectoryInput directoryInput ->
handleDirectoryInput(directoryInput, outputProvider)
}
// 遍歷jar 第三方引入的 class
input.jarInputs.each { JarInput jarInput ->
handleJarInput(jarInput, outputProvider)
}
}
}
}
3、MethodTimerClassVisitor
通過(guò)visitMethod拿到方法進(jìn)行修改。
class MethodTimerClassVisitor extends ClassVisitor {
...省略中間非關(guān)鍵代碼,詳細(xì)請(qǐng)到github中查看...
/**
* 掃描類的方法進(jìn)行調(diào)用
* @param access 修飾符
* @param name 方法名字
* @param descriptor 方法簽名
* @param signature 泛型信息
* @param exceptions 拋出的異常
* @return
*/
@Override
MethodVisitor visitMethod(int methodAccess, String methodName, String methodDescriptor, String signature, String[] exceptions) {
MethodVisitor methodVisitor = super.visitMethod(methodAccess, methodName, methodDescriptor, signature, exceptions)
if ((methodAccess & Opcodes.ACC_INTERFACE) == 0 && "<init>" != methodName && "<clinit>" != methodName) {
methodVisitor = new MethodTimerAdviceAdapter(api, methodVisitor, methodAccess, methodName, methodDescriptor)
}
return methodVisitor
}
}
4、MethodTimerAdviceAdapter
這里就是我們插入打印方法耗時(shí)的地方了,可以看到代碼沒(méi)有很多。
-
onMethodEnter在方法進(jìn)入時(shí)調(diào)用,我們先在這里插入一個(gè)時(shí)間戳,標(biāo)記方法開(kāi)始的時(shí)間。 -
onMethodExit在方法退出前調(diào)用,這里我們也插入一個(gè)時(shí)間戳,標(biāo)記方法結(jié)束的時(shí)間。最后把兩個(gè)時(shí)間戳相減得到方法耗時(shí)時(shí)間并打印。
聽(tīng)完解釋后是不是覺(jué)得非常簡(jiǎn)單呢。
大家最關(guān)心的編(sheng)寫(cheng)ASM代碼,今天它來(lái)了。
- 首先我們創(chuàng)建一個(gè)Test類,先用java代碼來(lái)實(shí)現(xiàn)我們的需求,代碼如下:
public class Test {
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
String str = "--- I'm the code line ---";
long endTime = System.currentTimeMillis();
long time = endTime - startTime;
if(time > 500){
System.out.println("程序運(yùn)行時(shí)間: " + time + "ms");
}
}
}
細(xì)心的同學(xué)會(huì)發(fā)現(xiàn)代碼中有一段分割線字符串 String str = "--- I'm the code line ---";
前面說(shuō)過(guò)方法進(jìn)入時(shí)和方法退出前分別是 onMethodEnter 和 onMethodExit,因此我們通過(guò)分割線字符串來(lái)判斷代碼插入的時(shí)機(jī)。
分割線字符之前的代碼在 onMethodEnter 插入,分割線字符之后的代碼在onMethodExit插入。
- 代碼右鍵 ASM Bytecode Viewer 自動(dòng)生成ASM插樁代碼,生成代碼如下:
{
methodVisitor = classWriter.visitMethod(ACC_PUBLIC | ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);
methodVisitor.visitParameter("args", 0);
methodVisitor.visitCode();
methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
methodVisitor.visitVarInsn(LSTORE, 1);
methodVisitor.visitLdcInsn("--- I'm the code line ---");
methodVisitor.visitVarInsn(ASTORE, 3);
methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
methodVisitor.visitVarInsn(LSTORE, 4);
methodVisitor.visitVarInsn(LLOAD, 4);
methodVisitor.visitVarInsn(LLOAD, 1);
methodVisitor.visitInsn(LSUB);
methodVisitor.visitVarInsn(LSTORE, 6);
methodVisitor.visitVarInsn(LLOAD, 6);
methodVisitor.visitLdcInsn(new Long(500L));
methodVisitor.visitInsn(LCMP);
Label label0 = new Label();
methodVisitor.visitJumpInsn(IFLE, label0);
methodVisitor.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
methodVisitor.visitTypeInsn(NEW, "java/lang/StringBuilder");
methodVisitor.visitInsn(DUP);
methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
methodVisitor.visitLdcInsn("\u7a0b\u5e8f\u8fd0\u884c\u65f6\u95f4\uff1a ");
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
methodVisitor.visitVarInsn(LLOAD, 6);
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
methodVisitor.visitLdcInsn("ms");
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
methodVisitor.visitLabel(label0);
methodVisitor.visitInsn(RETURN);
methodVisitor.visitMaxs(4, 8);
methodVisitor.visitEnd();
}
我們把 methodVisitor.visitCode(); 之后 methodVisitor.visitLdcInsn("--- I'm the code line ---"); 之前的代碼插入到 onMethodEnter 。把 methodVisitor.visitLdcInsn("--- I'm the code line ---"); 之后 methodVisitor.visitInsn(RETURN); 之前的代碼插入到 onMethodExit 。
最終的 MethodTimerAdviceAdapter 代碼如下:
class MethodTimerAdviceAdapter extends AdviceAdapter {
int slotIndex
...省略中間非關(guān)鍵代碼,詳細(xì)請(qǐng)到github中查看...
@Override
protected void onMethodEnter() {
super.onMethodEnter()
for (MethodTimerEntity entity : StatisticPlugin.METHOD_TIMER_LIST) {
if (methodOwner.contains(entity.getOwner())) {
slotIndex = newLocal(Type.LONG_TYPE)
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false)
mv.visitVarInsn(LSTORE, slotIndex)
}
}
}
@Override
void onMethodExit(int opcode) {
for (MethodTimerEntity entity : StatisticPlugin.METHOD_TIMER_LIST) {
if (methodOwner.contains(entity.getOwner())) {
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false)
mv.visitVarInsn(LLOAD, slotIndex)
mv.visitInsn(LSUB)
mv.visitVarInsn(LSTORE, slotIndex)
mv.visitVarInsn(LLOAD, slotIndex)
mv.visitLdcInsn(new Long(entity.getTime()))
mv.visitInsn(LCMP)
Label label0 = new Label()
mv.visitJumpInsn(IFLE, label0)
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;")
mv.visitTypeInsn(NEW, "java/lang/StringBuilder")
mv.visitInsn(DUP)
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false)
mv.visitLdcInsn(methodOwner + "/" + methodName + " --> execution time : (")
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false)
mv.visitVarInsn(LLOAD, slotIndex)
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false)
mv.visitLdcInsn("ms)")
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false)
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false)
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false)
mv.visitLabel(label0)
}
}
super.onMethodExit(opcode)
}
}
---這里畫個(gè)重點(diǎn)---
局部變量表(Local Variable Table) 是一組變量值存儲(chǔ)空間,用于存放方法參數(shù)和方法內(nèi)定義的局部變量。具體的順序是 this-方法接收的參數(shù)-方法內(nèi)定義的局部變量 。而我們通過(guò) ASM Bytecode Viewer 生成的ASM代碼是1,2,3按順序?qū)懰赖模晕覀兺ㄟ^(guò) newLocal(type) 來(lái)重新獲取壓入的位置 slotIndex 把參數(shù)壓入到局部變量表中。
5、 如何使用?
5.1、 先打包插件到本地倉(cāng)庫(kù)進(jìn)行引用

5.2、 在項(xiàng)目的根build.gradle加入插件的依賴
repositories {
google()
mavenCentral()
jcenter()
maven{
url uri('repos')
}
}
dependencies {
classpath "com.android.tools.build:gradle:$gradle_version"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'com.meituan.android.walle:plugin:1.1.7'
// 使用自定義插件
classpath 'com.example.plugin:statistic:1.0.0'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
5.3、 在app的build.gradle中使用并配置參數(shù)
plugins {
id 'com.android.application'
id 'statistic'
}
statistic {
methodTimer = [
[
// 打印大于time的方法
'time' : 500L,
// 需要打印方法的范圍
'owner': 'com/example/fragment',
],
[
'time' : 5000L,
'owner': 'com/google',
]
]
}
6、 運(yùn)行項(xiàng)目查看輸出日志
2021-07-20 11:31:51.915 12028-12060/com.example.fragment.project.debug I/System.out: com/example/fragment/library/base/http/SimpleHttp$get$2/invokeSuspend --> execution time : (2066ms)
2021-07-20 11:31:52.565 12028-12028/com.example.fragment.project.debug I/System.out: com/example/fragment/library/common/utils/WanHelper/setTreeList --> execution time : (1184ms)
2021-07-20 11:31:52.565 12028-12028/com.example.fragment.project.debug I/System.out: com/example/fragment/project/model/MainViewModel$getTree$1/invokeSuspend --> execution time : (1184ms)
2021-07-20 11:31:53.768 12028-12028/com.example.fragment.project.debug I/System.out: com/example/fragment/library/common/utils/WanHelper/setTreeList --> execution time : (1186ms)
2021-07-20 11:31:53.768 12028-12028/com.example.fragment.project.debug I/System.out: com/example/fragment/module/system/model/SystemViewModel$getTree$1/invokeSuspend --> execution time : (1186ms)
Thanks
以上就是本篇文章的全部?jī)?nèi)容,如有問(wèn)題歡迎指出,我們一起進(jìn)步。
如果喜歡的話希望點(diǎn)個(gè)贊吧,您的鼓勵(lì)是我前進(jìn)的動(dòng)力。
謝謝~~
項(xiàng)目地址
- github: https://github.com/miaowmiaow