聊聊 Xposed

前言

本文涉及的 Android 源码部分基于 Android 10

Xposed

在聊 Xposed 前我们先简单在脑海里回顾下 Zygote 进程的启动流程,如果记不清的可以先看一看 Android Framework 之 Zygote

入口函数的差异

Zygote 进程 native 层入口函数为 frameworks/base/cmds/app_process/app_main.cpp,而在 Xposed 项目中也有 app_main.cpp

image.png

Xposed 的这个文件有什么用呢?

其实在 recovery 模式下刷 Xposed 时,它会用 app_main.cppapp_main2.cpp 替换系统的 app_process/app_main.cpp,在 Android.mk 中有如下代码。

// Android.mk
ifeq (1,$(strip $(shell expr $(PLATFORM_SDK_VERSION) \>= 21)))
  LOCAL_SRC_FILES := app_main2.cpp
else
  LOCAL_SRC_FILES := app_main.cpp
endif

即版本高于 21 时使用 app_main2.cpp,否则用 app_main.cpp

来看看 app_main2.cpp 有哪些主要的改动。

// app_process/app_main.cpp
if (zygote) {
    runtime.start("com.android.internal.os.ZygoteInit", args, zygote);
} else if (className) {
    runtime.start("com.android.internal.os.RuntimeInit", args, zygote);
}

// app_main2.cpp
if (zygote) {
    isXposedLoaded = xposed::initialize(true, startSystemServer, NULL, argc, argv);
    runtimeStart(runtime, isXposedLoaded ? XPOSED_CLASS_DOTS_ZYGOTE : "com.android.internal.os.ZygoteInit", args, zygote);
} else if (className) {
    isXposedLoaded = xposed::initialize(false, false, className, argc, argv);
    runtimeStart(runtime, isXposedLoaded ? XPOSED_CLASS_DOTS_TOOLS : "com.android.internal.os.RuntimeInit", args, zygote);
}

可以看到,主要有如下差异:

  1. 在启动 Zygote 进程或孵化应用进程时会先执行 xposed::initialize() 进行初始化;
  2. 执行 runtimeStart(),注意这里的 XPOSED_CLASS_DOTS_ZYGOTEXPOSED_CLASS_DOTS_TOOLS

xposed::initalize()

先看看 xposed::initialize(),源码在 xposed.hxposed.cpp

// xposed.h
#define XPOSED_JAR               "/system/framework/XposedBridge.jar"

// xposed.cpp
bool initialize(bool zygote, bool startSystemServer, const char* className, int argc, char* const argv[]) {
    // ...
    return addJarToClasspath();
}

bool addJarToClasspath() {
    //...
    if (access(XPOSED_JAR, R_OK) == 0) {
        // 把 XposedBridge.jar 添加到 classPath
        if (!addPathToEnv("CLASSPATH", XPOSED_JAR))
            return false;
        return true;
    }
}

总的来说 xposed::initialize() 会把 XposedBridge.jar 添加到 classpath,这样 Zygote 进程孵化的应用进程就都具备了 XposedBridge.jar 的代码。

执行 runtimeStart()

来看看 runtimeStart()

// app_main2.cpp
static void runtimeStart(AppRuntime& runtime, const char *classname, const Vector<String8>& options, bool zygote)
{
#if PLATFORM_SDK_VERSION >= 23
  runtime.start(classname, options, zygote);
#else
  void (*ptr1)(AppRuntime&, const char*, const Vector<String8>&, bool);
  *(void **) (&ptr1) = dlsym(RTLD_DEFAULT, "_ZN7android14AndroidRuntime5startEPKcRKNS_6VectorINS_7String8EEEb");

  if (ptr1 != NULL) {
    ptr1(runtime, classname, options, zygote);
    return;
  }

  void (*ptr2)(AppRuntime&, const char*, const Vector<String8>&);
  *(void **) (&ptr2) = dlsym(RTLD_DEFAULT, "_ZN7android14AndroidRuntime5startEPKcRKNS_6VectorINS_7String8EEE");

  if (ptr2 != NULL) {
    ptr2(runtime, classname, options);
    return;
  }
#endif
}

做了版本判断,这里最终还是会调用 AndroidRuntimestart(),了解 Zygote 进程启动流程的应该知道这里主要做了三件事,创建虚拟机,注册 JNI,通过反射调用 classnamemain()

创建虚拟机

Xposed 在创建虚拟机之后做了一些修改,先看看 AndroidRuntimeonVmCreated()

// app_main2.cpp
class AppRuntime : public AndroidRuntime
{
    virtual void onVmCreated(JNIEnv* env)
    {
        if (isXposedLoaded)
            xposed::onVmCreated(env);
    }
}

这里调用 xposed::onVmCreated(),其源码在 xposed.cpp

// xposed.cpp
void onVmCreated(JNIEnv* env) {
    // 加载 libxposed_art.so
    void* xposedLibHandle = dlopen(xposedLibPath, RTLD_NOW);

    // 初始化 xposed 相关 library
    bool (*xposedInitLib)(XposedShared* shared) = NULL;
    *(void **) (&xposedInitLib) = dlsym(xposedLibHandle, "xposedInitLib");
    if (xposedInitLib(xposed)) {
        // 执行 onVmCreated(env)
        xposed->onVmCreated(env);
    }
}

这里通过 dlopen() 函数加载 libxposed_art.so,在 Android.mk 中有如下定义。

// Android.mk
ifeq (1,$(strip $(shell expr $(PLATFORM_SDK_VERSION) \>= 21)))
  include frameworks/base/cmds/xposed/ART.mk
endif

使用 ART.mk 生成 libxposed_art.so

// ART.mk
LOCAL_SRC_FILES += \
  libxposed_common.cpp \
  libxposed_art.cpp

LOCAL_MODULE := libxposed_art

libxposed_art.so 加载后,接着会初始化 xposed 相关 library,然后调用 xposedInitLib(),其源码在 libxposed_art.cpp

// libxposed_art.cpp
bool xposedInitLib(XposedShared* shared) {
    xposed = shared;
    xposed->onVmCreated = &onVmCreatedCommon;
    return true;
}

这里进行赋值,即 xposed->onVmCreated(env) 会执行 onVmCreatedCommon(),其源码在 libxposed_common.cpp

// libxposed_common.cpp
void onVmCreatedCommon(JNIEnv* env) {
    if (!initXposedBridge(env) || !initZygoteService(env)) {
        return;
    }

    if (!onVmCreated(env)) {
        return;
    }

    xposedLoadedSuccessfully = true;
    return;
}

initXposedBridge() 会初始化 XposedBridge.javainitZygoteService() 会初始化 ZygoteService.java,主要看看 initXposedBridge()

bool initXposedBridge(JNIEnv* env) {
    classXposedBridge = env->FindClass(CLASS_XPOSED_BRIDGE);
    classXposedBridge = reinterpret_cast<jclass>(env->NewGlobalRef(classXposedBridge));
    // 这里会注册 native 函数
    if (register_natives_XposedBridge(env, classXposedBridge) != JNI_OK) {
        return false;
    }

    // 这里会获取到 XposedBridge 的 handleHookedMethod
    methodXposedBridgeHandleHookedMethod = env->GetStaticMethodID(classXposedBridge, "handleHookedMethod",
        "(Ljava/lang/reflect/Member;ILjava/lang/Object;Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;");
    return true;
}

// 注册 native 函数
int register_natives_XposedBridge(JNIEnv* env, jclass clazz) {
    const JNINativeMethod methods[] = {
        NATIVE_METHOD(XposedBridge, hadInitErrors, "()Z"),
        NATIVE_METHOD(XposedBridge, getStartClassName, "()Ljava/lang/String;"),
        NATIVE_METHOD(XposedBridge, getRuntime, "()I"),
        NATIVE_METHOD(XposedBridge, startsSystemServer, "()Z"),
        NATIVE_METHOD(XposedBridge, getXposedVersion, "()I"),
        NATIVE_METHOD(XposedBridge, initXResourcesNative, "()Z"),
        NATIVE_METHOD(XposedBridge, hookMethodNative, "(Ljava/lang/reflect/Member;Ljava/lang/Class;ILjava/lang/Object;)V"),
        NATIVE_METHOD(XposedBridge, setObjectClassNative, "(Ljava/lang/Object;Ljava/lang/Class;)V"),
        NATIVE_METHOD(XposedBridge, dumpObjectNative, "(Ljava/lang/Object;)V"),
        NATIVE_METHOD(XposedBridge, cloneToSubclassNative, "(Ljava/lang/Object;Ljava/lang/Class;)Ljava/lang/Object;"),
        NATIVE_METHOD(XposedBridge, removeFinalFlagNative, "(Ljava/lang/Class;)V"),
#if PLATFORM_SDK_VERSION >= 21
        NATIVE_METHOD(XposedBridge, invokeOriginalMethodNative,
            "!(Ljava/lang/reflect/Member;I[Ljava/lang/Class;Ljava/lang/Class;Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;"),
        NATIVE_METHOD(XposedBridge, closeFilesBeforeForkNative, "()V"),
        NATIVE_METHOD(XposedBridge, reopenFilesAfterForkNative, "()V"),
#endif
#if PLATFORM_SDK_VERSION >= 24
        NATIVE_METHOD(XposedBridge, invalidateCallersNative, "([Ljava/lang/reflect/Member;)V"),
#endif
    };
    return env->RegisterNatives(clazz, methods, NELEM(methods));
}

这里主要是注册 native 函数,并获取 handleHookedMethod() 赋值给 methodXposedBridgeHandleHookedMethod

总的来说这里主要是加载 libxposed_art.so,并初始化一些 library,然后注册 native 函数。

反射调用 XPOSED_CLASS_DOTS_ZYGOTE 的 main()

最终会通过反射调用 XPOSED_CLASS_DOTS_ZYGOTEmain(),其定义位于 xposed.h

#define XPOSED_CLASS_DOTS_ZYGOTE "de.robv.android.xposed.XposedBridge"

即最终会执行 de.robv.android.xposed.XposedBridge.main()。它是 XposedBridge.jar 中的类,源码位于 XposedBridge 项目,来看看其 main()

protected static void main(String[] args) {
    // 1. 初始化 Xposed framework 和 Xposed module(也就是我们平常写的 Xposed 插件)
    try {
        if (!hadInitErrors()) {
            initXResources();

            SELinuxHelper.initOnce();
            SELinuxHelper.initForProcess(null);

            runtime = getRuntime();
            XPOSED_BRIDGE_VERSION = getXposedVersion();

            if (isZygote) {
                // hook 各版本资源获取与创建
                XposedInit.hookResources();
                // hook 系统关键方法
                XposedInit.initForZygote();
            }
            // 加载 Xposed Modules
            XposedInit.loadModules();
        }
    } catch (Throwable t) {
        disableHooks = true;
    }

    // 2. 原 Zygote 和 应用进程 启动流程
    if (isZygote) {
        ZygoteInit.main(args);
    } else {
        RuntimeInit.main(args);
    }
}

这里主要做了如下事情:

  1. 调用 XposedInit.hookResources()hook 系统资源相关方法;
  2. 调用 XposedInit.initForZygote()hook Zygote 相关方法;
  3. 调用 XposedInit.loadModules() 来加载 Xposed Module

这里说下 loadModules(),来看看 loadModules() 源码。

private static final String INSTALLER_PACKAGE_NAME = "de.robv.android.xposed.installer";
private static final String BASE_DIR = Build.VERSION.SDK_INT >= 24
        ? "/data/user_de/0/" + INSTALLER_PACKAGE_NAME + "/"
        : "/data/data/" + INSTALLER_PACKAGE_NAME + "/";

static void loadModules() throws IOException {
    final String filename = BASE_DIR + "conf/modules.list";

    ClassLoader topClassLoader = XposedBridge.BOOTCLASSLOADER;
    ClassLoader parent;
    while ((parent = topClassLoader.getParent()) != null) {
        topClassLoader = parent;
    }

    // 读安装目录的 conf/modules.list 文件
    InputStream stream = service.getFileInputStream(filename);
    BufferedReader apks = new BufferedReader(new InputStreamReader(stream));
    String apk;
    while ((apk = apks.readLine()) != null) {
        // 调用 loadModule() 加载 xposed module 
        loadModule(apk, topClassLoader);
    }
    apks.close();
}

这里会加载安装目录下的 conf/modules.list 文件,然后对每个 xposed module 调用 loadModule() 来进行注册。

private static void loadModule(String apk, ClassLoader topClassLoader) {
    DexFile dexFile;
    try {
        dexFile = new DexFile(apk);
    } catch (IOException e) {
        return;
    }

    // 禁止 Android Studio 的 instant run
    if (dexFile.loadClass(INSTANT_RUN_CLASS, topClassLoader) != null) {
        Log.e(TAG, "  Cannot load module, please disable \"Instant Run\" in Android Studio.");
        closeSilently(dexFile);
        return;
    }

    // 注意 Xposed Module 对 xposed 库的依赖是 compileOnly,不会被打入到 apk 中
    if (dexFile.loadClass(XposedBridge.class.getName(), topClassLoader) != null) {
        Log.e(TAG, "  Cannot load module:");
        Log.e(TAG, "  The Xposed API classes are compiled into the module's APK.");
        Log.e(TAG, "  This may cause strange issues and must be fixed by the module developer.");
        Log.e(TAG, "  For details, see: http://api.xposed.info/using.html");
        closeSilently(dexFile);
        return;
    }

    closeSilently(dexFile);

    // 读取 assets/xposed_init 配置文件
    ZipFile zipFile = null;
    InputStream is;
    try {
        zipFile = new ZipFile(apk);
        ZipEntry zipEntry = zipFile.getEntry("assets/xposed_init");
        // 找不到 xposed_init 配置文件
        if (zipEntry == null) {
            Log.e(TAG, "  assets/xposed_init not found in the APK");
            closeSilently(zipFile);
            return;
        }
        is = zipFile.getInputStream(zipEntry);
    } catch (IOException e) {
        closeSilently(zipFile);
        return;
    }

    // 找注册类
    ClassLoader mcl = new PathClassLoader(apk, XposedBridge.BOOTCLASSLOADER);
    BufferedReader moduleClassesReader = new BufferedReader(new InputStreamReader(is));
    try {
        String moduleClassName;
        while ((moduleClassName = moduleClassesReader.readLine()) != null) {
            moduleClassName = moduleClassName.trim();
            if (moduleClassName.isEmpty() || moduleClassName.startsWith("#"))
                continue;

            try {
                Class<?> moduleClass = mcl.loadClass(moduleClassName);

                if (!IXposedMod.class.isAssignableFrom(moduleClass)) {
                    continue;
                } else if (disableResources && IXposedHookInitPackageResources.class.isAssignableFrom(moduleClass)) {
                    continue;
                }

                final Object moduleInstance = moduleClass.newInstance();
                if (XposedBridge.isZygote) {
                    if (moduleInstance instanceof IXposedHookZygoteInit) {
                        IXposedHookZygoteInit.StartupParam param = new IXposedHookZygoteInit.StartupParam();
                        param.modulePath = apk;
                        param.startsSystemServer = startsSystemServer;
                        ((IXposedHookZygoteInit) moduleInstance).initZygote(param);
                    }

                    if (moduleInstance instanceof IXposedHookLoadPackage)
                        XposedBridge.hookLoadPackage(new IXposedHookLoadPackage.Wrapper((IXposedHookLoadPackage) moduleInstance));

                    if (moduleInstance instanceof IXposedHookInitPackageResources)
                        XposedBridge.hookInitPackageResources(new IXposedHookInitPackageResources.Wrapper((IXposedHookInitPackageResources) moduleInstance));
                }
            } catch (Throwable t) {
            }
        }
    } catch (IOException e) {
    } finally {
        closeSilently(is);
        closeSilently(zipFile);
    }
}

这里会解析 assets/xposed_init 配置文件,然后对配置中声明的类进行注册。

方法如何被 hook

Xposed Module 中,想要 hook 某个方法可以通过 XposedHelpers.findAndHookMethod(),来看看其源码。

public static XC_MethodHook.Unhook findAndHookMethod(String className, ClassLoader classLoader, String methodName, Object... parameterTypesAndCallback) {
    return findAndHookMethod(findClass(className, classLoader), methodName, parameterTypesAndCallback);
}

public static XC_MethodHook.Unhook findAndHookMethod(Class<?> clazz, String methodName, Object... parameterTypesAndCallback) {
    if (parameterTypesAndCallback.length == 0 || !(parameterTypesAndCallback[parameterTypesAndCallback.length-1] instanceof XC_MethodHook))
        throw new IllegalArgumentException("no callback defined");

    // 就是 findAndHookMethod() 的最后一个参数
    XC_MethodHook callback = (XC_MethodHook) parameterTypesAndCallback[parameterTypesAndCallback.length-1];
    // 反射找方法
    Method m = findMethodExact(clazz, methodName, getParameterClasses(clazz.getClassLoader(), parameterTypesAndCallback));
    // 调用 XposedBridge.hookMethod()
    return XposedBridge.hookMethod(m, callback);
}

来看看 XposedBridge.hookMethod()

public static XC_MethodHook.Unhook hookMethod(Member hookMethod, XC_MethodHook callback) {
        // 方法是否满足 hook 条件
    if (!(hookMethod instanceof Method) && !(hookMethod instanceof Constructor<?>)) {
        throw new IllegalArgumentException("Only methods and constructors can be hooked: " + hookMethod.toString());
    } else if (hookMethod.getDeclaringClass().isInterface()) {
        throw new IllegalArgumentException("Cannot hook interfaces: " + hookMethod.toString());
    } else if (Modifier.isAbstract(hookMethod.getModifiers())) {
        throw new IllegalArgumentException("Cannot hook abstract methods: " + hookMethod.toString());
    }

    // 缓存
    boolean newMethod = false;
    CopyOnWriteSortedSet<XC_MethodHook> callbacks;
    synchronized (sHookedMethodCallbacks) {
        callbacks = sHookedMethodCallbacks.get(hookMethod);
        if (callbacks == null) {
            callbacks = new CopyOnWriteSortedSet<XC_MethodHook>();
            sHookedMethodCallbacks.put(hookMethod, callbacks);
            newMethod = true;
        }
    }
    callbacks.add(callback);

    // 主要是这里
    if (newMethod) {
        Class<?> declaringClass = hookMethod.getDeclaringClass();
        int slot;
        Class<?>[] parameterTypes;
        Class<?> returnType;
        if (runtime == RUNTIME_ART) {
            slot = 0;
            parameterTypes = null;
            returnType = null;
        } else if (hookMethod instanceof Method) {
            slot = getIntField(hookMethod, "slot");
            parameterTypes = ((Method) hookMethod).getParameterTypes();
            returnType = ((Method) hookMethod).getReturnType();
        } else {
            slot = getIntField(hookMethod, "slot");
            parameterTypes = ((Constructor<?>) hookMethod).getParameterTypes();
            returnType = null;
        }

        AdditionalHookInfo additionalInfo = new AdditionalHookInfo(callbacks, parameterTypes, returnType);
        // 调用 native 方法 
        hookMethodNative(hookMethod, declaringClass, slot, additionalInfo);
    }

    return callback.new Unhook(hookMethod);
}

从这里可以看到,一个方法能否被 hook,必须满足:

  1. 是普通的方法或构造器;
  2. 不是接口方法;
  3. 不是抽象方法。

最终执行 hook 操作的是 native 方法 hookMethodNative(),其源码位于 libxposed_art.cpp

void XposedBridge_hookMethodNative(JNIEnv* env, jclass, jobject javaReflectedMethod,
            jobject, jint, jobject javaAdditionalInfo) {
    // 把 java method 转换成 ArtMethod
    ArtMethod* artMethod = ArtMethod::FromReflectedMethod(soa, javaReflectedMethod);

    // 这里是真正的 hook 逻辑
    artMethod->EnableXposedHook(soa, javaAdditionalInfo);
}

这里将 JavaMethod 转为 ArtMethod,然后调用 EnableXposedHook(),这里是最终做 hook 的地方,其源码位于 android_artart_method.cc

void ArtMethod::EnableXposedHook(ScopedObjectAccess& soa, jobject additional_info) {
  // 1. 备份
  auto* cl = Runtime::Current()->GetClassLinker();
  auto* linear_alloc = cl->GetAllocatorForClassLoader(GetClassLoader());
  ArtMethod* backup_method = cl->CreateRuntimeMethod(linear_alloc);
  backup_method->CopyFrom(this, cl->GetImagePointerSize());
  backup_method->SetAccessFlags(backup_method->GetAccessFlags() | kAccXposedOriginalMethod);

  // 2. 创建备份方法的反射对象
  mirror::AbstractMethod* reflected_method;
  if (IsConstructor()) {
    reflected_method = mirror::Constructor::CreateFromArtMethod(soa.Self(), backup_method);
  } else {
    reflected_method = mirror::Method::CreateFromArtMethod(soa.Self(), backup_method);
  }
  reflected_method->SetAccessible<false>(true);

  // 3. 将信息存放到结构体中
  XposedHookInfo* hook_info = reinterpret_cast<XposedHookInfo*>(linear_alloc->Alloc(soa.Self(), sizeof(XposedHookInfo)));
  hook_info->reflected_method = soa.Vm()->AddGlobalRef(soa.Self(), reflected_method);
  hook_info->additional_info = soa.Env()->NewGlobalRef(additional_info);
  hook_info->original_method = backup_method;

  // 4. 准备工作,处理函数的JIT即时编译以及其他
  ScopedThreadSuspension sts(soa.Self(), kSuspended);
  jit::ScopedJitSuspend sjs;
  gc::ScopedGCCriticalSection gcs(soa.Self(),
                                  gc::kGcCauseXposed,
                                  gc::kCollectorTypeXposed);
  ScopedSuspendAll ssa(__FUNCTION__);

  cl->InvalidateCallersForMethod(soa.Self(), this);

  jit::Jit* jit = art::Runtime::Current()->GetJit();
  if (jit != nullptr) {
    jit->GetCodeCache()->MoveObsoleteMethod(this, backup_method);
  }

  // 5. 设置被 hook 函数的入口点(关键的 hook 逻辑)
  // 将 hook 信息存到 entry_point_from_jni 这个指针
  SetEntryPointFromJniPtrSize(reinterpret_cast<uint8_t*>(hook_info), sizeof(void*));
  // 设替换函数入口点 entry_point_from_quick_compiled_code_ 为自己的 art_quick_proxy_invoke_handler
  SetEntryPointFromQuickCompiledCode(GetQuickProxyInvokeHandler());
  // 设置函数在 CodeItem 偏移
  SetCodeItemOffset(0);

  // 更改属性并添加 kAccXposedHookedMethod 标记
  const uint32_t kRemoveFlags = kAccNative | kAccSynchronized | kAccAbstract | kAccDefault | kAccDefaultConflict;
  SetAccessFlags((GetAccessFlags() & ~kRemoveFlags) | kAccXposedHookedMethod);

  MutexLock mu(soa.Self(), *Locks::thread_list_lock_);
  Runtime::Current()->GetThreadList()->ForEach(StackReplaceMethodAndInstallInstrumentation, this);
}

这里面涉及太多 art 相关知识,暂时没能力细究,简单说一下,其实就是找到被 hook 函数的地址值,然后替换成另外一个函数,这样当函数执行时会先执行 callbackbeforeHookedMethod(),然后执行被 hook 函数,最后执行 callbackafterHookedMethod()

Xposed Module

下面再说说如何编写一个 Xposed Module,编写 Xposed Module 其实只需要四步即可。

添加对 Xposed 库的依赖

app/build.gradle 中添加对 Xposed 库的依赖。

compileOnly 'de.robv.android.xposed:api:82'
compileOnly 'de.robv.android.xposed:api:82:sources'

注意这里依赖的方式为 compileOnly,不知道为什么的往前看 loadModule()

增加 Xposed 配置

AndroidManifest.xml 中添加 Xposed 相关配置。

<!--  标志该 apk 为一个 Xposed 模块,供 Xposed 框架识别-->
<meta-data
    android:name="xposedmodule"
    android:value="true" />

<!--模块说明,一般为模块的功能描述-->
<meta-data
    android:name="xposeddescription"
    android:value="这个模块是用来检测用户隐私合规的,在用户未授权同意前,调用接口获取信息属于违规" />

<!--模块兼容版本-->
<meta-data
    android:name="xposedminversion"
    android:value="54" />

编写 hook 插件

一般情况下会编写类实现 IXposedHookLoadPackage,并在 handleLoadPackage() 做相关操作。

public class HookTrack implements IXposedHookLoadPackage {

    @Override
    public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) {
        // ... 在这里进行相关操作
    }

增加 xposed_init 配置

assets 目录下新增 xposed_init 配置文件,并添加插件类的全路径声明。

image.png

这样 Xposed Module 就编写完了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349