Android 插件化

news2025/1/19 20:19:38

demo

如果要加载插件模块编译的apk插件包中的Activity类,需要执行如下流程:

1)加载类对象:使用DexClassLoader加载Activity对应的Class字节码类对象;

2)管理生命周期:处理加载进来的Activity类的生命周期,创建ProxyActivity,通过其生命周期回调,管理插件包中加载的未纳入应用管理的组件Activity类;

3)注入上下文:为加载进来的Activity类注入上下文;

4)加载资源:使用AssetManager将插件包apk中的资源主动加载进来。
 

1.创建工程

app为宿主app 

plugin 为插件app

plugin_core为插件化的核心加载文件

2.app

//配置selinux权限:allow untrusted_app app_data_file:file { execute };
public class MainActivity extends AppCompatActivity {

    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);


        if(checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED
                && checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { //有存储权限
//           路径为 /storage/emulated/0/Android/data/com.example.insert/files
            String path = getExternalFilesDir(null).getPath()+"/plugin-debug.apk"; //插件模块apk存放位置

            PluginManager.getInstance().loadPlugin( this, path); // 加载插件模块的apk文件

        } else { //没有存储权限,申请权限

            requestPermissions(new String[]{ Manifest.permission.READ_EXTERNAL_STORAGE,Manifest.permission.WRITE_EXTERNAL_STORAGE}, 100);

        }

    }

    public void onClick(View view) {

        //要跳转到plugin_core模块的代理Activity,首先要获取插件apk中的入口Activity类

        Intent intent = new Intent(this, ProxyActivity.class);

        ActivityInfo[] activityInfos = PluginManager.getInstance().getmPackageInfo().activities; // 获取插件apk中的Activity数组信息

        if (activityInfos.length > 0) { // 获取的插件包中的Activity不为空 , 才进行界面跳转

            // 取插件apk中的第1个Activity,次序就是在AndroidManifest.xml清单文件中定义Activity组件的次序,因此必须将Launcher Activity定义在第一个位置,不能在Launcher Activity之前定义Activity组件

            intent.putExtra("className", activityInfos[0].name); // 传入的是代理的目标组件的全类名,即插件apk中第一个activity的全类名

            startActivity(intent);

        }

    }

}
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.insert">

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
    <application
        android:requestLegacyExternalStorage="true"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.Insert">
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

2.plugin_core

定义一个Activity基类BaseActivity,继承AppCompatActivity并实现PluginActivityInterface接口。插件模块中的Activity类都继承该类,它是具体的Activity业务类的父类。

BaseActivity中涉及到的生命周期函数重复了,如AppCompatActivity中的onCreate()方法与PluginActivityInterface接口中的onCreate()方法是重复的,这里在每个方法前面加上@SuppressLint("MissingSuperCall")注解,忽略该报错。
 

public class BaseActivity extends AppCompatActivity implements PluginActivityInterface {

    private Activity proxyActivity; //注入的Activity,代理该Activity类作为上下文

    @Override

    public void attach(Activity proxyActivity) {

        this.proxyActivity = proxyActivity;  //注入代理Activity,在PluginActivity中将代理Activity组件注入进来

    }

    @Override

    public void setContentView(int layoutResID) {

        // 调用代理Activity中的setContentView方法

        if (proxyActivity != null) {

            proxyActivity.setContentView( layoutResID);

        }else{

            super.setContentView(layoutResID);

        }

    }

    @Override

    public void setContentView(View view) {

        if (proxyActivity != null) {

            proxyActivity.setContentView(view);

        }else{

            super.setContentView(view);

        }

    }

    @Override

    public <T extends View> T findViewById(int id){

        if (proxyActivity != null) {

            return proxyActivity.findViewById(id);

        }else{

            return super.findViewById(id);

        }

    }

    @Override

    public void startActivity(Intent intent) {

        if (proxyActivity != null) {

            intent.putExtra("className", intent.getComponent().getClassName());

            proxyActivity.startActivity(intent);

        }else{

            super.startActivity(intent);

        }

    }

    @SuppressLint("MissingSuperCall")

    @Override

    public void onCreate(Bundle savedInstanceState) {

    }

    @SuppressLint("MissingSuperCall")

    @Override

    public void onStart() {

    }

    @SuppressLint("MissingSuperCall")

    @Override

    public void onResume() {

    }

    @SuppressLint("MissingSuperCall")

    @Override

    public void onPause() {

    }

    @SuppressLint("MissingSuperCall")

    @Override

    public void onStop() {

    }

    @SuppressLint("MissingSuperCall")

    @Override

    public void onDestroy() {

    }

    @SuppressLint("MissingSuperCall")

    @Override

    public void onSaveInstanceState(Bundle outState) {

    }

}
public interface PluginActivityInterface {
    //绑定代理Activity

    void attach(Activity proxyActivity);

    void onCreate(Bundle savedInstanceState);

    void onStart();

    void onResume();

    void onPause();

    void onStop();

    void onDestroy();

    void onSaveInstanceState(Bundle outState);

    boolean onTouchEvent(MotionEvent event);

    void onBackPressed();

}
public class PluginManager {

    private DexClassLoader mDexClassLoader;//类加载器,用于加载插件包apk中的classes.dex文件中的字节码对象

    private Resources mResources; //从插件包apk中加载的资源

    private PackageInfo mPackageInfo; //插件包信息类

    private Context mContext; //加载插件的上下文对象

    private static PluginManager instance;

    //获取单例类

    public static PluginManager getInstance(){

        if (instance == null) {

            instance = new PluginManager();

        }

        return instance;

    }

    // 加载插件,参数分别为加载插件的app的上下文、加载的插件包地址

    public void loadPlugin(Context context, String loadPath) {

        this.mContext = context;

        //DexClassLoader的optimizedDirectory目录必须是私有,即模式为Context.MODE_PRIVATE

        File optimizedDirectory = context.getDir( "cache_plugin", Context.MODE_PRIVATE);
        Log.e("TAG",loadPath);

        mDexClassLoader = new DexClassLoader( loadPath, optimizedDirectory.getAbsolutePath(), null, context.getClassLoader()); // 创建类加载器

        // 加载资源

        try {

            AssetManager assetManager = AssetManager.class.newInstance(); //通过反射创建AssetManager

            Method addAssetPathMethod = assetManager.getClass().getDeclaredMethod("addAssetPath", String.class); //通过反射获取AssetManager中的addAssetPath隐藏方法

            addAssetPathMethod.invoke( assetManager, loadPath); //调用反射方法

            mResources = new Resources( assetManager, context.getResources().getDisplayMetrics(), context.getResources().getConfiguration()); //获取资源

            mPackageInfo = context.getPackageManager().getPackageArchiveInfo(loadPath, PackageManager.GET_ACTIVITIES); // 获取插件包中的Activity类信息

        } catch (Exception e) {
            e.printStackTrace();

        }

    }

    //获取类加载器

    public DexClassLoader getmDexClassLoader(){

        return mDexClassLoader;

    }

    //获取插件包中的Package信息

    public PackageInfo getmPackageInfo() {

        return mPackageInfo;

    }

    //获取插件包中的资源

    public Resources getmResources() {

        return mResources;

    }

}

 

在宿主apk中需要跳转到插件apk时,首先创建一个跳转到ProxyActivity(在核心库里)的intent,在intent中加上插件apk的入口activity。这样ProxyActivity作为代理Activity ,它持有从插件apk中加载的PluginActivity类对象。

ProxyActivity是空的Activity,没有任何实际的业务逻辑,只是作为一个生命周期的转接代理接口。但是ProxyActivity有着完整的生命周期回调机制,在进入该界面时会回调onCreate、onStart、onResume生命周期方法,在退出该界面时会回调 onPause、onStop、onDestroy生命周期方法。因此只需要在ProxyActivity的生命周期方法中,调用PluginActivity相应的生命周期方法即可。而且ProxyActivity运行时会有上下文,PluginActivity使用上下文时调用ProxyActivity的上下文。
注意:插件模块的包名可以和宿主的包名一样也可以不一样。当插件apk的包名和宿主一样时,要注意插件apk里的Activity名字一定不要和宿主apk中Activity的名字一样,否则包名和Activity名都一样就无法跳转到插件Activity了。

//该Activity只是个空壳,主要用于持有从插件apk加载的Activity类,并在ProxyActivity生命周期方法中调用对应PluginActivity类的生命周期方法

public class ProxyActivity extends Activity {

    private String className = ""; //被代理的目标Activity组件的全类名

    private PluginActivityInterface pluginActivity;//插件包中的Activity界面组件

    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_proxy);

        className = getIntent().getStringExtra("className"); //得到要代理的目标组件的全类名,即插件apk中的入口activity的全类名

        try {

            Class<?> clazz = getClassLoader().loadClass(
                    className); //使用类加载器加载插件中的界面组件。注意这里类加载器必须是PluginManager中的类加载器,即getClassLoader()是重写的方法,该方法返回PluginManager中的类加载器

            Activity activity = (Activity) clazz.newInstance(); //使用反射创建插件apk中的Activity

            if (activity instanceof PluginActivityInterface) { // 判断Activity组件是否是PluginActivityInterface接口类型的

                this.pluginActivity =
                        (PluginActivityInterface) activity; // 如果是PluginActivityInterface类型 , 则强转为该类型

                pluginActivity.attach(
                        this);  // 上下文注入。将ProxyActivity绑定注入到插件包的PluginActivity类中。该ProxyActivity具有运行的上下文,一旦绑定注入成功,则被代理的PluginActivity也具有了上下文

                pluginActivity.onCreate(savedInstanceState);  //调用pluginActivity的onCreate生命周期

            }

        } catch (Exception e) {

            e.printStackTrace();

        }

    }

    @Override

    protected void onStart() {

        super.onStart();

        pluginActivity.onStart();

    }

    @Override

    protected void onResume() {

        super.onResume();

        pluginActivity.onResume();

    }

    @Override

    protected void onPause() {

        super.onPause();

        pluginActivity.onPause();

    }

    @Override

    protected void onStop() {

        super.onStop();

        pluginActivity.onStop();

    }

    @Override

    protected void onDestroy() {

        super.onDestroy();

        pluginActivity.onDestroy();

    }

    @Override

    protected void onSaveInstanceState(Bundle outState) {

        super.onSaveInstanceState(outState);

        pluginActivity.onSaveInstanceState(outState);

    }

    @Override

    public boolean onTouchEvent(MotionEvent event) {

        super.onTouchEvent(event);

        return pluginActivity.onTouchEvent(event);

    }

    @Override

    public void onBackPressed() {

        //super.onBackPressed();

        //pluginActivity.onBackPressed();

        finish();

    }

    //重写getClassLoader方法,因为这里需要使用插件包加载的类加载器

    @Override

    public ClassLoader getClassLoader() {

        return PluginManager.getInstance().getmDexClassLoader();

    }

    //重写getResources方法,因为这里需要使用插件包加载的资源

    @Override

    public Resources getResources() {

        return PluginManager.getInstance().getmResources();

    }
}

 

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.plugin_core">
    <application>
        <activity android:name=".ProxyActivity"/>
    </application>

</manifest>

3.plugin

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.plugin">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.Insert">
        <activity
            android:name=".PluginActivity"
            android:exported="true">
        </activity>
    </application>

</manifest>

 

所有的插件包中的Activity都要继承 BaseActivity。

这样写的目的是为了方便在代理Activity中可以随意调用插件apk中的Activity类的生命周期函数,这些生命周期函数都是protected方法,不能直接调用,否则每个方法调用时,还要先反射修改访问性,才能调用
 

public class PluginActivity extends BaseActivity {

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

代码完成,接下来看如何运行:

①首先编译插件模块,生成插件apk安装包:

②拷贝插件apk

将编译的插件包apk拷贝到 /storage/emulated/0/Android/data/com.example.insert/files目录中。在 " Device FIle Explorer " 面板中 , 右键点击/storage/emulated/0/DCIM目录 , 点击"Upload " 即可将apk上传到指定位置。

③运行宿主模块,在onCreate方法中就会加载解析插件apk,解析dex文件与资源文件。此时点击跳转按钮,即可跳转到插件模块Activity中。
 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/44200.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【算法自由之路】前缀树 桶排序之计数排序和基数排序

【算法自由之路】前缀树 & 桶排序之计数排序和基数排序 前缀树&#xff08;字典树&#xff09; 首先是前缀树&#xff0c;前缀树是由字符构成的树结构&#xff0c;它记录有多少前缀字符通过&#xff0c;以及有多少个同样的字符串&#xff0c;其找这类信息的时间复杂度是极…

minigui编译移植

minigui编译移植 一:文件系统依赖支持二:交叉编译libminigui-1.6.10三:交叉编译mg-samples-1.6.10四:资源minigui-res-1.6.10四:开发板拷贝资源五:/etc/MiniGUI.cfg配置文件修改六:系统环境变量设置一:文件系统依赖支持 zlib libpng libjpeg 二:交叉编译libminigui-1.6.10 conf…

第五届安洵杯网络挑战赛WP

Crypto Cry1 crypto签到题&#xff0c;就是先对SHA256的哈希值进行爆破&#xff0c;然后猜数字 用hashcat一条命令秒穿 hashcat --custom-charset1 ?d?l?u -a 3 -m 1400 3075696ea46516c3a0a43930fab5a0f1c68ea4b315dd87a9cd123dac7f20f3a6 ?1?1?1?1GJWVMYlh5ApWLbF…

MySQL源码分析之SQL函数执行

1.MySQL中执行一条SQL的总体流程 一条包含函数的SQL语句&#xff0c;在MySQL中会经过: 客户端发送&#xff0c;服务器连接&#xff0c;语法解析&#xff0c;语句执行的过程。 调试源码&#xff0c;分析函数的具体执行过程&#xff0c;在客户端&#xff0c;执行select to_char…

【数据结构与算法】初识时间空间复杂度

文章目录1.数据结构与算法概念2.时间复杂度3.大O计数法表示时间复杂度4.线性结构与非线性结构1.数据结构与算法概念 &#xff08;1&#xff09;什么是数据结构 数据结构指的是相互之间有一种或者多种特定的关系数据元素集合。数据结构可以分成逻辑结构和物理结构。逻辑结构&a…

全网首发克莱斯勒东南大捷龙jeep道奇DIY数码碟盒增加USB和蓝牙播放音乐功能使用原车接口无损改装

文章目录前言碟盒功能1、设计指标3、外观设计4、PCB设计5、程序设计6、调试7、大捷龙车机尾插接口定义公头东南大捷龙车机白色插头模块与白色插头连接方法8、安装方法9、 使用方法9.1 CD车机按钮功能定义11、 联系我前言 ​ 之前写过四篇关于车机增加音频输入的方法。 1、07宝…

[数据结构] 并查集

并查集相关概念并查集的模拟实现1&#xff09;实现基本框架2&#xff09;实现基础操作findRoot查找元素属于哪个集合Union合并两个集合IsOneSet判断两个元素是否属于同一集合SetSize集合个数相关概念 初始时&#xff0c;每个数据的下标都为-1&#xff0c;表示10棵树&#xff1…

【EDA365电子论坛】RISC-V 能否超越 x86、Arm,成为新一代计算机系统架构?

前言 指令集架构(Instruction Set Architecture&#xff0c;缩写为ISA&#xff09;&#xff0c;是一组指令的集合&#xff0c;指令是指处理器进行操作的最小单元&#xff08;譬如加减乘除操作或者读&#xff0f;写存储器数据&#xff09;。指令集架构&#xff0c;有时简称为“架…

[附源码]SSM计算机毕业设计小超市进销存管理系统JAVA

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

【js】日期控件的实现

需求&#xff1a;通过日期控件实现只显示年月 效果如下图&#xff1a; 日期控件使用的是My97DatePicker&#xff1a; 可以从官网下载&#xff1a;http://www.my97.net/&#xff0c;或者&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1KRXSjfqpyguZ67vBrOWM8g 提取码…

Python创建增量目录的代码实例

目录1、需求很简单2、代码3、使用方法1、需求很简单 比如我在做机器学习实验的时候&#xff0c;实验结果的保存路径是’runs/exp’。 这样就会出现一个问题&#xff1a;当我第二次运行程序的时候&#xff0c;如果我忘记更改代码中的路径名或者清除上次实验结果&#xff0c;这…

BurpSuite官方实验室之逻辑漏洞

BurpSuite官方实验室之逻辑漏洞 这是BurpSuit官方的实验室靶场&#xff0c;以下将记录个人逻辑漏洞共11个Lab的通关过程 Web Security Academy: Free Online Training from PortSwigger lab1&#xff1a; Excessive trust in client-side controls 过度信任客户端控件 目…

PyTorch学习笔记-Convolution Layers与Pooling Layers

1. Convolution Layers 由于图像是二维的&#xff0c;因此基本上最常用到的就是二维的卷积类&#xff1a;torch.nn.Conv2d&#xff0c;官方文档&#xff1a;torch.nn.Conv2d。 Conv2d 的主要参数有以下几个&#xff1a; in_channels&#xff1a;输入图像的通道数&#xff0c…

IDEA关于数据库报错SQL dialect is not configured或Unable to resolve table ‘表名‘

目录一、SQL dialect is not configured1.1 报错场景展示1.2 方式一&#xff0c;万能altenter1.3 方式二&#xff0c;在setting中设置二、Unable to resolve table 表名2.1 报错场景展示2.2 方式一&#xff0c;万能altenter2.3 方式二&#xff0c;在setting中设置一、SQL diale…

vscode开发STM32(三)---调试篇

vscode开发STM32&#xff08;三&#xff09;—调试篇 文章目录vscode开发STM32&#xff08;三&#xff09;---调试篇前提条件配置调试配置JLink使用JLinkGDB进行调试配置stlink使用openOCD进行调试完整的launch文件内容前提条件 安装Cortex-Debug插件 安装OpenOCD 安装JLink驱…

LeetCode HOT 100 —— 48.旋转图像

题目 给定一个 n n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。 你必须在 原地 旋转图像&#xff0c;这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。 思路 方法一&#xff1a;使用辅助数组 可以得出规律&#xff0c;将图像旋…

集合框架----源码解读HashMap篇(一)

1.HashMap官方介绍 基于哈希表的Map接口实现。该实现提供了所有可选的映射操作&#xff0c;并允许空值和空键。(HashMap类大致相当于Hashtable&#xff0c;除了它是非同步的&#xff0c;并且允许为空值。)这个类不保证映射的顺序;特别是&#xff0c;它不能保证顺序随时间的推移…

Nodejs -- Express托管静态资源

文章目录托管静态资源1 expess.static()2 托管多个静态资源目录3 挂载路径前缀托管静态资源 1 expess.static() express提供了一个非常好用的函数&#xff0c;叫做express.static()&#xff0c;通过它&#xff0c;我们可以非常方便地创建一个静态资源服务器&#xff0c;例如&…

PG::FunboxEasyEnum

nmap -Pn -p- -T4 --min-rate1000 192.168.81.132 nmap -Pn -p 22,80 -sCV 192.168.81.132 80端口是Apache2 Ubuntu的默认页面 尝试路径爆破 /mini.php可以进行文件上传 直接上传reverse-php-shell 上传linpeas脚本进行枚举&#xff0c;得到oracle用户的密码hash oracle…

2022-11-28-大数据可视化“可视化国产/进口电影票房榜单”分析,特征维度大于50

可视化国产/进口电影票房榜单前言数据分析数据可视化过程分析总结前言 党的十八大以来&#xff0c;国产电影产业与事业快速发展&#xff0c;创作水平不断提高&#xff0c;题材类型丰富多元&#xff0c;受众口碑不断提升&#xff0c;在市场竞争中表现愈发突出&#xff0c;已成为…