kotlin 技巧-写Intent的扩展

记一次写Intent的扩展

前言

写项目的时候经常会写到跳转Activiey,写到跳转Activit就一定会写到putExtra,就像下面那样

1
2
3
4

intent.putExtra("a", item.a)
intent.putExtra("b", item.b)
intent.putExtra("c", item.c)

挺多哈,每次都要重复前面相同的内容,所以就想着能不能写个扩展来简写。

开始

我们最终实现是这样的:

1
2
3
4
5
6
7

Intent(context, TestActivity::class.java)
.putExtraVararg(
"a" to item.a,
"b" to item.b,
"c" to item.c
)

仿mapOf

经常用到mapOf()或者mutableMapOf()就会知道,里面是用了Pair类来生成Map的,所以受此启发,打算用Pair来作为参数实现,那么写的时候就应该是这样写,参数为可变参数类型:

1
2
3
4
5
6
7

// 作为Intent的扩展
fun Intent.putExtraVararg(
vararg extras: Pair<String, Any?>
): Intent {
// 先省略
}

putExtra的第一个参数类型肯定是String类型,第二个参数的类型包括Bundle, Boolean, BooleanArray, Byte, ByteArray, Char, CharArray, String等等,所以就用Any类型,因为是可空的,所以加上?

那么调用的时候,就是这样子的

1
2
3
4
5
6
7

Intent(context, TestActivity::class.java)
.putExtraVararg(
"a" to item.a,
"b" to item.b,
"c" to item.c
)

是不是就是一开始说的那样,对比平常写的是不是简单了很多。

参数类型匹配

写好了参数,那么就要进行对参数类型的匹配进行对应的putExtra

vararg参数可以用forEach来循环对每个参数类型进行匹配:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

fun Intent.putExtraVararg(
vararg extras: Pair<String, Any?>
): Intent {
if (extras.isEmpty()) return this
extras.forEach { (key, value) ->
value ?: let {
it.putExtra(key, it.toString())
return@forEach
}
when (value) {
is Bundle -> this.putExtra(key, value)
is Boolean -> this.putExtra(key, value)
is BooleanArray -> this.putExtra(key, value)
is Byte -> this.putExtra(key, value)
is ByteArray -> this.putExtra(key, value)
is Char -> this.putExtra(key, value)
is CharArray -> this.putExtra(key, value)
is String -> this.putExtra(key, value)
is CharSequence -> this.putExtra(key, value)
is Double -> this.putExtra(key, value)
is DoubleArray -> this.putExtra(key, value)
is Float -> this.putExtra(key, value)
is FloatArray -> this.putExtra(key, value)
is Int -> this.putExtra(key, value)
is IntArray -> this.putExtra(key, value)
is Long -> this.putExtra(key, value)
is LongArray -> this.putExtra(key, value)
is Short -> this.putExtra(key, value)
is ShortArray -> this.putExtra(key, value)
is Parcelable -> this.putExtra(key, value)
is Serializable -> this.putExtra(key, value)
else -> {
throw IllegalArgumentException("Not support $value type ${value.javaClass}..")
}
}
}
return this
}

发现没有,还少了ArrayList<String>ArrayList<CharSequence>ArrayList<? extends Parcelable>这三个List的参数类型匹配,因为不能直接is来匹配对应的Array类型,在群里问过之后,才得出最终的方法,Array里面有一个匹配对应类型的扩展函数isArrayOf()

1
2
3
4
5
6
7

/**
* Checks if array can contain element of type [T].
*/
@Suppress("REIFIED_TYPE_PARAMETER_NO_INLINE")
public fun <reified T : Any> Array<*>.isArrayOf(): Boolean =
T::class.java.isAssignableFrom(this::class.java.componentType)

so,这样就简单了,我们先匹配Array,然后在匹配对应的ArrayList<String>ArrayList<CharSequence>ArrayList<? extends Parcelable>,最后在对应put方法那里用as

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

is Array<*> -> {
@Suppress("UNCHECKED_CAST")
when {
value.isArrayOf<String>() -> {
this.putStringArrayListExtra(key, value as ArrayList<String?>)
}
value.isArrayOf<CharSequence>() -> {
this.putCharSequenceArrayListExtra(key, value as ArrayList<CharSequence?>)
}
value.isArrayOf<Parcelable>() -> {
this.putParcelableArrayListExtra(key, value as ArrayList<out Parcelable?>)
}
}
}

这样就实现了不同Array类型的putExtra

最终

最后完整的代码为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59

fun Intent.putExtraVararg(
vararg extras: Pair<String, Any?>
): Intent {
if (extras.isEmpty()) return this
extras.forEach {
val key = it.first
val value = it.second ?: let {
it.putExtra(key, it.toString())
}
}
extras.forEach { (key, value) ->
value ?: let {
it.putExtra(key, it.toString())
return@forEach
}
when (value) {
is Bundle -> this.putExtra(key, value)
is Boolean -> this.putExtra(key, value)
is BooleanArray -> this.putExtra(key, value)
is Byte -> this.putExtra(key, value)
is ByteArray -> this.putExtra(key, value)
is Char -> this.putExtra(key, value)
is CharArray -> this.putExtra(key, value)
is String -> this.putExtra(key, value)
is CharSequence -> this.putExtra(key, value)
is Double -> this.putExtra(key, value)
is DoubleArray -> this.putExtra(key, value)
is Float -> this.putExtra(key, value)
is FloatArray -> this.putExtra(key, value)
is Int -> this.putExtra(key, value)
is IntArray -> this.putExtra(key, value)
is Long -> this.putExtra(key, value)
is LongArray -> this.putExtra(key, value)
is Short -> this.putExtra(key, value)
is ShortArray -> this.putExtra(key, value)
is Array<*> -> {
@Suppress("UNCHECKED_CAST")
when {
value.isArrayOf<String>() -> {
this.putStringArrayListExtra(key, value as ArrayList<String?>)
}
value.isArrayOf<CharSequence>() -> {
this.putCharSequenceArrayListExtra(key, value as ArrayList<CharSequence?>)
}
value.isArrayOf<Parcelable>() -> {
this.putParcelableArrayListExtra(key, value as ArrayList<out Parcelable?>)
}
}
}
is Parcelable -> this.putExtra(key, value)
is Serializable -> this.putExtra(key, value)
else -> {
throw IllegalArgumentException("Not support $value type ${value.javaClass}..")
}
}
}
return this
}

转成Java是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100

public final class IntentExtKt {
@NotNull
public static final Intent putExtraVararg(@NotNull Intent $receiver, @NotNull Pair... extras) {
Intrinsics.checkParameterIsNotNull($receiver, "receiver$0");
Intrinsics.checkParameterIsNotNull(extras, "extras");
if (extras.length == 0) {
return $receiver;
} else {
Pair[] var3 = extras;
int var4 = extras.length;

int var5;
Pair element$iv;
boolean var8;
String key;
for(var5 = 0; var5 < var4; ++var5) {
element$iv = var3[var5];
var8 = false;
key = (String)element$iv.getFirst();
if (element$iv.getSecond() == null) {
int var12 = false;
$receiver.putExtra(key, $receiver.toString());
}
}

var3 = extras;
var4 = extras.length;

for(var5 = 0; var5 < var4; ++var5) {
element$iv = var3[var5];
var8 = false;
key = (String)element$iv.component1();
Object value = element$iv.component2();
if (value != null) {
if (value instanceof Bundle) {
$receiver.putExtra(key, (Bundle)value);
} else if (value instanceof Boolean) {
$receiver.putExtra(key, (Boolean)value);
} else if (value instanceof boolean[]) {
$receiver.putExtra(key, (boolean[])value);
} else if (value instanceof Byte) {
$receiver.putExtra(key, ((Number)value).byteValue());
} else if (value instanceof byte[]) {
$receiver.putExtra(key, (byte[])value);
} else if (value instanceof Character) {
$receiver.putExtra(key, (Character)value);
} else if (value instanceof char[]) {
$receiver.putExtra(key, (char[])value);
} else if (value instanceof String) {
$receiver.putExtra(key, (String)value);
} else if (value instanceof CharSequence) {
$receiver.putExtra(key, (CharSequence)value);
} else if (value instanceof Double) {
$receiver.putExtra(key, ((Number)value).doubleValue());
} else if (value instanceof double[]) {
$receiver.putExtra(key, (double[])value);
} else if (value instanceof Float) {
$receiver.putExtra(key, ((Number)value).floatValue());
} else if (value instanceof float[]) {
$receiver.putExtra(key, (float[])value);
} else if (value instanceof Integer) {
$receiver.putExtra(key, ((Number)value).intValue());
} else if (value instanceof int[]) {
$receiver.putExtra(key, (int[])value);
} else if (value instanceof Long) {
$receiver.putExtra(key, ((Number)value).longValue());
} else if (value instanceof long[]) {
$receiver.putExtra(key, (long[])value);
} else if (value instanceof Short) {
$receiver.putExtra(key, ((Number)value).shortValue());
} else if (value instanceof short[]) {
$receiver.putExtra(key, (short[])value);
} else if (value instanceof Object[]) {
if ((Object[])value instanceof String[]) {
$receiver.putStringArrayListExtra(key, (ArrayList)value);
} else if ((Object[])value instanceof CharSequence[]) {
$receiver.putCharSequenceArrayListExtra(key, (ArrayList)value);
} else if ((Object[])value instanceof Parcelable[]) {
$receiver.putParcelableArrayListExtra(key, (ArrayList)value);
}
} else if (value instanceof Parcelable) {
$receiver.putExtra(key, (Parcelable)value);
} else {
if (!(value instanceof Serializable)) {
throw (Throwable)(new IllegalArgumentException("Not support " + value + " type " + value.getClass() + ".."));
}

$receiver.putExtra(key, (Serializable)value);
}
} else {
int var13 = false;
$receiver.putExtra(key, $receiver.toString());
}
}

return $receiver;
}
}
}

顺手写别的扩展

既然用到Intent的扩展,那么就顺手写下Activity的startActivity的扩展

1
2
3
4
5
6
7
8
9
10
11
12
13

/**
* 同Context的startActivity
*/
fun Context.toActivity(packageContext: Context?, cls: Class<*>, vararg extras: Pair<String, Any?>) {
startActivity(Intent(packageContext, cls).putExtraVararg(*extras))
}
/**
* 同Context的startActivity
*/
fun Context.toActivity(intent: Intent) {
startActivity(intent)
}

调用的时候就是这样子的

1
2
3
4
5
6
7
8
9
10

context.toActivity(
context,
SearchActivity::class.java,
SearchActivity.SEARCH_TYPE to item.type,
SearchActivity.SEARCH_UID to item.uid,
SearchActivity.SEARCH_NAME to item.name,
SearchActivity.SEARCH_KEY to item.key,
SearchActivity.SEARCH_GROUP to item.group
)

总结

通过这次的扩展,也学到了关于Array的一些扩展函数,可以说是很美好了。


借助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的时候,通常会这样写:

1
2
3
4
5
6
val map = mapOf(
"key0" to "value0",
"key1" to "value1",
"key2" to "value2"
)

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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设置多个参数的时候,就可以写成这样:

1
2
3
4
5
6
7
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,来把它简化成一行,就像这样:

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

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

1
2
3
4
5
6
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后面的<out Activity>,在Java中等同于<T extends Activity>
params前面的vararg,就是定义成可变参数的意思,等同于Java中的Pair<String, Any>... params
最后可以看到,也是直接调用了ActivitystartActivity方法,并把调用putExtras扩展方法后的Intent传进去。

就这么简单。

简化startActivityForResult操作

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

1
2
3
4
5
6
7
8
9
10
11
12
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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了,看代码:

1
2
3
4
5
    private var sRequestCode = 0
set(value) {
field = if (value >= Integer.MAX_VALUE) 1 else value
}
1234

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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处理任务完成之后,通常要把一些数据传回给启动者,这个操作我们也是可以优化的:

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

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

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

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

优化Intent.getExtra操作

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

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

1
2
3
4
5
6
7
8
9
10
11
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正是在这个方法里面初始化的,然后通过*Parcel*的readArrayMapInternal方法来填充键值对 。所以等下在获取mMap实例之前,还要先调用一下unparcel方法(也是通过反射)。
那现在要用到的内部属性和方法一共有3个了(mExtrasmMapunparcel),我们可以把它缓存起来,这样就不用每次都重新获取,可以提升运行效率:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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()
}
}
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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参数时就可以写成这样了:

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

或者这样:

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

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

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
第一次:
15
182
1242
第二次:
8
100
1145
第三次:
8
96
1143
第四次:
8
104
1206
第五次:
10
115
1365

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

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

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

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

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

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

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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代码:

1
2
3
4
......
fragment.init(++sRequestCode, intent)
......

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

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

自动生成的settergetter如下:

1
2
3
4
5
6
7
8
9
10
// $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


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!