linux下多线程文件操作的坑
有一块业务之前的代码逻辑大概是: 图片下载以后会在kotlin中的协程中 对这个图片进行一系列的读写操作,但是因为业务逻辑会在 bindViewHolder 中进行处理,所以这个流程会在短时间内执行多次
线上有用户反馈 这个业务逻辑的图片有时候会加载不出来,有时候加载处理这个图片只能展示一部分(这个最奇怪)
这个问题定位了很久 最终找到的原因简单概括下:
在 linux 中,一个进程内,如果有多个线程打开了同一个文件文件,那么只要有一个线程持有的 FD 做了 close 操作,那么其他线程的文件操作 都会失败
本身是一个 android 问题,最终还是 linux 默默承担了一切
实际上很多 app 都有类似的问题,比如 sqlite 文件锁 失败以后就会导致 sqlite 的崩溃, 很多 app 的 sqlite 文件锁失败的原因 都是 app 在启动的时候做了文件扫描,扫描到 sqlite 文件锁的文件之后 close 从而导致 sqlite 整个文件锁机制失效
java.lang.RuntimeException: android.os.TransactionTooLargeException: data parcel size
通常人们会认为这是比较简单的 bundle 传值导致的异常,而实际上排查下来 我们并没有出现过直接用 bundle 传递大数据的情况
所以真相只可能是 onSaveInstanceState 这个方法执行的过程 间接的保存了 bundle 导致的问题
这里推荐 com.gu.android:toolargetool:0.3.0 这个库,这个库可以自动打印出 bundle 的大小
最终的解决方案也比较简单,就是在onSaveInstanceState方法中 判断下 bundle 是否超过警戒值,超过了就 clear 不走状态保存
val bundleSize = BundleUtils.sizeAsParcel(outState)
if (bundleSize >= MAX_BUNDLE_SIZE) {
// 清空数据
outState.clear()
} else {
}
object BundleUtils {
/**
*
* @return bundle的大小 单位是byte
*/
fun sizeAsParcel(bundle: Bundle): Int {
val parcel = Parcel.obtain()
try {
parcel.writeBundle(bundle)
return parcel.dataSize()
} finally {
parcel.recycle()
}
}
}
稳定复现app 挂在后台 回收 activity 的方案:
app挂到后台之后
adb shell kill -9 进程号
然后再切换到app到前台
java.lang.IllegalArgumentException: reportSizeConfigurations: ActivityRecord not found for:
老 android 系统的 bug issuetracker.google.com/issues/1197…
要解决这个问题,try catch 即可
但是要注意了 这个代码是在 ams 这个 binder 的 server 进程中的,我们干预不了这个进程 所以我们 try-cath 的对象应该是 activitymanger 这个 client
找到这条调用链,剩下就是 hook 即可,这里就用常见的动态代理 hook 即可
try {
// 第一步 先拿到这个静态变量
val am: Field = ActivityManager::class.java.getDeclaredField("IActivityManagerSingleton")
am.isAccessible = true
val iActivityManagerSingleton = am.get(null) ?: return
// 第二步 就是要拿到 IActivityManagerSingleton 这个静态变量的mInstance
// android framework 中的单例实现为 Singleton.java 这个类 其实拿的就是这个类的mInstance
val singletonCls = iActivityManagerSingleton.javaClass.superclass ?: return
val instance: Field = singletonCls.getDeclaredField("mInstance")
instance.isAccessible = true
val iActivityManager = instance.get(iActivityManagerSingleton)
// 一般来说 android系统中 如果你看到一个以I开头的类 他有.h文件 那几乎可以判定 是binder中的实现
// 此时Ixxxx.java 并不会在源码中搜索到 他是通过aidl工具生成的
// IActivityManager.aidl通过AIDL工具自动生成IActivityManager.java
// 这里要注意 我们自己写进程间通信 不使用aidl 都可以,手写也不是问题,aidl只是方便 自动化帮你对齐
val iActivityManagerCls = Class.forName("android.app.IActivityManager") ?: return
val classes = arrayOf(iActivityManagerCls)
val iActivityManagerProxy = Proxy.newProxyInstance(
iActivityManagerCls.classLoader,
classes,
IActivityManagerProxy(iActivityManager)
)
instance.set(iActivityManagerSingleton, iActivityManagerProxy)
} catch (e: Exception) {
}
private class IActivityManagerProxy(
private val mActivityManager: Any?
) : InvocationHandler {
override fun invoke(proxy: Any?, method: Method?, args: Array<out Any>?): Any? {
return if ("reportSizeConfigurations" == method?.name) {
try {
method.invoke(mActivityManager, *(args ?: emptyArray()))
} catch (e: Exception) {
null
}
} else {
try {
method?.invoke(mActivityManager, *(args ?: emptyArray()))
} catch (e: Exception) {
}
}
}
}