Matrix源码分析系列-如何计算App启动耗时(一)

简介: Matrix源码分析系列-如何计算App启动耗时

image.png

什么是启动耗时


分为两个角度:

  • 冷启动:就是点击应用图标到打开应用的冷启动响应时间,且前提是应用从未被创建过进程,
  • 热启动:测量点击应用图标到打开应用的热启动响应时间,被测应用之前已经被打开过,无关闭应用行为,测试时被重新切换到前台

启动耗时影响什么


第一想到的肯定是用户体验,如果你的应用半分钟没有启动起来,那谁还想用呢?所以很多大厂App,虽然一个App承载的业务多的数不胜数,但肯定都有一个特点,一点就开,即开即用。

启动耗时的标准是什么


各类应用的冷启动时间应≤2000毫秒、游戏类应用和影音娱乐类应用冷启动时间≤3000毫秒,各类应用的热启动时间应≤500毫秒、游戏类应用和影音娱乐类应用冷启动时间≤1000毫秒。同样来源于软件绿色联盟应用体验标准3.0——性能标准,请点击链接查看详情。

如何查看启动耗时呢


其实查看启动耗时,官方已经给我提供了很多工具,如TraceView,我们就可以通过它来查看图形执行时间,调用栈等,但是它的缺点也很明显,运行时开销严重,得出的结果并不真实,同样的我们还可以借助android studio terminal,来一个简单的测试,如下: 命令:

adb shell am start -W [packageName]/[AppstartActivity全路径]

image.png

可以看到,我用同一个命令,测试了两次,第一次是应用进程存活时,LaunchState是HOT,TotalTime就是启动耗时,WaitTime是AMS启动Activity的总耗时,包括创建进程 + Application初始化 + Activity初始化到界面显示的过程。第二次的冷启动低于500ms,还算是比较合理的,热启动在115ms,是不是很优秀,其实这个应用不具有代表性,因为是测试Demo,代码比较简单,所以启动很快,对于一些大型引用肯定就不是这样了,虽然这种方式可以很快的拿到指标数据,但有个缺点你会发现,即使我知道了耗时,如果出现耗时不正常的操作,就不知道哪里出现的问题。所以我就想,是不是Matrix能解决这些问题呢,待我们去验证。我们还是不着急去看Matrix的源码,我们先来看下如何通过代码实现一个启动耗时统计,除了以上方法,google还给我们提供了Systrace 命令行工具,可以结合代码插桩一起完成耗时分析,插桩就是在需要监听的方法前后,插入一行代码。 最新消息google 在Android 10 中引入的全新平台级跟踪工具 Perfetto,具体请看developer.android.com/topic/perfo…,具体可以理解为Systrace的升级版本,我们现在不研究这些工具,先来看看,我们如何通过代码插桩的方式来监控应用的启动耗时。插桩也是Matrix实现的核心,所以我们仔细聊聊。

需要监控的函数

既然我们决定使用代码的插桩来实现,那么就需要知道对哪些函数做操作,具体什么函数,这要看App整个启动过程的函数调用顺序,我整理了几个流程图,请看: 大致流程就是这样,并没有特别详细,基本原理给大家搞清楚,然后知道函数的调用顺序就ok,从图中分析出的知识点:

  • 包括SystemServer在内,我们的app也都是zygote进程fork出来的
  • 当别人通过startActivity启动我们的app时,其实是ActivityManagerService通过startProcessLocked告知zygote进程
  • 当app进程被创建后,进程中会创建出ActivityThread,通过源码我们发现ActivityThread中有个java的main函数,main函数调用attach函数,如图

image.png

attach函数通过binder又返回到ActivityManagerService中,再由ActivityManagerService调用attachApplicationLocked,然后再通过binder调到ApplicationThread.bindApplication,ApplicationThread是ActivityThread中的私有类,如图

image.png

  • ApplicationThread 通过handler message通信,最终调用ActivityThread的handleBindApplication函数,然后在该方法中根据拿到的appInfo信息,创建appContext,最后创建Application,调用application的onCreate函数。
  • Activity的创建通过ActivityStackSupervisor.realStartActivityLocked,最终通过binder,在ActivityThread中执行handleLaunchActivity,紧接着attach到对应的上下文中。

从这张图中,我们了解了App的启动过程,其实在Android不同的SDK版本中都有升级,会导致部分代码找不到,但大同小异。我们的目的其实是为了找到插桩的地方,且有一点我们用到的插桩是java字节码,所以有binder通信的地方,我们只能改动java层的代码,所以基本可以敲定,插桩代码就是在我们的App进程中。 简单定义一个计算公式: App的启动耗时 = 第一个Activity创建好的时间 - Application onCreate 时间 当然有的app是启动页+Home主页才算是app启动完成,这里先不纠结这个,我们现在已经可以明确的点,Application onCreate方法和 Activity的相关方法(后面再分析哪个方法更合适)都是我们要插桩的点。那么接下来我们简单说下插桩的几个框架,来看看哪个更加合适。

插桩方案选择哪个?AspectJ、ASM、ReDex

AspectJ 和 ASM 框架是我们最常用的 Java 字节码处理框架。AspectJ是 Java 中流行的 AOP(aspect-oriented programming)编程扩展框架,从使用上来看,作为字节码处理元老,AspectJ 的框架的确有自己的一些优势,但官方建议切换到 ObjectWeb 的 ASM 框架,而ReDex是 Facebook 开源的工具,通过对字节码进行优化,以减小 Android Apk 大小,同时提高 App 启动速度。ReDex 里甚至提供了 Method Tracing 和 Block Tracing 工具,可以在所有方法或者指定方法前面插入一段跟踪代码。我们为什么不用它呢,因为Matrix用的ASM,并且ASM可以实现 100% 场景的 Java 字节码操作,已经满足了我们的需求。那么接下来,我们用ASM来实现一个代码插桩用例。

ASM实现插桩用例


我们的目标是给Android的某个类,做函数插桩,下面我们做一个demo作为本次的用例,带你有序的了解,该如何通过ASM做函数插桩。

1.Demo项目创建


这一步不用多说,直接在Android Studio中,new project 就行,等待项目第一次编译完成

2.gradle插件创建


在项目的根目录中,创建buildSrc文件夹,然后构建一下项目,然后在buildSrc文件夹中创建build.gradle配置文件,如下:

plugins{
    //使用 java groovy 插件
    id 'java'
    id 'groovy'
}
group 'com.julive.sam'
version '0.0.1'
sourceCompatibility = 1.8
repositories{
    //使用阿里云的maven代理
    maven { url 'https://maven.aliyun.com/repository/google' }
    maven { url 'https://maven.aliyun.com/repository/public' }
    maven {
        url 'http://maven.aliyun.com/nexus/content/groups/public/'
    }
    maven {
        url 'http://maven.aliyun.com/nexus/content/repositories/jcenter'
    }
}
def asmVersion = '8.0.1'
dependencies {
  //引入gradle api
    implementation gradleApi()
    implementation localGroovy()
    //引入android studio扩展gradle的相关api
    implementation "com.android.tools.build:gradle:4.1.0"
    //引入apache io
    implementation 'org.apache.directory.studio:org.apache.commons.io:2.4'
    //引入ASM相关api,这是我们插桩的关键,要靠他实现方法插桩
    implementation "org.ow2.asm:asm:$asmVersion"
    implementation "org.ow2.asm:asm-util:$asmVersion"
    implementation "org.ow2.asm:asm-commons:$asmVersion"
}

接下来创建插件代码目录,由于我们使用java写的插件,所以需要选中buildSrc,然后鼠标右键选择new,再选择directory,最后出现的对话框中选择 src/main/java,下图中是因为我的项目已经创建完了,所以只有groovy目录,如果你需要写groovy的实现就创建下图中文件夹路径,创建完这个下一步就是创建插件。

image.png

在java目录中,创建包名com.julive.sam,在该包路径下创建Plugins插件,代码如下:

public class Plugins implements Plugin<Project> {
    @Override
    public void apply(Project target) {
    }
}

然后创建插件的配置resources文件夹,和java文件夹同级,在resources下创建文件夹META-INF/gradle-plugins/,最终在gradle-plugins中创建com.julive.sam.properties,意思是你的包名.properties ,一定要对应好包名,然后在该文件中加入代码

implementation-class=com.julive.sam.Plugins

com.julive.sam.Plugins 你点击后,看能否跳转至 上面创建的Plugins插件中,如果可以直接跳转那就ok了。

3.下一步在App的build.gradle中配置插件


image.png

4.创建gradle的Transform实现


Transform是在.class -> .dex转换期间,用来修改.class文件的一套标准API,所以你现在应该知道了,在transform中我们肯定要调用ASM的实现,来实现.class文件的修改,最终转换为.dex文件。创建Transform的实现如下:

public class TransformTest extends Transform {
    @Override
    public String getName() {
        // 随便起个名字
        return "TransformSam";
    }
    @Override
    public Set<QualifiedContent.ContentType> getInputTypes() {
        //代表处理的 java 的 class 文件
        return TransformManager.CONTENT_CLASS; 
    }
    @Override
    public Set<? super QualifiedContent.Scope> getScopes() {
        //要处理所有的class字节码
        return TransformManager.SCOPE_FULL_PROJECT;
    }
    @Override
    public boolean isIncremental() {
        // 是否增量编译,我们先不考虑,返回false
        return false; 
    }
    @Override
    public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        super.transform(transformInvocation);
        try {
            //待实现
            doTransform(transformInvocation); // hack
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}    

看上面注释是不是就对Transform有了一定的了解呢,那么如何处理.class文件呢?我们来实现doTransform函数,来看如何处理

private void doTransform(TransformInvocation transformInvocation) throws IOException {
        System.out.println("doTransform   =======================================================");
        //inputs中是传过来的输入流,其中有两种格式,一种是jar包格式一种是目录格式。
        Collection<TransformInput> inputs = transformInvocation.getInputs();
        //获取到输出目录,最后将修改的文件复制到输出目录,这一步必须做不然编译会报错
        TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();
        //删除之前的输出
        if (outputProvider != null)
            outputProvider.deleteAll();
        inputs.forEach(transformInput -> {
            //遍历directoryInputs
            transformInput.getDirectoryInputs().forEach(directoryInput -> {
            });
            //jarInputs
            transformInput.getJarInputs().forEach(jarInput -> {
            });
        });
    }

从transformInvocation的api中,我们获取了两个东西,一个是inputs,一个是outputProvider,我们遍历inputs后发现,他有两个api getDirectoryInputs和getJarInputs 这俩是什么东西呢?我描述不太好,我加了日志,来看下日志输出:

image.png

image.png

这下是不是看明白了,其实我对getDirectoryInputs做了一层文件筛选处理

transformInput.getDirectoryInputs().forEach(directoryInput -> {
      ArrayList<File> list = new ArrayList<>();
      getFileList(directoryInput.getFile(), list);
});
  //递归查找该文件夹下所有文件,因为我们修改的是.class 文件,而不关系文件夹
   void getFileList(File file, ArrayList<File> fileList) {
        if (file.isFile()) {
            fileList.add(file);
        } else {
            File[] list = file.listFiles();
            for (File value : list) {
                getFileList(value, fileList);
            }
        }
    }

好,从上面我们看出,已经找到了MainActivity的class文件,那么接下来给MainActivity.class的onCreate函数,插入两行代码,

5.现在开始操作ASM的api


首先要实现ASM的 ClassVisitor 类来操作我们想要操作的类,它可以访问class文件的各个部分,比如方法变量注解等 基本的实现如下:

public class TestClassVisitor extends ClassVisitor{
    private String className;
    private String superName;
    TestClassVisitor(final ClassVisitor classVisitor) {
        super(Opcodes.ASM6, classVisitor);
    }
    /**
     * 这里可以拿到关于.class的所有信息,比如当前类所实现的接口类表等
     *
     * @param version    表示jdk的版本
     * @param access     当前类的修饰符 (这个和ASM 和 java有些差异,比如public 在这里就是ACC_PUBLIC)
     * @param name       当前类名
     * @param signature  泛型信息
     * @param superName  当前类的父类
     * @param interfaces 当前类实现的接口列表
     */
    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        super.visit(version, access, name, signature, superName, interfaces);
        this.className = name;
        this.superName = superName;
    }
    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        //委托函数
        MethodVisitor methodVisitor = cv.visitMethod(access, name, descriptor, signature, exceptions);
        //找到我们需要修改的类,注意这里是/ 斜杠来表示文件的路径,并不是java代码中的.
        if (className.equals("com/julive/samtest/MainActivity")) {
            // 判断方法name是onCreate
            if (name.startsWith("onCreate")) {
                //插桩函数的实现,同样用到ASM提供的对象,下面看具体实现代码
                return new TestMethodVisitor(Opcodes.ASM6, methodVisitor, access, name, descriptor, className, superName);
            }
        }
        return methodVisitor;
    }
}

这里集成AdviceAdapter,其实AdviceAdapter是继承自MethodVisitor,这是不是就跟ClassVisitor一一呼应呢,使用它是因为它比较方便的实现,提供了onMethodEnter,onMethodExit,正好是我们的需求。在onCreate的函数的前后各插入一行代码。但仔细看onMethodEnter的函数实现,你会发现一脸懵逼,不知道是啥玩意。往下看

public class TestMethodVisitor extends AdviceAdapter {
    private String className;
    private String superName;
    protected TestMethodVisitor(int i, MethodVisitor methodVisitor, int i1, String s, String s1,String className,String superName) {
        super(i, methodVisitor, i1, s, s1);
        this.className = className;
        this.superName = superName;
    }
    @Override
    protected void onMethodEnter() {
        super.onMethodEnter();
        mv.visitLdcInsn("TAG");
        mv.visitLdcInsn(className + "---->" + superName);
        mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "i", "(Ljava/lang/String;Ljava/lang/String;)I", false);
        mv.visitInsn(Opcodes.POP);
    }
    @Override
    protected void onMethodExit(int opcode) {
        mv.visitLdcInsn("TAG");
        mv.visitLdcInsn("this is end");
        mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "i", "(Ljava/lang/String;Ljava/lang/String;)I", false);
        mv.visitInsn(Opcodes.POP);
        super.onMethodExit(opcode);
    }
}

在这里推荐一个插件,plugins.jetbrains.com/plugin/1486…,用插件测试代码如下:

public class Test {
    void aa() {
        Log.i("TAG", "this is end");
    }
}

转换ASM代码如下:

public static byte[] dump() throws Exception {
        ClassWriter classWriter = new ClassWriter(0);
        FieldVisitor fieldVisitor;
        MethodVisitor methodVisitor;
        AnnotationVisitor annotationVisitor0;
        classWriter.visit(V1_8, ACC_PUBLIC | ACC_SUPER, "com/julive/samtest/Test", null, "java/lang/Object", null);
        classWriter.visitSource("Test.java", null);
        {
            methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
            methodVisitor.visitCode();
            Label label0 = new Label();
            methodVisitor.visitLabel(label0);
            methodVisitor.visitLineNumber(5, label0);
            methodVisitor.visitVarInsn(ALOAD, 0);
            methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
            methodVisitor.visitInsn(RETURN);
            Label label1 = new Label();
            methodVisitor.visitLabel(label1);
            methodVisitor.visitLocalVariable("this", "Lcom/julive/samtest/Test;", null, label0, label1, 0);
            methodVisitor.visitMaxs(1, 1);
            methodVisitor.visitEnd();
        }
        {
            methodVisitor = classWriter.visitMethod(0, "aa", "()V", null, null);
            methodVisitor.visitCode();
            Label label0 = new Label();
            methodVisitor.visitLabel(label0);
            methodVisitor.visitLineNumber(8, label0);
            methodVisitor.visitLdcInsn("TAG");
            methodVisitor.visitLdcInsn("this is end");
            methodVisitor.visitMethodInsn(INVOKESTATIC, "android/util/Log", "i", "(Ljava/lang/String;Ljava/lang/String;)I", false);
            methodVisitor.visitInsn(POP);
            Label label1 = new Label();
            methodVisitor.visitLabel(label1);
            methodVisitor.visitLineNumber(9, label1);
            methodVisitor.visitInsn(RETURN);
            Label label2 = new Label();
            methodVisitor.visitLabel(label2);
            methodVisitor.visitLocalVariable("this", "Lcom/julive/samtest/Test;", null, label0, label2, 0);
            methodVisitor.visitMaxs(2, 1);
            methodVisitor.visitEnd();
        }
        classWriter.visitEnd();
        return classWriter.toByteArray();
    }

是不是很长,哈哈,这段代码其实是将整个Test类的东西,都通过ASM的方式生成,我们只需要找到对应的日志如下:

methodVisitor.visitLdcInsn("TAG");
   methodVisitor.visitLdcInsn("this is end");
   methodVisitor.visitMethodInsn(INVOKESTATIC, "android/util/Log", "i", "(Ljava/lang/String;Ljava/lang/String;)I", false);
   methodVisitor.visitInsn(POP);

然后将其放入到onMethodExit函数中,就可以了。

目录
相关文章
|
6月前
买东西太多折扣套路,使用SwiftUI搭建一个折扣计算器App帮你计算吧(下)
买东西太多折扣套路,使用SwiftUI搭建一个折扣计算器App帮你计算吧(下)
34 0
|
6月前
|
存储 索引
买东西太多折扣套路,使用SwiftUI搭建一个折扣计算器App帮你计算吧(上)
买东西太多折扣套路,使用SwiftUI搭建一个折扣计算器App帮你计算吧(上)
43 1
|
数据可视化
Matrix源码分析系列-如何计算App启动耗时(二)
Matrix源码分析系列-如何计算App启动耗时
130 0
Matrix源码分析系列-如何计算App启动耗时(二)
|
iOS开发
通过Html启动IOS的APP
通过Html启动IOS的APP
94 0
|
XML Android开发 UED
Android APP启动黑屏及解决方案
相信做过Android的朋友都知道,当一个APP启动时,界面会首先展示一个白屏或者黑屏,然后再进入欢迎页,稍作停留最后进入APP主页。那么这个黑屏或者白屏到底是怎么一回事呢?
564 0
|
机器学习/深度学习
PIE-engine APP教程 ——基于水体指数或监督分类方法的水体频率计算
PIE-engine APP教程 ——基于水体指数或监督分类方法的水体频率计算
202 0
PIE-engine APP教程 ——基于水体指数或监督分类方法的水体频率计算
|
XML 前端开发 定位技术
Android MVVM框架使用(十三)UI更新 (App启动白屏优化、适配Android10.0深色模式)
Android MVVM框架使用(十三)UI更新 (App启动白屏优化、适配Android10.0深色模式)
355 0
Android MVVM框架使用(十三)UI更新 (App启动白屏优化、适配Android10.0深色模式)
|
存储 Android开发 UED
Android 音乐APP(二)启动白屏优化、定位当前播放歌曲
Android 音乐APP(二)启动白屏优化、定位当前播放歌曲
238 0
Android 音乐APP(二)启动白屏优化、定位当前播放歌曲
|
消息中间件 ARouter 安全
「性能优化系列」APP启动优化理论与实践(下)
● 启动耗时监测实战:手动打点以及AspectJ方式对比; ● 启动优化实战:有向无环图启动器、IdleHandler启动器以及其他黑科技方案; ● 优化工具介绍。
286 0