以回调形式使用startActivityForResult方法,并解决Activity被回收的问题

54 篇文章 2 订阅
15 篇文章 0 订阅

前言

之前写过一篇文章写一个逻辑清晰的startActivityForResult(),拒绝来回扒拉代码,写了使用回调形式使用startActivityForResult方法,配合Kotlin的语法,可以很简单的处理startActivityForResult的返回时机和返回数据.使用方式如下:

ps:由于之前名字使用startActivityForResult会导致有时导错包,所以现在名字改成了jumpForResult

看上图我们可以分析实现方式:

1.首先调用系统的startActivityForResult方法来启动目标页面

2.将方法传入的回调保存在某个地方

3.在onActivityResult方法中获取并调用回调

问题

但是这里有个问题,熟悉安卓系统的同学都知道,系统会在内存紧张的时候对除了顶层的Activity进行回收,然后在你返回的时候自动重建出来,虽然看上去好像是一个页面,但其实对象已经不是一个了,其中的变量肯定也不相同,所以前面这篇文章在回调的地方会有如下问题:

1.回调的作用域问题

2.回调在何处保存才不会丢失

3.回调如何处理才不会内存泄漏

ps:可以打开开发者选项中的不保留活动选项来进行测试

解决方案

回调的作用域问题

之前的回调类型是 (Intent?) -> Unit 编译成字节码后其实就是一个 Function1<Intent?,Unit>类型的匿名内部类,而jvm中匿名内部类的特性之一是会在构造中传入上层类的对象,所以说该回调中可以操作当前Activity中的变量(因为持有了它)

而我们要处理上述问题,就不能使用匿名内部类构造中的Activity对象(因为重建并恢复后就不是同一个对象了),正好kotlin中有很好的方法来处理这个问题,我们可以使用这个类型的回调 Activity.(Intent?) -> Unit ,相当于以Activity为Receiver的 (Intent?) -> Unit ,这种Receiver特性在kotlin基础库中被广泛应用,比如T.apply:

public inline fun <T> T.apply(block: T.() -> Unit): T {//省略其他代码
    block()
    return this
}

我们可以在lambda的作用域中直接调用其属性和函数,而不需要使用this@xxx. (java中 xxx.this.):

Activity.(Intent?)->Unit 在kotlin中的示例:

但其实都是kotlin编译器帮我们做的.

所以同理,如果我们使用 Activity.(Intent?) -> Unit 类型,就可以在使用方不知不觉中调换this receiver,这样只要我们那拿到重建后的Activity对象,就可以在无感知的情况下使用相应的对象中的变量

回调在何处保存才不会丢失

这个其实很简单,可以有多种实现:比如放在单例中,赋值给某个静态变量,或者放在静态的数据结构中,本篇文章的解决方案就选择放在静态的数据结构中

    companion object {
        @JvmStatic
        private val map = HashMap<Class<out Activity>, Activity.(Intent?) -> Unit>()
    }

回调如何处理才不会内存泄漏

由于我们将回调保存在了static中,而回调由于是匿名内部类,其中会引用其上层的Activity,所以这里要注意千万不要造成内存泄漏,不然会导致对应的Activity(包括其中的View等对象)和回调都无法被gc回收

首先我们需要设想一下A间接调用startActivityForResult后,B调用finish后所有的路径:

1.启动Fragment的时候发现A已经finish了,此时直接移除static中的回调

2.B调用了finish,A没有重建,这时我们可以在onActivityResult方法中移除回调

3.B调用了finish,且A页面和Fragment都被销毁了,这时我们可以在onSaveInstanceState中将A的Class保存起来,然后在重建后的onCreate中获取Class对象,然后通过获取的Class对象在onActivityResult方法中移除回调

这块需要结合实际代码和Fragment运行流程来说,所以下面直接看正文

正文

首先我们的起始位置还是先new出来ResultCallbackFragment对象并调用setCallbackAndIntent函数来获取实例并设置参数

如下代码:

//这里我先new出来这个中间类对象,然后调用setCallbackAndIntent方法来设置一下startActivityForResult需要用到的参数
ResultCallbackFragment<T>().setCallbackAndIntent(this, callback, intent, result_ok)

方法的定义也很简单,主要就是赋值,然后把Class和回调存入static的map中 

    //ps:这里的T为 <T : Activity>
    fun setCallbackAndIntent(t: T, callback: T.(Intent?) -> Unit, intent: Intent, result_ok: Boolean): ResultCallbackFragment<T> {
        val tClass = t::class.java
        map[tClass] = callback as Activity.(Intent?) -> Unit
        this.callback = callback
        this.clazz = tClass
        this.intent = intent
        this.result_ok = result_ok
        return this
    }

ps:这里为什么不使用arguments来传递参数呢?是因为其通过序列化传输,传入和获取到的对象就不是同一个了,具体请自行百度

pps:这里为什么不将构造私有,然后使用一个伴生对象方法来创建ResultCallbackFragment对象呢?这是因为Android框架恢复Fragment需要使用public的空参构造来完成重建,所以无法将构造私有化,这样会造成崩溃,所以使用一个伴生对象方法来创建对象也无太多意义了

然后将Fragment附加到Activity上

supportFragmentManager
        .beginTransaction()
        .add(ResultCallbackFragment<T>().setCallbackAndIntent(this, callback, intent, result_ok), ContextConstant.TAG)
        .commitAllowingStateLoss()//commit()和该方法的区别就是,这个方法不会去检查状态,而commit会检查状态(mStateSaved状态),如果状态不对则会抛异常

ResultCallbackFragment被附加到Activity上后,会执行生命周期onCreate方法

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        retainInstance = true//当设备旋转时,fragment会随托管activity一起销毁并重建。为true可保留fragment
        //如果到onCreate这一步,clazz还是为空,说明是Activity被重建了,这里可以取出保存在savedInstanceState中的clazz
        if (clazz == null) {
            clazz = savedInstanceState?.getSerializable("clazz") as? Class<Activity>
            result_ok = savedInstanceState?.getBoolean("result_ok") ?: true
        }
        if (activity?.isFinishing != false) {
            finishFragment()
            map.remove(clazz)
            return
        }
        //如果intent不为null,说明是创建完成第一次附加到Activity上,这里调用startActivityForResult来调起下一个页面
        if (intent != null)
            startActivityForResult(intent, ContextConstant.START_ACTIVITY_FOR_RESULT_REQUEST_CODE)
        intent = null
    }

我们会在第一次的onCreate中调用startActivityForResult来启动B Activity

中间会触发onSaveInstanceState回调,我们将clazz对象保存在Bundle中,方便重建后在onCreate获取(对应onCreate方法中if(clazz==null)处的判断)

    override fun onSaveInstanceState(outState: Bundle) {
        outState.putSerializable("clazz", clazz)
        outState.putBoolean("result_ok", result_ok)
        super.onSaveInstanceState(outState)
    }

最后B调用finish后会触发Fragment的onActivityResult方法(不管B是否调用了setResult方法,只要是通过startActivityForResult开启的,A就会响应onActivityResult)

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        finishFragment()
        val clazz = clazz ?: throw NullPointerException()
        //移除static中的回调,保证有回调时不会内存泄漏
        val mCallback = map.remove(clazz) ?: callback ?: return
        val t = AppManager.getActivity(clazz) as? T ?: return
        //在这里处理RESULT_OK的判断
        if (resultCode == Activity.RESULT_OK && requestCode == ContextConstant.START_ACTIVITY_FOR_RESULT_REQUEST_CODE)
            mCallback(t, data)
        else if (!result_ok && requestCode == ContextConstant.START_ACTIVITY_FOR_RESULT_REQUEST_CODE)
            mCallback(t, data)
    }

ps:其中的AppManager是一个管理Activity的工具类,可以在BaseActivity的onCreate中添加进去,在onDestroy时移除,getActivity方法就是通过class去遍历获取 

这样整个流程就完成了,然后封装一下入口:

/**
 * 使用callback的方式来执行startActivityForResult方法,就不用来回查找代码了,提高了可读性
 * 更安全的回调(即使下面的activity被回收了也能使重建的activity拿到数据)
 * 注意事项: 如果当前Activity重写了onActivityResult,需要调用super方法
 *          同一个class的activity不能在未回调时再次调用,否则会有回调冲突
 *          无法修改重建后的上层函数中的局部变量
 *
 * @param intent 跳转的intent
 * @param result_ok 是否去判断result_ok,如果是false,就不判断
 * @param callback 成功的回调
 */
fun <T : FragmentActivity> T.jumpForResult(intent: Intent, result_ok: Boolean = true, callback: T.(Intent?) -> Unit) = supportFragmentManager
        .beginTransaction()
        .add(ResultCallbackFragment<T>().setCallbackAndIntent(this, callback, intent, result_ok), ContextConstant.TAG)
        .commitAllowingStateLoss()

/**
 * [T]是自身的泛型,[A]是跳转到的页面
 */
inline fun <T : FragmentActivity, reified A : Activity> T.jumpForResult(initIntent: (intent: Intent) -> Unit = {}, result_ok: Boolean = true, noinline callback: T.(Intent?) -> Unit) =
        jumpForResult(Intent(this, A::class.java).apply(initIntent), result_ok, callback)

两种使用方式:

第一种表示自身是MainActivity,需要打开WebViewActivity,在响应了onActivityResult后,就调用回调.可以看到我们在回调中并不关心是哪一个MainActivity对象,直接使用其中的属性或方法即可

第二种表示自身是MainActivity(因为只有一个泛型并且能自动推断出来,所以不需要显式声明),其打开一个隐式意图,如果回调后,就获取Intent的data字段,其实第一种还是调用的第二种,只不过中间的Intent对象给自动创建出来了

完整源码如下:

import android.app.Activity
import android.content.Intent
import android.os.Bundle
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import java.util.*

class ResultCallbackFragment<T : Activity> : Fragment() {
    companion object {
        /**
         * 静态变量暂存回调,防止页面被回收时回调也被回收掉
         */
        @JvmStatic
        private val map = HashMap<Class<out Activity>, Activity.(Intent?) -> Unit>()
    }

    var intent: Intent? = null
    var result_ok = true
    var clazz: Class<out Activity>? = null
    var callback: (T.(Intent?) -> Unit)? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        retainInstance = true//当设备旋转时,fragment会随托管activity一起销毁并重建。为true可保留fragment
        //如果到onCreate这一步,clazz还是为空,说明是Activity被重建了,这里可以取出保存在savedInstanceState中的clazz
        if (clazz == null) {
            clazz = savedInstanceState?.getSerializable("clazz") as? Class<Activity>
            result_ok = savedInstanceState?.getBoolean("result_ok") ?: true
        }
        if (activity?.isFinishing != false) {
            finishFragment()
            map.remove(clazz)
            return
        }
        //如果intent不为null,说明是创建完成第一次附加到Activity上,这里调用startActivityForResult来调起下一个页面
        if (intent != null)
            startActivityForResult(intent, ContextConstant.START_ACTIVITY_FOR_RESULT_REQUEST_CODE)
        intent = null
    }

    private fun finishFragment() {
        activity?.supportFragmentManager
                ?.beginTransaction()
                ?.remove(this)
                ?.commitAllowingStateLoss()
    }

    override fun onSaveInstanceState(outState: Bundle) {
        outState.putSerializable("clazz", clazz)
        outState.putBoolean("result_ok", result_ok)
        super.onSaveInstanceState(outState)
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        finishFragment()
        val clazz = clazz ?: throw NullPointerException()
        //移除static中的回调,保证有回调时不会内存泄漏
        val mCallback = map.remove(clazz) ?: callback ?: return
        val t = AppManager.getActivity(clazz) as? T ?: return
        if (resultCode == Activity.RESULT_OK && requestCode == ContextConstant.START_ACTIVITY_FOR_RESULT_REQUEST_CODE)
            mCallback(t, data)
        else if (!result_ok && requestCode == ContextConstant.START_ACTIVITY_FOR_RESULT_REQUEST_CODE)
            mCallback(t, data)
    }

    fun setCallbackAndIntent(t: T, callback: T.(Intent?) -> Unit, intent: Intent, result_ok: Boolean): ResultCallbackFragment<T> {
        val tClass = t::class.java
        map[tClass] = callback as Activity.(Intent?) -> Unit
        this.callback = callback
        this.clazz = tClass
        this.intent = intent
        this.result_ok = result_ok
        return this
    }
}

/**
 * 使用callback的方式来执行startActivityForResult方法,就不用来回查找代码了,提高了可读性
 * 更安全的回调(即使下面的activity被回收了也能使重建的activity拿到数据)
 * 注意事项: 如果当前Activity重写了onActivityResult,需要调用super方法
 *          同一个class的activity不能在未回调时再次调用,否则会有回调冲突
 *          无法修改重建后的上层函数中的局部变量
 *
 * @param intent 跳转的intent
 * @param result_ok 是否去判断result_ok,如果是false,就不判断
 * @param callback 成功的回调
 */
fun <T : FragmentActivity> T.jumpForResult(intent: Intent, result_ok: Boolean = true, callback: T.(Intent?) -> Unit) = supportFragmentManager
    .beginTransaction()
    .add(ResultCallbackFragment<T>().setCallbackAndIntent(this, callback, intent, result_ok), ContextConstant.TAG)
    .commitAllowingStateLoss()

/**
 * [T]是自身的泛型,[A]是跳转到的页面
 */
inline fun <T : FragmentActivity, reified A : Activity> T.jumpForResult(initIntent: (intent: Intent) -> Unit = {}, result_ok: Boolean = true, noinline callback: T.(Intent?) -> Unit) =
    jumpForResult(Intent(this, A::class.java).apply(initIntent), result_ok, callback)

//简化版AppManager
object AppManager {
    private val activityStack: ArrayDeque<Activity> = ArrayDeque()

    /**
     * 加入队列
     */
    fun addActivity(activity: Activity) = activityStack.add(activity)

    /**
     * 获取指定class的
     */
    fun <T : Activity> getActivity(cls: Class<out T>): T? {
        for (value in activityStack) {
            if (value.javaClass == cls) {
                return value as? T
            }
        }
        return null
    }

    fun removeActivity(activity: Activity?): AppManager {
        if (activity != null) {
            activityStack.remove(activity)
        }
        return this
    }
}

object ContextConstant {
    const val START_ACTIVITY_FOR_RESULT_REQUEST_CODE = 965//startActivityForResult方法所使用到的requestCode
    const val TAG = "ResultCallbackFragment"//startActivityForResult方法所使用到的查找fragment用的tag
}

可以直接运行测试的demo:GitHub - ltttttttttttt/JumpForResultSample: 以回调形式使用startActivityForResult方法,并解决Activity被回收的问题,demo

结语

该问题解决的主要思路就是通过保存Class对象在Bundle中,然后保存回调在static中,通过替换回调的Receiver(this)来实现无感的回调

而其实这个方式也是可以支持Fragment打开Activity的,但是由于篇幅原因和加入后逻辑相较乱的问题,所以只实现了Activity打开Activity的代码,感兴趣的读者可以自行实现,我这里可以提供一下相关查找Fragment的方法

/**
 * 在FragmentActivity或FragmentManager中遍历查找Fragment,深度优先
 */
fun <T : Fragment> FragmentActivity.getFragment(clazz: Class<T>): T? =
        supportFragmentManager.getFragment(clazz)

fun <T : Fragment> FragmentManager.getFragment(clazz: Class<T>): T? {
    for (f in fragments) {
        f ?: continue
        if (f::class.java == clazz)
            return f as T
        val fragment = f.childFragmentManager.getFragment(clazz)
        if (fragment != null)
            return fragment
    }
    return null
}

inline fun <reified T : Fragment> FragmentActivity.getFragment(): T? = getFragment(T::class.java)

而且由于回调是使用Class当做key的,所以一个Activity无法同一时间打开两个Activity,但是也可以通过其他方式绕过去(尽管我认为是伪需求)

经bennyhuo大佬指点:由于回调的时候可能Receiver(this)已经被替换掉了,所以函数内的局部变量无法修改,只能读取(而且读取的也是之前的Activity的函数的内容),所以使用该方式不要在回调用读取或修改函数中的局部变量,比如下面这样就是一个无效操作(防止有人这样写遇见bug)

end

对Kotlin或KMP感兴趣的同学可以进Q群 101786950

如果这篇文章对您有帮助的话

可以扫码请我喝瓶饮料或咖啡(如果对什么比较感兴趣可以在备注里写出来)

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值