zoukankan      html  css  js  c++  java
  • 插件化框架解读之四大组件调用原理-Activity(三)上篇

    阿里P7移动互联网架构师进阶视频(每日更新中)免费学习请点击:https://space.bilibili.com/474380680

    本文通过Activity调用原理来解读Replugin插件化技术

    一、开启插件Activity流程

    第1步
    开启插件Activity的入口在Replugin.StartActivity(Context context, Intent intent),其他intent内部一定要传入想要开启的插件名。

    public static boolean startActivity(Context context, Intent intent) {
            // TODO 先用旧的开启Activity方案,以后再优化
            ComponentName cn = intent.getComponent();
            if (cn == null) {
                // TODO 需要支持Action方案
                return false;
            }
            String plugin = cn.getPackageName();
            String cls = cn.getClassName();
            return Factory.startActivityWithNoInjectCN(context, intent, plugin, cls, IPluginManager.PROCESS_AUTO);
        }
    

    Factory.startActivityWithNoInjectCN内部调用sPluginManager.startActivity方法,那么sPluginManager是哪里传进去的?
    PMF.java

    Factory.sPluginManager = PMF.getLocal();
    Factory2.sPluginManager = PMF.getInternal();
    

    看下PMF.init的方法可以看到sPluginManager是PMF内部维护的sPluginMgr.mLocal,也就是Pmbase内部的PmLocalmpl mLocal对象,继续跟踪发现真正实现startActivity逻辑的是PmBase内部的PmInternalmpl.startActivity下面这个方法。

    public boolean startActivity(Context context, Intent intent, String plugin, String activity, int process, boolean download) {
            。。。
        }
    

    第2步
    重点走读下上面startActivity方法,首先会判断要跳转的插件是否已经存在了,如果不存在则回调callback接口去提示用户下载等逻辑操作,如果插件状态不正确,则回调外部callback去提示用户插件不可用或者去升级,如果插件首次加载并且是大插件则异步加载并弹窗显示正在加载。然后在调用context.startActivity方法前会去通过下面的loadPluginActivity方法将目标插件Activity class替换为“坑位”Activity,这样其实传给AMS的还是宿主的坑位Activity。
    PmInternalmpl.java

    public boolean startActivity(Context context, Intent intent, String plugin, String activity, int process, boolean download) {
            if (LOG) {
                LogDebug.d(PLUGIN_TAG, "start activity: intent=" + intent + " plugin=" + plugin + " activity=" + activity + " process=" + process + " download=" + download);
            }
    
            // 1. 是否启动下载
            // 若插件不可用(不存在或版本不匹配),则直接弹出“下载插件”对话框
            // 因为已经打开UpdateActivity,故在这里返回True,告诉外界已经打开,无需处理
            if (download) {
                if (PluginTable.getPluginInfo(plugin) == null) {
                    if (LOG) {
                        LogDebug.d(PLUGIN_TAG, "plugin=" + plugin + " not found, start download ...");
                    }
     
                    // 如果用户在下载即将完成时突然点按“取消”,则有可能出现插件已下载成功,但没有及时加载进来的情况
                    // 因此我们会判断这种情况,如果是,则重新加载一次即可,反之则提示用户下载
                    // 原因:“取消”会触发Task.release方法,最终调用mDownloadTask.destroy,导致“下载服务”的Receiver被注销,即使文件下载了也没有回调回来
                    // NOTE isNeedToDownload方法会调用pluginDownloaded再次尝试加载
                    if (isNeedToDownload(context, plugin)) {
                        return RePlugin.getConfig().getCallbacks().onPluginNotExistsForActivity(context, plugin, intent, process);
                    }
                }
            }
              ...
            // 2. 如果插件状态出现问题,则每次弹此插件的Activity都应提示无法使用,或提示升级(如有新版)
            // Added by Jiongxuan Zhang
            if (PluginStatusController.getStatus(plugin) < PluginStatusController.STATUS_OK) {
                if (LOG) {
                    LogDebug.d(PLUGIN_TAG, "PmInternalImpl.startActivity(): Plugin Disabled. pn=" + plugin);
                }
                return RePlugin.getConfig().getCallbacks().onPluginNotExistsForActivity(context, plugin, intent, process);
            }
     
            // 3. 若为首次加载插件,且是“大插件”,则应异步加载,同时弹窗提示“加载中”
            // Added by Jiongxuan Zhang
            if (!RePlugin.isPluginDexExtracted(plugin)) {
                PluginDesc pd = PluginDesc.get(plugin);
                if (pd != null && pd.isLarge()) {
                    ...
                    return RePlugin.getConfig().getCallbacks().onLoadLargePluginForActivity(context, plugin, intent, process);
                }
            }
     
            // WARNING:千万不要修改intent内容,尤其不要修改其ComponentName
            // 因为一旦分配坑位有误(或压根不是插件Activity),则外界还需要原封不动的startActivity到系统中
            // 可防止出现“本来要打开宿主,结果被改成插件”,进而无法打开宿主Activity的问题
     
            // 缓存打开前的Intent对象,里面将包括Action等内容
            Intent from = new Intent(intent);
     
            // 帮助填写打开前的Intent的ComponentName信息(如有。没有的情况如直接通过Action打开等)
            if (!TextUtils.isEmpty(plugin) && !TextUtils.isEmpty(activity)) {
                from.setComponent(new ComponentName(plugin, activity));
            }
     
            //4. 会去将目标Activity替换为坑位Activity
            ComponentName cn = mPluginMgr.mLocal.loadPluginActivity(intent, plugin, activity, process);
            if (cn == null) {
                if (LOG) {
                    LogDebug.d(PLUGIN_TAG, "plugin cn not found: intent=" + intent + " plugin=" + plugin + " activity=" + activity + " process=" + process);
                }
                return false;
            }
     
            // 将Intent指向到“坑位”。这样:
            // from:插件原Intent
            // to:坑位Intent
            intent.setComponent(cn);
     
     
            //5. 调用系统startActivity方法
            context.startActivity(intent);
     
            // 6. 通知外界,已准备好要打开Activity了
            // 其中:from为要打开的插件的Intent,to为坑位Intent
            RePlugin.getConfig().getEventCallbacks().onPrepareStartPitActivity(context, from, intent);
     
            return true;
        }
    

    坑位Activity的替换规格,逻辑是在PmLocalmpl.allocActivityContainer方法中执行,这个有兴趣的可以自己去跟踪下,坑位Activity替换规格,这里就不深入去分析了。
    第3步
    上述几步使用坑位Activity来替换目标Activity之后,AMS内部都是基于这个坑位Activity来实现对其合法性、生命周期进行回调啦,AMS一系列流程走完,然后AMS再通过宿主进程的ActivityThread提供的IApplicationThread Binder代理对象去实现宿主进程中类加载坑位Activity啦,并对其回调相关接口如onCreate。正常startActivity是这样没错,但是我们明明是希望调用的宿主的Activity啊,又不是坑位Activity?是的这里需要对坑位Activity进行恢复真身处理,恢复为目标插件Activity。在哪里恢复呢?没错,就是在我们唯一hook点:RePluginClassLoader中。
    我们在《Replugin插件化技术解读之框架初始化、插件安装与加载(二)》一文中已经分析的很明白,从RePluginClassLoader的loadClass方法中会首先去使用PMF.loadClass去加载插件,去生成插件的Loader对象,
    初始化好插件的DexClassLoader、Resource、PluginContext等资源,然后用插件的ClassLoader去尝试加载目标Activity。具体执行逻辑在PluginProcessPer.resolveActivityClass方法中。

    /**
       * 类加载器根据容器解析到目标的activity
       * @param container
       * @return
       */
      final Class<?> resolveActivityClass(String container) {
          String plugin = null;
          String activity = null;
    
          // 先找登记的,如果找不到,则用forward activity
          PluginContainers.ActivityState state = mACM.lookupByContainer(container);
          if (state == null) {
              // PACM: loadActivityClass, not register, use forward activity, container=
              if (LOGR) {
                  LogRelease.w(PLUGIN_TAG, "use f.a, c=" + container);
              }
              return ForwardActivity.class;
          }
          plugin = state.plugin;
          activity = state.activity;
    
          Plugin p = mPluginMgr.loadAppPlugin(plugin);
          if (p == null) {
              return null;
          }
          ClassLoader cl = p.getClassLoader();
          if (LOG) {
          Class<?> c = null;
          try {
              c = cl.loadClass(activity);
          } catch (Throwable e) {
              if (LOGR) {
                  LogRelease.e(PLUGIN_TAG, e.getMessage(), e);
              }
          }
          return c;
      }
    

    这样我们插件目标Activity就恢复成功,目标Activity内部逻辑就能正常执行啦。
    总结:
    开启插件Activity的大致流程为:
    插件Activity -> Replugin.startActivity -> 解析出插件Activity对应的pluginName -> 挑选坑位Activity替换 ->调用系统startActivity方法 -> AMS回调执行坑位Activity类加载 ->加载并初始化目标插件资源、上下文、类加载器(首次) ->Hook掉ClassLoader的类加载中恢复为插件Activity -> 插件Activity启动,并拥有完整生命周期。

    二、插件内部startActivity流程

    插件中startActiviy有两种场景:调用自己插件本身的Activity或者调用宿主的Activity。显然光凭插件自己是无法成功开启的。因为插件的Manifest根本就没有被AMS识别到呀,必须还是要用统一的Replugin.startActivity接口去按照上述分析的流程重走一遍才行的啊。
    在《Replugin插件化技术解读之目录结构解读》一文中,我们了解到其实插件在动态编译过程中会在字节码层做适量修改,如插件的所有Activity都会被继承replugin-plugin-lib中的PluginActivity这样一个基类。所以我们插件内部其实Activity内部最后执行startActivity都会先走PluginActivity的此方法;
    PluginActivity.java

    static void initLocked(final ClassLoader classLoader) {  
      
                final String factory2 = "com.qihoo360.i.Factory2";  
                final String factory = "com.qihoo360.i.Factory";  
      
                // 初始化Factory2相关方法  
                createActivityContext = new MethodInvoker(classLoader, factory2, "createActivityContext", new Class<?>[]{Activity.class, Context.class});  
                handleActivityCreateBefore = new MethodInvoker(classLoader, factory2, "handleActivityCreateBefore", new Class<?>[]{Activity.class, Bundle.class});  
                handleActivityCreate = new MethodInvoker(classLoader, factory2, "handleActivityCreate", new Class<?>[]{Activity.class, Bundle.class});  
                handleActivityDestroy = new MethodInvoker(classLoader, factory2, "handleActivityDestroy", new Class<?>[]{Activity.class});  
                handleRestoreInstanceState = new MethodInvoker(classLoader, factory2, "handleRestoreInstanceState", new Class<?>[]{Activity.class, Bundle.class});  
                startActivity = new MethodInvoker(classLoader, factory2, "startActivity", new Class<?>[]{Activity.class, Intent.class});  
                startActivityForResult = new MethodInvoker(classLoader, factory2, "startActivityForResult", new Class<?>[]{Activity.class, Intent.class, int.class, Bundle.class});  
      
                // 初始化Factory相关方法  
                loadPluginActivity = new MethodInvoker(classLoader, factory, "loadPluginActivity", new Class<?>[]{Intent.class, String.class, String.class, int.class});  
    

    显然,反射的是Factory2中的startActivity方法,而Factory.startActivity其实就是调用的PmInternalmpl.startActivity方法,显然这个跟上面宿主启动插件Activity对上啦。

    那么RepluginInternal.initLocked方法是什么时候走的呢?容易看到是在RepluginFramework.init中调用,而RepluginFramework.init初始化时在Entry的create方法中,这里是不是很熟悉啦。没错,在《Replugin插件化技术解读之框架初始化、插件安装与加载(二)》一文中我们知道在加载插件的时候会通过回调执行插件的Entry的create方法初始化插件,显然这里这条线又连上啦。

    这样插件内部启动startActivity说白了就是通过反射调用宿主Factory2中startActivity方法去重走了遍上述分析的标准启动插件Activity流程。注意,这里其实仅仅是通过插件Activity继承PluginActivity重写了Activity内部StartActivity方法,改变了Activity内startActivity方法启动步骤。

    我们显然还要修改Context上下文对应的startActivity方法才能保证这里调用的时候也会走到我们设定好的宿主startActivityq启动流程,

    那么插件内部其他startActivity的地方都要进行这样的调整才行,这个就大家自己跟下就能明白了,原理差不多。

    public abstract class PluginActivity extends Activity {
     
        @Override
        protected void attachBaseContext(Context newBase) {
            newBase = RePluginInternal.createActivityContext(this, newBase);
            super.attachBaseContext(newBase);
        }
    

    PluginActivity的attachBaseContext方法中我们会使用RepluginInternal.createActivityContext方法,通过反射,最终吊起来PmInternalImpl.createActivityContext方法。创建对应插件的PluginContext对象,PluginContext前文已经介绍,然后用这个PluginContext替换掉插件内部的上下文BaseContext,其实也就是Activity继承的ContextWrapper类的Context mBase对象。跟踪Android源码很容易发现,ContextWrapper内部封装了startActivity startService等方法,所以这里我们要将这个Context替换掉。那么PluginContext内部又做了什么呢?显然它也重写了startActivity方法。

     @Override
        public void startActivity(Intent intent) {
            if (mContextInjector != null) {
                mContextInjector.startActivityBefore(intent);
            }
     
            super.startActivity(intent);
     
            if (mContextInjector != null) {
                mContextInjector.startActivityAfter(intent);
            }
        }
    

    这里 super.startActivity其实就是对应宿主上下文了,直接走宿主startActivity流程。
    其他还有在PluginApplicationClient为插件创建的Application对象,在PluginApplicationClient.callAttachBaseContext方法中使用反射调用了Application.attachf方法,用PluginContext对象替换了原来的Context,这个就各位看官自己去梳理了哈。

    原文链接https://blog.csdn.net/hellogmm/article/details/79058135
    阿里P7移动互联网架构师进阶视频(每日更新中)免费学习请点击:https://space.bilibili.com/474380680

  • 相关阅读:
    有关获取session属性时报nullPointException(空指针异常)的解决方案
    常用的正则表达式集锦
    使用java实现持续移动的小球
    Java基础系列--包装类
    java中关于类的封装与继承,this、super关键字的使用
    简单了解static
    【01】npm/cnpm安装
    【转】Nodejs学习笔记(二)--- 模块
    【转】Nodejs学习笔记(一)--- 简介及安装Node.js开发环境
    【转】axios的基本使用
  • 原文地址:https://www.cnblogs.com/Android-Alvin/p/11983660.html
Copyright © 2011-2022 走看看