写文件的时候不希望写入一半的时候掉电丢失

Christoph Hellwig 在 2019 Linux Storage, Filesystem, and Memory-Management Summit (LSFMM)会议上,分享了他关于文件系统原子操作的思考。如果 application 在写文件的时候出现了程序崩溃,人们肯定希望文件系统里的数据要么是旧数据,要么是新数据,肯定不希望看到新旧数据混杂在一起。这样就需要对文件有 atomic write 操作的能力。他介绍了在 XFS 里实现的 atomic write,想看看其他文件系统开发者有什么想法。

目前,如果 application 希望对文件做 atomic write,会有两种方法。一种是在 user space 加锁来保护,数据库方案里面通常会这样来做;另一种是重新写一个新文件,然后做“atomic rename”。可惜,应用程序开发者通常都没有正确使用 fsync(),所以最终数据还是会有丢失。

在现代存储系统里面,哪怕存储设备硬件本身,在写操作的时候都不是立刻写入存储单元的。例如闪存设备都有一个 flash translation layer (FTL)抽象层,会把写操作分发到 flash 的各个地方去,避免某些存储单元被写入太多过早损坏(wear leveling),所以它们实际上从来不会在原地址更新数据。对 NVMe 设备来说,每次更新它的一个逻辑块地址(LBA)的数据的时候,都是能确保是 atomic 操作的,不过这个接口地位比较尴尬,估计很少人会用。SCSI 的接口更加好一些,在做 atomic write 的时候能够有错误报告,不过他也没见过哪个厂商真的实现了这个接口。

有的文件系统可以在写操作的时候写在别的地址 ,例如 XFS, Btrfs,等等。这样就能很容易在文件系统层实现 aotmic write。在 5 年前,HP Research 就有一篇有意思的论文,介绍了如何增加一个特殊的 open() flag 来专门指定要 atomic write。这篇论文还只是学术论文,没有真正处理各种 corner case,以及针对现实生活中的限制来实现,不过想法很合理。

在这个系统中,用户写入文件的时候无论写入多少数据都没用,在明确调用 commit 操作之前都不会真正生效。当 commit 操作结束后,所有的改动就真正生效了。这里比较简单的一种实现方式就是在 fsync()里面来恰当地调用 commit 操作,这样就不需要再加一个新的系统调用了。

不久之前,他开始在 XFS 里面用这种方式实现 atomic write。他向社区发布出了一组 patch,不过当时还有不少问题,因此他后来继续修改了这组 patch。目前论文的原作者一直在跟他紧密交流希望能拿到代码,然后就能跟他合作再写一篇论文出来。此外还有一些人也希望使用这个功能。

Chris Mason 问他这里的粒度是多大,是针对每个单独的 write(),还是更多?Hellwig 回答,在 commit 操作之前的所有 write,都会等 commit 的时候一次写入。文件系统会负责对最多能写多少数据来设一个上限。对 XFS 来说,会根据这些 write 所涉及到的不连续区域的数量,来决定这个上限。

不光是传统的 write()系统调用,mmap()映射出来的区域也一样适用。例如,人们现在更改 B-Tree 的多个节点的时候,很难做 atomic update,而这个功能合入后,application 可以简单的在文件 mmap 出来的内存区域做这些更改,然后简单做一次 commit 即可。如果 application 崩溃了,文件系统里存储的数据仍然保证是旧版本或者是新版本,不会混杂起来。

Ted Ts’o 提到他的 Android 领域的朋友也在提需求想要这么一个功能,不过是针对每个文件系统级别的。他们希望每次对 Android 做版本更新的时候,ext4 或者 F2FS 文件系统都可以通过一个 magic option 来加载上来并且关闭所有日志记录真正触发写入操作。等文件更新完毕然后就发一个 ioctl()来开始把所有那些日志都刷到存储设备里。这个方案有点不美观,不过能实现 90%的功能。最后,Ts’o 也认为 ext4 会需要有一个 atomic write 功能,不过每次 commit 之前究竟能更新多少数据,这里可能更加受限制一点。

Hellwig 表示了一些担忧,因为他此前也做过类似的实现方案,都是在内存里面做数据更新,不过最终发现每一批次能缓存的数据非常有限。Ts’o 介绍了 Android 的情况,这里数据块都是会写入存储设备的,而内存中缓存的只是 metadata 相关的更新,一般也就缓存几分钟,这是个非常特殊的应用场景,不具有普适性。不过这个新的实现方法替代了此前的利用 device-mapper 的机制(那个太慢了)。

Chris Mason 提到,只要 interface 设计的好,他很愿意让 Btrfs 支持这个功能。Hellwig 也说对 Btrfs 来说实现这个功能会很直接。对他来说一个比较大的阻碍是怎么支持 O_DIRECT。如果某个 application 先做了 atomic write,然后又把内容读回来,最好是能把刚刚写入的数据读回来。一般的 application 都不会这么做,不过 NFS 确实有这种行为。Linux I/O 的代码里面没有完全支持好这部分功能,所以他还需要做一些修改。

还有一些讨论是关于为什么利用 fsync()的,为什么不用一个专用的系统调用,或者其他什么接口。Hellwig 觉得用 fsync()没有什么不好,毕竟这也是它的本来含义,不应该只做一部分工作,而不做完。Amir Goldstein 问到是否有可能其他进程同时也对这个文件做 fsync()操作,相当于是某种类型的攻击。 Hellwig 说他本来是使用了一个 open()的 flag,不过后来有人提醒没有用过的 flag 可能不会在 open()里面检查,所以利用 flag 来保证数据一致性不是一个很好的主意。 在那种使用模式下,使用 fsync()的接口仅仅会对那些被用这个 flag 打开的 file descriptor 才会做 commit 操作。后来他改成了使用 inode 的 flag,这样更加合理一点,不过目前还没有处理好那些恶意的 fsync()调用的问题。

android 设备写入文件,立即断电重启后,文件丢失,数据没有保存

在 android 开发的过程中碰到写入文件后,立即断电重启,发现写入的文件丢失了

写入时检查了,写入是没有失败的,经过查找资料可能是如下问题引起:

Linux/Unix 系统中,在文件或数据处理过程中一般先放到内存缓冲区中,等到适当的时候再写入磁盘,以提高系统的运行效率。

可能是因为断电时,文件没有写入的物理介质中导致,解决办法如下:

   在write/fwrite写入后,添加fsync(), 这样可以将缓存中的内容强制写入到磁盘中

关于 write/fwrite 和 fsync 的关系如下:
read/write/fsync:

  1. Linux 底层操作;

  2. 内核调用, 涉及到进程上下文的切换,即用户态到核心态的转换,这是个比较消耗性能的操作。

fread/fwrite/fflush:

  1. C 语言标准规定的 io 流操作,建立在 read/write/fsync 之上

  2. 在用户层, 又增加了一层缓冲机制,用于减少内核调用次数,但是增加了一次内存拷贝。

两者之间的关系,见下图:

补充:

  1. 对于输入设备,调用 fsync/fflush 将清空相应的缓冲区,其内数据将被丢弃;
  2. 对于输出设备或磁盘文件,fflush 只能保证数据到达内核缓冲区,并不能保证数据到达物理设备, 因此应该在调用 fflush 后,调用 fsync(fileno(stream)),确保数据存入磁盘。
  3. 在 android java 层中,需要调用 FileDescriptor 的 sync()方法来确保数据存入磁盘。
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
/* Java 示例代码 */

FileOutputStream fos = null;

byte[] buf = {1,2,3,4};

try {

fos = new FileOutputStream("/sdcard/test.txt");

//将buf中的数据写入fos

fos.write(buf);

//将fos的数据保存到内核缓冲区

//不能确保数据保存到物理存储设备上,如突然断点可能导致文件未保存

fos.flush();

//将数据同步到达物理存储设备

FileDescriptor fd = fos.getFD();

fd.sync();

} catch(Exception e) {

e.printStackTrace();

} finally {

if(fos!=null)

fos.close();

}

第三方App系统签名

刚入职的时候,固件中 app 需要在源码中编译,不是很理解,为什么不使用 IDE 编译,然后 copy 到 /system/app/*,咨询导师,得到反馈 app 需要使用系统的权限 在 AndroidManifest.xml 中声明了系统全下申明了系统权限android:sharedUserId="android.uid.system"

随着工作时间延长,慢慢理解了 apk 签名机制,为了解决上述疑问,出现了 2 套解决方案:

单独签名解决方案
找到平台签名文件“platform.pk8”和“platform.x509.pem”
文件位置 android/build/target/product/security/
签名工具“signapk.jar”
位置:android/prebuilts/sdk/tools/lib
签名证书“platform.pk8 ”“platform.x509.pem ”,签名工具“signapk.jar ”放置在同一个文件夹;
执行命令

java -jar signapk.jar platform.x509.pem platform.pk8 Demo.apk signedDemo.apk

或者直接在 Ubuntu 编译环境执行

java -jar out/host/linux-x86/framework/signapk.jar build/target/product/security/platform.x509.pem build/target/product/security/platform.pk8 input.apk output.apk

IDE 中添加源码平台生成证书 platform.keystore
生成平台 platform.keystore 文件:
编译平台签名文件“platform.pk8”和“platform.x509.pem”
文件位置:android/build/target/product/security/
把 pkcs8 格式的私钥转化成 pkcs12 格式:
openssl pkcs8 -in platform.pk8 -inform DER -outform PEM -out shared.priv.pem -nocrypt
把 x509.pem 公钥转换成 pkcs12 格式:
openssl pkcs12 -export -in platform.x509.pem -inkey shared.priv.pem -out shared.pk12 -name androiddebugkey
密码都是:jfz123456
生成 platform.keystore
keytool -importkeystore -deststorepass jfz123456 -destkeypass jfz123456 -destkeystore platform.keystore -srckeystore shared.pk12 -srcstoretype PKCS12 -srcstorepass jfz123456 -alias androiddebugkey
Eclipse 添加证书
将生成的 platform.keystore 导入 eclipse 在 eclipse 下 Windows/preferences/Android/build 中设置“Custom debug keystore”为刚才生成的 platform.keystore 即可

调试 apk 时直接点击 Debug As —> Android Application 即使用系统签名签名了该 apk

Android Studio 添加证书

android的listview加入EditText

在日常开发中,ListView 是我们常用的控件,也是遇到坑比较多的一个控件。在之前的项目中,有这样的一个布局需求,在 ListView 的 item 中包含有 EditText,第一个问题就是焦点问题,会发现 edittext 获取不到焦点。

1.焦点问题
比如我们有如下的代码:

1
2
3
4
5
6
7
8
9
10
11
12
activity_main.xml

<?xml version="1.0" encoding="utf-8"?>

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView android:id="@+id/listView"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</RelativeLayout>

####MainActivity.java

1
2
3
4
5
6
7
8
9
10
11
public class MainActivity extends Activity {
ListView mListView;
MyAdapter mMyAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mListView = (ListView) findViewById(R.id.listView);
mMyAdapter = new MyAdapter(this);
mListView.setAdapter(mMyAdapter);
}}

####MyAdapter.java

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
public class MyAdapter extends BaseAdapter {
private Context mContext;
public MyAdapter(Context context) {
this.mContext = context;
}
@Override
public int getCount() {
return 20;
}
@Override
public Object getItem(int position) {
return null;
}
@Override
public long getItemId(int position) {
return 0;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
EditText editText;
if (convertView == null) {
editText = new EditText(mContext);
convertView = editText;
} else {
editText = (EditText) convertView;
}
System.out.println("current pos:" + position);
return convertView;
}}

当你运行上述简单的代码后发现 EditText 是无法获取焦点的,导致无法输入任何东东,那么原因何在呢?

####其实,是 listview 先于子 item 抢占了焦点,那么我们首先想到的就是让 listview 失去焦点,让子 item 获取焦点(当然,listview 的 onitem 相关监听事件会失效)。
mListView.setFocusable(false);

这是再运行发现键盘弹出了,可是 editText 获取到焦点然后又失去了,需要你手动再次点击才能获取到,然后才能输入。
而且当你输入完毕,关闭软键盘,发现输入的东西不见了,自动清空。这又产生了两个问题。

第一个问题是 listview 每次调用 getview 都会使 EditText 失去焦点,第二个问题归结于下面要讲的 listview 的 item 复用产生的问题。

第一种方式行不通,查询相关资料发现,可以通过给 listview 的 item 的根布局设置 descendantFocusability 属性。

1
2
3
4
#####android:descendantFocusability 属性有三个值:
beforeDescendants:viewgroup 会优先其子类控件而获取到焦点
afterDescendants:viewgroup 只有当其子类控件不需要获取焦点时才获取焦点
blocksDescendants:viewgroup 会覆盖子类控件而直接获得焦点

####那么我们修改 adapter 中的 getView 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public View getView(int position, View convertView, ViewGroup parent) {
EditText editText;
if (convertView == null) {
convertView = LayoutInflater.from(mContext).inflate(R.layout.list_edittext, parent, false);
editText = (EditText) convertView.findViewById(R.id.editText);
convertView.setTag(editText);
} else {
editText = (EditText) convertView.getTag();
}
System.out.println("current pos:" + position);
return convertView;
}

####list_edittext.xml

1
2
3
4
5
6
7
8
9
10
11
<?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:descendantFocusability="beforeDescendants"
android:orientation="vertical">
<EditText android:id="@+id/editText"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>

发现还是无效果,其实我们少了一句关键的代码,就是给相应的 activity 设置 windowSoftInputMode= adjustPan 即可。。

##终上所述,我认为的解决方案就是给 ListView 或者 ListView 的 item 的根布局添加 android:descendantFocusability="beforeDescendants",然后设置相应的 activity 的 windowSoftInputMode 属性为 adjustPan 。

##2.数据问题
解决完焦点问题后,另一个问题就是 edittext 的数据问题了。当我们在当前屏幕的 edittext 中输入东东后,往下滑,发现下面的 edittext 自动输入了我们输入过得东东,这明显是我们不愿意看到的。

其实这是由于 getView 方法的复用 view 导致的,加入你在 position=0 的 edittext 中输入了内容,当你往下滑时,当 position 为 0 的 view 完全消失时,该 view 会被加入到 mActiveViews[]中,当下方的 item 检测到由可用的 view,则从该数组中取出,所以下方的 edittext 的内容会跟上面你输入的一样,其实就是同一个 edittext。关于 listview 源码级解析详见链接

#####解决方案——保存 edittext 的内容

修改 adapter 代码:

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
//新增一个数组用于保存 edittext 的内容
private SparseArray<String> mStringSparseArray;

@Override
public View getView(final int position, View convertView, ViewGroup parent) {
EditTextHolder editTextHolder;
if (convertView == null) {
convertView = LayoutInflater.from(mContext).inflate(R.layout.list_edittext, parent, false);

editTextHolder = new EditTextHolder();
editTextHolder.mEditText = (EditText) convertView.findViewById(R.id.editText);

editTextHolder.mMyTextWatcher = new MyTextWatcher(position, mStringSparseArray);
//给edittext设置watcher
editTextHolder.mEditText.addTextChangedListener(editTextHolder.mMyTextWatcher);

convertView.setTag(editTextHolder);

} else {
editTextHolder = (EditTextHolder) convertView.getTag();
//由于复用了 edittext,导致他的 watcher 里的 position 还是之前的 positiono,所以需要通知
//watcher 更新 positon,才能保存正确的 positon 的值
editTextHolder.updatePosition(position);
}
System.out.println(position);
editTextHolder.mEditText.setText(mStringSparseArray.get(position));
return convertView;
}
static class EditTextHolder {
EditText mEditText;
MyTextWatcher mMyTextWatcher;
public void updatePosition(int position) {
mMyTextWatcher.updatePosition(position);
}
}

static class MyTextWatcher implements TextWatcher {
private int position;
private SparseArray<String> sparseArray;
//更新 postion
public void updatePosition(int position) {
this.position = position;
}
public MyTextWatcher(int position, SparseArray<String> sparseArray) {
this.position = position;
this.sparseArray = sparseArray;
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) { }
@Override
public void afterTextChanged(Editable s) {
//保存 edittext 的值
sparseArray.put(position, s.toString());
}
}

运行代码,发现 edittext 数据错乱问题解决,此方法同样适用于 checkbox 错乱等问题。

android的listview解决事件穿透问题

bug 重现:通过 mListView.setOnItemClickListener 设置 item 点击事件,正常情况下没有问题,但当 item 里面嵌套了抢焦点的控件(比如 Button ,CheckBox 等),那么点击 item 的时候,Button 等抢焦点的控件会抢先反应,就会导致点击 item 时没有反应。

解决办法:想要 item 有自己的焦点,Button 等控件有自己的焦点的话,需要在 item 的根控件里面设置 android:descendantFocusability="blocksDescendants",这个属性值表示子有子的焦点,父有父的焦点。

java 代码

1
2
3
4
5
6
mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Log.i(TAG, "onItemClick: ------");
}
});

item 布局文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?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="wrap_content"
android:descendantFocusability="blocksDescendants"
android:orientation="horizontal">

<!--标题-->
<TextView
android:id="@+id/tv_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:singleLine="true"
android:text="标题" />

<!--删除按钮-->
<Button
android:id="@+id/bt_cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="删除"/>

</LinearLayout>

属性 android:descendantFocusability 的值有三种:

1
2
3
beforeDescendants:viewgroup会优先其子类控件而获取到焦点
afterDescendants:viewgroup只有当其子类控件不需要获取焦点时才获取焦点
blocksDescendants:viewgroup会覆盖子类控件而直接获得焦点,也就是各有各的焦点

Android透明度百分比转十六进制表

100% — FF
99% — FC
98% — FA
97% — F7
96% — F5
95% — F2
94% — F0
93% — ED
92% — EB
91% — E8

90% — E6
89% — E3
88% — E0
87% — DE
86% — DB
85% — D9
84% — D6
83% — D4
82% — D1
81% — CF

80% — CC
79% — C9
78% — C7
77% — C4
76% — C2
75% — BF
74% — BD
73% — BA
72% — B8
71% — B5

70% — B3
69% — B0
68% — AD
67% — AB
66% — A8
65% — A6
64% — A3
63% — A1
62% — 9E
61% — 9C

60% — 99
59% — 96
58% — 94
57% — 91
56% — 8F
55% — 8C
54% — 8A
53% — 87
52% — 85
51% — 82

50% — 80
49% — 7D
48% — 7A
47% — 78
46% — 75
45% — 73
44% — 70
43% — 6E
42% — 6B
41% — 69

40% — 66
39% — 63
38% — 61
37% — 5E
36% — 5C
35% — 59
34% — 57
33% — 54
32% — 52
31% — 4F

30% — 4D
29% — 4A
28% — 47
27% — 45
26% — 42
25% — 40
24% — 3D
23% — 3B
22% — 38
21% — 36

20% — 33
19% — 30
18% — 2E
17% — 2B
16% — 29
15% — 26
14% — 24
13% — 21
12% — 1F
11% — 1C

10% — 1A
9% — 17
8% — 14
7% — 12
6% — 0F
5% — 0D
4% — 0A
3% — 08
2% — 05
1% — 03
0% — 00

android常见问题汇总

Android8.0/8.1 屏幕旋转崩溃

原因:

android:screenOrientation=”portrait”
activity透明<item name="android:windowIsTranslucent">true</item>

解决方式:
去掉启动页的透明风格属性,并且启动页 style 加上如下属性:

1
<item name="android:windowDisablePreview">true</item>

禁止屏幕旋转

方法一,设置activity的

1
2
screenOrientation=“portrait”

方法二,activity的onCreate中

1
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); // 禁用横屏

被后台杀死后,Android应用如何重新走闪屏逻辑

Android应用运行在后台的时候,经常被系统的LowMemoryKiller杀掉,当用户再次点击icon或者从最近的任务列表启动的时候,进程会被重建,并且恢复被杀之前的现场。什么意思呢?假如APP在被杀之前的Activity堆栈是这样的,A<B<C,C位于最上层

后台杀死与恢复的堆栈.jpg

APP被后台杀死后,APP端进程被销毁了,也就不存在什么Activity了,也就没有什么Activity堆栈,不过AMS的却是被保留了下来:

后台杀死与恢复的堆栈-杀后.jpg

当用户再次启动APP时候会怎么样呢?这个时候,首先看到其实C,而不是栈底部的A,也就是说往往被杀死后,恢复看到的第一个界面是用户最后见到的那个界面。

后台杀死与恢复的堆栈-恢复.jpg

而用户点击返回,看到的就是上一个界面B,其次是A

后台杀死与恢复的堆栈-恢复b.jpg

之所以这样是因为APP端Activity的创建其实都是由AMS管理的,AMS严格维护这APP端Activity对应的ActivityRecord栈,可以看做当前APP的场景,不过,APP端Activity的销毁同AMS端ActivityRecord的销毁并不一定是同步的,最明显的就是后台杀死这种场景。Android为了能够让用户无感知后台杀死,就做了这种恢复逻辑,不过,在开发中,这种逻辑带了的问题确实多种多样,甚至有些产品就不希望走恢复流程,本文就说说如何避免走恢复流程。结合常见的开发场景,这里分为两种,一种是针对推送唤起APP,一种是针对从最近任务列表唤起APP(或者icon)。

从最近的任务列表唤起,不走恢复流程

首先,APP端必须知道当前Activity的启动是不是在走恢复流程,Activity有一个onCreate方法,在ActivityThread新建Activity之后,会回调该函数,如果是从后台杀死恢复来的,回调onCreate的时候会传递一个非空的Bundle savedInstanceState给当前Activity,只要判断这个非空就能知道是否是恢复流程。

1
2
3
4
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}

知道恢复流程之后,如何处理呢?其实很简单,直接吊起闪屏页就可以了,不过这里有一点要注意的是,在启动闪屏页面的时候,必须要设置其IntentFlag:Intent.FLAG_ACTIVITY_NEW_TASK|Intent.FLAG_ACTIVITY_CLEAR_TASK,这样做的理由是为了清理之前的场景,不然之前的ActivityRecord栈仍然保留在ActivityManagerService中,具体实现如下,放在BaseActivity中就可以:

1
2
3
Intent intent = new Intent(this, SplashActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK|Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(intent);

如果不设置会怎么样呢?举个例子,最常见的就是闪屏之后跳转主界面,主界面经常有router逻辑,并且其启动模式一般都是singleTask,处理一些推送,所以其onCreate跟onNewIntent都有相应的处理,如果不设置,在闪屏结束后,在startActivity启动主界面的时候,其实是先走恢复逻辑,然后走singleTask的onNewIntent逻辑,也就是说,onNewIntent跟onCreate是会同时调用的,也可能就会引发重复处理的逻辑,因此最好清理干净。

从推送唤起被杀APP时,如何走闪屏逻辑

对于推送消息的处理,其路由器一般放在MainActivity,并且在onCreate跟onNewIntent都有添加,如果APP存活的情况,可以直接跳转目标页面,如果APP被杀,这个时候,希望先跳转主界面,再跳转目标页面,在效果上来看就是,用户先看到目标页面,点击返回的时候再看到主界面,如果加上闪屏,希望达到的效果是先看到闪屏、点击返回看到目标页,再点击返回看到主页面。如果简单划分一下推送场景,可以看做一下三种

  • 进程存活,Activity存活
  • 进程存活,但是没有Activity存活
  • 进程不存在(无论是否被杀)

其实后面两种完全可以看做一种,这个时候,都是要先start MainActivity,然后让MainActivity在其OnCreate中通过startActivityForResult启动SplashActivity,SplashActivity返回后,在start TargetActivity。下面的讨论都是针对后面两种,需要做的有两件事

  • 一是:检测出后面两种场景,并且在唤起主界面的时候需要添加Intent.FLAG_ACTIVITY_CLEAR_TASK清理之前的现场
  • 二是:在MainActivity的路由系统中,针对这两种场景要,先跳转闪屏,闪屏回来后,再跳转推送页
    如何判断呢,后面两种场景其实只需要判断是否有Activity存活即可,也就是查查APP的topActivity是否为null,注意不要去向AMS查询,而是在本地进程中查询,可以通过反射查询ActivityThread的mActivities,也可以根据自己维护的Activity堆栈来判断,判断没有存活Activity的前提下,就跳转主页面去路由
1
2
3
4
Intent intent = new Intent(this, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK|Intent.FLAG_ACTIVITY_CLEAR_TASK);
intent.setDate(跳转的Uri scheme)
startActivity(intent);

在MainActivity的路由中,需要准确区分是否是推送跳转进来的,如果不是推送跳转进来,就不需要什么特殊处理,如果是推送跳转进来一定会携带跳转scheme数据,根据是否携带数据做区分即可,看一下MainActivity的代码:

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
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Uri uri= getIntent().getData();
// 只有在intent被设置了跳转数据的时候才去跳转,一般是推送就来,如果冷启动,是没有数据的
if(uri!=null){
SplashActivity.startActivityForResult(this,JUMP_TO_TARGET)
}
}
//Intent.FLAG_ACTIVITY_CLEAR_TASK保证了onNewIntent被调用的时候,进程一定是正常活着的
@Override
protected void onNewIntent(Intent intent) {
Uri uri= intent.getData();
intent.setData(null);
router(uri);
}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if(requestCode==JUMP_TO_TARGET && requestCode == RESULT_OK){
router(getIntent().getData());
getIntent().setData(null);
}
}

private void router(Uri uri) {

}

通过上面两部分的处理,基本能够满足APP“死亡”的情况下,先跳转闪屏的需求。

如何让你的app在后台被干掉后优雅的启动

作为一名Android开发师,肯定在处理用户的体验上下一定的功夫。有这么一个场景,在用户用着你开发的app的时候,突然某个聊天工具来消息了,切换到聊天工具后长时间停留,并且可能做了一些你不知道的操作,比如看视频阿,刷刷消息圈什么的。一般这种情况下都很容易出现手机内存不足的情况,内存不足就会可能被干掉。这种时候用户切换到app准备继续操作时,如果开发师处理不好,就会引起崩溃的情况,肯定会出现返回的时候一瞬间的白屏,对于用户体验的来说,非常不好。

开始

首先要介绍下Android中activity的四种启动模式(就当作复习一下旧知识吧,资料来源于网络总结):

Standard:是默认的也是标准的Task模式,在没有其他因素的影响下,使用此模式的Activity,会构造一个Activity的实例,加入到调用者的Task栈中去,对于使用频度一般开销一般什么都一般的Activity而言,standard模式无疑是最合适的,因为它逻辑简单条理清晰,所以是默认的选择。
singleTop:基本上于standard一致,仅在请求的Activity正好位于栈顶时,有所区别。此时,配置成singleTop的Activity,不再会构造新的实例加入到Task栈中,而是将新来的Intent发送到栈顶Activity中,栈顶的Activity可以通过重载onNewIntent来处理新的Intent(当然,也可以无视…)。这个模式,降低了位于栈顶时的一些重复开销,更避免了一些奇异的行为(想象一下,如果在栈顶连续几个都是同样的Activity,再一级级退出的时候,这是怎么样的用户体验…),很适合一些会有更新的列表Activity展示。一个活生生的实例是,在Android默认提供的应用中,浏览器(Browser)的书签Activity(BrowserBookmarkPage),就用的是singleTop。
singleTask:配置了这个属性的activity,最多仅有一个实例存在,而且,它在根的task中,在之后的被杀死重启的过程中我们会利用到这个配置,也就是我们的主界面MainActivity。
singleInstance:跟上面的singleTask基本上是一样的,但是,singleInstance的Activity,是它所在栈中仅有的一个Activity,如果涉及到的其他Activity,都移交到其他Task中进行,在实际开发中这个是用得比较少的。
这个是activity的生命周期:

就不多介绍这个生命周期了,相信都熟悉不过了,有想了解的自行Google或者百度吧。

重点

接下来是我们的重点:程序如果在后台被杀死之后,我们怎么去处理?是立刻恢复还是重新启动?哪个方法更适合我们?

首先,我们得知道,为什么程序会在后台被干掉的?我们又没有手动关闭程序。

app在后台被强杀,是在内存不足的情况下被强制释放了,也有一些恶心的rom会强制杀掉那些后台进程以释放缓存以提高所谓的用户体验。(注:当你的代码写得混乱、冗余,而且非常消耗内存的时候,那你的app在后台运行时将会比较容易被系统给干掉的,所以从现在开始要约束自己要养成良好的编码习惯和注意内存泄漏的问题)

我们都觉得android rom很恶心,但同时还是用些更恶心的手法去绕开这些瓶颈。乱,是因为在最上层没有一个很好的约束,这也是开源的弊端。anyway。我们还是得想破脑袋来解决这些问题,否则饭碗就没了。

我们现在来重现这个熟悉的一幕:

假设:App A -> B -> C

在C activity中点Home键后台运行,打开ddms,选中该App进程,强杀。

然后从“最近打开的应用”中选中该App,回到的界面是C activity,假设App中没有静态变量,这个时候是不会crash的,点击返回到B,这个时候也只是短暂白屏后显示B界面。但如果B中有引用静态变量,并想要获取静态变量中的某个值时,就NullPointer了。

以上复现的流程就几个点,我们展开说下:

当应用被强杀,整个App进程都是被杀掉了,所有变量全都被清空了。包括Application实例。更别提那些静态变量了。

虽然变量被清空了,但Android给了一些补救措施。activity栈没有被清空,也就是说A -> B -> C这个栈还保存了,只是ABC这几个activity实例没有了。所以回到App时,显示的还是C页面。另外当activity被强杀时,系统会调用onSaveInstance去让你保存一些变量,但我个人觉得面对海量的静态变量,这个根本不够用。返回到B会白屏,是因为B要重绘,重走onCreate流程,渲染上需要点时间,所以会白屏了。

大概是以上这些点。如果App中没有静态变量的引用,那就不用出现NullPointer这个crash,也就不需要解决。一旦你有静态变量,或者有些Application的全局变量,那就很危险了。比如登录状态,user profile等等。这些值都是空了。

肯定会有人说,这没关系啊,所有的静态变量都改到单例去不就好了吗?然后附加上一些持久化cache,空了再取缓存就ok了嘛。嗯,这肯定也是一个办法,但是这样的束手束脚对开发来说也是痛苦,至少需要多50%的编码时间才能全部cover。另外,还有那么多帮你挖坑的队友,难省心啊。

既然App都被强杀了,干嘛不重新走第一次启动的流程呢,别让App回到D而是启动A,这样所有的变量都是按正常的流程去初始化,也就不会空指针了,对吧?有人说这方案用户体验一点都不好呀。但哪有十全十美的事呢,是重走流程好,还是一点一个NullPointer好?好好去沟通,相信产品也不会为难你的。当然你也可以拿来举例,iOS在最近打开的应用里杀了某个App,重新点击那个App,还是会重走流程的啊。

如果你说用户已经打开了C界面,所以重新打开的是是恢复到C界面,这样的用户体验会更好啊,如果你是这样认为的,那你很多时间都是在防止恢复的时候不让你的app crash了,与其这样,还不如让整个app重新走整个流程呢,这样更省时间,而且这样也不用担心随时都会崩溃的情况,难道这样的用户体验不会更好吗?

那且想想如何让它不回到C而是重走流程呢?也就是说中断C的初始化而回到A,并且按back键,不会回到C,B。考虑一下。

我们先实例化这个场景吧。
A为App的启动页
B为首页
C为二级页面

把首页launchMode设置为singleTask,具体为什么上面介绍activity的启动模式的时候已经介绍了singleTask的作用了。

在BaseActivity中onCreate中判断App是否被强杀,强杀就不往下走,直接重走App流程。

首页起一个承接或者中转的作用,所有跨级跳转都需要通过首页来完成。

再给个提示,以上场景的解决方案也可以用于解决其它相关问题:
在任意页面退出App
在任意页面返回到首页
其实最重要的知识点就是launchMode

具体实现

AppStatusConstant

1
2
3
4
5
public static final int STATUS_FORCE_KILLED = -1;//应用放在后台被强杀了
public static final int STATUS_NORMAL = 2; //APP正常态//intent到MainActivity区分跳转目的
public static final String KEY_HOME_ACTION = "key_home_action";//返回到主页面
public static final int ACTION_BACK_TO_HOME = 0;//默认值
public static final int ACTION_RESTART_APP = 1;//被强杀

AppStatusManager

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public int appStatus= AppStatusConstant.STATUS_FORCE_KILLED; //APP状态初始值为没启动不在前台状态
public static AppStatus ManagerappStatusManager;
privateAppStatusManager() {
}
public static AppStatus ManagergetInstance() {
if(appStatusManager==null) {
appStatusManager=newAppStatusManager();
}
return appStatusManager;
}
public int getAppStatus() {
return appStatus;
}
public void setAppStatus(int appStatus) {
this.appStatus= appStatus;
}

BaseActivity(大致内容)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
switch(AppStatusManager.getInstance().getAppStatus()) {
case AppStatusConstant.STATUS_FORCE_KILLED:
restartApp();
break;
case AppStatusConstant.STATUS_NORMAL:
setUpViewAndData();
break;
}
protected abstract void setUpViewAndData();
protected void restartApp() {
Intent intent =newIntent(this,MainActivity.class);
intent.putExtra(AppStatusConstant.KEY_HOME_ACTION,AppStatusConstant.ACTION_RESTART_APP);
startActivity(intent);
}

每一个继承于父activity的都不要在oncreat中实现界面初始化和数据的初始化,因为如果被杀死之后,回来会走一次正常的生命流程的。

StartPageActivity配置(在oncreate()方法配置,并且在super()前):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
AppStatusManager.getInstance().setAppStatus(AppStatusConstant.STATUS_NORMAL);//进入应用初始化设置成未登录状态
MainActivity(配置了singleTask的主界面)

@Override
protected void restartApp() {
Toast.makeText(getApplicationContext(),"应用被回收重启",Toast.LENGTH_LONG).show();
startActivity(newIntent(this,StartPageActivity.class));
finish();
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
intaction = intent.getIntExtra(AppStatusConstant.KEY_HOME_ACTION,AppStatusConstant.ACTION_BACK_TO_HOME);
switch(action) {
case AppStatusConstant.ACTION_RESTART_APP:
restartApp();
break;
}
}

当应用打开的时候,启动StartPageActivity,然后设置app的status为normal状态,记住,一定要设置,因为默认的是被杀死的状态的。

当应用被杀死之后,所有数据都会被回收,所以之前设置的app status也会置于默认状态,即杀死状态,所以再次打开app的时候,status为杀死状态,就会走重启的流程,这里为什么要先跳转到MainActivity呢?就是因为MainActivity配置为了Sing了Task,当跳转到这个界面时,MainActivity就会置于Activity Task的最上层,其他的Activity将会被默认销毁掉,利用这种技巧去销毁其他的Activity,最后才是重新启动StartPageActivity。整个流程就是这样了。

大致的实现就如上所述了,我所倡导的宗旨就是花最少的时间,写最好的代码,实现最好的体验!之前也参考过很多网上大神们的实现方式,但是我觉得以上实现的应该是比较完整的一种了。

Android四种启动模式的生命周期

Android四种启动模式的进入时的生命周期:

  1. standard:
    第一次进入:onCreate => onStart
    在栈顶再次进入: onCreate => onStart
    不在栈顶再次进入:onCreate => onStart
    从其他Activity返回:onRestart => onStart
    切换应该后再返回:onRestart => onStart

  2. singleTop:
    第一次进入:onCreate => onStart
    在栈顶再次进入:onNewIntent
    不在栈顶再次进入:onCreate => onStart
    从其他Activity返回:onRestart => onStart
    切换应该后再返回:onRestart => onStart

  3. singleTask:
    第一次进入:onCreate => onStart
    在栈顶再次进入:onNewIntent
    不在栈顶再次进入:onNewIntent => onRestart => onStart
    从其他Activity返回:onRestart => onStart
    切换应该后再返回:onRestart => onStart

  4. singleInstance:
    第一次进入:onCreate => onStart
    在栈顶再次进入: onNewIntent
    不在栈顶再次进入:onNewIntent => onRestart => onStart
    从其他Activity返回:onRestart => onStart
    切换应该后再返回:onRestart => onStart

Activity四种启动模式的onNewIntent调用时机:

  1. standard
    默认启动模式,每次激活Activity时都会创建Activity,并放入任务栈中,永远不会调用onNewIntent()。

  2. singleTop
    如果在任务的栈顶正好存在该Activity的实例, 就重用该实例,并调用其onNewIntent(),否者就会创建新的实例并放入栈顶(即使栈中已经存在该Activity实例,只要不在栈顶,都会创建实例,而不会调用onNewIntent(),此时就跟standard模式一样)。

  3. singleTask
    如果在任务栈中已经有该Activity的实例,就重用该实例(会调用实例的onNewIntent())。重用时,会让该实例回到栈顶,因此在它上面的实例将会被从栈中移除。如果栈中不存在该实例,将会创建新的实例放入栈中(此时不会调用onNewIntent())。

  4. singleInstance
    在一个新栈中创建该Activity实例,并让多个应用共享该栈中的该Activity实例。一旦该模式的Activity的实例存在于某个栈中,任何应用再激活该Activity时都会重用该栈中的实例,其效果相当于多个应用程序共享一个应用,不管谁激活该Activity都会进入同一个应用中。

注意事项:
当调用到onNewIntent(intent)的时候,需要在onNewIntent() 中使用setIntent(intent)修改getIntent()的返回值。否则,后续的getIntent()都是得到老的Intent。