Dynamic-Load-Apk源码解析

博文出处:Dynamic-Load-Apk源码解析,欢迎大家关注我的博客,谢谢!

0x00

趁着今天是周末,无所事事,来讲讲 Dynamic-Load-Apk 框架。Dynamic-Load-Apk 是任主席主导开发的一款插件化框架,其中心思想主要就是两个字——代理。和我之前分析的 android-pluginmgr 插件化框架不同的是,Dynamic-Load-Apk 框架完全基于在应用层上实现,并不依靠 ActivityThread 、Instrumentation 等。另外,Dynamic-Load-Apk 框架在插件化发展历程中诞生较早,对后来不断涌现的插件化框架具有深刻的指导意义。

0x01

注:本文分析的 Dynamic-Load-Apk 为 master 分支,版本为1.0.0;

其实 Dynamic-Load-Apk 框架的思想很巧妙,大致的思路如下:在宿主中首先申明了一些 ProxyActivity 以及 ProxyService ,插件中的 PluginActivity 要继承指定的 DLBasePluginActivity 。然后启动插件中的 Activity 时,实际上启动的是 ProxyActivity , 之后利用接口回调调用了 PluginActivity 中的生命周期方法。也就是说,PluginActivity 并不是实质上的 Activity ,其实只是一个普通的 Java 类。

在分析源码之前,先在这里简单地说一下 Dynamic-Load-Apk 框架的结构:

dynamic-load-apk的类结构

从上面的图中大致可以看出来,整个框架中的 Java 类基本可以分为五种类型:

  • DLPluginManager :顾名思义,是整个插件的管理类。主要作用就是加载以及管理插件,启动插件中的Activity、Service等;
  • DLProxyActivity 、DLProxyFragmentActivity 、DLProxyService :代理组件。可以看到有 Activity 、Service 等,在启动插件时实质上启动的是这些代理组件,之后在代理组件中利用接口回调插件的相关“生命周期”;
  • DLProxyImpl 、DLServiceProxyImpl :属于启动插件过程中一些公共逻辑代码。在代理组件连接插件组件时,把一些公共的方法抽取出来放入了这些类中;
  • DLBasePluginActivity 、DLBasePluginFragmentActivity 、DLBasePluginService :插件的基类。用户使用的插件需要继承自这些基类,之后接口才会回调插件的“生命周期”。
  • DLPlugin 、DLServicePlugin :插件“生命周期”定义的接口。在这两个类中定义了 Activity 、Service 相关的生命周期方法。

那么接下来我们就一一来解析源码吧。

DLPluginManager.loadApk

DLPluginManager 是个单例类,我们先来看看它的初始化方法 DLPluginManager.getInstance(Context context)

private DLPluginManager(Context context) {
    mContext = context.getApplicationContext();
    mNativeLibDir = mContext.getDir("pluginlib", Context.MODE_PRIVATE).getAbsolutePath();
}

public static DLPluginManager getInstance(Context context) {
    if (sInstance == null) {
        synchronized (DLPluginManager.class) {
            if (sInstance == null) {
                sInstance = new DLPluginManager(context);
            }
        }
    }

    return sInstance;
}

可以看到在构造函数中设置了 .so 文件存储的目录。初始化完成后,通过 loadApk(final String dexPath, boolean hasSoLib) 方法来加载插件:

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(final String dexPath, boolean hasSoLib) 主要做了两件事:

  1. preparePluginEnv(PackageInfo packageInfo, String dexPath) 方法中把插件 packageInfo 封装成 pluginPackage ;
  2. 复制 .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;
}

根据代码可知,在方法中创建了插件的 ClassLoader ,插件的 res 资源。如果你看过我另一篇插件化框架分析的文章《插件化框架android-pluginmgr全解析》,那么想必对这其中的原理已经熟知了:

  1. 插件的类加载器是 DexClassLoader 或其子类,可以指定加载 dex 的目录。对应着上面的 createDexClassLoader(dexPath) 方法;
  2. 插件的 res 资源访问主要通过 AssetManager 的 addAssetPath 方法来获取。需要注意的是,addAssetPath 方法是 @hide 的,需要反射来执行。对应着 createAssetManager(dexPath) 方法;

createXxxx 方法具体的代码就不在这里贴出来了,想了解的可以查看源码。通过这些 createXxxx 方法,就把插件的 ClassLoader 和 res 资源问题解决了。最后封装成一个 pluginPackage 对象,方便之后使用。

DLPluginManager.startPluginActivityForResult

加载完插件之后,我们就要着手于如何启动插件了。想要启动插件,就要调用 startPluginActivity(Context context, DLIntent dlIntent) 方法,而 startPluginActivity(Context context, DLIntent dlIntent) 方法内部又是调用 startPluginActivityForResult(Context context, DLIntent dlIntent, int requestCode) 方法的,所以我们直接查看 startPluginActivityForResult(Context context, DLIntent dlIntent, int requestCode) 的源码:

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(Context context, DLIntent dlIntent, int requestCode) 方法里基本上都有注释,要明白的是,intent 启动的是代理的 Activity ,并不是我们插件的 Activity 。另外,在 DLPluginManager 里还有启动插件 Service 的相关代码,不过具体的流程和启动插件 Activity 是相似的。如果有想要进一步了解的童鞋可以自行看源码。

DLProxyActivity

经过上一步之后,我们就启动了代理类 Activity 。代理类 Activity 有两种:DLProxyActivity 和 DLProxyFragmentActivity 。但是其中的逻辑都是一样的。在这里我们只分析 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());
    }

    ...

}

从类的结构上看到,DLProxyActivity 实现了 DLAttachable 接口。那么 DLAttachable 接口的作用是什么呢:

public interface DLAttachable {
    /**
     * when the proxy impl ( {@see DLProxyImpl#launchTargetActivity()} ) launch
     * the plugin activity , dl will call this method to attach the proxy activity
     * and pluginManager to the plugin activity. the proxy activity will load
     * the plugin's resource, so the proxy activity is a resource delegate for
     * plugin activity.
     * 
     * @param proxyActivity a instance of DLPlugin, {@see DLBasePluginActivity}
     *            and {@see DLBasePluginFragmentActivity}
     * @param pluginManager DLPluginManager instance, manager the plugins
     */
    public void attach(DLPlugin proxyActivity, DLPluginManager pluginManager);
}

根据注释的意思是,attach(DLPlugin proxyActivity, DLPluginManager pluginManager) 方法可以在 ProxyImpl 调用 launchTargetActivity() 时把 PluginActivity 和 ProxyActivity 绑定在一起。那样就达到了可以在 ProxyActivity 中使用 PluginActivity 的效果。那么到底在什么时候调用 proxyImpl.launchTargetActivity() 方法呢?我们回到上面的 DLProxyActivity 类中来,看到了 DLProxyActivity 中有一个 impl 成员变量。在 onCreate(Bundle savedInstanceState) 中调用了 impl.onCreate(getIntent()) ,我们猜想在 impl.onCreate(getIntent()) 的方法里一定会去调用 launchTargetActivity() 方法。下面我们就来看看源码:

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() ,说明我们的猜想是正确的。来看看在 launchTargetActivity 方法中到底干了什么:

@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
protected void launchTargetActivity() {
    try {
        // 得到插件 Activity 的 class
        Class<?> localClass = getClassLoader().loadClass(mClass);
        Constructor<?> localConstructor = localClass.getConstructor(new Class[] {});
        // 创建插件 Activity 的对象
        Object instance = localConstructor.newInstance(new Object[] {});
        mPluginActivity = (DLPlugin) instance;
        // 调用 attach 方法,把插件和代理绑定起来
        ((DLAttachable) mProxyActivity).attach(mPluginActivity, mPluginManager);
        Log.d(TAG, "instance = " + instance);
        // attach the proxy activity and plugin package to the mPluginActivity
        // 手动调用插件的 attach 方法
        mPluginActivity.attach(mProxyActivity, mPluginPackage);

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

我们发现在方法中使用反射创建了插件 Activity 的对象,又因为插件 Activity 必须继承指定的基类,这些基类是实现了 DLPlugin 接口的。所以插件 Activity 可以强转为 DLPlugin 。DLPlugin 接口定义了一系列的 Activity 生命周期方法,之后手动回调了 attachonCreate 方法。

现在我们再回过头来看看 DLProxyActivity 里的其他生命周期方法,发现都有一句 mRemoteActivity.onXxxxx() 。其中的 mRemoteActivity 就是通过 DLAttachable 接口绑定的插件 Activity 对象。所以每当代理 ProxyActivity 回调生命周期方法时,都调用了 DLPlugin 接口一致的生命周期方法,这样就实现了插件 Activity 也有“生命周期”方法。

DLBasePluginActivity

讲解了 DLProxyActivity 之后,再来看看 DLBasePluginActivity 就发现轻松多了。

public class DLBasePluginActivity extends Activity implements DLPlugin {

    ....

    @Override
    public void setContentView(int layoutResID) {
        if (mFrom == DLConstants.FROM_INTERNAL) {
            super.setContentView(layoutResID);
        } else {
            mProxyActivity.setContentView(layoutResID);
        }
    }

    @Override
    public void onResume() {
        if (mFrom == DLConstants.FROM_INTERNAL) {
            super.onResume();
        }
    }

    @Override
    public void onPause() {
        if (mFrom == DLConstants.FROM_INTERNAL) {
            super.onPause();
        }
    }    

}

DLBasePluginActivity 实现了 DLPlugin 接口,就有了 onCreate()onResume() 这些“生命周期”方法。另外在重写的方法中会判断当前否被代理,以此来确定直接走父类逻辑还是代理 Activity 或是空逻辑。

0x02

讲到这里,整个启动插件 Activity 的流程就走完了。除此之外,还有启动插件 Service 其实也是相似的流程。现在的 Dynamic-Load-Apk 框架如果实际使用起来可能有比较多的问题,作者也基本上很早就停止更新了。但是这并不妨碍我们分析源码,学习其中的精髓。我想大部分人看完源码都会体会到 Dynamic-Load-Apk 的核心思想——代理,这也正是和其他插件化框架不同的地方。在这里感谢那些为 Dynamic-Load-Apk 作出贡献的人。

如果有问题可以留言,Goodbye !

0x03

Reference:

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

推荐阅读更多精彩内容