借助Kotlin特性打造一个有Kotlin味道的Activity跳转工具类库

前言

相信同学们都有过这种感受:在日常开发中,每次使用startActivityForResult时,要做的事情都好多,好麻烦:

  1. 定义一个requestCode

  2. 重写onActivityResult方法并在里面去判断requestCoderesultCode

  3. 如果有携带参数,还要一个个通过putExtra方法putIntent里;

  4. 目标Activity处理完成后还要把数据一个个putIntent中,setResult然后finish

  5. 如果参数是可序列化的泛型类对象(如ArrayList),取出来的时候不但要显式强制转型,还要把 UNCHECKED_CAST 警告抑制;

当然了,在Github上已经有好几个开源库把 “需要重写onActivityResult方法来接收处理结果” 的问题解决了(其中的原理相信很多同学都已经了解过了,这个我们等下也会详细讲解的)。
但如果有携带参数的话,依然很麻烦:有多少个参数,就要调用多少次putExtra方法
而且最烦的是第5点,转成泛型类对象时,一块黄色的警告在那里,看着挺难受。

不过还好,随着Kotlin越来越普及,越来越多的开发者都体验到了它的魅力,也许我们可以借助它的一些特性,来做一些事情。。。


简化Intent.putExtra操作

这个思路我是直接CV了大佬旺的代码,原文链接:https://www.jowanxu.top/2019/02/11/Android-Intent-Extension/

Kotlin的项目中,当我们初始化Map的时候,通常会这样写:

    val map = mapOf(
        "key0" to "value0", 
        "key1" to "value1", 
        "key2" to "value2"
    )

KeyValue之间的to,是一个中缀函数,它返回一个Pair对象。
mapOf方法接收一个类型为Pair的可变参数:

    fun <K, V> mapOf(vararg pairs: Pair<K, V>)

最终的实现,它会遍历这个数组pairs,并依次把这些键值对put到一个LinkedHashMap中。

那么我们也可以参照这个mapOf,来为Intent写一个扩展方法putExtras,让它在使用的时候,就像创建Map对象那样:

    fun Intent.putExtras(vararg params: Pair<String, Any>): Intent {
        if (params.isEmpty()) return this
        params.forEach { (key, value) ->
            when (value) {
                is Int -> putExtra(key, value)
                is Byte -> putExtra(key, value)
                is Char -> putExtra(key, value)
                is Long -> putExtra(key, value)
                is Float -> putExtra(key, value)
                is Short -> putExtra(key, value)
                is Double -> putExtra(key, value)
                is Boolean -> putExtra(key, value)
                is Bundle -> putExtra(key, value)
                is String -> putExtra(key, value)
                is IntArray -> putExtra(key, value)
                is ByteArray -> putExtra(key, value)
                is CharArray -> putExtra(key, value)
                is LongArray -> putExtra(key, value)
                is FloatArray -> putExtra(key, value)
                is Parcelable -> putExtra(key, value)
                is ShortArray -> putExtra(key, value)
                is DoubleArray -> putExtra(key, value)
                is BooleanArray -> putExtra(key, value)
                is CharSequence -> putExtra(key, value)
                is Array<*> -> {
                    when {
                        value.isArrayOf<String>() ->
                            putExtra(key, value as Array<String?>)
                        value.isArrayOf<Parcelable>() ->
                            putExtra(key, value as Array<Parcelable?>)
                        value.isArrayOf<CharSequence>() ->
                            putExtra(key, value as Array<CharSequence?>)
                        else -> putExtra(key, value)
                    }
                }
                is Serializable -> putExtra(key, value)
            }
        }
        return this
    }

因为IntentputExtra方法只接受String类型的Key,所以我们也直接指定的Pair的第一个值的类型为String了。
总体的逻辑很简单,就像mapOf方法那样:遍历Pair数组,判断每一个参数值的类型(不同于Java的是,在Kotlin中用is关键字检查类型符合之后,会自动转换成对应的类型,无须显式转换),并通过IntentputExtra方法把参数put进去。

那么现在给Intent设置多个参数的时候,就可以写成这样:

    val intent = Intent()
    intent.putExtras(
        "key0" to true,
        "key1" to 1.23F,
        "key2" to listOf("1",  "2")
    )

哈哈,的确方便又好看了好多。


简化startActivity操作

虽然说,在没有工具类的帮助下,startActivity一样也可以一句代码搞定,如果不携带参数的话。
但如果要携带参数,就至少要三句了:

  1. 创建Intent对象;
  2. 把参数put进Intent中;
  3. 调用startActivity方法;

这种情况我们完全可以借助刚刚的扩展方法putExtras,来把它简化成一行,就像这样:

 startActivity(this, TestActivity::class, "key0" to "value0", "key1" to "value1")

内部是怎么写的呢? 其实超简单:

   fun startActivity(
       starter: Activity,
       target: KClass<out Activity>,
       vararg params: Pair<String, Any>
   ) = starter.startActivity(Intent(starter, target.java).putExtras(*params))

我们封装的startActivity方法接收三个参数,分别是:发起的Activity要启动的Activity可变参数键值对
target的类型KClass后面的,在Java中等同于
params前面的vararg,就是定义成可变参数的意思,等同于Java中的Pair... params
最后可以看到,也是直接调用了ActivitystartActivity方法,并把调用putExtras扩展方法后的Intent传进去。

就这么简单。


简化startActivityForResult操作

相信很多同学在Java项目上也有用过一些类似的库,就是把重写onActivityResult改成匿名内部类
不过我们现在用了Kotlin,就可以把匿名内部类改成Lambda了,就像这样:

    startActivityForResult(this, TestActivity::class,
        "key0" to "value0",
        "key1" to 1.23F,
        "key2" to listOf(true, false)
    ) {
        if (it == null) {
            //未成功处理,即(ResultCode != RESULT_OK)
        } else {
            //处理成功,这里可以操作返回的intent
        }
    }

当目标Activity处理完成之后,就会回调到后面的Lambda中。

其中的原理很简单,就是临时附加一个无界面的Fragment,当这个Fragment附加成功之后,调用startActivityForResult方法,并在重写的onActivityResult方法里面去回调外面传进来的Lambda
来看看代码怎么写:
先是Fragment

class GhostFragment : Fragment() {

    private var requestCode = -1
    private var intent: Intent? = null
    private var callback: ((result: Intent?) -> Unit)? = null

    fun init(requestCode: Int, intent: Intent, callback: ((result: Intent?) -> Unit)) {
        this.requestCode = requestCode
        this.intent = intent
        this.callback = callback
    }

    override fun onAttach(context: Context) {
        super.onAttach(context)
        //附加到Activity之后马上startActivityForResult
        intent?.let { startActivityForResult(it, requestCode) }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        //检查requestCode
        if (requestCode == this.requestCode) {
            //检查resultCode,如果不OK的话,那就直接回传个null
            val result = if (resultCode == Activity.RESULT_OK && data != null) data else null
            //执行回调
            callback?.let { it(result) }
        }
    }

    override fun onDetach() {
        super.onDetach()
        intent = null
        callback = null
    }
}

可以看到有一个init方法,它用来接收外面传进来的requestCode,intentcallback
requestCode怎么定义好呢?
为了避免相同的requestCode,我们可以定义一个静态的Int变量,在每次startActivityForResult之后自增,如果次数多到要溢出时,重新设置为1就OK了,看代码:

    private var sRequestCode = 0
        set(value) {
            field = if (value >= Integer.MAX_VALUE) 1 else value
        }

好,现在来创建一个startActivityForResult方法:

    fun startActivityForResult(
        starter: Activity,
        target: KClass<out Activity>,
        vararg params: Pair<String, Any>,
        callback: ((result: Intent?) -> Unit)
    ) {
        //初始化intent
        val intent = Intent(starter, target.java).putExtras(*params)
        val fm = starter.supportFragmentManager
        //无界面的Fragment
        val fragment = GhostFragment()
        fragment.init(++sRequestCode/*先让requestCode自增*/, intent) { result ->
            //包装一层:在回调执行完成之后把对应的Fragment移除掉
            callback(result)
            fm.beginTransaction().remove(fragment).commitAllowingStateLoss()
        }
        //把Fragment添加进去
        fm.beginTransaction().add(fragment, GhostFragment::class.java.simpleName).commitAllowingStateLoss()
    }

emmm,这个startActivityForResult,就比刚刚的startActivity多了一个callback参数。
可以看到我们并没有直接把这个callback传进fragment里面,而是在外面包装了一层。当这个callback执行完成之后,还会把对应的FragmentFragmentManager中移除掉,就是为了能让这个FragmentonDetach方法尽快回调(把callback的引用置空),一定程度上避免内存泄漏(因为外部的callback可能持有生命周期比较短的对象引用)。
在最后,会把Fragment附加到Activity中,当FragmentonAttach回调时,就会启动目标Activity了。


简化finish操作

startActivityForResult方法启动的Activity处理任务完成之后,通常要把一些数据传回给启动者,这个操作我们也是可以优化的:

    fun finish(src: Activity, vararg params: Pair<String, Any>) = with(src) {
        setResult(Activity.RESULT_OK, Intent().putExtras(*params))
        finish()
    }

可以看到,把参数putIntent之后,就调用了setResult方法把Intent放进去,并finish
那么在使用的时候,就可以这样写了:

    finish(this, "key0" to "value0", "key1" to "value1")

哈哈,是不是简洁了许多。


优化Intent.getExtra操作

还记不记得findViewById
哈哈哈哈,在compileSdkVersion为26之前,通过findViewById取得的View子类实例时都要进行强制类型转换,26之后,才换成了泛型方法。
现在的getSerializableExtra也存在同样的问题,如果取出来的是一个泛型类的实例,比如List,强制转换后还会出一个 UNCHECKED_CAST 警告,黄色的一块,看着很难受。

那么现在我们也可以参照findViewById,给Intent创建一个扩展方法:
想一下:IntentgetStringExtragetIntExtragetBooleanExtra等一系列方法,内部都是借助Bundle来实现的,CTRL点进去会看到,Bundle是通过一个Map来储存这些键值对的,比如这个getStringExtra方法:

    public String getString(String key) {
        unparcel();
        final Object o = mMap.get(key);
        try {
            return (String) o;
        } catch (ClassCastException e) {
            typeWarning(key, o, "String", e);
            return null;
        }
    }

可以看到,它从mMap中取出值之后,还是会显式地进行强制类型转换的,那等我们给Intent加了泛型的扩展方法之后,除了可代替getSerializableExtra之外,剩下的那些getXXXExtra,是不是也一样可以代替掉呢?
答案是肯定的。因为这些键值对,都是存放在同一个Map的实例里面,如果拿到了这个实例。。。就可以为所欲为了 ,哈哈哈。
那怎么拿到这个存放键值对的实例呢?
用反射。先拿到Intent里面的mExtras(Bundle的实例),再通过这个mExtras来获取到mMap(存放键值对的Map实例)。

不过,我看了不同版本的Bundle代码,发现在5.0之后,Bundle的主要逻辑被抽取到一个叫BaseBundle的类里面了,而在此之前,是没有BaseBundle这个类的,所以等下在获取mMapField之前要做一下版本判断。
还有,当我直接用反射拿到mMap的对象引用之后,却发现是null的,再次翻源码后才注意到:Bundle里面的每一个getXXX方法中,第一句都是会调用unparcel方法的,这个方法里面会调用一个叫initializeFromParcelLocked的方法,没错,mMap正是在这个方法里面初始化的,然后通过ParcelreadArrayMapInternal方法来填充键值对 。所以等下在获取mMap实例之前,还要先调用一下unparcel方法(也是通过反射)。
那现在要用到的内部属性和方法一共有3个了(mExtrasmMapunparcel),我们可以把它缓存起来,这样就不用每次都重新获取,可以提升运行效率:

internal object IntentFieldMethod {
    lateinit var mExtras: Field
    lateinit var mMap: Field
    lateinit var unparcel: Method

    init {
        try {
            mExtras = Intent::class.java.getDeclaredField("mExtras")
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                mMap = BaseBundle::class.java.getDeclaredField("mMap")
                unparcel = BaseBundle::class.java.getDeclaredMethod("unparcel")
            } else {
                mMap = Bundle::class.java.getDeclaredField("mMap")
                unparcel = Bundle::class.java.getDeclaredMethod("unparcel")
            }
            mExtras.isAccessible = true
            mMap.isAccessible = true
            unparcel.isAccessible = true
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }
}

然后在扩展方法中直接引用:

    fun <O> Intent.get(key: String): O? {
        try {
            //获取Intent.mExtras实例
            val extras = IntentFieldMethod.mExtras.get(this) as Bundle
            //调用unparcel方法来初始化mMap
            IntentFieldMethod.unparcel.invoke(extras)
            //获取Bundle.mMap实例
            val map = IntentFieldMethod.mMap.get(extras) as Map<String, Any>
            //取出对应的key
            return map[key] as O
        } catch (e: Exception) {
            //Ignore
        }
        return null
    }

emmm,有了这个扩展方法之后,在获取Intent参数时就可以写成这样了:

    //预先声明好类型
    var mData: List<String>? = null
    mData = intent.get("Key1")

或者这样:

    //取出时再决定类型
    val result = intent.get<Int>("Key2")

因为我们定义的是泛型的方法,所以无论取出来什么类型的值都好,都不用显式地强制转换了,也没有 UNCHECKED_CAST 警告了,哈哈,是不是很棒。

上面所提到的内部属性和方法,都是没有被标记@hide的,可放心使用。

刚刚说到把那些FieldMethod缓存起来,可以提高效率,那究竟比没有缓存的情况下提高了多少呢?
我分别对以下操作进行了耗时统计:

  1. 直接调用IntentgetXXExtra来获取值;

  2. 重用FieldMethod,每次先通过反射获取mMap,然后调用mMap.get方法来获取值;

  3. 不重用,每次都先通过Class取得对应的FieldMethod,然后通过反射获取mMap,再通过mMap.get方法来获取值;

以下是分别循环10W次的总耗时(单位: ms):

第一次:
15
182
1242
第二次:
8
100
1145
第三次:
8
96
1143
第四次:
8
104
1206
第五次:
10
115
1365

可以看到,重用FieldMethod的话,效率会提升10倍左右,虽然远没有不使用反射的效率高,但是,重复10W次才100毫秒左右,也就是平均每次才花费0.001ms左右,牺牲这点微不足道的效率给我们带来方便,是非常划算的。


Kotlin内联函数不为人知的一面

我们在学习Kotlin的过程中,都会了解到内联函数,适当地使用可以提高性能,节省资源。如果一个方法它被标记成了内联函数,那么在编译时,会将它的方法体,替换到调用它的地方,这时候就不算是正常的方法调用了。

当我写完这个工具类之后,把它发给大佬旺看,他说可以在startActivitystartActivityForResult上面加个具体化的泛型,就像这样:

    inline fun <reified TARGET : Activity> startActivity(
        starter: Activity,
        vararg params: Pair<String, Any>
    ) = starter.startActivity(Intent(starter, TARGET::class.java).putExtras(*params))

跟原来的代码区别就是把target: KClass换成了
reified关键字修饰的泛型与普通的泛型区别就是:前者可以获取到泛型的类型信息,而后者不行。
但是reified依赖内联函数,也就是说,如果使用具体化泛型的话,方法必须用inline修饰。

改完之后,就可以这样来使用了:

    startActivity<TestActivity>(this, "key0" to "value0", "key1" to "value1")

哈哈,可读性是不是更高了。

当一个方法用inline修饰了之后,表示该方法为内联函数,此时无法访问访问权限比自己低的成员,比如一个内联函数为public,那就不能访问该类用private修饰的成员。
我们接下来要修改的startActivityForResult方法就碰上这个问题了:

    private var sRequestCode = 0
        set(value) {
            field = if (value >= Integer.MAX_VALUE) 1 else value
        }
        
    inline fun <reified TARGET : Activity> startActivityForResult(
        starter: Activity,
        vararg params: Pair<String, Any>,
        crossinline callback: ((result: Intent?) -> Unit)
    ) {
        ......
        fragment.init(++sRequestCode, intent) {
            ......
        }
        ......
    }

上面这段代码AS会报错,因为fragmentinit方法需要传sRequestCode进去,而这个sRequestCodeprivate的,内联函数是public
那应该怎么做呢?难道说就没有办法可以解决吗?
大佬旺给出的解决方案是:把private改成@PublishedApi internal
这样做虽然可以正常编译运行,但是,加了@PublishedApi注解之后的sRequestCode,却可以在任何地方访问和修改!这显然违背了开闭原则。虽然说没有人会手贱去修改它,但每次出代码提示的时候,这个sRequestCode就会显示出来。。。很碍眼。

除了加PublishedApi注解之外,还有别的办法吗?
大佬旺翻了一下资料后说:在方法前加个@Suppress("NON_PUBLIC_CALL_FROM_PUBLIC_INLINE")把它抑制了就能编译通过并正常运行了。。。
试了一下果然可以。但是,为什么呢?

为什么加了Suppress之后就能正常编译和运行?
我看了一下编译成class之后反编译的代码,发现sRequestCode多了个settergetter方法!
也就是说,在编译过程中,会自动帮我们生成publicsettergetter方法!
这两个public的静态方法,在编译前是没有的,所以也就不会出代码提示了。
我们刚刚的Kotlin代码:

    ......
    fragment.init(++sRequestCode, intent)
    ......

在反编译后,看到的Java代码是这样的:

      access$setSRequestCode$p(access$getSRequestCode$p() + 1);
      fragment.init(access$getSRequestCode$p(), intent);

自动生成的settergetter如下:

   // $FF: synthetic method
   public static final int access$getSRequestCode$p() {
      return sRequestCode;
   }

   // $FF: synthetic method
   public static final void access$setSRequestCode$p(int var0) {
      sRequestCode = var0;
   }

哈哈哈,看来加Suppress的这个做法,是完全可行的。
查了一下源码,发现我们常用的synchronized方法(在Kotlin中不是关键字),也是使用了这个抑制的。
以后在项目中的其他地方有类似的需求,相信同学们已经知道要怎么做了吧~


好啦,本篇文章到此结束,有错误的地方请指出,谢谢大家!

Github地址:https://github.com/wuyr/ActivityMessenger 欢迎Star

你可能感兴趣的:(借助Kotlin特性打造一个有Kotlin味道的Activity跳转工具类库)