Android插件化原理—ClassLoader加载机制
前面《Android 插件化原理学习 —— Hook 机制之动态代理》一文中我们探索了一下动态代理 hook 实现了 启动没有在 AndroidManifest.xml 中显式声明的 Activity 的功能。我们加载的是应用内部的一个 Activity,但是通常 Android 插件化及沙箱机制都是加载外部的文件,这时我们还需要其他的机制保证插件加载,大部分插件化框架都是基于 ClassLoader 实现对插件的加载。在学习 ClassLoader 之前我们最好对一些基本的概念有些认识。
虚拟机基础
Dalvik 虚拟机
Java 虚拟机,简称 JVM (Java Virtual Machine)。JVM 字节码由 .class 文件组成,每个文件一个 class,JVM 在运行的时候为每一个类装载字节码。
Dalvik 虚拟机,简称 DVM (Dalvik Virtual Machine)。Dalvik 虚拟机运行的程序只包含一个.dex 文件,这个文件包含了程序中所有的类。Java 编译器创建了 JVM 字节码之后,Dalvik 的 dx 编译器删除 .class 文件,重新把它们编译成 Dalvik 字节码,然后把它们写进一个.dex 文件中。
大多数虚拟机包括 JVM 都是一种堆栈机器,而 Dalvik 虚拟机则是寄存器机。两种架构各有优劣,一般而言,基于堆栈的机器需要更多指令,而基于寄存器的机器指令更长。
Dalvik 虚拟机的功能:
- Dalvik 主要是完成对象生命周期管理,堆栈管理,线程管理,安全和异常管理,以及垃圾回收等等重要功能。
- Dalvik 负责进程隔离和线程管理,每一个 Android 应用在底层都会对应一个独立的 Dalvik 虚拟机实例,其代码在虚拟机的解释下得以执行。
- 不同于 Java 虚拟机运行 java 字节码,Dalvik 虚拟机运行的是其专有的文件格式 dex。
- dex 文件格式可以减少整体文件尺寸,提高 I/O 操作的类查找速度。
- odex 是为了在运行过程中进一步提高性能,对 dex 文件的进一步优化。
- 所有的 Android 应用的线程都对应一个 Linux 线程,虚拟机因而可以更多的依赖操作系统的线程调度和管理机制。
- 有一个特殊的虚拟机进程 Zygote,他是虚拟机实例的孵化器。它在系统启动的时候就会产生,它会完成虚拟机的初始化、库的加载、预制类库和初始化的操作。如果系统需要一个新的虚拟机实例,它会迅速复制自身,以最快的速度提供给系统。对于一些只读的系统库,所有虚拟机实例都和 Zygote 共享一块内存区域。
Dalvik 与 ART
从 Android 5.0 版起,Android Runtime(ART)替换 Dalvik 成为系统内默认虚拟机。(这不代表你的手机用的 6.0 系统,使用的就是 ART 虚拟机,国产 Android 系统中很多升级为 6.0 系统的任然使用的是 Dalvik)。
Android Runtime(缩写为 ART),是一种在 Android 操作系统上的运行环境,由 Google 公司研发,并在 2013 年作为 Android 4.4 系统中的一项测试功能正式对外发布,在 Android 5.0 及后续 Android 版本中作为正式的运行时库取代了以往的 Dalvik 虚拟机。ART 能够把应用程序的字节码转换为机器码,是 Android 所使用的一种新的虚拟机。它与 Dalvik 的主要不同在于:Dalvik 采用的是 JIT 技术,而 ART 采用 Ahead-of-time(AOT)技术。ART 同时也改善了性能、垃圾回收(Garbage Collection)、应用程序除错以及性能分析。
- JIT:Dalvik 虚拟机运行 App 的机制,运行期实时翻译(Just In Time)。
- AOT:ART 虚拟机对 App 运行的优化机制,它在应用安装时就提前(Ahead-Of-Time)做好了字节码到机器码的翻译工作。
获取手机的虚拟机类型:
public CharSequence getCurrentRuntimeValue() {String SELECT_RUNTIME_PROPERTY = "persist.sys.dalvik.vm.lib";String LIB_DALVIK = "libdvm.so";String LIB_ART = "libart.so";String LIB_ART_D = "libartd.so";try {Class<?> systemProperties = Class.forName("android.os.SystemProperties");try {Method get = systemProperties.getMethod("get",String.class, String.class);if (get == null) {return "未获取到";}try {final String value = (String) get.invoke(systemProperties, SELECT_RUNTIME_PROPERTY,/* Assuming default is */"Dalvik");if (LIB_DALVIK.equals(value)) {return "Dalvik";} else if (LIB_ART.equals(value)) {return "ART";} else if (LIB_ART_D.equals(value)) {return "ART debug build";}return value;} catch (IllegalAccessException e) {return "IllegalAccessException";} catch (IllegalArgumentException e) {return "IllegalArgumentException";} catch (InvocationTargetException e) {return "InvocationTargetException";}} catch (NoSuchMethodException e) {return "SystemProperties.get(String key, String def) method is not found";}} catch (ClassNotFoundException e) {return "SystemProperties class is not found";}
}
Android 编译工具
我们从上面可以知道 Android Dalvik/ART 虚拟机,不是 JVM,也不能直接加载 jar 文件,而是加载 dex 文件,那么我们如何获取能够加载的 dex 文件呢?
我们可以使用 Android SDK 的 dx 工具把 jar 文件优化成 dex 文件。dx 工具在 Android SDK build-tools 中可以找到,然后执行下面的方法即可:
# 配置环境变量
export PATH=$PATH:/Users/zhaomenghuan/Library/Android/sdk/build-tools/26.0.2
dx --dex --output=plugin.dex plugin.jar
俗话说"工欲善其事,必先利其器",如果我们需要对 Android 编译时或者运行时理解更深刻,少不了会学习编译相关的技术,这里我们介绍一下相关常用的工具。
dex2jar
开源地址:https://github.com/pxb1988/dex2jar
dex2jar 包含以下组件:
- dex-reader/writer:旨在读/写 Dalvik 可执行文件(.dex / .odex),它具有与 ASM 类似的轻量级 API;
- d2j-dex2jar:转换 .dex 文件到 .class 文件(压缩为 jar);
- d2j-smali/baksmali:将 dex 分解为 smali 文件,并从 smali 文件中组装 dex;
例如上述 dx 工具将 jar 转成 dex 也可以使用 dex2jar 实现:
添加可执行权限:
chmod a+x d2j_invoke.sh
chmod a+x d2j-dex2jar.sh
chmod a+x d2j-jar2dex.sh
dex 转 jar:
./d2j-dex2jar.sh classes.dex
jar 转 dex:
./d2j-jar2dex.sh plugin.jar
jd-gui
开源地址:https://github.com/java-decompiler/jd-gui
apktool
开源地址:https://ibotpeaches.github.io/Apktool/
我们可以通过 dex2jar + jd-gui 进行 jar/dex 的反编译,那么对于资源我们如何去处理呢?
我们可以使用 apktool 反编译资源:
apktool d xxx.apk
修改后重新打包命令:
apktool b xxx -o Newxxx.apk
ClassyShark
开源地址:https://github.com/google/android-classyshark
ClassLoader 基础
我们知道,Java 源文件(.java)经过编译器编译之后,会转换成 Java 字节码(.class),然后最终被 Java 虚拟机(JVM)执行,然而程序是如何加载这些字节码文件到内存中呢?这就用到了 ClassLoader,即类加载器。ClassLoader 类加载器负责读取 Java 字节代码,并转换成 java.lang.Class 类的一个实例。从而只有 class 文件被载入到了内存之后,才能被其程序所引用。所以 ClassLoader 就是用来动态加载 class 文件到内存当中用的。
传统 Java 时代的插件化:
从服务端下载一个 jar,然后构造一个对应路径的 ClassLoader,在直接调用 main 方法,或者反射其他入口方法即可。 Jar 直接运行在 JVM 上,直接和 JVM 交互。
ClassLoader 家族
Android 中的常用几种类加载器类型继承关系划分可以用一组关系图来表示:
- DexClassLoader:可以加载文件系统上的 jar、dex、apk
- PathClassLoader:只能加载已安装的 apk 的 dex
DexClassLoader 简介
DexClassLoader 是一个可以从包含 classes.dex 实体的.jar 或.apk 文件中加载 classes 的类加载器,可以用于实现 dex 的动态加载、代码热更新等等。Android 里面 dex 字节码运行在 Dalvik 虚拟机上。
public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent)
参数详解:
- dexPath:dex 文件路径列表,多个路径使用”:”分隔
- optimizedDirectory:经过优化的 dex 文件(odex)文件输出目录
- librarySearchPath:动态库路径(将被添加到 app 动态库搜索路径列表中)
- parent:这是一个 ClassLoader,这个参数的主要作用是保留 java 中 ClassLoader 的委托机制(优先父类加载器加载 classes,由上而下的加载机制,防止重复加载类字节码)
PathClassLoader 简介
PathClassLoader 提供一个简单的 ClassLoader 实现,可以操作在本地文件系统的文件列表或目录中的 classes,但不可以从网络中加载 classes。PathClassLoader 提供两个常用构造方法:
public PathClassLoader(String dexPath, ClassLoader parent)
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent)
参数详解:
- dexPath:文件或者目录的列表
- librarySearchPath:包含 lib 库的目录列表
- parent:父类加载器
DexClassLoader 与 PathClassLoader 的区别
PathClassLoader 与 DexClassLoader 这两个类的构造函数的第 2 个参数 optimizedDirectory 的值不一样,PathClassLoader 里面 optimizedDirectory 参数值为 null。
DexClassLoader 可以指定自己的 optimizedDirectory,可以将 dex 复制到内部路径的 optimizedDirectory,所以它可以加载外部的 dex。
DexClassLoader 实例
定义一个插件
package cn.com.agree.plugin;import android.content.Context;
import android.widget.Toast;public class ToastUtil {public void show(Context context) {Toast.makeText(context, "我是一个插件", Toast.LENGTH_SHORT).show();}
}
通过 gradle 脚本编译生成 Jar 文件:
task makeJar(type: Jar) {from zipTree(file('build/intermediates/bundles/release/classes.jar'))from fileTree(dir: 'src/main', includes: ['assets/**'])baseName = "plugin"destinationDir = file(rootProject.ext.releaseDirsPath)exclude('android/', 'BuildConfig.class', 'R.class')exclude {it.name.startsWith('R$');}
}makeJar.dependsOn(build)
DexClassLoader 加载插件反射调用
// dex压缩文件的路径(可以是 apk,jar,zip 格式)
String dexPath = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "plugin.dex";
// dex解压释放后的目录
File dexOutputDirFile = context.getDir("dex", MODE_PRIVATE);// 定义DexClassLoader
// 第一个参数:是dex压缩文件的路径
// 第二个参数:是dex解压缩后存放的目录
// 第三个参数:是C/C++依赖的本地库文件目录,可以为null
// 第四个参数:是上一级的类加载器
DexClassLoader dexClassLoader = new DexClassLoader(dexPath, dexOutputDirFile.getAbsolutePath(), null, getClassLoader());
// 使用DexClassLoader加载类
try {Class<?> clz = dexClassLoader.loadClass("cn.com.agree.plugin.ToastUtil");Object instance = clz.newInstance();Method showToast = clz.getMethod("show", Context.class);showToast.invoke(instance, context);
} catch (Exception e) {e.printStackTrace();
}
这里也可以使用接口的方式去调用,插件里面通过 ToastUtil 实现 IToast 中的方法,然后在 host App 中使用接口调用。
Hook APK 加载
重新认识 ActivityThread
在之前的文章中简单介绍了 Activity 启动过程,但是我们没有深入去探讨 APK 是怎么被启动的,在 Java / iOS Objective-C 中我们一般都是 main 方法开始执行的,但是很奇怪 Android 中我们并没有看到 main 方法,是不是 Android 中没有 main 方法呢?如果没有 main 方法,那么 Android 应用是从哪里启动的呢?
带着这个问题我们重新看看 ActivityThread 类,这个类在 Android 源码中,默认无法直接从 Android Studio 点进去阅读,我们可以在线阅读或者对本地的 Android SDK 或者 Android Studio 做些手脚(如替换为 Android.jar 去除@hide 注解的版本:https://github.com/anggrayudi/android-hidden-api),这里我使用在线地址:http://androidxref.com/ 查看 Android 源码。
ActivityThread 的 main 函数如下:
xref: /frameworks/base/core/java/android/app/ActivityThread.java#main
6459 public static void main(String[] args) {
6460 Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ActivityThreadMain");
6461
6462 // CloseGuard defaults to true and can be quite spammy. We
6463 // disable it here, but selectively enable it later (via
6464 // StrictMode) on debug builds, but using DropBox, not logs.
6465 CloseGuard.setEnabled(false);
6466
6467 Environment.initForCurrentUser();
6468
6469 // Set the reporter for event logging in libcore
6470 EventLogger.setReporter(new EventLoggingReporter());
6471
6472 // Make sure TrustedCertificateStore looks in the right place for CA certificates
6473 final File configDir = Environment.getUserConfigDirectory(UserHandle.myUserId());
6474 TrustedCertificateStore.setDefaultUserDirectory(configDir);
6475
6476 Process.setArgV0("<pre-initialized>");
6477
6478 Looper.prepareMainLooper();
6479
6480 ActivityThread thread = new ActivityThread();
6481 thread.attach(false);
6482
6483 if (sMainThreadHandler == null) {
6484 sMainThreadHandler = thread.getHandler();
6485 }
6486
6487 if (false) {
6488 Looper.myLooper().setMessageLogging(new
6489 LogPrinter(Log.DEBUG, "ActivityThread"));
6490 }
6491
6492 // End of event ActivityThreadMain.
6493 Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
6494 Looper.loop();
6495
6496 throw new RuntimeException("Main thread loop unexpectedly exited");
6497 }
在其中有两句话可以证明 handler 在 UI 线程之所以不需要初始化 looper 的原因:
Looper.prepareMainLooper();
Looper.loop();
在 main 函数中主要进行的是初始化的操作,初始化过程包括 looper,运行环境,logger 等一系列东西,在其中有个重要的方法 thread.attach(false):
xref: /frameworks/base/core/java/android/app/ActivityThread.java#attach
6478 private void attach(boolean system, long startSeq) {
6479 sCurrentActivityThread = this;
6480 mSystemThread = system;
6481 if (!system) {
6482 ViewRootImpl.addFirstDrawHandler(new Runnable() {
6483 @Override
6484 public void run() {// 检查jit是否能用
6485 ensureJitEnabled();
6486 }
6487 });
6488 android.ddm.DdmHandleAppName.setAppName("<pre-initialized>",
6489 UserHandle.myUserId());
6490 RuntimeInit.setApplicationObject(mAppThread.asBinder());// 获得IActivityManager的一个实例,IActivityManager其实算是一个binder对象,负责跟底层沟通
6491 final IActivityManager mgr = ActivityManager.getService();
6492 try {// 关联到 ApllicationThread类
6493 mgr.attachApplication(mAppThread, startSeq);
6494 } catch (RemoteException ex) {
6495 throw ex.rethrowFromSystemServer();
6496 }
6497 // 添加GC监察者
6498 BinderInternal.addGcWatcher(new Runnable() {
6499 @Override public void run() {
6500 if (!mSomeActivitiesChanged) {
6501 return;
6502 }
6503 Runtime runtime = Runtime.getRuntime();
6504 long dalvikMax = runtime.maxMemory();
6505 long dalvikUsed = runtime.totalMemory() - runtime.freeMemory();
6506 if (dalvikUsed > ((3*dalvikMax)/4)) {
6507 if (DEBUG_MEMORY_TRIM) Slog.d(TAG, "Dalvik max=" + (dalvikMax/1024)
6508 + " total=" + (runtime.totalMemory()/1024)
6509 + " used=" + (dalvikUsed/1024));
6510 mSomeActivitiesChanged = false;
6511 try {
6512 mgr.releaseSomeActivities(mAppThread);
6513 } catch (RemoteException e) {
6514 throw e.rethrowFromSystemServer();
6515 }
6516 }
6517 }
6518 });
6519 } else {
6520 // Don't set application object here -- if the system crashes,
6521 // we can't display an alert, we just want to die die die.
6522 android.ddm.DdmHandleAppName.setAppName("system_process",
6523 UserHandle.myUserId());
6524 try {
6525 mInstrumentation = new Instrumentation();
6526 mInstrumentation.basicInit(this);
6527 ContextImpl context = ContextImpl.createAppContext(
6528 this, getSystemContext().mPackageInfo);
6529 mInitialApplication = context.mPackageInfo.makeApplication(true, null);
6530 mInitialApplication.onCreate();
6531 } catch (Exception e) {
6532 throw new RuntimeException(
6533 "Unable to instantiate Application():" + e.toString(), e);
6534 }
6535 }
6536
6537 // add dropbox logging to libcore
6538 DropBox.setReporter(new DropBoxReporter());
6539
6540 ViewRootImpl.ConfigChangedCallback configChangedCallback
6541 = (Configuration globalConfig) -> {
6542 synchronized (mResourcesManager) {
6543 // We need to apply this change to the resources immediately, because upon returning
6544 // the view hierarchy will be informed about it.
6545 if (mResourcesManager.applyConfigurationToResourcesLocked(globalConfig,
6546 null /* compat */)) {
6547 updateLocaleListFromAppContext(mInitialApplication.getApplicationContext(),
6548 mResourcesManager.getConfiguration().getLocales());
6549
6550 // This actually changed the resources! Tell everyone about it.
6551 if (mPendingConfiguration == null
6552 || mPendingConfiguration.isOtherSeqNewer(globalConfig)) {
6553 mPendingConfiguration = globalConfig;
6554 sendMessage(H.CONFIGURATION_CHANGED, globalConfig);
6555 }
6556 }
6557 }
6558 };
6559 ViewRootImpl.addConfigCallback(configChangedCallback);
6560 }
我们可以继续一步步去找 ActivityThread#main 在哪里被调用了,我们可以一直找到 Zygote 的初始化环节,这里我们暂不做深入探究。
点击 Launcher 中应用图标将会执行以下的流程:
在 Android 插件化原理学习 —— Hook 机制之动态代理#动态代理 中,我们通过 Hook Activity 类的 mInstrumentation 属性,重写了 execStartActivity 方法,从而实现 Activity 的 "移花接木",经过 AMS 验证后,启动 Activity 会调用 ActivityThread 的 scheduleLaunchActivity 方法, 最终实际调用的是 mH.sendMessage(msg);
,我们 Hook ActivityThread 类的 mH 属性,重写了的 handleMessage 方法,从而实现 Activity 还原。
xref: /frameworks/base/core/java/android/app/ActivityThread.java#handleMessage
1580 public void handleMessage(Message msg) {
1581 if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
1582 switch (msg.what) {
1583 case LAUNCH_ACTIVITY: {
1584 Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart");
1585 final ActivityClientRecord r = (ActivityClientRecord) msg.obj;
1586
1587 r.packageInfo = getPackageInfoNoCheck(
1588 r.activityInfo.applicationInfo, r.compatInfo);
1589 handleLaunchActivity(r, null, "LAUNCH_ACTIVITY");
1590 Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
1591 } break;
我们之前在这里改了 r 对象的 intent 属性,将我们需要真正的 Activity 恢复回来了,但是我们没有探究这里的 r.packageInfo 和 handleLaunchActivity 方法。
我们首先看看 ActivityClientRecord 对象:
xref: /frameworks/base/core/java/android/app/ActivityThread.java#ActivityClientRecord
340 static final class ActivityClientRecord {
341 IBinder token;
342 int ident;
343 Intent intent;
344 String referrer;
345 IVoiceInteractor voiceInteractor;
346 Bundle state;
347 PersistableBundle persistentState;
348 Activity activity;
349 Window window;
350 Activity parent;
351 String embeddedID;
352 Activity.NonConfigurationInstances lastNonConfigurationInstances;
353 boolean paused;
354 boolean stopped;
355 boolean hideForNow;
356 Configuration newConfig;
357 Configuration createdConfig;
358 Configuration overrideConfig;
359 // Used for consolidating configs before sending on to Activity.
360 private Configuration tmpConfig = new Configuration();
361 // Callback used for updating activity override config.
362 ViewRootImpl.ActivityConfigCallback configCallback;
363 ActivityClientRecord nextIdle;
364
365 ProfilerInfo profilerInfo;
366
367 ActivityInfo activityInfo;
368 CompatibilityInfo compatInfo;
369 LoadedApk packageInfo;
370
371 List<ResultInfo> pendingResults;
372 List<ReferrerIntent> pendingIntents;
373
374 boolean startsNotResumed;
375 boolean isForward;
376 int pendingConfigChanges;
377 boolean onlyLocalRequest;
378
379 Window mPendingRemoveWindow;
380 WindowManager mPendingRemoveWindowManager;
381 boolean mPreserveWindow;
382
383 // Set for relaunch requests, indicates the order number of the relaunch operation, so it
384 // can be compared with other lifecycle operations.
385 int relaunchSeq = 0;
386
387 // Can only be accessed from the UI thread. This represents the latest processed message
388 // that is related to lifecycle events/
389 int lastProcessedSeq = 0;
390
391 ActivityClientRecord() {
392 parent = null;
393 embeddedID = null;
394 paused = false;
395 stopped = false;
396 hideForNow = false;
397 nextIdle = null;
398 configCallback = (Configuration overrideConfig, int newDisplayId) -> {
399 if (activity == null) {
400 throw new IllegalStateException(
401 "Received config update for non-existing activity");
402 }
403 activity.mMainThread.handleActivityConfigurationChanged(
404 new ActivityConfigChangeData(token, overrideConfig), newDisplayId);
405 };
406 }
407
408 public boolean isPreHoneycomb() {
409 if (activity != null) {
410 return activity.getApplicationInfo().targetSdkVersion
411 < android.os.Build.VERSION_CODES.HONEYCOMB;
412 }
413 return false;
414 }
415
416 public boolean isPersistable() {
417 return activityInfo.persistableMode == ActivityInfo.PERSIST_ACROSS_REBOOTS;
418 }
419
420 public String toString() {
421 // ...
427 }
428
429 public String getStateString() {
430 // ...
453 }
454 }
ActivityClientRecord 用来记录一系列关于 activity 的相关变量的信息,并将 ActivityClientRecord 对象通过 handler 发送,通过 handleLaunchActivity 处理启动 Activity。handleLaunchActivity 里面调用了 performLaunchActivity 方法:
xref: /frameworks/base/core/java/android/app/ActivityThread.java#performLaunchActivity
2644 private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
2647 ActivityInfo aInfo = r.activityInfo;
2648 if (r.packageInfo == null) {
2649 r.packageInfo = getPackageInfo(aInfo.applicationInfo, r.compatInfo,
2650 Context.CONTEXT_INCLUDE_CODE);
2651 }
2652
2653 ComponentName component = r.intent.getComponent();
2654 if (component == null) {
2655 component = r.intent.resolveActivity(
2656 mInitialApplication.getPackageManager());
2657 r.intent.setComponent(component);
2658 }
2659
2660 if (r.activityInfo.targetActivity != null) {
2661 component = new ComponentName(r.activityInfo.packageName,
2662 r.activityInfo.targetActivity);
2663 }
2664
2665 ContextImpl appContext = createBaseContextForActivity(r);
2666 Activity activity = null;
2667 try {// 创建Activity
2668 java.lang.ClassLoader cl = appContext.getClassLoader();
2669 activity = mInstrumentation.newActivity(
2670 cl, component.getClassName(), r.intent);
2671 StrictMode.incrementExpectedActivityCount(activity.getClass());
2672 r.intent.setExtrasClassLoader(cl);
2673 r.intent.prepareToEnterProcess();
2674 if (r.state != null) {
2675 r.state.setClassLoader(cl);
2676 }
2677 } catch (Exception e) {
2678 if (!mInstrumentation.onException(activity, e)) {
2679 throw new RuntimeException(
2680 "Unable to instantiate activity " + component
2681 + ": " + e.toString(), e);
2682 }
2683 }// ...
2783
2784 return activity;
2785 }
这里可以很明显地看到,系统通过待启动的 Activity 的类名 className,然后使用 ClassLoader 对象 cl 把这个类加载进虚拟机,最后使用反射创建了这个 Activity 类的实例对象。要想干预这个 ClassLoader(告知它我们的路径或者替换他),我们首先得看看这玩意到底是从哪里创建的。
这里比较关键的是 ActivityClientRecord 里面的 packageInfo 属性,最终都是通过 getPackageInfo 调用。
2071 private LoadedApk getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compatInfo,
2072 ClassLoader baseLoader, boolean securityViolation, boolean includeCode,
2073 boolean registerPackage) {// 获取 userId 信息
2074 final boolean differentUser = (UserHandle.myUserId() != UserHandle.getUserId(aInfo.uid));
2075 synchronized (mResourcesManager) {
2076 WeakReference<LoadedApk> ref;
2077 if (differentUser) {
2078 // Caching not supported across users
2079 ref = null;
2080 } else if (includeCode) {// 从缓存中取
2081 ref = mPackages.get(aInfo.packageName);
2082 } else {
2083 ref = mResourcePackages.get(aInfo.packageName);
2084 }
2085
2086 LoadedApk packageInfo = ref != null ? ref.get() : null;
2087 if (packageInfo == null || (packageInfo.mResources != null
2088 && !packageInfo.mResources.getAssets().isUpToDate())) {
2089 // 缓存没有命中,直接 new
2094 packageInfo =
2095 new LoadedApk(this, aInfo, compatInfo, baseLoader,
2096 securityViolation, includeCode &&
2097 (aInfo.flags&ApplicationInfo.FLAG_HAS_CODE) != 0, registerPackage);
2098
2099 if (mSystemThread && "android".equals(aInfo.packageName)) {
2100 packageInfo.installSystemApplicationInfo(aInfo,
2101 getSystemContext().mPackageInfo.getClassLoader());
2102 }
2103
2104 if (differentUser) {
2105 // Caching not supported across users
2106 } else if (includeCode) {// 保存缓存
2107 mPackages.put(aInfo.packageName,
2108 new WeakReference<LoadedApk>(packageInfo));
2109 } else {
2110 mResourcePackages.put(aInfo.packageName,
2111 new WeakReference<LoadedApk>(packageInfo));
2112 }
2113 }
2114 return packageInfo;
2115 }
2116 }
从上述分析中我们得知,在获取 LoadedApk 的过程中使用了一份缓存数据 mPackages:
final ArrayMap<String, WeakReference<LoadedApk>> mPackages = new ArrayMap<>();
这个缓存数据是一个 Map,从包名到 LoadedApk 的一个映射。正常情况下,我们的插件肯定不会存在于这个对象里面;但是如果我们手动把我们插件的信息添加到里面呢?系统在查找缓存的过程中,会直接命中缓存!进而使用我们添加进去的 LoadedApk 的 ClassLoader 来加载这个特定的 Activity 类!这样我们就能接管我们自己插件类的加载过程了!
这个缓存对象 mPackages 存在于 ActivityThread 类中;老方法,我们首先获取这个对象:
// 先获取到当前的ActivityThread对象
Object currentActivityThread = RefInvoke.getStaticFieldObject("android.app.ActivityThread", "sCurrentActivityThread");
// 获取包的缓存
Map mPackages = (Map) RefInvoke.getFieldObject(currentActivityThread, "mPackages");
拿到这个 Map 之后接下来怎么办呢?我们需要填充这个 map,把插件的信息塞进这个 map 里面,以便系统在查找的时候能命中缓存。但是这个填充这个 Map 我们出了需要包名之外,还需要一个 LoadedApk 对象;如何创建一个 LoadedApk 对象呢?
创建插件的 LoadedApk 对象
我们当然可以直接反射调用它的构造函数直接创建出需要的对象,但是万一哪里有疏漏,构造参数填错了怎么办?又或者 Android 的不同版本使用了不同的参数,导致我们创建出来的对象与系统创建出的对象不一致,无法 work 怎么办?
因此我们需要使用与系统完全相同的方式创建 LoadedApk 对象;从上文分析得知,系统创建 LoadedApk 对象是通过 getPackageInfo 来完成的,因此我们可以调用这个函数来创建 LoadedApk 对象;但是这个函数是 private 的,我们无法使用。
有的童鞋可能会有疑问了,private 不是也能反射到吗?我们确实能够调用这个函数,但是 private 表明这个函数是内部实现,或许那一天 Google 高兴,把这个函数改个名字我们就直接 GG 了;但是 public 函数不同,public 被导出的函数你无法保证是否有别人调用它,因此大部分情况下不会修改;我们最好调用 public 函数来保证尽可能少的遇到兼容性问题。(当然,如果实在木有路可以考虑调用私有方法,自己处理兼容性问题,这个我们以后也会遇到)
间接调用 getPackageInfo 这个私有函数的 public 函数有同名的 getPackageInfo 系列和 getPackageInfoNoCheck;简单查看源代码发现,getPackageInfo 除了获取包的信息,还检查了包的一些组件;为了绕过这些验证,我们选择使用 getPackageInfoNoCheck 获取 LoadedApk 信息。
public final LoadedApk getPackageInfoNoCheck(ApplicationInfo ai,CompatibilityInfo compatInfo) {return getPackageInfo(ai, compatInfo, null, false, true, false);
}
获取 ApplicationInfo 信息
ApplicationInfo 类提供了一个应用的基本信息。这些信息是从 AndroidManifest.xml 的 <application>
标签获取的。可以通过 PackageParser 获取 ApplicationInfo 的信息。这个类的兼容性很差;Google 几乎在每一个 Android 版本都对这个类动刀子,如果坚持使用系统的解析方式,必须写一系列兼容行代码。DroidPlugin 和 VirtualApp 就选择了这种方式。
- PackageParser 类中 parseBaseApk()方法,会解析指定路径 apk 的 AndroidManifest.xml 文件。
- PackageParser 类中 的 generateApplicationInfo 方法来生成 ApplicationInfo 并返回。
由于 PackageParser 是@hide 的,因此我们需要通过反射进行调用。我们根据这个 generateApplicationInfo 方法的签名:
public static ApplicationInfo generateApplicationInfo(Package p, int flags, PackageUserState state, int userId)
可以写出调用 generateApplicationInfo 的反射代码:
public static ApplicationInfo generateApplicationInfo(File apkFile)throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException, NoSuchFieldException {// 找出需要反射的核心类: android.content.pm.PackageParserClass<?> packageParserClass = Class.forName("android.content.pm.PackageParser");// 获取PackageParser$Package类Class<?> packageParser$PackageClass = Class.forName("android.content.pm.PackageParser$Package");Class<?> packageUserStateClass = Class.forName("android.content.pm.PackageUserState");Method generateApplicationInfoMethod = packageParserClass.getDeclaredMethod("generateApplicationInfo",packageParser$PackageClass,int.class,packageUserStateClass);// 首先, 我们得创建出一个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);// 第三个参数 mDefaultPackageUserState 我们直接使用默认构造函数构造一个出来即可Object defaultPackageUserState = packageUserStateClass.newInstance();// 反射 generateApplicationInfo 得到ApplicationInfo对象ApplicationInfo applicationInfo = (ApplicationInfo) generateApplicationInfoMethod.invoke(packageParser,packageObj, 0, defaultPackageUserState);String apkPath = apkFile.getPath();applicationInfo.sourceDir = apkPath;applicationInfo.publicSourceDir = apkPath;return applicationInfo;
}
获取 LoadedApk 信息
public static void hookLoadedApkInActivityThread(File apkFile) throws ClassNotFoundException,NoSuchMethodException, InvocationTargetException, IllegalAccessException, NoSuchFieldException, InstantiationException {// 先获取到当前的ActivityThread对象Object currentActivityThread = RefInvoke.getStaticFieldObject("android.app.ActivityThread", "sCurrentActivityThread");// 获取包的缓存对象Map mPackages = (Map) RefInvoke.getFieldObject(currentActivityThread, "mPackages");// 获取 ApplicationInfoApplicationInfo applicationInfo = generateApplicationInfo(apkFile);// 获取 CompatibilityInfoClass<?> compatibilityInfoClass = Class.forName("android.content.res.CompatibilityInfo");Object defaultCompatibilityInfo = RefInvoke.getStaticFieldObject(compatibilityInfoClass, "DEFAULT_COMPATIBILITY_INFO");// 构建 LoadedApkObject loadedApk = RefInvoke.invokeInstanceMethod(currentActivityThread, "getPackageInfoNoCheck",new Class[]{ApplicationInfo.class, compatibilityInfoClass},new Object[]{applicationInfo, defaultCompatibilityInfo});// 由于是弱引用, 因此我们必须在某个地方存一份, 不然容易被GC; 那么就前功尽弃了.sLoadedApk.put(applicationInfo.packageName, loadedApk);WeakReference weakReference = new WeakReference(loadedApk);mPackages.put(applicationInfo.packageName, weakReference);
}
还原插件中的 Activity
通过 Hook ActivityThread 类的 mH 属性,重写了的 handleMessage 方法实现 Activity 还原,对于插件内的 Activity 我们需要通过包名获取
xref: /frameworks/base/core/java/android/app/ActivityThread.java#handleMessage
@Override
public boolean handleMessage(Message msg) {Log.i(TAG, "接受到消息了msg:" + msg);if (msg.what == 100) {try {Object obj = msg.obj;Intent intent = (Intent) RefInvoke.getFieldObject(obj, "intent");Intent targetIntent = intent.getParcelableExtra(StubActivity.TARGET_COMPONENT);intent.setComponent(targetIntent.getComponent());// 根据包名获取 LoadedApk 的信息; 因此这里我们需要手动填上, 从而能够命中缓存ActivityInfo activityInfo = (ActivityInfo) RefInvoke.getFieldObject(obj, "activityInfo");activityInfo.applicationInfo.packageName = targetIntent.getPackage() == null ? targetIntent.getComponent().getPackageName() : targetIntent.getPackage();} catch (Exception e) {e.printStackTrace();}}mBaseHandler.handleMessage(msg);return true;
}
至此,我们完成了对插件中 Activity 的动态加载,但是暂时无法加载插件中的资源,我们可以通过自定义 ClassLoader 实现,本节暂时先写到这里,后续文章再介绍四大组件和资源加载的实现方式。
本文学习案例地址:classloader-loadapk
Android插件化原理—ClassLoader加载机制相关推荐
- Android 插件化原理学习 —— Hook 机制之动态代理
前言 为了实现 App 的快速迭代更新,基于 H5 Hybrid 的解决方案有很多,由于 webview 本身的性能问题,也随之出现了很多基于 JS 引擎实现的原生渲染的方案,例如 React Nat ...
- Android 插件化原理解析——Hook机制之AMSPMS
在前面的文章中我们介绍了DroidPlugin的Hook机制,也就是代理方式和Binder Hook:插件框架通过AOP实现了插件使用和开发的透明性.在讲述DroidPlugin如何实现四大组件的插件 ...
- 携程Android App插件化和动态加载实践
转载自:http://www.infoq.com/cn/articles/ctrip-android-dynamic-loading?email=947091870@qq.com 编者按:本文为携程无 ...
- Android 插件化原理解析——Activity生命周期管理
之前的 Android插件化原理解析 系列文章揭开了Hook机制的神秘面纱,现在我们手握倚天屠龙,那么如何通过这种技术完成插件化方案呢?具体来说,插件中的Activity,Service等组件如何在A ...
- Android 插件化原理(一),通过dex文件调用插件app代码
Android插件化原理,从以下三个问题切入: 什么是插件化 如何实现插件类的加载 如何实现插件资源的加载 什么是插件化 插件化技术最初是源于免安装运行APK的想法,这个免安装的APK就可以理解为插件 ...
- (4.6.29.3)插件化之代码加载:启动Activity等四大组件之hook方式
文章目录 一.代理模式和Hook原理 1.1 Hook 原理 1.2 代理模式 二.Binder Hook 2.1 分析:系统服务的获取过程 2.2 寻找Hook点 2.3 hook Binder示例 ...
- Android 插件化原理入门笔记
Android开发笔记 onGithub 笔记,参考7.2中所列参考文章所写,DEMO地址在PluginTestDemoApplication 1.综述 2015年是Android插件化技术突飞猛进的 ...
- Android插件化原理(一)Activity插件化
title: " Android插件化原理(一)Activity插件化" date: 2018-5-28 00:16 photos: https://s2.ax1x.com/201 ...
- Android 插件化原理解析——插件加载机制
上文 Activity生命周期管理 中我们地完成了『启动没有在AndroidManifest.xml中显式声明的Activity』的任务:通过Hook AMS和拦截ActivityThread中H类对 ...
最新文章
- JScrollPane 滚动处理
- 转这个博客了,以前的博客不用了。(技术为主,寒暄为辅)
- 什么是Session共享?请举出使用场景
- 桥接模式源码解析(jdk)
- [USACO18JAN][luoguP4183 ]Cow at Large P
- 软件测试面试选择判断提,软件测试面试常考判断题
- 运用Arc Hydro提取河网
- mysql批量写入100万数据_Mysql数据库实践操作之————批量插入数据(100万级别的数据)-阿里云开发者社区...
- Spring boot 自定义拦截器 获取 自定义注解 信息
- 关于23届大数据岗实习总结
- 用户输入错误验证码错误三次后,锁定该用户3分钟 redis 使用案列
- tensorflow使用较为底层的方式复现VGG16
- 深入理解Java虚拟机读书笔记之垃圾收集器与内存分配策略
- ubuntu16.04升级18.04时问题, (appstreamcli:5132): GLib-CRITICAL **: g_strchomp: assertion 'string != NULL'
- 整合SpringBoot + MybatisPlus 搭建JAVA多模块项目基本骨架
- Java彩信接口开发经验及具体开发实现
- 2013驾考科目一理论知识重点归纳
- oracle同义词删除重建,Oracle同义词的创建与删除
- 【小经验】Windows 11 家庭中文版连接远程桌面,出现身份验证错误。要求的函数不受支持
- react函数组件实现四栏轮播图切换