乐趣区

关于android:面试官今日头条启动很快你觉得可能是做了哪些优化

前言

网上对于启动优化的文章多不胜数,内容千篇一律,大都是列举一些耗时操作,采纳异步加载、懒加载等。

而在面试过程中,对于启动优化的问题,如果只是很外表地答复耗时操作应该放在子线程,显然太过于一般,无奈跟竞争者拉开差距。如何让面试官晓得你的“内功深厚”,那必定是要往原理层面去答复。

本文重点还是关注原理,冷启动优化这个问题能延长到很多原理层面的知识点,本文比拟有意思的中央是通过反编译今日头条 App,钻研大厂的启动优化计划。

讲启动优化之前,先看下利用的启动流程

一、利用启动流程

利用过程不存在的状况下,从点击桌面利用图标,到利用启动(冷启动),大略会经验以下流程:

  1. Launcher startActivity
  2. AMS startActivity
  3. Zygote fork 过程
  4. ActivityThread main()
    4.1. ActivityThread attach
    4.2. handleBindApplication
    4.3 attachBaseContext
    4.4. installContentProviders
    4.5. Application onCreate
  5. ActivityThread 进入 loop 循环
  6. Activity 生命周期回调,onCreate、onStart、onResume…

整个启动流程咱们能干涉的次要是 4.3、4.5 和 6,利用启动优化次要从这三个中央动手。现实情况下,这三个中央如果不做任何耗时操作,那么利用启动速度就是最快的,然而事实很骨感,很多开源库接入第一步个别都是在 Application onCreate 办法初始化,有的甚至间接内置 ContentProvider,间接在 ContentProvider 中初始化框架,不给你优化的机会。

二、启动优化

直奔主题,常见的启动优化形式大略有这些:

  • 闪屏页优化
  • MultipDex 优化(本文重点)
  • 第三方库懒加载
  • WebView 优化
  • 线程优化
  • 零碎调用优化

2.1 闪屏页优化

打消启动时的白屏 / 黑屏,市面上大部分 App 都采纳了这种办法,非常简单,是一个障眼法,不会缩短理论冷启动工夫,简略贴下实现形式吧。

<application
    android:name=".MainApplication"
    ...
    android:theme="@style/AppThemeWelcome>

styles.xml 减少一个主题叫 AppThemeWelcome

<style name="AppThemeWelcome" parent="Theme.AppCompat.NoActionBar">
    ...
    <item name="android:windowBackground">@drawable/logo</item>  <!-- 默认背景 -->
</style>

闪屏页设置这个主题,或者全局给 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>

这样的话启动 Activity 之后背景会始终在,所以在 Activity 的 onCreate 办法中切换成失常主题

protected void onCreate(@Nullable Bundle savedInstanceState) {setTheme(R.style.AppTheme); // 切换失常主题
    super.onCreate(savedInstanceState);

这样关上桌面图标会马上显示 logo,不会呈现黑 / 白屏,直到 Activity 启动实现,替换主题,logo 隐没,然而总的启动工夫并没有扭转。

2.2 MultiDex 优化(本文重点)

MultiDex 之前,先梳理下 apk 编译流程

2.2.1 apk 编译流程

Android Studio 按下编译按钮后产生了什么?

  1. 打包资源文件,生成 R.java 文件(应用工具 AAPT)
  2. 解决 AIDL 文件,生成 java 代码(没有 AIDL 则疏忽)
  3. 编译 java 文件,生成对应.class 文件(java compiler)
  4. .class 文件转换成 dex 文件(dex)
  5. 打包成没有签名的 apk(应用工具 apkbuilder)
  6. 应用签名工具给 apk 签名(应用工具 Jarsigner)
  7. 对签名后的.apk 文件进行对齐解决,不进行对齐解决不能公布到 Google Market(应用工具 zipalign)

在第 4 步,将 class 文件转换成 dex 文件,默认只会生成一个 dex 文件,单个 dex 文件中的办法数不能超过 65536,不然编译会报错:

Unable to execute dex: method ID not in [0, 0xffff]: 65536

App 集成一堆库之后,办法数个别都是超过 65536 的,解决办法就是:一个 dex 装不下,用多个 dex 来装,gradle 减少一行配置即可。

multiDexEnabled true

这样解决了编译问题,在 5.0 以上手机运行失常,然而 5.0 以下手机运行间接 crash,报错 Class NotFound xxx。

Android 5.0 以下,ClassLoader 加载类的时候只会从 class.dex(主 dex)里加载,ClassLoader 不意识其它的 class2.dex、class3.dex、…,当拜访到不在主 dex 中的类的时候,就会报错:Class NotFound xxx,因而谷歌给出兼容计划,MultiDex

2.2.2 MultiDex 原来这么耗时

在 Android 4.4 的机器打印 MultiDex.install(context) 耗时如下:

MultiDex.install 耗时:1320

均匀耗时 1 秒以上,目前大部分利用应该还是会兼容 5.0 以下手机,那么 MultiDex 优化是冷启动优化的大头。

为什么 MultiDex 会这么耗时?老规矩,剖析一下 MultiDex 原理~

2.2.3 MultiDex 原理

上面看下 MultiDex 的 install 办法做了什么事

public static void install(Context context) {Log.i("MultiDex", "Installing application");
        if (IS_VM_MULTIDEX_CAPABLE) { //5.0 以上 VM 根本反对多 dex,啥事都不必干
            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");
        }
    }

从入口的判断来看,如果虚拟机自身就反对加载多个 dex 文件,那就啥都不必做;如果是不反对加载多个 dex(5.0 以下是不反对的),则走到 doInstallation 办法。

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 {
...
                    // 获取非主 dex 文件
                    File dexDir = getDexDir(mainContext, dataDir, secondaryFolderName);
                    MultiDexExtractor extractor = new MultiDexExtractor(sourceApk, dexDir);
                    IOException closeException = null;

                    try {

                        // 1. 这个 load 办法,第一次没有缓存,会十分耗时
                        List files = extractor.load(mainContext, prefsKeyPrefix, false);

                        try {
                            //2. 装置 dex
                            installSecondaryDexes(loader, dexDir, files);
                        } 
                        ...

                }
            }
        }
    }

先看正文 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 {
                    // 读缓存的 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);
                    // 读取缓存的 dex 失败,可能是损坏了,那就从新去解压 apk 读取,跟 else 代码块一样
                    files = this.performExtractions();
                    // 保留标记位到 sp,下次进来就走 if 了,不走 else
                    putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(this.sourceApk), this.sourceCrc, files);
                }
            } else {
                // 没有缓存,解压 apk 读取
                files = this.performExtractions();
                // 保留 dex 信息到 sp,下次进来就走 if 了,不走 else
                putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(this.sourceApk), this.sourceCrc, files);
            }

            Log.i("MultiDex", "load found" + files.size() + "secondary dex files");
            return files;
        }
    }

查找 dex 文件,有两个逻辑,有缓存就调用 loadExistingExtractions 办法,没有缓存或者缓存读取失败就调用 performExtractions 办法,而后再缓存起来。应用到缓存,那么performExtractions 办法想必应该是很耗时的,剖析一下代码:

private List<MultiDexExtractor.ExtractedDex> performExtractions() throws IOException {
        // 先确定命名格局
        String extractedFilePrefix = this.sourceApk.getName() + ".classes";
        this.clearDexDir();
        List<MultiDexExtractor.ExtractedDex> files = new ArrayList();
        ZipFile apk = new ZipFile(this.sourceApk); // apk 转为 zip 格局

        try {
            int secondaryNumber = 2;
            //apk 曾经是改为 zip 格局了,解压遍历 zip 文件,外面是 dex 文件,// 名字有法则,如 classes1.dex,class2.dex
            for(ZipEntry dexFile = apk.getEntry("classes" + secondaryNumber + ".dex"); dexFile != null; dexFile = apk.getEntry("classes" + secondaryNumber + ".dex")) {
                // 文件名:xxx.classes1.zip
                String fileName = extractedFilePrefix + secondaryNumber + ".zip";
                // 创立这个 classes1.zip 文件
                MultiDexExtractor.ExtractedDex extractedFile = new MultiDexExtractor.ExtractedDex(this.dexDir, fileName);
                //classes1.zip 文件增加到 list
                files.add(extractedFile);
                Log.i("MultiDex", "Extraction is needed for file" + extractedFile);
                int numAttempts = 0;
                boolean isExtractionSuccessful = false;

                while(numAttempts < 3 && !isExtractionSuccessful) {
                    ++numAttempts;
                    // 这个办法是将 classes1.dex 文件写到压缩文件 classes1.zip 里去,最多重试三次
                    extract(apk, dexFile, extractedFile, extractedFilePrefix);

                 ...
                }
        // 返回 dex 的压缩文件列表
        return files;
    }

这里的逻辑就是解压 apk,遍历出外面的 dex 文件,例如 class1.dex,class2.dex,而后又压缩成 class1.zip,class2.zip…,而后返回 zip 文件列表。

思考为什么这里要压缩呢?前面波及到 ClassLoader 加载类原理的时候会剖析 ClassLoader 反对的文件格式。

第一次加载才会执行解压和压缩过程,第二次进来读取 sp 中保留的 dex 信息,间接返回 file list,所以第一次启动的时候比拟耗时。

dex 文件列表找到了,回到下面 MultiDex#doInstallation 办法的正文 2,找到的 dex 文件列表,而后调用 installSecondaryDexes 办法进行装置,怎么装置呢?办法点进去看 SDK 19 以上的实现

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 反射 ClassLoader 的 pathList 字段
            Object dexPathList = pathListField.get(loader);
            ArrayList<IOException> suppressedExceptions = new ArrayList();
            // 2 扩大数组
            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));
        }
    }
  1. 反射 ClassLoader 的 pathList 字段
  2. 找到 pathList 字段对应的类的makeDexElements 办法
  3. 通过MultiDex.expandFieldArray 这个办法扩大 dexElements 数组,怎么扩大?看下代码:
    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)); // 取出原来的 dexElements 数组
        Object[] combined = (Object[])((Object[])Array.newInstance(original.getClass().getComponentType(), original.length + extraElements.length)); // 新的数组
        System.arraycopy(original, 0, combined, 0, original.length); // 原来数组内容拷贝到新的数组
        System.arraycopy(extraElements, 0, combined, original.length, extraElements.length); //dex2、dex3... 拷贝到新的数组
        jlrField.set(instance, combined); // 将 dexElements 从新赋值为新的数组
    }

就是创立一个新的数组,把原来数组内容(主 dex)和要减少的内容(dex2、dex3…)拷贝进去,反射替换原来的 dexElements 为新的数组,如下图

看起来有点眼生,Tinker 热修复 的原理也是通过反射将修复后的 dex 增加到这个 dex 数组去,不同的是热修复是增加到数组最后面,而 MultiDex 是增加到数组前面。这样讲可能还不是很好了解?来看看 ClassLoader 怎么加载一个类的就明确了~

2.2.4 ClassLoader 加载类原理

不论是 PathClassLoader还是 DexClassLoader,都继承自BaseDexClassLoader,加载类的代码在 BaseDexClassLoader

4.4 源码

/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java

  1. 构造方法通过传入 dex 门路,创立了DexPathList
  2. ClassLoader 的 findClass 办法最终是调用 DexPathList 的 findClass 办法

接着看 DexPathList 源码 /dalvik/src/main/java/dalvik/system/DexPathList.java

DexPathList外面定义了一个 dexElements 数组,findClass 办法中用到,看下

findClass 办法逻辑很简略,就是 遍历 dexElements 数组,拿到外面的 DexFile 对象,通过 DexFile 的 loadClassBinaryName 办法加载一个类。

最终创立 Class 是通过 native 办法,就不追下去了,大家有趣味能够看下 native 层是怎么创立 Class 对象的。DexFile.cpp

那么问题来了,5.0 以下这个 dexElements 外面只有主 dex(能够认为是一个 bug),没有 dex2、dex3…,MultiDex 是怎么把 dex2 增加进去呢? 答案就是反射 DexPathListdexElements字段,而后把咱们的 dex2 增加进去,当然,dexElements 外面放的是 Element 对象,咱们只有 dex2 的门路,必须转换成 Element 格局才行,所以 反射 DexPathList 外面的 makeDexElements 办法,将 dex 文件转换成 Element 对象即可。

dex2、dex3… 通过 makeDexElements 办法转换成要新增的 Element 数组,最初一步就是反射 DexPathList 的 dexElements 字段,将原来的 Element 数组和新增的 Element 数组合并,而后反射赋值给 dexElements 变量,最初 DexPathList 的 dexElements 变量就蕴含咱们新加的 dex 在外面了。

makeDexElements办法会判断 file 类型,下面讲 dex 提取的时候解压 apk 失去 dex,而后又将 dex 压缩成 zip,压缩成 zip,就会走到第二个判断里去。认真想想,其实 dex 不压缩成 zip,走第一个判断也没啥问题吧,那谷歌的 MultiDex 为什么要将 dex 压缩成 zip 呢?在 Android 开发高手课中看到张绍文也提到这一点

而后我在反编译头条 App 的时候,发现头条参考谷歌的 MultiDex,本人写了一套,猜测可能是优化这个多余的压缩过程,头条的计划上面会介绍。

2.2.5 原理小结

ClassLoader 加载类原理:

ClassLoader.loadClass -> DexPathList.loadClass -> 遍历 dexElements 数组 ->DexFile.loadClassBinaryName

艰深点说就是:ClassLoader 加载类的时候是通过遍历 dex 数组,从 dex 文件外面去加载一个类,加载胜利就返回,加载失败则抛出 Class Not Found 异样。

MultiDex 原理:

在明确 ClassLoader 加载类原理之后,咱们能够通过反射 dexElements 数组,将新增的 dex 增加到数组前面,这样就保障 ClassLoader 加载类的时候能够从新增的 dex 中加载到指标类,通过剖析后最终 MultipDex 原理图如下:

2.2.6 MultiDex 优化(两种计划)

晓得了 MultiDex 原理之后,能够了解 install 过程为什么耗时,因为波及到解压 apk 取出 dex、压缩 dex、将 dex 文件通过反射转换成 DexFile 对象、反射替换数组。

那么 MultiDex 到底应该怎么优化呢,放子线程可行吗?

计划 1:子线程 install(不举荐)

这个办法大家很容易就能想到,在闪屏页开一个子线程去执行 MultiDex.install,而后加载完才跳转到主页。须要留神的是闪屏页的 Activity,包含闪屏页中援用到的其它类必须在主 dex 中,不然在MultiDex.install 之前加载这些不在主 dex 中的类会报错 Class Not Found。这个能够通过 gradle 配置,如下:

    defaultConfig {
        // 分包,指定某个类在 main dex
        multiDexEnabled true
        multiDexKeepProguard file('multiDexKeep.pro') // 打包到 main dex 的这些类的混同规制,没非凡需要就给个空文件
        multiDexKeepFile file('maindexlist.txt') // 指定哪些类要放到 main dex
    }

maindexlist.txt 文件指定哪些类要打包到主 dex 中,内容格局如下

com/lanshifu/launchtest/SplashActivity.class

在已有我的项目中用这种形式,一顿操作猛如虎之后,编译运行在 4.4 的机器上,启动闪屏页,加载完筹备进入主页间接崩掉了。

报错 NoClassDefFoundError,个别都是该类没有在主 dex 中,要在 maindexlist.txt 将配置指定在主 dex。** 第三方库中的 ContentProvider 必须指定在主 dex 中,否则也会找不到,为什么?** 文章结尾说过利用的启动流程,ContentProvider 初始化机会 如下图:

ContentProvider 初始化太早了,如果不在主 dex 中,还没启动闪屏页就曾经 crash 了。

所以这种计划的毛病很显著:

  1. MultiDex 加载逻辑放在闪屏页的话,闪屏页中援用到的类都要配置在主 dex。
  2. ContentProvider 必须在主 dex,一些第三方库自带 ContentProvider,保护比拟麻烦,要一个一个配置。

这时候就思考一下,有没有其它更好的计划呢?大厂是怎么做的?今日头条必定要对 MultiDex 进行优化吧,反编译瞧瞧?

点了一根烟之后,开始偷代码 …

MultiDex 优化计划 2:今日头条计划

今日头条没有加固,反编译后很容易通过关键字搜寻找到 MultidexApplication 这个类,

看正文 1 的 d.a(this); 这个办法,代码尽管混同了,然而办法外部的代码还是能够看出是干嘛的,持续跟这个办法,为了不影响浏览,我对混同做了一些解决,改成失常的办法名。

每个办法结尾都有 PatchProxy.isSupport 这个 if 判断,这个是美团 Robust 热修复生成的代码,今日头条没有本人的热修复框架,没有用 Tinker,而是用美团的,想理解对于 Robust 细节能够参考文末链接。Robust 间接跳过,看 else 代码块即可。

持续看 loadMultiDex 办法

逻辑如下:
1. 创立临时文件,作为判断 MultiDex 是否加载完的条件
2. 启动 LoadDexActivity 去加载 MultiDex(LoadDexActivity 在独自过程),加载完会删除临时文件
3. 开启 while 循环,直到临时文件不存在才跳出循环,进入 Application 的 onCreate

创立临时文件代码

while 循环代码

LoadDexActivity 只有一个加载框,加载完再跳转到闪屏页

dex 加载完应该要 finish 掉以后 Activity

依照下面代码剖析,今日头条在 5.0 以下手机首次启动应该是这样:

  1. 关上桌面图标
  2. 显示默认背景
  3. 跳转到加载 dex 的界面,展现一个 loading 的加载框几秒钟
  4. 跳转到闪屏页

实际上是不是这样呢,用 4.4 机器试下?

看起来齐全跟猜测的统一,撸几行代码验证应该不难吧?

点了一根烟之后,开始撸代码,最终实现成果如下

成果跟今日头条是统一的,不再反复剖析代码了,源码上传到 github,感兴趣的同学能够参考参考,头条的计划,值得尝试~ github.com/lanshifu/Mu…

再次梳理一下这种形式:

  1. 在主过程 Application 的 attachBaseContext 办法中判断如果须要应用 MultiDex,则创立一个临时文件,而后开一个过程(LoadDexActivity),显示 Loading,异步执行 MultiDex.install 逻辑,执行完就删除临时文件并 finish 本人。
  2. 主过程 Application 的 attachBaseContext 进入 while 代码块,定时轮循临时文件是否被删除,如果被删除,阐明 MultiDex 曾经执行完,则跳出循环,持续失常的利用启动流程。
  3. 留神 LoadDexActivity 必须要配置在 main dex 中。

有些同学可能会问,启动还是很久啊,冷启动工夫有变动吗?冷启动工夫是指导击桌面图标到第一个 Activity 显示这段时间。

MultiDex 优化总结

计划 1:间接在闪屏页开个子线程去执行 MultiDex 逻辑,MultiDex 不影响冷启动速度,然而难保护。

计划 2:今日头条的 MultiDex 优化计划:

  1. 在 Application 的 attachBaseContext 办法里,启动另一个过程的 LoadDexActivity 去异步执行 MultiDex 逻辑,显示 Loading。
  2. 而后主过程 Application 进入 while 循环,一直检测 MultiDex 操作是否实现
  3. MultiDex 执行完之后主过程 Application 持续走,ContentProvider 初始化和 Application onCreate 办法,也就是执行主过程失常的逻辑。

其实应该还有计划 3,因为我发现头条并没有间接应用 Google 的 MultiDex,而是参考谷歌的 MultiDex,本人写了一套,耗时应该会少一些,大家有趣味能够去钻研一下。

2.3 预创立 Activity

这段代码是今日头条外面的,Activity 对象事后 new 进去,

对象第一次创立的时候,java 虚拟机首先查看类对应的 Class 对象是否曾经加载。如果没有加载,jvm 会依据类名查找.class 文件,将其 Class 对象载入。同一个类第二次 new 的时候就不须要加载类对象,而是间接实例化,创立工夫就缩短了。

头条真是把启动优化做到极致。

2.4 第三方库懒加载

很多第三方开源库都说在 Application 中进行初始化,十几个开源库都放在 Application 中,必定对冷启动会有影响,所以能够思考按需初始化,例如 Glide,能够放在本人封装的图片加载类中,调用到再初始化,其它库也是同理,让 Application 变得更轻。

2.5 WebView 启动优化。

WebView 启动优化文章也比拟多,这里只说一下大略优化思路。

  1. WebView 第一次创立比拟耗时,能够事后创立 WebView,提前将其内核初始化。
  2. 应用 WebView 缓存池,用到 WebView 的中央都从缓存池取,缓存池中没有缓存再创立,留神内存透露问题。
  3. 本地预置 html 和 css,WebView 创立的时候先预加载本地 html,之后通过 js 脚本填充内容局部。

这一部分能够参考:mp.weixin.qq.com/s/KwvWURD5W…

2.6 数据预加载

这种形式个别是在主页闲暇的时候,将其它页面的数据加载好,保留到内存或数据库,等到关上该页面的时候,判断曾经预加载过,间接从内存或数据库读取数据并显示。

2.7 线程优化

线程是程序运行的根本单位,线程的频繁创立是耗性能的,所以大家应该都会用线程池。单个 cpu 状况下,即便是开多个线程,同时也只有一个线程能够工作,所以线程池的大小要依据 cpu 个数来确定。

启动优化形式就先介绍到这里,常见的就是这些,其它的能够作为补充。

三、启动耗时分析方法

TraceView性能损耗太大,失去的后果不实在。Systrace 能够不便追踪要害零碎调用的耗时状况,如 Choreographer,然而不反对利用程序代码的耗时剖析。

3.1 Systrace + 函数插桩

联合 Systrace 函数插桩,就是将如下代码插入到每个办法的入口和进口

class Trace{public static void i(String tag){android.os.Trace.beginSection(tag);
    }

    public static void o(){android.os.Trace.endSection();
    }

}

插桩后的代码如下

void test(){Trace.i("test");
    System.out.println("doSomething");
    Trace.o();}

插桩工具参考:github.com/AndroidAdva…

mac 下 systrace 门路在

/Users/{xxx}/Library/Android/sdk/platform-tools/systrace/

编译运行 app,执行命令

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

最初按下 Enter 进行捕捉 trace 信息,在目录下生成报告 test.log.html,间接能够用谷歌浏览器关上查看。

3.2 BlockCanary 也能够检测

BlockCanary 能够监听主线程耗时的办法,将阈值设置低一点,比方 200 毫秒,这样的话如果一个办法执行工夫超过 200 毫秒,获取堆栈信息并告诉开发者。

BlockCanary 原理在之前那篇卡顿优化的文章外面讲过一些,这里就不再反复。

总结

文章有点长,看到这里,是不是遗记结尾讲什么了?总结一下这篇文章次要波及到哪些内容:

  1. 利用启动流程
  2. 闪屏页优化
  3. MultiDex 原理剖析
  4. ClassLoader 加载一个类的流程剖析
  5. 热修复原理
  6. MultiDex 优化:介绍了两种形式,一种是间接在闪屏页开个子线程去加载 dex,难保护,不举荐;一种是今日头条的计划,在独自一个过程加载 dex,加载完主过程再持续。
  7. 疾速启动 Activity 的形式:预创立 Activity,预加载数据。
  8. 启动工夫监控的形式:Systrace+ 插桩、BlockCanary。

面试问到启动优化问题,不要简略一两句话答复,能够说说本人在理论我的项目中做了哪些优化,比方 Multidex 优化,把整个流程,原理说分明。当然,前提是本人要去实际,了解为什么要这样做。

就这样,有问题请留言,更多文章,敬请期待。


相干视频:

【2021 最新版】Android studio 装置教程 +Android(安卓)零基础教程视频(适宜 Android 0 根底,Android 初学入门)_哔哩哔哩_bilibili

Android 架构设计原理与实战——Jetpack 联合 MVP 组合利用开发一个优良的 APP!_哔哩哔哩_bilibili

Android 进阶必学:jetpack 架构组件—Navigation_哔哩哔哩_bilibili

Android 进阶零碎学习——Jetpack 先天优良的基因能够防止数据内存透露_哔哩哔哩_bilibili

本文转自 https://juejin.cn/post/6844903958113157128,如有侵权,请分割删除。

退出移动版