前言

之前写过一篇文章写一个逻辑清晰的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 {@JvmStaticprivate 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.javamap[tClass] = callback as Activity.(Intent?) -> Unitthis.callback = callbackthis.clazz = tClassthis.intent = intentthis.result_ok = result_okreturn 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中的clazzif (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 ?: returnval 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 {/*** 静态变量暂存回调,防止页面被回收时回调也被回收掉*/@JvmStaticprivate val map = HashMap<Class<out Activity>, Activity.(Intent?) -> Unit>()}var intent: Intent? = nullvar result_ok = truevar clazz: Class<out Activity>? = nullvar callback: (T.(Intent?) -> Unit)? = nulloverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)retainInstance = true//当设备旋转时,fragment会随托管activity一起销毁并重建。为true可保留fragment//如果到onCreate这一步,clazz还是为空,说明是Activity被重建了,这里可以取出保存在savedInstanceState中的clazzif (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 ?: returnval t = AppManager.getActivity(clazz) as? T ?: returnif (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.javamap[tClass] = callback as Activity.(Intent?) -> Unitthis.callback = callbackthis.clazz = tClassthis.intent = intentthis.result_ok = result_okreturn 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方法所使用到的requestCodeconst val TAG = "ResultCallbackFragment"//startActivityForResult方法所使用到的查找fragment用的tag
}

可以直接运行测试的demo:https://github.com/ltttttttttttt/JumpForResultDemo

结语

该问题解决的主要思路就是通过保存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 ?: continueif (f::class.java == clazz)return f as Tval 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

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

  1. Android:通过startActivityForResult方法来得到Activity的回传值

    在一些情况下,我们通过 A activity跳转到 B activity上,这时希望 A activtiy能从 B activity上得到一些返回值,这个时候我们就不能使用startActivity方 ...

  2. asp.net定时执行任务-解决应用池回收问题----转载

    在复杂的业务应用程序中,有时候会要求一个或者多个任务在一定的时间或者一定的时间间隔内计划进行,比如定时备份或同步数据库,定时发送电子邮件,定期处理用户状态信息,支付系统中定期同步异常账单等等,我们称之 ...

  3. shiro和Spring整合使用注解时没有执行realm的doGetAuthorizationInfo回调方法的解决

    shiro和Spring整合使用注解时没有执行realm的doGetAuthorizationInfo回调方法的解决 from :http://blog.csdn.net/babys/article/ ...

  4. layui表单提交使用form.on(‘submit(sub)‘,function (){}) 使用ajax请求时回调不执行的原因及解决方法

    layui表单提交使用form.on('submit(sub)',function (){}) 使用ajax请求时回调不执行的原因及解决方法 参考文章: (1)layui表单提交使用form.on(' ...

  5. vue 执行函数this_在vue中使用回调函数,this调用无效的解决

    let self = this //使用新变量替换this,以免this无效 //updatestudentinfotoserver是一个将本身部分数据异步上传的接口,接收三个参数,其中第一个是数据, ...

  6. [学习笔记] PHP回调函数的实现方法 [转]

    目录 前言 全局函数的回调 静态函数的回调 对象方法的回调 php事件模型(观察者模式)的实现思路      前言 最近在开发一个PHP系统,为了提高系统的扩展性,我想在系统中加入类似Javascri ...

  7. iPhone开发之第三方回调函数的使用方法

    回调函数在程序世界里随处可见,iPhone中也不例外,但在iPhone中经常会遇到用常规方法无法回调,上一篇文章可以解决此问题,今天再上一种方法,专门的第三方回调函数. 1.在需要回调的类中定义回调: ...

  8. [论文阅读笔记26]MRC4NER:使用阅读理解方法来解决NER任务

    题目 A Unified MRC Framework for Named Entity Recognition 命名实体识别的统一MRC框架 论文URL:https://www.semanticsch ...

  9. startActivityForResult方法过时

    使用代码调用系统相机进行拍摄照片发现调用startActivityForResult方法过时 查看竟然被标志为Deprecated 于是带着好奇心去研究一番,既然这个过时了,那要怎么使用原本的star ...

最新文章

  1. 独家发布 | 产品经理生存现状
  2. Redirecting to /bin/systemctl restart sshd.service
  3. python rfind函数用法_Python语法速查:字符串格式简单处理、子串查找与判断方法?...
  4. FactoryBean 源码
  5. android代码获取应用名称,Android获取应用程序名称(ApplicationName)
  6. mysql字段中有逗号隔开_在MySQL字段中使用逗号分隔符
  7. 可以设置选项背景颜色的DropDownList
  8. 掌握Spark机器学习库-06-基础统计部分
  9. JAVA 使用Dom4j 解析XML
  10. UVA - 10976 分数拆分
  11. 【转载】RESTful 架构风格概述
  12. 【肌电信号】基于matlab GUI脉搏信号处理系统【含Matlab源码 1062期】
  13. max30102c语言,max30102问题
  14. linux化学公式软件下载,化学公式编辑器下载-Efofex FX Chem(化学公式编辑器)下载 v3.004.0 官方特别版-IT猫扑网...
  15. php excel 高度,PHPExcel,自动调整行高
  16. nginx gzip
  17. 系统集成项目管理工程师备考资料(口袋应试第二版)14
  18. 腾讯企业邮箱管理权限可实现什么?
  19. 【网络运维与安全岗位】月薪2.5w,您还不知道的前景!
  20. UEdit百度富文本编辑器

热门文章

  1. ubantu使用apt安装时出现: xxx is not found 的解决方法
  2. 机器学习笔记:牛顿方法
  3. 1 文巾解题 191. 位1的个数
  4. 360手机麦克风测试软件,【奇酷小技巧】教你无需ROOT增大话筒、听筒和外放声音!...
  5. 进度条模块tqdm介绍
  6. 语言检测工具-langid
  7. LeetCode-剑指 Offer 12. 矩阵中的路径
  8. 微服务实践分享(2)api网关
  9. 短信验证码、图形验证码、邮件验证的自动化测试
  10. 解读dbcp自动重连那些事---转载