2023年插件化学习,从Activity开始

1

前言

插件化技术从2015年就开始百花齐放,如: 奇虎360的replugin、任玉刚的VirtualAPK和腾讯的Shadow,插件化经历了严峻的市场考验,目前已经很成熟,今天小木箱带大家手把手学习插件化Activity,如果本文对你有所帮助,希望点赞收藏加转发。

2

插件化概念

插件化是一种动态加载四大组件的技术。最早为了解决65535限制的问题而诞生,后来Google出来了multidex专门解决65535限制问题。

目前市面的插件化一定程度上可以达到缩包目的,而且对项目组件化,工程职责颗粒化,模块低耦合有着莫大裨益。

插件化同时能实现bug热修复,由于Davilk虚拟机存在,Java支持动态加载任意类。

因为安卓系统在四大组件上做了限制,如果你尝试打开不在清单中的组件时,Android系统会让程序Crash。

插件化本质上是绕过Android系统的管控,让我们的APP自由打开、使用四大组件。

3

插件化业务价值

插件化是为了解决类加载和资源加载的问题,资源加载通过反射AssertManager,按照类加载划分,插件化分为静态代理和Hook两种方式,使用插件化是为了解决应用新版本覆盖慢问题。

四大组件可动态加载,意味着用户不需要手动安装新版本的应用,我们可以给用户提供新的功能和页面,或者在用户无感的情况下修复bug。

4

插件化项目结构

 

5

插件化开发流程

第一步: 创建 app 主工程作为宿主工程

 

第二步: 创建plugin_package作为插件工程,负责打插件包 

第三步: 创建接口工程 lifecycle_manager ,负责管理四大组件的生命周期

 

第四步: 安装插件

4.1 把Assets里面的文件复制到/data/data/files目录下

 
public static void extractAssets(Context context, String sourceName) {
    AssetManager am = context.getAssets();
    InputStream is = null;
    FileOutputStream fos = null;
    try {
        is = am.open(sourceName);
        File extractFile = context.getFileStreamPath(sourceName);
        fos = new FileOutputStream(extractFile);
        byte[] buffer = new byte[1024];
        int count = 0;
        while ((count = is.read(buffer)) > 0) {
            fos.write(buffer, 0, count);
        }
        fos.flush();
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        closeSilently(is);
        closeSilently(fos);
    }

}

4.2 通过静态代理构建DexClassLoader

因为没有上下文环境,上下文环境需要宿主提供给一个DexClassLoader包含一个插件。

 
// 获取插件目录下的文件
File extractFile = mContext.getFileStreamPath(mApkName);
// 获取插件包路径
String dexPath = extractFile.getPath();
// 创建Dex输出路径
File fileRelease = mContext.getDir("dex", Context.MODE_PRIVATE);

// 构建 DexClassLoader 生成目录
mPluginClassLoader = new DexClassLoader(dexPath,
        fileRelease.getAbsolutePath(), null, mContext.getClassLoader());

Hook方式是把dex文件合并到宿主的DexClassLoader里面,但绕过AMS清单文件注册的Activity会抛ClassNotFuoundException,所以需要Hook startActivity和handleResumeActivity,前者实现简单,兼容性好,而且插件是分离的,后者兼容性差,开发方便,但是如果多个插件如果有相同的类,就会出现问题,这里使用静态代理来处理。

4.3 通过反射 AssertManager 实现资源加载

 
try {
    AssetManager assetManager = AssetManager.class.newInstance();
    Method method = AssetManager.class.getMethod("addAssetPath", String.class);
    method.invoke(assetManager, dexPath);
    mPluginResources = new Resources(assetManager, mContext.getResources().getDisplayMetrics(),
            mContext.getResources().getConfiguration());
} catch (Exception e) {
    Toast.makeText(mContext, "加载 Plugin 失败", Toast.LENGTH_SHORT).show();
}

第五步: 解析插件

静态代理实现方式很简单,不需要熟悉 Activity 启动流程什么的,直接面向接口编程,首先需要在宿主 App 加载插件构造 DExClassCloder 和 Resource 对象,有了 DexClassLoader,就可以加载插件里面的类 Resource 是通过反射 AssertManager 的 addAssertPath 创建一个 AssertManage,再构造 Resource 对象,当然启动 Service、注册动态广播其实和启动 Activity 一样,都是通过宿主的 Context 去启动,但是 DL 框架不支持静态广播。静态广播是在应用安装的时候才会去解析并注册的,而我们插件的 Manifest 是没法注册的,所以里面的静态广播只能我们手动去解析注册,利用的是反射调用 PackageParser 的 parsePackage 方法,把静态广播都转变为动态广播,具体实现是在 PluginManager#parserApkAction 方法的实现。

 
public void parserApkAction() {
    try {
        Class packageParserClass = Class.forName("android.content.pm.PackageParser");
        Object packageParser = packageParserClass.newInstance();
        Method method = packageParserClass.getMethod("parsePackage", File.class, int.class);
        File extractFile = mContext.getFileStreamPath(mApkName);
        Object packageObject = method.invoke(packageParser, extractFile, PackageManager.GET_RECEIVERS);
        Field receiversFields = packageObject.getClass().getDeclaredField("receivers");
        ArrayList arrayList = (ArrayList) receiversFields.get(packageObject);

        Class packageUserStateClass = Class.forName("android.content.pm.PackageUserState");
        Class userHandleClass = Class.forName("android.os.UserHandle");
        int userId = (int) userHandleClass.getMethod("getCallingUserId").invoke(null);

        for (Object activity : arrayList) {
            Class component = Class.forName("android.content.pm.PackageParser$Component");
            Field intents = component.getDeclaredField("intents");
            // 1.获取 Intent-Filter
            ArrayList<IntentFilter> intentFilterList = (ArrayList<IntentFilter>) intents.get(activity);
            // 2.需要获取到广播的全类名,通过 ActivityInfo 获取
            // ActivityInfo generateActivityInfo(Activity a, int flags, PackageUserState state, int userId)
            Method generateActivityInfoMethod = packageParserClass
                    .getMethod("generateActivityInfo", activity.getClass(), int.class,
                            packageUserStateClass, int.class);
            ActivityInfo activityInfo = (ActivityInfo) generateActivityInfoMethod.invoke(null, activity, 0,
                    packageUserStateClass.newInstance(), userId);
            Class broadcastReceiverClass = getClassLoader().loadClass(activityInfo.name);
            BroadcastReceiver broadcastReceiver = (BroadcastReceiver) broadcastReceiverClass.newInstance();
            for (IntentFilter intentFilter : intentFilterList) {
                mContext.registerReceiver(broadcastReceiver, intentFilter);
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

有了 AssertManager 对象就可以访问资源文件了,但是插件是没有 Context 上下文环境的,这个上下文环境需要宿主提供给他,具体做法是通过 PackManager 获取插件入口的 Activity 注注入宿主 Context,这就完成了宿主 App 跳转插件 App 的步骤。但是插件 App 是没有上下文环境的,所以插件 App 里面是不能直接 startActivity,需要拿到宿主 Context startActivity。

第六步,代理 Activity: 在 lifecycle_mananager 构建 ActivityInterface 负责管理插件 Activity 生命周期

 
public interface ActivityInterface {

// 插入Activity上下文
    void insertAppContext(Activity hostActivity);

// Activity各个生命周期方法
    void onCreate(Bundle savedInstanceState);

    void onStart();

    void onResume();

    void onPause();

    void onStop();

    void onDestroy();
}

第七步,代理 Activity: 在 plugin_package 构建 BaseActivity 实现 ActivityInterface

在 BaseActivity 提供 startActivity,丢给宿主 Activity 去启动。

 
public void startActivity(Intent intent) {

    Intent newIntent = new Intent();
    newIntent.putExtra("ext_class_name", intent.getComponent().getClassName());
    mHostActivity.startActivity(newIntent);
}

第八步,代理 Activity: 在 plugin_package 构建 Activity 插件

 
public class PluginActivity extends BaseActivity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

    findViewById(R.id.btn_start).setOnClickListener(
                v -> startActivity(new Intent(mHostActivity, TestActivity.class))
        );
 }
}

// 测试插件Activity
public class TestActivity extends BaseActivity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
    }
}

 

第九步: 启动插件的入口 Activity

这一步主要做的就是给插件注册一个宿主的 Context。

 
// PorxyActivity
protected void onCreate(@Nullable Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   // 获取到真正要启动的插件 Activity,然后执行 onCreate 方法
   String className = getIntent().getStringExtra(EXT_CLASS_NAME);
   try {
       Class clazz = getClassLoader().loadClass(className);
       ActivityInterface activityInterface = (ActivityInterface) clazz.newInstance();
       // 注册宿主的 Context
       activityInterface.insertAppContext(this);
       activityInterface.onCreate(savedInstanceState);
   } catch (Exception e) {
       e.printStackTrace();
   }
}

   @Override
public void startActivity(Intent intent) {
    String className = intent.getStringExtra(EXT_CLASS_NAME);
    Intent proxyIntent = new Intent(this, ProxyActivity.class);
    proxyIntent.putExtra(EXT_CLASS_NAME, className);
    super.startActivity(proxyIntent);
}

这样其实就已经完成了 PluginActivity 的启动了,但是需要注意的是,在插件的 Activity 里面,我们不能再使用 this 了,因为插件并没有上下文环境,所以一些调用 Context 的方法都需要使用宿主的 Context 去执行,比如:

在 BaseActivity 提供 findViewById,可以查找布局 Id 文件。

 
public View findViewById(int layoutId) {
    return mHostActivity.findViewById(layoutId);
}

在 BaseActivity 提供 setContentView,方便渲染 UI 布局。

 
public void setContentView(int resId) {
    mHostActivity.setContentView(resId);
}

6

插件化原理介绍

 

  1. 使用 DexClassLoader 加载插件的 Apk。

  2. 通过代理的 Activity 去执行插件中的 Activity,加载对应的生命周期。

  3. 通过反射调用 AssetManager 的 addAssetPath 来加载插件中的资源。

7

插件化遇到的问题

1. 找到的 Activity 不在插件包里面

我们真正打开的却是一个在插件包中定义的 Activity,这个 Activity 需要的信息在插件包中的,而不是宿主的。

解决方案

插件Activity也同时重写了attachBaseContext方法。在这一步, 用插件的classloader和Resources实例创建一个自己的上下文,并用它替换base context传递给父类保存。如此一来,业务调用 getClassLoader()或者getResources()时,取得的就都是插件的信息了。

2. 资源 Id 类型不匹配 找不到

你需要通过一个资源ID获取一个drawable的时候,取得的是color 或者其他资源。

解决方案

主要发生在8.0以下版本。经过调查发现在 8.0 以下的插件包中,ContextThemeWrapper.mResources是宿主的Resource,而非插件的 Resource。从而导致同一个ID找到的资源不对应。

3. 插件包 leakcanary 引发的崩溃

leakcanary 会使用栈顶的 activity 的 Resource 去加载它要显示的一张图片,但这个资源有可能不在当前插件中。

解决方案

宿主和所有插件都依赖 leakcanary 即可。

8

总结

本文主要是根据我自身实际投产的 插件组件化 实践,分享一些动态加载 SDK 插件 时需要考虑的问题。内容主要包括插件化方案的共同问题、插件包 leakcanary 引发的崩溃、资源 Id 类型不匹配 、宿主 Activity 找不到问题,千言万语汇成一句话:

插件有风险,使用须谨慎!

参考链接

沐小晨曦

https://github.com/Omooo/VirtualApplication

VirtualApk 插件化

https://github.com/MicroKibaco/mk_virtual_plugin

转自:2023年插件化学习,从Activity开始

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值