容器模式插件(一)——解析APK

本文介绍了Android应用中使用容器模式实现插件化的原理,包括为何使用插件化、代理模式的缺点以及容器模式的解析APK过程。通过反射机制解析PackageParser以获取APK信息,然后利用ActivityThread的startActivityNow方法绕过AMS启动插件Activity。启动Activity需要构建Activity、LoadedApk、ContextImpl和Application等关键对象。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

基本介绍

使用插件的原因:

  • 减小安装包的体积,通过网络选择性地进行插件下发
  • 模块化升级,减小网络流量
  • 静默升级,用户无感知情况下进行升级
  • 解决低版本机型方法数超限导致无法安装的问题
  • 代码解耦

目前插件方式

  代理模式。在主程序的AndroidManifest.xml中声明一些代理ActivityProxyActivity,启动插件中的Activity会转为启动主程序中的一个ProxyActivityProxyActivity中所有系统回调都会调用插件Activity中对应的实现,最后的效果就是启动的这个Activity实际上是主程序中已经声明的一个Activity,但是相关代码执行的却是插件Activity中的代码。

   缺点:

   1).不够灵活,依赖代理Activity(Service)
   2).需要在AndroidManifest配置的属性无法动态添加,每种配置需对应一种代理的Activity(Service)
   3).插件需要经过修改才能独立运行(虽然改动很小)
   4).插件代码无法断点调试。问题追踪比较麻烦(只能打log)

容器模式

Hack Activity(Service)启动过程,将AndroidManifest的信息动态的注入到运行环境里。用一个容器Activity启动插件的Activity。
优点:真正意义上的独立插件,插件部分无需做特殊的逻辑,和独立开发的app一样。

下面就介绍如何完成一个容器模式的插件框架。

1.解析APK
Android 安装一个APK的时候首先会解析APKPackageManagerService会调用PackageParser.parserPackage方法来解析APK清单。PackageParser存在于android.content.pm包中,解析AndroidManifest.xml文件,将所有APKManifest封装到各种对象中并保存在内存当中。

解析完之后,使用android.content.pm下各种xxxInfo类来进行封装:



所以第一步我们先来试一试使用PackageParser.parserPackage解析apk中的info。PackageParser是一个隐藏的类,所以可以利用Java的反射机制来访问其中隐藏的类和方法。


<span style="font-family:Tahoma;">  public void loadedAPK(){
      String apkPath = Environment.getExternalStorageDirectory() + "/Download/plugin/plugin.apk";
        try {
            // 这是一个Package 解释器, 是隐藏的  
            // 构造函数的参数只有一个, apk文件的路径  
            Class cPackageParser = Class.forName("android.content.pm.PackageParser");
            Class[] typeArgs = new Class[1];
            typeArgs[0] = String.class;
            Constructor conPackageParser= cPackageParser .getConstructor(typeArgs);
            Object[] valueArgs = new Object[1];
            valueArgs[0] = apkPath;
            Object packageParserObj = conPackageParser.newInstance(valueArgs);
            // 这个是与显示有关的, 里面涉及到一些像素显示等等, 我们使用默认的情况
            DisplayMetrics metrics = new DisplayMetrics();
            metrics.setToDefaults();
            typeArgs = new Class[4];
            typeArgs[0] = File.class;
            typeArgs[1] = String.class;
            typeArgs[2] = DisplayMetrics.class;
            typeArgs[3] = Integer.TYPE;
            Method mParsePackage = cPackageParser .getDeclaredMethod("parsePackage", typeArgs);
            valueArgs = new Object[4];
            valueArgs[0] = new File(apkPath);
            valueArgs[1] = apkPath;
            valueArgs[2] = metrics;
            valueArgs[3] = 0;
            Object packageObj = mParsePackage .invoke(packageParserObj , valueArgs);
            // 获取应用程序信息包
            Field appInfoFld = packageObj.getClass().getDeclaredField("applicationInfo");
            ApplicationInfo info = (ApplicationInfo) appInfoFld.get(packageObj);
            Log.d("ANDROID_LAB", "pkg:" + info.packageName + " uid=" + info.uid);
            //获取Activity信息
            Field factivities = packageObj.getClass().getDeclaredField("activities");
            ArrayList<Object> activitiesObject = (ArrayList<Object>)factivities.get(packageObj);
            Class cActivityInfo = Class.forName("android.content.pm.PackageParser$Activity");
            Field factivityInfo= cActivityInfo .getDeclaredField("info");
            for (Object object : activitiesObject){
                ActivityInfo actInfo = (ActivityInfo)cActivityInfo.get(object);
                Log.d("ANDROID_LAB", " name=" + actInfo.name);
            }
        } catch (Exception e) {
              e.printStackTrace();
        }
    }</span>

从Logcat中可以看到输出了Application的包名、uid以及Activity的名称,这样我们就已经成功解析了APK中的信息。



2.要将这些信息注入到运行环境中。

构造一个LoadedApk ,构造DexClassLoader和Resources,设置给LoadedApk,然后将LoadedApk实例加到 mainThread的Packages里面去
 

获得AndroidManifest.xml信息后,如何启动Activity呢?无论是通过Launcher来启动Activity,还是通过Activity内部调用startActivity来启动新的Activity,都通过Binder进程间通信进入到ActivityManagerService进程中ActivityManagerService没有提供相关接口,没办法通过AMS启动插件。但是研究源码发现ActivityThread中的startActivityNow这个方法绕过了AMS,就是说AMS此时根本不知道已经启动了一个Activity。所以我们可以用startActivityNow获取插件的Activity对象,再将Activity实例的DecorView设置成代理Activity的ContentView的方法启动插件Activity。


public final Activity startActivityNow(Activity parent, String id,
Intent intent, ActivityInfo activityInfo, IBinder token, Bundle state,
Activity.NonConfigurationInstances lastNonConfigurationInstances) {
        ...
  return performLaunchActivity(r, null);
}


我们可以看到startActivityNow调用了performLaunchActivity。应用程序进程通过performLaunchActivity函数将即将要启动的Activity加载到当前进程空间来,同时为启动Activity做准备。在该函数中,首先通过PMS服务查找到即将启动的Activity的包名信息,然后通过类反射方式创建一个该Activity实例,同时为应用程序启动的每一个Activity创建一个LoadedApk实例对象,应用程序进程中创建的所有LoadedApk对象保存在ActivityThread的成员变量mPackages中。接着通过LoadedApk对象的makeApplication函数,使用单例模式创建Application对象,因此在android应用程序进程中有且只有一个Application实例。然后为当前启动的Activity创建一个ContextImpl上下文对象,并初始化该上下文,到此我们可以知道,启动一个Activity需要以下对象:
1)      Activity对象,需要启动的Activity;
2)      LoadedApk对象,每个启动的Activity都拥有属于自身的LoadedApk对象;
3)      ContextImpl对象,每个启动的Activity都拥有属于自身的ContextImpl对象;
4)      Application对象,应用程序进程中有且只有一个实例,和Activity是一对多的关系;

这些对象需要我们自己构建。

1)  

//
ComponentName componentName = new ComponentName(pkg,cls);
//
mActivityInfo = load.getActivityInfo(componentName);
         Object[] startActivityNowParams = new Object[7];
            startActivityNowParams[0] = null;
            startActivityNowParams[1] = mActivityInfo.targetActivity;
            startActivityNowParams[2] = innerIntent;
            startActivityNowParams[3] = mActivityInfo;
            startActivityNowParams[4] = null;
            startActivityNowParams[5] = savedInstanceState;
            startActivityNowParams[6] = null;
            Method mStartActivityNow = mReflectManager.cActivityThread.mClass.getDeclaredMethod("startActivityNow",new Class[] {Activity.class, String.class, Intent.class,
                    ActivityInfo.class, IBinder.class, Bundle.class, mReflectManager.cConfigurationInstances.mClass});
            mStartActivityNow.setAccessible(true);
            mActivity =  (Activity)mStartActivityNow.invoke(load.getMainThreadObj(), startActivityNowParams);





评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值