背景
目前來說分唾,對于使用Android Studio的朋友來說鳍寂,MultiDex應該不陌生情龄,就是Google為了解決『65535天花板』問題而給出的官方解決方案捍壤,但是這個方案并不完美鹃觉,所以美團又給出了異步加載Dex文件的方案。今天這篇文章是我最近研究MultiDex方案的一點收獲祷肯,最后還留了一個沒有解決的問題,如果你有思路的話翼闹,歡迎交流蒋纬!
產生65535問題的原因
單個Dex文件中蜀备,method個數(shù)采用使用原生類型short來索引,即4個字節(jié)最多65536個method输虱,field瓷蛙、class的個數(shù)也均有此限制,關于如何解決由于引用過多基礎依賴項目横堡,造成field超過65535問題冠桃,請參考@寒江不釣的這篇文章『當Field邂逅65535』食听。
對于Dex文件,則是將工程所需全部class文件合并且壓縮到一個DEX文件期間葬项,也就是使用Dex工具將class文件轉化為Dex文件的過程中迹蛤, 單個Dex文件可被引用的方法總數(shù)(自己開發(fā)的代碼以及所引用的Android框架盗飒、類庫的代碼)被限制為65536。
這就是65535問題的根本來源蝶溶。
LinearAlloc問題的原因
這個問題多發(fā)生在2.x版本的設備上宣渗,安裝時會提示INSTALL_FAILED_DEXOPT,這個問題發(fā)生在安裝期間摊唇,在使用Dalvik虛擬機的設備上安裝APK時涯鲁,會通過DexOpt工具將Dex文件優(yōu)化為ODex文件,即Optimised Dex岛请,這樣可以提高執(zhí)行效率崇败。
在Android版本不同分別經歷了4M/5M/8M/16M限制肩祥,目前主流4.2.x系統(tǒng)上可能都已到16M混狠, 在Gingerbread或以下系統(tǒng)LinearAllocHdr分配空間只有5M大小的, 高于Gingerbread的系統(tǒng)提升到了8M贡避。Dalvik linearAlloc是一個固定大小的緩沖區(qū)予弧。dexopt使用LinearAlloc來存儲應用的方法信息掖蛤。Android 2.2和2.3的緩沖區(qū)只有5MB,Android 4.x提高到了8MB或16MB致讥。當應用的方法信息過多導致超出緩沖區(qū)大小時彪置,會造成dexopt崩潰拳魁,造成INSTALL_FAILED_DEXOPT錯誤撮弧。
Google提出的MultiDex方案
當App不斷迭代的時候姚糊,總有一天會遇到這個問題救恨,為此Google也給出了解決方案释树,具體的操作步驟我就不多說了奢啥,無非就是配置Application和Gradle文件,下面我們簡單看一下這個方案的實現(xiàn)原理寂纪。
MultiDex實現(xiàn)原理
實際起作用的是下面這個jar包
~/sdk/extras/android/support/multidex/library/libs/android-support-multidex.jar
不管是繼承自MultiDexApplication還是重寫attachBaseContext()赌结,實際都是調用下面的方法
public class MultiDexApplication extends Application {
protected void attachBaseContext(final Context base) {
super.attachBaseContext(base);
MultiDex.install((Context)this);
}
}
下面重點看下MutiDex.install(Context)的實現(xiàn)柬姚,代碼很容易理解量承,重點的地方都有注釋
static {
//第二個Dex文件的文件夾名,實際地址是/date/date/<package_name>/code_cache/secondary-dexes
SECONDARY_FOLDER_NAME = "code_cache" + File.separator + "secondary-dexes";
installedApk = new HashSet<String>();
IS_VM_MULTIDEX_CAPABLE = isVMMultidexCapable(System.getProperty("java.vm.version"));
}
public static void install(final Context context) {
//在使用ART虛擬機的設備上(部分4.4設備焕梅,5.0+以上都默認ART環(huán)境)贞言,已經原生支持多Dex阀蒂,因此就不需要手動支持了
if (MultiDex.IS_VM_MULTIDEX_CAPABLE) {
Log.i("MultiDex", "VM has multidex support, MultiDex support library is disabled.");
return;
}
if (Build.VERSION.SDK_INT < 4) {
throw new RuntimeException("Multi dex installation failed. SDK " + Build.VERSION.SDK_INT + " is unsupported. Min SDK version is " + 4 + ".");
}
try {
final ApplicationInfo applicationInfo = getApplicationInfo(context);
if (applicationInfo == null) {
return;
}
synchronized (MultiDex.installedApk) {
//如果apk文件已經被加載過了蚤霞,就返回
final String apkPath = applicationInfo.sourceDir;
if (MultiDex.installedApk.contains(apkPath)) {
return;
}
MultiDex.installedApk.add(apkPath);
if (Build.VERSION.SDK_INT > 20) {
Log.w("MultiDex", "MultiDex is not guaranteed to work in SDK version " + Build.VERSION.SDK_INT + ": SDK version higher than " + 20 + " should be backed by " + "runtime with built-in multidex capabilty but it's not the " + "case here: java.vm.version=\"" + System.getProperty("java.vm.version") + "\"");
}
ClassLoader loader;
try {
loader = context.getClassLoader();
}
catch (RuntimeException e) {
Log.w("MultiDex", "Failure while trying to obtain Context class loader. Must be running in test mode. Skip patching.", (Throwable)e);
return;
}
if (loader == null) {
Log.e("MultiDex", "Context class loader is null. Must be running in test mode. Skip patching.");
return;
}
try {
//清楚之前的Dex文件夾昧绣,之前的Dex放置在這個文件夾
//final File dexDir = new File(context.getFilesDir(), "secondary-dexes");
clearOldDexDir(context);
}
catch (Throwable t) {
Log.w("MultiDex", "Something went wrong when trying to clear old MultiDex extraction, continuing without cleaning.", t);
}
final File dexDir = new File(applicationInfo.dataDir, MultiDex.SECONDARY_FOLDER_NAME);
//將Dex文件加載為File對象
List<File> files = MultiDexExtractor.load(context, applicationInfo, dexDir, false);
//檢測是否是zip文件
if (checkValidZipFiles(files)) {
//正式安裝其他Dex文件
installSecondaryDexes(loader, dexDir, files);
}
else {
Log.w("MultiDex", "Files were not valid zip files. Forcing a reload.");
files = MultiDexExtractor.load(context, applicationInfo, dexDir, true);
if (!checkValidZipFiles(files)) {
throw new RuntimeException("Zip files were not valid.");
}
installSecondaryDexes(loader, dexDir, files);
}
}
}
catch (Exception e2) {
Log.e("MultiDex", "Multidex installation failure", (Throwable)e2);
throw new RuntimeException("Multi dex installation failed (" + e2.getMessage() + ").");
}
Log.i("MultiDex", "install done");
}
從上面的過程來看,只是完成了加載包含著Dex文件的zip文件删壮,具體的加載操作都在下面的方法中
installSecondaryDexes(loader, dexDir, files);
下面重點看下
private static void installSecondaryDexes(final ClassLoader loader, final File dexDir, final List<File> files) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {
if (!files.isEmpty()) {
if (Build.VERSION.SDK_INT >= 19) {
install(loader, files, dexDir);
}
else if (Build.VERSION.SDK_INT >= 14) {
install(loader, files, dexDir);
}
else {
install(loader, files);
}
}
}
到這里為了完成不同版本的兼容央碟,實際調用了不同類的方法均函,我們僅看一下>=14的版本苞也,其他的類似
private static final class V14
{
private static void install(final ClassLoader loader, final List<File> additionalClassPathEntries, final File optimizedDirectory) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
//通過反射獲取loader的pathList字段,loader是由Application.getClassLoader()獲取的坯认,實際獲取到的是PathClassLoader對象的pathList字段
final Field pathListField = findField(loader, "pathList");
final Object dexPathList = pathListField.get(loader);
//dexPathList是PathClassLoader的私有字段牛哺,里面保存的是Main Dex中的class
//dexElements是一個數(shù)組劳吠,里面的每一個item就是一個Dex文件
//makeDexElements()返回的是其他Dex文件中獲取到的Elements[]對象痒玩,內部通過反射makeDexElements()獲取
//expandFieldArray是為了把makeDexElements()返回的Elements[]對象添加到dexPathList字段的成員變量dexElements中
expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, new ArrayList<File>(additionalClassPathEntries), optimizedDirectory));
}
private static Object[] makeDexElements(final Object dexPathList, final ArrayList<File> files, final File optimizedDirectory) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
final Method makeDexElements = findMethod(dexPathList, "makeDexElements", (Class<?>[])new Class[] { ArrayList.class, File.class });
return (Object[])makeDexElements.invoke(dexPathList, files, optimizedDirectory);
}
}
PathClassLoader.java
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String libraryPath,
ClassLoader parent) {
super(dexPath, null, libraryPath, parent);
}
}
BaseDexClassLoader的代碼如下蠢古,實際上尋找class時,會調用findClass()洽糟,會在pathList中尋找堕战,因此通過反射手動添加其他Dex文件中的class到pathList字段中嘱丢,就可以實現(xiàn)類的動態(tài)加載,這也是MutiDex方案的基本原理汁政。
public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList;
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
Class c = pathList.findClass(name, suppressedExceptions);
if (c == null) {
ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
for (Throwable t : suppressedExceptions) {
cnfe.addSuppressed(t);
}
throw cnfe;
}
return c;
}
}
缺點
通過查看MultiDex的源碼烂完,可以發(fā)現(xiàn)MultiDex在冷啟動時诵棵,因為會同步的反射安裝Dex文件履澳,進行IO操作,容易導致ANR
- 在冷啟動時因為需要安裝Dex文件柄冲,如果Dex文件過大時忠蝗,處理時間過長阁最,很容易引發(fā)ANR
- 采用MultiDex方案的應用因為linearAlloc的BUG,可能不能在2.x設備上啟動
美團的多Dex分包姜盈、動態(tài)異步加載方案
首先我們要明白配阵,美團的這個動態(tài)異步加載方案棋傍,和插件化的動態(tài)加載方案要解決的問題不一樣,我們這里討論的只是單純的為了解決65535問題近上,并且想辦法解決Google的MutiDex方案的弊端拂铡。
多Dex分包
首先感帅,采用Google的方案我們不需要關心Dex分包,開發(fā)工具會自動的分析依賴關系岖是,把需要的class文件及其依賴class文件放在Main Dex中豺撑,因此如果產生了多個Dex文件,那么classes.dex內的方法數(shù)一般都接近65535這個極限爷肝,剩下的class才會被放到Other Dex中陆错。如果我們可以減小Main Dex中的class數(shù)量音瓷,是可以加快冷啟動速度的。
美團給出了Gradle的配置纵竖,但是由于沒有具體的實現(xiàn)杏愤,所以這塊還需要研究声邦。
tasks.whenTaskAdded { task ->
if (task.name.startsWith('proguard') && (task.name.endsWith('Debug') || task.name.endsWith('Release'))) {
task.doLast {
makeDexFileAfterProguardJar();
}
task.doFirst {
delete "${project.buildDir}/intermediates/classes-proguard";
String flavor = task.name.substring('proguard'.length(), task.name.lastIndexOf(task.name.endsWith('Debug') ? "Debug" : "Release"));
generateMainIndexKeepList(flavor.toLowerCase());
}
} else if (task.name.startsWith('zipalign') && (task.name.endsWith('Debug') || task.name.endsWith('Release'))) {
task.doFirst {
ensureMultiDexInApk();
}
}
}
實現(xiàn)Dex自定義分包的關鍵是分析出class之間的依賴關系亥曹,并且干涉Dex文件的生成過程。
Dex也是一個工具骗炉,通過設置參數(shù)可以實現(xiàn)哪一些class文件在Main Dex中蛇受。
afterEvaluate {
tasks.matching {
it.name.startsWith('dex')
}.each { dx ->
if (dx.additionalParameters == null) {
dx.additionalParameters = []
}
dx.additionalParameters += '--multi-dex'
dx.additionalParameters += '--set-max-idx-number=30000'
println("dx param = "+dx.additionalParameters)
dx.additionalParameters += "--main-dex-list=$projectDir/multidex.keep".toString()
}
}
- --multi-dex 代表采用多Dex分包
- --set-max-idx-number=30000 代表每個Dex文件中的最大id數(shù)兢仰,默認是65535把将,通過修改這個值可以減少Main Dex文件的大小和個數(shù)。比如一個App混淆后方法數(shù)為48000请垛,即使開啟MultiDex,也不會產生多個Dex漫拭,如果設置為30000混稽,則就產生兩個Dex文件
- --main-dex-list= 代表在Main Dex中的class文件
需要注意的是,上面我給出的gredle task挑宠,只在1.4以下管用颓影,在1.4+版本的gradle中诡挂,app:dexXXX task 被隱藏了(更多信息請參考Gradle plugin的更新信息)临谱,jacoco, progard, multi-dex三個task被合并了悉默。
The Dex task is not available through the variant API anymore….
The goal of this API is to simplify injecting custom class manipulations without having to deal with tasks, and to offer more flexibility on what is manipulated. The internal code processing (jacoco, progard, multi-dex) have all moved to this new mechanism already in 1.5.0-beta1.
所以通過上面的方法無法對Dex過程進行劫持抄课。這也是我現(xiàn)在還沒有解決的問題,有解決方案的朋友可以指點一下间聊!
異步加載方案
其實前面的操作都是為了這一步操作的抵拘,無論將Dex分成什么樣僵蛛,如果不能異步加載,就解決不了ANR和加載白屏的問題驼壶,所以異步加載是一個重點喉酌。
異步加載主要問題就是:如何避免在其他Dex文件未加載完成時泵喘,造成的ClassNotFoundException問題般妙?
美團給出的解決方案是替換Instrumentation碟渺,但是博客中未給出具體實現(xiàn),我對這個技術點進行了簡單的實現(xiàn)芜繁,Demo在這里MultiDexAsyncLoad骏令,對ActivityThread的反射用的是攜程的解決方案垄提。
首先繼承自Instrumentation铡俐,因為這一塊需要涉及到Activity的啟動過程,所以對這個過程不了解的朋友請看我的這篇文章【凱子哥帶你學Framework】Activity啟動過程全解析吏够。
/**
* Created by zhaokaiqiang on 15/12/18.
*/
public class MeituanInstrumentation extends Instrumentation {
private List<String> mByPassActivityClassNameList;
public MeituanInstrumentation() {
mByPassActivityClassNameList = new ArrayList<>();
}
@Override
public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
if (intent.getComponent() != null) {
className = intent.getComponent().getClassName();
}
boolean shouldInterrupted = !MeituanApplication.isDexAvailable();
if (mByPassActivityClassNameList.contains(className)) {
shouldInterrupted = false;
}
if (shouldInterrupted) {
className = WaitingActivity.class.getName();
} else {
mByPassActivityClassNameList.add(className);
}
return super.newActivity(cl, className, intent);
}
}
至于為什么重寫了newActivity()锅知,是因為在啟動Activity的時候喉镰,會經過這個方法惭笑,所以我們在這里可以進行劫持沉噩,如果其他Dex文件還未異步加載完,就跳轉到Main Dex中的一個等待Activity——WaitingActivity蚜厉。
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
ActivityInfo aInfo = r.activityInfo;
if (r.packageInfo == null) {
r.packageInfo = getPackageInfo(aInfo.applicationInfo, r.compatInfo,
Context.CONTEXT_INCLUDE_CODE);
}
Activity activity = null;
try {
java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
} catch (Exception e) {
}
}
在WaitingActivity中可以一直輪訓昼牛,等待異步加載完成贰健,然后跳轉至目標Activity。
public class WaitingActivity extends BaseActivity {
private Timer timer;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_wait);
waitForDexAvailable();
}
private void waitForDexAvailable() {
final Intent intent = getIntent();
final String className = intent.getStringExtra(TAG_TARGET);
timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
while (!MeituanApplication.isDexAvailable()) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.d("TAG", "waiting");
}
intent.setClassName(getPackageName(), className);
startActivity(intent);
finish();
}
}, 0);
}
@Override
protected void onDestroy() {
super.onDestroy();
if (timer != null) {
timer.cancel();
}
}
}
異步加載Dex文件放在什么時候合適呢?
我放在了Application.onCreate()中
public class MeituanApplication extends Application {
private static final String TAG = "MeituanApplication";
private static boolean isDexAvailable = false;
@Override
public void onCreate() {
super.onCreate();
loadOtherDexFile();
}
private void loadOtherDexFile() {
new Thread(new Runnable() {
@Override
public void run() {
MultiDex.install(MeituanApplication.this);
isDexAvailable = true;
}
}).start();
}
public static boolean isDexAvailable() {
return isDexAvailable;
}
}
那么替換系統(tǒng)默認的Instrumentation在什么時候呢导狡?
當SplashActivity跳轉到MainActivity之后旱捧,再進行替換比較合適看彼,于是
public class MainActivity extends BaseActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
MeituanApplication.attachInstrumentation();
}
}
MeituanApplication.attachInstrumentation()實際就是通過反射替換默認的Instrumentation
public class MeituanApplication extends Application {
public static void attachInstrumentation() {
try {
SysHacks.defineAndVerify();
MeituanInstrumentation meiTuanInstrumentation = new MeituanInstrumentation();
Object activityThread = AndroidHack.getActivityThread();
Field mInstrumentation = activityThread.getClass().getDeclaredField("mInstrumentation");
mInstrumentation.setAccessible(true);
mInstrumentation.set(activityThread, meiTuanInstrumentation);
} catch (Exception e) {
e.printStackTrace();
}
}
}
至此靖榕,異步加載Dex方案的一個基本思路就通了茁计,剩下的就是完善和版本兼容了谓松。
參考資料
- Android 使用android-support-multidex解決Dex超出方法數(shù)的限制問題,讓你的應用不再爆棚
- dex分包變形記
- Android dex分包方案
- 美團Android DEX自動拆包及動態(tài)加載簡介
- 手動分割Dex文件的build.gradle配置
- Multi-dex to rescue from the infamous 65536 methods limit
- secondary-dex-gradle
- Using Gradle to split external libraries in separated dex files to solve Android Dalvik 64k methods limit
關于我
江湖人稱『凱子哥』鬼譬,Android開發(fā)者优质,喜歡技術分享,熱愛開源演怎。