前言

在 Android 中,ViewModel 的作用就是在 UI 控制器( 如 Activity、Fragment)的生命周期中保存和管理 UI 相关的数据。ViewModel 保存的数据在配置更改(如屏幕旋转)后会依然存在,不会丢失。

在屏幕旋转的时候,Activity 会重建,为了不让数据丢失,我们通常的做法是在 onSaveInstanceState() 方法中通过 bundle 保存数据,然后在 onCreate()onRestoreInstanceState() 方法中取出 bundle 来恢复数据。然而,这种方式有一定的局限性,它只适用于可序列化然后反序列化的少量数据,对于 Bitmap 等比较大的数据就不适用了。

另一方面,UI 控制器通常需要做一些耗时的异步调用操作,并且需要去管理这些调用。UI 控制器需要确保系统在销毁后去清理掉这些异步调用,以避免潜在的内存泄漏,这种管理方式需要大量的维护工作。而且,在配置更改后重建对象是很浪费资源的,因为该对象可能必须重新发出之前已经发出过的调用。

UI 控制器一般只负责显示和处理用户操作,加载数据库数据或网络数据的工作应该委托给其它类,这样会让测试工作更加容易地进行。因此,将视图数据相关操作从 UI 控制器逻辑中分离出来是很有必要。

ViewModel 使用

比如,一个 ViewModelActivity 需要展示一个 User 的列表数据,那么可以定义一个 UserViewModel 来获取数据,然后在 ViewModelActivity 中创建一个 UserViewModel 对象来获取到 User 的列表数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class UserViewModel : ViewModel() {

private lateinit var users: MutableLiveData<List<User>>

fun getUsers(): LiveData<List<User>> {
if (!::users.isInitialized) {
users = MutableLiveData()
loadUsers()
}
return users
}

private fun loadUsers() {
// Do an asynchronous operation to fetch users .
Thread(Runnable {
Thread.sleep(3000)
// 在子线程发送值用 postValue , 否则用 setValue .
users.postValue(listOf(User("1", "AA"), User("2", "BB")))
}).start()
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class ViewModelActivity : AppCompatActivity() {

private val TAG = "ViewModelActivity"

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_view_model)

// 就算配置更改(如屏幕旋转)了,获取到的 userViewModel 对象还会是上一次的 UserViewModel 对象
val userViewModel = ViewModelProviders.of(this).get(UserViewModel::class.java)

// 这里的 this 需要用实现了 LifecycleOwner 的类的 this . 如 AppCompatActivity、FragmentActivity
userViewModel.getUsers().observe(this, Observer {
Log.e(TAG, it.toString())
// 打印结果:[User(id=1, name=AA), User(id=2, name=BB)]
})
}
}

查看源码可知,ViewModelProviders.of(this) 获取了一个全新的 ViewModelProvider 对象,

1
2
3
4
5
6
7
8
public static ViewModelProvider of(@NonNull FragmentActivity activity,
@Nullable Factory factory) {
Application application = checkApplication(activity);
if (factory == null) {
factory = ViewModelProvider.AndroidViewModelFactory.getInstance(application);
}
return new ViewModelProvider(ViewModelStores.of(activity), factory);
}

ViewModelProvider 对象调用 get() 方法获取到我们需要的 ViewModel 对象。追踪一下 get() 方法可以知道,ViewModel 对象是存储在一个 ViewModelStore 类的对象中的,该类里面使用 HashMap 来保存和获取 ViewModel .

1
ViewModel viewModel = mViewModelStore.get(key);

获取 ViewModel 使用的 key 相对具体的 ViewModel 类是不会变化的,因此从 ViewModelStore 中取出的 ViewModel 对象也不会变。包括在配置更改后也可以获取到之前的 ViewModel .

当宿主 Activity 调用了 finish() 方法,系统会调用 ViewModel 对象的 onCleared() 方法来让它清理掉资源,到这里之后 ViewModel 才会被释放掉。

ViewModel 里面不要引用 View、或者任何持有 Activity 类的 context , 否则会引发内存泄漏问题。

当 ViewModel 需要 Application 类的 context 来获取资源、查找系统服务等,可以继承 AndroidViewModel 类。

1
2
3
4
5
6
7
8
9
10
11
12
13
class MyAndroidViewModel(application: Application) : AndroidViewModel(application) {

private val app
get() = getApplication<Application>()

fun getStatus(code: Int): String {
return when (code) {
1 -> app.resources.getString(R.string.be_late) // 迟到
2 -> app.resources.getString(R.string.leave_early) // 早退
else -> app.resources.getString(R.string.absenteeism) // 旷工
}
}
}
1
2
3
val myAndroidViewModel = ViewModelProviders.of(this).get(MyAndroidViewModel::class.java)
Log.e(TAG, myAndroidViewModel.getStatus(2))
// 打印结果:早退

ViewModel 的生命周期

ViewModel 会一直保留在内存中,直到 Activity / Fragment 在以下情况下才会销毁:

  • 宿主 Activity 被 finish 后调用 onDestroy 方法。
  • 宿主 Fragment 被 detached 后调用 onDetach 方法。

下图展示了一个 Activity 经历了旋转然后调用 finish 的各种生命周期状态,同时展示了关联了该 Activity 的 ViewModel 的生命周期。(UI 控制器是 Fragment 的情况也类似。)

Fragment 之间共享数据

假设我们有这样的需求:在一个 MasterFragment 中有一个 User 列表,点击列表项后将点中的 User 对象传递给 DetailFragment 用于展示详细的 User 信息。

我们一般的做法是:在两个 Fragment 中定义一些通信接口,并且宿主 Activity 需要把它们绑定起来,这样做相当繁琐。并且两个 Fragment 还需要处理另外的 Fragment 尚未创建或者可见的场景。

为了避免以上繁琐的做法,我们可以通过两个 Fragment 之间共享一个 ViewModel 的方式来实现数据通信。

1
2
3
4
5
6
7
8
class SharedViewModel : ViewModel() {

val selected = MutableLiveData<User>()

fun select(user: User) {
selected.value = user
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class MasterFragment : Fragment() {

private val dataList = listOf(User("1", "张三"), User("2", "李四"), User("3", "王五"))

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_master, container, false)
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

var model = activity?.run {
ViewModelProviders.of(this).get(SharedViewModel::class.java)
} ?: throw Exception("Invalid Activity")

lvMaster.adapter = ArrayAdapter<User>(
activity,
android.R.layout.simple_expandable_list_item_1,
dataList)

lvMaster.setOnItemClickListener { _, _, position, _ ->
model.select(dataList[position])
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class DetailFragment : Fragment() {

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_detail, container, false)
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

var model: SharedViewModel = activity?.run {
ViewModelProviders.of(this).get(SharedViewModel::class.java)
} ?: throw Exception("Invalid Activity")

model.selected.observe(this, Observer<User> { item ->
tvDetail.setText("${item?.id} : ${item?.name}")
})
}
}

需要特别注意,两个 Fragment 都需要使用它们的宿主 Activty 的 this 来获取 ViewModelProviders , 这样才确保它们获取到的是同一个 ViewModel 对象。

这种数据通信的方式有以下几个好处:

  • 宿主 Activity 不需要做任何的事情,也完全不知道 Fragment 之间的通信;
  • 一个 Fragment 不需要知道另一个 Fragment 中除了 ViewModel 契约之外的其它事情,哪怕另一个 Fragment 消失了,它也继续保持正常工作;
  • 每个 Fragment 都有自己的生命周期,它们之间互不影响,哪怕某一个 Fragment 被其它 Fragment 替换了,UI 还是会继续工作,没有任何问题。

文中 Demo GitHub 地址

函数定义与使用

run()

定义

1
2
3
4
5
6
7
8
9
10
/**
* Calls the specified function [block] and returns its result.
*/
@kotlin.internal.InlineOnly
public inline fun <R> run(block: () -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block()
}

示例

1
2
3
4
5
6
7
8
fun runTest1() {
var name = "AA"
run {
val name = "BB"
Log.e(TAG, name) // BB
}
Log.d(TAG, name) // AA
}

run() 函数在 runTest1 函数中又提供了自己的作用域,并且 run() 函数中可以重新定义一个 name 变量,该变量只存在于 run() 函数中。以下介绍的几个函数和 run() 函数同理,都是提供了自己的作用域。

1
2
3
4
5
6
7
8
9
10
11
fun runTest2() {
var success = true
var result = run {
if (success) {
"200"
} else {
"404"
}
}
Log.d(TAG, result) // 200
}

run() 返回作用域中的最后一个对象

T.run()

定义

1
2
3
4
5
6
7
8
9
10
/**
* Calls the specified function [block] with `this` value as its receiver and returns its result.
*/
@kotlin.internal.InlineOnly
public inline fun <T, R> T.run(block: T.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block()
}

示例

1
2
3
4
5
6
7
fun tRunTest() {
val result = "ABCDEF".run {
Log.d(TAG, "字符串的长度为$length") // 字符串的长度为6
substring(2)
}
Log.d(TAG, result) // CDEF
}

T.run() 中通过 this 来获取 “ABCDEF” 对象,然后输出 length . T.run() 返回作用域中的最后一个对象

with()

定义

1
2
3
4
5
6
7
8
9
10
/**
* Calls the specified function [block] with the given [receiver] as its receiver and returns its result.
*/
@kotlin.internal.InlineOnly
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return receiver.block()
}

示例

1
2
3
4
5
6
fun withTest() {
val result = with("ABCDEF") {
substring(2)
}
Log.d(TAG, result) // CDEF
}

with() 返回作用域中的最后一个对象

T.let()

定义

1
2
3
4
5
6
7
8
9
10
/**
* Calls the specified function [block] with `this` value as its argument and returns its result.
*/
@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block(this)
}

示例

1
2
3
4
5
6
fun letTest() {
val result = "ABCDEF".let {
it.substring(2) // it 代表 "ABCDEF"
}
Log.d(TAG, result) // CDEF
}

T.let() 返回作用域中的最后一个对象

T.also()

定义

1
2
3
4
5
6
7
8
9
10
11
12
/**
* Calls the specified function [block] with `this` value as its argument and returns `this` value.
*/
@kotlin.internal.InlineOnly
@SinceKotlin("1.1")
public inline fun <T> T.also(block: (T) -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block(this)
return this
}

示例

1
2
3
4
5
6
fun alsoTest() {
val result = "ABCDEF".also {
it.substring(2) // it 代表 "ABCDEF"
}
Log.d(TAG, result) // ABCDEF
}

T.also() 返回原来的对象不变

T.apply()

定义

1
2
3
4
5
6
7
8
9
10
11
/**
* Calls the specified function [block] with `this` value as its receiver and returns `this` value.
*/
@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block()
return this
}

示例

1
2
3
4
5
6
fun applyTest() {
val result = "ABCDEF".apply {
this.substring(2) // this 代表 "ABCDEF"
}
Log.d(TAG, result) // ABCDEF
}

T.apply() 返回原来的对象不变

函数特点

T.run()、T.run()、T.also()、T.apply() 函数

xxx 表示函数名

1
2
3
4
5
6
7
8
9
10
11
class MyClass {
fun test() {
val str = "AA"
val result = str.xxx {
print(this) // 接收者
print(it) // 传参
100 // 返回值
}
print(result)
}
}
函数 接收者(this) 传参(it) 返回值(result)
T.run() “AA” 编译错误 作用域中的最后一个对象
T.let() this@Myclass “AA” 作用域中的最后一个对象
T.also() this@Myclass “AA” “AA” 对象(本身)
T.apply() “AA” 编译错误 “AA” 对象(本身)

run() 与 with(T) 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class MyClass {
fun runTest() {
var result = run {
print(this) // 接收者
print(it) // 传参
100 // 返回值
}
print(result)
}

fun withTest() {
val str = "AA"
var result = with(str) {
print(this) // 接收者
print(it) // 传参
100 // 返回值
}
print(result)
}
}
函数 接收者(this) 传参(it) 返回值(result)
run() this@Myclass 编译错误 作用域中的最后一个对象
with() “AA” 编译错误 作用域中的最后一个对象

函数特点汇总

函数 接收者(this) 传参(it) 返回值(result)
T.run() “AA” 编译错误 作用域中的最后一个对象
run() this@Myclass 编译错误 作用域中的最后一个对象
with() “AA” 编译错误 作用域中的最后一个对象
T.let() this@Myclass “AA” 作用域中的最后一个对象
T.also() this@Myclass “AA” “AA” 对象(本身)
T.apply() “AA” 编译错误 “AA” 对象(本身)

我们知道 localStorage 的生命周期是永久的。除非用户在浏览器上手动删除 localStorage 信息,否则这些信息将永久存在。

sessionStorage 的生命周期为当前窗口或者标签页。用户一旦关闭了窗口或者标签页,那么通过 sessionStorage 存储的数据也将被清空。

不同浏览器间无法共享 localStorage 或 sessionStorage 中的数据,相同浏览器的不同页面间可共享相同的 localStorage(页面属于相同域名和端口),但不同页面或标签间无法共享 sessionStorage 的数据。

如此看来,localStorage 更适合用来做历史输入记录的存储。

思路:存储的历史记录用 historyItems 表示,historyItems 以 ‘|’ 分隔符存储各项记录,当某项记录 a 在 historyItems 中存在,那么把原来的 a 去掉,把新的记录 a 放到最前面。

存储数据

1
2
3
4
5
6
7
8
9
10
11
12
13
setHistoryItem(keyword) {
keyword = keyword.trim();
let { historyItems } = localStorage;
if (historyItems === undefined) {
localStorage.historyItems = keyword;
} else {
const onlyItem = historyItems.split('|').filter(e => e != keyword);
if (onlyItem.length > 0) {
historyItems = keyword + '|' + onlyItem.join('|');
}
localStorage.historyItems = historyItems;
}
}

获取所有数据

1
2
3
4
5
6
getHistoryItems() {
if (localStorage.historyItems === undefined) {
return [];
}
return localStorage.historyItems.split('|');
}

根据关键字获取数据

1
2
3
4
5
6
7
8
getHistoryItemsByKeyword(keyword) {
if (localStorage.historyItems === undefined) {
return [];
}
keyword = keyword.trim();
let seletedHistoryItems = localStorage.historyItems.split('|').filter(e => e.indexOf(keyword) != -1);
return seletedHistoryItems;
}

根据关键字删除数据

1
2
3
4
5
6
7
8
9
10
11
12
deleteHistoryItemByKeyword(keyword) {
if (localStorage.historyItems === undefined) {
return;
}
let historyItems = localStorage.historyItems.split('|');
let index = historyItems.indexOf(keyword);
if (index < 0) {
return;
}
historyItems.splice(index, 1);
localStorage.historyItems = historyItems.join('|');
}

清空数据

1
2
3
clearHistory() {
localStorage.removeItem('historyItems');
}

本文讲述的是 Windows 的 WebStorm 2018 版本的破解。步骤如下:

  1. 下载 WebStorm . 笔者下载的版本为 WebStorm 2018.1.2

  2. 下载破解补丁。下载链接,密码:yxb5

  3. 拷贝补丁到 WebStorm 安装目录的 bin 目录下

  4. 同时修改 bin 目录下的 WebStorm.exe.vmoptions 和 WebStorm64.exe.vmoptions 文件,在它们的最上面添加以下格式的代码:

    -javaagent:webstorm安装路径/bin/破解补丁名字.jar

    如笔者要添加的一行代码为:

    1
    -javaagent:C:/Program Files/JetBrains/WebStorm 2018.1.2/bin/JetbrainsCrack-2.8-release-enc.jar

    注意斜杠的方向

  5. 保存文件。启动 WebStorm , 选择 activation code , 并将上面的那一行代码作为激活码拷贝进入即可破解成功。

在项目开发中,当我们接入第三方 SDK 时,可能会要求我们自己的 Application 继承它们的 Application , 但是 Java 只能是单继承的,这时我们就可以使用接口代理的方式来间接地实现“多继承”了。

假设我们的 MyApplication 需要继承两个第三方 SDK 的类 XApplication 与 YApplication .

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.zch.demo.app;

import android.app.Application;
import android.content.Context;
import android.content.res.Configuration;
import android.util.Log;

/**
* Created by zch on 2018/8/3.
*/
public class XApplication extends Application {

@Override
public void attachBaseContext(Context base) {
super.attachBaseContext(base);
Log.d("info-->", "XApplication attachBaseContext");
}

@Override
public void onCreate() {
super.onCreate();
Log.d("info-->", "XApplication onCreate");
}

@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
Log.d("info-->", "XApplication onConfigurationChanged");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.zch.demo.app;

import android.app.Application;
import android.content.Context;
import android.content.res.Configuration;
import android.util.Log;

/**
* Created by zch on 2018/8/3.
*/
public class YApplication extends Application {

@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
Log.d("info-->", "YApplication attachBaseContext");
}

@Override
public void onCreate() {
super.onCreate();
Log.d("info-->", "YApplication onCreate");
}

@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
Log.d("info-->", "YApplication onConfigurationChanged");
}
}

显然,我们自家的 MyApplication 是不可能同时直接继承上面的两个 Application 的。我们可以让 MyApplication 继承一个代理类 ProxyApplication , 然后在 ProxyApplication 中通过反射和接口回调的方式调用组合实现类 ApplicationImpl(组合了多个 Application 接口)对应的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.zch.demo.app;

import android.content.Context;
import android.content.res.Configuration;
import android.util.Log;

/**
* Created by zch on 2018/8/3.
*/
public class MyApplication extends ProxyApplication {

@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);

Log.d("info-->", "MyApplication attachBaseContext");
}

@Override
public void onCreate() {
super.onCreate();

Log.d("info-->", "MyApplication onCreate");
}

@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);

Log.d("info-->", "MyApplication onConfigurationChanged");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package com.zch.demo.app;

import android.app.Application;
import android.content.Context;
import android.content.res.Configuration;

/**
* Created by zch on 2018/8/3.
*/
public class ProxyApplication extends Application {

private IApplicationListener iApplicationListener;

@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);

iApplicationListener = getProxyApplication();

if (iApplicationListener != null) {
iApplicationListener.onProxyAttachBaseContext(base);
}
}

@Override
public void onCreate() {
super.onCreate();

if (iApplicationListener != null) {
iApplicationListener.onProxyCreate();
}
}

@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);

if (iApplicationListener != null) {
iApplicationListener.onProxyConfigurationChanged(newConfig);
}
}

private IApplicationListener getProxyApplication() {
try {
Class clazz = Class.forName("com.zch.demo.app.ApplicationImpl");
return (IApplicationListener) clazz.newInstance();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return null;
}
}

在组合实现类 ApplicationImpl 中,我们通过反射代理的方式调用多个 Application 的生命周期方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
package com.zch.demo.app;

import android.app.Application;
import android.content.Context;
import android.content.res.Configuration;

/**
* Created by zch on 2018/8/3.
*/
public class ApplicationImpl extends Application implements IApplicationListener {

private IXApplicationListener ixApplicationListener;
private IYApplicationListener iyApplicationListener;

@Override
public void onProxyAttachBaseContext(Context base) {
super.attachBaseContext(base);

ixApplicationListener = getXApplication();
iyApplicationListener = getYApplication();

if (ixApplicationListener != null) {
ixApplicationListener.onXAttachBaseContext(base);
}
if (iyApplicationListener != null) {
iyApplicationListener.onYAttachBaseContext(base);
}
}

@Override
public void onProxyCreate() {
super.onCreate();

if (ixApplicationListener != null) {
ixApplicationListener.onXCreate();
}
if (iyApplicationListener != null) {
iyApplicationListener.onYCreate();
}
}

@Override
public void onProxyConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);

if (ixApplicationListener != null) {
ixApplicationListener.onXConfigurationChanged(newConfig);
}
if (iyApplicationListener != null) {
iyApplicationListener.onYConfigurationChanged(newConfig);
}
}

private IXApplicationListener getXApplication() {
Class clazz = null;
try {
clazz = Class.forName("com.zch.demo.app.XApplicationImpl");
return (IXApplicationListener) clazz.newInstance();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return null;
}

private IYApplicationListener getYApplication() {
Class clazz = null;
try {
clazz = Class.forName("com.zch.demo.app.YApplicationImpl");
return (IYApplicationListener) clazz.newInstance();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return null;
}
}

接口 IApplicationListener

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.zch.demo.app;

import android.content.Context;
import android.content.res.Configuration;

/**
* Created by zch on 2018/8/3.
*/
public interface IApplicationListener {

void onProxyAttachBaseContext(Context base);

void onProxyCreate();

void onProxyConfigurationChanged(Configuration newConfig);
}

实现类 XApplicationImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.zch.demo.app;

import android.content.Context;
import android.content.res.Configuration;

/**
* Created by zch on 2018/8/3.
*/
public class XApplicationImpl extends XApplication implements IXApplicationListener {

@Override
public void onXAttachBaseContext(Context base) {
super.attachBaseContext(base);
}

@Override
public void onXCreate() {
super.onCreate();
}

@Override
public void onXConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
}
}

接口 IXApplicationListener

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.zch.demo.app;

import android.content.Context;
import android.content.res.Configuration;

/**
* Created by zch on 2018/8/3.
*/
public interface IXApplicationListener {

void onProxyAttachBaseContext(Context base);

void onProxyCreate();

void onProxyConfigurationChanged(Configuration newConfig);
}

实现类 YApplicationImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.zch.demo.app;

import android.content.Context;
import android.content.res.Configuration;

/**
* Created by zch on 2018/8/3.
*/
public class YApplicationImpl extends YApplication implements IYApplicationListener {

@Override
public void onYAttachBaseContext(Context base) {
super.attachBaseContext(base);
}

@Override
public void onYCreate() {
super.onCreate();
}

@Override
public void onYConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
}
}

接口 IYApplicationListener

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.zch.demo.app;

import android.content.Context;
import android.content.res.Configuration;

/**
* Created by zch on 2018/8/3.
*/
public interface IYApplicationListener {

void onYAttachBaseContext(Context base);

void onYCreate();

void onYConfigurationChanged(Configuration newConfig);
}

当我们在清单文件中声明了 MyApplication 并跑起项目时,会打印以下日志:

1
2
3
4
5
6
08-03 13:56:33.830 9314-9314/? D/info-->: XApplication attachBaseContext
08-03 13:56:33.830 9314-9314/? D/info-->: YApplication attachBaseContext
08-03 13:56:33.830 9314-9314/? D/info-->: MyApplication attachBaseContext
08-03 13:56:33.830 9314-9314/? D/info-->: XApplication onCreate
08-03 13:56:33.830 9314-9314/? D/info-->: YApplication onCreate
08-03 13:56:33.830 9314-9314/? D/info-->: MyApplication onCreate

说明我们在 MyApplication 中成功地调用了 XApplication 与 YApplication 中的生命周期方法。

产生原因

Android 在 Android 3.0(API 级别 11)中引入了 Fragment(片段),主要是为了给大屏幕(如平板电脑)上更加动态和灵活的 UI 设计提供支持。由于平板电脑的屏幕比手机屏幕大得多,因此可用于组合和交换 UI 组件的空间更大。利用 Fragment 实现此类设计时,无需管理对视图层次结构的复杂更改。 通过将 Activity 布局分成 Fragment , 可以在运行时修改 Activity 的外观,并在由 Activity 管理的返回栈中保留这些更改。

简述

Fragment 可视为 Activity 的模块化组成部分,它具有自己的生命周期。Fragment 必须始终嵌入在 Activity 中,其生命周期直接受宿主 Activity 生命周期的影响。

每个 Fragment 都可设计为可重复使用的模块化 Activity 组件,可以将一个 Fragment 加入多个 Activity . 因此,应该采用可复用式设计,避免直接从某个 Fragment 直接操纵另一个 Fragment . 因为模块化 Fragment 可以通过更改 Fragment 的组合方式来适应不同的屏幕尺寸。在设计可同时支持平板电脑和手机的应用时,可以在不同的布局配置中重复使用 Fragment , 以根据可用的屏幕空间优化用户体验。 例如,在手机上,如果不能在同一 Activity 内储存多个 Fragment , 可能必须利用单独 Fragment 来实现单窗格 UI .

当 Activity 正在运行(处于已恢复生命周期状态)时,可独立操纵每个 Fragment , 如添加或移除它们。当执行此类 Fragment 事务时,也可以将其添加到由 Activity 管理的返回栈Activity 中的每个返回栈条目都是一条已发生 Fragment 事务的记录。返回栈让用户可以通过按返回按钮撤消 Fragment 事务(后退)。

创建Fragment

要创建一个 Fragment 必须扩展 Fragment 类(或已有的其子类 DialogFragment、ListFragment、PreferenceFragment)。

  • DialogFragment

    显示浮动对话框。使用此类创建对话框可有效地替代使用 Activity 类中的对话框帮助程序方法,因为您可以将片段对话框纳入由 Activity 管理的片段返回栈,从而使用户能够返回清除的片段。

  • ListFragment

    显示由适配器(如 SimpleCursorAdapter)管理的一系列项目,类似于 ListActivity . 它提供了几种管理列表视图的方法,如用于处理点击事件的 onListItemClick() 回调。

  • PreferenceFragment

    以列表形式显示 Preference 对象的层次结构,类似于 PreferenceActivity . 这在为您的应用创建“设置” Activity 时很有用处。

添加用户界面

Fragment 通常用作 Activity 用户界面的一部分,将其自己的布局融入 Activity . 要想为 Fragment 提供布局,必须实现 onCreateView() 回调方法,Android 系统会在 Fragment 需要绘制其布局时调用该方法。此方法返回的 View 必须是 Fragment 布局的根视图。

如果是 ListFragment 的子类,则默认实现会从 onCreateView() 返回一个 ListView,因此无需实现它。

创建布局

1
2
3
4
5
6
public static class ExampleFragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.example_fragment, container, false);
}
}

传递至 onCreateView() 的 container 参数是 Fragment 布局将插入到的父 ViewGroup(来自 Activity 的布局)。savedInstanceState 参数是在恢复 Fragment 时,提供上一 Fragment 实例相关数据的 Bundle .

inflate() 方法带有三个参数:

  • 您想要扩展的布局的资源 ID
  • 将作为扩展布局父项的 ViewGroup
  • 指示是否应该在扩展期间将扩展布局附加至 ViewGroup(第二个参数)的布尔值。(在本例中,其值为 false , 因为系统已经将扩展布局插入 container — 传递 true 值会在最终布局中创建一个多余的视图组。)

向Activity添加片段

在Activity的布局文件内声明Fragment
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">

<fragment
android:id="@+id/list"
android:name="com.zch.learnbase.modules.fragment.ArticleListFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />

<fragment
android:id="@+id/detail"
android:name="com.zch.learnbase.modules.fragment.ArticleDetailFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="2" />
</LinearLayout>
  • <fragment> 中的 android:name 属性指定要在布局中实例化的 Fragment 类。
  • 当系统创建此 Activity 布局时,会实例化在布局中指定的每个 Fragment , 并为每个 Fragment 调用 onCreateView() 方法,以检索每个 Fragment 的布局。系统会直接插入 Fragment 返回的 View 来替代 <fragment> 元素。

每个 Fragment 都需要一个唯一的标识符,重启 Activity 时,系统可以使用该标识符来恢复 Fragment(还可以使用该标识符来捕获 Fragment 以执行某些事务,如将其移除)。

可以通过三种方式为 Fragment 提供唯一的标识符:

  • 为 android:id 属性提供唯一 ID
  • 为 android:tag 属性提供唯一字符串
  • 如果未给以上两个属性提供值,系统会使用容器视图的 ID
或者通过编程方式将Fragment添加到某个现有ViewGroup
1
2
3
4
5
FragmentManager fragmentManager = getFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
ArticleListFragment fragment = new ArticleListFragment();
fragmentTransaction.add(R.id.fragment_container, fragment);
fragmentTransaction.commit();

可以在 Activity 运行期间随时将 Fragment 添加到 Activity 布局中。在 Activity 中执行 Fragment 事务(如添加、移除或替换片段),必须使用 FragmentTransaction 中的 API . 一旦通过 FragmentTransaction 做出了更改,就必须调用 commit() 以使更改生效。

添加没有UI的Fragment

还可以使用 Fragment 为 Activity 提供后台行为,而不显示额外 UI . 只能通过 add (Fragment fragment, String tag) 的方式添加,用 tag 做唯一标识符。获取该 Fragment 则需要使用 findFragmentByTag() . 由于它并不与 Activity 布局中的视图关联,因此不会收到对 onCreateView() 的调用。因此,不需要实现该方法。

将没有 UI 的 Fragment 用作后台工作线程的示例 Activity 位于:SDK 示例(通过 Android SDK 管理器提供)中,以 /APIDemos/app/src/main/java/com/example/android/apis/app/FragmentRetainInstance.java 形式位于您的系统中。

管理Fragment

要管理 Fragment , 需要使用 FragmentManager , FragmentManager 执行的操作包括:

  • 通过 findFragmentById() 或 findFragmentByTag() 获取 Activity 中存在的 Fragment .
  • 通过 popBackStack()(模拟用户发出的返回命令)将 Fragment 从返回栈中弹出。
  • 通过 addOnBackStackChangedListener() 注册一个侦听返回栈变化的侦听器。

管理Fragment回退栈

  • 跟踪回退栈状态

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class MyClass implements FragmentManager.OnBackStackChangedListener

    @Override
    public void onBackStackChanged() {

    }
    // ...
    // 添加回退栈监听接口
    getSupportFragmentManager().addOnBackStackChangedListener(this);
    // ...
    }
  • 管理回退栈

    • FragmentTransaction.addToBackStack(String) // 将一个刚刚添加的 Fragment 加入到回退栈中
    • getSupportFragmentManager().getBackStackEntryCount() // 获取回退栈中实体数量
    • getSupportFragmentManager().popBackStack(String name, int flags) // 根据 name 立刻弹出栈顶的 Fragment
    • getSupportFragmentManager().popBackStack(int id, int flags) // 根据 id 立刻弹出栈顶的 Fragment

Fragment常用的API

  • android.support.v4.app.Fragment 主要用于定义 Fragment

  • android.support.v4.app.FragmentManager 主要用于在 Activity 中操作 Fragment , 可以使用 FragmentManager.findFragmenById、FragmentManager.findFragmentByTag 等方法去找到一个 Fragment

  • android.support.v4.app.FragmentTransaction 保证一系列 Fragment 操作的原子性

  • 主要的操作都是 FragmentTransaction 的方法(一般我们为了向下兼容,都使用 support.v4 包里面的 Fragment)

    1
    getFragmentManager() // Fragment 若使用的是 support.v4 包中的,那就使用 getSupportFragmentManager 代替
  • FragmentTransaction 的一些操作方法

执行Fragment事务

在 Activity 中使用 Fragment 的一大优点是,可以根据用户行为通过它们执行添加、移除、替换以及其他操作。 提交给 Activity 的每组更改都称为事务,可以使用 FragmentTransaction 中的 API 来执行一项事务。也可以将每个事务保存到由 Activity 管理的返回栈内,从而让用户能够回退 Fragment 更改(类似于回退 Activity)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

Fragment newFragment = new ArticleListFragment();
FragmentTransaction transaction = getFragmentManager().beginTransaction();

// 往 Activity 中添加一个 Fragment
transaction.add();

// 从 Activity 中移除一个 Fragment
transaction.remove();

// 使用另一个 Fragment 替换当前的,实际上就是 remove() 然后 add() 的合体
transaction.replace();

// 隐藏当前的 Fragment , 仅仅是设为不可见,并不会销毁
transaction.hide();

// 显示之前隐藏的 Fragment
transaction.show();

// 将以上一组事务保存到返回栈,以便用户能够通过按返回按钮撤消事务并回退到上一 Fragment
transaction.addToBackStack(null);

transaction.commit(); //提交一个事务

说明

  • 每个事务都是想要同时执行的一组更改。可以使用 add()、remove() 和 replace() 等方法为给定事务设置想要执行的所有更改。然后,要想将事务应用到 Activity , 必须调用 commit() .
  • 调用 commit() 之前,可调用 addToBackStack() , 以将事务添加到 Fragment 事务返回栈,该返回栈由 Activity 管理,允许用户通过按返回按钮返回上一 Fragment 状态。
  • 如果向事务添加了多个更改(如有一个 add() 或 remove()),并且调用了 addToBackStack() , 则在调用 commit() 前应用的所有更改都将作为单一事务添加到返回栈,并且返回按钮会将它们一并撤消。
  • 如果向同一容器添加多个 Fragment , 则您添加 Fragment 的顺序将决定它们在视图层次结构中的出现顺序。
  • 如果没有在执行移除 Fragment 的事务时调用 addToBackStack() , 则事务提交时该 Fragment 会被销毁,用户将无法回退到该 Fragment . 如果调用了 addToBackStack() , 系统会停止该 Fragment , 并在用户回退时将其恢复。
  • 对于每个 Fragment 事务,都可以通过在提交前调用 setTransition() 来应用过渡动画。
  • 调用 commit() 不会立即执行事务,而是在 Activity 的 UI 线程可以执行该操作时再安排其在线程上运行。不过,如有必要,也可以从 UI 线程调用 executePendingTransactions() 以立即执行 commit() 提交的事务。通常不必这样做,除非其他线程中的作业依赖该事务。
  • 只能在 Activity 保存其状态(用户离开 Activity)之前使用 commit() 提交事务。如果试图在该时间点后提交,则会引发异常。 这是因为如需恢复 Activity , 则提交后的状态可能会丢失。 对于丢失提交无关紧要的情况,请使用 commitAllowingStateLoss() .

Fragment生命周期

1、Fragment必须依存于Activity

2、Fragment依附于Activity的生命状态

3、Fragment生命周期回调方法含义

  • public void onAttach(Context context)

    在 Fragment 已与 Activity 关联时调用 onAttach 方法。从该方法起就可通过 Fragment.getActivity 方法获取与 Fragment 关联的 Activity 对象。此时由于 Fragment 的控件尚未初始化,因此不能操纵控件。

  • public void onCreate(Bundle savedInstanceState)

    onCreate 方法在 onAttach 执行完后马上执行。在该方法中可以读取保存的状态,获取、初始化一些数据,可在 Bundle 对象获取一些从 Activity 传递过来的数据。

  • public View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState)

    在该方法中会创建在 Fragment 显示的 View . inflater 用来装载布局文件;container 是 <fragment> 标签的父标签对应对象;savedInstanceState 可获取 Fragment 保存的状态,为 null 表示未保存。

  • public void onViewCreated(View view,Bundle savedInstanceState)

    创建完 Fragment 中的 View 后会立即调用该方法。参数 view 就是 onCreateView 方法返回的 View 对象。

  • public void onActivityCreated(Bundle savedInstanceState)

    该方法在 Activity 的 onCreate 方法执行完之后调用,表示窗口已经初始化完成。在该方法中可以通过 getActivity().findViewById(Id) 来操纵 Activity 中的 view 了。

  • public void onStart()

    调用该方法时,Fragment 已经可见了,但还无法与用户交互。

  • public void onResume()

    调用该方法时,Fragment 已经可以与用户交互了。

  • public void onPause()

    Fragment 活动正在暂停或者它的操作正在 Activity 中被修改,不再与用户交互。在此可做一些需要临时暂停的工作,如保存音乐播放的进度,然后在 onResume 中恢复。

  • public void onStop()

    Fragment 活动正在停止或者它的操作正在 Activity 中被修改,不再对用户可见。

  • public void onDestoryView()

    移除在 onCreateView 方法中创建的 View 时调用。

  • public void onDestroy()

    做一些最后清理 Fragment 的状态。

  • public void onDetach()

    取消 Fragment 与 Activity 的关联时调用。

与Activity通信

Fragment 可通过 getActivity() 访问 Activity 实例,并轻松地执行在 Activity 布局中查找 View 等任务。

1
View listView = getActivity().findViewById(R.id.list);

Activity 也可以使用 findFragmentById() 或 findFragmentByTag() , 通过从 FragmentManager 获取对 Fragment 的引用来调用 Fragment 中的方法。

1
ExampleFragment fragment = (ExampleFragment) getFragmentManager().findFragmentById(R.id.example_fragment);

Fragment 与 Activity 之间的交互可以通过 Fragment.setArguments(Bundle args) 以及 Fragment.getArguments() 来实现。

创建对 Activity 的事件回调

在某些情况下,可能需要通过 Fragment 与 Activity 共享事件。执行此操作的一个好方法是,在 Fragment 内定义一个回调接口,并要求宿主 Activity 实现它。当 Activity 通过该接口收到回调时,可以根据需要与布局中的其它 Fragment 共享这些信息。

例如,如果一个新闻应用的 Activity 有两个 Fragment , 一个用于显示文章列表(FragmentA),另一个用于显示文章详情(FragmentB),那么 FragmentA 必须在列表项被选定后告知 Activity , 以便它告知 FragmentB 显示该文章详情。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static class FragmentA extends ListFragment {

OnArticleSelectedListener mListener;
...

@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
try {
mListener = (OnArticleSelectedListener) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString() + " must implement OnArticleSelectedListener");
}
}
...

// Container Activity must implement this interface
public interface OnArticleSelectedListener {
public void onArticleSelected(Uri articleUri);
}
...
}

宿主 Activity 会实现 OnArticleSelectedListener 接口并复写 onArticleSelected() , 将来自 FragmentA 的事件通知 FragmentB . 为确保宿主 Activity 实现此接口,FragmentA 的 onAttach() 回调方法会通过转换传递到 onAttach() 中的 Activity 来实例化 OnArticleSelectedListener 的实例。如果 Activity 未实现接口,则片段会引发 ClassCastException .

实现时,mListener 成员会保留对 Activity 的 OnArticleSelectedListener 实现的引用,以便 FragmentA 可以通过调用 OnArticleSelectedListener 接口定义的方法与 Activity 共享事件。

Fragment状态的持久化

由于 Activity 会经常性地发生配置变化,所以依附于它的 Fragment 就可能需要将其状态保存起来。有两个常用的方法可将 Fragment 的状态持久化。

  1. 通过 onSaveInstanceState 与 onRestoreInstanceState 保存和恢复状态。

  2. 让 Android 自动帮我们保存 Fragment 状态。

    在 Activity 中保存 Fragment 的方法:FragmentManager.putFragment(Bundle bundle, String key, Fragment fragment) ; 在 Activity 中获取所保存的 Fragment 的方法:FragmentManager.getFragment(Bundle bundle, String key) .

    这个方法仅仅能够保存 Fragment 中的控件状态,比如说 EditText 中用户已经输入的文字(注意!在这里,控件需要设置一个 id , 否则 Android 将不会为我们保存控件的状态),而 Fragment 中需要持久化的变量依然会丢失,此时就需要利用方法 1 了。

以下为状态持久化的事例代码:

Activity 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
FragmentA fragmentA;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.fragment_activity);

if( savedInstanceState != null ){
fragmentA = (FragmentA) getSupportFragmentManager().getFragment(savedInstanceState,"fragmentA");
}
...
}

@Override
protected void onSaveInstanceState(Bundle outState) {
if( fragmentA != null ){
getSupportFragmentManager().putFragment(outState,"fragmentA",fragmentA);
}

super.onSaveInstanceState(outState);
}

FragmentA 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
   @Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
if ( null != savedInstanceState ){
String savedString = savedInstanceState.getString("string");
}
View root = inflater.inflate(R.layout.fragment_a,null);
return root;
}

@Override
public void onSaveInstanceState(Bundle outState) {
outState.putString("string","anAngryAnt");
super.onSaveInstanceState(outState);
}

参考资料

  • Android Developers
  • LearningNotes

Activity正常生命周期流程

Activity 的生命周期回调方法有:onCreate() , onStart() , onResume() , onPause() , onStop() , onRestart() , onDestroy() .

  • onCreate()

    Activity 正在被创建。这是生命周期的第一个方法,在整个生命周期中只会被调用一次,一般在此做一些初始化工作。参数 savedInstanceState 保存 Activity 因异常情况而被销毁前的状态,可利用此参数做一些数据恢复的操作,若 Activity 正常启动,savedInstanceState 为 null .

  • onStart()

    Activity 正在被启动。此时 Activity 已经可见,但还没出现在前台,还无法与用户交互。

  • onResume()

    Activity 启动完成。此时 Activity 已经可见,并出现在前台,可以与用户交互了。该方法在 Activity 的整个生命周期中可能会多次被调用到。

  • onPause()

    Activity 正在被停止。在此可做一些存储数据、停止动画等工作,但注意不能太耗时,因为这会影响到新 Activity 的显示,onPause 必须执行完,新 Activity 的 onResume 才会执行。

  • onStop()

    Activity 即将停止。当前 Activity 不可见时回调此方法。在此处可释放全部用户使用不到的数据,可做一些稍微重量级的回收工作,同样不能太耗时,如对注册广播的解注册,对一些状态数据的存储。此时 Activity 还不会被销毁掉,而是保持在内存中,但随时都会被回收。

  • onRestart()

    Activity 正在重新启动。一般情况下,当当前 Activity 从不可见重新变为可见状态时,onRestart 就会被调用。这种情况一般由用户行为所导致,比如用户按 Home 键切换到桌面或者用户打开一个新 Activity , 此时当前 Activity 就会暂停,也就是 onPause 和 onStop 被执行了,接着用户又回到这个 Activity , 就会导致该 Activity 的 onRestart 被调用。

  • onDestroy

    Activity 即将被销毁。这是 Activity 生命周期中的最后一个回调,在此可做一些回收工作和最终的资源释放。

通常情况下:onCreate() 和 onDestroy() 成对存在;onStart() 和 onStop() 成对存在;onResume() 和 onPause() 成对存在。

Activity异常情况生命周期分析

  • 系统资源配置发生改变导致 Activity 被杀死并重新创建

    当系统配置发生变化(如旋转屏幕),Activity 会被销毁,其 onPause、onStop、onDestroy 均会被调用,同时由于 Activity 是在异常情况下终止的,系统会调用 onSaveInstanceState 来保存当前 Activity 的状态。onSaveInstanceState 方法的调用时机是在 onStop 之前,它和 onPause 没有既定的时序关系。当 Activity 被重建后,系统会调用 onRestoreInstanceState , 并把 Activity 销毁时的 onSaveInstanceState 方法所保存的 Bundle 对象作为参数同时传递给 onRestoreInstanceState 和 onCreate 方法。因此可以通过 onRestoreInstanceState 和 onCreate 方法来判断 Activity 是否被重建了,如果被重建了,我们就可以取出之前保存的数据并恢复,从时序上说,onRestoreInstanceState 的调用时机在 onStart 之后。

    系统只在 Activity 异常终止的时候才会调用 onSaveInstanceState 和 onRestoreInstanceState 来存储和恢复数据,其他情况不会触发这个过程,但是按 Home 键或启动新 Activity 仍然会触发 onSaveInstanceState 的调用。

    在 onSaveInstance 和 onRestoreInstanceState 方法中,系统自动为我们做了一定的恢复工作。当 Activity 在异常情况下需要重建时,系统会默认保存了当前 Activity 的视图结构,并且在 Activity 重启后为我们恢复这些数据,如文本框中用户的输入数据、ListView 滚动位置等。

    在 onCreate 和 onRestoreInstanceState 中接收保存的数据的区别是:onRestoreInstanceState 一旦被调用,其参数 savedIntanceState 一定有值,而不需要额外判断它是否为空;但是 onCreate 如果是正常启动的话,其参数 saveInstanceState 为 null , 所以必须额外判断。官方文档建议采用 onRestoreInstance 去恢复数据。

  • 资源内存不足导致低优先级的 Activity 被杀死

    Activity 按优先级从高到低如下:

    • 前台 Activity , 正在与用户交互的 Activity,优先级最高。
    • 可见但非前台 Activity , 如 Activity 中弹出一个对话框,导致 Activity 可见但是位于后台无法与用户直接交互。
    • 后台 Activity , 已经被停止的 Activity , 如执行了 onStop , 优先级最低。

    当系统内存不足时,系统就会按照上述优先级去杀死目标 Activity 所在的进程,并在后续通过 onSaveInstanceState 和 onRestoreInstanceState 来存储和恢复数据。如果一个进程中没有四大组件在执行,那么这个进程很快被系统杀死,因此,一些后台工作不适合脱离四大组件而独自运行在后台中,这样进程很容易被杀死。比较好的方法是将后台工作放在 Service 中从而保证进程有一定的优先级,这样就不会轻易被系统杀死。

Activity生命周期附加说明

  1. 当用户打开新的 Activity 或切换到桌面时,回调如下:onPause –> onStop . 但是有一种情况,如果新 Activity 采用了透明主题,那么当前 Activity 不会调用 onStop .

  2. 不能在 onPause 中做重量级的操作,因为必须 onPause 执行完成后新启动的 Activity 才能 Resume .

configChanges属性的应用

  1. 防止屏幕旋转时 Activity 重启
1
android:configChanges="orientation | screenSize"

有了上面的设置,系统会调用 Activity 的 onConfigurationChanged 方法,此时我们可以做一些自己的特殊处理。

Activity启动模式

我们在开发项目的过程中,会涉及到该应用中多个 Activity 组件之间的跳转,或者夹带其它应用的可复用的 Activity . 例如我们可能希望跳转到原来某个 Activity 实例,而不是产生大量重复的 Activity . 这样就需要我们为 Activity 配置特定的加载模式,而不是使用默认的加载模式。

四种加载模式

  • standard 模式

    标准模式。这是默认模式,每次激活 Activity 时都会创建 Activity 实例,并放入任务栈中,不管这个实例是否存在。一个任务栈中可以有多个实例,每个实例也可以属于不同的任务栈。在这种模式下,谁启动了这个 Activity , 那么这个 Activity 就运行在启动它的那个 Activity 所在的任务栈中。启动的生命周期为:onCreate() -> onStart() -> onResume() .

  • singleTop 模式

    栈顶复用模式。如果在任务的栈顶正好存在该 Activity 的实例,就重用该实例(同时 onNewIntent 方法会被回调,通过该方法的 Intent 参数我们可以取出当前请求的信息),此时这个 Activity 的生命周期顺序为:onPause() ->onNewIntent()->onResume() , 否则就会创建新的 Activity 实例并放入栈顶,即使栈中已经存在该 Activity 的实例,只要不在栈顶,都会创建新的实例。此时生命周期顺序为:onCreate()->onStart()->onResume() .

  • singleTask 模式

    栈内复用模式。这是一种单实例模式,如果在栈中已经有该 Activity 的实例,就重用该实例(会调用实例的 **onNewIntent()**)。具体地说,当一个具有 singleTask 模式的 Activity 请求启动后,如 Activity A , 系统首先会寻找是否存在 A 想要的任务栈,若不存在,则重新创建一个任务栈,然后创建 A 的实例后把 A 放入栈中。若存在,这时要看 A 是否在栈中有实例存在,若有实例存在,那么系统就会把 A 调到栈顶(此时还会把 A 上面的实例移除出栈)并调用它的 onNewIntent 方法,若没有实例存在,则创建 A 的实例并把 A 压入栈中。

  • singleInstance 模式

    单实例模式。这是一种加强的 singleTask 模式,它除了具有 singleTask 的所有特性外,还加强了一点,那就是具有此种模式的 Activity 只能单独存在于一个任务栈中。它在一个新栈中创建该 Activity 的实例,并让多个应用共享该栈中的该 Activity 实例。一旦该模式的 Activity 实例已经存在于某个栈中,任何应用再激活该 Activity 时都会重用该栈中的实例(会同时调用实例的 **onNewIntent()**)。其效果相当于多个应用共享一个应用,不管谁激活该 Activity 都会进入同一个应用中。

设置启动模式的位置在 AndroidManifest.xml 文件中 Activity 元素的 Android:launchMode 属性。

LaunchMode附加说明

  1. 使用 TaskAffinity 属性指定一个 Activity 所需要的任务栈的名字,默认情况下,所有 Activity 所需的任务栈的名字为应用的包名。TaskAffinity 属性主要和 singleTask 启动模式或者 allowTaskReparenting 属性配对使用。

  2. 任务栈分为前台任务栈后台任务栈,后台任务栈中的 Activity 位于暂停状态,用户可通过切换将后台任务栈再次调到前台。

Activity的Flags

除了可以在 manifest 中设置 Activity 的启动模式,也可以通过设置 Intent 的 flag 标识来设定 Activity 的启动模式。

常用的有:FLAG_ACTIVITY_NEW_TASK、FLAG_ACTIVITY_SINGLE_TOP、FLAG_ACTIVITY_CLEAR_TOP

  • FLAG_ACTIVITY_NEW_TASK

    相当于 singleTask 启动模式。

  • FLAG_ACTIVITY_SINGLE_TOP

    相当于 singleTop 启动模式。

  • FLAG_ACTIVITY_CLEAR_TOP

    设置此标识的 Activity 在启动时,如果当前的任务栈内存在此 Activity 实例,则跳转到此实例,并清除掉在此实例上面的所有 Activity 实例,此时此 Activity 实例位于任务栈的栈顶。

IntentFilter的匹配规则

启动 Activity 分为显示隐式调用,显示调用需要明确指定被启动对象的组件信息,包括包名和类名,而隐式调用则不需要明确指定组件信息。当显示和隐式调用共存时以显示调用为主。显示调用很简单,这里只介绍隐式调用。隐式调用需要 Intent 能够匹配目标组件的 IntentFilter 中所设置的过滤信息,如果不匹配将无法启动目标 Activity , IntentFilter 中的过滤信息有 action、category、data .

一个过滤列表中的 action、category 和 data 可以有多个,所有的 action、category、data 分别构成不同类别,同一类别的信息共同约束当前类别的匹配过程。只有一个 Intent 同时匹配 action、category、data 类别才算完全匹配,只有完全匹配才能成功启动目标 Activity . 一个 Activity 中可以有多个 intent-filter , 一个 Intent 只要能匹配任何一组 intent-filter 即可成功启动对应的 Activity .

action的匹配规则

action 是一个字符串,系统预定义了一些 action , 同时我们也可以在应用中定义自己的 action . 一个过滤规则中可以有多个 action . action 的匹配要求 Intent 中的 action 必须存在且和过滤规则中的其中一个 action 完全相同即可匹配成功,若 Intent 中没有指定 aciotn , 则匹配失败。另外,action是 区分大小写的。

category的匹配规则

category 是一个字符串,系统预定义了一些 category , 同时我们也可以在应用中自定义自己的 category . category 的匹配规则是,只要 Intent 中出现了 categoty , 不管有几个 category , 对于每一个 category , 它必须是过滤规则中已经定义了的 category , 系统在调用 startActivity 或者 startActivityForResult 的时候会默认给 Intent 加上 android.intent.category.DEFAULT 这个 category . 因此,为了我们的 activity 能够接收隐式调用,就必须在 intent-filter 中指定这个默认的 category .

data的匹配规则

data 的匹配规则和 action 类似,它要求 Intent 中必须含有 data 数据,并且 data 数据能够完全匹配过滤规则中的某一个 data . 这里说的完全匹配是指过滤规则中出现的 data 部分也出现在了 Intent 中的 data 中

参考资料

  • Android开发艺术探索

前言

在 Android 开发中会经常用到 RecyclerView , 当 RecyclerView 的 item 类型有多种时,我们需要重写 getItemViewType 方法。

1
2
3
4
@Override
public int getItemViewType(int i) {
return 0;
}

getItemViewType 方法指定返回一个 int 值,如果我们需要返回多个值怎么办?

场景

当我们做 IM 的聊天消息展示时,可能会有 state 代表消息状态(接收的已读消息、接收的未读消息、发送成功的消息、发送失败的消息),type 代表消息类型(文字、图片、视频、音频)。此时,我们可能需要 getItemViewType 返回 state 和 type 两个值。

1
2
3
4
5
6
7
@Override
public int getItemViewType(int i) {
int state = mMessageList.get(i).state;
int type = mMessageList.get(i).type;

return 0;
}

解决方法

一个 int 类型有 32 位,我们用左边的高 16 位存储 state 的值,用右边的低 16 位存储 type 的值

1
2
3
4
5
6
7
8
9
10
@Override
public int getItemViewType(int i) {
int state = mMessageList.get(i).state;
int type = mMessageList.get(i).type;

int result = (state & 0x7fff) << 16;
result |= (type & 0x7fff);

return result;
}

在 onCreateViewHolder 中通过 viewType 解析出 state 和 type .

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public AbstractChatHolder onCreateViewHolder(ViewGroup parent, int viewType) {
ViewHolder viewHolder;

int type = viewType & 0x7fff;
int state = (viewType >> 16) & 0x7fff;

// 根据 state 和 type 创建各种 ViewHolder ......

// 省略其它代码 ......

return viewHolder;
}

注意点

  • state 和 type 的取值范围都是 [0,32767],即最小值为 0,最大值为 32767(十六进制的 0x7fff),因为它们实际各占 15 位

先说结论,再通过运行程序去验证。

结论

  • new 一个子类的初始化顺序

    父类静态成员变量、静态代码块 –> 子类静态成员变量、静态代码块 –> 父类成员变量、构造代码块 –> 父类无参构造方法 –> 子类成员变量、 构造代码块 –> 子类无参构造方法

  • 多次创建对象,静态成员变量和静态代码块都只执行一次

  • 除非在子类的构造方法中显示调用父类的构造方法,否则它们默认会先去访问父类的无参构造方法,即在构造方法中的第一行默认调用 super();

程序运行

打印类(用于打印结果)

1
2
3
4
5
6
class Test {

public Test(String str) {
System.out.println(str);
}
}

父类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Parent {

private static Test t10 = new Test("父类静态成员变量 0");
private Test t11 = new Test("父类成员变量 0");

{
Test t14 = new Test("父类构造代码块 0");
}

static {
Test t12 = new Test("父类静态代码块 0");
}

private Test t16 = new Test("父类成员变量 1");

{
Test t15 = new Test("父类构造代码块 1");
}

private static Test t13 = new Test("父类静态成员变量 1");

public Parent() {
System.out.println("父类无参构造方法");
}

public Parent(int i) {
System.out.println("父类有参构造方法 0");
}

public void f() {
System.out.println("父类成员方法");
}

public static void g() {
System.out.println("父类静态成员方法");
}
}

子类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class Son extends Parent {

private static Test t0 = new Test("子类静态成员变量 0");
private Test t1 = new Test("子类成员变量 0");

{
Test t4 = new Test("子类构造代码块 0");
}

static {
Test t2 = new Test("子类静态代码块 0");
}

private Test t6 = new Test("子类成员变量 1");

{
Test t5 = new Test("子类构造代码块 1");
}

private static Test t3 = new Test("子类静态成员变量 1");

public Son() {
// super(); 默认调用
System.out.println("子类无参构造方法");
}

public Son(int i) {
// super(); 默认调用
System.out.println("子类有参构造方法 0");
}

public Son(int i, int j) {
// super(); 不会调用了,因为显式调用了 super(1)
super(1);
System.out.println("子类有参构造方法 1");
}

public void ff() {
System.out.println("子类成员方法");
}

public static void gg() {
System.out.println("子类静态成员方法");
}
}

调用类

1
2
3
4
5
6
7
8
9
10
11
12
public class Solution {

public static void main(String[] args) {
new Son();
System.out.println("------------------------------------------------");
new Son();
System.out.println("------------------------------------------------");
new Son(1);
System.out.println("------------------------------------------------");
new Son(1, 2);
}
}

执行了调用类后,打印结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
父类静态成员变量 0
父类静态代码块 0
父类静态成员变量 1
子类静态成员变量 0
子类静态代码块 0
子类静态成员变量 1
父类成员变量 0
父类构造代码块 0
父类成员变量 1
父类构造代码块 1
父类无参构造方法
子类成员变量 0
子类构造代码块 0
子类成员变量 1
子类构造代码块 1
子类无参构造方法
------------------------------------------------
父类成员变量 0
父类构造代码块 0
父类成员变量 1
父类构造代码块 1
父类无参构造方法
子类成员变量 0
子类构造代码块 0
子类成员变量 1
子类构造代码块 1
子类无参构造方法
------------------------------------------------
父类成员变量 0
父类构造代码块 0
父类成员变量 1
父类构造代码块 1
父类无参构造方法
子类成员变量 0
子类构造代码块 0
子类成员变量 1
子类构造代码块 1
子类有参构造方法 0
------------------------------------------------
父类成员变量 0
父类构造代码块 0
父类成员变量 1
父类构造代码块 1
父类有参构造方法 0
子类成员变量 0
子类构造代码块 0
子类成员变量 1
子类构造代码块 1
子类有参构造方法 1

扩展说明

引自此处

  • 子类中所有的构造方法默认都会先去调用父类中的无参构造方法。因为子类继承了父类的数据,可能还会使用到父类的数据,因此,子类初始化前需先完成父类数据的初始化。父类的初始化是通过调用方法区中的构造方法进行的,不会创建父类对象,对象需要通过 new 关键字创建。

  • new 关键字有两个作用。①、分配内存,创建对象;②、调用构造方法,完成对象的初始化工作。完成这两步后,才算创建了一个完整的 Java 对象。
    所以 new 子类的时候,调用父类的构造方法不是创建了一个父类对象,而是只对它的数据进行初始化,那么父类这些数据存储在哪里呢?通俗地说,子类对象内存区域中会划一部分区域给父类的数据做存储,即子类对象内存中封装了父类的初始化数据,创建子类对象时,父类的数据就是子类的对象的一部分,不存在独立的父类的对象,所有的东西在一起才是一个完整的子类的对象。

在开发过程中,前端和后端的工作通常都是并行的,要想有效地提高工作效率,后端的接口文档就显得特别重要。

接口文档代表着一份请求/响应的契约书,简单地讲就是前端需要带什么样的数据过去?后端返回什么样的数据?

有了接口文档,我们就清楚了与后端交互的数据结构,然后可以通过 Mock 模拟请求/响应的数据。这样可以在前后端互不干扰的情况下完成各自的工作,大大地提高了开发效率。

以下简单介绍 Github 上的一个 Mock 开源库的使用 - moco

step 1:

下载如图所示的 jar 包,并简单命名为 moco .

Mou icon

step 2:

把上面的 moco.jar 放到一个文件夹里面,并在该文件夹中创建配置文件 config.json 。config.json 中配置了两个请求,一个输出 Hello World 的请求,一个检查 App 升级的请求,他们分别输出 hello.json 文件和 checkAppUpgrade.json 文件中的内容。

Mou icon

config.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[
{
"request": {
"uri": "/hello",
"method": "get"
},
"response": {
"file": "hello.json"
}
},
{
"request": {
"uri": "/checkAppUpgrade",
"method": "get"
},
"response": {
"file": "checkAppUpgrade.json"
}
}
]

hello.json

1
2
3
4
5
6
7
{
"code": 0,
"msg": "请求成功",
"data": {
"desc": "Hello World !"
}
}

checkAppUpgrade.json

1
2
3
4
5
6
7
8
9
10
11
{
"code": 0,
"msg": "请求成功",
"data": {
"versonName": "1.2",
"versonCode": 3,
"downloadUrl": "http://www.baidu.com/v1.2.apk",
"desc": "v1.2 版本修复了重大 bug .",
"isForceUpdate": true
}
}

step 3:

在当前文件夹 cmd 运行以下命令即可启动 moco 服务器。

1
java -jar moco.jar http -p 8089 -c config.json

step 4:

在浏览器输入 config.json 配置文件中配置的 uri 即可输出对应的 file 文件中指定的 json 数据。

Mou icon

Mou icon

把 localhost 换成自己电脑的 ip 就可以在手机上访问了。

0%