2024年安卓最新面试官:今日头条启动很快,你觉得可能是做了哪些优化?,面试必背

文末

很多人在刚接触这个行业的时候或者是在遇到瓶颈期的时候,总会遇到一些问题,比如学了一段时间感觉没有方向感,不知道该从那里入手去学习,对此我整理了一些资料,需要的可以免费分享给大家

这里笔者分享一份自己收录整理上述技术体系图相关的几十套腾讯、头条、阿里、美团等公司2021年的面试题,把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,这里以图片的形式给大家展示一部分。

【视频教程】

天道酬勤,只要你想,大厂offer并不是遥不可及!希望本篇文章能为你带来帮助,如果有问题,请在评论区留言。

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

4.3 **attachBaseContext**  

4.4. installContentProviders  

4.5. **Application onCreate**
  1. ActivityThread 进入loop循环

  2. 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中,内容格式如下

题外话

我们见过很多技术leader在面试的时候,遇到处于迷茫期的大龄程序员,比面试官年龄都大。这些人有一些共同特征:可能工作了7、8年,还是每天重复给业务部门写代码,工作内容的重复性比较高,没有什么技术含量的工作。问到这些人的职业规划时,他们也没有太多想法。

其实30岁到40岁是一个人职业发展的黄金阶段,一定要在业务范围内的扩张,技术广度和深度提升上有自己的计划,才有助于在职业发展上有持续的发展路径,而不至于停滞不前。

不断奔跑,你就知道学习的意义所在!

注意:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题(含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

// 指定哪些类要放到main dex

}

复制代码




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


### 题外话


我们见过很多技术leader在面试的时候,遇到处于迷茫期的大龄程序员,比面试官年龄都大。这些人有一些共同特征:可能工作了7、8年,还是每天重复给业务部门写代码,工作内容的重复性比较高,没有什么技术含量的工作。问到这些人的职业规划时,他们也没有太多想法。

其实30岁到40岁是一个人职业发展的黄金阶段,一定要在业务范围内的扩张,技术广度和深度提升上有自己的计划,才有助于在职业发展上有持续的发展路径,而不至于停滞不前。

不断奔跑,你就知道学习的意义所在!


> **注意:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题(含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)**

[外链图片转存中...(img-EXAo7lo2-1715109825009)]



**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**[需要这份系统化学习资料的朋友,可以戳这里获取](https://bbs.csdn.net/topics/618156601)**

**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值