服务粉丝

我们一直在努力
当前位置:首页 > 财经 >

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




最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!


推荐阅读:

CoordinatorLayout驾轻就熟,不怕UI任意需求
为了讲清楚Android触摸事件,我“拆了部手机”
分享 4 个案例,一起玩一下 ASM



点击 关注我的公众号

如果你想要跟大家分享你的文章,欢迎投稿~


┏(^0^)┛明天见!

相关阅读

  • 《生物工程学报》2023年2期目次

  • 目次Contents↓↓↓点击稿件题名可查看全文↓↓↓导读2023年2期导读综述基于CRISPR-Cas9基因编辑技术在作物中的应用殷文晶,陈振概,黄佳慧,叶涵斐,芦涛,路梅,饶玉春植物苯丙烷代谢
  • 有声海报 | 倾听两会里的“她”声音

  • 话心声、传民意从政府工作报告看新期盼来自重庆的女代表、女委员们认真履职尽责,积极建言献策围绕成渝地区双城经济圈建设乡村振兴、西部陆海新通道传统民族文化保护等方面用
  • 致敬“她力量”!当妇女节遇上全国两会……

  • ▲视频|查婧雨、万雨欣三月春光,姹紫嫣红。3月8日,“三八”国际劳动妇女节与2023年全国两会相遇。会上,女代表、女委员建言献策、共议国是,用自己独特的视角提出让生活更加美好的
  • 800亿元以上!江西组建一新公司!

  • 为推动江西现代林业产业示范省建设,加快推进江西由林业大省向林业强省转变,近日,江西省人民政府办公厅印发了《中林(江西)林业投资开发集团有限公司组建方案》。功能定位中林(江西
  • JVM常用排查工具这些你都会用吗

  • 关注我,回复关键字“spring”,免费领取Spring学习资料。阅读本文你可以学到以下命令的常规使用【jps,jinfo,jstat,jmap,jstack,jcmd,jrunscript,jjs】jps获取当前运行中java进程,示例:j
  • C盘漂红爆满怎么办?手把手教你搞定!

  • “设为星标”第一时间接收推送,精彩内容不容错过!前言通常情况下,我们电脑的第一个盘符都是C盘,在你未修改盘符的情况下,C盘也是Windows系统的系统盘。随着使用时间的增加,C盘就会

热门文章

  • “复活”半年后 京东拍拍二手杀入公益事业

  • 京东拍拍二手“复活”半年后,杀入公益事业,试图让企业捐的赠品、家庭闲置品变成实实在在的“爱心”。 把“闲置品”变爱心 6月12日,“益心一益·守护梦想每一步”2018年四

最新文章

  • 海南一批女民警和警嫂、警察母亲获通报表扬

  • 海南2名女民警、6名警嫂和6名警察母亲获公安部、全国妇联通报表扬为表扬先进、树立榜样,在“三八”国际劳动妇女节来临之际,公安部、全国妇联决定,对全国公安机关200名成绩突出
  • 今天的头条,给海南这些“花”儿们!

  • 2023年全国两会开在美好的春天里今天(3月8日)恰逢“三八”国际妇女节海南日报新媒体联合昌江黎族自治县委宣传部推出全国两会特别策划——乡村振兴主题短视频《黎花开》一起聆
  • 2023年插件化学习,从Activity开始

  • 1前言插件化技术从2015年就开始百花齐放,如: 奇虎360的replugin、任玉刚的VirtualAPK和腾讯的Shadow,插件化经历了严峻的市场考验,目前已经很成熟,今天小木箱带大家手把手学习插
  • 促进妇女提高职业技能 实现高质量充分就业

  • □ 中国妇女报全媒体记者 周丽婷 今年两会,全国政协委员、全国妇联副主席、书记处书记张晓兰带来一份提案,提案建议:为女性提供更多培训资源,加大女性培训经费保障,促进妇女