Android-插件化探索(一)

浩浩 等级 709 1 1

前言

由于近期项目中要用到插件,所以特地去翻找资料学习了一番,现在在这里分享我所学到的东西给大家,有什么错误的希望能给我指出来,文章有点长,希望大家能认真读完。 近些年来,插件化可谓是特别的火热,就拿支付宝美团等软件来说,都是使用这个技术来支撑他们的产品。但是什么是插件化呢,插件化到底有什么好处呢? 插件化也就是运行的APP(宿主APP)去加载插件APP(没有安装的APP),这就是所谓的插件化开发。

Android-插件化探索(一)

插件化到底运行在什么场景下呢?其实插件化使用的场景有很多,这里就比如下图的支付宝或者美团等APP,点击某个相应的item,就会跳转到相应的页面当中,其实这个页面是插件apk中的页面,但是它到底怎么做到的呢?怎么做到不安装apk而加载插件中的页面呢?

Android-插件化探索(一)

下面我们就来探索探索不用安装插件apk是怎么去加载里面的Activity、Service、BroadCastReceiver等这些组件的。本篇文章所提的是占位式(插桩式)插件化。 由于插件apk是没有安装的,也就是插件apk没有组件的一些环境,比如context上下文对象之类的,如果要用到这些环境就必须依赖宿主的环境运行。所以我们就要宿主跟插件之间定义一个标准。用来传递宿主中的环境给插件。 Android-插件化探索(一)

加载插件中的Activity

第一步:

首先我们先定义一个标准,让插件实现我们的标准来传递宿主APP的环境,下图是定义Activity类的标准,下面我们从加载插件中的Activity开始讲起。 Android-插件化探索(一) Android-插件化探索(一)

首先我们要在插件中实现刚才我们定义的标准,由于插件都需要宿主APP的环境,所以我们就定义一个基类来实现该标准,然后让我们的插件的Activity来继承该基类,该Activity就具有了宿主的环境了。如图所示。

Android-插件化探索(一)

第二步:

build工程,得到插件apk,命名为 plugin.apk 并把它放到我们的sd目录下。让宿主APP来加载插件。 首先我们要加载插件apk中的类,就需要用到DexClassLoader这个类,下面是用该类来加载插件apk的方法。

 File file = new File(path);
 File pluginDir = context.getDir("plugin", Context.MODE_PRIVATE);
 //加载插件的class
 dexClassLoader = new DexClassLoader(path, pluginDir.getAbsolutePath(), null, context.getClassLoader());

参数说明: path:插件所存放的目录(plugin.apk存放的目录) pluginDir.getAbsolutePath():插件apk解析后dex文件所存放的路径 null:该参数是so库存放的路径,由于插件里没有so库,所以为null。 context.getClassLoader():类加载器

获取到了DexClassLoader的对象,我们就可以拿到插件中的类了,接下来我们要获取插件apk中的资源对象,也就是 Resources对象。由于插件apk没有宿主的环境,也就无法使用 context.getResources()的方式来得到一个Resources对象,我们只能new 一个对象出来。

从上面代码中可以看出,我们可以看出创建一个 Resources对象需要传递一个 AssetManager、DisplayMetrics、Configuration对象,而这些对象我们怎么可以获取到呢?其实后两个参数很容易获取到。后面的两个参数我们可以直接用宿主的Resources对象里面的属性就可以了,如下

 Resources appResources = context.getResources();
 DisplayMetrics displayMetrics = appResources.getDisplayMetrics();
 Configuration configuration = appResources.getConfiguration();

现在我们比较头疼的问题是如何获取AssetManager这个类的对象?其实我们可以利用反射的方式来获取一个AssetManager的对象,如下

AssetManager pluginAssetManager = AssetManager.class.newInstance();

阅读源码发现,AssetManager类里面要更改资源路径就必须要调用 addAssetPath 方法。从这里得出我们可以反射拿到这个方法,然后传入插件apk的资源路径即可。

AssetManager pluginAssetManager = AssetManager.class.newInstance();
Method addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class);
addAssetPath.setAccessible(true);
addAssetPath.invoke(pluginAssetManager, path);

通过上述代码可以获取到我们的插件apk的 AssetManager对象了,拥有该对象,我们就可以获取到 Resources对象了,总体代码如下:

 //加载插件的资源文件
 //1、获取插件的AssetManager
 AssetManager pluginAssetManager = AssetManager.class.newInstance();
 Method addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class);
 addAssetPath.setAccessible(true);
 addAssetPath.invoke(pluginAssetManager, path);
 //2、获取宿主的Resources
 Resources appResources = context.getResources();
 //实例化插件的Resources
 pluginResource = new Resources(pluginAssetManager, appResources.getDisplayMetrics(), appResources.getConfiguration());

拿到了DexClassLoader对象和Resources对象我们就可以加载插件apk中的activity等组件了。要跳转到插件中的activity,我们首先要获取到插件找那个的Activity,通过 PackageManager.getPackageArchiveInfo()我们可以得到一个 PackageInfo对象,而这个对象里面就有我们需要的Activity数组,如下图:

Android-插件化探索(一) 获取插件Activity数组我们就可以拿到Activity对象,并且可以进行跳转了:

//获取插件包的Activity
PackageManager packageManager = getPackageManager();
PackageInfo packageArchiveInfo = packageManager.getPackageArchiveInfo(path, PackageManager.GET_ACTIVITIES);
//获取在manifest文件中注册的第一个activity
ActivityInfo activity = packageArchiveInfo.activities[0];
Intent intent = new Intent(this, ProxyActivity.class);
intent.putExtra("className", activity.name);

通过以上代码显示,我们先是跳转到了一个ProxyActivity,然后把插件的Activity类名传递过去,为什么不直接跳转到插件的Activity呢?原因是因为插件的Activity没有在我们的宿主的manifest文件中进行注册,如果直接跳转就会发生崩溃,所以我们这里先跳转到一个代理的Activity,因为该Activity是宿主里面的并且在manifest文件中进行注册了,所以我们用它来进行模拟Activity入栈出栈的操作,当我们的代理Activity回调onCreate() 的时候,我们可以获取到传递过来的className来反射得到我们的插件中的Activity对象,由于我们插件中的Activity是实现了上面所述的标准,我们可以通过标准来调用插件Activity中的生命周期。

  @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //真正的加载插件里面的Activity
        String className = getIntent().getStringExtra("className");
        try {
            Class<?> pluginActivity1Clazz = getClassLoader().loadClass(className);
            Constructor<?> constructor = pluginActivity1Clazz.getConstructor(new Class[]{});
            pluginActivity1 = (IActivityInterface) constructor.newInstance(new Object[]{});
            pluginActivity1.insertAppContext(this);
            Bundle bundle = new Bundle();
            bundle.putString("value", "我是宿主传递过来的字符串");
            pluginActivity1.onCreate(bundle);

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

    }

当执行完上述代码后我们就可以加载出插件的Activity了



插件内跳转Activity


跳转成功后,我们就要实现插件中的Activity跳转到插件中的另一个Activity了。大家都知道跳转Activity的话我们都要执行startActivity() 方法,而查看源码而知,startActivity() 里调用了 this.startActivity(intent, null);这里的 this表示着上下文对象,但是我们的插件APP没有上下文对象的环境,如果执行了 startActivity() 方法肯定会发生崩溃的,那么怎样才能避免崩溃,并且成功跳转呢。其实非常简单,我们只要使用宿主APP的环境就可以来实现该功能了,只不过是稍微有些麻烦。我们需要重写 startActivity() 方法,并且传递我们需要跳转的Activity的全类名给宿主,且跳转的方法交给我们的宿主来进行就可以了,然后我们重写宿主APP的代理Activity 的startActivity() 方法用来接收插件APP传递过来的全类名,最后执行宿主APP的代理Activity 的startActivity() 方法即可。如下:

Android-插件化探索(一)

Android-插件化探索(一)

另外提示一点,我们插件的Activity 所有使用的都是宿主的环境,比如 setContentView() 、findViewById() 等方法

至此,我们动态加载插件APP的Activity已经讲完了,至于动态加载Service与动态加载广播的方法跟加载Activity的方法类似,这里不做讲解


注册插件内的静态广播

要加载插件中的静态广播,我们先来提问一个问题。我们APP中的静态广播到底是什么时候被注册的呢? 其实在手机开机的时候,手机里面所有的已经安装APP,系统会再次进行安装一遍,这也就能说明Android手机开机的时候为什么会那么慢。等到安装完成后,会马上扫描 /data/app/ 目录,然后逐个去解析该目录下的所有 apk里面的 Manifest.xml 文件, 如果里面有静态广播后,就会自动注册。 我们现在来研究系统是如何去解析 apk文件里面的组件信息的。我们知道解析apk 文件会用到 PackageManagerService 这个类,所以我们来查看这个类的源码。

Android-插件化探索(一)

跟踪源码发现,执行到 packageParser.parsePackage() 这个方法的时候会返回一个 PackageParser.Package 对象,我们进去查看一下 PackageParser 这个类的源码,发现 Package 是其里面的静态内部类。

Android-插件化探索(一)

以下是代码的实现:

Class<?> mPackageParserClass = Class.forName("android.content.pm.PackageParser");
Object mPackageParser = mPackageParserClass.newInstance();
Method parsePackageMethod = mPackageParserClass.getMethod("parsePackage", File.class, int.class);
Object mPackage = parsePackageMethod.invoke(mPackageParser, file, PackageManager.GET_ACTIVITIES);

//2、获取Package类下的   public final ArrayList<Activity> receivers = new ArrayList<Activity>(0); 广播集合
Field mReceiversField = mPackage.getClass().getDeclaredField("receivers");
//本质上是 ArrayList<Activity> receivers
ArrayList<Object> receivers = (ArrayList<Object>) mReceiversField.get(mPackage);

第二步:

在第一步我们获取到了 receivers 这个广播集合,有了这个集合我们就可以拿到里面的广播来进行注册了。我们先来看看先跟到这个 List 尖括号里面的对象,发现这个Activity 不是我们四大组件的Activity,它纯粹是为了封装这些数据的Java bean,所以千万不要把它搞混了。

Android-插件化探索(一) 我们可以发现里面有个ActivityInfo 的属性。我们继续跟这个类和他的父类的源码,结果发现,name 这个属性刚好就是 android:name="com.xxx.xxx" 对应里面的包名跟类名,我们需要获取到这个值来 new 一个广播对象出来。

Android-插件化探索(一)

我们现在想办法得到上述 Activity 里面的 info这个属性这样我们就可以获得 name的值了。继续阅读 PackageParser 的源码,我们发现 Android-插件化探索(一) 通过这个方法我们可以得到一个 ActivityInfo 对象,所以我们就来执行这个方法。执行该方法我们需要传递四个参数,第一个Activity类型我们已经有了,第二个参数我们可以直接传个0,第三个我们可以反射来获取一个类的实例。如

 Class<?> mPackageUserStateClass = Class.forName("android.content.pm.PackageUserState");
 Object mPackageUserState = mPackageUserStateClass.newInstance();

现在就剩下最后一个参数了,就是我们的userId,到底怎么获取这个参数值呢?其实我们可以从 android.os.UserHandle 这个类入手,分析该类得出的结果 Android-插件化探索(一) 通过图中的方法可以获取一个userId;所以我们就以反射来获取该userId; 详细代码如下:

//先获取到 ActivityInfo类
Class<?> mPackageUserStateClass = Class.forName("android.content.pm.PackageUserState");
Object mPackageUserState = mPackageUserStateClass.newInstance();

Method generateActivityInfoMethod = mPackageParserClass.getMethod("generateActivityInfo", mActivity.getClass(),
int.class, mPackageUserStateClass, int.class);
//获取userId
Class<?> mUserHandleClass = Class.forName("android.os.UserHandle");
//public static @UserIdInt int getCallingUserId()
int userId = (int) mUserHandleClass.getMethod("getCallingUserId").invoke(null);

//执行此方法 由于是静态方法 所以不用传对象
ActivityInfo activityInfo = (ActivityInfo) generateActivityInfoMethod.invoke(null, mActivity, 0, mPackageUserState, userId);

以上我们就可以获取到一个ActivityInfo 对象了。

第三步: 我们知道注册广播的时候还要添加一个IntentFliter,但是这个应该从哪里取到呢? 其实我们刚才分析的 Activity 那个类说起 ,它继承与 Component 在跟进这个类去看,发现里面有个 intents 的集合,查看泛型继承的 IntentInfo,就是我们想要的 IntentFliter,所以我们要得到这个集合,然后进行遍历就可以得到 IntentFliter,然后我们就可以注册广播了。 Android-插件化探索(一) Android-插件化探索(一) 所以注册广播的代码如下:

//3、遍历所有的静态广播
//Activity 该Activity 不是四大组件里面的activity,而是一个Java bean对象,用来封装清单文件中的activity和receiver
for (Object mActivity : receivers) {
    //4、获取该广播的全类名 即 <receiver android:name=".PluginStaticReceiver"> android:name属性后面的值
    //  /**
    //     * Public name of this item. From the "android:name" attribute.
    //     */
    //    public String name;

    // public static final ActivityInfo generateActivityInfo(Activity a, int flags,
    //            PackageUserState state, int userId)
    //先获取到 ActivityInfo类
    Class<?> mPackageUserStateClass = Class.forName("android.content.pm.PackageUserState");
    Object mPackageUserState = mPackageUserStateClass.newInstance();

    Method generateActivityInfoMethod = mPackageParserClass.getMethod("generateActivityInfo", mActivity.getClass(),
    int.class, mPackageUserStateClass, int.class);
    //获取userId
    Class<?> mUserHandleClass = Class.forName("android.os.UserHandle");
    //public static @UserIdInt int getCallingUserId()
    int userId = (int) mUserHandleClass.getMethod("getCallingUserId").invoke(null);

    //执行此方法 由于是静态方法 所以不用传对象
    ActivityInfo activityInfo = (ActivityInfo) generateActivityInfoMethod.invoke(null, mActivity, 0, mPackageUserState, userId);
    String receiverClassName = activityInfo.name;
    Class<?> receiverClass = getClassLoader().loadClass(receiverClassName);
    BroadcastReceiver receiver = (BroadcastReceiver) receiverClass.newInstance();

    //5、获取 intent-filter  public final ArrayList<II> intents;这个是intent-filter的集合
    //静态内部类反射要用 $+类名
    //getField(String name)只能获取public的字段,包括父类的;
    //而getDeclaredField(String name)只能获取自己声明的各种字段,包括public,protected,private。
    Class<?> mComponentClass = Class.forName("android.content.pm.PackageParser$Component");
    Field intentsField = mActivity.getClass().getField("intents");
    ArrayList<IntentFilter> intents = (ArrayList<IntentFilter>) intentsField.get(mActivity);
    for (IntentFilter intentFilter : intents) {
        //6、注册广播
        context.registerReceiver(receiver, intentFilter);
    }
}

到这里我们已经完成了对插件中的静态广播进行注册

下面附上Github的项目地址,给需要的小伙伴进行参考:

https://github.com/onecalf/android-plugin-1

收藏
评论区

相关推荐

Android-插件化探索(一)
前言 由于近期项目中要用到插件,所以特地去翻找资料学习了一番,现在在这里分享我所学到的东西给大家,有什么错误的希望能给我指出来,文章有点长,希望大家能认真读完。 近些年来,插件化可谓是特别的火热,就拿支付宝美团等软件来说,都是使用这个技术来支撑他们的产品。但是什么是插件化呢,插件化到底有什么好处呢? 插件化也就是运行的APP(宿主APP)去加载插件APP
Android Activity生命周期,启动模式,启动过程详解
前言 接触过Android开发的同学都知道Activity,Activity作为Android四大组件之一,使用频率高。简单来说Activity提供了一个显示界面,让用户进行各种操作,本文主要分为以下三个部分:Activity的生命周期,启动模式,以及Activity的工作过程。文中大部分篇幅来自《Android开发艺术探索》一书,尽管想多以流程或图
Android中一个Activity关闭另一个Activity或者在一个Activity中关闭多个Activity
前言 最近项目中涉及需要在一个Activity中关闭另一个Activity或者在一个Activity中关闭多个Activity的需求,不涉及到应用的退出。自己首先想了一些方案,同时也查了一些方案,就各个方案比较下优劣。 方案一 广播的方式 这个是最容易想到的,同时也是网上提供最多的。 由于多个Activity要使用,关闭页面
Android输入法遮挡了输入框,使用android:fitsSystemWindows="true"后界面顶部出现白条
问题1、页面布局文件:<LinearLayout xmlns:android"http://schemas.android.com/apk/res/android" android:id"@+id/layoutorderdetail" android:layoutwidth"matchparent" android:layoutheigh
Android深入理解Context(二)Activity和Service的Context创建过程
Android框架层 Android深入理解Contextcategories: Android框架层本文首发于微信公众号「刘望舒」 前言上一篇文章我们学习了Context关联类和Application Context的创建过程,这一篇我们接着来学习Activity和Service的Context创建过程。需要注意的是,本篇的知识点会和深入理解四大组件系列的
Android深入四大组件(一)应用程序启动过程(前篇)
Android框架层 Android深入四大组件categories: Android框架层本文首发于微信公众号「后厂技术官」 前言在此前的文章中,我讲过了Android系统启动流程和Android应用进程启动过程,这一篇顺理成章来学习Android 7.0的应用程序的启动过程。分析应用程序的启动过程其实就是分析根Activity的启动过程。<!more 1
Android深入四大组件(二)Service的启动过程
Android框架层 Android深入四大组件categories: Android框架层本文首发于微信公众号「刘望舒」 前言此前我用较长的篇幅来介绍Android应用程序的启动过程(根Activity的启动过程),这一篇我们接着来分析Service的启动过程。建议阅读此篇文章前,请先阅读和这两篇文章。<!more 1.ContextImpl到Activi
Android深入四大组件(六)Android8.0 根Activity启动过程(前篇)
Android框架层 Android深入四大组件categories: Android框架层本文首发于微信公众号「刘望舒」 前言在几个月前我写了和这两篇文章,它们都是基于Android 7.0,当我开始阅读Android 8.0源码时发现应用程序(根Activity)启动过程照Android 7.0有了一些变化,因此又写下了本篇文章,本篇文章照此前的文章不仅
Android深入四大组件(七)Android8.0 根Activity启动过程(后篇)
Android框架层 Android深入四大组件categories: Android框架层本文首发于微信公众号「刘望舒」 前言在几个月前我写了和这两篇文章,它们都是基于Android 7.0,当我开始阅读Android 8.0源码时发现应用程序(根Activity)启动过程照Android 7.0有了一些变化,因此又写下了本篇文章,本篇文章照此前的文章不仅
Android解析ActivityManagerService(二)ActivityTask和Activity栈管理
Android框架层 Android系统服务 ActivityManagerService Android框架层本文首发于微信公众号「刘望舒」 前言关于AMS,原计划是只写一篇文章来介绍,但是AMS功能繁多,一篇文章的篇幅远远不够。这一篇我们接着来学习与AMS相关的ActivityTask和Activity栈管理。 1.ActivityStackActivi
Android包管理机制(一)PackageInstaller的初始化
Android框架层 Android包管理机制 Android框架层本文首发于微信公众号「刘望舒」 前言包管理机制是Android中的重要机制,是应用开发和系统开发需要掌握的知识点之一。包指的是Apk、jar和so文件等等,它们被加载到Android内存中,由一个包转变成可执行的代码,这就需要一个机制来进行包的加载、解析、管理等操作,这就是包管理机制。包管理
Android包管理机制(二)PackageInstaller安装APK
Android框架层 Android包管理机制 Android框架层本文首发于微信公众号「刘望舒」 前言在本系列上一篇文章中我们学习了PackageInstaller是如何初始化的,这一篇文章我们接着学习PackageInstaller是如何安装APK的。本系列文章的源码基于Android8.0。 1.PackageInstaller中的处理紧接着上一篇的内
Android包管理机制(三)PMS处理APK的安装
Android框架层 Android包管理机制 Android框架层本文首发于微信公众号「刘望舒」 前言在上一篇文章中,我们学习了PackageInstaller是如何安装APK的,最后会将APK的信息交由PMS处理。那么PMS是如何处理的呢?这篇文章会给你答案。 1.PackageHandler处理安装消息APK的信息交由PMS后,PMS通过向Packag
Android包管理机制(五)APK是如何被解析的
Android框架层 Android包管理机制 Android框架层本文首发于微信公众号「刘望舒」 前言在本系列的前面文章中,我介绍了PackageInstaller的初始化和安装APK过程、PMS处理APK的安装和PMS的创建过程,这些文章中经常会涉及到一个类,那就是PackageParser,它用来在APK的安装过程中解析APK,那么APK是如何被解析的
网站打包成Apk的正确姿势
大家好,我是IT共享者,人称皮皮。前言 安卓手机想必很多人都在使用,我们手机上安卓的每一款应用的后缀名都是以“.apk”结尾,那么这些Apk是如何做出来的了,就目前小编知道的来讲,有这以下几种:1.使用三方软件转换生成,比如E4a,火山,蓝鸟,Iapp2.使用原生的Android代码,如 Android studio目前职业玩家是第二种,一般玩家大都聚集在