MultiDex解决方案

微信这篇文章给出了一个非常重要的观点:安装完成之后第一次启动时,是secondary.dex的dexopt花费了更多的时间。认识到这点非常重要,使得问题又转化为:在不阻塞UI线程的前提下,完成dexopt,以后都不需要再次dexopt,所以可以在UI线程install dex 了!文章最后给了一个对FB方案的改进版。
仔细读完感觉完全可行。
1.对现有代码改动量最小。
2.该方案不关注Application被哪个组件启动。Activity ,Service ,Receiver ,ContentProvider 都满足。(有个问题要说明:如细心网友指出的那样,新安装还未启动但是收到Receiver的场景下,会导致Load界面出现。这个场景实际出现几率比较少,且仅出现一次。可以接受。) 
3.该方案不限制 Application ,Activity ,Service ,Receiver ,ContentProvider 继续新增业务。
于是实现了这篇文章最后介绍的改进版的方法,稍微有一点点扩充。

流程图如下

方案的流程图


上最终解决问题版的代码!
在Application里面(这里不要再继承自MultiApplication了,我们要手动加载Dex):


import java.util.Map;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
public class App extends Application {
public static final String KEY_DEX2_SHA1 = "dex2-SHA1-Digest";
@Override
protected void attachBaseContext(Context base) {
super .attachBaseContext(base);
LogUtils.d( "loadDex", "App attachBaseContext ");
if (!quickStart() && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {//>=5.0的系统默认对dex进行oat优化
if (needWait(base)){
waitForDexopt(base);
}
MultiDex.install (this );
} else {
return;
}
}
@Override
public void onCreate() {
super .onCreate();
if (quickStart()) {
return;
}
...
}
public boolean quickStart() {
if (StringUtils.contains( getCurProcessName(this), ":mini")) {
LogUtils.d( "loadDex", ":mini start!");
return true;
}
return false ;
}
//neead wait for dexopt ?
private boolean needWait(Context context){
String flag = get2thDexSHA1(context);
LogUtils.d( "loadDex", "dex2-sha1 "+flag);
SharedPreferences sp = context.getSharedPreferences(
PackageUtil.getPackageInfo(context). versionName, MODE_MULTI_PROCESS);
String saveValue = sp.getString(KEY_DEX2_SHA1, "");
return !StringUtils.equals(flag,saveValue);
}
/**
* Get classes.dex file signature
* @param context
* @return
*/
private String get2thDexSHA1(Context context) {
ApplicationInfo ai = context.getApplicationInfo();
String source = ai.sourceDir;
try {
JarFile jar = new JarFile(source);
Manifest mf = jar.getManifest();
Map<String, Attributes> map = mf.getEntries();
Attributes a = map.get("classes2.dex");
return a.getValue("SHA1-Digest");
} catch (Exception e) {
e.printStackTrace();
}
return null ;
}
// optDex finish
public void installFinish(Context context){
SharedPreferences sp = context.getSharedPreferences(
PackageUtil.getPackageInfo(context).versionName, MODE_MULTI_PROCESS);
sp.edit().putString(KEY_DEX2_SHA1,get2thDexSHA1(context)).commit();
}
public static String getCurProcessName(Context context) {
try {
int pid = android.os.Process.myPid();
ActivityManager mActivityManager = (ActivityManager) context
.getSystemService(Context. ACTIVITY_SERVICE);
for (ActivityManager.RunningAppProcessInfo appProcess : mActivityManager
.getRunningAppProcesses()) {
if (appProcess.pid == pid) {
return appProcess. processName;
}
}
} catch (Exception e) {
// ignore
}
return null ;
}
public void waitForDexopt(Context base) {
Intent intent = new Intent();
ComponentName componentName = new
ComponentName( "com.zongwu", LoadResActivity.class.getName());
intent.setComponent(componentName);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
base.startActivity(intent);
long startWait = System.currentTimeMillis ();
long waitTime = 10 * 1000 ;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB_MR1 ) {
waitTime = 20 * 1000 ;//实测发现某些场景下有些2.3版本有可能10s都不能完成optdex
}
while (needWait(base)) {
try {
long nowWait = System.currentTimeMillis() - startWait;
LogUtils.d("loadDex" , "wait ms :" + nowWait);
if (nowWait >= waitTime) {
return;
}
Thread.sleep(200 );
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

PackageUtil的方法

public static PackageInfo getPackageInfo(Context context){
PackageManager pm = context.getPackageManager();
try {
return pm.getPackageInfo(context.getPackageName(), 0);
} catch (PackageManager.NameNotFoundException e) {
LogUtils.e(e.getLocalizedMessage());
}
return new PackageInfo();
}

这里使用了classes(N).dex的方式保存了后面的dex而不是像微信目前的做法放到assest文件夹。前面有说到ART模式会将多个dex优化合并成oat文件。如果放置在asset里面就没有这个好处了。
Launcher Activity 依然是原来的代码里的WelcomeActivity。
在Application启动的时候会检测dexopt是否已经完成过,(检测方式是查看sp文件是否有dex文件的SHA1-Digest记录,这里要两个进程读取该sp,读取模式是MODE_MULTI_PROCESS)。如果没有就启动LoadDexActivity(属于:mini进程) 。否则就直接install dex !对,直接install。通过日志发现,已经dexopt的dex文件再次install的时候 只耗费几十毫秒。
LoadDexActivity 的逻辑比较简单,启动AsyncTask 来install dex 这时候会触发dexopt 。

public class LoadResActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
requestWindowFeature(Window.FEATURE_NO_TITLE);
super .onCreate(savedInstanceState);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN , WindowManager.LayoutParams.FLAG_FULLSCREEN );
overridePendingTransition(R.anim.null_anim, R.anim.null_anim);
setContentView(R.layout.layout_load);
new LoadDexTask().execute();
}
class LoadDexTask extends AsyncTask {
@Override
protected Object doInBackground(Object[] params) {
try {
MultiDex.install(getApplication());
LogUtils.d("loadDex" , "install finish" );
((App) getApplication()).installFinish(getApplication());
} catch (Exception e) {
LogUtils.e("loadDex" , e.getLocalizedMessage());
}
return null;
}
@Override
protected void onPostExecute(Object o) {
LogUtils.d( "loadDex", "get install finish");
finish();
System.exit( 0);
}
}
@Override
public void onBackPressed() {
//cannot backpress
}


Manifest.xml 里面 

<activity
android:name= "com.zongwu.LoadResActivity"
android:launchMode= "singleTask"
android:process= ":mini"
android:alwaysRetainTaskState= "false"
android:excludeFromRecents= "true"
android:screenOrientation= "portrait" />
<activity
android:name= "com.zongwu.WelcomeActivity"
android:launchMode= "singleTop"
android:screenOrientation= "portrait">
<intent-filter >
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter >
</activity>


替换Activity默认的出现动画 R.anim.null_anim 文件的定义:

  1. <set xmlns:android="http://schemas.android.com/apk/res/android">
  2. <alpha
  3. android:fromAlpha="1.0"
  4. android:toAlpha="1.0"
  5. android:duration="550"/>
  6. </set>

微信开发团队的这篇文章所说,application启动了LoadDexActivity之后,自身不再是前台进程所以怎么hold 线程都不会ANR。

系统何时会对apk进行dexopt总悟君其实并没有十分明白。通过查看安装运行的日志发现,安装的时候packageManagerService会对classes.dex 进行dexopt 。在调用MultiDex.install()加载 secondary.dex的时候,也会进行一次dexopt 。 这背后的流程到底是怎样的?dexopt是如何在另外一个进程执行的?如果是另外一个进程执行为何会阻塞主app的UI进程? 官方文档并没有详细介绍这个,那就RTFSC一探究竟吧.

  • MultiDex的问题难点在:要持续解决好几个bug才能最终解决问题。进一步的,想要仔细分辨且解决这些bug,就必须持续探索一些关联性的概念和原理
  • 耗费了这么多时间来解决了Android系统的缺陷是不是有点略伤心。
  • FB的工程师们脑洞好大。思考问题的方式很值得借鉴。
  • 微信团队的文章提到逆向了不少App。


猜你喜欢

转载自blog.csdn.net/zhangcanyan/article/details/79047086