Android之Activity全面解析,有些知识点容易忘记
目录
目录(Activity)一、前言
Activity作为安卓四大组件之一,是最重要也是用得最多的组件,涉及的知识点非常多,有些知识点平时开发很少用到,但在某些场景下需要特别注意,本文详细整理了Activity涉及的知识点,供开发参考。
常见问题
针对Activity可以提出很多问题,如:
Activity 的生命周期?
Activity 之间的通信方式?
Activity 各种情况下的生命周期?
横竖屏切换时 Activity 的生命周期?
前台切换到后台,然后再回到前台时 Activity 的生命周期?
弹出 Dialog 的时候按 Home 键时 Activity 的生命周期?
两个Activity之间跳转时的生命周期?
下拉状态栏时 Activity 的生命周期?
Activity 与 Fragment 之间生命周期比较?
Activity 的四种 LaunchMode(启动模式)的区别?
Activity 状态保存与恢复?
Activity的转场动画有哪些实现方式?
Activity的生命周期中怎么获取控件宽高?
onNewIntent的执行时机?
如何连续退出多个Activity?
二、生命周期
2.1、生命周期经典图
生命周期2.2、不同场景的生命周期
(1) 启动和页面跳转
场景 | 生命周期回调 |
---|---|
第一次启动 | onCreate=>onStart=>onResume |
按back键后退 | onPause=>onStop=>onDestroy |
按home键 | onPause=>onStop |
重回应用 | onRestart=>onStart=>onResume |
从A跳转到B(不透明) | A_onPause=>B_onCreate=>B_onStart=> B_onResume=>A_onStop |
从B(不透明) 返回到A | B_onPause=>A_onReStart=>A_onStart=> A_onResume=>B_onStop=>B_onDestroy |
A跳转到B(透明) | A_onPause—B_onCreate—B_onResume |
从B(透明)返回到A | B_onPause=>A_onResume=>B_onStop=> B_onDestroy |
(2) Dialog弹出
类型 | 生命周期回调 |
---|---|
普通dialog | 不会触发Acitivty生命周期 |
Dialog Activity | 与跳转透明Acivity类似 |
如何把Acitivty设置成Dialog样式 ,android:theme="@android:style/Theme.Dialog"
(3) 解锁屏幕
操作 | 生命周期回调 |
---|---|
锁屏 | onPause=>onStop |
解屏 | onRestart=>onStart=>onResume |
(4) 横竖屏切换
关于横竖屏切换的生命周期,对应不同的手机,由于厂商定制的原因,会有不同的效果,如设置了configChanges="orientation”在有些手机会执行各个生命周期,但有些手机却不会执行。
网上常见的结论如下:
设置 | 生命周期回调 |
---|---|
设置1:不设置android:configChanges | 切屏会销毁重建,执行各个生命周期, onPause=>onStop=>onDestroy=>onCreate =>onStart=>onResum, 切竖屏会执行两次 |
设置2:android:configChanges="orientation“ | 切屏会销毁重建,都是执行一次 |
设置3:android:configChanges= "orientation|keyboardHidden“ |
targetSdkVersion>=13切屏会销毁重建,都是执行一次 targetSdkVersion<13,切屏不会执行生命周期,会执行onConfigurationChanged回调 |
设置4:android:configChanges= "orientation|keyboardHidden| screenSize “ |
targetSdkVersion无论多少,切屏不会执行生命周期,会执行onConfigurationChanged回调 |
但实际的测试如下:
手机 | 测试结果 |
---|---|
小米1(4.1.2) | 设置1:执行各个生命周期一次 设置2、3:效果都与上面设置3所说效果一致,跟targetSdkVersion有关 设置4:与上述一致 |
小米4LTE(6.0.1) | 同上 |
华为mate 20 plus(9.0) | 设置1:执行各个生命周期一次 设置2、3、4:无论targetSdkVersion多少,都只回调onConfigurationChanged |
Nexus 5x(8.0) | 同上 |
可以看出,不同厂商的手机切屏生命周期会有差异。
从API 13以上,当设备在横竖切屏时,“屏幕尺寸”也会发生变化,因此为了杜绝切屏导致页面销毁重建,需要加上screenSize,使用设置4,即android:configChanges="orientation|keyboardHidden|screenSize"
.
ps:实际开发中,偶尔碰到这样的场景,A页面设置只允许竖屏,即android:screenOrientation="portrait",跳转到一个横屏的页面B,如拍摄照片,返回A的时候,A也变为横屏了,没有恢复,或者旋转一两次后再变为竖屏,页面是重建了,这种场景同样需要对A设置android:configChanges="orientation|keyboardHidden|screenSize“.
(5) 内存不足
Activity的四种状态如下:
Activity四种状态
在activity处于paused或者stoped状态下,如果系统内存紧张,可能会被销毁,当重回该activity时会重建,正常返回和被回收后返回的生命周期如下:
场景 | 生命周期 |
---|---|
正常返回 | onRestart=>onStart=>onResume 或者onResume |
回收后返回 | onCreate=>onStart=>onResume |
如果是回收后返回,onCreate的参数savedInstanceState不为空。
ps:在实际开发中,如果需要跳转需要调用摄像头的页面,容易触发activity回收,需要在onSaveInstanceState中保存一些变量,然后在onRestoreInstanceState或者onCreate中恢复。
2.3、什么是 onNewIntent
有哪些场景会触发onNewIntent回调呢?跟启动模式有关,首先该Activity实例已经存在,再次启动才可能触发。一种情况是启动模式是singleTask或者singleInstance,无论该activity在栈中哪个位置,都会触发onNewIntent回调,并且把上面其他acitivity移除,另一种情况是启动模式是singleTop或者以FLAG_ACTIVITY_SINGLE_TOP启动,并且该activity实例在栈顶,会触发onNewIntent,如果不在栈顶是重新创建的,不会触发。
onNewIntent触发场景
ps:onNewIntent会收到新的参数intent,但此时getIntent获取的依然是旧的Intent,需要先调用setIntent后获取的才是新的Intent。一般会根据新的Intent加载新的内容,例如新闻、消息等。
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
getIntent();//旧的intent
setIntent(intent);//设置新的intent
getIntent();//新的intent
}
2.4、如何退出多个activity
在实际业务开发中,往往碰到需要连续退出多个activity实例,下面整理了几种常见方法:
连续退出多个activity
● 发送特定广播
1、在需要处理连续退出的activity注册该特定广播;
2、发起退出的activity发送该特定广播;
3、接收到该广播的activity 调用finish结束页面。
● 递归退出
1、用startActivityForResult启动新的activity;
2、前一个页面finish时,触发onActvityResult回调,再根据requestCode和resultCode处理是否finish,达到递归退出的效果。
● FLAG_ACTIVITY_CLEAR_TOP
通过intent.setFlag(Intent.FLAG_ACTIVITY_CLEAR_TOP)启动新activity,如果栈中已经有该实例,则会把该activity之上的所有activity关闭,达到singleTop启动模式的效果。
● 自定义activity栈
1、自定义activity列表,新打开activity则加入栈中,关闭则移除栈;
2、需要退出多个activity时,则循环从栈中移除activity实例,并调用finish。
三、启动模式
3.1、四大启动模式
启动模式 | 说明 |
---|---|
standard | 标准模式,也是系统的默认模式,每次启动一个 Activity 都会重新创建一个实例,加入到栈顶。 |
singleTop | 栈顶复用,如果新 Activity 已经位于任务栈的栈顶,那么此 Activity 不会被重新创建,同时它的 onNewIntent 方法会被回调,但如果已有实例不在栈顶,则会重新创建,加入栈顶。 |
singleTask | 栈内复用,只要 Activity 在一个栈中存在,那么多次启动此 Activity 都不会重新创建实例,和 singleTop 一样,系统也会回调其 onNewIntent,并把该实例之上所有activity实例关闭。 |
singleInstance | 单例模式,启动的 Activity 会创建一个新的任务栈并压入栈中,由于栈内复用的特性,后续的请求均不会创建新的 Activity,除非这个任务栈被系统销毁了。 |
3.2、各种启动模式的应用场景
启动模式 | 应用场景 |
---|---|
standard | 在mainfest中不设置就默认就是standard,一般场景都是使用默认的 |
singleTop | 消息推送详情页、新闻详情页、登录页等 |
singleTask | 程序主页、WXEntryActivity、WXPayEntryActivity、WebView页面等 |
singleInstance | 系统Launcher、锁屏键、来电显示、相机、闹钟等系统应用 |
3.3、任务栈及taskAffinity
(1) 任务栈
在讨论Activity启动模式经常提到任务栈,那到底什么是任务栈?
任务是一个Activity的集合,它使用栈的方式来管理其中的Activity,这个栈又被称为返回栈(back stack),栈中Activity的顺序就是按照它们被打开的顺序依次存放的。返回栈是一个典型的后进先出(last in, first out)的数据结构。下图通过时间线的方式非常清晰地向我们展示了多个Activity在返回栈当中的状态变化:
任务栈
(2) taskAffinity
taskAffinity 任务相关性,可以用于指定一个Activity更加愿意依附于哪一个任务,在默认情况下,同一个应用程序中的所有Activity都具有相同的affinity, 名字为应用的包名。当然了,我们可以为每个 Activity 都单独指定 taskAffinity 属性(不与包名相同)。taskAffinity 属性主要和 singleTask 启动模式和 allowTaskReparenting 属性配对使用,在其他情况下没有意义。
taskAffinity 有下面两种应用场景:
场景 | 说明 |
---|---|
activity设置taskAffinity | 如果不设置taskAffinity,无论是设置了singleTask还是使用intent.setFlag(Intent.FLAG_START_NEW_TASK),也不会启动新的任务栈,只有设置了taskAffinity 才会启动新的。 |
activity设置allowTaskReparenting为true | Activity就拥有了一个转移所在任务的能力,就是一个Activity现在是处于某个任务当中的,但是它与另外一个任务具有相同的affinity值,那么当另外这个任务切换到前台的时候,该Activity就可以转移到现在的这个任务当中。举例说,在我们的程序启动一个天气信息详情页面,该页面在我们程序的任务栈中的,但是设置了taskAffinity,且跟天气预报程序的taskAffinity 相同,当天气预报程序启动时,该页面会转移到天气预报的任务栈中。 |
3.4、启动模式的使用方式
启动模式的使用方式3.5、Activity的Flag
标记位 | 说明 |
---|---|
FLAG_ACTIVITY_NEW_TASK | 为 Activity 指定 singleTask 启动模式,跟在 xml 中指定启动模式效果相同 |
FLAG_ACTIVITY_SINGLE_TOP | 为 Activity 指定 singleTop 启动模式,跟在 xml 中指定启动模式效果相同 |
FLAG_ACTIVITY_CLEAR_TOP | 具有此标记位的 Activity,当启动它时,同一个任务栈中所有位于它上面的 Activity 都要出栈。这个模式一般需要和 FLAG_ACTIVITY_NEW_TASK 配合使用,在这种情况下,被启动的 Activity 实例如果已经存在,那么系统就会调用它的 onNewIntent。如果被启动的 Activity 采用 standard 模式启动,那么它连同它之上的 Activity 都要出栈,系统会创建新的 Activity 实例并放入栈顶 |
FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS | 具有这个标记的 Activity 不会出现在历史的 Activity 的列表中,当某些情况下我们不希望用户通过历史列表回到我们的 Activity 的时候这个标记比较有用。它等同于在 xml 中指定 Activity 的属性 android:excludeFormRecents="true" |
四、启动方式
4.1、Activity的启动方式
分为显示启动和隐式启动。
(1)显示启动
直接指定待调整的Activity类名。
Intent it = new Intent(A.this, B.class);
startActivity(it);
(2)隐式启动
Intent 能够匹配目标组件的 IntentFilter 中所设置的过滤信息,如果不匹配将无法启动目标 Activity。IntentFilter 的过滤信息有 action、category、data。
IntentFilter 需要注意的地方有以下:
● 一个 Activity 中可以有多个 intent-filter
● 一个 intent-filter 同时可以有多个 action、category、data
● 一个 Intent 只要能匹配任何一组 intent-filter 即可启动对应 Activity
● 新建的 Activity 必须加上以下这句,代表能够接收隐式调用
<category android:name="android.intent.category.DEFAULT" />
4.2、IntentFilter 匹配说明
(1)action匹配规则
只要匹配一个action即可跳转,注意的是action要区分大小写。
<activity android:name=".A">
<intent-filter>
<action android:name="action_name1"/>
<action android:name="action_name2"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
Intent intent = new Intent();
intent.setAction("action_name1");
startActivity(intent);
(2)category匹配规则
规则:如果intent中有category,则所有的都能匹配到intent-filter中的category,intent中的category数量可用少于intent-filter中的。另外,单独设置category是无法匹配activity的,因为category属性是一个执行Action的附加信息。
<activity android:name=".A">
<intent-filter>
<action android:name="action_name"/>
<category android:name="category_name"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
Intent intent = new Intent();
intent.setAction("action_name");
intent.addCategory("category_name");
startActivity(intent);
intent不添加category会匹配默认的,即 “android:intent.category.DEFAULT”
如果上面例子,如果去掉intent.setAction("action_name"),则会抛出异常:
android.content.ActivityNotFoundException: No Activity found to handle Intent { cat=[category_name]
(3)data匹配规则
规则:类似action,但data有复杂的结构,只要匹配一个data并且与data中所有属性都一致就能匹配到Activity,只要有1个属性不匹配,都无法找到activity。
data的结构:
<data android:scheme="axe"
android:host="xxx"
android:port="xxx"
android:path="xxx"
android:pathPattern="xxx"
android:pathPrefix="xxx"
android:mimeType="xxx"/>
data 主要是由 URI 和 mimeType 组成的。
URI 可配置很多信息,的结构如下:
<scheme>://<host>:<port>[<path>|<pathPrefix>|<pathPattern>]
与url类似,例如:
content://baidu:8888/dir/
https://www.baidu.com/
mineType:指资源类型包括文本、图片、音视频等等,例如:text/plain、 image/jpeg、video/* 等
下面看下data匹配的例子:
只匹配scheme
<activity android:name=".A">
<intent-filter>
<action android:name="action_name"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="content" />
</intent-filter>
</activity>
Intent intent = new Intent();
intent.setData(Uri.parse("content://baidu"));
startActivity(intent);
只匹配scheme也是能匹配到activity的。
匹配scheme、host、port
将上面的data改为
<data
android:host="baidu"
android:port="8888"
android:scheme="content" />
intet.setData(Uri.parse("content://baidu:8888"));
匹配mineType
<data
android:mimeType="abc"
android:host="baidu"
android:path="dir"
android:port="8888"
android:scheme="content" />
如果有mineType,则不能仅设置setData或setMineType了,因为setData会把mineType置为null,而setMineType会把data置为null,导致永远无法匹配到activity,要使用setDataAndType。
intent.setDataAndType(Uri.parse("content://baidu:8888/dir"),"abc");
使用scheme的默认值content\file
<data android:mimeType="image/*" />
intent.setDataAndType(Uri.parse("file://abc"),"image/png");
五、转场动画
5.1、overridePendingTransition(int enterAnim, int exitAnim)
注意该方法需要在startAtivity方法或者是finish方法调用之后立即执行,不能延迟,但可以在子线程执行。
5.2、定义Application的style
<item name="Android:windowAnimationStyle">@style/activityAnim</item>
<!-- 使用style方式定义activity切换动画 -->
<style name="activityAnim">
<item name="Android:activityOpenEnterAnimation">@anim/slide_in_left</item>
<item name="Android:activityOpenExitAnimation">@anim/slide_in_left</item>
</style>
而在windowAnimationStyle中存在四种动画:
activityOpenEnterAnimation // 打开新的Activity并进入新的Activity展示的动画
activityOpenExitAnimation // 打开新的Activity并销毁之前的Activity展示的动画
activityCloseEnterAnimation //关闭当前Activity进入上一个Activity展示的动画
activityCloseExitAnimation // 关闭当前Activity时展示的动画
5.3、ActivityOptions
overridePendingTransition的方式比较生硬,方法也比较老旧了,不适用于MD风格,google提供了新的转场动画ActivityOptions,并提供了兼容包ActivityOptionsCompat。
六、启动过程如何获取组件的宽高
我们知道在onCreate和onResume里面直接获取到控件宽高为0,那有什么办法获取到控件的实际宽高?只要有onWindowFocusChanged、view.post、ViewTreeObserver三种方式获取。
获取控件宽高方法
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mButton = findViewById(R.id.bt);
//方法一
mButton.post(new Runnable() {
@Override
public void run() {
Log.i("MainActivity", "post:getWidth()=" + mButton.getWidth());
}
});
//方法二
mButton.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
Log.i("MainActivity", "getViewTreeObserver().addOnGlobalLayoutListener:getWidth()=" + mButton.getWidth());
}
});
}
//方法三
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
Log.i("MainActivity", "onWindowFocusChanged:getWidth()=" + mButton.getWidth());
}
2019-11-14 10:30:04.042 13346-13346/? I/MainActivity: onCreate:getWidth()=0
2019-11-14 10:30:04.250 13346-13346/? I/MainActivity: onResume:getWidth()=0
2019-11-14 10:30:04.293 13346-13346/? I/MainActivity: getViewTreeObserver().addOnGlobalLayoutListener:getWidth()=1320
2019-11-14 10:30:04.294 13346-13346/? I/MainActivity: post:getWidth()=1320
2019-11-14 10:30:04.395 13346-13346/? I/MainActivity: onWindowFocusChanged:getWidth()=1320
七、启动流程
当用户点击桌面图标启动APP时,背后的流程如下:
activity启动流程
我们看到的手机桌面是Launch程序的界面,点击应用图标会触发点击事件,调用startActivity(intent),然后通过Binder IPC机制,与ActivityManagerService(AMS)通讯,AMS执行一系列操作,最终启动目前应用,大概流程如下:
1、Launche程序请求AMS过程(Binder IPC机制)
MyActivity.startActivity()
Activity.startActivity()
Activity.startActivityForResult
Instrumentation.execStartActivty //Instrumentation是android系统中启动Activity的一个实际操作类
ActivityManagerNative.getDefault().startActivityAsUser() //getDefault返回ActivityManagerProxy,是AMS客户端代理(Binder机制)
2、AMS执行过程
ActivityManagerService.startActivity()
ActvityiManagerService.startActivityAsUser()
ActivityStackSupervisor.startActivityMayWait()
ActivityStackSupervisor.startActivityLocked()
ActivityStackSupervisor.startActivityUncheckedLocked()
ActivityStackSupervisor.startActivityLocked()
ActivityStackSupervisor.resumeTopActivitiesLocked()
ActivityStackSupervisor.resumeTopActivityInnerLocked()
ActivityStackSupervisor.startSpecificActvityLocked()
if process exits ActivityStackSupervisor.realStartAcitvityLocked() //进程存在,则直接启动Activity
else create process ActivityStackSupervisor.startProcessLocked() //创建新进程
2.1、判断启动权限
通过PackageManager的resolveIntent()收集跳转intent对象的指向信息,然后通过grantUriPermissionLocked()方法来验证用户是否有足够的权限去调用该intent对象指向的Activity。如果有权限,则在新的task中启动目标activity,如果发现没有进程,则先创建进程。
2.2、创建进程
如果进程不存在,AMS会调用startProcessLocked创建新的进程,在该方法中,会通过socket的通讯方式通知zygote进程孵化新的进程并返回pid,在新的进程中会初始化ActivityThread,并依次调用Looper.prepareLoop()和Looper.loop()来开启消息循环。
2.3、绑定Application
创建好进程后下一步要将Application和进程绑定起来,AMS会调用上一节创建的ActivityThread对象的bindAppliction方法完成绑定工作,该方法会发送一条BIND_APPLICATION的消息,最终会调用handleBindApplication方法处理消息,并调用makeApplication方法处理消息,加载APP的classes到内存中。
3、ActivityThread启动Activity
通过前面的步骤,系统已经拥有了该Application的进程,后续的启动则是从已存在其他进程中启动Acitivity,即调用realStartAcitvityLocked,该方法会调用Application的主线程对象ActivityThread的sheduleLaunchActivity方法,在方法中会发送LAUNCH_ACTIVITY到消息队列,最终通过handleLaunchActivity处理消息,完成Acitivty的启动。
参考
Activity
Activity 的 36 大难点,你会几个?「建议收藏」
[译]Android Application启动流程分析