【android activity重难点突破】这些知识还不会,面试八成被劝退
highlight: a11y-dark
Activity
作为android
四大组件之一,地位就不用多说了吧,该组件看起来是比较简单的,但是也涉及到很多知识点,要想全部理解并在合适的业务场景下使用,也是需要一定的技术沉淀,本文主要是对activity
一些重要知识点进行总结整理,可能平时不一定用到,但是一定要有所了解。
当然这些知识点并没有设计过多源码部分,比如activity
的启动流程什么的,主要是零散的知识点,对于activity
的启动流程网上文章太多了,后面自己也准备重新梳理下,好记性不如烂笔头,在不断学习整理的过程中,一定会因为某个知识点而豁然开朗。
1.生命周期
①.两个页面跳转
从MainActivity
跳转到SecordActivity
的生命周期,重点关注Main
的onPause
和onStop
与Secord
几个关键生命周期的顺序,以及从Secord
返回时与Main
的生命周期的交叉:
可以发现Main
页面的onPause
生命周期之后直接执行Secord
的onCreate,onStart,onResume
,所以onPause
生命周期内不要执行耗时操作,以免影响新页面的展示,造成卡顿感。
②.弹出Dialog
- 单纯的弹出
Dialog
是不会影响Activity
的生命周期的; - 启动
dialog theme
的Activity
的时候,启动的activity
只会执行onPause
方法,onStop
不会执行,被启动的activity
会正常走生命周期,back
的时候,启动的Activity
会对应执行onResume
方法;
③.横竖屏切换
- AndroidManifest不配置
configChanges
时,横竖屏切换,会销毁重建Activity,生命周期会重新走一遍; - 当Activity
configChanges="orientation|screenSize"
时,横竖屏切换不会重新走Activity生命周期方法,只会执行onConfigurationChanged
方法,如需要可以在此方法中进行相应业务处理;
如横竖屏切换时需要对布局进行适配,可在res下新建
layout-port
、layout-land
目录,并提供相同的xml布局文件,横竖屏切换时即可自动加载相应布局。(前提是未配置configChanges
忽略横竖屏影响,否则不会重新加载布局)
④.启动模式对生命周期的影响
1.A(singleTask)启动(startActivity)B(standard),再从B启动A,生命周期如下:
A启动B:A_onPause、B_onCreate、B_onStart、B_onResume、A_onStop
第二步:B_onPause、A_onNewIntent、A_onRestart、A_onStart、A_onResume、B_onStop、B_onDestory
2.A(singleTask)启动A,或者A(singleTop)启动A
A_onPause、A_onNewIntent、A_Resume
3.singleInstance模式的activity
多次启动A(singleInstance
),只有第一次会创建一个单独的任务栈(全局唯一),再次启动会调用A_onPause、A_onNewIntent、A_Resume
。
2.启动模式
Activity的启动模式一直是standard
、singleTop
、singleTask
、singleInstance
四种,Android 12新增了singleInstancePerTask
启动模式,在这里不一一介绍,仅介绍重要知识点。
①.singleTask
1.Activity
是一个可以跨进程、跨应用的组件,当你在 A App
里打开 B App
的Activity
的时候,这个Activity
会直接被放进A的Task
里,而对于B的Task
,是没有任何影响的。
从A应用启动B应用,默认情况下启动的B应用的Activity
会进入A应用当前页面所在的任务栈中,此时按home建,再次启动B应用,会发现B应用并不会出现A启动的页面(前提是A应用启动的不是B应用主activity
,如果是必然一样),而是如第一次启动一般.
如果想要启动B应用的时候出现被A应用启动的页面,需要设置B应用被启动页的launchmode
为singleTask
,此时从A应用的ActivityA
页面启动B应用的页面ActivityB
(launchmode
为singleTask
),发现动画切换方式是应用间切换,此时ActivityB
和ActivityA
分别处于各自的任务栈中,并没有在一个task中,此时按Home键后,再次点击启动B应用,发现B应用停留在ActivityB
页面。
如果想要实现上述效果,除了设置launchmode之外,还可以通过设置allowTaskReparenting
属性达到同样的效果,Activity
默认情况下只会归属于一个 Task,不会在多个Task之间跳来跳去,但你可以通过设置来改变这个逻辑,如果你不设置singleTask
,而是设置allowTaskReparenting
为true
,此时从A应用的ActivityA
页面启动B应用的页面ActivityB
(设置了allowTaskReparenting
为true
),ActivityB
会进入ActivityA
的任务栈,此时按Home键,点击启动B应用,会进入ActivityB
页面,也就是说ActivityB
从ActivityA
的任务栈移动到了自己的任务栈中,此时点击返回,会依次退出ActivityB
所在任务栈的各个页面,直到B应用退出。
注意:allowTaskReparenting
在不同Android版本上表现有所不同,Android9以下是生效的,Android9,10又是失效的,但Android11又修复好了,在使用时一定要好好测试,避免一些因版本差异产生的问题。
②.singleInstance
singleInstance
具备singleTask
模式的所有特性外,与它的区别就是,这种模式下的Activity
会单独占用一个Task栈,具有全局唯一性,即整个系统中就这么一个实例,由于栈内复用的特性,后续的请求均不会创建新的Activity
实例,除非这个特殊的任务栈被销毁了。以singleInstance
模式启动的Activity
在整个系统中是单例的,如果在启动这样的Activity
时,已经存在了一个实例,那么会把它所在的任务调度到前台,重用这个实例。
③.singleInstancePerTask
释义:singleInstancePerTask
的作用和singleTask
几乎一模一样,不过singleInstancePerTask不需要为启动的Activity设置一个特殊的taskAffinity就可以创建新的task,换句话讲就是设置singleInstancePerTask
模式的activity
可以存在于多个task任务栈中,并且在每个任务栈中是单例的。
多次启动设置singleInstancePerTask模式的Activity并不会多次创建新的任务栈,而是如singleInstance模式一样,把当前Activity所在的任务栈置于前台展示,如果想每次以新的任务栈启动需要设置FLAG_ACTIVITY_MULTIPLE_TASK
和FLAG_ACTIVITY_NEW_DOCUMENT
,使用方式如下:
intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK | Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
此时,每次启动Activity就会单独创建新的任务栈。
注意:测试需要在Android12的真机或者模拟器上,否则默认为Standard模式
3.taskAffinity
taskAffinity
可以指定任务栈的名字,默认任务栈是应用的包名,前提是要和singleTask
,singleInstance
模式配合使用,standard
,singleTop
模式无效,当app存在多个任务栈时,如果taskAffinity
相同,则在最近任务列表中只会出现处于前台任务栈的页面,后台任务栈会“隐藏”在某处,如果taskAffinity
不同,最近任务列表会出现多个任务页面,点击某个就会把该任务栈至于前台。
4.清空任务栈
activity
跳转后设置FLAG_ACTIVITY_CLEAR_TASK
即可清空任务栈,并不是新建一个任务栈,而是清空并把当前要启动的activity
置于栈底,使用场景比如:退出登录跳转到登录页面,可以以此情况activity任务栈。
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK|Intent.FLAG_ACTIVITY_NEW_TASK);
注意:FLAG_ACTIVITY_CLEAR_TASK必须与FLAG_ACTIVITY_NEW_TASK一起使用.
5.Activity.FLAG
FLAG_ACTIVITY_NEW_TASK
FLAG_ACTIVITY_NEW_TASK
并不像起名字一样,每次都会创建新的task任务栈,而是有一套复杂的规则来判断:
- 通过
activity
类型的context
启动,如果要启动的Activity
的taskAffinity
与当前Activity
不一致,则会创建新的任务栈,并将要启动的Activity
置于栈底,taskAffinity
一致的话,就会存放于当前activity
所在的任务栈(注意启动模式章节第三点taskAffinity
的知识点); taskAffinity
一致的情况下,如果要启动的activity
已经存在,并且是栈根activity
,那么将没有任何反应(启动不了要启动的activity
)或者把要启动的activity
所在的任务栈置于前台;否则如果要启动的activity
不存在,将会在当前任务栈创建要启动的activity
实例,并入栈;taskAffinity
一致的情况下,如果要启动的activity
已经存在,但不是栈根activity
,依然会重新创建activity
示例,并入栈(前提是:要启动的activity
的launchMode
为standard
,意思就是是否会创建新实例会受到launchMode
的影响);- 非
activity
的context
启动activity
时(比如在service
或者broadcast
中启动activity
),在android7.0
之前和9.0
之后必须添加FLAG_ACTIVITY_NEW_TASK
,否则会报错(基于android-32的源码,不同版本可能不同):
``` //以下代码基于android 12 public void startActivity(Intent intent, Bundle options) { warnIfCallingFromSystemProcess(); final int targetSdkVersion = getApplicationInfo().targetSdkVersion;
//检测FLAG_ACTIVITY_NEW_TASK
if ((intent.getFlags() & Intent.FLAG_ACTIVITY_NEW_TASK) == 0
&& (targetSdkVersion < Build.VERSION_CODES.N
|| targetSdkVersion >= Build.VERSION_CODES.P)
&& (options == null
|| ActivityOptions.fromBundle(options).getLaunchTaskId() == -1)) {
//未设置FLAG_ACTIVITY_NEW_TASK,直接抛出异常
throw new AndroidRuntimeException(
"Calling startActivity() from outside of an Activity "
+ " context requires the FLAG_ACTIVITY_NEW_TASK flag."
+ " Is this really what you want?");
}
//正常启动activity
mMainThread.getInstrumentation().execStartActivity(
getOuterContext(), mMainThread.getApplicationThread(), null,
(Activity) null, intent, -1, options);
} ```
注意:FLAG_ACTIVITY_NEW_TASK的设置效果受到taskAffinity以及其他一些配置的影响,实际使用过程中一定要进行充分测试,并且不同的android版本也会表现不同,极端场景下要仔细分析测试,选择最优方案;
提示:通过
adb shell dumpsys activity activities
命令可以查看activity任务栈;
6.多进程
正常情况下,app运行在以包名为进程名的进程中,其实android四大组件支持多进程,通过manifest配置process
属性,可以指定与包名不同的进程名,即可运行在指定的进程中,从而开启多进程,那么,开启多进程有什么优缺点呢?
多进程下,可以分散内存占用,可以隔离进程,对于比较重的并且与其他模块关联不多的模块可以放在单独的进程中,从而分担主进程的压力,另外主进程和子进程不会相互影响,各自做各自的事,但开启了多进程后,也会带来一些麻烦事,比如会引起Application
的多次创建,静态成员失效,文件共享等问题。
所以是否选择使用多进程要看实际需要,我们都知道app进程分配的内存是有限的,超过系统上限就会导致内存溢出,如果想要分配到更多的内存,多进程不失为一种解决方案,但是要注意规避或处理一些多进程引起的问题;
设置多进程的方式:
``` android:process=":childProcess" //实际上完整的进程名为:包名:childProcess,这种方式声明的属于私有进程。
android:process="com.child.process" //完整的进程名即为声明的名字:com.child.process,这种方式声明的属于全局进程。 ```
7.excludeFromRecents
excludeFromRecents
如果设置为true
,那么设置的Activity
将不会出现在最近任务列表中,如果这个Activity
是整个Task
的根Activity
,整个Task将不会出现在最近任务列表中.
8.startActivityForResult被弃用
使用Activity Result Api代替,使用方式如下:
``` private val launcherActivity = registerForActivityResult( ActivityResultContracts.StartActivityForResult()) { Log.e("code","resultCode = "+it.resultCode) }
findViewById
9.Deep link
简单理解,所谓Deep Link就是可以通过外部链接来启动app或者到达app指定页面的一想技术,比如可以通过点击短信或者网页中的链接来拉起app到指定页面,以达到提供日活或者其他目的,一般流程是可以通过在manifest
的activity
标签中配置固定的schema
来实现这种效果,形如:
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE"/>
<data
android:scheme="jumptest"
android:host="work"
android:port="8801"
android:path="/main"
/>
</intent-filter>
然后在网页中就可以通过如下方式来启动当前activity:
<a href="jumptest://work:8801/main?data=123456">你好</a>
格式
<scheme>://<host>:<port>/<path>?<query>
被启动的app可以通过如下方式拿到传递的参数以及schmea配置项:
val host = schemaIntent.data?.host
val path = schemaIntent.data?.path
val schema = schemaIntent.data?.scheme
val query = schemaIntent.data?.query
Log.e("schema","host = $host, path = $path, schema = $schema, query = $query")
结果:
注意:
1.
intent-filter
与Main主Activity搭配使用时,要单独开启一个intent-filter,否则匹配不到。
2.从android12开始,设置了intent-filter
标签后,activity的exported必须设置成true,这个要注意(android12之前,其实添加了intent-filter,系统也会默认设置exported为true)。
①.app link
App link是一种特殊的Deep link,它的作用就是可以使通过网站地址打开app的时候,不需要用户选择使用哪个应用来打开,换种说法就是,我可以设置默认打开次地址的应用,这样一来,就可以直接引导到自己的app。
更多关于App link的可以参考这篇文章,或者看官网介绍。
10.setResult和finish的顺序关系
通过startActivityForResult
启动activity
,通常会在被启动的activity
的合适时机调用setResult
来回调数据给上一个页面,然后当前页面返回的时候就会回调onActivityResult
,这里要注意setResult
的调用时机,请一定要在activity的finish()方法之前调用,否则可能不会生效(不会回调onActivityResult)。
原因如下:
``` private void finish(int finishTask) { if (mParent == null) { int resultCode; Intent resultData; //会在finish的时候把回调数据赋值 synchronized (this) { resultCode = mResultCode; resultData = mResultData; } ··· if (ActivityClient.getInstance().finishActivity(mToken, resultCode, resultData, finishTask)) { mFinished = true; } } else { mParent.finishFromChild(this); } ··· }
//setResult对mResultCode,mResultData赋值 public final void setResult(int resultCode) { synchronized (this) { mResultCode = resultCode; mResultData = null; } }
``
由上述代码可以看出,
setResult必须在
finish之前赋值,才能够在
finish的时候拿到需要
callback的数据,以便在合适的时机回调
onActivityResult`;
11.onSaveInstanceState()和onRestoreInstanceState()
activity
在非正常情况被销毁的时候(非正常情况:横竖屏切换,系统配置发生变化,内存不足后台activity
被回收等),当重新回到该activity
,系统会重新实例化该对象,如果没有对页面输入的内容进行保存,就会存在内容丢失的情况,此时可以通过onSaveInstanceState
来保存页面数据,在onCreate
或者onRestoreInstanceState
中对数据进行恢复,形如:
``` override fun onSaveInstanceState(outState: Bundle) { outState.putString("SAVE_KEY","SAVE_DATA") outState.putString("SAVE_KEY","SAVE_DATA2") super.onSaveInstanceState(outState) } //需要判空,savedInstanceState不一定有值 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if(null != savedInstanceState){ saveData = savedInstanceState.getString("SAVE_KEY") ?: "" saveData2 = savedInstanceState.getString("SAVE_KEY2") ?: "" } setContentView(R.layout.activity_main) }
//或者在onRestoreInstanceState恢复数据,无需判空,回调此方法一定有值 override fun onRestoreInstanceState(savedInstanceState: Bundle) { saveData = savedInstanceState.getString("SAVE_KEY") ?: "" saveData2 = savedInstanceState.getString("SAVE_KEY2") ?: "" super.onRestoreInstanceState(savedInstanceState) }
```
注意:请使用
onSaveInstanceState(outState: Bundle)
一个参数的方法,两个参数的方法和Activity
的persistableMode
有关。
本文主要对Activity重难点知识进行整理和解释,希望对大家有所帮助,当然难免存在错误,如有发现,希望指正,如果感觉不错,麻烦点个赞,这将给我持续更文以更大的动力,后续如有其他知识点,也会持续更新。