前面《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加载机制相关推荐

  1. Android 插件化原理学习 —— Hook 机制之动态代理

    前言 为了实现 App 的快速迭代更新,基于 H5 Hybrid 的解决方案有很多,由于 webview 本身的性能问题,也随之出现了很多基于 JS 引擎实现的原生渲染的方案,例如 React Nat ...

  2. Android 插件化原理解析——Hook机制之AMSPMS

    在前面的文章中我们介绍了DroidPlugin的Hook机制,也就是代理方式和Binder Hook:插件框架通过AOP实现了插件使用和开发的透明性.在讲述DroidPlugin如何实现四大组件的插件 ...

  3. 携程Android App插件化和动态加载实践

    转载自:http://www.infoq.com/cn/articles/ctrip-android-dynamic-loading?email=947091870@qq.com 编者按:本文为携程无 ...

  4. Android 插件化原理解析——Activity生命周期管理

    之前的 Android插件化原理解析 系列文章揭开了Hook机制的神秘面纱,现在我们手握倚天屠龙,那么如何通过这种技术完成插件化方案呢?具体来说,插件中的Activity,Service等组件如何在A ...

  5. Android 插件化原理(一),通过dex文件调用插件app代码

    Android插件化原理,从以下三个问题切入: 什么是插件化 如何实现插件类的加载 如何实现插件资源的加载 什么是插件化 插件化技术最初是源于免安装运行APK的想法,这个免安装的APK就可以理解为插件 ...

  6. (4.6.29.3)插件化之代码加载:启动Activity等四大组件之hook方式

    文章目录 一.代理模式和Hook原理 1.1 Hook 原理 1.2 代理模式 二.Binder Hook 2.1 分析:系统服务的获取过程 2.2 寻找Hook点 2.3 hook Binder示例 ...

  7. Android 插件化原理入门笔记

    Android开发笔记 onGithub 笔记,参考7.2中所列参考文章所写,DEMO地址在PluginTestDemoApplication 1.综述 2015年是Android插件化技术突飞猛进的 ...

  8. Android插件化原理(一)Activity插件化

    title: " Android插件化原理(一)Activity插件化" date: 2018-5-28 00:16 photos: https://s2.ax1x.com/201 ...

  9. Android 插件化原理解析——插件加载机制

    上文 Activity生命周期管理 中我们地完成了『启动没有在AndroidManifest.xml中显式声明的Activity』的任务:通过Hook AMS和拦截ActivityThread中H类对 ...

最新文章

  1. JScrollPane 滚动处理
  2. 转这个博客了,以前的博客不用了。(技术为主,寒暄为辅)
  3. 什么是Session共享?请举出使用场景
  4. 桥接模式源码解析(jdk)
  5. [USACO18JAN][luoguP4183 ]Cow at Large P
  6. 软件测试面试选择判断提,软件测试面试常考判断题
  7. 运用Arc Hydro提取河网
  8. mysql批量写入100万数据_Mysql数据库实践操作之————批量插入数据(100万级别的数据)-阿里云开发者社区...
  9. Spring boot 自定义拦截器 获取 自定义注解 信息
  10. 关于23届大数据岗实习总结
  11. 用户输入错误验证码错误三次后,锁定该用户3分钟 redis 使用案列
  12. tensorflow使用较为底层的方式复现VGG16
  13. 深入理解Java虚拟机读书笔记之垃圾收集器与内存分配策略
  14. ubuntu16.04升级18.04时问题, (appstreamcli:5132): GLib-CRITICAL **: g_strchomp: assertion 'string != NULL'
  15. 整合SpringBoot + MybatisPlus 搭建JAVA多模块项目基本骨架
  16. Java彩信接口开发经验及具体开发实现
  17. 2013驾考科目一理论知识重点归纳
  18. oracle同义词删除重建,Oracle同义词的创建与删除
  19. 【小经验】Windows 11 家庭中文版连接远程桌面,出现身份验证错误。要求的函数不受支持
  20. react函数组件实现四栏轮播图切换

热门文章

  1. python linux 下开发环境搭建
  2. python 函数
  3. 我在百度运维的成长经历 之六
  4. 在阿里云上遇见更好的Oracle(四)
  5. MyEclipse使用经验归纳
  6. 适用于连续资源块的数组空闲链表的算法
  7. 精通MVC3摘译(9)-过滤器
  8. [转]唐骏谈职场 —— 管理者要学会让员工感动
  9. 基于TCP的网络游戏黑白棋系列(二):数据传输
  10. Apache Solr Java 企业级搜索引擎