Replugin 全面解析(3)

上一篇分析中我们分析了Replugin框架Host端的一些核心概念,还梳理了Activity启动的流程,但是有两个重要部分没有提及或者详细讲述,那就是Plugin的加载过程,Plugin端的初始化,所以本篇会重点看看这两个方面的内容。

目录

  • Plugin加载的详细过程
  • Plugin端初始化
  • Plugin中启动Activity

Plugin加载的详细过程

要讲解这个过程,我们得从Plugin.doLoad函数开始,在上一篇分析中其实已经提到过,只是当时并没有分析它的细节。这个函数做了三件事情:

  • 释放插件文件到相应的目录,比如so库,dex文件
  • 加载dex文件,比如组件信息,组件属性,资源等
  • 要运行Plugin,还需要初始化Plugin的运行环境

释放文件这一步很简单,就是将APK包打开以后将相应的文件放到不同的目录中,有兴趣的同学可以取跟以下代码,这里就不赘述了。

来看看dex文件的加载,这是通过Loader.loadDex函数实现的。这个方法比较长,我们拆开来看。

第一步,获取PackageInfo并缓存插件相关的信息,比如组件,组件属性,资源等,下次就可以直接从缓存中读取。

if (mPackageInfo == null) {
    // 通过PackageManager获取PackageInfo
    mPackageInfo = pm.getPackageArchiveInfo(mPath,
            PackageManager.GET_ACTIVITIES | PackageManager.GET_SERVICES | PackageManager.GET_PROVIDERS | PackageManager.GET_RECEIVERS | PackageManager.GET_META_DATA);
    ......
    // 添加针对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));
    }
}

第二步,解析组件信息,注册Plugin的Manifest中声明的BroadcastReceiver。

if (mComponents == null) {
    mComponents = new ComponentList(mPackageInfo, mPath, mPluginObj.mInfo);
    // 动态注册插件中声明的 receiver
    regReceivers();
    // 缓存表Components
    synchronized (Plugin.FILENAME_2_COMPONENT_LIST) {
        Plugin.FILENAME_2_COMPONENT_LIST.put(mPath, new WeakReference<>(mComponents));
    }
    ......
}

这一步用到了ComponentList类,在它的构造函数中完成了对Plugin的Manifest文件的解析,调用的是Minifestparser类。有兴趣的同学可以跟一跟这里的代码,并不复杂。读取到Manifest文件以后,使用ManifestParser.INS.parse函数调用XmlHandler解析,把解析以后的信息缓存到ManifestParser中。

public void parse(PluginInfo pli, String manifestStr) {
    XmlHandler handler = parseManifest(manifestStr);

    Map<String, List<IntentFilter>> activityFilterMap = new HashMap<>();
    putToMap(mPluginActivityInfoMap, activityFilterMap, pli);
    parseComponent(pli.getName(), activityFilterMap, handler.getActivities(), mActivityActionPluginsMap);

    Map<String, List<IntentFilter>> serviceFilterMap = new HashMap<>();
    putToMap(mPluginServiceInfoMap, serviceFilterMap, pli);
    parseComponent(pli.getName(), serviceFilterMap, handler.getServices(), mServiceActionPluginsMap);

    Map<String, List<IntentFilter>> receiverFilterMap = new HashMap<>();
    putToMap(mPluginReceiverInfoMap, receiverFilterMap, pli);
    parseComponent(pli.getName(), receiverFilterMap, handler.getReceivers(), null);
}

第三步,获取Plugin的资源,先查找缓存,如果找不到就通过PackageManager创建Resources对象,但这里做了一个多余的赋值操作是为了修复一个BUG,不必在意。最后将资源对象缓存下来。

mPkgResources = Plugin.queryCachedResources(mPath);
if (mPkgResources == null) {
    try {
        if (BuildConfig.DEBUG) {
            // 如果是Debug模式的话,防止与Instant Run冲突,资源重新New一个
            Resources r = pm.getResourcesForApplication(mPackageInfo.applicationInfo);
            mPkgResources = new Resources(r.getAssets(), r.getDisplayMetrics(), r.getConfiguration());
        } else {
            mPkgResources = pm.getResourcesForApplication(mPackageInfo.applicationInfo);
        }
    } catch (NameNotFoundException e) {
        return false;
    }
    // 缓存Resources
    synchronized (Plugin.FILENAME_2_RESOURCES) {
        Plugin.FILENAME_2_RESOURCES.put(mPath, new WeakReference<>(mPkgResources));
    }
}

第四步,注意咯,这里就是创建Plugin的PluginDexClassLoader的地方,将它缓存起来,不必每次都创建一个新的。

if (mClassLoader == null) {
    String out = mPluginObj.mInfo.getDexParentDir().getPath();
    ......
    String soDir = mPackageInfo.applicationInfo.nativeLibraryDir;
    // 创建PluginDexClassLoader对象
    mClassLoader = RePlugin.getConfig().getCallbacks().createPluginClassLoader(mPath, out, soDir, parent);   
    ......
    synchronized (Plugin.FILENAME_2_DEX) {
        Plugin.FILENAME_2_DEX.put(mPath, new WeakReference<>(mClassLoader)); // 缓存ClassLoader
    }
}

第五步, 为Plugin创建一个全局的PluginContext,并用上面创建的ClassLoader以及Resources作为参数。而这个PluginContext对象会被赋值给Plugin的Application对象(后面会讲到)。其实每一个Plugin的Activity都会创建一个PluginContext对象,并使用相同的ClassLoaderResources,因此在Plugin中就可以加载相关的类和使用资源了,跟原生程序一样。

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

到这里,Plugin的加载就算是全部完成了!

Plugin端初始化

Dex文件加载完成以后,要运行Plugin还需要初始化Plugin的运行环境相关的类,在Plugin.doLoad的最后阶段调用了loadEntryLocked函数,这个函数负责初始化Plugin的运行环境。

private boolean loadEntryLocked(PluginCommImpl manager) {
    if (mDummyPlugin) {
        ......
    } else {
        ......
        } else if (mLoader.loadEntryMethod3()) {  // 通过反射拿到Entry的create函数
            if (!mLoader.invoke2(manager)) {
                return false;
            }
        } else {
            return false;
        }
    }
    return true;
}

Loader.loadEntryMethod3通过反射将Plugin中的Entry类的create函数对象得到并保存在mCreateMethod2中。注意这里的mClassLoader是插件的PluginDexClassLoader,所以才能得到插件中的类。

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;
}

接着Loader.invoke2函数会用反射的方式调用上面拿到的create函数。

final boolean invoke2(PluginCommImpl x) {
    try {
        IBinder manager = null;
        final ClassLoader classLoader = getClass().getClassLoader();
        IBinder b = (IBinder) mCreateMethod2.invoke(null, mPkgContext, classLoader, manager);   // 调用create函数
        ......
        mBinderPlugin = new ProxyPlugin(b);
        mPlugin = mBinderPlugin;
    } catch (Throwable e) {
        return false;
    }
    return true;
}

注意,这里我们将会进入到Plugin端的代码中,来看看Entry.create做了什么。这里有一个参数是ClassLoader,这个ClassLoader是Host中的RepluginClassLoader,有了它,Plugin才能找到Host当中的类并调用这些类的方法。

public static final IBinder create(Context context, ClassLoader cl, IBinder manager) {
    RePluginFramework.init(cl); // 初始化一些类,这些类用于调用Host中类的方法
    RePluginEnv.init(context, cl, manager);

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

这里RepluginFramework.init初始化了一些代理类,这些代理类可以在Plugin中调用Host中的函数。

private static boolean initLocked(ClassLoader cl) {
        ......
        try {
            RePluginInternal.ProxyRePluginInternalVar.initLocked(cl);
            RePlugin.ProxyRePluginVar.initLocked(cl);
            PluginLocalBroadcastManager.ProxyLocalBroadcastManagerVar.initLocked(cl);
            PluginProviderClient.ProxyRePluginProviderClientVar.initLocked(cl);
            PluginServiceClient.ProxyRePluginServiceClientVar.initLocked(cl);
            IPC.ProxyIPCVar.initLocked(cl);

            mHostInitialized = true;

        } catch (final Throwable e) {
        }
        return mHostInitialized;
    }

我们拿RePlugin.ProxyRePluginVar.initLocked作为例子来讲解。这里还是通过反射,在Plugin中去获取Host中Replugin的相关方法并保存在一系列的MethodInvoker对象中,比如Replugin.installReplugin.preloadReplugin.startActivity等。

static void initLocked(final ClassLoader classLoader) {
        // 初始化Replugin的相关方法
        final String rePlugin = "com.qihoo360.replugin.RePlugin";
        install = new MethodInvoker(classLoader, rePlugin, "install", new Class<?>[]{String.class});
        preload = new MethodInvoker(classLoader, rePlugin, "preload", new Class<?>[]{String.class});
        ......
        startActivity = new MethodInvoker(classLoader, rePlugin, "startActivity", new Class<?>[]{Context.class, Intent.class});
        startActivity2 = new MethodInvoker(classLoader, rePlugin, "startActivity", new Class<?>[]{Context.class, Intent.class, String.class, String.class});
       ......
    }
}

当我们在Plugin中使用Replugin.startActivity时,实际上是通过MethodInvoker.call函数去执行Host中Replugin对应的startActivity函数。所以Plugin中的Replugin类的实现就只是一个空壳代理而已,你可以看到它的实现如下:

public static boolean startActivity(Context context, Intent intent) {
        if (!RePluginFramework.mHostInitialized) {
            return false;
        }
        try {  // 用反射的方式调用Host中Replugin的startActivity函数来启动Activity
            Object obj = ProxyRePluginVar.startActivity.call(null, context, intent);
            if (obj != null) {
                return (Boolean) obj;
            }
        } catch (Exception e) {
            if (LogDebug.LOG) {
                e.printStackTrace();
            }
        }
        return false;
    }

看到这里,你就跟上一篇中 插件Activity启动流程 这一节连起来啦!

插件中启动Activity

Android中ActivityContext都有startActivity函数,为了在插件中能正常启动Activity,我们要将这些函数都屏蔽掉,转而使用Replugin提供的startActivity

为什么一定要用Replugin提供startActivity方法呢?因为我们插件的组件在Host的Manifest中是没有声明的,

只能通过坑位来启动,而启动坑位的动作只能在Host中完成。

Replugin是怎么做的呢?来看看吧。

将官方Demo中的插件APK反编译出来,会发现所有继承了Activity的类都被强制修改成继承PluginActivity,这个工作是由replugin-plugin-gradle在编译阶段完成的。

实际上在replugin-plugin-lib中的com.qihoo360.replugin.loader.a包中,你可以找到更多的Activity关的类。

首先PluginActivity重写了startActivity函数,并在其中通过反射调用Host中的Replugin.startActivity函数,这样就能做到在插件中启动Activity了。这里的反射调用指的就是调用前面讲过的RePlugin.ProxyRePluginVar类在初始化时得到的函数。

public void startActivity(Intent intent) {
    if (RePluginInternal.startActivity(this, intent)) {
        return;
    }
    super.startActivity(intent);
}

然后,PluginActivity重写了attachBaseContext函数,同样在代理类中通过反射调用Host中Factory2.createActivityContext函数创建一个PluginContext对象,并用这个对象替换掉原生的Context对象。

protected void attachBaseContext(Context newBase) {
    newBase = RePluginInternal.createActivityContext(this, newBase); //创建PluginContext对象
    super.attachBaseContext(newBase); //替换原生的Context
}

那么这个PluginContext又做了什么呢?原来PluginContext也重写了startActivity函数,并且在其中调用了Factory2.startActivity函数,接着又会调用PluginLibraryInternalProxy.startActivity。后面的事情如果读过第一篇分析文章,你应该都知道啦!

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

到这里,你无论是使用getContext().startActivity还是直接在Activity中使用startActivity都会走Replugin的启动流程。但是事情并没有完,还有一个地方需要修改,那就是Application中的Context

PluginApplicationClient为插件创建了Application对象,在PluginApplicationClient.callAttachBaseContext函数中通过反射调用Applicaiton.attach函数,用一个PluginContext对象替换掉原来的Context对象。

这里的PluginContext对象就是在前面讲到的Dex加载过程中创建的,作为Plugin的全局Context。

public void callAttachBaseContext(Context c) {
    try {
        sAttachBaseContextMethod.setAccessible(true);   
        sAttachBaseContextMethod.invoke(mApplication, c);  // 将PluginContext对象传递给Application对象
    } catch (Throwable e) {
        if (BuildConfig.DEBUG) {
            e.printStackTrace();
        }
    }
}

OK,到这里我们在插件中就能正常的使用Replugin的启动流程在插件中启动Activity了!!!

注意

实际上在Plugin中调用getApplication获取到的是Host的Application对象,因为别忘了我们是利用坑位原理运行插件组件的,所以在插件中我们用到的都是Host的Application。但是万一Plugin的Application中有客户定制的任何动作要完成呢?所以在加载 Plugin之后,Host也会通过反射创建Plugin的Application对象,并反射调用它的attachcreate函数。

Plugin.load

final boolean load(int load, boolean useCache) {
   PluginInfo info = mInfo;
   boolean rc = loadLocked(load, useCache); //Plugin加载完成

   if (load == LOAD_APP && rc) {
       callApp();   // 关于Plugin的Application的操作都在这里了
   }
     ......
   return rc;
}

Plugin.callAppLocked

private void callAppLocked() {
    // 获取并调用Application的几个核心方法
    if (!mDummyPlugin) {
          ......
        mApplicationClient = PluginApplicationClient.getOrCreate(
                mInfo.getName(), mLoader.mClassLoader, mLoader.mComponents, mLoader.mPluginObj.mInfo);  // 创建Plugin的Appication对象

        if (mApplicationClient != null) { // 调用Application.attach
            mApplicationClient.callAttachBaseContext(mLoader.mPkgContext);
            mApplicationClient.callOnCreate(); //调用Application.onCreate
        }
    } else {
    }
}

总结:

通过这两篇文章,我们总结一下一个插件Activity启动的流程大致是:

  • 加载目标Activity信息

    开始查找Activity信息 —> 找到对应的Pugin信息 —> 解压APK文件 —> 加载Dex文件内容 —> 创建Application对象 —> 创建Entry对象初始化Plugin环境

  • 寻找坑位

    查找坑位 —> 将目标Activity与找到的坑位以ActivityState的形式缓存在PluginContainers中 —> 启动坑位Activity —> 系统调用RepluginClassLoader —> 调用PluginDexClassLoader加载坑位Activity类 —> 通过PluginContainers找到坑位对应的目标Activity类 —> 系统调用PluginActivityattachBaseContext函数 —> 创建PluginContext对象并替换 —> 目标Activity的正常启动流程(onCreateonStartonResume.....)

如果你已经读完了这两篇分析文章,但还没有将Replugin的里面如此多的类的关系理清楚,那么下面这张图也许可以帮到你。不过这张图目前还不完整,只针对目前已经讲到的部分,后面会逐渐补全。

replugin_1.jpg

绿色的部分是replugin-plugin-lib,蓝色和红色部分都是是replugin-host-lib包中的类,但是蓝色部分是运行在UI进程中,而红色部分是运行在Persistent进程中。

绿色部分和蓝色部分之间的调用都是通过反射来实现的,所以用类虚线箭头,同样蓝色部分和红色部分是通过Binder进程间通信机制来调用的,也用虚线箭头表示。

下一篇Replugin 全面解析 (4)我们会讲解Service以及进程相关内容!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,784评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,745评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,702评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,229评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,245评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,376评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,798评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,471评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,655评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,485评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,535评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,235评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,793评论 3 304
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,863评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,096评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,654评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,233评论 2 341

推荐阅读更多精彩内容