文末
很多人在刚接触这个行业的时候或者是在遇到瓶颈期的时候,总会遇到一些问题,比如学了一段时间感觉没有方向感,不知道该从那里入手去学习,对此我整理了一些资料,需要的可以免费分享给大家
这里笔者分享一份自己收录整理上述技术体系图相关的几十套腾讯、头条、阿里、美团等公司2021年的面试题,把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,这里以图片的形式给大家展示一部分。
【视频教程】
天道酬勤,只要你想,大厂offer并不是遥不可及!希望本篇文章能为你带来帮助,如果有问题,请在评论区留言。
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
4.3 **attachBaseContext**
4.4. installContentProviders
4.5. **Application onCreate**
-
ActivityThread 进入loop循环
-
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 按下编译按钮后发生了什么?
-
打包资源文件,生成R.java文件(使用工具AAPT)
-
处理AIDL文件,生成java代码(没有AIDL则忽略)
-
编译 java 文件,生成对应.class文件(java compiler)
-
.class 文件转换成dex文件(dex)
-
打包成没有签名的apk(使用工具apkbuilder)
-
使用签名工具给apk签名(使用工具Jarsigner)
-
对签名后的.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));
}
}
复制代码
-
反射ClassLoader 的 pathList 字段
-
找到pathList 字段对应的类的
makeDexElements
方法 -
通过
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
-
构造方法通过传入dex路径,创建了
DexPathList
。 -
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添加进去呢? 答案就是反射DexPathList
的dexElements
字段,然后把我们的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行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**