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.