【android每日一问】插件化原理解析(转载,原文链接找不到了)

// 尝试获取缓存信息
WeakReference ref;
if (differentUser) {
// Caching not supported across users
ref = null;
} else if (includeCode) {
ref = mPackages.get(aInfo.packageName);
} else {
ref = mResourcePackages.get(aInfo.packageName);
}

LoadedApk packageInfo = ref != null ? ref.get() : null;
if (packageInfo == null || (packageInfo.mResources != null
&& !packageInfo.mResources.getAssets().isUpToDate())) {
// 缓存没有命中,直接new
packageInfo =
new LoadedApk(this, aInfo, compatInfo, baseLoader,
securityViolation, includeCode &&
(aInfo.flags&ApplicationInfo.FLAG_HAS_CODE) != 0, registerPackage);

// 省略。。更新缓存
return packageInfo;
}
}

接下来我们分析一下上面这些代码:

  • 首先判断调用方和获取App信息的一方是不是同一个userId,如果是同一个User,那么可以共享缓存数据(包括代码数据和资源数据)。
  • 接下来尝试获取缓存,如果没有缓存,才通过LoadedApk的构造函数创建LoadedAPk对象,创建成功后如果是同一个user还会放入缓存中。

通过上述分析,我们只需要想办法拿到这份缓存数据,修改里面的ClassLoader,自己控制类加载的过程,这样加载插件中的Activity类的问题就解决了。这也是我们之前说的第一种方式。

方案一:Hook掉ClassLoader

通过分析得知,在获取LoadedApk的过程中使用了一份缓存数据,缓存在一个Map中,通过包名到LoadedApk的一个映射,正常情况下我们的插件肯定不会存在于这个对象里,但是如果我们手动把插件信息添加到里面呢?这样系统在查找缓存的时候,直接命中我们添加的ClassLoadeer,这样我们就直接接管了类加载的过程。

通过查找我们发现缓存对象mPackages存在于ActivityThread类中。

首先我们获取这个对象:

// 先获取到当前的ActivityThread对象
Class<?> activityThreadClass = Class.forName(“android.app.ActivityThread”);
Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod(“currentActivityThread”);
currentActivityThreadMethod.setAccessible(true);
Object currentActivityThread = currentActivityThreadMethod.invoke(null);

接着获取mPackages这个对象

// 获取到 mPackages 这个静态成员变量, 这里缓存了dex包的信息
Field mPackagesField = activityThreadClass.getDeclaredField(“mPackages”);
mPackagesField.setAccessible(true);
Map mPackages = (Map) mPackagesField.get(currentActivityThread);

拿到我们想要的Map,接下来就是把插件的信息插入map当中,也就是说我们要插入我们需要的包名以及一个LoadedApk对象。那么如何创建一个LoadedApk对象呢?

我们可以用与系统完全相同的方式来创建LoadedApk对象,系统创建是通过getPackageInfo来完成的,但是这个函数是个私有函数,不到万不得已我们尽量不去反射私有函数调用,避免产生很多兼容性问题。

发现有同名的public函数getPackageInfogetPackageInfoNoCheck,前者除了获取包的信息,还检查了包的一些组件,为了绕过这些检查,我们选择getPackageInfoNoCheck来获取LoadedApk对象。

构建插件LoadedApk对象

这一步就是通过getPackageInfoNoCheck函数创建出我们需要的LoadedApk对象,以供接下来使用。

public final LoadedApk getPackageInfoNoCheck(ApplicationInfo ai,CompatibilityInfo compatInfo) {

  • 我们需要传递两个参数ApplicationInfo和CompatibilityInfo
  • CompatibilityInfo这个参数代表App的兼容性信息,比如taregtSDK版本等等,我们只需要取出app的信息,因此直接使用默认的兼容性即可,在CompatibilityInfo类中有一个共有字段DEFAULT_COMPATIBILITY_INFO代表默认兼容性信息。

接下来我们的目标就是获取ApplicationInfo对象。

构建插件ApplicationInfo对象

首先看看ApplicationInfo是什么?

nformation you can retrieve about a particular application.
This corresponds to information collected from the AndroidManifest.xml’s tag.

显而易见,这个类就是xml里面<application>标签下的信息,AndroidManifest.xml是一个标准的xml文件,因此我们完全可以自己使用parse来解析这些信息。

但是,系统是如何获取这些信息的呢?

在Framework中就有一个这样的parser,叫做PackageParser,所以我们可以通过系统的parser来解析AndroidManifest.xml从而得到ApplicationInfo的信息,但是这里需要注意的是,这个类的兼容性很差,Google几乎每个版本都会对这个类做修改,你需要写大量的兼容代码来保证。 如下(DroidPlugin做的兼容处理):

DroidPlugin.png

经查发现PackageParser有方法generateApplication可以成功的拿到ApplicationInfo。由于其是@hide的,因此我们需要通过反射调用。

public static ApplicationInfo generateApplicationInfo(Package p, int flags,PackageUserState state)

反射代码如下:

Class<?> packageParserClass = Class.forName("android.content.pm.PackageParser"); Class<?> packageParser P a c k a g e C l a s s = C l a s s . f o r N a m e ( " a n d r o i d . c o n t e n t . p m . P a c k a g e P a r s e r PackageClass = Class.forName("android.content.pm.PackageParser PackageClass=Class.forName("android.content.pm.PackageParserPackage");
Class<?> packageUserStateClass = Class.forName(“android.content.pm.PackageUserState”);
Method generateApplicationInfoMethod = packageParserClass.getDeclaredMethod(“generateApplicationInfo”,
packageParser$PackageClass,
int.class,
packageUserStateClass);

要成功调用这个方法,还需要三个参数;因此接下来我们需要一步一步构建调用此函数的参数信息。

构建PackageParser.Package

generateApplicationInfo方法需要的第一个参数是PackageParser.Package;从名字上看这个类代表某个apk包的信息,我们看看文档怎么解释:

Representation of a full package parsed from APK files on disk. A package consists of a single base APK, and zero or more split APKs.

果然,这个类代表从PackageParser中解析得到的某个apk包的信息,是磁盘上apk文件在内存中的数据结构表示;因此,要获取这个类,肯定需要解析整个apk文件。PackageParser中解析apk的核心方法是parsePackage,这个方法返回的就是一个Package类型的实例,因此我们调用这个方法即可;使用反射代码如下:

// 首先, 我们得创建出一个Package对象出来供这个方法调用
// 而这个需要得对象可以通过 android.content.pm.PackageParser#parsePackage 这个方法返回得 Package对象得字段获取得到
// 创建出一个PackageParser对象供使用
Object packageParser = packageParserClass.newInstance();
// 调用 PackageParser.parsePackage 解析apk的信息
Method parsePackageMethod = packageParserClass.getDeclaredMethod(“parsePackage”, File.class, int.class);

// 实际上是一个 android.content.pm.PackageParser.Package 对象
Object packageObj = parsePackageMethod.invoke(packageParser, apkFile, 0);

这样,我们就得到了generateApplicationInfo的第一个参数;第二个参数是解析包使用的flag,我们直接选择解析全部信息,也就是0;

构建PackageUserState

第三个参数是PackageUserState,代表不同用户中包的信息。由于Android是一个多任务多用户系统,因此不同的用户同一个包可能有不同的状态;这里我们只需要获取包的信息,因此直接使用默认的即可.

至此,generateApplicaionInfo的参数我们已经全部构造完成,直接调用此方法即可得到我们需要的applicationInfo对象;在返回之前我们需要做一点小小的修改:使用系统的这个方法解析得到的ApplicationInfo对象中并没有apk文件本身的信息,所以我们把解析的apk文件的路径设置一下(ClassLoader依赖dex文件以及apk的路径):

// 第三个参数 mDefaultPackageUserState 我们直接使用默认构造函数构造一个出来即可
Object defaultPackageUserState = packageUserStateClass.newInstance();

ApplicationInfo applicationInfo = (ApplicationInfo) generateApplicationInfoMethod.invoke(packageParser,
packageObj, 0, defaultPackageUserState);
String apkPath = apkFile.getPath();
applicationInfo.sourceDir = apkPath;
applicationInfo.publicSourceDir = apkPath;

替换ClassLoader

获取LoadedApk信息

我们最终的目的是调用getPackageInfoNoCheck得到LoadedApk的信息,并替换其中的mClassLoader然后把把添加到ActivityThread的mPackages缓存中;从而达到我们使用自己的ClassLoader加载插件中的类的目的。

现在我们已经拿到了getPackageInfoNoCheck这个方法中至关重要的第一个参数applicationInfo;上文提到第二个参数CompatibilityInfo代表设备兼容性信息,直接使用默认的值即可;因此,两个参数都已经构造出来,我们可以调用getPackageInfoNoCheck获取LoadedApk:

// android.content.res.CompatibilityInfo
Class<?> compatibilityInfoClass = Class.forName(“android.content.res.CompatibilityInfo”);
Method getPackageInfoNoCheckMethod = activityThreadClass.getDeclaredMethod(“getPackageInfoNoCheck”, ApplicationInfo.class, compatibilityInfoClass);

Field defaultCompatibilityInfoField = compatibilityInfoClass.getDeclaredField(“DEFAULT_COMPATIBILITY_INFO”);
defaultCompatibilityInfoField.setAccessible(true);

Object defaultCompatibilityInfo = defaultCompatibilityInfoField.get(null);
ApplicationInfo applicationInfo = generateApplicationInfo(apkFile);
Object loadedApk = getPackageInfoNoCheckMethod.invoke(currentActivityThread, applicationInfo, defaultCompatibilityInfo);

我们成功地构造出了LoadedAPK, 接下来我们需要替换其中的ClassLoader,然后把它添加进ActivityThread的mPackages中:

String odexPath = Utils.getPluginOptDexDir(applicationInfo.packageName).getPath();
String libDir = Utils.getPluginLibDir(applicationInfo.packageName).getPath();
ClassLoader classLoader = new CustomClassLoader(apkFile.getPath(), odexPath, libDir, ClassLoader.getSystemClassLoader());
Field mClassLoaderField = loadedApk.getClass().getDeclaredField(“mClassLoader”);
mClassLoaderField.setAccessible(true);
mClassLoaderField.set(loadedApk, classLoader);

// 由于是弱引用, 因此我们必须在某个地方存一份, 不然容易被GC;
sLoadedApk.put(applicationInfo.packageName, loadedApk);

WeakReference weakReference = new WeakReference(loadedApk);
mPackages.put(applicationInfo.packageName, weakReference);

我们的这个CustomClassLoader非常简单,直接继承了DexClassLoader,什么都没有做;当然这里可以直接使用DexClassLoader,这里重新创建一个类是为了更有区分度;以后也可以通过修改这个类实现对于类加载的控制。

到这里,我们已经成功地把把插件的信息放入ActivityThread中,这样我们插件中的类能够成功地被加载;因此插件中的Activity实例能被成功创建。

总结如下:

  • 在ActivityThread接收到IApplication的 scheduleLaunchActivity远程调用之后,将消息转发给H。
  • H类在handleMessage的时候,调用了getPackageInfoNoCheck方法来获取待启动的组件信息。在这个方法中会优先查找mPackages中的缓存信息,而我们已经手动把插件信息添加进去;因此能够成功命中缓存,获取到独立存在的插件信息。
  • H类然后调用handleLaunchActivity最终转发到performLaunchActivity方法;这个方法使用从getPackageInfoNoCheck中拿到LoadedApk中的mClassLoader来加载Activity类,进而使用反射创建Activity实例;接着创建Application,Context等完成Activity组件的启动。

看起来好像已经天衣无缝万事大吉了;但是运行一下会出现一个异常,如下:

java.lang.RuntimeException: Unable to start activity ComponentInfo{com.com.sohu.test.app/com.sohu.test.app.MainActivity}:
java.lang.RuntimeException: Unable to instantiate application android.app.Application: java.lang.IllegalStateException:
Unable to get package info for com.sohu.test.app; is package not installed?

错误提示说是无法实例化 Application,而Application的创建也是在performLaunchActivity中进行的,这里有些蹊跷,我们仔细查看一下发现:

通过ActivityThread的performLaunchActivity方法可以得知,Application通过LoadedApk的makeApplication方法创建,我们查看这个方法,在源码中发现了上文异常抛出的位置:

try {
java.lang.ClassLoader cl = getClassLoader();
if (!mPackageName.equals(“android”)) {
initializeJavaContextClassLoader();
}
ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
app = mActivityThread.mInstrumentation.newApplication(
cl, appClass, appContext);
appContext.setOuterContext(app);
} catch (Exception e) {
if (!mActivityThread.mInstrumentation.onException(app, e)) {
throw new RuntimeException(
"Unable to instantiate application " + appClass

  • ": " + e.toString(), e);
    }
    }

经过排查发现:

private void initializeJavaContextClassLoader() {
IPackageManager pm = ActivityThread.getPackageManager();
android.content.pm.PackageInfo pi;
try {
pi = pm.getPackageInfo(mPackageName, 0, UserHandle.myUserId());
} catch (RemoteException e) {
throw new IllegalStateException("Unable to get package info for "

  • mPackageName + “; is system dying?”, e);
    }
    if (pi == null) {
    throw new IllegalStateException("Unable to get package info for "
  • mPackageName + “; is package not installed?”);
    }

boolean sharedUserIdSet = (pi.sharedUserId != null);
boolean processNameNotDefault =
(pi.applicationInfo != null &&
!mPackageName.equals(pi.applicationInfo.processName));
boolean sharable = (sharedUserIdSet || processNameNotDefault);
ClassLoader contextClassLoader =
(sharable)
? new WarningContextClassLoader()
: mClassLoader;
Thread.currentThread().setContextClassLoader(contextClassLoader);
}

这里,我们找出了这个异常的来源:原来这里调用了getPackageInfo方法获取包的信息;而我们的插件并没有安装在系统上,因此系统肯定认为插件没有安装,这个方法肯定返回null。所以,我们还要欺骗一下PMS,让系统觉得插件已经安装在系统上了.

private static void hookPackageManager() throws Exception {

// 这一步是因为 initializeJavaContextClassLoader 这个方法内部无意中检查了这个包是否在系统安装
// 如果没有安装, 直接抛出异常, 这里需要临时Hook掉 PMS, 绕过这个检查.

Class<?> activityThreadClass = Class.forName(“android.app.ActivityThread”);
Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod(“currentActivityThread”);
currentActivityThreadMethod.setAccessible(true);
Object currentActivityThread = currentActivityThreadMethod.invoke(null);

// 获取ActivityThread里面原始的 sPackageManager
Field sPackageManagerField = activityThreadClass.getDeclaredField(“sPackageManager”);
sPackageManagerField.setAccessible(true);
Object sPackageManager = sPackageManagerField.get(currentActivityThread);

// 准备好代理对象, 用来替换原始的对象
Class<?> iPackageManagerInterface = Class.forName("android.content.pm.IPackageManager"); Object proxy = Proxy.newProxyInstance(iPackageManagerInterface.getClassLoader(), new Class<?>[] { iPackageManagerInterface },
new IPackageManagerHookHandler(sPackageManager));

// 1. 替换掉ActivityThread里面的 sPackageManager 字段
sPackageManagerField.set(currentActivityThread, proxy);
}

到这里,我们已经能够成功地加载简单的独立的存在于外部文件系统中的apk了。

上面我们是通过开头我们说的第一种方法实现,下面介绍另一种方案:

方案二:委托系统,让系统帮忙加载

再来看看ActivityThread中加载Activity类的代码:

java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
StrictMode.incrementExpectedActivityCount(activity.getClass());
r.intent.setExtrasClassLoader(cl);

我们知道 这个r.packageInfo中的r是通过getPackageInfoNoCheck获取到的;在上述方案中我们把插件apk手动添加进缓存,采用自己加载办法解决;如果我们不干预这个过程,导致无法命中mPackages中的缓存,会发生什么?

从上一种方案可以知道如果没有命中缓存的情况下,系统直接new了一个LoadedApk;注意这个构造函数的第二个参数aInfo,这是一个ApplicationInfo类型的对象。在方案一中我们为了获取独立插件的ApplicationInfo花了不少心思;那么如果不做任何处理这里传入的这个aInfo参数是什么?

追本溯源不难发现,这个aInfo是从我们的替身StubActivity中获取的,而StubActivity存在于宿主程序中,所以,这个aInfo对象代表的实际上就是宿主程序的Application信息。

接下来会使用new出来的这个LoadedApk的getClassLoader()方法获取到ClassLoader来对插件的类进行加载;而获取到的这个ClassLoader是宿主程序使用的ClassLoader,因此现在还无法加载插件的类;那么,我们能不能让宿主的ClasLoader获得加载插件类的能力呢?如果我们告诉宿主使用的ClassLoader插件使用的类在哪里,就能帮助他完成加载!

宿主的ClassLoader在哪里,是唯一的吗?

答案是肯定的!

因为在FrameWork中宿主程序也是使用LoadedApk表示的,如同Activity启动是加载Activity类一样,宿主中的类也都是通过LoadedApk的getClassLoader()方法得到的ClassLoader加载的;由类加载机制的双亲委派特性,只要有一个应用程序类由某一个ClassLoader加载,那么它引用到的别的类除非父加载器能加载,否则都是由这同一个加载器加载的(不遵循双亲委派模型的除外)。

表示宿主的LoadedApk在Application类中有一个成员变量mLoadedApk,而这个变量是从ContextImpl中获取的;ContextImpl重写了getClassLoader方法,因此我们在Context环境中直接getClassLoader()获取到的就是宿主程序唯一的ClassLoader。

LoadedApk的ClassLoader到底是什么?

不论是宿主程序还是插件程序都是通过LoadedApk的getClassLoader()方法返回的ClassLoader进行类加载的,返回的这个ClassLoader到底是个什么?这个方法源码如下:

public ClassLoader getClassLoader() {
synchronized (this) {
if (mClassLoader != null) {
return mClassLoader;
}

if (mIncludeCode && !mPackageName.equals(“android”)) {
// 略…
mClassLoader = ApplicationLoaders.getDefault().getClassLoader(zip, lib,
mBaseClassLoader);

StrictMode.setThreadPolicy(oldPolicy);
} else {
if (mBaseClassLoader == null) {
mClassLoader = ClassLoader.getSystemClassLoader();
} else {
mClassLoader = mBaseClassLoader;
}
}
return mClassLoader;
}
}

可以看到,非android开头的包和android开头的包分别使用了两种不同的ClassLoader,我们只关心第一种;因此继续跟踪ApplicationLoaders类:

public ClassLoader getClassLoader(String zip, String libPath, ClassLoader parent)
{

ClassLoader baseParent = ClassLoader.getSystemClassLoader().getParent();

synchronized (mLoaders) {
if (parent == null) {
parent = baseParent;
}

if (parent == baseParent) {
ClassLoader loader = mLoaders.get(zip);
if (loader != null) {
return loader;
}

Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, zip);
PathClassLoader pathClassloader =
new PathClassLoader(zip, libPath, parent);
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);

mLoaders.put(zip, pathClassloader);
return pathClassloader;
}

Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, zip);
PathClassLoader pathClassloader = new PathClassLoader(zip, parent);
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
return pathClassloader;
}
}

可以看到,应用程序使用的ClassLoader都是PathClassLoader类的实例。那么,这个PathClassLoader是什么呢?从Android SDK给出的源码只能看出这么多:

public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
super((String)null, (File)null, (String)null, (ClassLoader)null);
throw new RuntimeException(“Stub!”);

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助

因此我收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
26)]

[外链图片转存中…(img-ROUz7TzS-1715797879827)]

[外链图片转存中…(img-GxArJgF2-1715797879828)]

[外链图片转存中…(img-SM2RuY2m-1715797879829)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

  • 9
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值