相信同学们都有过这种感受:在日常开发中,每次使用startActivityForResult
时,要做的事情都好多,好麻烦:
定义一个requestCode
;
重写onActivityResult
方法并在里面去判断requestCode
和resultCode
;
如果有携带参数,还要一个个通过putExtra
方法put
进Intent里;
目标Activity处理完成后还要把数据一个个put
进Intent中,setResult
然后finish
;
如果参数是可序列化的泛型类对象(如ArrayList
当然了,在Github上已经有好几个开源库把 “需要重写onActivityResult
方法来接收处理结果” 的问题解决了(其中的原理相信很多同学都已经了解过了,这个我们等下也会详细讲解的)。
但如果有携带参数的话,依然很麻烦:有多少个参数,就要调用多少次putExtra
方法。
而且最烦的是第5点,转成泛型类对象时,一块黄色的警告在那里,看着挺难受。
不过还好,随着Kotlin越来越普及,越来越多的开发者都体验到了它的魅力,也许我们可以借助它的一些特性,来做一些事情。。。
这个思路我是直接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"
)
Key
和Value
之间的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
}
因为Intent的putExtra
方法只接受String类型的Key,所以我们也直接指定的Pair的第一个值的类型为String了。
总体的逻辑很简单,就像mapOf
方法那样:遍历Pair数组,判断每一个参数值的类型(不同于Java的是,在Kotlin中用is关键字检查类型符合之后,会自动转换成对应的类型,无须显式转换),并通过Intent的putExtra
方法把参数put进去。
那么现在给Intent设置多个参数的时候,就可以写成这样:
val intent = Intent()
intent.putExtras(
"key0" to true,
"key1" to 1.23F,
"key2" to listOf("1", "2")
)
哈哈,的确方便又好看了好多。
虽然说,在没有工具类的帮助下,startActivity
一样也可以一句代码搞定,如果不携带参数的话。
但如果要携带参数,就至少要三句了:
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
最后可以看到,也是直接调用了Activity的startActivity
方法,并把调用putExtras
扩展方法后的Intent传进去。
就这么简单。
相信很多同学在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
,intent
和callback
。
那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
执行完成之后,还会把对应的Fragment从FragmentManager中移除掉,就是为了能让这个Fragment
的onDetach
方法尽快回调(把callback
的引用置空),一定程度上避免内存泄漏(因为外部的callback
可能持有生命周期比较短的对象引用)。
在最后,会把Fragment附加到Activity中,当Fragment的onAttach
回调时,就会启动目标Activity了。
当startActivityForResult
方法启动的Activity处理任务完成之后,通常要把一些数据传回给启动者,这个操作我们也是可以优化的:
fun finish(src: Activity, vararg params: Pair<String, Any>) = with(src) {
setResult(Activity.RESULT_OK, Intent().putExtras(*params))
finish()
}
可以看到,把参数put
进Intent之后,就调用了setResult
方法把Intent放进去,并finish
。
那么在使用的时候,就可以这样写了:
finish(this, "key0" to "value0", "key1" to "value1")
哈哈,是不是简洁了许多。
还记不记得findViewById
?
哈哈哈哈,在compileSdkVersion
为26之前,通过findViewById
取得的View子类实例时都要进行强制类型转换,26之后,才换成了泛型方法。
现在的getSerializableExtra
也存在同样的问题,如果取出来的是一个泛型类的实例,比如List
,强制转换后还会出一个 UNCHECKED_CAST 警告,黄色的一块,看着很难受。
那么现在我们也可以参照findViewById
,给Intent创建一个扩展方法:
想一下:Intent的getStringExtra
、getIntExtra
、getBooleanExtra
等一系列方法,内部都是借助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这个类的,所以等下在获取mMap
的Field之前要做一下版本判断。
还有,当我直接用反射拿到mMap
的对象引用之后,却发现是null
的,再次翻源码后才注意到:Bundle里面的每一个getXXX
方法中,第一句都是会调用unparcel
方法的,这个方法里面会调用一个叫initializeFromParcelLocked
的方法,没错,mMap
正是在这个方法里面初始化的,然后通过Parcel的readArrayMapInternal
方法来填充键值对 。所以等下在获取mMap
实例之前,还要先调用一下unparcel
方法(也是通过反射)。
那现在要用到的内部属性和方法一共有3个了(mExtras
、mMap
、unparcel
),我们可以把它缓存起来,这样就不用每次都重新获取,可以提升运行效率:
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的,可放心使用。
刚刚说到把那些Field和Method缓存起来,可以提高效率,那究竟比没有缓存的情况下提高了多少呢?
我分别对以下操作进行了耗时统计:
直接调用Intent的getXXExtra
来获取值;
重用Field和Method,每次先通过反射获取mMap
,然后调用mMap.get
方法来获取值;
不重用,每次都先通过Class取得对应的Field和Method,然后通过反射获取mMap
,再通过mMap.get
方法来获取值;
以下是分别循环10W次的总耗时(单位: ms):
第一次:
15
182
1242
第二次:
8
100
1145
第三次:
8
96
1143
第四次:
8
104
1206
第五次:
10
115
1365
可以看到,重用Field和Method的话,效率会提升10倍左右,虽然远没有不使用反射的效率高,但是,重复10W次才100毫秒左右,也就是平均每次才花费0.001ms左右,牺牲这点微不足道的效率给我们带来方便,是非常划算的。
我们在学习Kotlin的过程中,都会了解到内联函数,适当地使用可以提高性能,节省资源。如果一个方法它被标记成了内联函数,那么在编译时,会将它的方法体,替换到调用它的地方,这时候就不算是正常的方法调用了。
当我写完这个工具类之后,把它发给大佬旺看,他说可以在startActivity
和startActivityForResult
上面加个具体化的泛型,就像这样:
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会报错,因为fragment
的init
方法需要传sRequestCode
进去,而这个sRequestCode
是private的,内联函数是public。
那应该怎么做呢?难道说就没有办法可以解决吗?
大佬旺给出的解决方案是:把private
改成@PublishedApi internal
。
这样做虽然可以正常编译运行,但是,加了@PublishedApi
注解之后的sRequestCode
,却可以在任何地方访问和修改!这显然违背了开闭原则。虽然说没有人会手贱去修改它,但每次出代码提示的时候,这个sRequestCode
就会显示出来。。。很碍眼。
除了加PublishedApi注解之外,还有别的办法吗?
大佬旺翻了一下资料后说:在方法前加个@Suppress("NON_PUBLIC_CALL_FROM_PUBLIC_INLINE")
把它抑制了就能编译通过并正常运行了。。。
试了一下果然可以。但是,为什么呢?
为什么加了Suppress之后就能正常编译和运行?
我看了一下编译成class之后反编译的代码,发现sRequestCode
多了个setter和getter方法!
也就是说,在编译过程中,会自动帮我们生成public的setter和getter方法!
这两个public的静态方法,在编译前是没有的,所以也就不会出代码提示了。
我们刚刚的Kotlin代码:
......
fragment.init(++sRequestCode, intent)
......
在反编译后,看到的Java代码是这样的:
access$setSRequestCode$p(access$getSRequestCode$p() + 1);
fragment.init(access$getSRequestCode$p(), intent);
自动生成的setter和getter如下:
// $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中不是关键字),也是使用了这个抑制的。
以后在项目中的其他地方有类似的需求,相信同学们已经知道要怎么做了吧~