插件化 · 入门篇 · 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 找不到问题,千言万语汇成一句话:

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


相关文章
|
开发框架 Dart 前端开发
从零到应用:我的Flutter项目开发之旅
Flutter是一种流行的跨平台移动应用开发框架,由Google推出。它使用Dart编程语言,通过单一代码库可以同时构建iOS和Android应用。Flutter具有许多吸引力的特性,如快速的渲染性能、漂亮的用户界面、丰富的组件库以及热重载等。通过阅读这篇文章,你将获得一些关于Flutter项目开发的实际指导,可以帮助你更有效地构建高质量的移动应用程序。无论你是初学者还是有一定经验的开发者,希望这些笔记能够为你提供一些有用的思路和技巧,让你在Flutter项目开发中取得更好的成果。
|
3月前
|
C# Windows IDE
WPF入门实战:零基础快速搭建第一个应用程序,让你的开发之旅更上一层楼!
【8月更文挑战第31天】在软件开发领域,WPF(Windows Presentation Foundation)是一种流行的图形界面技术,用于创建桌面应用程序。本文详细介绍如何快速搭建首个WPF应用,包括安装.NET Framework和Visual Studio、理解基础概念、创建新项目、设计界面、添加逻辑及运行调试等关键步骤,帮助初学者顺利入门并完成简单应用的开发。
90 0
|
程序员 Android开发
一位Android大牛的BAT面试心得与经验总结,通用流行框架大全
一位Android大牛的BAT面试心得与经验总结,通用流行框架大全
一位Android大牛的BAT面试心得与经验总结,通用流行框架大全
|
Android开发
Android组件化开发实践(七):开发常见问题及解决方案
我们在单一工程里开发时代码运行良好,但是在进行组件化开发时,经常会出现一些莫名其妙的问题。 1. ButterKnife无法使用 组件化之后,在library中使用ButterKnife,会发现引用R.id的地方都会飘红报错: 查看错误原因都是:Attribute value must be constant。
2319 0
|
前端开发 Java 测试技术
|
Java Linux Android开发
|
前端开发 测试技术 数据库
下一篇
无影云桌面