每周分享第 3 期

这里记录过去一周,我看到的值得分享的内容。

titu

(题图:合肥翡翠湖)

文章

1、我以前在阿里巴巴的流量方法论

流量为王,可能对你有所启发。

2、吕先生的三个贵人

中华灵芝宝、双灵固本散、绿谷灵芝宝,你耳熟能详的东西,但可能并不真正了解真相。

3、为什么我从 Mac 换到了 Linux

它激发了我重新潜入的乐趣,我享受这段旅途的乐趣。

资源

1、9 大技术领域,1500+道面试题出炉!(资源版)

阿里技术特别策划。

2、分享一波安卓面试资料

2019 Android 面试题汇总(面试总结全)

2018最新安卓面试大全(含BAT,网易,滴滴)—-你面不上BAT的原因:面经宝典,都在这里啦 - Android - 掘金

Android面试整理(附答案) - 掘金

还原最真实最完整的一线公司面试题 - 阅读 - 掘金

Android 面试资料集锦 - 天涯海角路 - 博客园

言论

1、

我来到推特以后,才发现不管怎么沟通,一半人总是会讨厌另一半人。

– 迈克尔·阿灵顿

2、

关于钱的几个原则:

1)、不要贪婪,挣自己该挣的钱

2)、遵守契约,说好了就遵守约定

3)、掌握挣钱的能力比你现在有钱好很多。因为钱随时会没有啊。

4)、大部分挫折长期来看都是小问题

– 池建强

3、

“叮铃铃铃……”一阵清脆的铃声惊醒了哥的美梦,关上了 8848 钛金手机闹表,哥从床上缓缓爬了起来,熟练的穿上了脊柱整形明星产品背背佳,下地穿上了内含骨正基鞋垫的足力健老人鞋,嗯,浑身都暖和起来了。哥走了几步,感觉昨天做完火疗之后背痛症状没啥缓解,还不如前天的酸碱平 dds 治疗仪,“算了,可能是疗程不够!”哥心想着,拿出了一瓶本草清液吸了起来,一边打开了 e 人 e 本高端电脑,看了看股市,又咒骂了一番。嗯,这个产品味道还不赖,跟沙棘雪莲果饮料差不太多。哥心满意足的拔出吸管,咂摸着滋味。哎呀,都怪昨天小李,非得让哥多喝几杯鸿茅药酒,整得哥还有些许头晕,差点忘了服用极草 5x 含片。这含片没吃,燕之屋碗燕也喝不下去了,不知道会不会影响胶原蛋白的吸收,搞不好紫草精油还得续一个疗程。正想着,小罐茶茶具里的水沸腾了,哥赶紧拿出大师精心炒制的大红袍,晃了晃,听说好听就好茶。坐在碧玺温灸床垫上,手捧着茶杯,哥不禁感叹人生,所谓成功也不过如此吧!

——公众号读者「蜗牛的新微信号」留言

4、

真正的发现之旅不在于寻找新的土地,而在于用新的眼光来看待。

Marcel Proust

Android 面试题(6):谈谈你对 ANR 的了解?

什么是 ANR

ANR(Application Not responding),即应用程序无响应,简单来说,就是用户界面突然卡住,无法响应用户的操作(比如触摸事件)。

Android 系统对于一些事件需要在一定的时间范围内完成,如果超过预定时间能未能得到有效响应或者响应时间过长,都会造成 ANR。一般情况下,ANR 后会弹出一个提示框,告知用户当前应用无响应,用户可选择继续等待或者关闭应用。

出现场景

  • InputDispatching Timeout:5 秒内无法响应屏幕触摸事件或键盘输入事件。

  • BroadcastQueue Timeout :在执行前台广播(BroadcastReceiver)的onReceive()函数时 10s 没有处理完成,后台为 60s。

  • Service Timeout :前台服务 20s 内,后台服务在 200s 内没有执行完毕。

  • ContentProvider Timeout :ContentProvider的 publish 在 10s 内没进行完。

如何避免

基本的思路就是将 IO 操作在工作线程来处理,减少其他耗时操作和错误操作。比如网络请求、Socket 通信、SQL操作、文件读写和或者有可能阻塞 UI 线程的操作放在子线程。

ANR 分析

ANR 发生时 Logcat 会打印类似下面的日志:

1
2
3
/com.wuzy.anrtest I/zygote64: Thread[3,tid=6428,WaitingInMainSignalCatcherLoop,Thread*=0x7b3965ca00,peer=0x171c0020,"Signal Catcher"]: reacting to signal 3
/com.wuzy.anrtest I/zygote64: Wrote stack traces to '/data/anr/traces.txt'
/com.wuzy.anrtest I/Choreographer: Skipped 6000 frames! The application may be doing too much work on its main thread.

每次产生 ANR 之后,系统都会向/data/anr/traces.txt中写入新的日志数据。

获取日志的命令:

1
2
3
4
adb shell
cat /data/anr/traces.txt > /mnt/sdcard/traces.txt
exit
adb pull /sdcard/traces.txt

这里我模拟一个 ANR 情况,在按钮点击事件中调用 Thread.sleep,查看 traces 文件内容,可以看到线程名、线程优先级、线程 ID、线程状态和 ANR 的原因。

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
----- pid 13872 at 2019-11-14 15:18:50 -----
Cmd line: com.wuzy.anrtest

...

"main" prio=5 tid=1 Sleeping
| group="main" sCount=1 dsCount=0 flags=1 obj=0x735f1ad0 self=0x7b396a3a00
| sysTid=13872 nice=-10 cgrp=default sched=0/0 handle=0x7b3e6f69b0
| state=S schedstat=( 832049486 21080201 432 ) utm=77 stm=6 core=5 HZ=100
| stack=0x7fe0065000-0x7fe0067000 stackSize=8MB
| held mutexes=
at java.lang.Thread.sleep(Native method)
- sleeping on <0x026e2fdc> (a java.lang.Object)
at java.lang.Thread.sleep(Thread.java:386)
- locked <0x026e2fdc> (a java.lang.Object)
at java.lang.Thread.sleep(Thread.java:327)
at com.wuzy.anrtest.MainActivity$1.onClick(MainActivity.java:19)
at android.view.View.performClick(View.java:6291)
at android.view.View$PerformClick.run(View.java:24931)
at android.os.Handler.handleCallback(Handler.java:808)
at android.os.Handler.dispatchMessage(Handler.java:101)
at android.os.Looper.loop(Looper.java:166)
at android.app.ActivityThread.main(ActivityThread.java:7523)
at java.lang.reflect.Method.invoke(Native method)
at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:245)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:921)

ANR 监测机制

Android 应用程序是通过消息来驱动的,Android 某种意义上也可以说成是一个以消息驱动的系统,UI、事件、生命周期都和消息处理机制息息相关。Android 的 ANR 监测机制也是一样,大部分就是利用了 Android 的消息机制。

在 Android 中,实际上是系统服务在控制每个组件的生命周期回调,所以可以在这个逻辑入口开始计时,利用 Handler 机制,发生延时消息,如果超时了,就处理 ANR 事件消息,如果没有超时,就取消队列里的延时消息,也就不会出现 ANR。

具体源码细节,推荐阅读:

彻底理解安卓应用无响应机制

理解Android ANR的触发原理

理解Android ANR的信息收集过程

每周分享第 2 期

这里记录过去一周,我看到的值得分享的内容。

titu

(题图:J. Berengar Sölter )

文章

1、我们在淘宝京东拼多多买东西时,有哪些价格歧视?

作者在京东工作,对平台和商家的一些商业套路比较了解。文章深入介绍了价格歧视的原理,商家使用的价格歧视策略。

2、淘宝 1 小时交易额破千亿!

作者在双 11 前使用天猫历年交易额,做了一个多项式的拟合,预测了今年双 11 的交易额是 2692 亿,最终的结果是 2684 亿 ,不得不说,非常神奇。

img

3、想砍死北野武的女人们

文章介绍了北野武戏剧性的一生,值得一看。

“ 我认为,一个人是不是长大成熟,由他对父母的感情方式来判定。当你面对父母,觉得他们很不容易时,就是迈向成熟的第一步。”

img

4、做人,就做这样的人

人的一生,到底要怎么度过,如果你感到迷茫,充满焦虑,没有方向,可以看看这篇文章。

工具

1、AOSPXRef

Android 源码在线阅读网站,支持交叉引用跳转,服务器在国内,访问速度很快。

img

2、octohint

一款浏览器插件,在 GitHub 上浏览代码时能够快速定位变量出现或声明的位置。

img

3、GitHub Mobile

GitHub 发布首款官方手机客户端应用,目前只要 ios 的 App,安卓版本暂未发布。

github_mobile

资源

1、服务端高并发分布式架构演进之路

文章以淘宝作为例子,介绍从一百个并发到千万级并发情况下服务端的架构的演进过程,同时列举出每个演进阶段会遇到的相关技术,让大家对架构的演进有一个整体的认知,文章最后汇总了一些架构设计的原则。

img

2、命令行的艺术

熟练使用命令行是一种常常被忽视,或被认为难以掌握的技能,但实际上,它会提高你作为工程师的灵活性以及生产力。

3、Flutter 实战

由浅入深的介绍了 Flutter 技术和开发流程。

flutter

4、正则表达式手册

网站包含正则表达式全集和常见正则表达式。

5、GeeksforGeeks

这个网站包含了大量计算机相关的问题和解答,许多问题都有形象的图表和源代码。

6、HOW HTTPS WORKS

这个网站用图解的方式把 HTTP 相关的问题都说明得一清二楚,生动形象。如果你对 Http 不够熟悉,不妨看一下,肯定会很有收获的。

摘录

1、司汤达在 1806 年致波丽娜的信中,给波丽娜的几点建议:

以下是我着力要培养的习惯。

1.锻炼身体。

2.调侃邪恶之人和无聊之人的才能。

3.选择一个工作,并从中培养习惯。为达到这点,必须悉心寻找自己的主要热情所在。

4.承受得住悲伤。

5.不要过分夸大自己无法体味的幸福。

6.当上前与一个人攀谈时要自问:“他需要什么?”而不是索要什么。

7.简洁的习惯。

研习对人体有益的食物,并养成食用的习惯。反复温习上述几条,仔细思考一下以下三条警句:

1.习惯于伤悲,每个人每天都会经历七八件伤悲的事。

2.不要过分夸大不属于自己的幸福。

3.学会顶住各种艰难的时刻,致力于完善我们的思想以及知晓事理的艺术。

关于以上三条警句的思索几乎囊括了幸福的含义。

2、北野武一些好玩的言论

1.他离婚之后接受采访,北野武说:

「今年真是没啥好事,我的钱都没了,没钱后朋友都联络不上了。曝光后情妇也没了。
早知道这么难,不如和前妻在一起。」

2.他还有个很出名的渣男语录,逻辑非常奇妙,是这样说的:

「尽管没多大意思,但情人还是越多越好的。

如果只有一个情人,就会形成一种三角关系,而三角关系就是一种有棱角的关系。如果有两个呢,就是四角关系。三个呢,五角关系……

照此类推,情人越多,关系就越接近于圆,棱角也就越少。这样的话,彼此间的摩擦和风波也会减少。」

3.北野武踏入电视圈之后,没想到路异常的顺利。他对此的感想是:

「幸好没才华的人这么多,让这件事没有想象中复杂。」

4.有一次,北野武的后辈又吉直树2015年时得了芥川奖,他很直接地说:

「这可把我气坏了。当时我就想,那样的东西我也能写出来,我一定要让人惊讶原来北野武也能写出这样的东西。」

后来呢,纯粹为了赌气,他竟然真的写了一本纯爱小说![允悲]

5.在拔除从右脸横穿左脸的器械时,他在书里这样写:

我能感觉到金属棒在鼻子底下一点一点地挪出去,同时还发出嘎吱嘎吱的声音,那声音就像金属棒把我的脑汁也一并带了出来。

我说了句:「我现在完全理解了关东煮的心情。」

结果被医生怒斥:「别说蠢话!」

6.在某个他拍摄的广告发布会上,主持人热场:
「今天闪光灯真的多得让人睁不开眼啦。」

北野武:「嗯,特别像艺人外遇被曝光后的道歉记者发布会…….」

7.在欧洲,人们不知道他还是个喜剧演员,以为他是个很出名的黑帮分子。
有一次,他受英国电影协会邀请飞去伦敦,协会会长到机场接他,一路上诚惶诚恐。

「为您准备的豪华轿车突然发生故障,所以只好用这种车子来接您了,实在对不起。请您多包涵。」

他说自己几年之后才知道为什么会长是这种态度,他们成了好朋友之后会长告诉他:

「当时我心里真是吓得要死,怕你会为此杀了我。
因为我以为你是日本的黑帮头子。」

Android 面试题(5):谈谈 Handler 机制和原理?

这一系列文章致力于为 Android 开发者查漏补缺,准备面试。

所有文章首发于公众号「JaqenAndroid」,长期持续更新。

由于笔者水平有限,总结的内容难免会出现错误,欢迎留言指出,大家一起学习、交流、进步。

1、说一下 Handler 消息机制中涉及到哪些类,各自的功能是什么?

Handler 主要用于跨线程通信。涉及MessageQueue/Message/Looper/Handler 这 4 个类。

  • Message:消息,分为硬件产生的消息和软件生成的消息。

  • MessageQueue:消息队列,主要功能是向消息池投递信息 (MessageQueue.enqueueMessage) 和取走消息池的信息 (MessageQueue.next) 。

  • Handler:消息处理者,负责向消息池中发送消息 (Handler.enqueueMessage) 和处理消息 (Handler.handleMessage) 。

  • Looper:消息泵,不断循环执行 (Looper.loop) ,按分发机制将消息分发给目标处理者。

它们之间的类关系:

Looper 有一个 MessageQueue 消息队列;MessageQueue 有一组待处理的 Message;Message 中有一个用于处理消息的 Handler;Handler 中有 Looper 和 MessageQueue。

图片来源 gityuan

2、一个线程可以有几个 Looper、几个 MessageQueue 和几个 Handler?

在 Android 中,Looper 类利用了 ThreadLocal 的特性,保证了每个线程只存在一个 Looper 对象。

关于 ThreadLocal 可以看这篇文章:理解 ThreadLocal

1
2
3
4
5
6
7
8
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper(quitAllowed));
}

Looper 构造函数中创建了 MessageQueue 对象,因此一个线程只有一个 MessageQueue。

1
2
3
4
private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}

可以有多个 Handler。

Handler 在创建时与 Looper 和 MessageQueue 关联起来:

1
2
3
4
5
6
7
8
9
10
public Handler(Callback callback, boolean async) {
...
mLooper = Looper.myLooper();
if (mLooper == null) {
throw new RuntimeException(
"Can't create handler inside thread that has not called Looper.prepare()");
}
mQueue = mLooper.mQueue;
...
}

Handler 发送消息是将消息传递给 MessageQueue:

1
2
3
4
5
6
7
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
msg.target = this;
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}

注意 msg.target = this;, 这里将当前的 Handler 赋值给 Message 对象,在后面处理消息时就能依据 msg.target 区分不同的 Handler。

3、可以在子线程直接创建一个 Handler 吗?会出现什么问题,那该怎么做?

不能在子线程直接 new 一个 Handler。因为 Handler 的工作依赖于 Looper,而 Looper 又是属于某一个线程的,其他线程不能访问,所以在线程中使用 Handler 时必须要保证当前线程中 Looper 对象并且启动循环。不然会抛出异常。

1
throw new RuntimeException("Can't create handler inside thread " + Thread.currentThread() + " that has not called Looper.prepare()");

正确做法是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class LooperThread extends Thread {
public Handler mHandler;

public void run() {
Looper.prepare(); // 为线程创建 Looper 对象

mHandler = new Handler() {
public void handleMessage(Message msg) {

}
};

Looper.loop(); // 启动消息循环
}
}

4、既然线程中创建 Handler 时需要 Looper 对象,为什么主线程不用调用 Looper.prepare() 创建 Looper 对象?

在 App 启动的时候系统默认启动了一个主线程的 Looper(ActivityThread 的 main 方法中),Loop.prepareMainLooper 方法也是调用了 Looper.prepare方法,里面会创建一个不可退出的 Looper, 并 set 到 sThreadLocal 对象当中。

1
2
3
4
public static void main(String[] args) {
Looper.prepareMainLooper();
Looper.loop();
}

5、 Looper 死循环为什么不会导致应用卡死,会消耗大量资源吗?

引用 Gityuan :

对于线程即是一段可执行的代码,当可执行代码执行完成后,线程生命周期便该终止了,线程退出。而对于主线程,我们是绝不希望会被运行一段时间,自己就退出,那么如何保证能一直存活呢?简单做法就是可执行代码是能一直执行下去的,死循环便能保证不会被退出,例如,binder 线程也是采用死循环的方法,通过循环方式不同与 Binder 驱动进行读写操作,当然并非简单地死循环,无消息时会休眠。但这里可能又引发了另一个问题,既然是死循环又如何去处理其他事务呢?通过创建新线程的方式。真正会卡死主线程的操作是在回调方法 onCreate/onStart/onResume 等操作时间过长,会导致掉帧,甚至发生ANR,looper.loop本身不会导致应用卡死。

主线程的死循环一直运行是不是特别消耗CPU资源呢? 其实不然,这里就涉及到 Linux pipe/epoll 机制,简单说就是在主线程的 MessageQueue 没有消息时,便阻塞在 Loop 的 queue.next() 中的 nativePollOnce() 方法里,此时主线程会释放 CPU 资源进入休眠状态,直到下个消息到达或者有事务发生,通过往 pipe 管道写端写入数据来唤醒主线程工作。这里采用的epoll机制,是一种IO多路复用机制,可以同时监控多个描述符,当某个描述符就绪(读或写就绪),则立刻通知相应程序进行读或写操作,本质同步 I/O,即读写是阻塞的。 所以说,主线程大多数时候都是处于休眠状态,并不会消耗大量 CPU 资源。

详细解答:Android中为什么主线程不会因为Looper.loop()里的死循环卡死?

6、 MessageQueue 是队列吗?它是什么数据结构?

MessageQueue 不是队列,它内部使用一个 Message 链表实现消息的存和取。 链表的排列依据是 Message.when,表示 Message 期望被分发的时间,该值是 SystemClock. uptimeMillis()delayMillis 之和。

##7、 handler.postDelayed() 函数延时执行计时是否准确?

当上一个消息存在耗时任务的时候,会占用延时任务执行的时机,实际延迟时间可能会超过预设延时时间,这时候就不准确了。

##8、 你对 IdleHandler 有多少了解?

IdleHandler 是一个接口, 这个接口方法是在消息队列全部处理完成后或者是在阻塞的过程中等待更多的消息的时候调用的,返回值 false 表示只回调一次,true 表示可以接收多次回调。

1
2
3
4
5
6
Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
@Override
public boolean queueIdle() {
return false;
}
});

##9、 你了解 HandlerThread 吗?

HandlerThread 继承自 Thread,它是一种可以使用 Handler 的 Thread,它的实现也很简单,在 run方法中也是通过 Looper.prepare() 来创建消息队列,并通过Looper.loop()来开启消息循环(与我们手动创建方法基本一致),这样在实际的使用中就允许在 HandlerThread 中创建 Handler 了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class HandlerThread extends Thread {
@Override
public void run() {
mTid = Process.myTid();
Looper.prepare();
synchronized (this) {
mLooper = Looper.myLooper();
notifyAll();
}
Process.setThreadPriority(mPriority);
onLooperPrepared();
Looper.loop();
mTid = -1;
}
}

由于 HandlerThread 的run方法是一个无限循环,因此当不需要使用的时候通过quit或者quitSafely方法来终止线程的执行。

##10、 你对 Message.obtain() 了解吗, 或者你知道怎么维护消息池吗 ?

Message.obtain() 是从消息池取 Message,消息池其实是使用 Message 链表结构实现,消息池默认最大值 50。 Message.obtain() 每次都是把消息池表头的 Message 取走 ,再把表头指向 next。

1
2
3
4
5
6
7
8
9
10
11
12
13
public static Message obtain() {
synchronized (sPoolSync) {
if (sPool != null) {
Message m = sPool;
sPool = m.next;
m.next = null; //从sPool中取出一个Message对象,并消息链表断开
m.flags = 0; // 清除in-use flag
sPoolSize--; //消息池的可用大小进行减1操作
return m;
}
}
return new Message(); // 当消息池为空时,直接创建Message对象
}

消息在 loop 中被 handler 分发消费之后会执行回收的操作,将该消息内部数据清空并添加到消息链表的表头。

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
public void recycle() {
if (isInUse()) { //判断消息是否正在使用
if (gCheckRecycle) { //Android 5.0以后的版本默认为true,之前的版本默认为false.
throw new IllegalStateException("This message cannot be recycled because it is still in use.");
}
return;
}
recycleUnchecked();
}

//对于不再使用的消息,加入到消息池
void recycleUnchecked() {
//将消息标示位置为IN_USE,并清空消息所有的参数。
flags = FLAG_IN_USE;
what = 0;
arg1 = 0;
arg2 = 0;
obj = null;
replyTo = null;
sendingUid = -1;
when = 0;
target = null;
callback = null;
data = null;
synchronized (sPoolSync) {
if (sPoolSize < MAX_POOL_SIZE) { //当消息池没有满时,将Message对象加入消息池
next = sPool;
sPool = this;
sPoolSize++; //消息池的可用大小进行加1操作
}
}
}

最后,关于 Handler 的详细分析推荐阅读 Gityuan 的文章。

Android消息机制1-Handler(Java层)

Android消息机制2-Handler(Native层)

Android消息机制3-Handler(实战)

每周分享第 1 期

这里记录过去一周,我看到的值得分享的内容。

titu

(题图:大学一角,2019.10)

非常喜欢阮一峰老师的科技互联网周刊,增长了不少见识,也很佩服阮老师的坚持。信息爆炸的时代,能够阅读到优质内容很不容易。

受阮老师的启发,将每周看到的优质内容分享出来,应该挺有价值的。其实,分享本身就是一件有趣有意义的事。

文章

1、GQ报道 | 幸存者李佳琦:一个人变成算法,又想回到人

万字长文,GQ 带你走近当事人李佳琦,也许和你想象的不一样。

“李佳琦”这个符号是这台精密齿轮上最关键的部件,他也无法停止直播:“我不播了,那外面我的同事们怎么办?” 外面的同事怎么办?

2、你适合搞副业么?

其实搞副业这事,如小马过河,有的适合,有的不适合,因人而异。有的人副业发达,甚至副业转正。有的人捡了副业的芝麻丢了主业的西瓜,明显得不偿失。有的人为了追求副业收益被人狠狠收割。

3、这些年我从互联网收获的三桶金和五点感悟

别人的路也许复制不了,但是思维模式可以学习借鉴。

4、为刘润公号读者写了600多篇文章后,说说我的4点感悟

写作是每个人都应该学习的技能。

5、华为的“信任危机”

华为应该是今年最受关注的公司,这次华为 HR 胡玲发表在华为心声社区的实名控诉信,也再一次让华为成为舆论的热点。

寓言

1、小马过河

马棚里住着一匹老马和一匹小马。

有一天,老马对小马说:“你已经长大了,能帮妈妈做点事吗?”小马连蹦带跳地说:“怎么不能?我很愿意帮您做事。”老马高兴地说:“那好哇,你把这半口袋麦子驮到磨坊去吧。”

小马驮起麦子,飞快地往磨坊跑去。跑着跑着,一条小河挡住了去路,河水哗哗地流着。小马为难了,心想:我能不能过去呢?如果妈妈在身边,问问她该怎么办,那多好哇!

他向四周望望,看见一头老牛在河边吃草。小马嗒嗒嗒跑过去,问道:“牛伯伯,请您告诉我,这条河,我能趟过去吗?”老牛说:“水很浅,刚没小腿,能趟过去。”

小马听了老牛的话,立刻跑到河边,准备趟过去。突然,从树上跳下一只松鼠,拦住他大叫:“小马,别过河,别过河,河水会淹死你的!”小马吃惊地问:“水很深吗?”松鼠认真地说:“深得很呢!昨天,我的一个伙伴就是掉进这条河里淹死的!”

小马连忙收住脚步,不知道怎么办才好。他叹了口气,说:“唉!还是回家问问妈妈吧!”

小马甩甩尾巴,跑回家去。妈妈问:“怎么回来啦?”小马难为情地说:“一条河挡住了,我……我过不去。”妈妈说:“那条河不是很浅吗?”小马说:“是啊!牛伯伯也这么说。可是松鼠说河水很深,还淹死过他的伙伴呢!”妈妈说:“那么河水到底是深还是浅?你仔细想过他们的话吗?”小马低下了头,说:“没……没想过。”妈妈亲切地对小马说:“孩子,光听别人说,自己不动脑筋,不去试试,是不行的。河水是深是浅,你去试一试就会明白了。”

小马跑到河边,刚刚抬起前蹄,松鼠又大叫起来:“怎么,你不要命啦!”小马说:“让我试试吧。”他下了河,小心地趟了过去。原来河水既不像老牛说的那样浅,也不像松鼠说的那样深。

2、父子骑驴

在一个炎热的下午,一位父亲带着他儿子和一头驴走过满足灰尘的街。

父亲骑在驴上,儿子牵着它走。“可怜的孩子,”一位路人说道,“这个人怎能心安理得地骑在驴背上…”

父亲听到之后,就从驴背上下来让儿子坐上去。但走了没多久,又一位路人的声音传来“多么不孝。可怜的老父亲却在一旁跟着跑。小孩子听了之后连忙让父亲也坐在驴背上。

“你们谁见过这种事”一位妇女说道,“这么残酷地对待动物,可怜的驴子的背在下陷,而这个老家伙和他的儿子却悠然自得。”

父子俩闻言,只好从驴背上爬下来。但是,他们徒步走没多远,又一个陌生人笑着说:“我才不会这么蠢,放着好好的驴不用,却用脚来走。”

最后,人们看到这对父子俩抬着这头驴从街头走过。

工具

1、markdown-nice

支持自定义样式的微信 Markdown 排版工具,可能是目前最好用的。

言论

1、

人呐,往往一辈子要利还要名,就为个光宗耀祖。

– 三表龙门阵《李佳琦的阶级欲望

2、

大家 955 的时候,有些聪明人觉得如果自己 996 那么裁掉的就是别人了。结果聪明人太多了。

3、

你妈不知道的,你知道的,叫网红。

你妈知道的,你也知道的,叫明星。

你妈知道的,你知道的,你姥姥也知道的,叫大明星。

你妈知道的,你不知道的,叫过气明星。

– 三表龙门阵《李佳琦的阶级欲望

4、

计算机行业是个很有活力的行业,这个行业创造了很多让人羡慕的职位,比如:1)命令行运维(能够在十个以上的终端窗口来回切换输入命令还不出错);2)功能程序员(能够一个月做10个以上的功能);3)周报经理(能够把周报写得左手一条龙右手一个彩虹);4)PPT架构师(能够在PPT上把架构画成顶尖水平);5)JS全栈工程师(只掌握JS一门语言,有百年以上足够的耐心等待JS成为所有软件的语言);6)三句话产品经理(能够下面用三句话就能让开发把产品实现出来,比如:你我们的竞对有!客户就是要!告诉我能不能做?)

– 左耳朵耗子

Android 面试题(4):谈谈 Activity 的启动模式

这一系列文章致力于为 Android 开发者查漏补缺,面试准备。

所有文章首发于公众号「JaqenAndroid」,长期持续更新。

由于笔者水平有限,总结的答案难免会出现错误,欢迎留言指出,大家一起学习、交流、进步。

众所周知,Activity 有 4 种启动模式,分别是:standard、singleTop、singleTask 和 singleInstance,它们控制了 Activity 的启动行为,不同的启动模式使用于不同的应用场景。

启动的 Activity 会放在任务栈中,任务栈是一种后进先出的结构,按 Back 键的时候栈顶 Activity 会从任务栈中返回,当任务栈为空时系统就会回收这个任务栈。

本文将通过具体 Demo,详细分析这几种模式的差异和使用场景。

standard 标准模式

1
android:launchMode="standard"

Activity 默认的启动模式,每次启动都会创建新的实例,不管这个实例是否已经存在于任务栈。

1
2
3
4
5
6
7
8
9
// 启动顺序
MainActivity -> StandardActivity -> StandardActivity

// 栈内容(adb shell dumpsys activity activities)
Running activities (most recent first):
TaskRecord{b4e290f #18017 A=com.wuzy.androidlaunchmodetest U=0 StackId=1 sz=3}
Run #2: ActivityRecord{1666db1 u0 com.wuzy.androidlaunchmodetest/.StandardActivity t18017}
Run #1: ActivityRecord{17fa3e6 u0 com.wuzy.androidlaunchmodetest/.StandardActivity t18017}
Run #0: ActivityRecord{9e18184 u0 com.wuzy.androidlaunchmodetest/.MainActivity t18017}

启动两次 StandardActivity 会创建两个 StandardActivity 的实例对象。

使用场景

在一系列启动 Activity 的过程中需要保留用户操作的 Activity 的页面。比如: 社交应用中,点击查看用户 A 信息 -> 查看用户 A 粉丝 -> 在粉丝中挑选查看用户 B 信息 -> 查看用户 B 粉丝。

singleTop 栈顶复用模式

1
android:launchMode="singleTop"

singleTop 与 standard 几乎一样,使用 singleTop 的 Activity 也可以创建多个实例。不同点在于,如果启动的 Activity 已经位于任务栈的栈顶,则不需要创建新的实例,直接复用栈顶的 Activity 实例,intent 通过 Activity 的onNewIntent 方法传递到这个 Activity 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
例 1:
// 启动顺序
MainActivity -> SingleTopActivity -> SingleTopActivity

// 栈内容
Running activities (most recent first):
TaskRecord{9de11c2 #18073 A=com.wuzy.androidlaunchmodetest U=0 StackId=1 sz=2}
Run #1: ActivityRecord{f0acc0c u0 com.wuzy.androidlaunchmodetest/.SingleTopActivity t18073}
Run #0: ActivityRecord{b884d57 u0 com.wuzy.androidlaunchmodetest/.MainActivity t18073}

-----------------------------------------------------------------------------------------

例 2:
// 启动顺序
MainActivity -> SingleTopActivity -> StandardActivity -> SingleTopActivity

// 栈内容
Running activities (most recent first):
TaskRecord{9de11c2 #18073 A=com.wuzy.androidlaunchmodetest U=0 StackId=1 sz=4}
Run #3: ActivityRecord{c282b33 u0 com.wuzy.androidlaunchmodetest/.SingleTopActivity t18073}
Run #2: ActivityRecord{76fb23e u0 com.wuzy.androidlaunchmodetest/.StandardActivity t18073}
Run #1: ActivityRecord{b6969a8 u0 com.wuzy.androidlaunchmodetest/.SingleTopActivity t18073}
Run #0: ActivityRecord{b884d57 u0 com.wuzy.androidlaunchmodetest/.MainActivity t18073}

例 1 中由于栈顶已经是 SingleTopActivity,再启动 SingleTopActivity 时直接复用了栈顶 Activity,无需创建新的实例。

例 2 中第二次启动 SingleTopActivity 时,由于栈顶是 StandardActivity,所以启动 SingleTopActivity 时会创建新的实例。

使用场景

假设你在当前的 Activity 中又要启动同类型的 Activity,此时建议将此类型 Activity 的启动模式指定为 singleTop,能够减少 Activity 的创建,节省内存。

singleTask 栈内复用模式

1
android:launchMode="singleTask" 

singleTask 标记的 Activity 是栈内复用模式,如果当前任务栈内没有这个 Activity,那么创建新的 Activity,如果当前任务栈内有这个 Activity,不管它在任务栈的哪个位置,都会直接复用这个 Activity,这个 Activity 上面的其他的 Activity 都被移出栈, intent 通过 onNewIntent 传递到这个 Activity 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 1、启动顺序
MainActivity -> SingleTaskActivity -> StandardActivity

// 栈内容:
Running activities (most recent first):
TaskRecord{ebb2593 #18095 A=com.wuzy.androidlaunchmodetest U=0 StackId=1 sz=3}
Run #2: ActivityRecord{49b6bb8 u0 com.wuzy.androidlaunchmodetest/.StandardActivity t18095}
Run #1: ActivityRecord{48628d2 u0 com.wuzy.androidlaunchmodetest/.SingleTaskActivity t18095}
Run #0: ActivityRecord{86fe71f u0 com.wuzy.androidlaunchmodetest/.MainActivity t18095}

-----------------------------------------------------------------------------------------

// 2、启动顺序
MainActivity -> SingleTaskActivity -> StandardActivity -> SingleTaskActivity

// 栈内容
Running activities (most recent first):
TaskRecord{ebb2593 #18095 A=com.wuzy.androidlaunchmodetest U=0 StackId=1 sz=2}
Run #1: ActivityRecord{48628d2 u0 com.wuzy.androidlaunchmodetest/.SingleTaskActivity t18095}
Run #0: ActivityRecord{86fe71f u0 com.wuzy.androidlaunchmodetest/.MainActivity t18095}

可以看到,在第二次启动 SingleTaskActivity 时,由于栈内已经存在了 SingleTaskActivity 实例,栈顶 StandardActivity 被移出任务栈,复用了栈内 SingleTaskActivity 实例。

当以 singleTask 启动一个 Activity 的时候,首先去判断是否要为该 Activity 去创建一个任务栈?如果需要的话,那么就会创建一个任务栈,并且将该 Activity 放入栈中;如果不需要的话,直接将该 Activity 放入当前的任务栈中。

那么如何判断要不要为 singleTask Activity 创建一个任务栈?

任务栈的创建跟 taskAffinity 的属性相关,每个 Activity 都有 taskAffinity 属性,这个属性指出了它希望进入的任务栈。如果一个 Activity 没有显式的指明该 Activity 的 taskAffinity,那么它的这个属性就等于 Application 指明的 taskAffinity,如果 Application 也没有指明,那么该 taskAffinity 的值就等于包名。

这里我指定一下 Activity 的 taskAffinity :

1
2
3
4
5
<activity
android:name=".SingleTaskWithAffinityActivity"
android:label="SingleTaskWithAffinity Activity"
android:launchMode="singleTask"
android:taskAffinity="com.jaqen" />

看一下测试结果:

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
// 1、启动顺序
MainActivity -> SingleTaskWithAffinityActivity

// 任务栈
Running activities (most recent first):
TaskRecord{fa7e695 #18097 A=com.jaqen U=0 StackId=1 sz=1}
Run #1: ActivityRecord{6267f33 u0 com.wuzy.androidlaunchmodetest/.SingleTaskWithAffinityActivity t18097}
TaskRecord{efcc3aa #18096 A=com.wuzy.androidlaunchmodetest U=0 StackId=1 sz=1}
Run #0: ActivityRecord{ccdada8 u0 com.wuzy.androidlaunchmodetest/.MainActivity t18096}

-----------------------------------------------------------------------------------------

// 2、启动顺序
MainActivity -> SingleTaskWithAffinityActivity -> StandardActivity

// 栈内容
Running activities (most recent first):
TaskRecord{3f4af35 #18097 A=com.jaqen U=0 StackId=1 sz=2}
Run #2: ActivityRecord{23b4c1a u0 com.wuzy.androidlaunchmodetest/.StandardActivity t18097}
Run #1: ActivityRecord{d234ee0 u0 com.wuzy.androidlaunchmodetest/.SingleTaskWithAffinityActivity t18097}
TaskRecord{e27d53b #18096 A=com.wuzy.androidlaunchmodetest U=0 StackId=1 sz=1}
Run #0: ActivityRecord{f445d23 u0 com.wuzy.androidlaunchmodetest/.MainActivity t18096}

-----------------------------------------------------------------------------------------

// 3、启动顺序
MainActivity -> SingleTaskWithAffinityActivity -> StandardActivity -> SingleTaskWithAffinityActivity

// 栈内容
Running activities (most recent first):
TaskRecord{3f4af35 #18097 A=com.jaqen U=0 StackId=1 sz=1}
Run #1: ActivityRecord{d234ee0 u0 com.wuzy.androidlaunchmodetest/.SingleTaskWithAffinityActivity t18097}
TaskRecord{e27d53b #18096 A=com.wuzy.androidlaunchmodetest U=0 StackId=1 sz=1}
Run #0: ActivityRecord{f445d23 u0 com.wuzy.androidlaunchmodetest/.MainActivity t18096}

首次启动 SingleTaskWithAffinityActivity 会创建新的任务栈(大括号内 # 后的数字标识任务栈 id)。

在 SingleTaskWithAffinityActivity 启动 StandardActivity , 这个 StandardActivity 与 SingleTaskWithAffinityActivity 在同一个栈。

SingleTaskWithAffinityActivity 会出现在多任务界面。

第二次启动 SingleTopActivity 时直接复用了栈内已存 Activity,已存 Activity 上的 Activity 被移出任务栈。

使用场景

一般应用主页面可以用 singleTask 方式。比如用户在主页跳转到其他页面,运行多次操作后想返回到主页。

singleInstance 单实例模式

1
android:launchMode="singleInstance"

singleInstance 与 singleTask 类似,在应用都只存在一个实例,不同点在于存放 singleInstance Activity 实例的任务栈只能存放唯一的 singleInstance Activity。

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
// 1、启动顺序
MainActivity -> SingleInstanceActivity

// 栈内容
Running activities (most recent first):
TaskRecord{cd22626 #18116 A=com.wuzy.androidlaunchmodetest U=0 StackId=1 sz=1}
Run #1: ActivityRecord{462d9a2 u0 com.wuzy.androidlaunchmodetest/.SingleInstanceActivity t18116}
TaskRecord{c2b08bd #18115 A=com.wuzy.androidlaunchmodetest U=0 StackId=1 sz=1}
Run #0: ActivityRecord{812c79c u0 com.wuzy.androidlaunchmodetest/.MainActivity t18115}

-----------------------------------------------------------------------------------------

// 2、启动顺序
MainActivity -> SingleInstanceActivity -> StandardActivity

// 栈内容
Running activities (most recent first):
TaskRecord{e46fd18 #18115 A=com.wuzy.androidlaunchmodetest U=0 StackId=1 sz=2}
Run #2: ActivityRecord{540b885 u0 com.wuzy.androidlaunchmodetest/.StandardActivity t18115}
TaskRecord{5cab9d7 #18116 A=com.wuzy.androidlaunchmodetest U=0 StackId=1 sz=1}
Run #1: ActivityRecord{18780a3 u0 com.wuzy.androidlaunchmodetest/.SingleInstanceActivity t18116}
TaskRecord{e46fd18 #18115 A=com.wuzy.androidlaunchmodetest U=0 StackId=1 sz=2}
Run #0: ActivityRecord{6ceacf3 u0 com.wuzy.androidlaunchmodetest/.MainActivity t18115}

-----------------------------------------------------------------------------------------

// 3、启动顺序
MainActivity -> SingleInstanceActivity -> StandardActivity -> SingleInstanceActivity

// 栈内容
Running activities (most recent first):
TaskRecord{e46fd18 #18115 A=com.wuzy.androidlaunchmodetest U=0 StackId=1 sz=2}
Run #2: ActivityRecord{540b885 u0 com.wuzy.androidlaunchmodetest/.StandardActivity t18115}
TaskRecord{5cab9d7 #18116 A=com.wuzy.androidlaunchmodetest U=0 StackId=1 sz=1}
Run #1: ActivityRecord{18780a3 u0 com.wuzy.androidlaunchmodetest/.SingleInstanceActivity t18116}
TaskRecord{e46fd18 #18115 A=com.wuzy.androidlaunchmodetest U=0 StackId=1 sz=2}
Run #0: ActivityRecord{6ceacf3 u0 com.wuzy.androidlaunchmodetest/.MainActivity t18115}

启动 SingleInstanceActivity 会创建新的任务栈,从 SingleInstanceActivity 中启动 StandardActivity,StandardActivity 会被放到上一个任务栈中。

再此启动 SingleInstanceActivity,SingleInstanceActivity 会被复用。

使用场景

singleInstance 模式常应用于独立栈操作的应用,如闹钟的提醒页面,当你在A应用中看视频时,闹钟响了,你点击闹钟提醒通知后进入提醒详情页面,然后点击返回就再次回到A的视频页面,这样就不会过多干扰到用户先前的操作了。

Intent Flags

除了在 manifest 文件中设置 launchMode 之外,还可以在 Intent 中设置 Flag 达到同样的效果。

常见几种 Flag:

1、FLAG_ACTIVITY_NEW_TASK

在 google 的官方文档中介绍,它与 launchMode="singleTask" 具有相同的行为。实际上,并不是完全相同!具体看下面的案例分析。

2、FLAG_ACTIVITY_SINGLE_TOP

等同于 launchMode="singleTop"

3、FLAG_ACTIVITY_CLEAR_TOP

清除包含目标 Activity 的任务栈中位于该 Activity 实例之上的其他 Activity 实例。 但是是复用已有的目标 Activity,还是先删除后重建,则有以下规则:

  • 若是使用 FLAG_ACTIVITY_SINGLE_TOP 和 FLAG_ACTIVITY_CLEAR_TOP 标志位组合,那么不管目标 Activity 是什么启动模式,都会被复用。

  • 若是单独使用 FLAG_ACTIVITY_CLEAR_TOP,那么只有非 standard 启动模式的目标 Activity 才会被复用,否则都先被删除,然后被重新创建并入栈。

4、FLAG_ACTIVITY_CLEAR_TASK

首先清空已经存在的目标 Activity 实例所在的任务栈,这自然也就清除了之前存在的目标 Activity 实例,然后创建新的目标 Activity 实例并入栈。

通过几个案例查看 Flag 的使用效果。

  • MainActivity 为 standard 模式,未设置 Flag。

  • IntentFlagTestActivity 为 standard 模式,未设置 taskAffinity。

1
2
3
<activity
android:name=".IntentFlagTestActivity"
android:label="IntentFlagTestActivity" />
  • IntentFlagTestWithAffinityActivity 为 standard 模式,设置与 MainActivity 不同的 taskAffinity。
1
2
3
<activity android:name=".IntentFlagTestWithAffinityActivity"
android:taskAffinity="com.jaqen"
android:label="IntentFlagTestWithAffinityActivity"/>

1、单独使用 FLAG_ACTIVITY_NEW_TASK

  • taskAffinity 相同时:
1
2
3
4
// 启动 IntentFlagTestActivity 的方式
Intent intent = new Intent(MainActivity.this, IntentFlagTestActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);

测试结果:

1
2
3
4
5
6
7
8
9
10
// 启动流程
MainActivity -> IntentFlagTestActivity -> MainActivity -> IntentFlagTestActivity

// 栈内容
Running activities (most recent first):
TaskRecord{89317d5 #18128 A=com.wuzy.androidlaunchmodetest U=0 StackId=1 sz=4}
Run #3: ActivityRecord{2f1ac92 u0 com.wuzy.androidlaunchmodetest/.IntentFlagTestActivity t18128}
Run #2: ActivityRecord{a0104eb u0 com.wuzy.androidlaunchmodetest/.MainActivity t18128}
Run #1: ActivityRecord{9b84b56 u0 com.wuzy.androidlaunchmodetest/.IntentFlagTestActivity t18128}
Run #0: ActivityRecord{f9a57f4 u0 com.wuzy.androidlaunchmodetest/.MainActivity t18128}

从任务栈可以看出,在 taskAffinity 相同的情况下,单独使用 FLAG_ACTIVITY_NEW_TASK 不会产生任何效果!

  • taskAffinity 不同时:
1
2
3
4
// 启动 IntentFlagTestWithAffinityActivity 的方式
Intent intent = new Intent(MainActivity.this, IntentFlagTestWithAffinityActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);

测试结果:

1
2
3
4
5
6
7
8
9
10
// 启动流程
MainActivity -> IntentFlagTestWithAffinityActivity -> MainActivity -> IntentFlagTestWithAffinityActivity

// 栈内容
Running activities (most recent first):
TaskRecord{d07e70 #18135 A=com.jaqen U=0 StackId=1 sz=2}
Run #2: ActivityRecord{997119c u0 com.wuzy.androidlaunchmodetest/.MainActivity t18135}
Run #1: ActivityRecord{8a7f641 u0 com.wuzy.androidlaunchmodetest/.IntentFlagTestWithAffinityActivity t18135}
TaskRecord{84f526e #18134 A=com.wuzy.androidlaunchmodetest U=0 StackId=1 sz=1}
Run #0: ActivityRecord{5aca49d u0 com.wuzy.androidlaunchmodetest/.MainActivity t18134}

在 taskAffinity 不同的情况下, 添加 FLAG_ACTIVITY_NEW_TASK 确实产生了一些效果,第一次启动 IntentFlagTestWithAffinityActivity 创建了新的任务栈,但是第二次从 MainActivity 中启动 IntentFlagTestWithAffinityActivity 时,没有任何反应。

结论:

单独使用 FLAG_ACTIVITY_NEW_TASK 并不会产生与 singleTask 相同的效果

2、FLAG_ACTIVITY_NEW_TASK + FLAG_ACTIVITY_CLEAR_TOP

  • taskAffinity 相同时:
1
2
3
4
// 启动 IntentFlagTestActivity 的方式
Intent intent = new Intent(MainActivity.this, IntentFlagTestActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);

测试结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 1、启动流程
MainActivity -> IntentFlagTestActivity -> MainActivity

// 栈内容
Running activities (most recent first):
TaskRecord{7bb1982 #18139 A=com.wuzy.androidlaunchmodetest U=0 StackId=1 sz=3}
Run #2: ActivityRecord{535c0d0 u0 com.wuzy.androidlaunchmodetest/.MainActivity t18139}
Run #1: ActivityRecord{c5253ff u0 com.wuzy.androidlaunchmodetest/.IntentFlagTestActivity t18139}
Run #0: ActivityRecord{1b04db1 u0 com.wuzy.androidlaunchmodetest/.MainActivity t18139}

-----------------------------------------------------------------------------------------

// 2、启动流程
MainActivity -> IntentFlagTestActivity -> MainActivity -> IntentFlagTestActivity

// 栈内容
Running activities (most recent first):
TaskRecord{7bb1982 #18139 A=com.wuzy.androidlaunchmodetest U=0 StackId=1 sz=2}
Run #1: ActivityRecord{705bf3 u0 com.wuzy.androidlaunchmodetest/.IntentFlagTestActivity t18139}
Run #0: ActivityRecord{1b04db1 u0 com.wuzy.androidlaunchmodetest/.MainActivity t18139}

在 taskAffinity 相同情况下,FLAG_ACTIVITY_NEW_TASK + FLAG_ACTIVITY_CLEAR_TOP 不会创建新的任务栈。

貌似和 singleTask 启动模式效果相同,但是细看会发现区别:前后两次 IntentFlagTestActivity 并不是同一个实例,也就是并没有复用栈内的 IntentFlagTestActivity,而是清除了 IntentFlagTestActivity 本身及其之上的所有 Activity,然后新建 IntentFlagTestActivity 实例添加到当前任务栈。

  • taskAffinity 不同时:
1
2
3
4
// 启动 IntentFlagTestWithAffinityActivity 的方式
Intent intent = new Intent(MainActivity.this, IntentFlagTestWithAffinityActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);

测试结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 1、启动流程
MainActivity -> IntentFlagTestWithAffinityActivity -> MainActivity

// 任务栈
Running activities (most recent first):
TaskRecord{a1b1a38 #18152 A=com.jaqen U=0 StackId=1 sz=2}
Run #2: ActivityRecord{ae8c352 u0 com.wuzy.androidlaunchmodetest/.MainActivity t18152}
Run #1: ActivityRecord{5647f4a u0 com.wuzy.androidlaunchmodetest/.IntentFlagTestWithAffinityActivity t18152}
TaskRecord{e2f8776 #18151 A=com.wuzy.androidlaunchmodetest U=0 StackId=1 sz=1}
Run #0: ActivityRecord{864a2f5 u0 com.wuzy.androidlaunchmodetest/.MainActivity t18151}

-----------------------------------------------------------------------------------------

// 2、启动流程
MainActivity -> IntentFlagTestWithAffinityActivity -> MainActivity -> IntentFlagTestWithAffinityActivity

Running activities (most recent first):
TaskRecord{a1b1a38 #18152 A=com.jaqen U=0 StackId=1 sz=1}
Run #1: ActivityRecord{cf5fce6 u0 com.wuzy.androidlaunchmodetest/.IntentFlagTestWithAffinityActivity t18152}
TaskRecord{e2f8776 #18151 A=com.wuzy.androidlaunchmodetest U=0 StackId=1 sz=1}
Run #0: ActivityRecord{864a2f5 u0 com.wuzy.androidlaunchmodetest/.MainActivity t18151}

可见,与 taskAffinity 相同类似(除了创建新的任务栈),在第二次启动 IntentFlagTestWithAffinityActivity 时也是直接清除了 IntentFlagTestWithAffinityActivity 自身及其之上所有的 Activity,然后创建新的 IntentFlagTestWithAffinityActivity 实例添加到任务栈中。

结论:

FLAG_ACTIVITY_NEW_TASK + FLAG_ACTIVITY_CLEAR_TOP 标志位组合产生的效果总体上和 singleTask 模式相同,但不会复用 Activity。

3、FLAG_ACTIVITY_NEW_TASK + FLAG_ACTIVITY_CLEAR_TASK

  • taskAffnity 相同时:
1
2
3
4
// 启动 IntentFlagTestActivity 的方式
Intent intent = new Intent(MainActivity.this, IntentFlagTestActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(intent);

测试结果:

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
// 1、启动流程
MainActivity -> IntentFlagTestActivity

// 栈内容
Running activities (most recent first):
TaskRecord{572ae8d #18253 A=com.wuzy.androidlaunchmodetest U=0 StackId=1 sz=1}
Run #0: ActivityRecord{e0aa3a u0 com.wuzy.androidlaunchmodetest/.IntentFlagTestActivity t18253}

-----------------------------------------------------------------------------------------

// 2、启动流程
MainActivity -> IntentFlagTestActivity -> MainActivity

// 栈内容
Running activities (most recent first):
TaskRecord{572ae8d #18253 A=com.wuzy.androidlaunchmodetest U=0 StackId=1 sz=2}
Run #1: ActivityRecord{65fb5c2 u0 com.wuzy.androidlaunchmodetest/.MainActivity t18253}
Run #0: ActivityRecord{e0aa3a u0 com.wuzy.androidlaunchmodetest/.IntentFlagTestActivity t18253}

-----------------------------------------------------------------------------------------

// 3、启动流程
MainActivity -> IntentFlagTestActivity -> MainActivity -> IntentFlagTestActivity

// 栈内容
Running activities (most recent first):
TaskRecord{572ae8d #18253 A=com.wuzy.androidlaunchmodetest U=0 StackId=1 sz=1}
Run #0: ActivityRecord{9eccfa3 u0 com.wuzy.androidlaunchmodetest/.IntentFlagTestActivity t18253}

可见, 当通过FLAG_ACTIVITY_NEW_TASK + FLAG_ACTIVITY_CLEAR_TASK 标志位组合启动 IntentFlagTestActivity 时,首先会清空 IntentFlagTestActivity 所在的任务栈,然后再创建新的 IntentFlagTestActivity 实例并入栈。

  • taskAffnity 不同时:
1
2
3
4
// 启动 IntentFlagTestWithAffinityActivity 的方式
Intent intent = new Intent(MainActivity.this, IntentFlagTestWithAffinityActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(intent);

测试结果:

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
// 1、启动流程
MainActivity -> IntentFlagTestWithAffinityActivity

// 栈内容
Running activities (most recent first):
TaskRecord{c081b17 #18257 A=com.jaqen U=0 StackId=1 sz=1}
Run #1: ActivityRecord{8f8a3c5 u0 com.wuzy.androidlaunchmodetest/.IntentFlagTestWithAffinityActivity t18257}
TaskRecord{60908ed #18256 A=com.wuzy.androidlaunchmodetest U=0 StackId=1 sz=1}
Run #0: ActivityRecord{5924b86 u0 com.wuzy.androidlaunchmodetest/.MainActivity t18256}

-----------------------------------------------------------------------------------------

// 2、启动流程
MainActivity -> IntentFlagTestWithAffinityActivity -> MainActivity

// 栈内容
Running activities (most recent first):
TaskRecord{c081b17 #18257 A=com.jaqen U=0 StackId=1 sz=2}
Run #2: ActivityRecord{7e1a8a0 u0 com.wuzy.androidlaunchmodetest/.MainActivity t18257}
Run #1: ActivityRecord{8f8a3c5 u0 com.wuzy.androidlaunchmodetest/.IntentFlagTestWithAffinityActivity t18257}
TaskRecord{60908ed #18256 A=com.wuzy.androidlaunchmodetest U=0 StackId=1 sz=1}
Run #0: ActivityRecord{5924b86 u0 com.wuzy.androidlaunchmodetest/.MainActivity t18256}

-----------------------------------------------------------------------------------------

// 3、启动流程
MainActivity -> IntentFlagTestWithAffinityActivity -> MainActivity -> IntentFlagTestWithAffinityActivity

// 栈内容
Running activities (most recent first):
TaskRecord{c081b17 #18257 A=com.jaqen U=0 StackId=1 sz=1}
Run #1: ActivityRecord{e52a3c0 u0 com.wuzy.androidlaunchmodetest/.IntentFlagTestWithAffinityActivity t18257}
TaskRecord{60908ed #18256 A=com.wuzy.androidlaunchmodetest U=0 StackId=1 sz=1}
Run #0: ActivityRecord{5924b86 u0 com.wuzy.androidlaunchmodetest/.MainActivity t18256}

结果与 taskAffnity 相同情况下类似, 首先会清空 IntentFlagTestWithAffinityActivity 所在的任务栈,然后再创建新的 IntentFlagTestWithAffinityActivity 实例并入栈,这和 taskAffinity 属性相同是一致的效果,只不过这里第一次为 IntentFlagTestWithAffinityActivity 创建了新的任务栈。

结论:

FLAG_ACTIVITY_NEW_TASK + FLAG_ACTIVITY_CLEAR_TASK 标志位组会先清空任务栈,再创建新的 Activity 实例入栈。

4、单独使用 FLAG_ACTIVITY_CLEAR_TOP

  • IntentFlagTestActivity 启动模式:standard
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
// 1、启动流程
MainActivity -> IntentFlagTestActivity

// 栈内容
Running activities (most recent first):
TaskRecord{b6045f3 #18282 A=com.wuzy.androidlaunchmodetest U=0 StackId=1 sz=2}
Run #1: ActivityRecord{44dcf5f u0 com.wuzy.androidlaunchmodetest/.IntentFlagTestActivity t18282}
Run #0: ActivityRecord{c713f21 u0 com.wuzy.androidlaunchmodetest/.MainActivity t18282}

-----------------------------------------------------------------------------------------

// 2、启动流程
MainActivity -> IntentFlagTestActivity -> MainActivity

// 栈内容
Running activities (most recent first):
TaskRecord{b6045f3 #18282 A=com.wuzy.androidlaunchmodetest U=0 StackId=1 sz=3}
Run #2: ActivityRecord{13c806b u0 com.wuzy.androidlaunchmodetest/.MainActivity t18282}
Run #1: ActivityRecord{44dcf5f u0 com.wuzy.androidlaunchmodetest/.IntentFlagTestActivity t18282}
Run #0: ActivityRecord{c713f21 u0 com.wuzy.androidlaunchmodetest/.MainActivity t18282}

-----------------------------------------------------------------------------------------

// 3、启动流程
MainActivity -> IntentFlagTestActivity -> MainActivity -> IntentFlagTestActivity

// 栈内容
Running activities (most recent first):
TaskRecord{b6045f3 #18282 A=com.wuzy.androidlaunchmodetest U=0 StackId=1 sz=2}
Run #1: ActivityRecord{fa6320c u0 com.wuzy.androidlaunchmodetest/.IntentFlagTestActivity t18282}
Run #0: ActivityRecord{c713f21 u0 com.wuzy.androidlaunchmodetest/.MainActivity t18282}
  • IntentFlagTestActivity 启动模式:singleTask
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
// 1、启动流程
MainActivity -> IntentFlagTestActivity

// 栈内容
Running activities (most recent first):
TaskRecord{7c9d493 #18280 A=com.wuzy.androidlaunchmodetest U=0 StackId=1 sz=2}
Run #1: ActivityRecord{daafb1e u0 com.wuzy.androidlaunchmodetest/.IntentFlagTestActivity t18280}
Run #0: ActivityRecord{b547fca u0 com.wuzy.androidlaunchmodetest/.MainActivity t18280}

-----------------------------------------------------------------------------------------

// 2、启动流程
MainActivity -> IntentFlagTestActivity -> MainActivity

// 栈内容
Running activities (most recent first):
TaskRecord{7c9d493 #18280 A=com.wuzy.androidlaunchmodetest U=0 StackId=1 sz=3}
Run #2: ActivityRecord{511762c u0 com.wuzy.androidlaunchmodetest/.MainActivity t18280}
Run #1: ActivityRecord{daafb1e u0 com.wuzy.androidlaunchmodetest/.IntentFlagTestActivity t18280}
Run #0: ActivityRecord{b547fca u0 com.wuzy.androidlaunchmodetest/.MainActivity t18280}

-----------------------------------------------------------------------------------------

// 3、启动流程
MainActivity -> IntentFlagTestActivity -> MainActivity -> IntentFlagTestActivity

// 栈内容
Running activities (most recent first):
TaskRecord{7c9d493 #18280 A=com.wuzy.androidlaunchmodetest U=0 StackId=1 sz=2}
Run #1: ActivityRecord{daafb1e u0 com.wuzy.androidlaunchmodetest/.IntentFlagTestActivity t18280}
Run #0: ActivityRecord{b547fca u0 com.wuzy.androidlaunchmodetest/.MainActivity t18280}

从上面两个例子看出,单独使用 FLAG_ACTIVITY_CLEAR_TOP 时,

standard 启动模式下,目标 Activity 自身及其上的 Activity 都会被销毁,目标 Activity 自身会重新创建放入栈中;singleTask 启动模式下,先销毁目标 Activity 之上的所有 Activity,然后复用已有的 Activity。

此外,singleTop、singleInstance 与 singleTask 一样,都会复用已有 Activity。这里不在赘述。

结论:

单独使用 FLAG_ACTIVITY_CLEAR_TOP,那么只有非 standard 启动模式的目标 Activity 才会被复用,否则都先被删除,然后被重新创建并入栈。

5、FLAG_ACTIVITY_CLEAR_TOP + FLAG_ACTIVITY_SINGLE_TOP

  • IntentFlagTestActivity 启动模式 standard
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
// 1、启动流程
MainActivity -> IntentFlagTestActivity

// 栈内容
Running activities (most recent first):
TaskRecord{bf65b55 #18326 A=com.wuzy.androidlaunchmodetest U=0 StackId=1 sz=2}
Run #1: ActivityRecord{c62eb86 u0 com.wuzy.androidlaunchmodetest/.IntentFlagTestActivity t18326}
Run #0: ActivityRecord{eb38b03 u0 com.wuzy.androidlaunchmodetest/.MainActivity t18326}


-----------------------------------------------------------------------------------------

// 2、启动流程
MainActivity -> IntentFlagTestActivity -> MainActivity

// 栈内容
Running activities (most recent first):
TaskRecord{bf65b55 #18326 A=com.wuzy.androidlaunchmodetest U=0 StackId=1 sz=3}
Run #2: ActivityRecord{be92d5b u0 com.wuzy.androidlaunchmodetest/.MainActivity t18326}
Run #1: ActivityRecord{c62eb86 u0 com.wuzy.androidlaunchmodetest/.IntentFlagTestActivity t18326}
Run #0: ActivityRecord{eb38b03 u0 com.wuzy.androidlaunchmodetest/.MainActivity t18326}

-----------------------------------------------------------------------------------------

// 3、启动流程
MainActivity -> IntentFlagTestActivity -> MainActivity -> IntentFlagTestActivity

// 栈内容
Running activities (most recent first):
TaskRecord{bf65b55 #18326 A=com.wuzy.androidlaunchmodetest U=0 StackId=1 sz=2}
Run #1: ActivityRecord{c62eb86 u0 com.wuzy.androidlaunchmodetest/.IntentFlagTestActivity t18326}
Run #0: ActivityRecord{eb38b03 u0 com.wuzy.androidlaunchmodetest/.MainActivity t18326}

FLAG_ACTIVITY_CLEAR_TOP + FLAG_ACTIVITY_SINGLE_TOP 标志位组合情况, standard 模式下的 IntentFlagTestActivity 被复用了, 那么其他启动模式的 Activity 也必然会被复用。(单独使用 FLAG_ACTIVITY_CLEAR_TOP 都会被复用,何况又添加了 FLAG_ACTIVITY_SINGLE_TOP 标志位,通过 Demo 验证也确实如此,就不再给出具体案例了)。

结论:

使用 FLAG_ACTIVITY_SINGLE_TOP 和 FLAG_ACTIVITY_CLEAR_TOP 标志位组合,那么不管目标 Activity 是什么启动模式,都会被复用。

OK,Activity 启动模式相关的内容就介绍这些,希望感兴趣的朋友有帮助。

Demo 我已经放在了 GitHub 上,有兴趣可以下载下来,运行看看结果。

https://github.com/zywudev/AndroidLaunchModeTest

Android 面试题(3):回答一下什么是强、软、弱、虚引用以及它们之间的区别?

这一系列文章致力于为 Android 开发者查漏补缺,面试准备。

所有文章首发于公众号「JaqenAndroid」,长期持续更新。

由于笔者水平有限,总结的答案难免会出现错误,欢迎留言指出,大家一起学习、交流、进步。

从 JDK1.2 版本开始,把对象的引用分为四种级别,从而使程序能更加灵活的控制对象的生命周期。这四种级别由高到低依次为:强引用、软引用、弱引用和虚引用。

1、强引用(Strong Reference)

强引用就是我们经常使用的引用,其写法如下:

1
Object o = new Object();

特点

  • 只要还有强引用指向一个对象,垃圾收集器就不会回收这个对象。
  • 显式地设置 o 为 null,或者超出对象的生命周期,此时就可以回收这个对象。具体回收时机还是要看垃圾收集策略。
  • 在不用对象的时将引用赋值为 null,能够帮助垃圾回收器回收对象。比如 ArrayList 的 clear() 方法实现:
1
2
3
4
5
6
7
8
public void clear() {
modCount++;

// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}

2、软引用(Soft Reference)

如果一个对象只具有软引用,在内存足够时,垃圾回收器不会回收它;如果内存不足,就会回收这个对象的内存。

使用场景:

  • 图片缓存。图片缓存框架中,“内存缓存”中的图片是以这种引用保存,使得 JVM 在发生 OOM 之前,可以回收这部分缓存。
  • 网页缓存。
1
2
3
4
5
6
7
8
Browser prev = new Browser();               // 获取页面进行浏览
SoftReference sr = new SoftReference(prev); // 浏览完毕后置为软引用
if(sr.get()!=null) {
rev = (Browser) sr.get(); // 还没有被回收器回收,直接获取
} else {
prev = new Browser(); // 由于内存吃紧,所以对软引用的对象回收了
sr = new SoftReference(prev); // 重新构建
}

3、弱引用(Weak Reference)

简单来说,就是将对象留在内存的能力不是那么强的引用。当垃圾回收器扫描到只具有弱引用的对象,不管当前内存空间是否足够,都会回收内存。

使用场景

在下面的代码中,如果类 B 不是虚引用类 A 的话,执行 main 方法会出现内存泄漏的问题, 因为类 B 依然依赖于 A。

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
public class Main {
public static void main(String[] args) {

A a = new A();
B b = new B(a);
a = null;
System.gc();
System.out.println(b.getA()); // null

}

}

class A {}

class B {

WeakReference<A> weakReference;

public B(A a) {
weakReference = new WeakReference<>(a);
}

public A getA() {
return weakReference.get();
}
}

在静态内部类中,经常会使用虚引用。例如:一个类发送网络请求,承担 callback 的静态内部类,则常以虚引用的方式来保存外部类的引用,当外部类需要被 JVM 回收时,不会因为网络请求没有及时回应,引起内存泄漏。

4、虚引用(Phantom Reference)

虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。

虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。

1
2
3
Object obj = new Object();
ReferenceQueue refQueue = new ReferenceQueue();
PhantomReference<Object> phantomReference = new PhantomReference<Object>(obj,refQueue);

使用场景

可以用来跟踪对象呗垃圾回收的活动。一般可以通过虚引用达到回收一些非java内的一些资源比如堆外内存的行为。例如:在 DirectByteBuffer 中,会创建一个 PhantomReference 的子类 Cleaner 的虚引用实例用来引用该 DirectByteBuffer 实例,Cleaner 创建时会添加一个 Runnable 实例,当被引用的 DirectByteBuffer 对象不可达被垃圾回收时,将会执行 Cleaner 实例内部的 Runnable 实例的 run 方法,用来回收堆外资源。

Android 面试题(2):一般什么情况下会导致内存泄漏问题?

这一系列文章致力于为 Android 开发者查漏补缺,面试准备。

所有文章首发于公众号「JaqenAndroid」,长期持续更新。

由于笔者水平有限,总结的答案难免会出现错误,欢迎留言指出,大家一起学习、交流、进步。

内存泄漏也是面试常见问题,主要可以考察面试者是否了解内存泄漏,工作中是如何排查解决内存泄漏问题,还可以延伸考察 Java 内存回收机制,Java 中对象的引用方式等等。

这篇文章先来介绍下 Android 开发中常见的内存泄漏案例以及相应的解决方案。

单例造成的内存泄漏

单例模式在 Android 开发中使用率非常高,但使用不恰当的话也会造成内存泄漏。比如下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Singleton {

private static Singleton sInstance;
private Context mContext;

private Singleton(Context context) {
this.mContext = context;
}

public static Singleton getInstance(Context context) {
if (sInstance == null) {
sInstance = new Singleton(context);
}
return sInstance;
}
}

单例类对象的生命周期与应用的周期一样长,如果传入的是 Activity 的 Context,在 Activity 退出时,因单例对象持有 Activity 的引用,导致 Activity 的内存不能被回收,即内存泄漏。

解决方案

1)使用 Application 的 Context,生命周期一致;

2)将短生命周期的属性的引用方式改为弱引用。

非静态内部类造成的内存泄漏

非静态内部类持有外部类的引用,如果外部类的实例已经结束生命周期,但内部类仍然在执行,就会导致外部类不能被回收。比如上一期讲解的自定义 Handler 的使用造成的内存泄漏,主要原因 Activity 退出时,Handler 仍然持有 Activity 的引用,导致 Activity 不能被回收。

解决方案

1) 创建一个静态内部类,然后外部类的对象引用使用弱引用;

2)及时关闭耗时或者延时任务,在 Activity 被销毁时及时清除消息,从而及时回收 Activity,避免内存泄漏问题。

系统服务注册未取消造成的内存泄漏

系统服务可以通过 Context.getSystemService 获取,它们负责执行某些后台任务,或者为硬件访问提供接口。如果 Context 对象想要在服务内部的事件发生时被通知,那就需要把自己注册到服务的监听器中。然而,这会让服务持有 Activity 的引用,如果在 Activity 的 onDestory() 函数中没有释放掉引用就会内存泄漏。

解决方案

1)使用 Application 的 Context 代替 Activity 的 Context;

2)在 Activity 的 onDestory() 方法,调用反注册释放。

全局集合类造成的内存泄漏

一般情况下集合类不会造成内存泄漏,但如果是全局性的集合,如果在使用完毕后未进行 remove 清理操作,就很有可能造成内存泄漏,所以在集合不需要的时候要及时清理集合元素。

资源未关闭的内存泄漏

对于使用了 BroadcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap 等资源,应该在 Activity 销毁时及时关闭或者注销。

WebView 造成的内存泄漏

WebView 存在内存泄漏的问题,在应用中只要使用一次 WebView,内存就不会被释放掉。

解决方案

为 WebView 开启一个独立的进程,使用 AIDL 与应用的主进程进行通信,WebView 所在的进程可以根据业务的需要选择合适的时机进行销毁,达到正常释放内存的目的。

总结

总的来说就是生命周期长的对象持有了生命周期短的对象,导致生命周期短的对象在回收时无法被释放,就会导致内存泄漏。

了解常见内存泄漏及解决方案,能够帮助我们在开发中尽量少的出现内存泄漏问题。但有些内存泄漏的定位排查比较困难,需要借助一些工具,比如 LeakCanary、MAT 等。内存泄漏的排查定位方法会在后续文章中介绍,欢迎持续关注。

Android 面试题(1):使用 Handler 时如何有效地避免内存泄漏问题?

这一系列文章致力于为 Android 开发者查漏补缺,面试准备。

所有文章首发于公众号「JaqenAndroid」,长期持续更新。

由于笔者水平有限,总结的答案难免会出现错误,欢迎留言指出,大家一起学习、交流、进步。

什么是内存泄漏?

Java 中采用可达性分析算法判断一个对象是否可被回收。

基本思路是这样的:

通过一系列称为 “GC Roots” 的对象作为起始点,从这个节点向下搜索,搜索走过的路径就是引用链,当一个对象到 GC Roots 没有任何引用链相连,也就是从 GC Roots 到这个对象不可达,则这个对象不可达,可以被回收。

可作为 GC Roots 的对象有:

  • 虚拟机栈中的引用的对象

  • 方法区的静态变量和常量引用的对象

  • 本地方法栈中 JNI 引用的对象

当一个对象不需要在再使用了,本该被回收时, 而另外一个正在使用的对象持有它的引用从而导致它不能被回收,这就导致本该被回收的对象不能被回收而停留在堆内存中,内存泄漏就产生了。

Handler 是如何造成内存泄漏的?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class MainActivity extends AppCompatActivity {

private Handler mHandler = new Handler(){
@Override
public void handleMessage(@NonNull Message msg) {
// 处理数据
}
};

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
loadData();
}

private void loadData() {
// 耗时任务
// ...
// 发送数据
Message message = Message.obtain();
mHandler.sendMessage(message);
}
}

上面是一段简单的 Handler 的使用。 这种方式有可能造成内存泄漏吗?答案是有可能的。我们来分析下造成内存泄漏的原因?

我们知道 Java中非静态内部类会隐式持有外部类的引用,所以这里创建的 Handler 隐式持有外部类 MainActivity 的引用。

而 Handler 一般用来处理后台线程任务的执行结果,如果在线程任务之慈宁宫过程中,用户关闭了 Activity,此时线程尚未执行完,而该线程持有 Handler 的引用,Handler 又持有 Activity 的引用,就导致了 Activity 无法被回收(即内存泄漏)。

还有一种情况,如果你调用 Handler 的 postDelay() 方法执行了延时任务, 该方法会将你的Handler 装入一个 Message,并把这条 Message 推到 MessageQueue 中,那么在你设定的 delay 到达之前,会有一条 MessageQueue -> Message -> Handler -> Activity 的链,导致你的 Activity 被持有引用而无法被回收。

如果解决 Handler 导致的内存泄漏问题?

方法 1、静态内部类 + 弱引用

既然非静态内部类持有外部类的引用,那么可以将 Handler 声明为静态内部类,Handler 也就不再持有 Activity 的引用,所以 Activity 可以随便被回收。代码如下:

1
2
3
4
5
6
static class MyHandler extends Handler {
@Override
public void handleMessage(Message msg) {
// ...
}
}

此时 Handler 不再持有 Activity 的引用,导致 Handler 无法操作 Activity 中对象,所以可以在 Handler 中添加一个对 Activity 的弱引用( WeakReference ):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static class MyHandler extends Handler {
WeakReference<Activity > mActivityReference;

MyHandler(Activity activity) {
mActivityReference= new WeakReference<Activity>(activity);
}

@Override
public void handleMessage(Message msg) {
final Activity activity = mActivityReference.get();
if (activity != null) {
//...
}
}
}

弱引用的特点是: 在垃圾回收器一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。 所以用户在关闭 Activity 之后,就算后台线程还没结束,但由于仅有一条来自 Handler 的弱引用指向 Activity,Activity 也会被回收掉。这样,内存泄露的问题就不会出现了。

方法2: 通过程序逻辑来进行保护

在 Activity 被销毁时及时清除消息,从而及时回收 Activity,避免内存泄漏问题。

1
2
3
4
5
6
7
@Override
protected void onDestroy() {
super.onDestroy();
if (mHandler != null) {
mHandler.removeCallbacksAndMessages(null);
}
}

ijkplayer 编译实践

记录 ijkplayer 的编译过程,以及遇到的问题,有需要的朋友可以参考。

编译环境

Linux 环境

由于主机是 Windows 系统,所以使用 VMware 安装了 Ubuntu 18.0.4 系统。

VMware 安装 Ubuntu 系统的安装步骤网上非常多,这篇文章比较详细,没有经验的可以参考。

https://zhuanlan.zhihu.com/p/38797088

Android SDK

下载地址:https://developer.android.com/studio#downloads

Android NDK

下载地址:https://developer.android.google.cn/ndk/downloads/older_releases.html

注意 NDK 的最小版本支持是 10e,目前不支持 NDK 15!我这边下载的是 android-ndk-r14b

Android SDK 和 Android NDK 下载解压后,需要配置环境变量,可以参考我写的这篇文章。

http://wuzhangyang.com/2019/10/14/ubuntu-android-studio/

安装 git 和 yasm

1
2
sudo apt install git
sudo apt install yasm

注意,如果安装报错,先要执行 sudo apt update 进行更新。

开始编译

拉取 ijkplayer 源码

1
2
3
git clone https://github.com/Bilibili/ijkplayer.git ijkplayer-android
cd ijkplayer-android
git checkout -B latest k0.8.8

初始化 android

1
./init-android.sh

初始化 openssl 支持 https

1
./init-android-openssl.sh

配置编解码器格式支持

默认为最少支持,如果足够你使用,可以跳过这一步,否则可以改为以下配置:

  • module-default.sh 更多的编解码器/格式

  • module-lite-hevc.sh 较少的编解码器/格式(包括 hevc)

  • module-lite.sh 较少的编解码器/格式(默认情况)

1
2
3
4
5
6
7
8
# 进入 config 目录
cd config

# 删除当前的 module.sh 文件
rm module.sh

# 创建软链接 module.sh 指向 module-default.sh
ln -s module-default.sh module.sh

编译 openssl

1
2
3
4
5
6
7
8
# 进入 android/contrib 目录
cd android/contrib

# 清除 openssl 的编译文件
./compile-openssl.sh clean

# 编译 openssl
./compile-openssl.sh all

./compile-openssl.sh 后跟 all 表示编译所有 CPU 架构的 so 库, 如果只编译指定 CPU 架构的 so 库,后面就跟 CPU 架构,比如:./compile-ffmpeg.sh armv7a

这里,在执行 ./compile-openssl.sh all 时出现了编译错误:ERROR: Failed to create toolchain.

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
jaqen@jaqen-virtual-machine:~/Android/Projects/ijkplayer-android/android/contrib$ ./compile-openssl.sh all
====================
[*] check archs
====================
FF_ALL_ARCHS = armv5 armv7a arm64 x86 x86_64
FF_ACT_ARCHS = armv5 armv7a arm64 x86 x86_64

--------------------
[*] make NDK standalone toolchain
--------------------
build on Linux x86_64
ANDROID_NDK=/home/jaqen/Android/Sdk/android-ndk-r14b
IJK_NDK_REL=14.1.3816874
NDKr14.1.3816874 detected

--------------------
[*] make NDK standalone toolchain
--------------------
build on Linux x86_64
ANDROID_NDK=/home/jaqen/Android/Sdk/android-ndk-r14b
IJK_NDK_REL=14.1.3816874
NDKr14.1.3816874 detected
HOST_OS=linux
HOST_EXE=
HOST_ARCH=x86_64
HOST_TAG=linux-x86_64
HOST_NUM_CPUS=4
BUILD_NUM_CPUS=8
Auto-config: --arch=arm
ERROR: Failed to create toolchain.

解决办法是安装 python 后再执行编译。

1
sudo apt install python

编译 ffmpeg

1
2
3
4
5
# 清除 ffmpeg 的编译文件
./compile-ffmpeg.sh clean

# 编译 ffmpeg
./compile-ffmpeg.sh all

执行 ./compile-ffmpeg.sh all 时出现编译错误:linux/perf_event.h: No such file or directory

解决办法是在 config 文件夹下的 module.sh 文件中加入下面两句:

1
2
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --disable-linux-perf"
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --disable-bzlib"

再重新执行编译。

编译 ijkplayer

1
2
3
4
5
# 进入 android 目录
cd ..

# 编译 ijkplayer
./compile-ijk.sh all

编译完成之后,在 android/ijkpleyer 文件夹的对应架构文件下,在/src/main/libs/架构名/下生成libijkplayer.solibijkffmpeg.solibijksdl.so 三个文件。

ijkplayer_so

至此,ijkplayer 的编译工作就全部完成了。

编译过程中遇到问题的朋友欢迎留言交流。

不想编译的朋友,可以在公众号 「贾小昆」后台回复 ijk 获取 so 包。