# 现代安卓开发之 Jetpack Compose、Xposed Hook 与 Kotlin Multiplatform

原文：https://blog.grtsinry43.com/posts/modern-android-explore/

作者：grtsinry43

发布时间：2025-04-23T14:48:31Z

内容更新时间：2025-04-23T14:48:31Z

emm上来堆三个技术名词太劝退，每个部分开始之前我都会写一段简短的介绍，用很通俗的语言讲解下这是个什么东西。

写这篇文章起源于我最近悲惨的生活和精神状态，因此适当摸摸鱼写写有趣的东西，尤其是写kotlin真的很爽，最近是真的写了很多
![wakatime](https://blog.grtsinry43.com/uploads/2025/04/23/image.png_d8405d42-5a9a-4349-8df1-f5ba1c5c2588.png)



---



## Jetpack Compose

Jetpack Compose 是 Google 推出的用于构建原生 Android 界面的现代化声明式 UI 工具包。它从传统的命令式 UI（手动查找并更新 View）转变为声明式 UI，说人话就是你现在只需要描述你的UI在不同状态下是什么样子的，至于渲染和更新放心交给Compose就好。另外他完全基于 Kotlin 构建，充分利用了 Kotlin 的语言特性（如 Lambda、DSL、协程等），如果你会Kotlin，上手这个是相当舒服的。

前情提要（bushi）：
![app](https://blog.grtsinry43.com/uploads/2025/04/23/8a87fe1c943a4a3b294ad16e96e2d4e4.png_0f2b26bb-13ea-4e2e-8b79-f105024b9b93.png)


### 声明式

声明式UI首先是在web前端领域广泛应用的，Jetpack Compose受到启发，将它作为安卓平台的全新UI构建工具，它简化了原命令式UI复杂的操作过程find set等等

### 函数式

首先它是函数式的，函数是一等公民，而这些描述UI的函数被称为`Composable`函数，其具有`@Composable`注解，他们不是返回特定的对象，而是类似“发出UI”，它们之间相互调用可以组成复杂的UI结构

### 状态驱动

这里就和 React 什么的很像了，当`State` 对象改变时，Compose 会智能地“重组”（Recomposition）受影响的 Composable 函数，更新 UI。其中`mutableStateOf()` 创建一个可观察的可变状态持有者，而`remember { ... }` 用于在重组过程中“记住”状态或对象，避免每次重组都重新创建。

注：`remember` 本身是为了**在多次重组之间保持实例不变**，避免重复创建开销。与 `mutableStateOf` 结合使用 (`remember { mutableStateOf(...) }`) 才是创建**既能被记住又能触发重组**的状态的标准方式。

当然，类似于 React/Vue 的 diff，Compose 的优化也是相当好的，当 Composable 函数依赖的状态发生变化时，Compose 运行时会自动重新调用该函数及其可能受影响的子函数，以更新 UI。Compose 会进行智能优化，只重组必要的部分。

### 副作用和单向数据流

**副作用（Side Effects）:** 我们都知道不会影响状态的叫做纯函数，而改变状态的是副作用函数，实际开发中，还需要处理副作用（如网络请求、数据库操作、启动协程等）。Compose 提供了 `LaunchedEffect`, `SideEffect`, `DisposableEffect`等 API 来安全地处理这些与 Composable 生命周期相关的副作用。

**单向数据流 (Unidirectional Data Flow - UDF):** Compose 强烈推荐遵循 UDF 模式：状态向下流动（父组件传给子组件），事件向上传递（子组件通过回调通知父组件）。这有助于构建更可预测、更易于维护的 UI。

### 浅浅上手

在之前摸鱼的时候我写了个小小简陋的app：[传送门](https://github.com/grtsinry43/grtblog-app)

我们就以这个为例来快速上手 Jetpack Compose

![as](https://blog.grtsinry43.com/uploads/2025/04/23/image.png_dc34f482-f1e6-423c-8038-07c76b7ff121.png)


我们以这个卡片组件为例，如你所见，在Android Studio中，工作区分为两部分，代码和Compose Preview，你可以通过添加`@Preview`注解来启动对应组件的预览，当然也可以添加主题来查看对应主题的效果，比如默认的Material3

```kotlin
@Preview(showBackground = true)
@Composable
fun ArticleCardPreview() {
    MaterialTheme{
        // 你的组件
    }
}
```

我们重心还是回到上面的Composable函数上：

它从外向内分别是Box Row Column，类似三层flex div，modifier是样式修饰符，类比css，而里面也是由一个个封装好的Composable函数嵌套而成的（其实就是调用函数，组合UI）

我们可以根据传入的参数描述不同的UI，进而渲染不同的元素，例如：

```kotlin
// AsyncImage 加载封面
            if (!cover.isNullOrEmpty()) {
                // 构建完整URL
                val fullImageUrl = if (cover.startsWith("http")) cover else "$baseUrl$cover"

                AsyncImage(
                    model = ImageRequest.Builder(LocalContext.current)
                        .data(fullImageUrl)
                        .crossfade(true)
                        .build(),
                    contentDescription = "文章封面",
                    contentScale = ContentScale.Crop,
                    modifier = Modifier
                        .size(80.dp)
                        .clip(MaterialTheme.shapes.medium),
                    error = painterResource(id = R.mipmap.author),
                    placeholder = painterResource(id = R.mipmap.logo)
                )
            } else {
                // 如果没有封面，显示默认图片
                Image(
                    painter = painterResource(id = R.mipmap.author),
                    contentDescription = null,
                    modifier = Modifier
                        .size(64.dp)
                        .padding(8.dp)
                )
            }
```

当涉及到副作用时，我们可以以简单的网络加载为例

```kotlin
@Composable
fun ArticlesScreen(navController: NavController = rememberNavController()) {
    // 获取LocalContext
    val context = LocalContext.current

    // 创建ViewModel工厂
    val factory = remember {
        object : ViewModelProvider.Factory {
            override fun <T : ViewModel> create(modelClass: Class<T>): T {
                return ArticleViewModel(context.applicationContext as Application) as T
            }
        }
    }

    // 使用工厂创建ViewModel
    val viewModel: ArticleViewModel = viewModel(factory = factory)
    val listState = rememberLazyListState()

    // 收集分页状态
    val pagingState by viewModel.pagingState.collectAsState()
    val isLoading by viewModel.isLoading.collectAsState()

    // 检测滚动到底部
    val shouldLoadMore by remember {
        derivedStateOf {
            val lastVisibleItem = listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0
            val totalItems = pagingState.items.size
            lastVisibleItem >= totalItems - 3 && !pagingState.loading && !pagingState.endReached
        }
    }

    // 触发加载更多
    LaunchedEffect(shouldLoadMore) {
        if (shouldLoadMore) {
            viewModel.loadArticles()
        }
    }

    Scaffold { innerPadding ->
        LazyColumn(
            modifier = Modifier.padding(innerPadding),
            state = listState
        ) {
            item {
                PageHeader(title = "文章")
            }

            if (pagingState.items.isEmpty() && isLoading) {
                // 显示骨架屏
                items(6) { // 显示6个骨架项
                    ArticleCardSkeleton()
                }
            } else {
                // 显示实际内容
                itemsIndexed(pagingState.items) { _, article ->
                    ArticleCard(
                        title = article.title,
                        cover = article.cover,
                        description = article.summary,
                        date = article.createdAt,
                        onClick = {
                            navController.navigate(NavRoutes.articleDetail(article.shortUrl))
                        }
                    )
                }

                // 底部加载指示器
                item {
                    if (pagingState.loading) {
                        Box(
                            modifier = Modifier
                                .fillMaxWidth()
                                .padding(16.dp),
                            contentAlignment = Alignment.Center
                        ) {
                            CircularProgressIndicator()
                        }
                    }
                }

                // 错误提示
                item {
                    if (pagingState.error != null) {
                        Box(
                            modifier = Modifier
                                .fillMaxWidth()
                                .padding(16.dp),
                            contentAlignment = Alignment.Center
                        ) {
                            Text(text = pagingState.error ?: "加载失败")
                        }
                    }
                }
            }
        }
    }
}
```

注：这里我手动创建了 ViewModel Factory 来传递 `Application` Context，但在实际项目中，通常会使用 Hilt 或 Koin 等依赖注入库来简化这个过程。别感觉复杂，koin很简单的。

当然，想要深入使用还是得先浅浅学学Android中Activity的生命周期，这样才能会用ViewModel和各个Effect，不过这里只是简单介绍啦，它的思想和前端框架还是蛮像的。

## Xposed Hooks

这个如果是搞机大佬肯定不会陌生，它是 一个运行于 Android 系统底层的框架，允许用户和开发者在不修改应用程序 APK 文件的情况下，实时地修改（“Hook”）系统和应用程序的行为，当然现在用的更多的还是LSPosed（~~老**框架~~）

Hook意为钩子，可以理解为钩住对应的函数，拦截下来，进而完成你想要的操作，它在应用程序启动时加载 Xposed Bridge，从而获得在 Zygote 进程中 Hook 任意 Java 方法的能力。

emm还是一个摸鱼时候随便写的小项目：[传送门](https://github.com/grtsinry43/csuapp-justforfun)

**~~作者自评：我看你小子没少摸鱼~~**

### 核心实践

我们首先要确定一个模块入口类，让他继承`IXposedHookLoadPackage`，这样我们就有了修改程序的能力。

![代码结构](https://blog.grtsinry43.com/uploads/2025/04/23/image.png_9e6a950b-9d44-41ef-a355-6bfb3d2ee26b.png)


在这个重载方法里，我们可以针对加载模块做一些自定义，比如因为我后续需要广播信息，这里就获取了一下context。

然后hook的方法就是这样，提供具体的类，方法名，随后我们可以自定义自己的前钩子函数和后钩子函数。 为了便于调试，我们可以添加一些日志。

```kotlin
// 2. Hook 这个特定类的 onCreate 方法
XposedHelpers.findAndHookMethod(
    appClass, // *** 修改点：使用找到的特定类 appClass ***，真是被气炸
    "onCreate",
    object : XC_MethodHook() {
        override fun beforeHookedMethod(param: MethodHookParam?) {
            XposedBridge.log("[justforfun] Custom App ($TARGET_APPLICATION_CLASS_NAME) onCreate: BEFORE")
        }

        override fun afterHookedMethod(param: MethodHookParam) {
            XposedBridge.log("[justforfun] Custom App ($TARGET_APPLICATION_CLASS_NAME) onCreate: AFTER")
            // 3. 获取 Context 并调用业务 Hook
            targetAppContextRef = WeakReference(param.thisObject as Context)
            XposedBridge.log("[justforfun] Acquired context via custom App")
            hookBusinessMethods(lpparam) // 获取 Context 后调用业务 Hook
        }
    })
```

这里有一个param，我们可以通过它的args(Arr)和result(Object)来拿到所hook方法的参数和结果，整体效果大概这样：

```kotlin
package com.example.myxposedmodule // 替换成你的包名

import de.robv.android.xposed.IXposedHookLoadPackage
import de.robv.android.xposed.XC_MethodHook
import de.robv.android.xposed.XposedBridge
import de.robv.android.xposed.XposedHelpers
import de.robv.android.xposed.callbacks.XC_LoadPackage

class HookEntry : IXposedHookLoadPackage {

    override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) {
        // 检查是否是目标应用 (例如：com.target.app)
        if (lpparam.packageName != "com.target.app") {
            return
        }

        XposedBridge.log("模块加载成功，目标应用: ${lpparam.packageName}")

        try {
            // 假设要 Hook 的类是 com.target.app.SomeClass
            // 假设要 Hook 的方法是 someMethod(String text, int number) 返回 boolean
            val targetClass = XposedHelpers.findClass("com.target.app.SomeClass", lpparam.classLoader)

            XposedHelpers.findAndHookMethod(
                targetClass, // 目标类
                "someMethod", // 目标方法名
                String::class.java, // 参数1类型
                Int::class.javaPrimitiveType, // 参数2类型 (注意基本类型用 .javaPrimitiveType)
                object : XC_MethodHook() { // Hook 回调
                    @Throws(Throwable::class)
                    override fun beforeHookedMethod(param: MethodHookParam) {
                        XposedBridge.log("进入 beforeHookedMethod: someMethod")
                        val arg1 = param.args[0] as String
                        val arg2 = param.args[1] as Int
                        XposedBridge.log("  参数1: $arg1")
                        XposedBridge.log("  参数2: $arg2")

                        // 示例：修改第一个参数
                        param.args[0] = "已被 Hook 修改"

                        // 示例：如果参数2是特定的值，则阻止原方法执行并直接返回 true
                        if (arg2 == 123) {
                            XposedBridge.log("  参数2为 123，阻止原方法执行并返回 true")
                            param.result = true // 设置返回值
                        }
                    }

                    @Throws(Throwable::class)
                    override fun afterHookedMethod(param: MethodHookParam) {
                        XposedBridge.log("进入 afterHookedMethod: someMethod")
                        // 只有当 beforeHookedMethod 没有设置 param.result 时，原方法才会执行
                        if (param.result != null) {
                            XposedBridge.log("  方法返回值 (可能被 before 修改): ${param.result}")
                        } else {
                            XposedBridge.log("  原方法执行完毕")
                            // 注意：如果原方法抛出异常，这里不会执行，且 param.throwable != null
                        }

                        // 示例：读取/修改最终返回值
                        val originalResult = param.result as? Boolean ?: false // 安全转换
                        XposedBridge.log("  最终返回值 (处理前): $originalResult")
                        // param.result = !originalResult // 比如取反
                    }
                }
            )

            XposedBridge.log("成功 Hook com.target.app.SomeClass.someMethod")

        } catch (e: Throwable) { // 捕获所有可能的错误，如 ClassNotFoundException, NoSuchMethodException 等
            XposedBridge.log("Hook 失败: ${e.message}")
            XposedBridge.log(e) // 打印完整堆栈跟踪
        }
    }
}
```

### 完整简要流程

当然，为了大家复现，我也贴了一段由AI生成的创建模块的讲解

**开发流程:**

1. **环境准备:**

   - **IDE:** Android Studio (最新稳定版推荐)。
   - **构建系统:** Gradle (Android Studio 内置)。
   - **编程语言:** Java 或 Kotlin (Kotlin 因其简洁性和现代特性更受欢迎)。
   - **LSPosed API:** 需要在项目中引入 LSPosed 提供的 API 库。
   - 测试设备:
     - 一台已 Root 的 Android 设备或模拟器。
     - 已安装 Magisk (用于 Zygisk) 或 Riru (较旧)。
     - 已安装 LSPosed Manager (通过 Magisk Manager 或 Riru 安装 LSPosed Zygisk/Riru 版本，然后安装管理器 APK)。

2. **创建 Android 项目:**

   - 在 Android Studio 中，选择 "File" -> "New" -> "New Project..."。
   - 选择一个模板，通常 "Empty Activity" 或 "No Activity" 都可以，因为模块本身不一定需要界面 (除非你想提供配置界面)。
   - 配置项目名称、包名、保存位置、语言 (Java/Kotlin) 和最低 SDK 版本。最低 SDK 通常可以设置得较低 (如 API 21 或更高)，但要确保你的代码兼容。

3. **配置项目依赖和 Manifest:**

   - 添加 LSPosed API 依赖:

     - 打开项目级别的 `build.gradle` 或 `build.gradle.kts` 文件 (通常是 `app` 模块下的那个)。
     - 在 `dependencies` 代码块中添加 LSPosed API 依赖。**关键：** 使用 `compileOnly` 或 `provided` 作用域，因为这个 API 在运行时由 LSPosed 框架提供，不需要打包进你的模块 APK 中。

     Gradle

     ```kotlin
     // build.gradle (Groovy)
     dependencies {
         // ... 其他依赖
         compileOnly 'de.robv.android.xposed:api:82' // 使用官方 Xposed API 版本号 82
         // 或者，如果你想用 LSPosed 提供的包含一些额外帮助类的 API (不常用，一般用官方 82 就行)
         // compileOnly 'org.lsposed.lsparanoid:lsparanoid:+' // '+ ' 会获取最新版，建议指定版本
     }
     
     // build.gradle.kts (Kotlin)
     dependencies {
         // ... 其他依赖
         compileOnly("de.robv.android.xposed:api:82")
         // compileOnly("org.lsposed.lsparanoid:lsparanoid:+")
     }
     ```

     - **注意:** API 版本 `82` 是 Xposed 原始 API 的最后一个正式版本，LSPosed 完全兼容它，通常使用这个即可。

       > 提示：这里别忘加源
       >
       > ```kotlin
       > dependencyResolutionManagement {
       >     repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
       >     repositories {
       >         google()
       >         mavenCentral()
       > 
       >         maven { url = uri("https://api.xposed.info/") }
       >     }
       > }
       > ```
       >
       > 

   - 配置 `AndroidManifest.xml`:

     - 打开 `app/src/main/AndroidManifest.xml`。
     - 在 `<application>` 标签内添加以下 `<meta-data>` 标签，以声明这是一个 LSPosed 模块：

     XML

     ```xml
     <application ...>
         <meta-data
             android:name="lsposedmodule"
             android:value="true" />
         <meta-data
             android:name="lsposeddescription"
             android:value="这里写模块的描述，会显示在 LSPosed 管理器中" />
         <meta-data
             android:name="lsposedminversion"
             android:value="1" /> <activity .../>
     </application>
     ```

4. **分析目标应用和寻找 Hook 点:**

   - **确定目标:** 你想修改哪个应用或系统功能的行为？
   - **反编译:** 使用反编译工具 (如 JADX-GUI) 打开目标应用的 APK 文件。
   - **代码分析:** 浏览反编译后的 Java 代码，找到你想要修改的功能对应的类和方法。记下完整的类名、方法名、参数类型和返回类型。
   - 注意事项:
     - 目标应用可能经过混淆 (ProGuard/R8)，类名和方法名可能变成无意义的字母 (如 `a.b.c`)。这会增加定位难度，并且 Hook 点可能在应用更新后失效。
     - 优先寻找逻辑清晰、不易变动的方法进行 Hook。
     - 注意方法的访问修饰符 (public, private, protected, package-private)。

5. **编写 Hook 代码:**

   - **创建 Hook 入口类:** 创建一个新的 Java 或 Kotlin 类，实现 `de.robv.android.xposed.IXposedHookLoadPackage` 接口。这个接口只有一个方法需要实现：`handleLoadPackage`。

   - 实现 `handleLoadPackage` 方法:

     这个方法会在每个应用加载时被 LSPosed 调用。

     - **过滤目标应用:** 在方法内部，首先检查 `LoadPackageParam` (通常命名为 `lpparam`) 的 `packageName` 字段，判断当前加载的是否是你的目标应用。如果不是，直接 `return`。

     - **执行 Hook:** 如果是目标应用，使用 `de.robv.android.xposed.XposedHelpers` 类提供的静态方法来查找并 Hook 目标方法。最常用的是 `findAndHookMethod`。

     - `XC_MethodHook` 回调:`findAndHookMethod `的最后一个参数是一个`XC_MethodHook`的匿名内部类 (或 Lambda 表达式) 实例。你需要重写它的`beforeHookedMethod`和/或`afterHookedMethod`方法。

       - `beforeHookedMethod(MethodHookParam param)`: 在原始方法执行 *之前* 调用。你可以读取/修改方法的输入参数 (`param.args`)，或者直接阻止原始方法执行并设置返回值/抛出异常 (`param.setResult()`, `param.setThrowable()`)。
       - `afterHookedMethod(MethodHookParam param)`: 在原始方法执行 *之后* 调用。你可以读取/修改方法的返回值 (`param.getResult()`, `param.setResult()`)，或者读取原始方法的执行结果和参数，然后执行其他操作。

## Kotlin Multiplatform

Kotlin Multiplatform (以前称为 Kotlin Multiplatform Mobile 或 KMM，现在范围更广) 是 JetBrains 提供的一种技术，允许开发者使用 Kotlin 编写代码，并将其共享到多个平台，如 Android, iOS, Web (JS), Desktop (JVM, Native), Server (JVM) 等。

简单来说就是逻辑代码一次编写处处运行，尽管现在 Compose Multiplatform 正在探索跨平台 UI，但是目前还不是production ready。kmp还主要是共享业务逻辑、数据层、网络请求、数据模型等非 UI 代码。

它大概是这样的：

- **Common Code (`commonMain`):** 编写平台无关的 Kotlin 代码。

- **Platform-Specific Code (`androidMain`, `iosMain`, `jsMain` 等):** 编写需要调用特定平台 API 的代码。

- **`expect`/`actual` 机制:** 在 `commonMain` 中声明预期的功能 (`expect` class/function/property)，然后在每个平台源集 (`androidMain`, `iosMain` 等) 中提供具体的实现 (`actual` class/function/property)。

  ```kotlin
  // commonMain
  expect fun getPlatformName(): String
  
  // androidMain
  actual fun getPlatformName(): String = "Android"
  
  // iosMain
  actual fun getPlatformName(): String = "iOS"
  ```

  

其实就是能共用的就共用，平台有关的就通过expect`/`actual在各个平台分别实现。

~~当然这个我也摸鱼...~~额，是有的，只不过这个想认真写写，写的差不多再开源。

emm这个怎么说，学习曲线有点陡，因为得各个平台开发比较熟悉，整体是：

- **编写 Common Logic:** 在 `commonMain` 中用纯 Kotlin 编写大部分共享逻辑非常顺畅，尤其是利用 Kotlin Coroutines 处理异步、Serialization 处理 JSON 等，共用的网络库，拦截管道和错误处理，共用的viewstate什么的真的非常爽

- **处理平台差异 (`expect`/`actual`):** 对于需要平台 API 的功能（如文件系统访问、特定硬件交互、日期格式化等），使用 `expect`/`actual` 模式。这部分需要同时理解 Kotlin 和目标平台的 API。当平台差异很大时，`actual` 实现可能会变得复杂，这个就得每个平台都得会了。

~~还有，kotlin和swift互操作是真的难受~~

这里先不贴代码了，因为我感觉自己写的不是很行啊@_@

## 简要说说

好了，这次的“摸鱼心得”就先到这里。（误）

从 Jetpack Compose 带来的 UI 编写新范式，到 Xposed (LSPosed) 赋予我们的“魔改”能力，再到 Kotlin Multiplatform 对“一次编写，多端运行”的探索，不难看出安卓（以及更广阔的移动开发领域）正经历着快速的演进。

这三者或许代表了现代安卓开发的不同切面：**面向未来的 UI 构建**、**深入底层的系统定制**、以及**跨平台代码复用的探索**。它们都基于 Kotlin 的强大能力。对我来说，折腾这些新技术，也确是“悲惨生活”中的一点短暂休息了。

讲的不深，简单说说，希望这篇小文章让你对这些技术产生兴趣，~~那就下次摸鱼再见吧！~~
