Android开发Android开发经验谈

Android之Activity全面解析,有些知识点容易忘记

2019-11-06  本文已影响0人  码农翻身记

目录

目录(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启动流程分析

上一篇下一篇

猜你喜欢

热点阅读