简单聊下最近处理的 3 个线上问题

446 阅读3分钟

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…

image.png

要解决这个问题,try catch 即可

但是要注意了 这个代码是在 ams 这个 binder 的 server 进程中的,我们干预不了这个进程 所以我们 try-cath 的对象应该是 activitymanger 这个 client

image.png

找到这条调用链,剩下就是 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) {
            }
        }
    }
}