Android-插件化探索(一)

浩浩
• 阅读 1739

前言

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

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
4个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Easter79 Easter79
3年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Eclipse插件开发_学习_00_资源帖
一、官方资料 1.eclipseapi(https://www.oschina.net/action/GoToLink?urlhttp%3A%2F%2Fhelp.eclipse.org%2Fmars%2Findex.jsp%3Ftopic%3D%252Forg.eclipse.platform.doc.isv%252Fguide%2
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
10个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这