Android四大组件之一——Activity

Activity是Android四大组件中最重要的一个,相关知识也错综复杂,此篇记录学习及工作中Activity的相关知识。

Activity生命周期

典型情况下的生命周期

说到Activity,就不得不说其生命周期。说到生命周期,就更不得不放上一张刚开始接触Android时总是能碰到的一张图,此图清楚的描述了大多数情况下的Activity生命周期。

  1. onCreate表示Activity正在被创建,是生命周期的第一个方法,可以做些初始化,包括数据的初始化及布局界面的初始化。
  2. onStart表示Activity正在被启动,此时Activity已经可见,但是并没有出现在前台,也就还无法进行交互。
  3. onRestart表示Activity正在被重新启动,当当前Activity从不可见到可见的时会被调用,包括但不限于切换到桌面后再重新进入Activity或进入另一个Activity后又返回。
  4. onResume表示Activity已经出现在前台且已经可以进行交互。
  5. onPause表示Activity正在停止,此方法可以做些数据存储、动画停止等操作,但不能是耗时操作,否则会影响新的Activity的显示,因为只有此Activity执行完毕onPause后才会执行新的Activity的创建显示等一系列操作。
  6. onStop表示Activity即将停止,同样可以做些回收操作,同样也不能太耗时。
  7. onDestroy表示Activity即将被销毁,是Activity生命周期的最后一个方法回调。可以做些回收操作及资源的释放,如Handler延时任务清空,线程销毁,持续动画的停止销毁等,注意持有此Activity的各种情况,处理不当会导致Activity因为被引用而不能被回收的内存泄漏问题。
  8. 补充:onStart和onResume、onPause和onStop的实质不同就是不以同一个角度来回调,onStart和onStop是以是否可见的角度,onResume和onPause是以是否当前Activity位于前台的角度。

除上面的正常的Activity流程外,还有以下常用的回调方法:

  1. onSaveInstanceState(Bundle)
  2. onRestoreInstanceState(Bundle)
  3. 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优先级排序

    1. 前台Activity,正在和用户进行交互的Activity,优先级最高。
    2. 可见但非前台Activity,比如Activity中弹出一个Dialog,导致Activity可见,但是处于后台无法和用户交互。
    3. 后台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时的数据传递

    执行onNewIntent时的数据传递

    启动时没携带数据,再次启动时携带了一个字符串,因为lunchMode是singleTask,所以在**onNewIntent()方法中可以看到是qwer,但是之后的onResume()方法中通过getIntent()方法却获取不到数据,这是因为Activity持有的Intent还是首次启动时的Intent,正确的获取方法应该是先在onNewIntent()方法中执行setIntent(Intent)**方法。

    执行onNewIntent时的数据传递

    这才是正确的获取方法。

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。

    不在
    启动SingleTop A
    A是否在栈顶
    onNewIntent方法
    按standrad方式启动A
  • singleTask 栈内复用模式

    只要Activity在一个栈中存在,那么多次启动此Activity都不会重新创建实例,和singleTop一样,系统会回调**onNewIntent()**方法。

    当在启动singTask Activity时会先寻找Activity所需的栈,如果有则寻找栈内是否有目标Activity,若有则清除目标Activity之上的所有Activity,显示目标Activity并执行onNewIntent方法,若没有目标Activity,则直接创建Activity。没有所需的栈就直接创建栈后创建目标Activity。

    启动SingleTask A
    是否有目标栈
    是否有目标Activity
    创建目标栈
    是否在栈顶
    创建Activity后压栈
    执行onNewIntent方法
    执行ClearTop清除栈顶

    举例:

    1. 任务栈A1中有ABC,现在启动需要A2任务栈的D,那么会创建A2栈后再创建D压入A2栈中。此时存在两个栈A1和A2,分别有ABC和D。
    2. 与情形1同样的条件,但是D所需的任务栈也为A1,那么此时就直接创建D并压入A1。
    3. D所需的任务栈为A1,此时A1中有ABDC,此时D不会重新创建,而是将C出栈,并调用onNewIntent方法显示D。
  • singleInstance 单实例模式

    加强型singleTask,除具有singleTask所有的特性外,还有一个特殊点是此模式的Activity只能单独的存在于一个任务栈中。如果从此模式的Activity中启动其他模式的Activity那么不会在此压入栈中。

  • 其他补充

    1. 任务栈: Activity默认任务栈就是以包名命名的,默认情况下,Activity所需的任务栈也就是此任务栈,除非手动指定Activity标签下的TaskAffinity属性。

    2. TaskAffinity: 主要和SingleTask活着AllowTaskReParenting属性配对使用。另外任务栈分为前台任务栈和后台任务栈,后台任务栈中的Activity处于暂停状态,可以通过切换后台任务栈调到前台。

    3. TaskAffinity和SingleTask配对使用: 待启动的Activity会运行在名字和TaskAffinity相同的任务栈中。

    4. 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的话会直接以包名命名。

    5. 设置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.actionImplicitAction,在配置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表示路径的前缀。

分情况补充说明:

  1. <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授予临时权限。

  2. <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");
    
  3. 还有一个特殊情况,与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

判断的方法有两种:

  1. 用PackageManager的resolveActivity方法或者Intent的resolveActivity方法,如果找不到会返回null。

    public abstract ResolveInfo resolveActivity(Intent intent, @ResolveInfoFlags int flags);
    
  2. 用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或者默认即可

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值