MultiDex(三)之异步加载优化

作者: 未来的理想 | 来源:发表于2016-12-26 20:52 被阅读889次

    一、前言

    在上一篇文章《Multidex(二)之Dex预加载优化》中我们提到主进程中直接开启一个子线程执行MultiDex的工作确实可以避免ANR的问题,然而此时主进程中调用到的类,可能会因为SecondaryDex的优化尚未完成或者没有被加入到ClassLoader中而导致画面太美不敢看的ClassNotFoundException。如何是好?明知山有虎,偏往虎山行!<br />

    本文就带你实战MultiDex的异步加载优化。

    二、分析

    既然我们要做的是异步加载优化,那毋庸置疑MultiDex.install是要放在子线程了,这步简单。接下来我们分析下ClassNotFoundException的原因,在非主Dex没有被优化、加载到ClassLoader之前引用到了其中的Class,肯定找不到秒秒钟异常给你看。那问题就转换成了下面这两个:

    • 如何保证程序的入口类以及入口类的引用类都在主Dex?
    • 以及在非主Dex类加载的时候如何进行判断干预?

    问题一:在保证主Dex不被撑爆的前提下,我们可以定义一个Task对Gradle打包的流程进行自定义,将程序的入口类以及入口类的引用类都放到主Dex中。
    问题二:在非主Dex类加载的时候进行校验,当异步优化还没完成的时候返回或者加载一个Loading的界面提示用户等待。如何对一个具体的类进行校验呢?看起来比较复杂。换个思路,我们把基类都放到主Dex中,保证非主Dex加载的时候都是调用四大组件,然后进行Hook。这样非主Dex被调用的时候都是通过四大组件来调用的,而基础类都在主Dex已经提前被加载,可以放心调用。

    三、上代码,Show The Code

    Application异步执行Multidex.install;<br />

    public static boolean dexLoadDone;//标示非主Dex有没有被加载成功
    
    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        hookInstrumentation();
        new Thread(){
            @Override
            public void run() {
                super.run();
                //子线程执行,完成后修改标示
                MultiDex.install(ChrApplication.this);
                dexLoadDone = true;
            }
        }.start();
    }
    

    干预打包流程,保证入口类以及入口类的引用类都放在主Dex中。

    build.gradle截图

    如何对四大组件进行Hook?此处分析Activity为例。
    startActivty的时候最终都会调用到Instrumentation.execStartActivity方法
    ContextImpl.java

      @Override
      public void startActivity(Intent intent, Bundle options) {
          warnIfCallingFromSystemProcess();
          if ((intent.getFlags()&Intent.FLAG_ACTIVITY_NEW_TASK) == 0) {
              throw new AndroidRuntimeException(
                      "Calling startActivity() from outside of an Activity "
                      + " context requires the FLAG_ACTIVITY_NEW_TASK flag."
                      + " Is this really what you want?");
          }
          mMainThread.getInstrumentation().execStartActivity(
                  getOuterContext(), mMainThread.getApplicationThread(), null,
                  (Activity) null, intent, -1, options);
      }
    

    <br />

    Activity.java

      public void startActivityForResult(Intent intent, int requestCode, @Nullable Bundle options) {
        if (mParent == null) {
            Instrumentation.ActivityResult ar =
                mInstrumentation.execStartActivity(
                    this, mMainThread.getApplicationThread(), mToken, this,
                    intent, requestCode, options);
            if (ar != null) {
                mMainThread.sendActivityResult(
                    mToken, mEmbeddedID, requestCode, ar.getResultCode(),
                    ar.getResultData());
            }
            if (requestCode >= 0) {
                // If this start is requesting a result, we can avoid making
                // the activity visible until the result is received.  Setting
                // this code during onCreate(Bundle savedInstanceState) or onResume() will keep the
                // activity hidden during this time, to avoid flickering.
                // This can only be done when a result is requested because
                // that guarantees we will get information back when the
                // activity is finished, no matter what happens to it.
                mStartedActivity = true;
            }
    
            cancelInputsAndStartExitTransition(options);
            // TODO Consider clearing/flushing other event sources and events for child windows.
        } else {
            if (options != null) {
                mParent.startActivityFromChild(this, intent, requestCode, options);
            } else {
                // Note we want to go through this method for compatibility with
                // existing applications that may have overridden it.
                mParent.startActivityFromChild(this, intent, requestCode);
            }
        }
    }
    

    追踪Instrumentation对象的来路,是在ActivityThread里的performLaunchActivity方法。那我们就对Instrumentation进行Hook。

      /** 
       ** 最好在Application中调。
       */
      public void hookInstrumentation() {
        try {
            Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
            Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
            currentActivityThreadMethod.setAccessible(true);
            //通过currentActivityThread这个静态的方法获取到ActivityThread的实例对象。
            Object currentActivityThread = currentActivityThreadMethod.invoke(null);
    
            // 拿到原始的 mInstrumentation字段
            Field mInstrumentationField = activityThreadClass.getDeclaredField("mInstrumentation");
            mInstrumentationField.setAccessible(true);
            Instrumentation mInstrumentation = (Instrumentation) mInstrumentationField.get(currentActivityThread);
    
            // 创建代理对象
            Instrumentation chrInstrumentation = new ChrInstrumentation(mInstrumentation);
            // 替换
            mInstrumentationField.set(currentActivityThread, chrInstrumentation);
        } catch (Exception e) {
            Log.i("lz", "hookInstrumentation Exception");
        }
    }
    
    public class ChrInstrumentation  extends Instrumentation{
    // ActivityThread中原始对象,反射需要
    private Instrumentation mBase;
    
    public ChrInstrumentation(Instrumentation base) {
        mBase = base;
    }
    
    @Override
    public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
        if(!ChrApplication.dexLoadDone){
            //没有完成MultiDex的加载,就替换显示Activity。
            className = LoadDexActivity.class.getName();
            Log.i("lz","未完成,重定向");
        }
        return super.newActivity(cl, className, intent);
    }
    
    public ActivityResult execStartActivity(
            Context who, IBinder contextThread, IBinder token, Activity target,
            Intent intent, int requestCode, Bundle options) {
        try {
            //newActivity与execStartActivity两个方法只需要覆写一个即可。
            if (!ChrApplication.dexLoadDone) {
                intent = new Intent(MyApp.myApp, ThirdActivity.class);
            }
            //这个方法是@hide的,反射调用。
            Method execStartActivity = Instrumentation.class.getDeclaredMethod(
                    "execStartActivity",
                    Context.class, IBinder.class, IBinder.class, Activity.class,
                    Intent.class, int.class, Bundle.class);
            execStartActivity.setAccessible(true);
            return (ActivityResult) execStartActivity.invoke(mBase, who,
                    contextThread, token, target, intent, requestCode, options);
        } catch (Exception e) {
            Log.i("lz", e.toString());
            return null;
        }
    }}
    

    此处重写了Instrumentation的两个方法,newActivity与execStartActivity,这两个方法都可以实现我们的需求,具体用哪一个呢?推荐newActivity,此处创建Activity对象,执行顺序靠前。

    四、看效果

    反编译查看方法数以及keep_in_main_dex.txt里的文件是否在主Dex中。


    单Dex方法数不超过设置的48k

    在子线程中Sleep若干秒,快速点击非主Dex中的Activity,制造现场。可以看到打印出来的Log以及,显示的Activity也是LoadDexActivity,实现了拦截。

    五、问题

    1、这个方案的缺点?
    通过以上分析及代码实践,可以看到,这个方案虽然实现了Dex在主进程的子线程中的加载,但是改动量极大;

    • 需要对Gradle打包的过程进行定制,将入口类以及入口类的引用类都放的keep_in_main_dex.txt,这个过程需要用脚本实现,并且经常性的更新、维护;
    • 不同版本Gradle的Api可能会有变化,因此需要额外处理;
    • 本文分析的是对Activity的Hook,而原则上四大组件都需要Hook,时间成本上肯定更长;
    • 还有其他类如记录真实跳转Activity,在异步处理完毕之后跳往真实Activity的逻辑。

    2、推荐方案?
    鉴于问题1中描述的缺点,所以更推荐上一篇文章《Multidex(二)之Dex预加载优化》的方案,使用方便简单。
    <br />
    以上就是MultiDex系列文章的全部三篇,对MultiDex的原理及优化方案进行了分析。

    欢迎关注微信公众号:定期分享Java、Android干货!

    欢迎关注

    相关文章

      网友评论

      • 李云龙_:老哥,那个 Instrumentation 的 execStartActivity() 调用了 ThirdActivity 来加载 dex,那这个调用能保证 加载完 dex 后回到之前要跳转的 Activity吗?就是 ActivityA -> Activity B,中间hook了,还能跳到 ActivityB吗,,
        (ActivityResult) execStartActivity.invoke(mBase, who,
        contextThread, token, target, intent, requestCode, options);
      • 冉桓彬:大佬, 我觉得你的文章跟赵凯强的文章很相似, 而且你引用的代码是美团的解决方案吧? 那为何不把参考文章链接出来呢?
      • f9255619252a:想法very nice,但是方案很鸡肋,只适合学习不能不适用于生产。:smile:

      本文标题:MultiDex(三)之异步加载优化

      本文链接:https://www.haomeiwen.com/subject/kbkhvttx.html