Activity的启动模式(android:launchMode)
-
standard 标准模式
系统默认模式,每次启动ACtivity,不管该Activity的实例是否存在,都会创建一个新的实例。该Activity与启动它的Activity属于同一个任务栈。
注意:applicationContext不可启动standard模式的Activity。因为非Activity的context类型没有任务栈,那么如果要启动该模式的Activity,那该Activity无法进入任务栈,那么该Activity就不属于任何任务栈。
-
singleTop
栈顶复用模式。如果将要启动的Activity处于任务栈的顶部,那么该Activity会直接复用栈顶的实例。因为该Activity的实例已经存在,那么便不会执行onCreate和onStart,但会调用onNewIntent方法。在onNewIntent中可以取出这次请求的信息。但是如果该Activity不在任务栈的顶部,则会新建该Activity的实例。
-
singleTask
栈内复用模式。如果即将启动的Activity在某个任务栈中存在,则直接复用该Activity实例。再次过程中,只会执行onNewIntnent。具体而言,当存在该Activity所需要的任务栈,那么在该任务栈中查找该Activity的实例,如果查找到则直接复用,否则新建该Activity的实例并入栈。如果不存在该Activity的任务栈,则新建一个任务栈,然后新建Activity实例,并入栈。
**注意:**当栈内复用时,那么将会把栈内该Activity之上的所有实例出栈。
-
singleInstance
栈内单例模式。如果某个Activity为该模式,那么该Activity只能单独的存在于某一个任务栈中,其他特征与singleTask相同。
注意: 上述任务栈是由taskAffinity属性来判定的,默认情况下所有的Activity的taskAffinity属性都是应用包名。如果想指定某个Activity所在的任务栈,修改该属性即可。该属性仅仅在于singleTask或allowTaskReparenting配合使用时才有意义。
附:FLAF vs android:launchMode
FLAG_ACTIVITY_NEW_TASK:singleTask
FLAG_ACTIVITY_CLEAR_TOP: 如果该Activity在当前任务栈中存在实体,那么销毁栈中在其之上的Activity实例,然后创建一个新的实例添加到栈顶。
FLAG_ACTIVITY_SINGLE_TOP: singleTop
总结:使用FLAG则不支持singleInstance,使用android:launchMode则不支持CLEAR_TOP
Task字段说明
-
Android:allowTaskReparenting
该属性用来标记当Activity退居后台之后,是否能从启动它的Activity所在的任务栈移动到与它有相同taskAffinity的任务栈。eg:在应用中页面A启动了浏览器,那么此时打开的浏览器页面B与A是属于同一个任务栈的,当B退居后台之后,B就会移动到浏览器所在的任务栈中,并且处于该栈的顶部,所以当再次打开浏览器的时候,显示的是页面B。
-
android:allowRetainTaskState
该属性用来标记是否能够保持原有的状态,但是该属性仅仅只对根Activity起作用(所谓根Activity一般指app的主页面)。
-
android:clearTaskOnLaunch
如果该属性为true,那么当启动该Activity时,便会清除该Activity所在任务栈的其他所有Activity。
-
android:finishOnTaskLaunch
如果设置该属性为true,那么点击home键回到屏幕之后,再次点击应用图标进入引用之后,系统自动销毁该Activity。
-
android:alwaysRetainTaskState
如果当前任务栈的根Activity的该属性设置为true,在该任务栈stop之后仍然保持该任务栈中所有Activity的状态。
intent和intent-filter
intent是一个消息传递对象,用来促进组件之间的通信,基本用例主要有:
- 启动Activity:
- 启动Service
- 传递Broadcast
intent主要分为两类:
- 显式intent:按类名来显示指定要启动的组件。一般情况下,我们仅在自己的应用程序中使用显式intent,因为仅在自己的应用程序中我们才知道要启动组件的完整类名。
- 隐式intent:不会指定特定的组件,而是说明要执行的操作,系统查找可以执行此操作的组件并启动它。通常情况下,应用程序中通过隐式intent来提供全局通用服务,比如阅读pdf文件。
警告:为了确保应用的安全性,启动 Service 时,请始终使用显式 Intent,且不要为服务声明 Intent 过滤器。使用隐式 Intent 启动服务存在安全隐患,因为您无法确定哪些服务将响应 Intent,且用户无法看到哪些服务已启动。从 Android 5.0(API 级别 21)开始,如果使用隐式 Intent 调用 bindService(),系统会抛出异常。
不管是显式intent还是隐式intent,intent中都包含了启动组建的信息,包括以下几种:
- 组件名称:可选项,在显式intent中必须制定该项。换言之,如果在intent中没有说明目标组件名称,那么该intent就是隐式intent。如果是隐式intent,那么系统将根据intent的其他信息来判断启动哪个组件。
- ACTION:字符串类型,说明目标组件所要执行的操作。系统提供了一些通用的操作: * ACTION_VIEW:目标组件会提供一些“查看服务”,例如查看图库、查看位置等。 * ACTION_SEND:也叫作“共享操作”,所以我们清楚地知道:如果目标组件可接收其他应用程序的共享数据,则将目标组件的ACTION设置为ACTION_SEND
- Data:引用带操组的数据和/或该数据的MIME类型。提供的数据类型通常由ACTION来确定,例如ACTION为ACTION_EDIT,那么数据应该是待编辑的文档的uri。 * 创建 Intent 时,除了指定 URI 以外,指定数据类型(其 MIME 类型)往往也很重要。例如,能够显示图像的Activity可能无法播放音频文件,即便 URI 格式十分类似时也是如此。因此,指定数据的 MIME 类型有助于 Android 系统找到接收 Intent 的最佳组件。但有时,MIME 类型可以从 URI 中推断得出,特别当数据是 content: URI 时尤其如此。这表明数据位于设备中,且由 ContentProvider 控制,这使得数据 MIME 类型对系统可见。要仅设置数据 URI,请调用 setData()。要仅设置 MIME 类型,请调用 setType()。如有必要,您可以使用 setDataAndType() 同时显式设置二者。
- 警告:若要同时设置 URI 和 MIME 类型,请勿调用 setData() 和 setType(),因为它们会互相抵消彼此的值。请始终使用 setDataAndType() 同时设置 URI 和 MIME 类型。
- Category:指定intent的类别,一般情况下所有的Activity默认为CATEGORY_DEFAULT,不需要指定。如果某个组件要支持隐式intent,则必须添加一个category(如果不支持其他特殊的category,例如CATEGORY_BROWSABLE,那么必须添加CATEGORY_DEFAULT),否则不起作用。
以上四类intent属性表示了intent的既定特征,通过这些特征,系统就可以分标出要启动的目标组件。intent还有一些不影响其如何解析的属性,如下:
- Extra:携带完成请求操作所需的附加信息的键值对。正如某些操作使用特定类型的数据 URI 一样,有些操作也使用特定的附加数据。 您可以使用各种 putExtra() 方法添加附加数据,每种方法均接受两个参数:键名和值。您还可以创建一个包含所有附加数据的 Bundle 对象,然后使用 putExtras() 将 Bundle 插入 Intent 中。 例如,使用 ACTION_SEND 创建用于发送电子邮件的 Intent 时,可以使用 EXTRA_EMAIL 键指定“目标”收件人,并使用 EXTRA_SUBJECT 键指定“主题”。Intent 类将为标准化的数据类型指定多个 EXTRA_* 常量。如需声明自己的附加数据 键(对于应用接收的 Intent ),请确保将应用的软件包名称作为前缀。例如:
```
static final String EXTRA_GIGAWATTS = "com.example.EXTRA_GIGAWATTS";
```
复制代码
- Flag:标志可以指示 Android 系统如何启动 Activity
显式intent示例
例如,如果在应用中构建了一个名为 DownloadService、旨在从 Web 中下载文件的服务,则可使用以下代码启动该服务:
// Executed in an Activity, so 'this' is the Context
// The fileUrl is a string URL, such as "http://www.example.com/image.png"
Intent downloadIntent = new Intent(this, DownloadService.class);
downloadIntent.setData(Uri.parse(fileUrl));
startService(downloadIntent);
复制代码
隐式intent示例
警告:用户可能没有任何应用处理您发送到 startActivity() 的隐式 Intent。如果出现这种情况,则调用将会失败,且应用会崩溃。要验证 Activity 是否会接收 Intent,请对 Intent 对象调用 resolveActivity()。如果结果为非空,则至少有一个应用能够处理该 Intent,且可以安全调用 startActivity()。如果结果为空,则不应使用该 Intent。如有可能,您应禁用发出该 Intent 的功能。
PendingIntent
pendingIntent的目的是授权外部应用使用pendingIntent中包含的intent。 用例场景:通知、桌面小组件、Alarm
Activity的各个方法及调用时机
生命周期
onPause() 与 onResume()
在正常使用应用的过程中,前台Activity有时会被因为某些原因导致Activity暂停的可视组件阻挡。 例如,当半透明Activity打开时(比如对话框样式中的Activity),上一个Activity会暂停。 只要Activity仍然部分可见但目前又未处于焦点之中,它会一直暂停。但是,一旦Activity完全被阻挡并且不可见,它便停止,进入onStop()。
当Activity进入暂停状态时,系统会对Activity调用onPause()方法,通过该方法,您可以停止不应在暂停状态下继续进行的操作(比如视频)或保留任何应该永久保存的信息,以防用户离开应用。如果用户从暂停状态返回到Activity,系统会重新开始该Activity并调用 onResume() 方法。
当系统为Activity调用 onPause() 时,它从技术角度看意味着Activity仍然处于部分可见状态,但往往说明用户即将离开Activity并且它很快就要进入“停止”状态。 您通常应使用 onPause() 回调:
- 停止动画或其他可能消耗 CPU 的进行之中的操作。
- 提交未保存的更改,但仅当用户离开时希望永久性保存此类更改(比如电子邮件草稿)。
- 释放系统资源,比如广播接收器、传感器手柄(比如 GPS) 或当您的Activity暂停且用户不需要它们时仍然可能影响电池寿命的任何其他资源。
例如,如果您的应用使用 Camera, onPause() 方法是释放它的好位置。但您应避免在 onPause() 期间执行 CPU 密集型工作,比如向数据库写入信息,因为这会拖慢向下一Activity过渡的过程(您应改为在 onStop()期间执行高负载关机操作。 在此说明一下,当从ActivityA切换到ActivityB时,两个Activity各个方法的执行过程为:
- Activity A 的 onPause() 方法执行。
- Activity B 的 onCreate()、onStart() 和 onResume() 方法依次执行。(Activity B 现在具有用户焦点。)
- 然后,如果 Activity A 在屏幕上不再可见,则其 onStop() 方法执行。
当用户从“暂停”状态继续您的Activity时,系统会调用 onResume() 方法。
请注意,每当您的Activity进入前台时系统便会调用此方法,包括它初次创建之时。 同样地,您应实现onResume() 初始化您在 onPause() 期间释放的组件并且执行每当Activity进入“继续”状态时必须进行的任何其他初始化操作(比如开始动画和初始化只在Activity具有用户焦点时使用的组件)。
onResume() 的以下示例对应于以上的 onPause() 示例,因此它初始化Activity暂停时释放的照相机。
onStop() 与 onStart() 与 onRestart();
正确停止和重新开始Activity是Activity生命周期中的重要过程,其可确保您的用户知晓应用始终保持Activity状态并且不会丢失进度。有几种Activity停止和重新开始的关键场景:
- 用户打开“最近应用”窗口并从您的应用切换到另一个应用。当前位于前台的您的应用中的Activity将停止。 如果用户从主屏幕启动器图标或“最近应用”窗口返回到您的应用,Activity会重新开始。
- 用户切换到新的Activity。当第二个Activity创建好后,当前Activity便停止。 如果用户之后按了返回按钮,第一个Activity会重新开始。
- 用户在其手机上使用您的应用的同时接听来电。
当Activity调用到onStop()时,它不在可见,并且应该释放此时所有不需要的资源。一旦Activity处于停止状态,那么它可能会因为需要恢复系统内存还被销毁。 在极端情况下,系统可能会仅终止应用进程,而不会调用Activity的最终 onDestroy() 回调,因此您使用 onStop() 释放可能泄露内存的资源非常重要。
尽管 onPause() 方法在 onStop()之前调用,您应使用 onStop() 执行更大、占用更多 CPU 的关闭操作,比如向数据库写入信息。
Fragment的各个方法及调用时机
FragmentManager
FragmentPagerAdapter vs FragmentStatePagerAdapter
相信大家都使用过viewPager+PagerAdapter的方法来进行页面切换,其中PageAdapter提供page,viewPager显示page。当page的内容发生变化时,PagerAdapter将此变化通知给ViewPager,也就是说PagerAdapter负责创建Page、管理page,ViewPager负责显示page。但是因为各种各样的需求,要求PagerAdapter提供对page不同的管理方式,比如当page较少时,从page1切换到page2,再切换到page3时,要求不得将page1~page2销毁,这样当用户返回page1时,page1的内容会立马展示给用户;当page较多时,考虑到android系统对单一应用内存的限制,就不可能将所有的page保存起来,接下来我们详细了解一下系统为我们提供的各种PagerAdapter。
PagerAdapter
该类是page最简单的管理类,如果继承自该类,至少需要实现 instantiateItem(), destroyItem(), getCount() 以及 isViewFromObject()。
- getItemPosition() * 该函数用以返回给定对象的位置,给定对象是由 instantiateItem() 的返回值 在 ViewPager.dataSetChanged() 中将对该函数的返回值进行判断,以决定是否最终触发 PagerAdapter.instantiateItem() 函数。 在 PagerAdapter 中的实现是直接传回 POSITION_UNCHANGED。如果该函数不被重载,则会一直返回 POSITION_UNCHANGED,从而导致 ViewPager.dataSetChanged() 被调用时,认为不必触发 PagerAdapter.instantiateItem()。很多人因为没有重载该函数,而导致调用 PagerAdapter.notifyDataSetChanged() 后,什么都没有发生。
- public Object instantiateItem(View container, int position) * 在每次 ViewPager 需要一个用以显示的 Object 的时候,该函数都会被 ViewPager.addNewItem() 调用。
- public void notifyDataSetChanged() * 在数据集发生变化的时候,一般 Activity 会调用 PagerAdapter.notifyDataSetChanged(),以通知 PagerAdapter,而 PagerAdapter 则会通知在自己这里注册过的所有 DataSetObserver。其中之一就是在 ViewPager.setAdapter() 中注册过的 PageObserver。PageObserver 则进而调用 ViewPager.dataSetChanged(),从而导致 ViewPager 开始触发更新其内含 View 的操作。
FragmentPagerAdapter
FragmentPagerAdapter 继承自 PagerAdapter。相比通用的 PagerAdapter,该类更专注于每一页均为 Fragment 的情况。如文档所述,该类内的每一个生成的 Fragment 都将保存在内存之中,因此适用于那些相对静态的页,数量也比较少的那种;如果需要处理有很多页,并且数据动态性较大、占用内存较多的情况,应该使用FragmentStatePagerAdapter。FragmentPagerAdapter 重载实现了几个必须的函数,因此来自 PagerAdapter 的函数,我们只需要实现 getCount(),即可。且,由于 FragmentPagerAdapter.instantiateItem() 的实现中,调用了一个新增的虚函数 getItem(),因此,我们还至少需要实现一个 getItem()。因此,总体上来说,相对于继承自 PagerAdapter,更方便一些。
- getItem() * 该类中新增的一个虚函数。函数的目的为生成新的 Fragment 对象。重载该函数时需要注意这一点。在需要时,该函数将被 instantiateItem() 所调用。如果需要向 Fragment 对象传递相对静态的数据时,我们一般通过 Fragment.setArguments() 来进行,这部分代码应当放到 getItem()。它们只会在新生成 Fragment 对象时执行一遍。如果需要在生成 Fragment 对象后,将数据集里面一些动态的数据传递给该 Fragment,那么,这部分代码不适合放到 getItem() 中。因为当数据集发生变化时,往往对应的 Fragment 已经生成,如果传递数据部分代码放到了 getItem() 中,这部分代码将不会被调用。这也是为什么很多人发现调用 PagerAdapter.notifyDataSetChanged() 后,getItem() 没有被调用的一个原因。
- instantiateItem() * 函数中判断一下要生成的 Fragment 是否已经生成过了,如果生成过了,就使用旧的,旧的将被 Fragment.attach();如果没有,就调用 getItem() 生成一个新的,新的对象将被 FragmentTransation.add()。
FragmentPagerAdapter 会将所有生成的 Fragment 对象通过 FragmentManager 保存起来备用,以后需要该 Fragment 时,都会从 FragmentManager 读取,而不会再次调用 getItem() 方法。 如果需要在生成 Fragment 对象后,将数据集中的一些数据传递给该 Fragment,这部分代码应该放到这个函数的重载里。在我们继承的子类中,重载该函数,并调用 FragmentPagerAdapter.instantiateItem() 取得该函数返回 Fragment 对象,然后,我们该 Fragment 对象中对应的方法,将数据传递过去,然后返回该对象。 否则,如果将这部分传递数据的代码放到 getItem()中,在 PagerAdapter.notifyDataSetChanged() 后,这部分数据设置代码将不会被调用。
- destroyItem() * 该函数被调用后,会对 Fragment 进行 FragmentTransaction.detach()。这里不是 remove(),只是 detach(),因此 Fragment 还在 FragmentManager 管理中,Fragment 所占用的资源不会被释放。
FragmentStatePagerAdapter
FragmentStatePagerAdapter 和前面的 FragmentPagerAdapter 一样,是继承子 PagerAdapter。但是,和 FragmentPagerAdapter 不一样的是,正如其类名中的 'State' 所表明的含义一样,该 PagerAdapter 的实现将只保留当前页面,当页面离开视线后,就会被消除,释放其资源;而在页面需要显示时,生成新的页面(就像 ListView 的实现一样)。这么实现的好处就是当拥有大量的页面时,不必在内存中占用大量的内存。
- getItem() * 一个该类中新增的虚函数。函数的目的为生成新的 Fragment 对象。Fragment.setArguments() 这种只会在新建 Fragment 时执行一次的参数传递代码,可以放在这里。由于 FragmentStatePagerAdapter.instantiateItem() 在大多数情况下,都将调用 getItem() 来生成新的对象,因此如果在该函数中放置与数据集相关的 setter 代码,基本上都可以在 instantiateItem() 被调用时执行,但这和设计意图不符。毕竟还有部分可能是不会调用 getItem() 的。因此这部分代码应该放到 instantiateItem() 中。
- instantiateItem() * 除非碰到 FragmentManager 刚好从 SavedState 中恢复了对应的 Fragment 的情况外,该函数将会调用 getItem() 函数,生成新的 Fragment 对象。新的对象将被 FragmentTransaction.add()。FragmentStatePagerAdapter 就是通过这种方式,每次都创建一个新的 Fragment,而在不用后就立刻释放其资源,来达到节省内存占用的目的的。
- destroyItem() * 将 Fragment 移除,即调用 FragmentTransaction.remove(),并释放其资源。
Activity, ViewPager, Fragment实现懒加载
系统配置变化导致的Activity变化
1. Device Configurations
Orientation, Keyboard, Language.
2. 原理简介及解决方案
一般情况下,当Device Configuration 在Application运行时发生变化,那么系统会自动重启该Activity(此时先onSaveInstance保存数据,然后执行onDestroy,最后执行onCreate)。
所以我们必须在Activity销毁之前使用onSaveInsance保存数据,在onCreate或者onRestoreInsanceState中回复数据,以此来提供良好的用户体验。但是有时,我们需要保存大量的数据,遇到这种情况一般有两种解决方案:
a. 引用对象
由于在回调onSaveInstanceState保存的数据不适合保存大批量的数据对象(例如bitmap),而且保存的数据对象必须是Serialized的。这种情况下,当系统配置发送变化时,我们通过引用Fragment来保存数据,在fragment中保存数据对象。具体实现如下:
//注意:当保存数据时,千万不要保存任何引用Activity实例的对象,否则会造成内存泄漏
复制代码
public class RetainedFragment extends Fragment {
// data object we want to retain
private MyDataObject data;
// this method is only called once for this fragment
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// retain this fragment
setRetainInstance(true);**重点内容**
}
public void setData(MyDataObject data) {
this.data = data;
}
public MyDataObject getData() {
return data;
}
}
复制代码
然后使用FragmentManager将fragment添加到Activity中。
public class MyActivity extends Activity {
private RetainedFragment dataFragment;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
// find the retained fragment on activity restarts
FragmentManager fm = getFragmentManager();
dataFragment = (DataFragment) fm.findFragmentByTag(“data”);
// create the fragment and data the first time
if (dataFragment == null) {
// add the fragment
dataFragment = new DataFragment();
fm.beginTransaction().add(dataFragment, “data”).commit();
// load the data from the web
dataFragment.setData(loadMyData());
}
// the data is available in dataFragment.getData()
...
}
@Override
public void onDestroy() {
super.onDestroy();
// store the data in the fragment
dataFragment.setData(collectMyLoadedData());
}
}
复制代码
b. 自己处理系统配置变化引起的改变
当系统配置发生变化时,如果Activity不需要更新数据或自动应用资源,那么可以声明自己处理该配置变化。
首先,在AndroidManifest中作如下声明:
<activity android:name=".MyActivity"
<-- 在此处定义自己要处理的变化-->
android:configChanges="orientation|keyboardHidden"
android:label="@string/app_name">
复制代码
当在xml中声明的任意配置发生变化时,系统不会自动重启该Activity,此时系统将回调onConfigurationChanged()。
注意: 当App的targetSdkVersion大于等于13,如果您想处理屏幕方向切换配置变化,那么你必须在android:configuration中包含screenSize属性。
Activity启动与加载过程
当用户启动Activity时,Instrumentation会接收该请求,然后instrumentation利用Binder向ActivityManagerService发请求。ActivityManagerService内部维护者Activity的调用堆栈(ActivityStack)及各个Activity的状态同步,ActivityManagerService通过ActivityThread去管理Activity的状态从而完成Activity的生命周期的管理。