What is startup time

Divided into two angles:

  • Cold start: the cold start response time from clicking on the application icon to opening the application, provided that the application has never been created.
  • Hot start: Measure the hot start response time from clicking the application icon to opening the application. The application under test has been opened before, and the application is switched back to the foreground during the test without closing the application

What is the impact of startup time

The first thing that comes to mind is user experience. If your app isn’t up in half a minute, who wants to use it? So a lot of big factory App, although an App carries a lot of business, but certainly have a characteristic, a little open, that is.

What are the criteria for startup time

The cold startup time of various applications should be less than or equal to 2000 ms, the cold startup time of game applications and audiovisual entertainment applications should be less than or equal to 3000 ms, the hot startup time of various applications should be less than or equal to 500 ms, and the cold startup time of game applications and audiovisual entertainment applications should be less than or equal to 1000 ms. Also from Software Green Alliance Application Experience Standard 3.0 – Performance Standards, please click the link for details.

How do I check startup time

In fact, to check the startup time, the official has provided me with many tools, such as TraceView, we can use it to view the graph execution time, call stack and so on. However, its disadvantages are also obvious, such as serious runtime overhead, and the results obtained are not real. Similarly, we can also use Android Studio Terminal. Here’s a simple test:

Adb shell am start -w [packageName]/[AppstartActivity full path]Copy the code

LaunchState is HOT, TotalTime is HOT, WaitTime is the TotalTime for AMS to start the Activity, and WaitTime is the TotalTime for AMS to start the Activity. Includes the process of creating process + Application initialization + Activity initialization to the interface display process. The second cold start is less than 500 ms, is relatively reasonable, warm start at 115 ms, is very good, in fact this application is not representative, because is to test the Demo, the code is simpler, so start soon, for some large reference certainly isn’t true, although this way can quickly to get the index data, But one of the disadvantages you’ll find is that even though I know how long it takes, if there’s something that’s not taking as long, I don’t know what’s wrong. So I wonder if Matrix can solve these problems. Let’s verify it. Instead of looking at the Matrix source code, let’s take a look at how to implement a startup time statistic in code. In addition to the above method, Google also provides a Systrace command line tool, which can be used in combination with code pile-in, which is to insert a line of code before and after the method that needs to be monitored.Google has introduced Perfetto, a new platform-level tracking tool for Android 10For details, please seeDeveloper.android.com/topic/perfo…Instead of looking at these tools, let’s take a look at how we can monitor startup time using code stubs. Piling is also central to the Matrix implementation, so let’s talk more about it.

Functions that need to be monitored

Since we have decided to use code piling to implement, we need to know which functions to operate on and the specific functions, which depends on the order of function calls in the whole startup process of the App. I have sorted out several flow charts, please see: The general process is like this, and there is no special detail, the basic principle for you to make clear, and then know the order of function call ok, from the diagram analysis of knowledge:

  • Our apps, including SystemServer, are also fork out of zygote process
  • When someone starts our app with startActivity, it is ActivityManagerService that notifies Zygote with startProcessLocked
  • When an APP process is created, an ActivityThread will be created in the process. Through the source code, we find that there is a Java main function in the ActivityThread, and the main function calls attach function, as shown in the figure

  • Attach function is returned to attachApplicationLocked by ActivityManagerService. And then through the binder to ApplicationThread bindApplication, ApplicationThread is the private class in ActivityThread, as shown in figure

  • ApplicationThread communicates via the Handler Message, and finally calls the ActivityThread’s handleBindApplication function. In this method, it creates an appContext based on the appInfo information it gets. Finally, create Application and call the onCreate function of Application.
  • Activity created by ActivityStackSupervisor realStartActivityLocked, finally by binder, performs handleLaunchActivity in ActivityThread, Then attach to the corresponding context.

From this picture, we understand the startup process of App. In fact, there are upgrades in different SDK versions of Android, which will cause some code to be missing, but it is similar with little difference. What we want to do is find a place to plug in, and one of the things that we use is Java bytecode, so where binder communicates, we can only change the Code in the Java layer, so it’s pretty much fixed. The plug code is in our App process. Simply define a calculation formula: App startup time = the time it takes for the first Activity to be created – Application onCreate time The Application onCreate method and the Activity’s related methods (which one is more appropriate) are both the points where we want to pile. So let’s briefly talk about several frames for piling, to see which is more suitable.

Which piling scheme to choose? AspectJ, ASM, and ReDex

The AspectJ and ASM frameworks are our most commonly used Java bytecode processing frameworks. AspectJ is a popular AOP (Aspect-oriented Programming) extension framework in Java. As a bytecode processing veteran, AspectJ’s framework does have some advantages in terms of usage. But the official recommendation is to switch to ObjectWeb’s ASM framework, and ReDex is Facebook’s open source tool that optimizes bytecode to reduce the size of Android Apk and speed up App startup. ReDex even offers Method Tracing and Block Tracing tools that insert a trace before all or specified methods. Why don’t we use it, because Matrix uses ASM, and ASM can achieve 100% scenarios of Java bytecode operation, has met our needs. So next, we implement a code piling use case with ASM.

ASM implements the pile-driving use case

Our goal is to do function pile-in for a class in Android. Let’s do a demo as this use case, to show you how to do function pile-in through ASM in order.

1. Create a Demo project

This step goes without saying, just go to Android Studio, new Project, and wait for the project to compile for the first time

2. Create gradle plug-in

In the root directory of the project, create the buildSrc folder and then build the project. Then create the build.gradle configuration file in the buildSrc folder as follows:

plugins{
    // Use the Java Groovy plugin
    id 'java'
    id 'groovy'
}

group 'com.julive.sam'
version '0.0.1'

sourceCompatibility = 1.8

repositories{
    // Use ali Cloud's Maven agent
    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 {
	// Introduce gradle API
    implementation gradleApi(a)
    implementation localGroovy(a)
    // Introduce android Studio extension gradle related APIImplementation "com. Android. Tools. Build: gradle: 4.1.0." "// Import Apache IOImplementation 'org.apache.directory.studio:org.apache.com mons. IO: 2.4'// Introduce the ASM API, this is the key to our pile, it is up to him to implement method pile
    implementation "org.ow2.asm:asm:$asmVersion"
    implementation "org.ow2.asm:asm-util:$asmVersion"
    implementation "org.ow2.asm:asm-commons:$asmVersion"
}
Copy the code

Next, create a directory of plug-in code. Since we are using Java plug-ins, select “buildSrc”, right-click “New”, and select “directory”. In the dialog box that appears, select “SRC /main/ Java”. If you need to write the groovy implementation, create the folder path shown in the following image. After that, the next step is to create the plugin.In the Java directory, create the package name com.julive. Sam and create the Plugins in the package path as follows:

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

And then create the plug-in configuration resources folder, folders and Java, at the same level under the resources to create the folder meta-inf/gradle – plugins /, eventually create com in gradle – plugins. Julive. Sam. The properties, This means your package name.properties, be sure to match the package name, and then add code to that file

implementation-class=com.julive.sam.Plugins
Copy the code

After you click on com.julive.sam.plugins, see if you can jump to the Plugins created above. If you can jump directly, it’s ok.

3. Next, configure the plug-in in build.gradle of the App

Create gradle’s Transform implementation

Transform is a standard API used to modify.class files during the transformation of.class ->.dex, so you should know by now that we must call the ASM implementation to implement the transformation of.class files into.dex files. The implementation of creating a Transform is as follows:

public class TransformTest extends Transform {

    @Override
    public String getName() {
        // Pick a name
        return "TransformSam";
    }

    @Override
    public Set<QualifiedContent.ContentType> getInputTypes() {
    		// represents processing java the class file
        return TransformManager.CONTENT_CLASS; 
    }

    @Override
    public Set<? super QualifiedContent.Scope> getScopes() {
    		// To process all class bytecode
        return TransformManager.SCOPE_FULL_PROJECT;
    }

    @Override
    public boolean isIncremental() {
    		// Whether we compile incrementally or not, we don't care, return false
        return false; 
    }

    @Override
    public void transform(TransformInvocation transformInvocation) throws TransformException.InterruptedException.IOException {
        super.transform(transformInvocation);
        try {
        		/ / implementation
            doTransform(transformInvocation); // hack
        } catch (Exception e) {
            e.printStackTrace();}}}Copy the code

Look at the comment above to see if you have a good idea of Transform. How do you handle.class files? Let’s implement the doTransform function and see what happens

private void doTransform(TransformInvocation transformInvocation) throws IOException {
        System.out.println("doTransform   = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = ");
        Inputs: //inputs: jar package format: directory format:
        Collection<TransformInput> inputs = transformInvocation.getInputs();
        // Obtain the output directory and copy the modified files to the output directory. This step must be done otherwise the compilation will report an error
        TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();

        // Delete the previous output
        if (outputProvider ! = null)
            outputProvider.deleteAll();

        inputs.forEach(transformInput -> {
            / / traverse directoryInputs
            transformInput.getDirectoryInputs().forEach(directoryInput ->{});
            //jarInputs
            transformInput.getJarInputs().forEach(jarInput ->{});
        });
    }
Copy the code

From the API of transformInvocation, we get two things, one is the inputs and the other is the outputProvider, and we go through the inputs and we find, He has two apis getDirectoryInputs and getJarInputs and what are these two? I didn’t describe it very well, so I added the log, and look at the log output:I did a layer of filtering on getDirectoryInputs

transformInput.getDirectoryInputs().forEach(directoryInput -> {
      ArrayList<File> list = new ArrayList<>();
      getFileList(directoryInput.getFile(), list);
});
  // Find all files in this folder recursively, because we are modifying the.class file, not the relational folder
   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); }}}Copy the code

Ok, so we’ve found the class file for MainActivity, so insert two lines of code into the onCreate function for MainActivity. Class.

5. Now start manipulating the ASM API

We first implement the ASM ClassVisitor class to operate on the class we want to operate on. It accesses various parts of the class file such as methods, variables, annotations, and so on. The basic implementation is as follows:

public class TestClassVisitor extends ClassVisitor{

    private String className;
    private String superName;

    TestClassVisitor(final ClassVisitor classVisitor) {
        super(Opcodes.ASM6, classVisitor);
    }

    /** * You can get all the information about.class, such as the list of interface classes implemented by the current class **@paramVersion Indicates the JDK version *@paramAccess's current class modifier (this is different from ASM and Java, for example, public is ACC_PUBLIC in this case) *@paramName Name of the current class *@paramSignature generic information *@paramSuperName Specifies the superclass * of the current class@paramInterfaces List of interfaces implemented by the current class */
    @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) {
        // Delegate function
        MethodVisitor methodVisitor = cv.visitMethod(access, name, descriptor, signature, exceptions);
        // Find the class we need to modify. Note that the/slash is used to indicate the path to the file, not the Java code.
        if (className.equals("com/julive/samtest/MainActivity")) {
            // The name of the method is onCreate
            if (name.startsWith("onCreate")) {
                // Insert the pile function to implement, also use ASM provided objects, see the concrete implementation code
                return newTestMethodVisitor(Opcodes.ASM6, methodVisitor, access, name, descriptor, className, superName); }}returnmethodVisitor; }}Copy the code

So we’re integrating the AdviceAdapter, which is actually inherits from the MethodVisitor, which is kind of echoing the ClassVisitor, because it’s a convenient implementation, which provides onMethodEnter, onMethodExit, Just what we need. Insert one line of code before and after the onCreate function. But if you look closely at the implementation of onMethodEnter, you will find that you have no idea what it is. To look down

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(a) {
        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); }}Copy the code

Here recommend a plug-in, plugins.jetbrains.com/plugin/1486… , test the code with the plugin as follows:

public class Test {

    void aa(a) {
        Log.i("TAG"."this is end"); }}Copy the code

Convert ASM code as follows:

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();
    }
Copy the code

This code is used to generate the entire Test class using ASM. We just need to find the corresponding log as follows:

   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);
Copy the code

And then you put it into the onMethodExit function, and there you go.

6.Tranfrom combined with ASM

Change the class file from Tranform to ASM. Change the class file from doTransform to ASM.

private void doTransform(TransformInvocation transformInvocation) throws IOException {
        System.out.println("doTransform =======================================================");
        Inputs: //inputs: jar package format: directory format:
        Collection<TransformInput> inputs = transformInvocation.getInputs();
        // Obtain the output directory and copy the modified files to the output directory. This step must be done otherwise the compilation will report an error
        TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();

        // Delete the previous output
        if(outputProvider ! =null)
            outputProvider.deleteAll();

        inputs.forEach(transformInput -> {
            / / traverse directoryInputs
            transformInput.getDirectoryInputs().forEach(directoryInput -> {
                ArrayList<File> list = new ArrayList<>();
                getFileList(directoryInput.getFile(), list);
                list.forEach(file -> {
                    System.out.println("getDirectoryInputs =======================================================" + file.getName());
                    // Determine if it is a.class file
                    if (file.isFile() && file.getName().endsWith(".class")) {
                        try {
                            // Object provided by ASM to read class information
                            ClassReader classReader = new ClassReader(new FileInputStream(file));
                            // The class provided by ASM modifies the object and passes the read information to classWriter
                            ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS);
                            // Create the modification rule, TestClassVisitor
                            ClassVisitor visitor = new TestClassVisitor(classWriter);
                            // Will change the rules to classReader
                            classReader.accept(visitor, ClassReader.EXPAND_FRAMES);
                            // Use the toByteArray method to convert the changed information into a byte array
                            byte[] bytes = classWriter.toByteArray();
                            // Write to the original file in the output stream
                            FileOutputStream fileOutputStream = new FileOutputStream(file.getAbsolutePath());
                            fileOutputStream.write(bytes);
                            fileOutputStream.close();
                        } catch(IOException e) { e.printStackTrace(); }}});if(outputProvider ! =null) {
                    File dest = outputProvider.getContentLocation(directoryInput.getName(), directoryInput.getContentTypes(), directoryInput.getScopes(), Format.DIRECTORY);
                    try {
                        // Put the file into the target directory, this step must be implemented, otherwise the dex file will not find the file
                        FileUtils.copyDirectory(directoryInput.getFile(), dest);
                    } catch(IOException e) { e.printStackTrace(); }}});//jarInputs
            transformInput.getJarInputs().forEach(jarInput -> {
                ArrayList<File> list = new ArrayList<>();
                getFileList(jarInput.getFile(), list);
                list.forEach(file -> {
                    System.out.println("getJarInputs =======================================================" + file.getName());
                });
                if(outputProvider ! =null) {
                    File dest = outputProvider.getContentLocation(
                            jarInput.getName(),
                            jarInput.getContentTypes(),
                            jarInput.getScopes(),
                            Format.JAR);
                    // Put the file into the target directory, this step must be implemented, otherwise the dex file will not find the file
                    try {
                        FileUtils.copyFile(jarInput.getFile(), dest);
                    } catch(IOException e) { e.printStackTrace(); }}}); }); }Copy the code

Decompile the check code

Ok, we have inserted the code into the onCreate function of MainActivity through a simple ASM operation. How do we verify that? You can do this either by decompilating it or by logging it. Logs are not very reasonable, because we don’t insert many logs to verify that we have inserted them. There are too many logs to take care of, so let’s decompilate it.

git clone https://github.com/skylot/jadx.git
cd jadx
./gradlew dist
Copy the code

After the command is successfully executed, you can perform the following operations:

jadx-gui
Copy the code

Then the tool will be called, as follows:Then drag the APP debug APK package to this window, as follows:We find MainActivity as follows:And our source code looks like this, and it works exactly as we expected.All right, you have mastered the basic ASM operation, if you need to further study, please go to the website to learn. Next, back to our topic, looking at Matrix startup time, what code to insert?

Matrix Start time statistics pile code

Following the above idea, let’s analyze its code to find the Plugins, as follows:

class MatrixPlugin implements Plugin<Project> {
    private static final String TAG = "Matrix.MatrixPlugin"

    @Override
    void apply(Project project) {
        // Create a new configuration, which is the configuration you used in build.gradle
        project.extensions.create("matrix", MatrixExtension)
        project.matrix.extensions.create("trace", MatrixTraceExtension)
        project.matrix.extensions.create("removeUnusedResources", MatrixDelUnusedResConfiguration)
        // Only applications are supported. If configured in the library, the gradle project will fail to compile
        if(! project.plugins.hasPlugin('com.android.application')) {
            throw new GradleException('Matrix Plugin, Android Application plugin required')}// One of the more common callback methods for configuration parameters will be called if the project is successfully configured
        project.afterEvaluate {
            // Get the Android configuration for the project
            def android = project.extensions.android
            // Get the matrix configuration
            def configuration = project.matrix
            / / ApplicationVariant object
            android.applicationVariants.all { variant ->
				// In the matrix configuration, enable MatrixTraceTransform if the enable attribute in trace is true
                if (configuration.trace.enable) {
                    com.tencent.matrix.trace.transform.MatrixTraceTransform.inject(project, configuration.trace, variant.getVariantData().getScope())
                }
				// If removing unusable resources is true, create related tasks in project's tasks.
                if (configuration.removeUnusedResources.enable) {
                    if (Util.isNullOrNil(configuration.removeUnusedResources.variant) || variant.name.equalsIgnoreCase(configuration.removeUnusedResources.variant)) {
                        Log.i(TAG, "removeUnusedResources %s", configuration.removeUnusedResources)
                        RemoveUnusedResourcesTask removeUnusedResourcesTask = project.tasks.create("remove" + variant.name.capitalize() + "UnusedResources", RemoveUnusedResourcesTask)
                        removeUnusedResourcesTask.inputs.property(RemoveUnusedResourcesTask.BUILD_VARIANT, variant.name)
                        project.tasks.add(removeUnusedResourcesTask)
                        removeUnusedResourcesTask.dependsOn variant.packageApplication
                        variant.assemble.dependsOn removeUnusedResourcesTask
                    }
                }

            }
        }
    }
}
Copy the code

We found the MatrixTraceTransform, and that’s the second step of the pile, so let’s look at the code and get to the point

	@Override
    public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        super.transform(transformInvocation);
        // Record the start time
        long start = System.currentTimeMillis();
        try {
            // Start piling
            doTransform(transformInvocation); // hack
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        long cost = System.currentTimeMillis() - start;
        long begin = System.currentTimeMillis();
        origTransform.transform(transformInvocation);
        long origTransformCost = System.currentTimeMillis() - begin;
        Log.i("Matrix." + getName(), "[transform] cost time: %dms %s:%sms MatrixTraceTransform:%sms", System.currentTimeMillis() - start, origTransform.getClass().getSimpleName(), origTransformCost, cost);
    }
 private void doTransform(TransformInvocation transformInvocation) throws ExecutionException, InterruptedException {
     	// Determine whether to compile incrementally
        final boolean isIncremental = transformInvocation.isIncremental() && this.isIncremental();

        /** * step 1 */
        long start = System.currentTimeMillis();
		//Future represents the result of an asynchronous computation. It provides a way to check if a calculation is complete, wait for it to be completed, and get the result of the calculation.
        // You can only use the get method to retrieve the result after the calculation is complete. If necessary, you can block the method until the calculation is complete.
     	// It can be processed asynchronously and returned synchronously
        List<Future> futures = new LinkedList<>();
     	// Store the mapping between the methods before and after obfuscation
        final MappingCollector mappingCollector = new MappingCollector();
        final AtomicInteger methodId = new AtomicInteger(0);
     	// Store the method name to be staked and the method wrapper TraceMethod
        final ConcurrentHashMap<String, TraceMethod> collectedMethodMap = new ConcurrentHashMap<>();

        futures.add(executor.submit(new ParseMappingTask(mappingCollector, collectedMethodMap, methodId)));
		// Store the mapping between the original source file and the output source file
        Map<File, File> dirInputOutMap = new ConcurrentHashMap<>();
     	// Store the mapping between the original JAR file and the output JAR file
        Map<File, File> jarInputOutMap = new ConcurrentHashMap<>();
        Collection<TransformInput> inputs = transformInvocation.getInputs();
     
        // Let's look at the following ASM project implementation code
        for (TransformInput input : inputs) {

            for (DirectoryInput directoryInput : input.getDirectoryInputs()) {
                / / found a pile of ASM implementation place, see the next CollectDirectoryInputTask source, its final output increment dirInputOutMap
                futures.add(executor.submit(new CollectDirectoryInputTask(dirInputOutMap, directoryInput, isIncremental)));
            }

            for (JarInput inputJar : input.getJarInputs()) {
                / / same as CollectDirectoryInputTask almost
                futures.add(executor.submit(newCollectJarInputTask(inputJar, isIncremental, jarInputOutMap, dirInputOutMap))); }}Future tasks are executed concurrently in executor thread pools.
        for (Future future : futures) {
            future.get();
        }
        futures.clear();
		// Execution is complete
        Log.i(TAG, "[doTransform] Step(1)[Parse]... cost:%sms", System.currentTimeMillis() - start);


        /** * step 2 */
        start = System.currentTimeMillis();
     	// Calculate the dirInputOutMap file to be processed and start piling
        MethodCollector methodCollector = new MethodCollector(executor, mappingCollector, methodId, config, collectedMethodMap);
        methodCollector.collect(dirInputOutMap.keySet(), jarInputOutMap.keySet());
        Log.i(TAG, "[doTransform] Step(2)[Collection]... cost:%sms", System.currentTimeMillis() - start);

        /** * step 3 */
        start = System.currentTimeMillis();
        // The name should be the thread logic associated with the Trace, and our startup time should be here
        MethodTracer methodTracer = new MethodTracer(executor, mappingCollector, config, methodCollector.getCollectedMethodMap(), methodCollector.getCollectedClassExtendMap());
        methodTracer.trace(dirInputOutMap, jarInputOutMap);
        Log.i(TAG, "[doTransform] Step(3)[Trace]... cost:%sms", System.currentTimeMillis() - start);

    }

Copy the code
	// Trace method for MethodTracer
	public void trace(Map<File, File> srcFolderList, Map<File, File> dependencyJarList) throws ExecutionException, InterruptedException {
        List<Future> futures = new LinkedList<>();
        traceMethodFromSrc(srcFolderList, futures);
        traceMethodFromJar(dependencyJarList, futures);
        for (Future future : futures) {
            future.get();
        }
        futures.clear();
    }
	private void traceMethodFromSrc(Map<File, File> srcMap, List<Future> futures) {
        if (null! = srcMap) {for (Map.Entry<File, File> entry : srcMap.entrySet()) {
                futures.add(executor.submit(new Runnable() {
                    @Override
                    public void run(a) {
                        // Insert trace related methods into non-JAR package filesinnerTraceMethodFromSrc(entry.getKey(), entry.getValue()); }})); }}}private void traceMethodFromJar(Map<File, File> dependencyMap, List<Future> futures) {
        if (null! = dependencyMap) {for (Map.Entry<File, File> entry : dependencyMap.entrySet()) {
                futures.add(executor.submit(new Runnable() {
                    @Override
                    public void run(a) {
                        // Insert trace related methods into the JAR packageinnerTraceMethodFromJar(entry.getKey(), entry.getValue()); }})); }}}// Start inserting code
private void innerTraceMethodFromSrc(File input, File output) {
		// Find all files and filter them into folders
        ArrayList<File> classFileList = new ArrayList<>();
        if (input.isDirectory()) {
            listClassFiles(classFileList, input);
        } else {
            classFileList.add(input);
        }
		// Go through all the files and insert the pile
        for (File classFile : classFileList) {
            InputStream is = null;
            FileOutputStream os = null;
            try {
                final String changedFileInputFullPath = classFile.getAbsolutePath();
                final File changedFileOutput = new File(changedFileInputFullPath.replace(input.getAbsolutePath(), output.getAbsolutePath()));
                if(! changedFileOutput.exists()) { changedFileOutput.getParentFile().mkdirs(); } changedFileOutput.createNewFile();// Check whether it is a.class file based on the class name
                if (MethodCollector.isNeedTraceFile(classFile.getName())) {
                    is = new FileInputStream(classFile);
                    ClassReader classReader = new ClassReader(is);
                    ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
                    // Modify the class file according to the TraceClassAdapter rules
                    ClassVisitor classVisitor = new TraceClassAdapter(Opcodes.ASM5, classWriter);
                    classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES);
                    is.close();

                    if (output.isDirectory()) {
                        os = new FileOutputStream(changedFileOutput);
                    } else {
                        os = new FileOutputStream(output);
                    }
                    os.write(classWriter.toByteArray());
                    os.close();
                } else{ FileUtil.copyFileUsingStream(classFile, changedFileOutput); }}catch (Exception e) {
                Log.e(TAG, "[innerTraceMethodFromSrc] input:%s e:%s", input.getName(), e);
                try {
                    Files.copy(input.toPath(), output.toPath(), StandardCopyOption.REPLACE_EXISTING);
                } catch(Exception e1) { e1.printStackTrace(); }}finally {
                try {
                    is.close();
                    os.close();
                } catch (Exception e) {
                    // ignore}}}}Copy the code
private class TraceClassAdapter extends ClassVisitor {

        private String className;
        private boolean isABSClass = false;
        private boolean hasWindowFocusMethod = false;
        private boolean isActivityOrSubClass;
        private boolean isNeedTrace;

        TraceClassAdapter(int i, ClassVisitor classVisitor) {
            super(i, classVisitor);
        }

        @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.isActivityOrSubClass = isActivityOrSubClass(className, collectedClassExtendMap);
            this.isNeedTrace = MethodCollector.isNeedTrace(configuration, className, mappingCollector);
            if ((access & Opcodes.ACC_ABSTRACT) > 0 || (access & Opcodes.ACC_INTERFACE) > 0) {
                this.isABSClass = true; }}@Override
        public MethodVisitor visitMethod(int access, String name, String desc,
                                         String signature, String[] exceptions) {
            if (isABSClass) {
                return super.visitMethod(access, name, desc, signature, exceptions);
            } else {
                if(! hasWindowFocusMethod) {// Check whether the method name is onWindowFocusChanged
                    hasWindowFocusMethod = MethodCollector.isWindowFocusChangeMethod(name, desc);
                }
                MethodVisitor methodVisitor = cv.visitMethod(access, name, desc, signature, exceptions);
                // Methods insert rules
                return new TraceMethodAdapter(api, methodVisitor, access, name, desc, this.className, hasWindowFocusMethod, isActivityOrSubClass, isNeedTrace); }}@Override
        public void visitEnd(a) {
            if(! hasWindowFocusMethod && isActivityOrSubClass && isNeedTrace) { insertWindowFocusChangeMethod(cv, className); }super.visitEnd(); }}// Insert rules for methods
private class TraceMethodAdapter extends AdviceAdapter {

        private final String methodName;
        private final String name;
        private final String className;
        private final boolean hasWindowFocusMethod;
        private final boolean isNeedTrace;
        private final boolean isActivityOrSubClass;

        protected TraceMethodAdapter(int api, MethodVisitor mv, int access, String name, String desc, String className,
                                     boolean hasWindowFocusMethod, boolean isActivityOrSubClass, boolean isNeedTrace) {
            super(api, mv, access, name, desc);
            TraceMethod traceMethod = TraceMethod.create(0, access, className, name, desc);
            this.methodName = traceMethod.getMethodName();
            this.hasWindowFocusMethod = hasWindowFocusMethod;
            this.className = className;
            this.name = name;
            this.isActivityOrSubClass = isActivityOrSubClass;
            this.isNeedTrace = isNeedTrace;

        }

        @Override
        protected void onMethodEnter(a) {
            TraceMethod traceMethod = collectedMethodMap.get(methodName);
            / / method starting position insert com/tencent/matrix/trace/core/AppMethodBeat class I method
            if(traceMethod ! =null) {
                traceMethodCount.incrementAndGet();
                mv.visitLdcInsn(traceMethod.id);
                mv.visitMethodInsn(INVOKESTATIC, TraceBuildConstants.MATRIX_TRACE_CLASS, "i"."(I)V".false); }}@Override
        protected void onMethodExit(int opcode) {
            TraceMethod traceMethod = collectedMethodMap.get(methodName);
            if(traceMethod ! =null) {
                // If the method is onWindowFocusChanged and is an Activity or subclass, and Trace is enabled
                if (hasWindowFocusMethod && isActivityOrSubClass && isNeedTrace) {
                    TraceMethod windowFocusChangeMethod = TraceMethod.create(-1, Opcodes.ACC_PUBLIC, className,
                            TraceBuildConstants.MATRIX_TRACE_ON_WINDOW_FOCUS_METHOD, TraceBuildConstants.MATRIX_TRACE_ON_WINDOW_FOCUS_METHOD_ARGS);
                    if (windowFocusChangeMethod.equals(traceMethod)) {
                        // Insert code into onWindowFocusChanged function
                        traceWindowFocusChangeMethod(mv, className);
                    }
                }
                traceMethodCount.incrementAndGet();
            	/ / method end inserted into the com/tencent/matrix trace/core/AppMethodBeat o method
                mv.visitLdcInsn(traceMethod.id);
                mv.visitMethodInsn(INVOKESTATIC, TraceBuildConstants.MATRIX_TRACE_CLASS, "o"."(I)V".false); }}/ / insert code is com/tencent/matrix/trace/core/AppMethodBeat ats function
     private void traceWindowFocusChangeMethod(MethodVisitor mv, String classname) {
        mv.visitVarInsn(Opcodes.ALOAD, 0);
        mv.visitVarInsn(Opcodes.ILOAD, 1);
        mv.visitMethodInsn(Opcodes.INVOKESTATIC, TraceBuildConstants.MATRIX_TRACE_CLASS, "at"."(Landroid/app/Activity; Z)V".false); }}Copy the code

Found the function of the pile, but I don’t know what did, back to the com/tencent/matrix/trace/core/AppMethodBeat three function to glance at in class

 public static void i(int methodId) {

        if (status <= STATUS_STOPPED) {
            return;
        }
        if (methodId >= METHOD_ID_MAX) {
            return;
        }

        if (status == STATUS_DEFAULT) {
            synchronized (statusLock) {
                if (status == STATUS_DEFAULT) {
                    // This function calculates the time, as shown belowrealExecute(); status = STATUS_READY; }}}long threadId = Thread.currentThread().getId();
        if(sMethodEnterListener ! =null) {
            sMethodEnterListener.enter(methodId, threadId);
        }

        if (threadId == sMainThreadId) {
            if (assertIn) {
                android.util.Log.e(TAG, "ERROR!!! AppMethodBeat.i Recursive calls!!!");
                return;
            }
            assertIn = true;
            if (sIndex < Constants.BUFFER_SIZE) {
                mergeData(methodId, sIndex, true);
            } else {
                sIndex = 0;
                mergeData(methodId, sIndex, true);
            }
            ++sIndex;
            assertIn = false; }}private static void realExecute(a) {
        MatrixLog.i(TAG, "[realExecute] timestamp:%s", System.currentTimeMillis());

        sCurrentDiffTime = SystemClock.uptimeMillis() - sDiffTime;

        sHandler.removeCallbacksAndMessages(null);
        sHandler.postDelayed(sUpdateDiffTimeRunnable, Constants.TIME_UPDATE_CYCLE_MS);
        sHandler.postDelayed(checkStartExpiredRunnable = new Runnable() {
            @Override
            public void run(a) {
                synchronized (statusLock) {
                    MatrixLog.i(TAG, "[startExpired] timestamp:%s status:%s", System.currentTimeMillis(), status);
                    if (status == STATUS_DEFAULT || status == STATUS_READY) {
                        status = STATUS_EXPIRED_START;
                    }
                }
            }
        }, Constants.DEFAULT_RELEASE_BUFFER_DELAY);
		/ / hook android. App. ActivityThread Handler object in mH mCallBack, its assignment for HackCallback
        ActivityThreadHacker.hackSysHandlerCallback();
        // Add Looper monitor
        LooperMonitor.register(looperMonitorListener);
    }
	// Hook the mCallBack property of the Handler object mh in ActivityThread
  	public static void hackSysHandlerCallback(a) {
        try {
            sApplicationCreateBeginTime = SystemClock.uptimeMillis();
            sApplicationCreateBeginMethodIndex = AppMethodBeat.getInstance().maskIndex("ApplicationCreateBeginMethodIndex"); Class<? > forName = Class.forName("android.app.ActivityThread");
            Field field = forName.getDeclaredField("sCurrentActivityThread");
            field.setAccessible(true);
            Object activityThreadValue = field.get(forName);
            Field mH = forName.getDeclaredField("mH");
            mH.setAccessible(true); Object handler = mH.get(activityThreadValue); Class<? > handlerClass = handler.getClass().getSuperclass();if (null! = handlerClass) {// Assign HackCallback to mCallback
                Field callbackField = handlerClass.getDeclaredField("mCallback");
                callbackField.setAccessible(true);
                Handler.Callback originalCallback = (Handler.Callback) callbackField.get(handler);
                HackCallback callback = new HackCallback(originalCallback);
                callbackField.set(handler, callback);
            }

            MatrixLog.i(TAG, "hook system handler completed. start:%s SDK_INT:%s", sApplicationCreateBeginTime, Build.VERSION.SDK_INT);
        } catch (Exception e) {
            MatrixLog.e(TAG, "hook system handler err! %s", e.getCause().toString()); }}ActivityManagerService starts ApplicationThread with binder and then sends message to ApplicationThread with binder.
    // Finally start the luanchActivity in ActivityThread, hook it to listen for messages, find luanchActivity messages, then record the corresponding information, such as app startup completed flag.

 	private final static class HackCallback implements Handler.Callback {
        private static final int LAUNCH_ACTIVITY = 100;
        private static final int CREATE_SERVICE = 114;
        private static final int RECEIVER = 113;
        private static final int EXECUTE_TRANSACTION = 159; / / for Android 9.0
        private static boolean isCreated = false;
        private static int hasPrint = 10;

        private final Handler.Callback mOriginalCallback;

        HackCallback(Handler.Callback callback) {
            this.mOriginalCallback = callback;
        }

        @Override
        public boolean handleMessage(Message msg) {

            if(! AppMethodBeat.isRealTrace()) {return null! = mOriginalCallback && mOriginalCallback.handleMessage(msg); }// Check whether it is a launchActivity message
            boolean isLaunchActivity = isLaunchActivity(msg);

            if (hasPrint > 0) {
                MatrixLog.i(TAG, "[handleMessage] msg.what:%s begin:%s isLaunchActivity:%s", msg.what, SystemClock.uptimeMillis(), isLaunchActivity);
                hasPrint--;
            }
            if (isLaunchActivity) {
                ActivityThreadHacker.sLastLaunchActivityTime = SystemClock.uptimeMillis();
                ActivityThreadHacker.sLastLaunchActivityMethodIndex = AppMethodBeat.getInstance().maskIndex("LastLaunchActivityMethodIndex");
            }

            if(! isCreated) {if (isLaunchActivity || msg.what == CREATE_SERVICE || msg.what == RECEIVER) { // todo for provider
                    / / assignment app start end time sApplicationCreateEndTime - sApplicationCreateBeginTime is our app startup time
                    ActivityThreadHacker.sApplicationCreateEndTime = SystemClock.uptimeMillis();
                    ActivityThreadHacker.sApplicationCreateScene = msg.what;
                    isCreated = true;
                    sIsCreatedByLaunchActivity = isLaunchActivity;
                    MatrixLog.i(TAG, "application create end, sApplicationCreateScene:%d, isLaunchActivity:%s", msg.what, isLaunchActivity);
                    synchronized (listeners) {
                        for (IApplicationCreateListener listener : listeners) {
                            // App startup completed callbacklistener.onApplicationCreateEnd(); }}}}return null! = mOriginalCallback && mOriginalCallback.handleMessage(msg); }private Method method = null;
		// Determine whether the message is a LaunchActivity
        private boolean isLaunchActivity(Message msg) {
            if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O_MR1) {
                if(msg.what == EXECUTE_TRANSACTION && msg.obj ! =null) {
                    try {
                        if (null == method) {
                            Class clazz = Class.forName("android.app.servertransaction.ClientTransaction");
                            method = clazz.getDeclaredMethod("getCallbacks");
                            method.setAccessible(true);
                        }
                        List list = (List) method.invoke(msg.obj);
                        if(! list.isEmpty()) {return list.get(0).getClass().getName().endsWith(".LaunchActivityItem"); }}catch (Exception e) {
                        MatrixLog.e(TAG, "[isLaunchActivity] %s", e); }}return msg.what == LAUNCH_ACTIVITY;
            } else {
                returnmsg.what == LAUNCH_ACTIVITY; }}}/**
     * hook method when it's called out.
     *
     * @param methodId
     */
    public static void o(int methodId) {
        if (status <= STATUS_STOPPED) {
            return;
        }
        if (methodId >= METHOD_ID_MAX) {
            return;
        }
        if (Thread.currentThread().getId() == sMainThreadId) {
            if (sIndex < Constants.BUFFER_SIZE) {
                mergeData(methodId, sIndex, false);
            } else {
                sIndex = 0;
                mergeData(methodId, sIndex, false); } ++sIndex; }}/**
     * when the special method calls,it's will be called.
     *
     * @param activity now at which activity
     * @param isFocus  this window if has focus
     */
    public static void at(Activity activity, boolean isFocus) {
        String activityName = activity.getClass().getName();
        if (isFocus) {
            if (sFocusActivitySet.add(activityName)) {
                synchronized (listeners) {
                    for (IAppMethodBeatListener listener : listeners) {
                        listener.onActivityFocused(activity);
                    }
                }
                MatrixLog.i(TAG, "[at] visibleScene[%s] has %s focus!", getVisibleScene(), "attach"); }}else {
            if (sFocusActivitySet.remove(activityName)) {
                MatrixLog.i(TAG, "[at] visibleScene[%s] has %s focus!", getVisibleScene(), "detach"); }}}Copy the code

It is found that the start time of App startup is recorded when the I function is executed for the first time, and the end time is recorded when the Handler is hooked. It is found that the start time of the entire application has been recorded during the LaunchActivity. But we have so many splashActivities, why is there no relevant logic? Let’s look at one more piece of code

   // We found this in the StartupTracer class
 	@Override
    protected void onAlive(a) {
        super.onAlive();
        MatrixLog.i(TAG, "[onAlive] isStartupEnable:%s", isStartupEnable);
        if (isStartupEnable) {
            AppMethodBeat.getInstance().addListener(this);
            // All activity life callbacks are registered through the application
            Matrix.with().getApplication().registerActivityLifecycleCallbacks(this); }}// Life cycle,
    public interface ActivityLifecycleCallbacks {
        void onActivityCreated(Activity activity, Bundle savedInstanceState);
        void onActivityStarted(Activity activity);
        void onActivityResumed(Activity activity);
        void onActivityPaused(Activity activity);
        void onActivityStopped(Activity activity);
        void onActivitySaveInstanceState(Activity activity, Bundle outState);
        void onActivityDestroyed(Activity activity);
    }
 	/ / the same in StartupTracer, found that this method is not in ActivityLifecycleCallback, actually this lifecycle is plugged into the pile at the callback function
    // The at function inserts code for each onActivityFocused function, so it calls back here
  	@Override
    public void onActivityFocused(Activity activity) {
        if (ActivityThreadHacker.sApplicationCreateScene == Integer.MIN_VALUE) {
            Log.w(TAG, "start up from unknown scene");
            return;
        }

        String activityName = activity.getClass().getName();
        / / cold start
        if (isColdStartup()) {
            // Check whether there is a startup page
            boolean isCreatedByLaunchActivity = ActivityThreadHacker.isCreatedByLaunchActivity();
            MatrixLog.i(TAG, "#ColdStartup# activity:%s, splashActivities:%s, empty:%b, "
                            + "isCreatedByLaunchActivity:%b, hasShowSplashActivity:%b, "
                            + "firstScreenCost:%d, now:%d, application_create_begin_time:%d, app_cost:%d",
                    activityName, splashActivities, splashActivities.isEmpty(), isCreatedByLaunchActivity,
                    hasShowSplashActivity, firstScreenCost, uptimeMillis(),
                    ActivityThreadHacker.getEggBrokenTime(), ActivityThreadHacker.getApplicationCost());
			// Retrieve the createdTime from the createdTimeMap, using the activity name and hash as the key. The createdTime is recorded in onActivityCreated
            String key = activityName + "@" + activity.hashCode();
            Long createdTime = createdTimeMap.get(key);
            if (createdTime == null) {
                createdTime = 0L;
            }
            // Record the current Activity startup time
            createdTimeMap.put(key, uptimeMillis() - createdTime);

            if (firstScreenCost == 0) {
                // First screen startup time, minus the app startup start time
                this.firstScreenCost = uptimeMillis() - ActivityThreadHacker.getEggBrokenTime();
            }
            if (hasShowSplashActivity) {
                // The total time of cold startup is calculated by subtracting the startup time of the splash page from the startup time of the application
                // The cold start time is the time when lauchActivity messages are sent when there is no splash page.
                // This is the time after the splash page is started
                coldCost = uptimeMillis() - ActivityThreadHacker.getEggBrokenTime();
            } else {
                if (splashActivities.contains(activityName)) {
                    hasShowSplashActivity = true;
                } else if (splashActivities.isEmpty()) { //process which is has activity but not main UI process
                    if (isCreatedByLaunchActivity) {
                        coldCost = firstScreenCost;
                    } else {
                        firstScreenCost = 0; coldCost = ActivityThreadHacker.getApplicationCost(); }}else {
                    if (isCreatedByLaunchActivity) {
// MatrixLog.e(TAG, "pass this activity[%s] at duration of start up! splashActivities=%s", activity, splashActivities);
                        coldCost = firstScreenCost;
                    } else {
                        firstScreenCost = 0; coldCost = ActivityThreadHacker.getApplicationCost(); }}}if (coldCost > 0) {
                Long betweenCost = createdTimeMap.get(key);
                if (null! = betweenCost && betweenCost >=30 * 1000) {
                    MatrixLog.e(TAG, "%s cost too much time[%s] between activity create and onActivityFocused, "
                            + "just throw it.(createTime:%s) ", key, uptimeMillis() - createdTime, createdTime);
                    return;
                }
                // Update the time and issue the report
                analyse(ActivityThreadHacker.getApplicationCost(), firstScreenCost, coldCost, false); }}else if (isWarmStartUp()) {
            // Hot start, just record the time when the last activity was created
            isWarmStartUp = false;
            long warmCost = uptimeMillis() - lastCreateActivity;
            MatrixLog.i(TAG, "#WarmStartup# activity:%s, warmCost:%d, now:%d, lastCreateActivity:%d", activityName, warmCost, uptimeMillis(), lastCreateActivity);

            if (warmCost > 0) {
                analyse(0.0, warmCost, true); }}}Copy the code

Now let’s summarize what Trace Canary did in the startup time:

  • Insert the I, o, and at functions to record the start time of the app, hook the ActivityThread Handler object, and get the launchActivity message through the callBack to record the end time of the application startup
  • The at function calls back the Activity’s onActivityFocused lifecycle function, which records the start time of the Activity. The start time is recorded in onActivityCreated

Pile insertion, Hook, register Activity life cycle monitoring, etc., simplify the complex process, free hands. If you’re wondering, why doesn’t it just pile in ActivityThread? So we don’t need hook, right? Sorry, I can’t, I didn’t find the relevant support information. Hook idea is also a good thing, worth further study. I hope you found this analysis helpful.