Xposed模块使用DataStore存储配置信息
探索
想在Xposed模块中存储用户偏好配置信息?翻看各种教程文章和API,都是使用 SharedPreferences 来存储用户配置信息:
在模块中 val sp = Context.getSharedPreferences("name", MODE_WORLD_READABLE) 来获取 SharedPreferences 对象,在宿主中,使用 val xsp = XSharedPreferences(BuildConfig.APPLICATION_ID, "prefFileName") 来获取 XSharedPreferences 对象。
优点是易于理解,缺点是需要定义字符串的 key ,每次获取需要写默认值: getString("key", "defaultValue") ,当有大量的配置时,代码量会非常多。
在常规的安卓开发中,已经有了现代化的解决方案:DataStore
Jetpack DataStore 是一种数据存储解决方案,允许您使用协议缓冲区存储键值对或类型化对象。DataStore 使用 Kotlin 协程和 Flow 以异步、一致的事务方式存储数据。
如果您目前是使用 SharedPreferences 存储数据的,请考虑迁移到 DataStore 。
对比 DataStore 和 XSharedPreferences 文件存储位置
DataStore 把数据存在哪里了呢?查看源码:
package androidx.datastore.core
public fun Context.deviceProtectedDataStoreFile(fileName: String): File {
// Make sure it is a DeviceProtectedStorageContext and convert to a
// DeviceProtectedStorageContext if it is not.
val deviceProtectedContext = requireDeviceProtectedStorageContext()
return File(deviceProtectedContext.filesDir, "datastore/$fileName")
}
package androidx.datastore
public fun Context.dataStoreFile(fileName: String): File {
return File(this.applicationContext.filesDir, "datastore/$fileName")
}
- 使用
val Context.dataStore by dataStore("YourFileName.json", YourSerializer) 则存储在 /data/data/your.packagename/files/datastore/YourFileName.json 文件中。
- 使用
val Context.dataStore by deviceProtectedDataStore("YourFileName.json", YourSerializer) 则存储在 /data/user_de/0/your.packagename/files/datastore/YourFileName.json 文件中。
宿主能不能读取这两个文件呢? 用 MT管理器给这两个文件赋予 644 权限( -rw-r--r-- 全都可读),再到 Termux 里 cat 一下,很不幸,全都是 Permission denied 。
那 XSharedPreferences 是怎样做到宿主读取配置文件的呢?
试一下 XSharedPreferences(BuildConfig.APPLICATION_ID, "YourFileName").file.absolutePath ,路径是 /data/misc/f1624d22-c2dd-46cd-9adb-ce0d87bf645c/prefs/your.packagename/YourFileName.xml (其中的 UUID 每个手机并不相同),而且发现它的权限就是 644 ,Termux cat 一下,成功读取。
劫持模块自身 DataStore 创建文件
那么就有一个绝妙的思路:劫持模块自身 DataStore 创建文件!在上述路径下创建一个文件,并通过 hook 返回该文件:
object SelfHook {
fun enableDataStoreFileSharing(loadPackageParam: LoadPackageParam) {
if (loadPackageParam.packageName != BuildConfig.APPLICATION_ID) return
val path = XSharedPreferences(BuildConfig.APPLICATION_ID).file.parent
val file = File(path, "YourDataStoreFileName.json")
if (!file.exists()) {
file.writeText("{}")
@SuppressLint("SetWorldReadable")
file.setReadable(true, false)
}
findAndHookMethod(
"androidx.datastore.core.DeviceProtectedDataStoreFile",
loadPackageParam.classLoader,
"deviceProtectedDataStoreFile",
Context::class.java,
String::class.java,
returnConstant(file)
)
findAndHookMethod(
"androidx.datastore.DataStoreFile",
loadPackageParam.classLoader,
"dataStoreFile",
Context::class.java,
String::class.java,
returnConstant(file)
)
}
}
Shift + F10 运行完毕,立刻到 Termux cat 一下,哇,能读取到数据!
立马编写 hook 逻辑,在宿主里
val path = XSharedPreferences(BuildConfig.APPLICATION_ID).file.parent
val file = File(path, "YourDataStoreFileName.json")
val json = file.readText()
果然在宿主里也能读取到数据,大功告成!了吗?
解决文件权限
开发一定要做好自测✍️✍️✍️。在模块中修改配置,然后重启宿主,发现功能没有生效,打印日志发现文件的权限变成 600 (其他用户不可读)。
此时灵感枯竭,于是问当今最强AI: Gemini 2.5 Pro,它告诉我在每回 dataStore.updateData {} 之后重新设置该文件为可读。我才不会这样做,这太不优雅了,每次调用加一句 dataStoreFile.setReadable(true, false) ,跟正常的 DataStore 使用方式不一样,很麻烦,而且如果某次忘了加这句代码,就会导致配置文件都变成不可读,导致 hook 功能失效。
那我 hook 自身的 dataStore.updateData {} 在其执行后执行
val path = XSharedPreferences(BuildConfig.APPLICATION_ID).file.parent
val file = File(path, "YourFileName.json")
@SuppressLint("SetWorldReadable")
file.setReadable(true, false)
真是优雅的实现,但经过测试发现,在更改配置后, YourDataStoreFileName.json 仍然是 600 不可读……
不放弃,那我试试 hook Serializer 的 writeTo 方法
object PreferenceSerializer : Serializer<Preference> {
override suspend fun readFrom(input: InputStream): Preference {
return try {
Json.decodeFromString(
deserializer = Preference.serializer(),
string = input.readBytes().decodeToString()
)
} catch (e: SerializationException) {
e.printStackTrace()
defaultValue
}
}
override suspend fun writeTo(t: Preference, output: OutputStream) {
output.write(
Json.encodeToString(
serializer = Preference.serializer(),
value = t
).encodeToByteArray()
)
}
override val defaultValue: Preference = Preference()
}
val Context.dataStore by dataStore("whatever", PreferenceSerializer)
很不幸,还是 600 不可读……
思索良久,每次写入配置文件都要设置全部可读权限,那能不能在写入配置文件完毕后,一次性设置为全部可读权限呢,只要保证设置权限之后不再修改模块配置,就能保证宿主可读取。那这不就是生命周期该干的事情吗?一个好的安卓开发此时应该意识到了 onPause() ,在 Activity 失去焦点时便会调用它,正符合需求。
修改 MainActivity :
// 这里使用 "whatever" 是因为 dataStoreFile() 方法已经被 hook 劫持了,所以用什么名字都行。
val preferenceFile = dataStoreFile("whatever")
@SuppressLint("SetWorldReadable")
override fun onPause() {
super.onPause()
preferenceFile.setReadable(true, false)
}
再次测试,修改模块配置,重启宿主,功能生效了。至此解决了模块配置文件权限问题。
新安装崩溃
再次谨记,开发一定要做好自测✍️✍️✍️,卸载模块,重新安装,并且不在 LSPosed 中启用,用来模拟第一次安装。打开模块,哦吼,闪退了,查看 logcat :
java.lang.RuntimeException: Unable to instantiate activity ComponentInfo{your.packagename/your.packagename.MainActivity}: java.lang.NullPointerException: Attempt to invoke virtual method 'android.content.Context android.content.Context.getApplicationContext()' on a null object reference
……
Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'android.content.Context android.content.Context.getApplicationContext()' on a null object reference
是 val preferenceFile = dataStoreFile("whatever") 出错了,根据源码
public fun Context.dataStoreFile(fileName: String): File {
return File(this.applicationContext.filesDir, "datastore/$fileName")
}
此时没有 applicationContext,所以崩溃了。好办,直接一手
val preferenceFile by lazy { dataStoreFile("whatever") }
@SuppressLint("SetWorldReadable")
private fun setWorldReadable(): Boolean = preferenceFile.setReadable(true, false)
@SuppressLint("SetWorldReadable")
override fun onPause() {
super.onPause()
setWorldReadable()
}
完美解决,而且还可以通过 setWorldReadable() 来判断模块是否被启用:
ProGuard/R8 压缩
接下来,开发一定要做好自测✍️✍️✍️。打 release 包,安装测试,又双叒叕出错了,查看 LSPosed 日志,发现找不到 DataStore 相关的类。哦哦哦,是我启用了 ProGuard/R8 压缩,那就保留相关的类及其方法:
-keep class androidx.datastore.DataStoreFile {
public static java.io.File dataStoreFile(android.content.Context, java.lang.String);
}
-keep class androidx.datastore.core.DeviceProtectedDataStoreFile {
public static java.io.File deviceProtectedDataStoreFile(android.content.Context, java.lang.String);
}
再次自测,没问题了。
总结引入 DataStore 的步骤
1. 添加依赖
libs.versions.toml :
[versions]
datastore = "1.2.0-alpha02"
kotlinxSerializationJson = "1.9.0"
[libraries]
datastore = { group = "androidx.datastore", name = "datastore", version.ref = "datastore" }
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
[plugins]
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
build.gradle.kts :
plugins {
alias(libs.plugins.kotlin.serialization)
}
dependencies {
implementation(libs.datastore)
implementation(libs.kotlinx.serialization.json)
}
按 Ctrl+Shift+O Sync Project With Gradle Files
2. Hook DataStore
PreferenceProvider :
object PreferenceProvider {
private const val PREFERENCE_FILE_NAME = "preference.json"
val preference: Preference? = try {
Json.decodeFromString<Preference>(getPreferenceFile().readText())
} catch (_: Throwable) {
null
}
fun getPreferenceFile(): File {
val path = XSharedPreferences(BuildConfig.APPLICATION_ID).file.parent
val file = File(path, PREFERENCE_FILE_NAME)
if (!file.exists()) {
file.writeText("{}")
@SuppressLint("SetWorldReadable")
file.setReadable(true, false)
}
return file
}
}
SelfHook :
object SelfHook {
fun enableDataStoreFileSharing(loadPackageParam: LoadPackageParam) {
if (loadPackageParam.packageName != BuildConfig.APPLICATION_ID) return
val callback = returnConstant(PreferenceProvider.getPreferenceFile())
findAndHookMethod(
"androidx.datastore.core.DeviceProtectedDataStoreFile",
loadPackageParam.classLoader,
"deviceProtectedDataStoreFile",
Context::class.java,
String::class.java,
callback
)
findAndHookMethod(
"androidx.datastore.DataStoreFile",
loadPackageParam.classLoader,
"dataStoreFile",
Context::class.java,
String::class.java,
callback
)
}
}
MainHook :
class MainHook : IXposedHookLoadPackage, IXposedHookInitPackageResources {
override fun handleLoadPackage(lpparam: LoadPackageParam) {
if (lpparam.packageName == BuildConfig.APPLICATION_ID) {
SelfHook.enableDataStoreFileSharing(lpparam)
}
val preference = PreferenceProvider.preference ?: return
when (lpparam.packageName) {
Package.ANDROID -> {
// 省略余下
3. 确保 DataStore 文件全部可读
修改偏好配置所在 Activity,在其失焦时使文件全部可读
MainActivity :
val preferenceFile by lazy { dataStoreFile("whatever") }
@SuppressLint("SetWorldReadable")
private fun setWorldReadable(): Boolean = preferenceFile.setReadable(true, false)
@SuppressLint("SetWorldReadable")
override fun onPause() {
super.onPause()
setWorldReadable()
}
4. ProGuard/R8
如果启用了代码压缩/混淆,则需要添加以下规则:
-keep class your.packagename.MainHook
-keep class androidx.datastore.DataStoreFile {
public static java.io.File dataStoreFile(android.content.Context, java.lang.String);
}
-keep class androidx.datastore.core.DeviceProtectedDataStoreFile {
public static java.io.File deviceProtectedDataStoreFile(android.content.Context, java.lang.String);
}
其中 your.packagename.MainHook 是模块入口类,与 assets/xposed_init 文件中的类名一致。