Android插件化与热修复(二)---Dynamic-load-apk

dynamic-load-apk 核心原理分析(代理模式)

dynamic-load-apk简介

项目地址:https://github.com/singwhatiwanna/dynamic-load-apk
dynamic-load-apk是2014年底,任玉刚发布的一个Android插件化项目,这跟后续出现的很多插件化项目都不太一样。它没有Hook太多的系统底层方法,而是在应用层上,通过代理的方式实现的一种插件化框架。主要特点:

  • plugin支持Activity、Service以及动态的BroadcastReceiver。
  • 基本无反射调用
  • 插件安装后仍可独立运行从而便于调试
  • 支持plugin对host的调用
  • 插件需要引入DL的一个jar包,遵循DL接口规范

为什么是核心原理分析不是源码分析?

dynamic-load-apk插件在项目实际应用中会有很多问题,并且支持组件少、对插件侵入严重,插件开发成本高等问题,所以在2015年该项目就已经停止更新。但作为国内第一个可参考的较完整的android插件化项目,他的核心原理我们还是要了解,有利于我们更全面的了解插件化方案。

总体设计

代码结构

一下为dynamic-load-apk框架的所有代码,对于一个插件化框架来说算是很少了

$RUG5PRJ.png

dynamic-load-apk主要分为四大模块:

  • DLPluginManager插件管理模块,负责插件的加载、管理以及启动插件组件。
  • Proxy代理组件模块,目前包括 DLProxyActivity(代理 Activity)、DLProxyFragmentActivity(代理 FragmentActivity)、DLProxyService(代理 Service)。
  • Proxy Impl代理组件公用逻辑模块,负责构建、加载插件组件的管理器。这些 Proxy Impl 通过反射得到插件组件,然后将插件与 Proxy 组件建立关联,最后调用插件组件的 onCreate 函数进行启动。
  • Base Plugin插件组件的基类模块,目前包括 DLBasePluginActivity(插件 Activity 的基类)、DLBasePluginFragmentActivity(插件 FragmentActivity 的基类)、DLBasePluginService(插件 Service 的基类)。

调用插件 Activity 的流程图

2_看图王.png

其他组件调用流程类似:
(1) 首先通过 DLPluginManager 的 loadApk 函数加载插件,这步每个插件只需调用一次。
(2) 通过 DLPluginManager 的 startPluginActivity 函数启动代理 Activity.。
(3) 代理 Activity 启动过程中构建、启动插件 Activity。

loadApk 加载插件

加载插件源码:

public DLPluginPackage loadApk(final String dexPath, boolean hasSoLib) {
    mFrom = DLConstants.FROM_EXTERNAL;

    PackageInfo packageInfo = mContext.getPackageManager().getPackageArchiveInfo(dexPath,
            PackageManager.GET_ACTIVITIES | PackageManager.GET_SERVICES);
    if (packageInfo == null) {
        return null;
    }
    // 得到插件信息封装类
    DLPluginPackage pluginPackage = preparePluginEnv(packageInfo, dexPath);
    // 如果有 .so 文件,则复制到 mNativeLibDir 目录
    if (hasSoLib) {
        copySoLib(dexPath);
    }

    return pluginPackage;
}

loadApk方法 主要做了两件事:

  • 在 preparePluginEnv方法中把插件 packageInfo 封装成 pluginPackage ;
  • eventInheritance 默认为false,支持事件继承:直接发送eventClass 事件。
  • 复制 .so 文件到 mNativeLibDir 目录,主要流程就是在 SoLibManager 中利用 I/O 流复制文件。

进一步到preparePluginEnv(PackageInfo packageInfo, String dexPath) 方法:

private DLPluginPackage preparePluginEnv(PackageInfo packageInfo, String dexPath) {
    // 先查看缓存中有没有该 pluginPackage
    DLPluginPackage pluginPackage = mPackagesHolder.get(packageInfo.packageName);
    if (pluginPackage != null) {
        return pluginPackage;
    }
    // 创建 加载插件的ClassLoader
    DexClassLoader dexClassLoader = createDexClassLoader(dexPath); 
    AssetManager assetManager = createAssetManager(dexPath);
    // 得到插件 res 资源
    Resources resources = createResources(assetManager);
    // create pluginPackage
    pluginPackage = new DLPluginPackage(dexClassLoader, resources, packageInfo);
    mPackagesHolder.put(packageInfo.packageName, pluginPackage);
    return pluginPackage;
}

preparePluginEnv方法主要做的事情:

  • 创建了插件的 ClassLoader ,用于之后加载插件类。
  • 创建插件的 resources 资源。插件的 res 资源访问主要通过 AssetManager 的 addAssetPath 方法来获取。需要注意的是,addAssetPath 方法是 @hide 的,需要反射来执行。
  • 最后封装成一个 pluginPackage 对象返回。

startPluginActivityForResult启动插件

startPluginActivityForResult源码

public int startPluginActivityForResult(Context context, DLIntent dlIntent, int requestCode) {
    // 判断是否宿主内部调用
    if (mFrom == DLConstants.FROM_INTERNAL) {
        dlIntent.setClassName(context, dlIntent.getPluginClass());
        performStartActivityForResult(context, dlIntent, requestCode);
        return DLPluginManager.START_RESULT_SUCCESS;
    }

    String packageName = dlIntent.getPluginPackage();
    if (TextUtils.isEmpty(packageName)) {
        throw new NullPointerException("disallow null packageName.");
    }
    // 得到插件信息
    DLPluginPackage pluginPackage = mPackagesHolder.get(packageName);
    if (pluginPackage == null) {
        return START_RESULT_NO_PKG;
    }
    // 得到插件 Activity 的全类名
    final String className = getPluginActivityFullPath(dlIntent, pluginPackage);
    // 得到对应的 class
    Class<?> clazz = loadPluginClass(pluginPackage.classLoader, className);
    if (clazz == null) {
        return START_RESULT_NO_CLASS;
    }

    // 根据插件 class 继承的是哪个基类,分别得到对应的代理类
    // 若继承的是 DLBasePluginActivity ,得到的就是 DLProxyActivity 代理类
    // 若继承的是 DLBasePluginFragmentActivity ,得到的就是 DLProxyFragmentActivity 代理类
    Class<? extends Activity> activityClass = getProxyActivityClass(clazz);
    if (activityClass == null) {
        return START_RESULT_TYPE_ERROR;
    }

    // 把插件信息传入 Intent 中
    dlIntent.putExtra(DLConstants.EXTRA_CLASS, className);
    dlIntent.putExtra(DLConstants.EXTRA_PACKAGE, packageName);
    // 这里启动的是上面得到的代理类 Activity
    dlIntent.setClass(mContext, activityClass);
    // 启动 Activity
    performStartActivityForResult(context, dlIntent, requestCode);
    return START_RESULT_SUCCESS;
}

startPluginActivityForResult方法里主要是获得插件主页 Activity 的clazz , 根据插件 class 继承的是哪个基类,分别得到对应的代理类
,并通过intent 启动的是代理的 Activity ,并不是我们插件的 Activity 。

DLProxyActivity绑定插件Acitivity并启动

DLProxyActivity 源码

public class DLProxyActivity extends Activity implements DLAttachable {

    protected DLPlugin mRemoteActivity;
    private DLProxyImpl impl = new DLProxyImpl(this);

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        impl.onCreate(getIntent());
    }

    ...

}

在ProxyActivity 的onCreate(Bundle savedInstanceState)方法 中调用了 impl.onCreate(getIntent()) , impl.onCreate(getIntent()) 的方法里

public void onCreate(Intent intent) {

    // set the extra's class loader
    intent.setExtrasClassLoader(DLConfigs.sPluginClassloader);
    // 得到传过来的插件 Activity 包名和全类名
    mPackageName = intent.getStringExtra(DLConstants.EXTRA_PACKAGE);
    mClass = intent.getStringExtra(DLConstants.EXTRA_CLASS);
    Log.d(TAG, "mClass=" + mClass + " mPackageName=" + mPackageName);
    // 得到插件相关的信息
    mPluginManager = DLPluginManager.getInstance(mProxyActivity);
    mPluginPackage = mPluginManager.getPackage(mPackageName);
    mAssetManager = mPluginPackage.assetManager;
    mResources = mPluginPackage.resources;
    // 得到要启动插件的 activityInfo,设置插件 Activity 的主题
    initializeActivityInfo();
    // 把 DLProxyActivity 的主题设置为插件 Activity 的主题
    handleActivityInfo();
    launchTargetActivity();
}

在 onCreate(Intent intent) 中得到了之前插件 Activity 相关的信息。然后把 DLProxyActivity 的主题设置为 PluginActivity 的主题。最后调用了 launchTargetActivity() ,把 PluginActivity 和 ProxyActivity 绑定在一起。
继续看捆绑方法launchTargetActivity() :

TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
protected void launchTargetActivity() {
    try {
        // 通过反射创建插件 Activity 的对象      
         Class<?> localClass = getClassLoader().loadClass(mClass);
        Constructor<?> localConstructor = localClass.getConstructor(new Class[] {});
        Object instance = localConstructor.newInstance(new Object[] {});
        mPluginActivity = (DLPlugin) instance;
         // 手动调用插件的 attach 方法,将ProxyActivity和PluginActivity绑定在一起
        ((DLAttachable) mProxyActivity).attach(mPluginActivity, mPluginManager);
        mPluginActivity.attach(mProxyActivity, mPluginPackage);

        Bundle bundle = new Bundle();
        bundle.putInt(DLConstants.FROM, DLConstants.FROM_EXTERNAL);
        // 手动调用插件的 onCreate 方法
        mPluginActivity.onCreate(bundle);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

launchTargetActivity() 在方法中使用反射创建了插件 PluginActivity的对象,又因为插件 Activity 必须继承指定的基类DLBasePluginActivity,这些基类是实现了 DLPlugin 接口的。所以插件 Activity 可以强转为 DLPlugin 。DLPlugin 接口定义了一系列的 Activity 生命周期方法,之后手动回调了 attach 和 onCreate 方法将ProxyActivity和PluginActivity绑定在一起。代理 ProxyActivity 回调mRemoteActivity生命周期方法时,都调用了 DLPlugin 接口一致的生命周期方法,这样就实现了插件 PluginActivity具备了完整的生命周期。

至此,dynamic-load-apk的插件化实现的主要流程介绍完了,主要解决了资源加载问题和代理activity的生命周期管理问题。最后附上主要流程图:

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

推荐阅读更多精彩内容