preface
There are numerous articles on startup optimization on the Internet, with the same content. Most of them list some time-consuming operations, such as asynchronous loading, lazy loading, etc.
During the interview, if the question about startup optimization is only answered superficially, the time-consuming operation should be placed in the sub thread. Obviously, it is too common to open the gap with competitors. How to let the interviewer know your "deep internal skill" must be answered at the principle level.
This paper focuses on the principle. The problem of cold start optimization can be extended to many knowledge points at the principle level. The interesting part of this paper is to study the startup optimization scheme of large manufacturers by decompiling today's headline App.
Before starting optimization, let's take a look at the application startup process
1, Application startup process
When the application process does not exist, the process from clicking the desktop application icon to application startup (cold startup) will probably go through the following processes:
- Launcher startActivity
- AMS startActivity
- Zygote fork process
- ActivityThread main()undefined4.1. ActivityThread attachundefined4.2. handleBindApplicationundefined4.3 attachBaseContextundefined4.4. installContentProvidersundefined4.5. Application onCreate
- ActivityThread enters loop loop loop
- Activity lifecycle callback, onCreate, onStart, onResume
In the whole startup process, we can mainly intervene in 4.3, 4.5 and 6. The application startup optimization mainly starts from these three places. Ideally, if these three places do not do any time-consuming operations, the application startup speed is the fastest, but the reality is very skinny. The first step in accessing many open source libraries is generally to initialize in the Application onCreate method. Some even directly build in the ContentProvider to initialize the framework directly in the ContentProvider, which does not give you the opportunity to optimize.
2, Start optimization
Go straight to the topic. The common startup optimization methods are as follows:
- Flash page optimization
- MultipDex optimization (focus of this article)
- Third party library lazy loading
- WebView optimization
- Thread optimization
- System call optimization
2.1 screen optimization
Eliminate the white screen / black screen during startup. This method is adopted by most apps on the market. It is very simple and a cover up. It will not shorten the actual cold start time. Simply paste the implementation method.
<application android:name=".MainApplication" ... android:theme="@style/AppThemeWelcome>
styles.xml adds a topic called AppThemeWelcome
<style name="AppThemeWelcome" parent="Theme.AppCompat.NoActionBar"> ... <item name="android:windowBackground">@drawable/logo</item> <!-- Default background--> </style>
Set this theme on the splash screen page, or set it globally for Application
<activity android:name=".ui.activity.DemoSplashActivity" android:configChanges="orientation|screenSize|keyboardHidden" android:theme="@style/AppThemeWelcome" android:screenOrientation="portrait"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity>
In this case, the background will remain after the Activity is started, so switch to the normal theme in the onCreate method of the Activity
protected void onCreate(@Nullable Bundle savedInstanceState) { setTheme(R.style.AppTheme); //Switch to normal theme super.onCreate(savedInstanceState);
In this way, opening the desktop icon will immediately display the logo without black / white screen until the Activity is started, the theme is replaced, and the logo disappears, but the total startup time has not changed.
2.2 MultiDex optimization (focus of this paper)
Before talking about MultiDex, first sort out the apk compilation process
2.2.1 apk compilation process
What happens when Android Studio presses the compile button?
- Package the resource file and generate the R.java file (using the tool AAPT)
- Process AIDL files and generate java code (ignored without AIDL)
- Compile java files to generate corresponding class file (java compiler)
- . Convert class file to dex file (dex)
- Package as an unsigned apk (using the tool apkbuilder)
- Use the signing tool to sign apk (use the tool Jarsigner)
- After the signature apk files are aligned. Without alignment, they cannot be published to Google Market (use the tool zippalign)
In step 4, convert the class file into a dex file. By default, only one dex file will be generated. The number of methods in a single dex file cannot exceed 65536, otherwise an error will be reported during compilation:
Unable to execute dex: method ID not in [0, 0xffff]: 65536
After the App integrates a stack of libraries, the number of methods is generally more than 65536. The solution is: one dex can not be installed, use multiple dex to install, and add a line of configuration to gradle.
multiDexEnabled true
This solves the compilation problem. Mobile phones above 5.0 operate normally, but mobile phones below 5.0 directly crash and report an error Class NotFound xxx.
Under Android 5.0, ClassLoader only loads classes from class Loaded in dex (main DEX), ClassLoader does not know other class2.dex, class3.dex,... When accessing a class that is not in the main DEX, it will report an error: Class NotFound xxx. Therefore, Google gives a compatibility scheme, MultiDex.
2.2.2 MultiDex used to be so time-consuming
Print multidex. On Android 4.4 machines Install (context) takes the following time:
MultiDex.install Time: 1320
It takes more than 1 second on average. At present, most applications should still be compatible with mobile phones under 5.0, so MultiDex optimization is the main part of cold start optimization.
Why does MultiDex take so long? Old rule, analyze the MultiDex principle~
2.2.3 MultiDex principle
Let's take a look at what the MultiDex install method does
public static void install(Context context) { Log.i("MultiDex", "Installing application"); if (IS_VM_MULTIDEX_CAPABLE) { //VM S above 5.0 basically support multiple dex, so you don't have to do anything Log.i("MultiDex", "VM has multidex support, MultiDex support library is disabled."); } else if (VERSION.SDK_INT < 4) { // throw new RuntimeException("MultiDex installation failed. SDK " + VERSION.SDK_INT + " is unsupported. Min SDK version is " + 4 + "."); } else { ... doInstallation(context, new File(applicationInfo.sourceDir), new File(applicationInfo.dataDir), "secondary-dexes", "", true); ... Log.i("MultiDex", "install done"); } }
Judging from the entry, if the virtual machine itself supports loading multiple dex files, you don't have to do anything; If loading multiple dex is not supported (not supported under 5.0), go to the doInstallation method.
private static void doInstallation(Context mainContext, File sourceApk, File dataDir, String secondaryFolderName, String prefsKeyPrefix, boolean reinstallOnPatchRecoverableException) throws IOException, IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, SecurityException, ClassNotFoundException, InstantiationException { ... //Get non primary dex file File dexDir = getDexDir(mainContext, dataDir, secondaryFolderName); MultiDexExtractor extractor = new MultiDexExtractor(sourceApk, dexDir); IOException closeException = null; try { // 1. This load method, without cache for the first time, will be very time-consuming List files = extractor.load(mainContext, prefsKeyPrefix, false); try { //2. Install dex installSecondaryDexes(loader, dexDir, files); } ... } } } }
First look at note 1, MultiDexExtractor#load
List<? extends File> load(Context context, String prefsKeyPrefix, boolean forceReload) throws IOException { if (!this.cacheLock.isValid()) { throw new IllegalStateException("MultiDexExtractor was closed"); } else { List files; if (!forceReload && !isModified(context, this.sourceApk, this.sourceCrc, prefsKeyPrefix)) { try { //Read cached dex files = this.loadExistingExtractions(context, prefsKeyPrefix); } catch (IOException var6) { Log.w("MultiDex", "Failed to reload existing extracted secondary dex files, falling back to fresh extraction", var6); //Failed to read the dex of the cache. It may be damaged. Then decompress the apk again, just like the else code block files = this.performExtractions(); //Save the flag bit to sp and leave if next time you come in instead of else putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(this.sourceApk), this.sourceCrc, files); } } else { //No cache, unzip apk read files = this.performExtractions(); //Save the dex information to sp, and leave if the next time you come in instead of else putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(this.sourceApk), this.sourceCrc, files); } Log.i("MultiDex", "load found " + files.size() + " secondary dex files"); return files; } }
To find a dex file, there are two logics. If there is a cache, call the loadExistingExtractions method. If there is no cache or the cache fails to read, call the performExtractions method, and then cache it. If the cache is used, the performExtractions method must be time-consuming. Analyze the code:
private List<MultiDexExtractor.ExtractedDex> performExtractions() throws IOException { //First determine the naming format String extractedFilePrefix = this.sourceApk.getName() + ".classes"; this.clearDexDir(); List<MultiDexExtractor.ExtractedDex> files = new ArrayList(); ZipFile apk = new ZipFile(this.sourceApk); // apk to zip format try { int secondaryNumber = 2; //apk has been changed to ZIP format. Unzip and traverse the zip file, which contains the dex file, //Names are regular, such as classes 1 dex,class2. dex for(ZipEntry dexFile = apk.getEntry("classes" + secondaryNumber + ".dex"); dexFile != null; dexFile = apk.getEntry("classes" + secondaryNumber + ".dex")) { //File name: XXX classes1. zip String fileName = extractedFilePrefix + secondaryNumber + ".zip"; //Create this classes1 Zip file MultiDexExtractor.ExtractedDex extractedFile = new MultiDexExtractor.ExtractedDex(this.dexDir, fileName); //classes1. Add zip file to list files.add(extractedFile); Log.i("MultiDex", "Extraction is needed for file " + extractedFile); int numAttempts = 0; boolean isExtractionSuccessful = false; while(numAttempts < 3 && !isExtractionSuccessful) { ++numAttempts; //This method is to put classes 1 The DEX file is written to the compressed file classes1 Zip, try again three times at most extract(apk, dexFile, extractedFile, extractedFilePrefix); ... } //Returns the compressed file list of dex return files; }
The logic here is to unzip the apk and traverse the DEX file inside, such as Class1 dex,class2.dex, and then compressed into Class1 zip,class2.zip..., Then return to the list of zip files.
Think about why it should be compressed here? Later, when it comes to the principle of ClassLoader loading classes, we will analyze the file formats supported by ClassLoader.
The decompression and compression process will be executed only after the first loading. The second time, you can read the dex information saved in sp and directly return to file list. Therefore, it is time-consuming to start for the first time.
The dex file list is found, back to the annotation 2 of the MultiDex#doInstallation method above, the list of dex files found, and then the installSecondaryDexes method is installed to install it. Methods Click to see the implementation of SDK 19 and above
private static final class V19 { private V19() { } static void install(ClassLoader loader, List<? extends File> additionalClassPathEntries, File optimizedDirectory) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException { Field pathListField = MultiDex.findField(loader, "pathList");//1 reflect the pathList field of ClassLoader Object dexPathList = pathListField.get(loader); ArrayList<IOException> suppressedExceptions = new ArrayList(); // 2 extended array MultiDex.expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, new ArrayList(additionalClassPathEntries), optimizedDirectory, suppressedExceptions)); ... } private static Object[] makeDexElements(Object dexPathList, ArrayList<File> files, File optimizedDirectory, ArrayList<IOException> suppressedExceptions) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { Method makeDexElements = MultiDex.findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class, ArrayList.class); return (Object[])((Object[])makeDexElements.invoke(dexPathList, files, optimizedDirectory, suppressedExceptions)); } }
- Reflect the pathList field of ClassLoader
- Find the makeDexElements method of the class corresponding to the pathList field
- Via multidex Expandfieldarray this method extends the dexElements array. How? Look at the code below:
private static void expandFieldArray(Object instance, String fieldName, Object[] extraElements) throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException { Field jlrField = findField(instance, fieldName); Object[] original = (Object[])((Object[])jlrField.get(instance)); //Take out the original dexElements array Object[] combined = (Object[])((Object[])Array.newInstance(original.getClass().getComponentType(), original.length + extraElements.length)); //New array System.arraycopy(original, 0, combined, 0, original.length); //Copy the contents of the original array to the new array System.arraycopy(extraElements, 0, combined, original.length, extraElements.length); //dex2,dex3... Copy to new array jlrField.set(instance, combined); //Reassign dexElements to a new array }
Create a new array, copy the contents of the original array (main DEX) and the contents to be added (dex2, dex3...), and replace the original dexElements with a new array by reflection, as shown in the following figure
It looks familiar. Tinker's principle of hot repair is to add the repaired dex to the dex array through reflection. The difference is that hot repair is added to the front of the array, while MultiDex is added to the back of the array. This may not be well understood? Let's see how ClassLoader loads a class~
2.2. 4. Principle of classloader loading classes
Both PathClassLoader and DexClassLoader inherit from BaseDexClassLoader. The code for loading classes is in BaseDexClassLoader
4.4 source code
/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
- The constructor creates a DexPathList by passing in the dex path.
- The findClass method of ClassLoader finally calls the findClass method of DexPathList
Then look at the source code of DexPathList /dalvik/src/main/java/dalvik/system/DexPathList.java
DexPathList defines a dexElements array, which is used in the findClass method. Take a look
The logic of findClass method is very simple. It is to traverse the dexElements array, get the DexFile object inside, and load a class through the loadClassBinaryName method of DexFile.
Finally, the Class is created through the native method, so we won't catch up. If you are interested, you can see how the native layer creates Class objects. DexFile.cpp
So here comes the question..., In the dexElements below 5.0, there is only the main DEX (it can be considered as a bug). How can MultiDex add dex2 without dex2, dex3...? the answer is to reflect the dexElements field of DexPathList, and then add our dex2. Of course, there are Element objects in dexElements. We only have the path of dex2, which must be converted into Element format, so we reflect makeDexElements in DexPathList Method to convert the DEX file into an Element object.
dex2,dex3... The last step is to reflect the dexElements field of DexPathList, merge the original Element array with the newly added Element array, and then assign the reflection value to the dexElements variable. Finally, the dexElements variable of DexPathList contains our newly added DEX.
The makeDexElements method will judge the file type. As mentioned above, when extracting dex, unzip apk to get dex, and then compress dex into zip, which will go to the second judgment. Think about it carefully. In fact, dex is not compressed into zip. It's no problem to make the first judgment. Why does Google's MultiDex compress dex into ZIP? Zhang Shaowen also mentioned this in the Android Development master class
Then when I decompile the headline App, I found that the headline refers to Google's MultiDex and wrote a set myself. I guess it may be to optimize the redundant compression process. The headline scheme will be introduced below.
2.2. 5 principle summary
ClassLoader loading principle:
ClassLoader. loadClass -> DexPathList. Loadclass - > traverse dexElements array - > dexfile loadClassBinaryName
Generally speaking, when ClassLoader loads a class, it traverses the dex array and loads a class from the dex file. If the loading is successful, it will return. If the loading fails, it will throw a Class Not Found exception.
MultiDex principle:
After understanding the principle of ClassLoader loading classes, we can add the new dex to the back of the array by reflecting the dexElements array, so as to ensure that the target class can be loaded from the new dex when ClassLoader loads classes. After analysis, the final MultipDex schematic diagram is as follows:
2.2.6 MultiDex optimization (two schemes)
After knowing the principle of MultiDex, you can understand why the install process is time-consuming, because it involves decompressing apk, taking out dex, compressing dex, converting dex files into DexFile objects through reflection, and replacing arrays with reflection.
So how should MultiDex be optimized? Is it feasible to put sub threads?
Scheme 1: sub thread install (not recommended)
It's easy to think of this method. Open a sub thread on the splash screen page to execute multidex Install, and then jump to the home page after loading. It should be noted that the Activity of the flash page, including other classes referenced in the flash page, must be in the main dex, otherwise in the multidex Loading these classes that are not in the main dex before install will report an error Class Not Found. This can be configured through gradle, as follows:
defaultConfig { //Subcontracting, specifying a class in main dex multiDexEnabled true multiDexKeepProguard file('multiDexKeep.pro') // For the confusion regulation of these classes packaged in main dex, an empty file is given without special requirements multiDexKeepFile file('maindexlist.txt') // Specify which classes to put in main dex }
maindexlist.txt file specifies which classes to package into the main dex. The content format is as follows
com/lanshifu/launchtest/SplashActivity.class
In the existing projects, after a fierce operation, compile and run on the 4.4 machine, start the flash screen page, load it and prepare to enter the home page, which directly crashes.
The error NoClassDefFoundError is reported. Generally, this class is not in the main dex, but in the maindexlist Txt specifies the configuration in the main dex** The ContentProvider in the third-party library must be specified in the main dex, otherwise it will not be found. Why** At the beginning of the article, the application startup process is described, and the initialization time of ContentProvider is shown in the figure below:
The ContentProvider is initialized too early. If it is not in the main dex, the flash page will crash before it is started.
Therefore, the disadvantages of this scheme are obvious:
If the MultiDex loading logic is placed on the flash screen page, the classes referenced in the flash screen page should be configured in the main dex. The ContentProvider must be in the main dex. Some third-party libraries have their own ContentProviders, which is cumbersome to maintain and needs to be configured one by one.
Think about it at this time. Is there any other better plan? How did big factories do it? Today's headlines must optimize MultiDex. Decompile and see?
After lighting a cigarette, start stealing code
MultiDex optimization scheme 2: today's headline scheme
Today's headlines are not reinforced. After decompilation, it is easy to find the class MultidexApplication through keyword search,
See d.a(this) in note 1; Although the code of this method is confused, the code inside the method can still see what it is. Continue to follow this method. In order not to affect reading, I have handled the confusion and changed it to a normal method name.
Each method begins with a patchproxy According to the if judgment of issupport, this is the code generated by meituan robot hot repair. Today's headlines do not have their own hot repair framework. Instead of Tinker, they use meituan's. for details about robot, please refer to the link at the end of the article. Robust just skip and look at the else code block.
Continue to look at the loadMultiDex method
The logic is as follows:
1. Create a temporary file as a condition to judge whether the MultiDex has been loaded
2. Start LoadDexActivity to load MultiDex (LoadDexActivity is in a separate process). After loading, the temporary file will be deleted
3. Start the while loop. Do not jump out of the loop until the temporary file does not exist and enter onCreate of the Application
Create temporary file code
while loop code
LoadDexActivity has only one loading box. After loading, jump to the flash screen page
After dex is loaded, you should finish the current Activity
According to the above code analysis, today's mobile phones with headlines below 5.0 should be started for the first time:
- Open desktop icon
- Show default background
- Jump to the interface of loading dex and display a loading box for a few seconds
- Jump to the splash screen page
Is this actually the case? Try it with the 4.4 machine?
It seems to be completely consistent with the conjecture. Shouldn't it be difficult to verify a few lines of code?
After lighting a cigarette, start rolling the code. The final implementation effect is as follows
The effect is consistent with today's headlines. The code will not be analyzed repeatedly. Upload the source code to github. Interested students can refer to the headline scheme, which is worth trying~ github.com/lanshifu/Mu...
Comb this way again:
- In the attachBaseContext method of the main process Application, if you need to use MultiDex, create a temporary file, then open a process (LoadDexActivity), display Loading, asynchronously execute the MultiDex.install logic, delete the temporary file and finish yourself after execution.
- The attachBaseContext of the main process Application enters the while code block to periodically cycle whether the temporary file has been deleted. If it has been deleted, it indicates that the execution of MultiDex has been completed. Then it jumps out of the loop and continues the normal Application startup process.
- Note that LoadDexActivity must be configured in main dex.
Some students may ask, it's still a long time to start. Has the cold start time changed? Cold start time refers to the time from clicking the desktop icon to the first Activity display.
MultiDex optimization summary
Scheme 1: directly open a sub thread on the splash screen page to execute MultiDex logic. MultiDex does not affect the cold start speed, but it is difficult to maintain.
Scheme 2: today's headline MultiDex optimization scheme:
- In the attachBaseContext method of the Application, start the LoadDexActivity of another process to asynchronously execute the MultiDex logic and display Loading.
- Then, the main process Application enters the while loop to continuously detect whether the MultiDex operation is completed
- After the execution of MultiDex, the main process Application continues, and the ContentProvider initialization and Application onCreate methods, that is, execute the normal logic of the main process.
In fact, there should be scheme 3, because I found that the headlines do not directly use Google's MultiDex, but refer to Google's MultiDex and write one by myself, which should take less time. If you are interested, you can study it.
2.3 pre create Activity
This code is in today's headlines. The Activity object is new in advance,
When the object is first created, the java virtual machine first checks whether the class object corresponding to the class has been loaded. If it is not loaded, the jvm looks it up based on the class name Class file and load its class object. When the same class is new for the second time, it does not need to load the class object, but is instantiated directly, and the creation time is shortened.
The headline really makes the startup optimization to the extreme.
2.4 lazy loading of third-party libraries
Many third party open source libraries say that they are initialized in Application, and more than ten open source libraries are placed in Application, which is sure to have an impact on cold start. Therefore, we can consider initializing on demand, such as Glide, which can be placed in the picture loading class of its own encapsulation. After calling it to start again, other libraries are also the same, making Application lighter.
2.5 WebView start optimization.
There are many articles on WebView startup optimization. Here are just some general optimization ideas.
- The first creation of WebView is time-consuming. You can create WebView in advance and initialize its kernel in advance.
- When using the WebView cache pool, all places where WebView is used are taken from the cache pool. There is no cache in the cache pool to create. Pay attention to memory leakage.
- HTML and css are preset locally. When WebView is created, the local html is preloaded first, and then the content part is filled through js script.
For this part, please refer to: mp.weixin.qq.com/s/KwvWURD5W...
2.6 data preloading
In this way, when the home page is idle, load the data of other pages and save it to memory or database. When the page is opened, judge that it has been preloaded, and directly read and display the data from memory or database.
2.7 thread optimization
Thread is the basic unit of program operation. Frequent creation of threads consumes performance, so everyone should use thread pool. In the case of a single cpu, even if multiple threads are opened, only one thread can work at the same time, so the size of the thread pool should be determined according to the number of CPUs.
The startup optimization methods are introduced here first. These are common, and others can be used as supplements.
3, Start time consuming analysis method
The performance loss of TraceView is too large, and the results obtained are untrue. Systrace can easily track the time-consuming situation of key system calls, such as Choreographer, but it does not support time-consuming analysis of application code.
3.1 Systrace + function pile insertion
By combining Systrace and function instrumentation, the following code is inserted into the entry and exit of each method
class Trace{ public static void i(String tag){ android.os.Trace.beginSection(tag); } public static void o(){ android.os.Trace.endSection(); } }
The code after pile insertion is as follows
void test(){ Trace.i("test"); System.out.println("doSomething"); Trace.o(); }
Pile insertion tool reference: github.com/AndroidAdva...
systrace path under mac
/Users/{xxx}/Library/Android/sdk/platform-tools/systrace/
Compile and run the app and execute the command
python2 /Users/lanshifu/Library/Android/sdk/platform-tools/systrace/systrace.py gfx view wm am pm ss dalvik app sched -b 90960 -a com.sample.systrace -o test.log.html
Finally, press Enter to stop capturing trace information, and generate the report test log. HTML, which can be opened and viewed directly with Google browser.
3.2 BlockCanary can also be detected
BlockCanary can monitor the methods that take time in the main process and set the threshold lower, such as 200ms. In this way, if the execution time of a method exceeds 200ms, obtain the stack information and notify the developer.
The principle of BlockCanary has been discussed in the previous article on Caton optimization, which will not be repeated here.
summary
The article is a little long. Did you forget what you said at the beginning? To summarize what this article mainly involves:
- Application startup process
- Flash page optimization
- MultiDex principle analysis
- Process analysis of ClassLoader loading a class
- Principle of thermal repair
- MultiDex Optimization: two methods are introduced. One is to directly open sub threads in the flash screen page to load dex, which is difficult to maintain and is not recommended; One is the scheme of today's headlines. Load dex in a single process and continue after loading the main process.
- How to quickly start an Activity: pre create an Activity and pre load data.
- Start time monitoring mode: Systrace + pile insertion and blockcanal.
When you ask the question of startup optimization in the interview, don't answer it in one or two sentences. You can talk about the optimization you have done in the actual project, such as Multidex optimization, and explain the whole process and principle clearly. Of course, the premise is to practice and understand why.
That's it. If you have any questions, please leave a message. For more articles, please look forward to it.
Advanced Android must learn: jetpack architecture component - Navigation_ Beep beep beep_ bilibili
This article is transferred from https://juejin.cn/post/6844903958113157128 , in case of infringement, please contact to delete.