插件化 · 入门篇 · 2023年插件化学习,从Activity开始

简介: 插件化 · 入门篇 · 2023年插件化学习,从Activity开始

1681600414244.png

改不完的 Bug,写不完的矫情。公众号 小木箱成长营 现在专注移动基础平台开发 ,涵盖音视频, APM和信息安全等各个知识领域;只做全网最 Geek 的公众号,欢迎您的关注!精彩内容不容错过~

前言

插件化技术从 2015 年就开始百花齐放,如: 奇虎 360 的 replugin,滴滴的 VirtualAPK,到现在的 VirtualApp,插件化经历了市场严峻的考验,也算逐步成熟,今天就带大家手把手实现一个插件化Activity框架,希望对你有所帮助~

插件化概念

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


现在市面使用插件化一定程度上可以减少安装包大小,实现项目组件化,将项目拆分方便隔离,降低组件化耦合度太高的问题


当然插件化也能 实现 bug 热修复,由于虚拟机的存在,Java 本身是支持动态加载任意类的。只是安卓系统在四大组件上做了限制,当你尝试打开不在清单中的组件时,给你一个崩溃。


所谓插件化,本质上是为了绕过这个限制,使得应用可以自由地打开和使用四大组件。

插件化业务价值

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


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

插件化项目结构

1681600491679.png

1681600524262.png


插件化开发流程

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

1681600561495.png

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


image.png

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

image.png

第四步: 安装插件

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 创建一个 AssertManager,再构造 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);
    }
}

image.png

第九步: 启动插件的入口 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);
    }

插件化原理介绍

  1. 使用 DexClassLoader 加载插件的 Apk
  2. 通过代理的 Activity 去执行插件中的 Activity,加载对应的生命周期
  3. 通过反射调用 AssetManager 的 addAssetPath 来加载插件中的资源

插件化遇到的问题

找到的 Activity 不在插件包里面

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

解决方案

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

资源 Id 类型不匹配 找不到

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

解决方案

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

插件包 leakcanary 引发的崩溃

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

解决方案

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

总结

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

插件有风险,投资须谨慎!


相关文章
|
3月前
|
设计模式 前端开发 Android开发
Android应用开发中的MVP架构模式解析
【5月更文挑战第25天】本文深入探讨了在Android应用开发中广泛采用的一种设计模式——Model-View-Presenter (MVP)。文章首先概述了MVP架构的基本概念和组件,接着分析了它与传统MVC模式的区别,并详细阐述了如何在实际开发中实现MVP架构。最后,通过一个具体案例,展示了MVP架构如何提高代码的可维护性和可测试性,以及它给开发者带来的其他潜在好处。
|
2月前
|
设计模式 安全 前端开发
探索Android应用开发的最佳实践
【6月更文挑战第19天】在这篇文章中,我们将深入探讨Android应用开发的最佳实践。从设计模式的选择到性能优化的技巧,我们将一一解析如何构建高效、可维护且用户友好的Android应用。无论你是新手还是经验丰富的开发者,这篇文章都将为你提供有价值的见解和实用的建议。让我们一起探索Android应用开发的奥秘吧!
38 4
|
3月前
|
存储 前端开发 Java
Android应用开发中的MVP架构模式实践
【5月更文挑战第5天】随着移动应用开发的复杂性增加,传统的MVC(Model-View-Controller)架构在应对大型项目时显得笨重且不灵活。本文将探讨一种更适应现代Android应用开发的架构模式——MVP(Model-View-Presenter),并展示如何在Android项目中实现该模式以提升代码的可维护性和可测试性。通过对比分析MVP与传统MVC的差异,以及提供一个实际案例,读者将能深入了解MVP的优势和实施步骤。
|
存储 ARouter 前端开发
Android组件化演进-第一篇
Android组件化演进-第一篇
184 0
Android组件化演进-第一篇
|
XML 架构师 Java
Activiti工作流引擎基础入门【收藏可做笔记系列】上
Activiti工作流引擎基础入门【收藏可做笔记系列】
Activiti工作流引擎基础入门【收藏可做笔记系列】上
|
XML 数据可视化 Java
Activiti工作流引擎基础入门【收藏可做笔记系列】下
Activiti工作流引擎基础入门【收藏可做笔记系列】
Activiti工作流引擎基础入门【收藏可做笔记系列】下
|
Kubernetes Cloud Native 架构师
如何设计架构图?一文带你了解架构设计的本质 | 开发者社区精选文章合集(二十九)
架构图是什么?为什么要画架构图?如何画?有哪些方法?,架构设计的本质是什么?教你如何设计一张合格的架构图!
如何设计架构图?一文带你了解架构设计的本质 | 开发者社区精选文章合集(二十九)
|
Java Linux Android开发