Activity是Android四大组件中最重要的一个,相关知识也错综复杂,此篇记录学习及工作中Activity的相关知识。
文章目录
Activity生命周期
典型情况下的生命周期
说到Activity,就不得不说其生命周期。说到生命周期,就更不得不放上一张刚开始接触Android时总是能碰到的一张图,此图清楚的描述了大多数情况下的Activity生命周期。
- onCreate表示Activity正在被创建,是生命周期的第一个方法,可以做些初始化,包括数据的初始化及布局界面的初始化。
- onStart表示Activity正在被启动,此时Activity已经可见,但是并没有出现在前台,也就还无法进行交互。
- onRestart表示Activity正在被重新启动,当当前Activity从不可见到可见的时会被调用,包括但不限于切换到桌面后再重新进入Activity或进入另一个Activity后又返回。
- onResume表示Activity已经出现在前台且已经可以进行交互。
- onPause表示Activity正在停止,此方法可以做些数据存储、动画停止等操作,但不能是耗时操作,否则会影响新的Activity的显示,因为只有此Activity执行完毕onPause后才会执行新的Activity的创建显示等一系列操作。
- onStop表示Activity即将停止,同样可以做些回收操作,同样也不能太耗时。
- onDestroy表示Activity即将被销毁,是Activity生命周期的最后一个方法回调。可以做些回收操作及资源的释放,如Handler延时任务清空,线程销毁,持续动画的停止销毁等,注意持有此Activity的各种情况,处理不当会导致Activity因为被引用而不能被回收的内存泄漏问题。
- 补充:onStart和onResume、onPause和onStop的实质不同就是不以同一个角度来回调,onStart和onStop是以是否可见的角度,onResume和onPause是以是否当前Activity位于前台的角度。
除上面的正常的Activity流程外,还有以下常用的回调方法:
- onSaveInstanceState(Bundle)
- onRestoreInstanceState(Bundle)
- onNewIntent(Intent)
常见情形
-
首次启动
-
再次启动
当lunchmode是standard或是singTop但目标Activity并不在栈顶时,重新启动Activity依旧是上面的调用过程。
当lunchmode是singleTask、singleInstance或singTop且目标Activity在栈顶时,启动顺序就如下图所示
不会再走**onCreate()方法了,而是调用的onNewIntent()**方法。
-
跳转之后
onSaveInstanceState()方法是会被调用的,不只是在Activity被异常销毁才会被调用,但是onRestoreInstanceState方法是是只会在异常销毁后才被调用。
-
跳转后返回
-
销毁
以上场景的log都是同一个Activity的。
-
跳转
当一个Activity启动另一个Activity时,两个Activity是以启动Activity的onPause->新Activity的onCreate、onStart、onResume的顺序隐藏及显示的。
异常情况下的生命周期
-
屏幕旋转
可以看到的是当屏幕旋转时,会首先销毁当前Activity,然后再重新创建,并且**onAttachedToWindow()**都会重新调用。
除此之外还会调用onSaveInstanceState方法保存当前Activity的各种现场,其中Android里的系统View不需要任何操作都会自动保存状态,是因为都实现了此方法,自定义View也可以实现保存恢复方法来应对这种异常情况,除View的各种状态外,还可以在此方法保存各种需要保存的内容,程序运行的临时数据等放入onSaveInstanceState的参数Bundle中。在Activity重新启动后就会调用onRestoreInstanceState方法恢复现场。
保存自定义内容代码如下:
@Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); int savedNum = 1221; outState.putInt("saved_num", savedNum); Log.e(TAG, "onSaveInstanceState: saved num " + savedNum); } @Override protected void onRestoreInstanceState(Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); int savedNum = savedInstanceState.getInt("saved_num"); Log.e(TAG, "onRestoreInstanceState: saved num " + savedNum); }
保存结果如下:
恢复状态从Bundle中取保存的数据时推荐在onRestoreInstanceState方法中,此方法只要是被调用,Bundle参数必定不为空,onCreate方法的Bundle参数也可以在异常启动时进行恢复,但是如果是正常启动此参数会为null。
-
低优先级Activity因为内存资源不足被杀死
Activity优先级排序
- 前台Activity,正在和用户进行交互的Activity,优先级最高。
- 可见但非前台Activity,比如Activity中弹出一个Dialog,导致Activity可见,但是处于后台无法和用户交互。
- 后台Activity,比如已经执行了onStop方法的Activity,优先级最低。
当资源紧张时会从下至上的依次杀死Activity以回收内存等资源,被杀死的Activity同样会依次执行onSaveInstanceState方法,等再回到此Activity时会执行onRestoreInstanceState方法。
-
禁止横屏或屏幕旋转时不进行Activity重建
如果不想让用户使用屏幕旋转功能,那么就可以在Manifest中的Activity标签加上以下属性强制竖屏(更多的Manifest内容可详看Android-Manifest文章)
android:screenOrientation="portrait"
其他的属性值含义为:
- unspecified,默认值,由系统决定,不同手机可能不一致
- landscape,强制横屏显示
- portrait,强制竖屏显
- behind,与前一个activity方向相同
- sensor,根据物理传感器方向转动,用户90度、180度、270度旋转手机方向,activity都更着变化
- sensorLandscape,横屏旋转,一般横屏游戏会这样设置
- sensorPortrait,竖屏旋转
- nosensor,旋转设备时候,界面不会跟着旋转。初始化界面方向由系统控制
- nosensor,旋转设备时候,界面不会跟着旋转。初始化界面方向由系统控制
如果不想在屏幕旋转时进行Activity的重建可加入此项属性
android:configChanges="orientation|screenSize"
-
执行onNewIntent时的数据传递
启动时没携带数据,再次启动时携带了一个字符串,因为lunchMode是singleTask,所以在**onNewIntent()方法中可以看到是qwer,但是之后的onResume()方法中通过getIntent()方法却获取不到数据,这是因为Activity持有的Intent还是首次启动时的Intent,正确的获取方法应该是先在onNewIntent()方法中执行setIntent(Intent)**方法。
这才是正确的获取方法。
LaunchMode
Activity的启动是在任务栈中的,默认的任务栈是以包名命名的,也可以更改自定义任务栈。既然是栈就会遵循先进后出的方式,当一个Activity A启动另一个Activity B后,栈内从上至下就是B A,当按返回键时会先退出B,再按就会退出A。
四种启动模式
-
standard 标准模式
每次启动一个Activity都会重新创建一个新的实例,不管这个Activity是否已经有实例存在。多次启动后,一个任务栈中会有多个实例。哪个Activity启动了此模式的Activity,那新Activity就会进入启动Activity的任务栈中。A启动A那么栈内就是AA。但是如果直接用ApplicationContext启动此模式的Activity会报错,因为不是ActivityContext没有任务栈信息所以才会要求指定一个任务栈或配置Intent的flag。
配置一个FLAG_ACTIVITY_NEW_TASK标志即可正确启动。
case R.id.btn_start_new_activity_appcontext: getApplicationContext().startActivity(new Intent(getApplicationContext(), LifeCycleActivity1.class)); break; case R.id.btn_start_new_activity_appcontext_flag: Intent intent = new Intent(getApplicationContext(), LifeCycleActivity1.class); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); getApplicationContext().startActivity(intent); break;
Android开发艺术探索有讲:
此时就相当于新Activity以singleTask方式启动。
但是singleTask方式有两个特别的点是clearTop及根据设置的TaskAffinity创建新的任务栈。如果新的Activity并没有设置TaskAffinity,并不会创建新的任务栈,而是直接压入了启动此Activity的栈,当然此测试都是在同一个APP下,如果是启动其他APP中的Activity,那必定会创建一个新的以Activity所在包名命名的栈中。clearTop也并没有体现,反复启动目标Activity也同样都是按默认标准模式启动的,甚是不解。
-
singleTop 栈顶复用模式
如果新Activity已经位于任务栈的栈顶,那么就不会再次创建,但是会调用**onNewIntent()**方法,而正常的生命周期onCreate、onStart不会被调用。如果新Activity不在栈顶,那么和standard标准模式一样,依然会重新创建实例。栈顶A启动singleTop A,如果A在栈顶则不会重新创建,只会调onNewIntent方法,AB栈顶B启动singleTop A,则还是创建A,栈变为ABA。
-
singleTask 栈内复用模式
只要Activity在一个栈中存在,那么多次启动此Activity都不会重新创建实例,和singleTop一样,系统会回调**onNewIntent()**方法。
当在启动singTask Activity时会先寻找Activity所需的栈,如果有则寻找栈内是否有目标Activity,若有则清除目标Activity之上的所有Activity,显示目标Activity并执行onNewIntent方法,若没有目标Activity,则直接创建Activity。没有所需的栈就直接创建栈后创建目标Activity。
举例:
- 任务栈A1中有ABC,现在启动需要A2任务栈的D,那么会创建A2栈后再创建D压入A2栈中。此时存在两个栈A1和A2,分别有ABC和D。
- 与情形1同样的条件,但是D所需的任务栈也为A1,那么此时就直接创建D并压入A1。
- D所需的任务栈为A1,此时A1中有ABDC,此时D不会重新创建,而是将C出栈,并调用onNewIntent方法显示D。
-
singleInstance 单实例模式
加强型singleTask,除具有singleTask所有的特性外,还有一个特殊点是此模式的Activity只能单独的存在于一个任务栈中。如果从此模式的Activity中启动其他模式的Activity那么不会在此压入栈中。
-
其他补充
-
任务栈: Activity默认任务栈就是以包名命名的,默认情况下,Activity所需的任务栈也就是此任务栈,除非手动指定Activity标签下的TaskAffinity属性。
-
TaskAffinity: 主要和SingleTask活着AllowTaskReParenting属性配对使用。另外任务栈分为前台任务栈和后台任务栈,后台任务栈中的Activity处于暂停状态,可以通过切换后台任务栈调到前台。
-
TaskAffinity和SingleTask配对使用: 待启动的Activity会运行在名字和TaskAffinity相同的任务栈中。
-
TaskAffinity和AllowTaskReparenting配对使用: 当A应用启动了B应用的某个Activity时,那么此Activity会运行在A的任务栈中,如果Activity的AllowTaskReparenting属性为true,那么在启动B应用时,Activity会直接转移到B的任务栈并直接显示此Activity。
应用B的Activity配置:
<activity android:name=".TestAllowTaskReParentActivity" android:allowTaskReparenting="true" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity>
应用A启动此Activity:
Intent skipIntent = new Intent(); ComponentName componentName = new ComponentName("com.libok.test", "com.libok.test.TestAllowTaskReParentActivity"); skipIntent.setComponent(componentName);
启动后的Activity Task如下:
启动应用B后的Activity Task:
此时AllowTaskReparenting就起作用了,为什么没指定TaskAffinity就可以实现是因为不同的应用包名不同,没指定TaskAffinity的话会直接以包名命名。
-
设置Activity启动模式: 可以在Manifest文件中设置,也可以在启动Activity的Intent中设置,但是Manifest文件中不能直接为Activity设置***FLAG_ACTIVITY_CLEAR_TOP***标志,而在Intent中不能直接指定***singleInstance***模式。
-
Activity的Flags
-
FLAG_ACTIVITY_NEW_TASK
指定Activity为singleTask模式,与Manifest中配置效果一样。
-
FLAG_ACTIVITY_SINGLE_TOP
指定Activity为singleTop模式,与Manifest中配置效果一样。
-
FLAG_ACTIVITY_CLEAR_TOP
当启动一个具有此标志的Activity时,在栈中此Activity之上的Activity都会被出栈。配合singleTask使用,实例存在则调用onNewIntent,不存在直接创建。如果Activity是standard模式,实例存在则连同实例及以上的Activity都会出栈,然后系统创建新的Activity实例入栈。
-
FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
此标记的Activity不会出现在历史Activity列表中,等同于在Manifest中设置android:excludeFromRecents属性。
Activity标签
Manifest
- Manifest文件中,Activity标签下android:excludeFromRecents="true"属性,是在应用退出后不存留于最近运行程序中
- 强制竖屏见上述Activity生命周期
- 其他Manifest内容详见Android-Manifest
IntentFilter
启动Activity有两种方式,显式启动和隐式启动。显式是直接指定具体的Activity,隐式是构建一个Intent并配置一些过滤参数,由系统去寻找适合的Activity,没有适合的是不会启动的。
显式:
// 启动本应用Activity
startActivity(new Intent(MainActivity.this, OtherActivity.class));
// 启动其他应用Activity
Intent intent = new Intent();
ComponentName componentName = new ComponentName("com.example.applicatioin", "com.example.application.XXXActivity");
skipIntent.setComponent(componentName);
startActivity(intent);
隐式:
// 指定网址隐式启动浏览器并跳转到baidu
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.parse("https://www.baidu.com"), "text/html");
startActivity(intent);
隐式启动的IntentFilter有三项匹配过滤信息,action、category、data。示例如下:
<activity android:name=".activity.ImplicitActivity">
<intent-filter>
<action android:name="com.example.application.action" />
<action android:name="ImplicitAction" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="com.example.application.category" />
<category android:name="ImplicitCategory" />
<data android:mimeType="text/plain" />
</intent-filter>
</activity>
此Activity可以用以下这种方式启动:
Intent intent = new Intent("ImplicitAction");
intent.addCategory("ImplicitCategory");
intent.addCategory("com.example.application.category");
intent.setDataAndType(Uri.parse("abc"), "text/plain");
此Activity中只有一个IntentFilter,实际是允许有多个的,当进行匹配时,只需要匹配到一个IntentFilter即可。
下面分别具体介绍匹配规则。
Action
Action是一个字符串,系统预置了一些Action,也可以自定义Action。
Action的匹配规则是Intent中的Action必须跟Manifest中IntentFilter的Action一致,即字符串值必须完全一样。一个IntentFilter中可以有多个Action,进行匹配时只需要匹配成功任何一个即可。之前的例子有两个Action,com.example.application.action和ImplicitAction,在配置Intent时只需一个就行。但是如果Intent中没有Action,那么必定是匹配不成功的。
Category
Category也是一个字符串,系统也预置了一些Category,也可以自定义Category。
Category的匹配规则是Intent中如果含有Category,那么不论有几个都必须是IntentFilter中出现的,如果不配置Category那么系统会添加一个默认Category,即android.intent.category.DEFAULT。另外需要注意的是如果在IntentFilter中自定义了Category,那么必须得手动带上android.intent.category.DEFAULT,否则是不会匹配成功的,因为自定义之后系统就不会在IntentFilter中自动添加。
Data
Data和Action的匹配规则类似,一个IntentFilter中可以有多个Data,Intent中只要有一个匹配到即可。但是Action只有一个name属性,而Data就比较复杂,属性结构如下:
<data android:mimeType="string"
android:host="string"
android:scheme="string"
android:port="string"
android:path="string"
android:pathPattern="string"
android:pathPrefix="string"/>
Data由两部分组成,MIME TYPE和URI。
MIME TYPE是媒体类型,比如image/jpeg、text/html等,可以表示不同的媒体格式。更多的格式详看MIME 参考手册。
URI是资源路径,结构如下:
<scheme>://<host>:<port>/[<path>|<pathPrefix>|<pathPattern>]
content://com.example.application:200/folder/subfolder/abc
https://www.baidu.com:80/search/info
- Scheme URI的模式,比如http、ftp、content等,如果URI没有指定scheme,那么整个URI是无效的。
- Host URI主机名,比如www.baidu.com,如果URI没有指定host,那么整个URI是无效的。
- Port URI端口号,比如80、443等,只有URI指定了scheme和host时port才是有意义的。
- Path、PathPattern和PathPrefix 表示路径信息,其中path表示完整的路径;pathPattern也表示完整路径,但是其中可以包含通配符”*“,”*“表示0个或任意多个字符,由于正则表达式的规范,如果想表示真实的字符串,”*“要写成”\\*“,”\“要写成”\\\\“;pathPrefix表示路径的前缀。
分情况补充说明:
-
<intent-filter> <data android:mimeType="image/*" /> ... </intent-filter>
此规则需要Intent中的MIME TYPE属性必须为image/*才能够匹配,虽然此种情况没有指定URI,但是却有默认值,URI的默认值为content和file,因此Intent中的URI部分必须是content或file才能匹配。Intent指定URI应直接调用setDataAndType方法,而不能先调用setData方法再调用setType方法,因为单独调用会导致另一个参数置空。示例如下:
intent.setDataAndType(Uri.parse("file://abc"), "image/png");
有一点需要注意的是自Android N(7) API24开始,不允许直接创建File格式的URI,因为不够安全,会出现android.os.FileUriExposedException: file://abc exposed beyond app through Intent.getData(),推荐用FileProvider授予临时权限。
-
<intent-filter> <data android:mimeType="video/mpeg" android:scheme="http" .../> <data android:mimeType="audio/mpeg" android:scheme="http" .../> ... </intent-filter>
指定了两组data规则,且每个data都指定了完整的属性值,可用以下实例匹配:
intent.setDataAndType(Uri.parse("http://abc"), "video/mpeg");
或
intent.setDataAndType(Uri.parse("http://abc"), "audio/mpeg");
-
还有一个特殊情况,与Action不同的地方,下面的两种写法作用是一样的。
<intent-filter> <data android:scheme="file" android:host="www.baidu.com" /> ... </intent-filter>
和
<intent-filter> <data android:scheme="file" /> <data android:host="www.baidu.com" /> ... </intent-filter>
补充说明
当通过隐式启动Activity时,可以做下判断,看是否有Activity能匹配到Intent的所有配置,如果找不到会出现ActivityNotFoundException。
判断的方法有两种:
-
用PackageManager的resolveActivity方法或者Intent的resolveActivity方法,如果找不到会返回null。
public abstract ResolveInfo resolveActivity(Intent intent, @ResolveInfoFlags int flags);
-
用PackageManager的queryIntentActivities方法,此方法与resolveActivity方法不同的是,会将所欲匹配成功的Activity返回,而不是返回最佳的Activity。
public abstract List<ResolveInfo> queryIntentActivities(Intent intent, @ResolveInfoFlags int flags);
上述两个方法第一个参数都是要过滤匹配的Intent,第二个参数flags需要使用MATCH_DEFAULT_ONLY这个标志位,意义在于只要返回的不是null,那么必定可以启动成功。因为不包含android.intent.category.DEFAULT这个Category的Activity是不能被隐式启动的。
Action和Category有个特别的属性值:
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
两者共同的作用是表明此Activity是一个入口Activity,并且会出现在系统的应用列表中,少了任何一个都没有意义,也无法出现在系统的应用列表中。
对于Service和BroadcastReceiver,PackageManager同样提供了resolve和query类似的方法以便获取匹配的组件。同样的也适配IntentFilter匹配规则。
Activity转场动画
共享元素
在同样的View下设置transitionName,并且在startActivity中加入ActivityOptions.makeSceneTransitionAnimation.toBundle()。如果在退出时需要用finish(),那么在设置完动画后应该用onBackPressed()或者finishAfterTransition()。
普通动画
[详看Android-动画]
APP转入后台
moveTaskToBack
APP真正进程死亡是System.exit(0)
onActivityResult的resultCode一直为0
是因为launchMode是singTask模式或singInstance模式,修改为singTop或者默认即可