前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Dynamic-Load-Apk源码解析

Dynamic-Load-Apk源码解析

作者头像
俞其荣
发布2022-07-28 08:16:41
3570
发布2022-07-28 08:16:41
举报
文章被收录于专栏:俞其荣的博客俞其荣的博客

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)

代码语言:javascript
复制
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) 方法来加载插件:

代码语言:javascript
复制
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) 方法:

代码语言:javascript
复制
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) 的源码:

代码语言:javascript
复制
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 了。

代码语言:javascript
复制
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 接口的作用是什么呢:

代码语言:javascript
复制
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() 方法。下面我们就来看看源码:

代码语言:javascript
复制
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 方法中到底干了什么:

代码语言:javascript
复制
@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 就发现轻松多了。

代码语言:javascript
复制
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:

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2017-12-04,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 0x00
  • 0x01
    • DLPluginManager.loadApk
      • DLPluginManager.startPluginActivityForResult
        • DLProxyActivity
          • DLBasePluginActivity
          • 0x02
          • 0x03
          相关产品与服务
          文件存储
          文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档