资源混淆是如何影响到Kotlin协程的

3,927 阅读4分钟

导言

随着kotlin的使用,协程也慢慢在我们工程中被开始被使用起来,但在我们工程中却遇到了一个问题,经过资源混淆处理之后的apk包,协程却不如期工作。那么两者到底有什么关联呢,资源混淆又是如何影响到协程的使用的,通过阅读本篇你会马上知晓。

本篇会从如下几个方面讲述这个问题

问题定义->问题分析->问题解决

问题定义

看下面这段demo代码:

package com.example.coroutinenotworkdemo

import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.provider.Settings
import android.util.Log
import android.widget.Toast
import android.widget.Toast.LENGTH_SHORT
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.coroutines.*
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import kotlin.system.measureTimeMillis

class MainActivity : AppCompatActivity(), CoroutineScope {
    override val coroutineContext: CoroutineContext
        get() = Job()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        clickid.setOnClickListener {
            GlobalScope.async {
                Log.i("pisa","start call async")
                val cost=measureTimeMillis {
                    val result=demoSupendFun()
                    Log.i("pisa","get result=$result")
                    //下面经过资源混淆之后,withContext里面的块没得到执行。。
                    withContext(Dispatchers.Main){
                        textview.text=result
                    }
                }
                Log.i("pisa","cost=$cost")
                0
            }
            Toast.makeText(this,"click result",LENGTH_SHORT)
        }

    }

    suspend fun demoSupendFun(): String {
        return suspendCoroutine {
            //模拟一个异步请求,然后回调,得到结果
            async {
                delay(1000)
                it.resume("get result")
            }
        }
    }
}

我们发现经过资源混淆之后,下面这段代码中,textview.text=result始终没有得到执行。

withContext(Dispatchers.Main){
    textview.text=result
}

那么这是为什么呢?

问题分析

既然跟资源混淆有关,那么我们看看经过资源混淆之后的apk和之前的apk到底又哪些改变。 资源混淆用的是之前微信开源的的[andResguard][1],简单来说,资源混淆包括如下几个步骤:

  1. 解压缩apk
  2. 混淆算法开始混淆res文件,并改下resources.arsc文件
  3. 用7zip重压缩apk,重签名

看起来,1和2对于影响到协程使用可能性很低,那么3呢,在对比前后apk过程中我们马上发现混淆前后的apk的METF-INF文件相差比较大,混淆后只保留了SF,MF,RSA文件,而混淆前的apk的METF-INF文件中包含了一些kotlin_module信息以及services文件夹,那么会不会和这些文件的丢失有关呢。

混淆前和混淆后META-INF文件夹差异

怎么验证呢。很简单,gradle里面配置packageOptions主动移除META-INF文件夹下的kotlin_module文件和services文件夹,然后debug调试一下发现问题复现。那么肯定和这里有关啦。

现在先不急着马上解决它,让我们看看为啥这几个文件的丢失就会导致上面那段协程代码工作不正常呢。既然有demo,那我们单步调试进去看看吧。

上面例子中调用了async函数,通过源码可以知道,如果start参数是用的默认的情况下,那么最后都会走到startCoroutineCancellable函数,而这个函数内部会调用runSafely,内部所有的异常都会被这个函数catch住,所以业务层没抛crash,直接把这个问题隐藏了,也给快速定位问题加大了难度。

在这里插入图片描述
既然用demo复现了这个问题,那么单步调试一下,看看withContext里面到底挂在了哪里?最终调试发现,果然这里runSafely里面catch住了一个exception,异常信息如下: Module with the Main dispatcher is missing.Add dependency providing the Main dispatcher, e.g. 'kotlinx-coroutines-android 所以上面withContext里面的代码就没有执行到了。

那么这里的MainDispatcher是什么呢?原来是在调用withContext来切换线程的时候,会用到类MainCoroutineDispatcher。这个类是个抽象类,会经过MainDispatcherFactory工厂来创建具体的dispatcher,在Android上是AndroidDispatcherFactory来负责创建,MainDispatcherFactory这个类是通过自定义的ServiceLoader加载进来的,在kotlin中定义了一个FastServiceLoader,这个类与java的ServiceLoader最大的区别是跳过了jar的校验,可以直接从jar包中加载某一个类的信息,如果用常规的ServiceLoader是需要读取整个jar包之后,在定位到对应的class文件信息,加载进来,这整个过程是一个非常耗时的操作,可能导致android设备发生ANR的现象。

看看FastServiceLoader是如何加载AndroidDispatcherFactory的,如下图所示:

在这里插入图片描述
看到这个类瞬间明白了,kotlin在编译的时候,会在META-INF文件夹下生成一个services的文件夹信息,该文件夹下面放一些支持类的信息,那么具体在放了哪些类呢,在源码当中有一个pro文件可以说明一切。
在这里插入图片描述
这样在调用相关类的时候会优先先用FastServiceLoader加载该类。一旦加载不到,就会构造一个MissingMainCoroutineDispatcher,并调用missing方法抛出异常。
在这里插入图片描述

问题解决

经过上述问题分析之后,其实解决方案就非常简单了。修改资源混淆重打包的流程,在重签名的时候保留META-INF的servcies文件夹信息即可

回顾总结

再来回顾一下问题的解决过程,虽然最终解决的方案比较简单,但有两个点需要我们特别关注一下

  1. 协程当中async内部有try catch机制,所以任何异常都会被内部catch住,而这个在我们开发当中很容易导致一些问题没有及时发现
  2. 在遇到一些奇怪的问题的时候,小而简单的demo外加源码阅读是必要的,这样方便我们快速能够追查到问题原因所在。