横向浅析Small,RePlugin两个插件化框架(四) RePlugin插件库原理解析

·  阅读 54

RePlugin启动Activity原理

终于来到重头戏了。RePlugin究竟是怎么样启动插件进程,或者说插件启动第二个进程的Activity。让我们先看看RePlugin是如何启动Activity的。 打开方式有两种,第一种直接用包名打开,第二种用别名打开。

RePlugin.startActivity(MainActivity.this, RePlugin.createIntent("com.qihoo360.replugin.sample.demo1", "com.qihoo360.replugin.sample.demo1.MainActivity"));


Intent intent = new Intent();
intent.setComponent(new ComponentName("demo1", "com.qihoo360.replugin.sample.demo1.activity.for_result.ForResultActivity"));
RePlugin.startActivityForResult(MainActivity.this, intent, REQUEST_CODE_DEMO1, null);
复制代码

了解怎么打开。我们这里需要进一步的探究了。 这里先上时序图,这里的时序图稍微有点长,有兴趣的可以跟着我的时序图看看源码。不看也没关系,我这里会挑出重点逐个分析。 RePlugin宿主启动Activity部分.png

根据这个时序图,我们直接看看PluginLibraryInternalProxy中的启动方法。这里只挑出核心方法

startActivity

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

        // Added by Jiongxuan Zhang
        if (PluginStatusController.getStatus(plugin) < PluginStatusController.STATUS_OK) {
            
            return RePlugin.getConfig().getCallbacks().onPluginNotExistsForActivity(context, plugin, intent, process);
        }


        if (!RePlugin.isPluginDexExtracted(plugin)) {
            PluginDesc pd = PluginDesc.get(plugin);
            if (pd != null && pd.isLarge()) {
                
                return RePlugin.getConfig().getCallbacks().onLoadLargePluginForActivity(context, plugin, intent, process);
            }
        }


        Intent from = new Intent(intent);

        // 帮助填写打开前的Intent的ComponentName信息(如有。没有的情况如直接通过Action打开等)
        if (!TextUtils.isEmpty(plugin) && !TextUtils.isEmpty(activity)) {
            from.setComponent(new ComponentName(plugin, activity));
        }

        ComponentName cn = mPluginMgr.mLocal.loadPluginActivity(intent, plugin, activity, process);
        if (cn == null) {
            
            return false;
        }

        // 将Intent指向到“坑位”。这样:
        // from:插件原Intent
        // to:坑位Intent
        intent.setComponent(cn);

        
        context.startActivity(intent);

        // 通知外界,已准备好要打开Activity了
        // 其中:from为要打开的插件的Intent,to为坑位Intent
        RePlugin.getConfig().getEventCallbacks().onPrepareStartPitActivity(context, from, intent);

        return true;
    }
复制代码

省略了版本不一致提供下载回调等。这里做的事情有两件。 第一:还记得我之前构建的基础插件化模型吗?这里实际上是把我们的目标Intent通过RePlugin的占坑(代理)的Activity给包装起来,用于骗过Android系统。

第二,此时开始加载插件中信息的内容,并且拿到要启动对应的类的ComponentName。整个核心的部分在这里:

ComponentName cn = mPluginMgr.mLocal.loadPluginActivity(intent, plugin, activity, process);
        if (cn == null) {
复制代码

loadPluginActivity

public ComponentName loadPluginActivity(Intent intent, String plugin, String activity, int process) {

        ActivityInfo ai = null;
        String container = null;
        PluginBinderInfo info = new PluginBinderInfo(PluginBinderInfo.ACTIVITY_REQUEST);

        try {
            // 获取 ActivityInfo(可能是其它插件的 Activity,所以这里使用 pair 将 pluginName 也返回)
            ai = getActivityInfo(plugin, activity, intent);
            if (ai == null) {
                
                return null;
            }

            // 存储此 Activity 在插件 Manifest 中声明主题到 Intent
            intent.putExtra(INTENT_KEY_THEME_ID, ai.theme);
            

            // 根据 activity 的 processName,选择进程 ID 标识
            if (ai.processName != null) {
                process = PluginClientHelper.getProcessInt(ai.processName);
            }

            // 容器选择(启动目标进程)
            IPluginClient client = MP.startPluginProcess(plugin, process, info);
            if (client == null) {
                return null;
            }

            // 远程分配坑位
            container = client.allocActivityContainer(plugin, process, ai.name, intent);
            
        } catch (Throwable e) {
            
        }

        // 分配失败
        if (TextUtils.isEmpty(container)) {
            return null;
        }

        PmBase.cleanIntentPluginParams(intent);



        PluginIntent ii = new PluginIntent(intent);
        ii.setPlugin(plugin);
        ii.setActivity(ai.name);
        ii.setProcess(IPluginManager.PROCESS_AUTO);
        ii.setContainer(container);
        ii.setCounter(0);
        return new ComponentName(IPC.getPackageName(), container);
    }
复制代码

这里主要做了三件事情。 第一件:getActivityInfo。读取插件中的数据,并且获取插件中要查找的Activity的ActivityInfo。

第二件:startPluginProcess。由于RePlugin有代理Activity有自己的进程,会查看你的Activity中是不是在其他的进程启动,并且分配进程给Activity。

第三件,组成ComponentName返回回去。

这里我们们先看看getActivityInfo的方法。

PluginCommImpl.getActivityInfo

public ActivityInfo getActivityInfo(String plugin, String activity, Intent intent) {
        // 获取插件对象
        Plugin p = mPluginMgr.loadAppPlugin(plugin);
        if (p == null) {
            
            return null;
        }

        ActivityInfo ai = null;

        // activity 不为空时,从插件声明的 Activity 集合中查找
        if (!TextUtils.isEmpty(activity)) {
            ai = p.mLoader.mComponents.getActivity(activity);
        } else {
            // activity 为空时,根据 Intent 匹配
            ai = IntentMatcherHelper.getActivityInfo(mContext, plugin, intent);
        }
        return ai;
    }
复制代码

这里先通过loadAppPlugin加载插件数据,接着在从数据中获取ActivityInfo。这里loadAppPlugin会调用PmBase的loadPlugin

final Plugin loadPlugin(Plugin p, int loadType, boolean useCache) {
        if (p == null) {
            return null;
        }
        if (!p.load(loadType, useCache)) {
            
            return null;
        }
        return p;
    }
复制代码

实际上,这个load的方法就是RePlugin的加载插件数据的核心。RePlugin加载插件数据都会调用这个方法。

Plugin.load

final boolean load(int load, boolean useCache) {
        PluginInfo info = mInfo;
        boolean rc = loadLocked(load, useCache);
        // 尝试在此处调用Application.onCreate方法
        // Added by Jiongxuan Zhang
        if (load == LOAD_APP && rc) {
            callApp();
        }

        if (rc && mInfo != info) {
            UpdateInfoTask task = new UpdateInfoTask((PluginInfo) mInfo.clone());
            Tasks.post2Thread(task);
        }
        return rc;
    }
复制代码

loadLocked加载数据。创建PluginApplicationClient实例。并且异步拷贝插件数据同步到常驻进程中。

loadLocked

private boolean loadLocked(int load, boolean useCache) {

        int status = PluginStatusController.getStatus(mInfo.getName(), mInfo.getVersion());
        if (status < PluginStatusController.STATUS_OK) {
            
            return false;
        }
        if (mInitialized) {
            if (mLoader == null) {
                
                return false;
            }
            if (load == LOAD_INFO) {
                boolean rl = mLoader.isPackageInfoLoaded();
                
                return rl;
            }
            if (load == LOAD_RESOURCES) {
                boolean rl = mLoader.isResourcesLoaded();
                
                return rl;
            }
            if (load == LOAD_DEX) {
                boolean rl = mLoader.isDexLoaded();
                
                return rl;
            }
            boolean il = mLoader.isAppLoaded();
           
            return il;
        }
        mInitialized = true;

        // 若开启了“打印详情”则打印调用栈,便于观察
       ...

        // 这里先处理一下,如果cache命中,省了后面插件提取(如释放Jar包等)操作
        if (useCache) {
            boolean result = loadByCache(load);
            // 如果缓存命中,则直接返回
            if (result) {
                return true;
            }
        }

        Context context = mContext;
        ClassLoader parent = mParent;
        PluginCommImpl manager = mPluginManager;

        //
        String logTag = "try1";
        String lockFileName = String.format(Constant.LOAD_PLUGIN_LOCK, mInfo.getApkFile().getName());
        ProcessLocker lock = new ProcessLocker(context, lockFileName);
        
        if (!lock.tryLockTimeWait(5000, 10)) {
            // 此处仅仅打印错误
            
        }
        //
        long t1 = System.currentTimeMillis();
        boolean rc = doLoad(logTag, context, parent, manager, load);
        
        //
        lock.unlock();
        
        if (!rc) {
            
        }
        if (rc) {

           
            try {
                // 至此,该插件已开始运行
                PluginManagerProxy.addToRunningPluginsNoThrows(mInfo.getName());
            } catch (Throwable e) {
                
            }

            return true;
        }

        //
        logTag = "try2";
       ...

        try {
            // 至此,该插件已开始运行
            PluginManagerProxy.addToRunningPluginsNoThrows(mInfo.getName());
        } catch (Throwable e) {
            
        }

        return true;
    }
复制代码

由于这里是加载插件,所以load的标志为LOAD_APP。这里做的事情,先是获取UI进程中有没有缓存,没有则加上一个进程锁,去跨进程的加载数据。提一句,进程锁的实现是通过文件锁完成的。实际上这个时候加载失败了还会解锁一次,再加载一次。最后加载成果,会进入到插件运行列表并且记录下来。

让我们看看doload这个核心方法。

private final boolean doLoad(String tag, Context context, ClassLoader parent, PluginCommImpl manager, int load) {
        if (mLoader == null) {
            // 试图释放文件
            PluginInfo info = null;
            if (mInfo.getType() == PluginInfo.TYPE_BUILTIN) {
                //
                File dir = context.getDir(Constant.LOCAL_PLUGIN_SUB_DIR, 0);
                File dexdir = mInfo.getDexParentDir();
                String dstName = mInfo.getApkFile().getName();
                boolean rc = AssetsUtils.quickExtractTo(context, mInfo, dir.getAbsolutePath(), dstName, dexdir.getAbsolutePath());
                if (!rc) {
                    // extract built-in plugin failed: plugin=
                    
                    return false;
                }
                File file = new File(dir, dstName);
                info = (PluginInfo) mInfo.clone();
                info.setPath(file.getPath());


                info.setType(PluginInfo.TYPE_PN_INSTALLED);

            } else if (mInfo.getType() == PluginInfo.TYPE_PN_JAR) {
                //
                V5FileInfo v5i = V5FileInfo.build(new File(mInfo.getPath()), mInfo.getV5Type());
                if (v5i == null) {
                    // build v5 plugin info failed: plugin=
                    
                    return false;
                }
                File dir = context.getDir(Constant.LOCAL_PLUGIN_SUB_DIR, 0);
                info = v5i.updateV5FileTo(context, dir, true, true);
                if (info == null) {
                    // update v5 file to failed: plugin=
                    
                    return false;
                }
                // 检查是否改变了?
                if (info.getLowInterfaceApi() != mInfo.getLowInterfaceApi() || info.getHighInterfaceApi() != mInfo.getHighInterfaceApi()) {
                    
                    // 看看目标文件是否存在
                    String dstName = mInfo.getApkFile().getName();
                    File file = new File(dir, dstName);
                    if (!file.exists()) {

                        return false;
                    }
                    // 重新构造
                    info = PluginInfo.build(file);
                    if (info == null) {
                        return false;
                    }
                }

            } else {
                //
            }

            //
            if (info != null) {
                // 替换
                mInfo = info;
            }

            //
            mLoader = new Loader(context, mInfo.getName(), mInfo.getPath(), this);
            if (!mLoader.loadDex(parent, load)) {
                return false;
            }

            // 设置插件为“使用过的”
            // 注意,需要重新获取当前的PluginInfo对象,而非使用“可能是新插件”的mInfo
            try {
                PluginManagerProxy.updateUsedIfNeeded(mInfo.getName(), true);
            } catch (RemoteException e) {
                // 同步出现问题,但仍继续进行
                
            }

            // 若需要加载Dex,则还同时需要初始化插件里的Entry对象
            if (load == LOAD_APP) {
                // NOTE Entry对象是可以在任何线程中被调用到
                if (!loadEntryLocked(manager)) {
                    return false;
                }
                // NOTE 在此处调用则必须Post到UI,但此时有可能Activity已被加载
                //      会出现Activity.onCreate比Application更早的情况,故应放在load外面立即调用
                // callApp();
            }
        }

        if (load == LOAD_INFO) {
            return mLoader.isPackageInfoLoaded();
        } else if (load == LOAD_RESOURCES) {
            return mLoader.isResourcesLoaded();
        } else if (load == LOAD_DEX) {
            return mLoader.isDexLoaded();
        } else {
            return mLoader.isAppLoaded();
        }
    }
复制代码

RePlugin先判断Loader也就是Plugin的加载器是否为空。第一次进来先是为空。这里会判断当前的记载的插件是什么。是内部插件还是jar包?是这两种则获取内部信息更新插件信息。核心不是这里,这个方法的核心有两个。

第一:loadDex加载插件中dex的数据,为插件生成对应的类加载器,Context等。

第二:启动插件内部插件框架。

我们一个个来看对应的方法。

Loader.loadDex

final boolean loadDex(ClassLoader parent, int load) {
        try {
            PackageManager pm = mContext.getPackageManager();

            mPackageInfo = Plugin.queryCachedPackageInfo(mPath);
            if (mPackageInfo == null) {
                // PackageInfo
                mPackageInfo = pm.getPackageArchiveInfo(mPath,
                        PackageManager.GET_ACTIVITIES | PackageManager.GET_SERVICES | PackageManager.GET_PROVIDERS | PackageManager.GET_RECEIVERS | PackageManager.GET_META_DATA);
                if (mPackageInfo == null || mPackageInfo.applicationInfo == null) {
                    
                    mPackageInfo = null;
                    return false;
                }
                
                mPackageInfo.applicationInfo.sourceDir = mPath;
                mPackageInfo.applicationInfo.publicSourceDir = mPath;

                if (TextUtils.isEmpty(mPackageInfo.applicationInfo.processName)) {
                    mPackageInfo.applicationInfo.processName = mPackageInfo.applicationInfo.packageName;
                }

                // 添加针对SO库的加载

                PluginInfo pi = mPluginObj.mInfo;
                File ld = pi.getNativeLibsDir();
                mPackageInfo.applicationInfo.nativeLibraryDir = ld.getAbsolutePath();

             }

                // 缓存表: pkgName -> pluginName
                synchronized (Plugin.PKG_NAME_2_PLUGIN_NAME) {
                    Plugin.PKG_NAME_2_PLUGIN_NAME.put(mPackageInfo.packageName, mPluginName);
                }

                // 缓存表: pluginName -> fileName
                synchronized (Plugin.PLUGIN_NAME_2_FILENAME) {
                    Plugin.PLUGIN_NAME_2_FILENAME.put(mPluginName, mPath);
                }

                // 缓存表: fileName -> PackageInfo
                synchronized (Plugin.FILENAME_2_PACKAGE_INFO) {
                    Plugin.FILENAME_2_PACKAGE_INFO.put(mPath, new WeakReference<PackageInfo>(mPackageInfo));
                }
            }


            if (mPluginObj.mInfo.getFrameworkVersion() == PluginInfo.FRAMEWORK_VERSION_UNKNOWN) {
                mPluginObj.mInfo.setFrameworkVersionByMeta(mPackageInfo.applicationInfo.metaData);
                // 只有“P-n”插件才会到这里,故无需调用“纯APK”的保存功能
                // PluginInfoList.save();
            }

            // 创建或获取ComponentList表
            // Added by Jiongxuan Zhang
            mComponents = Plugin.queryCachedComponentList(mPath);
            if (mComponents == null) {
                // ComponentList
                mComponents = new ComponentList(mPackageInfo, mPath, mPluginObj.mInfo);

                // 动态注册插件中声明的 receiver
                regReceivers();

                synchronized (Plugin.FILENAME_2_COMPONENT_LIST) {
                    Plugin.FILENAME_2_COMPONENT_LIST.put(mPath, new WeakReference<>(mComponents));
                }


                adjustPluginProcess(mPackageInfo.applicationInfo);


                adjustPluginTaskAffinity(mPluginName, mPackageInfo.applicationInfo);
            }

            if (load == Plugin.LOAD_INFO) {
                return isPackageInfoLoaded();
            }

            mPkgResources = Plugin.queryCachedResources(mPath);

            if (mPkgResources == null) {
                // Resources
                try {
                    if (BuildConfig.DEBUG) {

                    } else {
                        mPkgResources = pm.getResourcesForApplication(mPackageInfo.applicationInfo);
                    }
                } catch (NameNotFoundException e) {
                    
                    return false;
                }
                if (mPkgResources == null) {
                    
                    return false;
                }
                

                // 缓存表: Resources
                synchronized (Plugin.FILENAME_2_RESOURCES) {
                    Plugin.FILENAME_2_RESOURCES.put(mPath, new WeakReference<>(mPkgResources));
                }
            }
            if (load == Plugin.LOAD_RESOURCES) {
                return isResourcesLoaded();
            }

            mClassLoader = Plugin.queryCachedClassLoader(mPath);
            if (mClassLoader == null) {
                // ClassLoader
                String out = mPluginObj.mInfo.getDexParentDir().getPath();
                //changeDexMode(out);

                //
                if (BuildConfig.DEBUG) {
                   
                } else {
                    // 线上环境保持不变
                    parent = getClass().getClassLoader().getParent(); // TODO: 这里直接用父类加载器
                }
                String soDir = mPackageInfo.applicationInfo.nativeLibraryDir;

                long begin = 0;
                boolean isDexExist = false;

                

                mClassLoader = RePlugin.getConfig().getCallbacks().createPluginClassLoader(mPluginObj.mInfo, mPath, out, soDir, parent);


                if (mClassLoader == null) {
                    
                    return false;
                }

                

                // 缓存表:ClassLoader
                synchronized (Plugin.FILENAME_2_DEX) {
                    Plugin.FILENAME_2_DEX.put(mPath, new WeakReference<>(mClassLoader));
                }
            }
            if (load == Plugin.LOAD_DEX) {
                return isDexLoaded();
            }

            // Context
            mPkgContext = new PluginContext(mContext, android.R.style.Theme, mClassLoader, mPkgResources, mPluginName, this);
            

        } catch (Throwable e) {
           
            return false;
        }

        return true;
    }
复制代码

这里有一段方法我们必须注意:

 mPackageInfo = pm.getPackageArchiveInfo(mPath,
                        PackageManager.GET_ACTIVITIES | PackageManager.GET_SERVICES | PackageManager.GET_PROVIDERS | PackageManager.GET_RECEIVERS | PackageManager.GET_META_DATA);
复制代码

如果写过插件换肤框架都知道。这个方法是能够获取对应路径下apk文件的内部信息。根据后面的标志,会取出对应数据填入PackageInfo 。为什么之前我的插件化基础框架并没有使用这种方式呢?实际上这中思想就是完全把资源交给自己管理,而我们平时所考虑的,往往想要Android能够帮我们完成一大部分的内容,毕竟如果完全拿出来管理,会发现整个插件框架很沉重,而且反射的地方又不会太多的减少,大大的增加不稳定性。

这里面的思想大致就是根据读取的模式来读取PackageInfo下的数据,并且缓存到对应的集合中,以待下次获取。

这里做了很重要的一步,获取了插件的信息,并通过这些信息如apk路径,在UI进程为插件生成了对应的ClassLoader也即是PluginDexClassLoader,生成对应的Context也即是PluginContext,这些全部保存在Plugin类的Loader对象中。为之后启动插件框架做准备。这里我先不放出PluginDexClassLoader中的逻辑,先埋个伏笔,到下面和RePluginClassLoader一起分析。

loadEntryLocked

private boolean loadEntryLocked(PluginCommImpl manager) {
        if (mDummyPlugin) {
            ...
        } else {
            
            if (mLoader.loadEntryMethod2()) {
                ...
            } else if (mLoader.loadEntryMethod(false)) {
                ...
            } else if (mLoader.loadEntryMethod3()) {
                if (!mLoader.invoke2(manager)) {
                    return false;
                }
            } else {
                
                return false;
            }
        }
        return true;
    }
复制代码

这里我们只挑出我们需要关注的最新版本对应的分支。看看这个mLoader.loadEntryMethod3()究竟做了什么?

 /**
     * 新版SDK(RePlugin-library)插件入口报名前缀
     * 在插件中,该包名不能混淆
     */
    public static final String REPLUGIN_LIBRARY_ENTRY_PACKAGE_PREFIX = "com.qihoo360.replugin";

    /**
     * 插件的入口类
     * 在插件中,该名字不能混淆
     * @hide 内部框架使用
     */
    public static final String PLUGIN_ENTRY_CLASS_NAME = "Entry";

/**
     * 插件的入口类导出函数
     * 在插件中,该方法名不能混淆
     * 通过该函数创建IPlugin对象
     * @hide 内部框架使用
     */
    public static final String PLUGIN_ENTRY_EXPORT_METHOD_NAME = "create";

final boolean loadEntryMethod3() {
        //
        try {
            String className = Factory.REPLUGIN_LIBRARY_ENTRY_PACKAGE_PREFIX + "." + Factory.PLUGIN_ENTRY_CLASS_NAME;
            Class<?> c = mClassLoader.loadClass(className);
            
            mCreateMethod2 = c.getDeclaredMethod(Factory.PLUGIN_ENTRY_EXPORT_METHOD_NAME, Factory.PLUGIN_ENTRY_EXPORT_METHOD2_PARAMS);
        } catch (Throwable e) {
            
        }
        return mCreateMethod2 != null;
    }
复制代码

这个方法究竟在干啥?根据反射的String看看究竟调用了什么方法:

com.qihoo360.replugin.Entry.create( Context context, 
ClassLoader classloader, IBinder IBinder)
复制代码

你会发现,找遍了整个host-library你都找不到这个方法。但是这个mClassLoader仔细的读者会发现实际上是指刚才生成的PluginDexClassLoader。换句话说,我们想要反射的是插件也就是plugin-library中的类。

我这里整理一下这里面的思路。

看到这里估计聪明的读者大概猜到后续的思路。实际上RePlugin实际上就是为插件生成一个ClassLoader,为自己创造一个classloader。在宿主想要启动插件的class的时候,会先通过自己classloader,用插件的classloader去查找对应的类就能找到我们想要的类。这样就做到了RePlugin所说的最小入侵。

继续回来。回到原来的思路,当我们知道怎么查找类的,让我们看看假如我们要跨进程启动Activity又是如何。还记得我上面说的MP. startPluginProcess方法吗?我们进去看看。

MP.startPluginProcess

public static final IPluginClient startPluginProcess(String plugin, int process, PluginBinderInfo info) throws RemoteException {
        return PluginProcessMain.getPluginHost().startPluginProcess(plugin, process, info);
    }
复制代码

还记得这个getPluginHost,此时我们在UI进程就是指的是AIDL的远程常驻进程PmHostSvc的代理类。

startPluginProcessLocked

final IPluginClient startPluginProcessLocked(String plugin, int process, PluginBinderInfo info) {
        

        // 强制使用UI进程
        if (Constant.ENABLE_PLUGIN_ACTIVITY_AND_BINDER_RUN_IN_MAIN_UI_PROCESS) {
            if (info.request == PluginBinderInfo.ACTIVITY_REQUEST) {
                if (process == IPluginManager.PROCESS_AUTO) {
                    process = IPluginManager.PROCESS_UI;
                }
            }
            if (info.request == PluginBinderInfo.BINDER_REQUEST) {
                if (process == IPluginManager.PROCESS_AUTO) {
                    process = IPluginManager.PROCESS_UI;
                }
            }
        }

        //
        PluginProcessMain.schedulePluginProcessLoop(PluginProcessMain.CHECK_STAGE1_DELAY);

        // 获取
        IPluginClient client = PluginProcessMain.probePluginClient(plugin, process, info);
        if (client != null) {
            
            return client;
        }

        // 分配
        int index = IPluginManager.PROCESS_AUTO;
        try {
            index = PluginProcessMain.allocProcess(plugin, process);
           
        } catch (Throwable e) {
           
        }
        // 分配的坑位不属于UI、和自定义进程,就返回。
        if (!(index == IPluginManager.PROCESS_UI
                || PluginProcessHost.isCustomPluginProcess(index)
                || PluginManager.isPluginProcess(index))) {
            return null;
        }

        // 启动
        boolean rc = PluginProviderStub.proxyStartPluginProcess(mContext, index);
        
        if (!rc) {
            return null;
        }

        // 再次获取
        client = PluginProcessMain.probePluginClient(plugin, process, info);
        if (client == null) {
            
            return null;
        }

        

        return client;
    }
复制代码

RePlugin的IPluginClient是被PluginProcessPer实现的。这里将会去常驻进程查看有没有已经可以分配好的进程,已经分配好的说明这个进程已经启动就没有必要启动。

假如没有启动进程就通过下面的方法分配进程。

index = PluginProcessMain.allocProcess(plugin, process);
复制代码

真正启动进程的方法是下面


    static final boolean proxyStartPluginProcess(Context context, int index) {
        //
        ContentValues values = new ContentValues();
        values.put(KEY_METHOD, METHOD_START_PROCESS);
        values.put(KEY_COOKIE, PMF.sPluginMgr.mLocalCookie);
        Uri uri = context.getContentResolver().insert(ProcessPitProviderBase.buildUri(index), values);
        
        if (uri == null) {
            
            return false;
        }
        return true;
    }
复制代码

那究竟是怎么分配进程的呢? ProcessPitProviderBase这个是一个基础类。实际上我们会根据要启动的进程分配一下几种内容提供器。 不同进程的内容提供器.png

这样就是在这几个范围内加载RePlugin已经给予的进程坑位。当然这里面也涉及到了自己定义的进程Activity的情况,也会被RePlugin的Gradle插件修改RePlugin的坑位。

这就是RePlugin所说的这个框架本身支持多进程。

好的多进程大致上如何搭起来也明白,其他详细就有有好奇心的读者研究吧。这里篇幅原因就不继续分析。有了这些信息也能很好的明白其中的原理。

当RePlugin分配好了进程,就要调用context.startActivity的方法,跑到RePluginClassLoader查找类。

RePluginClassLoader和PluginDexClassLoader的分析比较。

这里我们其实只需要看两者的loadclass方法。

RePluginClassLoader

public class RePluginClassLoader extends PathClassLoader {

    private static final String TAG = "RePluginClassLoader";

    private final ClassLoader mOrig;

    /**
     * 用load系列代替
     */
    //private Method findClassMethod;

    private Method findResourceMethod;

    private Method findResourcesMethod;

    private Method findLibraryMethod;

    private Method getPackageMethod;

    public RePluginClassLoader(ClassLoader parent, ClassLoader orig) {


        super("", "", parent);
        mOrig = orig;


        copyFromOriginal(orig);

        initMethods(orig);
    }

    private void initMethods(ClassLoader cl) {
        Class<?> c = cl.getClass();
        findResourceMethod = ReflectUtils.getMethod(c, "findResource", String.class);
        findResourceMethod.setAccessible(true);
        findResourcesMethod = ReflectUtils.getMethod(c, "findResources", String.class);
        findResourcesMethod.setAccessible(true);
        findLibraryMethod = ReflectUtils.getMethod(c, "findLibrary", String.class);
        findLibraryMethod.setAccessible(true);
        getPackageMethod = ReflectUtils.getMethod(c, "getPackage", String.class);
        getPackageMethod.setAccessible(true);
    }

    private void copyFromOriginal(ClassLoader orig) {
       

        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.GINGERBREAD_MR1) {
            // Android 2.2 - 2.3.7,有一堆字段,需要逐一复制
            // 以下方法在较慢的手机上用时:8ms左右
            copyFieldValue("libPath", orig);
            copyFieldValue("libraryPathElements", orig);
            copyFieldValue("mDexs", orig);
            copyFieldValue("mFiles", orig);
            copyFieldValue("mPaths", orig);
            copyFieldValue("mZips", orig);
        } else {
            // Android 4.0以上只需要复制pathList即可
            // 以下方法在较慢的手机上用时:1ms
            copyFieldValue("pathList", orig);
        }
    }

    private void copyFieldValue(String field, ClassLoader orig) {
        try {
            Field f = ReflectUtils.getField(orig.getClass(), field);
            if (f == null) {
                
                return;
            }

            // 删除final修饰符
            ReflectUtils.removeFieldFinalModifier(f);

            // 复制Field中的值到this里
            Object o = ReflectUtils.readField(f, orig);
            ReflectUtils.writeField(f, this, o);

            
        } catch (IllegalAccessException e) {
            
        }
    }

    @Override
    protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
        //
        Class<?> c = null;
        c = PMF.loadClass(className, resolve);
        if (c != null) {
            return c;
        }
        //
        try {
            c = mOrig.loadClass(className);
            
            return c;
        } catch (Throwable e) {
            //
        }
        //
        return super.loadClass(className, resolve);
    }



    @Override
    protected Package getPackage(String name) {

        // SONGZHAOCHUN, 2016/02/29
        if (name != null && !name.isEmpty()) {
            Package pack = null;
            try {
                pack = (Package) getPackageMethod.invoke(mOrig, name);
            } catch (IllegalArgumentException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
            if (pack == null) {
                
                pack = super.getPackage(name);
            }
            if (pack == null) {
                
                return definePackage(name, "Unknown", "0.0", "Unknown", "Unknown", "0.0", "Unknown", null);
            }
            return pack;
        }
        return null;
    }
}
复制代码

首先这个ClassLoader是继承于PathClassLoader。这个classloader在Android内部是用来加载已经安装好的apk的dex文件。这个思想实际上使用的是我之前说的保守方案。

实际上着相当于一个原来宿主的classloader代理。我们将宿主的classloader的信息,pathList等注入到该classloader中。让这个classloader可以正常寻找宿主的类。这么做的目的就是为了下面这个方法

PmBase.loadClass

 @Override
    protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
        //
        Class<?> c = null;
        c = PMF.loadClass(className, resolve);
        if (c != null) {
            return c;
        }
        //
        try {
            c = mOrig.loadClass(className);
            
            return c;
        } catch (Throwable e) {
            //
        }
        //
        return super.loadClass(className, resolve);
    }
复制代码

在调用原来的classloader加载class之前,我们想要先去查找插件有没有这个类,有则直接返回。我们看看这个loadclass就是做了什么。这里我只取出关键的获取Activity类的片段

final Class<?> loadClass(String className, boolean resolve) {
        // 加载Service中介坑位
        if (className.startsWith(PluginPitService.class.getName())) {
            
            return PluginPitService.class;
        }

        //
        if (mContainerActivities.contains(className)) {
            Class<?> c = mClient.resolveActivityClass(className);
            if (c != null) {
                return c;
            }

            
            return DummyActivity.class;
        }

        ...

        //
        return loadDefaultClass(className);
    }
复制代码

关键方法在

mClient.resolveActivityClass(className);
复制代码

PluginProcessPer.resolveActivityClass

此时RePlugin会调用UI进程的resolveActivityClass方法。

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=
            
            return ForwardActivity.class;
        }
        plugin = state.plugin;
        activity = state.activity;

        

        Plugin p = mPluginMgr.loadAppPlugin(plugin);
        if (p == null) {
            // PACM: loadActivityClass, not found plugin
            
            return null;
        }

        ClassLoader cl = p.getClassLoader();
        
        Class<?> c = null;
        try {
            c = cl.loadClass(activity);
        } catch (Throwable e) {
            
        }
        

        return c;
    }
复制代码

我们先从本地缓存中寻找在进程初始化的存在的对应启动模式的坑位。接着获取出样板的Plugin,在从loadAppPlugin方法读取。这个方法还记得吧,不过这个时候已经有了缓存,直接获取缓存即可。加载了插件的数据,并且生成插件对应的classloader和context。

由于这个classloader是使用apk对应路径生成的,所以可以找到插件类的类,并且成功返回。

PluginDexClassLoader

我们看看这个给予插件的classloader是否有什么特殊。

public class PluginDexClassLoader extends DexClassLoader {

    private static final String TAG = "PluginDexClassLoader";

    private final ClassLoader mHostClassLoader;

    private static Method sLoadClassMethod;


    public PluginDexClassLoader(PluginInfo pi, String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
        super(dexPath, optimizedDirectory, librarySearchPath, parent);

        installMultiDexesBeforeLollipop(pi, dexPath, parent);

        mHostClassLoader = RePluginInternal.getAppClassLoader();

        initMethods(mHostClassLoader);
    }

    private static void initMethods(ClassLoader cl) {
        Class<?> clz = cl.getClass();
        if (sLoadClassMethod == null) {
            sLoadClassMethod = ReflectUtils.getMethod(clz, "loadClass", String.class, Boolean.TYPE);
            if (sLoadClassMethod == null) {
                throw new NoSuchMethodError("loadClass");
            }
        }
    }

    @Override
    protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {

        Class<?> pc = null;
        ClassNotFoundException cnfException = null;
        try {
            pc = super.loadClass(className, resolve);
            if (pc != null) {
                // 只有开启“详细日志”才会输出,防止“刷屏”现象
                
                return pc;
            }
        } catch (ClassNotFoundException e) {
            // Do not throw "e" now
            cnfException = e;
        }


        if (RePlugin.getConfig().isUseHostClassIfNotFound()) {
            try {
                return loadClassFromHost(className, resolve);
            } catch (ClassNotFoundException e) {
                // Do not throw "e" now
                cnfException = e;
            }
        }

        if (cnfException != null) {
            throw cnfException;
        }
        return null;
    }

    private Class<?> loadClassFromHost(String className, boolean resolve) throws ClassNotFoundException {
        Class<?> c;
        try {
            c = (Class<?>) sLoadClassMethod.invoke(mHostClassLoader, className, resolve);

           
        } catch (IllegalAccessException e) {
            throw new ClassNotFoundException("Calling the loadClass method failed (IllegalAccessException)", e);
        } catch (InvocationTargetException e) {

            throw new ClassNotFoundException("Calling the loadClass method failed (InvocationTargetException)", e);
        }
        return c;
    }


    private void installMultiDexesBeforeLollipop(PluginInfo pi, String dexPath, ClassLoader parent) {

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            return;
        }

        try {

            // get paths of extra dex
            List<File> dexFiles = getExtraDexFiles(pi, dexPath);

            if (dexFiles != null && dexFiles.size() > 0) {

                List<Object[]> allElements = new LinkedList<>();

                // get dexElements of main dex
                Class<?> clz = Class.forName("dalvik.system.BaseDexClassLoader");
                Object pathList = ReflectUtils.readField(clz, this, "pathList");
                Object[] mainElements = (Object[]) ReflectUtils.readField(pathList.getClass(), pathList, "dexElements");
                allElements.add(mainElements);

                String optimizedDirectory = pi.getExtraOdexDir().getAbsolutePath();

                for (File file : dexFiles) {
                   

                    DexClassLoader dexClassLoader = new DexClassLoader(file.getAbsolutePath(), optimizedDirectory, optimizedDirectory, parent);

                    Object obj = ReflectUtils.readField(clz, dexClassLoader, "pathList");
                    Object[] dexElements = (Object[]) ReflectUtils.readField(obj.getClass(), obj, "dexElements");
                    allElements.add(dexElements);
                }

                // combine Elements
                Object combineElements = combineArray(allElements);

                // rewrite Elements combined to classLoader
                ReflectUtils.writeField(pathList.getClass(), pathList, "dexElements", combineElements);

                // delete extra dex, after optimized
                FileUtils.forceDelete(pi.getExtraDexDir());

                //Test whether the Extra Dex is installed
                
            }

        } catch (Exception e) {
            e.printStackTrace();
        }

    }


...

}
复制代码

实际上我们只要关注到loadClass还是原来的那个。还能了解到这个classloader还具备着寻找宿主的类,只是默认这个选项是关闭的。

通过这些步骤,RePlugin就能启动对应的类了。但是又是怎么找到资源的呢?接下来就到了插件库的登场。

Plugin-Library插件库

还记得我之前说的Host库中通过反射,获取了插件库中的方法。我们看看对于插件库中,Entry.create方法都传了什么参数。

final boolean invoke2(PluginCommImpl x) {
        try {
            IBinder manager = null; // TODO
            IBinder b = (IBinder) mCreateMethod2.invoke(null, mPkgContext, getClass().getClassLoader(), manager);
            if (b == null) {
                
                return false;
            }
            mBinderPlugin = new ProxyPlugin(b);
            mPlugin = mBinderPlugin;
            
        } catch (Throwable e) {
            
            return false;
        }
        return true;
    }
复制代码

这里mPkgContext实际上是上面生成的PluginContext,并且把当前宿主的classloader传递过去。通过这个方法生成了AIDL中Plugin远程代理类。由于这里是多进程框架的分析,所以这个AIDL起到了恰到好处,我们可以通过远程代理类来和插件进程进行交互。

接下来让我们正式的看看这个方法Entry.create究竟做了什么。

public static final IBinder create(Context context, ClassLoader cl, IBinder manager) {
        // 初始化插件框架
        RePluginFramework.init(cl);
        // 初始化Env
        RePluginEnv.init(context, cl, manager);

        return new IPlugin.Stub() {
            @Override
            public IBinder query(String name) throws RemoteException {
                return RePluginServiceManager.getInstance().getService(name);
            }
        };
    }
复制代码

这里做了两件事情。 第一,初始化了插件框架,通过宿主传进来的classloader,初始化好所有所有调用宿主RePlugin宿主库的反射方法。

第二,保存context,classloader等数据,并且启动插件库的服务管理器。

到这里是不是就感觉结束了,并没有做什么动作也没有启动什么东西。感觉源码读不下去了。

其实这里RePlugin的Gradle插件还做了一件事情。我们看看LoaderActivityInjector中的

    def private static loaderActivityRules = [
            'android.app.Activity'                    : 'com.qihoo360.replugin.loader.a.PluginActivity',
            'android.app.TabActivity'                 : 'com.qihoo360.replugin.loader.a.PluginTabActivity',
            'android.app.ListActivity'                : 'com.qihoo360.replugin.loader.a.PluginListActivity',
            'android.app.ActivityGroup'               : 'com.qihoo360.replugin.loader.a.PluginActivityGroup',
            'android.support.v4.app.FragmentActivity' : 'com.qihoo360.replugin.loader.a.PluginFragmentActivity',
            'android.support.v7.app.AppCompatActivity': 'com.qihoo360.replugin.loader.a.PluginAppCompatActivity',
            'android.preference.PreferenceActivity'   : 'com.qihoo360.replugin.loader.a.PluginPreferenceActivity',
            'android.app.ExpandableListActivity'      : 'com.qihoo360.replugin.loader.a.PluginExpandableListActivity'
    ]

private def handleActivity(ClassPool pool, String activity, String classesDir) {
        def clsFilePath = classesDir + File.separatorChar + activity.replaceAll('\\.', '/') + '.class'
        if (!new File(clsFilePath).exists()) {
            return
        }

        println ">>> Handle $activity"

        def stream, ctCls
        try {
            stream = new FileInputStream(clsFilePath)
            ctCls = pool.makeClass(stream);
/*
             // 打印当前 Activity 的所有父类
            CtClass tmpSuper = ctCls.superclass
            while (tmpSuper != null) {
                println(tmpSuper.name)
                tmpSuper = tmpSuper.superclass
            }
*/
            // ctCls 之前的父类
            def originSuperCls = ctCls.superclass

            /* 从当前 Activity 往上回溯,直到找到需要替换的 Activity */
            def superCls = originSuperCls
            while (superCls != null && !(superCls.name in loaderActivityRules.keySet())) {
                // println ">>> 向上查找 $superCls.name"
                ctCls = superCls
                superCls = ctCls.superclass
            }

            // 如果 ctCls 已经是 LoaderActivity,则不修改
            if (ctCls.name in loaderActivityRules.values()) {
                // println "    跳过 ${ctCls.getName()}"
                return
            }

            /* 找到需要替换的 Activity, 修改 Activity 的父类为 LoaderActivity */
            if (superCls != null) {
                def targetSuperClsName = loaderActivityRules.get(superCls.name)
                // println "    ${ctCls.getName()} 的父类 $superCls.name 需要替换为 ${targetSuperClsName}"
                CtClass targetSuperCls = pool.get(targetSuperClsName)

                if (ctCls.isFrozen()) {
                    ctCls.defrost()
                }
                ctCls.setSuperclass(targetSuperCls)

                // 修改声明的父类后,还需要方法中所有的 super 调用。
                ctCls.getDeclaredMethods().each { outerMethod ->
                    outerMethod.instrument(new ExprEditor() {
                        @Override
                        void edit(MethodCall call) throws CannotCompileException {
                            if (call.isSuper()) {
                                if (call.getMethod().getReturnType().getName() == 'void') {
                                    call.replace('{super.' + call.getMethodName() + '($$);}')
                                } else {
                                    call.replace('{$_ = super.' + call.getMethodName() + '($$);}')
                                }
                            }
                        }
                    })
                }

                ctCls.writeFile(CommonData.getClassPath(ctCls.name))
                println "    Replace ${ctCls.name}'s SuperClass ${superCls.name} to ${targetSuperCls.name}"
            }

        } catch (Throwable t) {
            println "    [Warning] --> ${t.toString()}"
        } finally {
            if (ctCls != null) {
                ctCls.detach()
            }
            if (stream != null) {
                stream.close()
            }
        }
    }
复制代码

实际上,这段源码的意思就是指把所有继承Activity,Fragment的类全部都变成 RePlugin提供的Activity。那我们就看看下面这个类,在启动的时候做了什么手脚。

com.qihoo360.replugin.loader.a.PluginActivity
复制代码

PluginActivity

public abstract class PluginActivity extends Activity {

    @Override
    protected void attachBaseContext(Context newBase) {
        newBase = RePluginInternal.createActivityContext(this, newBase);
        super.attachBaseContext(newBase);
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        //
        RePluginInternal.handleActivityCreateBefore(this, savedInstanceState);

        super.onCreate(savedInstanceState);

        //
        RePluginInternal.handleActivityCreate(this, savedInstanceState);
    }
复制代码

我们要明白RePlugin是怎么查找资源。实际上我们需要看的就只有attachBaseContext这个方法。

还记得我上面的Activity的启动流程时序图吗? 实际上在Activity调用onCreate之前会有一次绑定的操作。下面是ActivityThread.performLaunchActivity的方法片段:

 Context appContext = createBaseContextForActivity(r, activity);
                CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());
                Configuration config = new Configuration(mCompatConfiguration);
                if (r.overrideConfig != null) {
                    config.updateFrom(r.overrideConfig);
                }

                Window window = null;
                if (r.mPendingRemoveWindow != null && r.mPreserveWindow) {
                    window = r.mPendingRemoveWindow;
                    r.mPendingRemoveWindow = null;
                    r.mPendingRemoveWindowManager = null;
                }
                activity.attach(appContext, this, getInstrumentation(), r.token,
                        r.ident, app, r.intent, r.activityInfo, title, r.parent,
                        r.embeddedID, r.lastNonConfigurationInstances, config,
                        r.referrer, r.voiceInteractor, window);

                if (customIntent != null) {
                    activity.mIntent = customIntent;
                }
                r.lastNonConfigurationInstances = null;
                activity.mStartedActivity = false;
                int theme = r.activityInfo.getThemeResource();
                if (theme != 0) {
                    activity.setTheme(theme);
                }

                activity.mCalled = false;
                if (r.isPersistable()) {
                    mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
                } else {
                    mInstrumentation.callActivityOnCreate(activity, r.state);
                }
复制代码

而这个绑定方法实际会调用attachBaseContext(context);把由系统生成的context绑定到Acitivity中。

那么在RePlugin插件库中的createActivityContext就是为了生成自己context去代替系统生成的context。

public static Context createActivityContext(Activity activity, Context newBase) {
        if (!RePluginFramework.mHostInitialized) {
            return newBase;
        }

        try {
            return (Context) ProxyRePluginInternalVar.createActivityContext.call(null, activity, newBase);
        } catch (Exception e) {
            
        }

        return null;
    }
复制代码

而这里做的工作就是判断插件库的是否初始化,没有则返回系统,有则反射获取宿主库的Context。

createActivityContext = new MethodInvoker(classLoader, factory2, 
"createActivityContext", new Class<?>[]{Activity.class, Context.class});
复制代码

而宿主库下面这个方法

public Context createActivityContext(Activity activity, Context newBase) {


        // 此时插件必须被加载,因此通过class loader一定能找到对应的PLUGIN对象
        Plugin plugin = mPluginMgr.lookupPlugin(activity.getClass().getClassLoader());
        if (plugin == null) {
            
            return null;
        }

        return plugin.mLoader.createBaseContext(newBase);
    }
复制代码

实际上就是我们在宿主寻找插件类时候初始化好的PluginContext。我们也能看看生成方法。

final Plugin lookupPlugin(ClassLoader loader) {
        for (Plugin p : mPlugins.values()) {
            if (p != null && p.getClassLoader() == loader) {
                return p;
            }
        }
        return null;
    }

final Context createBaseContext(Context newBase) {
        return new PluginContext(newBase, android.R.style.Theme, mClassLoader, mPkgResources, mPluginName, this);
    }
复制代码

那么为什么我们要替代掉系统的Context呢?实际上替代掉Context的主要原因就是为了让插件能够寻找到我们的资源文件。

这里又涉及到了Activity如何加载资源的源码,我在上面已经讲解过一边。

实际上当Android加载资源的时候,最终会调用LayoutInflater.inflate.而这个方法获取资源又是借用final Resources res = getContext().getResources();才获取到真正的资源对象。

所以当我们替换成为我们的PluginContext,就能让PluginActivity查找到了要加载的资源文件。

至此,RePlugin如何查找类,如何查找资源的过程全部明了,那么启动一个Activity也就顺其自然了。

这里借用恋猫de小郭 大佬的一张图就能很清楚了解到两个ClassLoader之间的关系

classloader关系图.png

到这里RePlugin的源码解析就结束了。但是既然是横向分析,那么我们需要总结出Small和RePlugin的异同。

Small和RePlugin的比较总结

实际上经过源码的分析,我们可以清楚:

Small

Small是一个单进程,而且代码比较轻量化的插件化的框架。为什么说轻量化,一个主要之一就是代码量比起其他的框架少了至少一倍。在实现上,类的加载和资源的加载统一放在一处框架集中管理。

而且在思想上,Small对插件的理解是除了外部apk外,内部所有的模块都是插件。就可以明白,Small刚开始创造出来的初衷也是除了能够管理外部的插件,主要还是为了形成组件化,让整个工程解藕。让整个工程能够灵活的热插拔。

Small插件化框架模型.png

RePlugin

RePlugin则是一个支持多进程的,重量级的框架。为什么说重量级别,一个原因是代码量比起DroidPlugin少,但是比起Small多许多。第二个,当我们默认开启进程的时候,平均每个进程大致上会占用多5M的内存空间。但是有一个很大的优点,那就是几乎没有入侵Android的系统源码。毫不夸张的说,RePlugin只反射了一处系统源码,而这处几乎是没有变动过,如果Google没有很大变动的化,RePlugin将会毫无疑问的是最稳定的一款,甚至说,可以兼容Android未来的版本。

在实现上,让插件和宿主借助常驻进程维护自己的资源和类,和DroidPlugin相似的地方就是宿主不需要管理插件的资源和类,希望每个插件只关注自己的类和资源。

从思想上,可能是本人见识的少,这是本人第一次见识到了两个库协同运行的方法,不管其他人如何想的。对于我来说,这是收获最大的一次。但是有一点需要诟病,如果RePlugin能够想Small一样把整个加载抽象出来管理,我感觉就完美了。

RePlugin插件框架模型.png

总结

从个人感觉来说,如果工程量不大,又对多进程没有太多的想法的工程完全可以优先使用Small。而如果整个工程量大,以后又可能使用多进程,追求稳定的大型项目还是推荐RePlugin。

这里如果好奇AppCompat应该如何兼容的读者可以看看: 红橙Darren:www.jianshu.com/p/e359fafe5…

还有对我的插件化基础模型感兴趣的可以去我的github上: github.com/yjy239/Host…

结束语

这里我先要感谢红橙Darren,一年前就是他这篇文章让我打开了Android的新世界大门。

接着我要感谢下面DroidPlugin开发团队的wiki系列文章: github.com/DroidPlugin…

感谢恋猫de小郭的文章: juejin.im/post/59752e…

感谢神罗天征_39a0www.jianshu.com/p/5994c2db1…

最后还要感谢RePlugin,Small作者给力的框架和思想,让鄙人学习到了很多东西。

如果有读者耐心的看到这里的读者,我先恭喜你,这片文章已经把绝大部分的插件化框架的思想都容纳了,至少在我眼里,自己改动别人的源码,甚至写属于自己的插件化框架也不在话下。

这篇横向分析插件化框架的分析文章花了整整一个多月的时间,每天晚上,周末两天都在沉浸这篇文章里面。其中大部分的时间都是在阅读新的7.0,8.0源码,复习源码。还有一个坏习惯,我有时候喜欢沉浸到源码里面看看,Android系统为什么是这样写,有时候跑远了,导致这篇文章花的时间太多了。

分类:
Android
标签:
收藏成功!
已添加到「」, 点击更改