androidMVP架构模式

android MVP模式在Android项目中的使用

在开发当中我们会涉及到一些架构模式,对于初级程序员来说是很陌生,但是对于中级过渡高级的程序员来说是非常重要需要了解的模式。首先我们先了解有哪些架构模式:不管都前端,移动端,后端都会涉及到这三种:MVC、MVP、MVVM。
概念:什么是MVP
MVP是模型(Model)、视图(View)、主持人(Presenter)的缩写,分别代表项目中3个不同的模块。
模型(Model):负责处理数据的加载或者存储,比如从网络或者本地数据库获取数据等;
视图(View):负责界面数据的展示,与用户进行交互;
主持人(Presenter):相当于协调者,是模型与视图之间的桥梁,将模型与视图分离开来。

如下图所示,View与Model并不直接交互,而是使用Presenter作为View与Model之间的桥梁。其中Presenter中同时持有View层以及Model层的Interface的引用,而View层持有Presenter层Ineterface的引用。当View层某个界面需要展示展示某些数据的时候,首先会调用Presenter层的某个接口,然后Presneter层会调用Model层请求数据,当Model层数据加载成功之后会调用Presenter层的回调方法通知Presenter层数据加载完毕,最后Presneter层再调用View层的接口将加载后的数据展示给用户。这就是Mvp模式的整个核心过程。
Alt text

这样分层的好处就是大大减少了Model与View层之间的耦合度。一方面可以使得View层和Model层单独开发与测试,互不依赖。另一方面Model层可以封装复用,可以极大的减少代码量。当然,Mvp还有其他的一些优点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1.View层
View层首页数据展示模块的组件是Activity,里面有一个RecycleView 布局如下:

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.RecyclerView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/recycle_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="vertical"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:paddingTop="@dimen/card_margin">
</android.support.v7.widget.RecyclerView>

首页列表模块主要展示从网络获取的首页列表信息,view层的接口大概需要如下方法:
(1)请求数据的过程中需要提示‘正在加载’的反馈信息给用户
(2)加载数据成功后,将加载得到的数据填充到RecycleView展示给用户
(3)加载数据成功后,将提示‘正在加载’反馈信息取消掉
(4)若加载数据失败,如网络连接、则需要给用户提示信息。

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
//业务接口
public interface HomeContract {
interface View {
void onLoading();
void onError(String error);
void updateUI(XXX xxx);
void ToastMessage(String message);
}
interface Action extends IPresenter {
void requestHome(XXX... xxx);
}
}

//业务处理请求
public class HomePresenter implements HomeContract.Action{
HomeContract.view view;
public HomePresenter(HomeContract.view view){
this.view =view;
}
@Override
pubublic void requestHome(XXX... xxx){
//请求网络数据处理业务层
view.onLoading();
view.onError();
view.updateUI();
view.ToastMessage();
}
}

首页列表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
public class HomeActivity extends AppCompatActivity implements HomeContract.view {
HomeContract.Action action;
@Override
public void onLoading(){
//提示加载信息
}
@Override
public void onError(String error){
//加载错误ui
}
@Override
public void updateUI(XXX xxx){
//更新UI数据
}
@Override
public void ToastMessage(String message){
//网络请求结束提示信息
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState){
super.onCreate(savedInstanceState);
action = new HomePresenter(this);
action.requestHome(xx);
}
}

当用户打开首页界面时,就请求网络获取数据,初始化Presenter引用实例,调用Presenter层的requestHome接口,业务处理完毕后,调用相应的onLoading()、onError()、updateUI()、ToastMessage()方法处理不同逻辑。

Handler的同步屏障机制

  • 对于Handler机制,想必大家都已经非常熟悉了吧,从迈进Android开发这扇大门的时候,就不停的研究和使用它,同样的这也是Android系统架构的精髓之一。然而在我们使用的时候,往往会忽略掉一些不常见却又很重要的内容,今天就来讲一讲经常被忽略的同步屏障以及异步消息。

制流程中窥视handler同步屏障

为了引出今天的主题,我们先来看看ui的渲染流程吧。在Android的绘制流程中,ViewRootImpl这个类发挥了非常重要的作用,首先我们看一下这个类中比较重要的一个方法requestLayout(),
下面开始按这个思路分析源码,直接从入口开始:

1
2
3
4
5
6
7
8
9
10
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
//校验主线程
checkThread();
mLayoutRequested = true;
//调用这个方法启动绘制流程
scheduleTraversals();
}
}

requestLayout()中会通过checkThread()方法检查发起布局请求的线程是否为主线程(校验ViewRootImpl构造时记录的mThread, 和当前线程是否一致),之后,在调用scheduleTraversals()的时候 postSyncBarrier添加同步消息屏障

requestLayout()中会通过checkThread()方法检查发起布局请求的线程是否为主线程(校验ViewRootImpl构造时记录的mThread, 和当前线程是否一致),之后,在调用scheduleTraversals()的时候 postSyncBarrier添加同步消息屏障

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
@UnsupportedAppUsage
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
//1. 往主线程的Handler对应的MessageQueue发送一个同步屏障消息
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
//2.将mTraversalRunnable保存到Choreographer中
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
...
//在doTraversal方法中移除同步消息屏障
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
//移除同步屏障
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
...
}
}

在这个方法中,涉及到三个比较重要的信息

  • mTraversalRunnable
  • Choreographer编舞者
  • 同步屏障消息
  1. 首先看mTraversalRunnable,它的作用就是从ViewRootImpl 从上往下执行performMeasure、performLayout、performDraw。

  2. Choreographer主要是为了配合Vsync信号,给上层app的渲染提供一个稳定的Message处理时机,也就是Vsync信号到来时,系统通过对Vsync信号的调整,来控制每一帧绘制操作的时机。当Vsync信号到来时,会往主线程的MessageQueue中插入一条异步消息,由于在scheduleTraversals中给MessageQueue中插入了同步屏障消息,那么当执行到同步屏障时,会取出异步消息执行。

看下Choreography中插入消息的方法是如何实现的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void postCallbackDelayedInternal(int callbackType,
Object action, Object token, long delayMillis) {
synchronized (mLock) {
...
if (dueTime <= now) {
scheduleFrameLocked(now);
} else {
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callbackType;
//设置为异步消息
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, dueTime);
}
}
}

通过以上的分析,我们知道了,在刷新ui的时候原来会有这么多的参与者,但是那些什么同步消息、异步消息、消息屏障又是些什么东西呢?接下来我们就来研究一下。

何为同步屏障?

message分类

Handler的message分为三种

  • 同步消息
  • 异步消息
  • 屏障消息

通常我们使用handler发送消息,都是使用默认的构造函数构造handler,然后使用send方法发送。这样发送的消息都是普通消息也就是同步消息,发出去的消息就会在MessageQueue中排队。异步消息正常情况下跟同步消息没有区别,只有在设置了同步屏障之后,才会出现差异。

同步屏障就是在消息队列中插入一个屏障,插入之后,所有的同步消息都会被屏蔽,不能被执行,但是异步消息却不受影响,可以继续执行。

插入消息屏障

正常插入消息会调用enqueueMessage方法,同时将handler赋值给message的target。

1
2
3
4
5
6
7
8
9
10
11
//将消息插入消息队列
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
long uptimeMillis) {
msg.target = this;
msg.workSourceUid = ThreadLocalWorkSource.getUid();
//进行判断是否将消息设置为异步消息
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}

在MessageQueue中进行判断,如果target为空也就是这个message没有对应的handler则会报异常。

1
2
3
4
5
6
7
8
9
10
11
12
boolean enqueueMessage(Message msg, long when) {
if (msg.target == null) {
throw new IllegalArgumentException("Message must have a target.");
}
if (msg.isInUse()) {
throw new IllegalStateException(msg + " This message is already in use.");
}
...
// 如果需要唤醒,则唤醒
if (needWake) {
nativeWake(mPtr);
}

通过MessageQueue的postSyncBarrier方法插入屏障,message的target属性为null

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
private int postSyncBarrier(long when) {

synchronized (this) {
final int token = mNextBarrierToken++;
//msg没有为target属性赋值
final Message msg = Message.obtain();
...
//根据时间插入到MessageQueue中
if (when != 0) {
while (p != null && p.when <= when) {
prev = p;
p = p.next;
}
}
if (prev != null) { // invariant: p == prev.next
msg.next = p;
prev.next = msg;
} else {
msg.next = p;
mMessages = msg;
}
//返回一个序号,通过它可以对屏障消息进行撤销
return token;
}
}

经过以上的操作,我们可以总结出

  • 屏障消息和普通消息的区别是屏障消息没有target属性,普通消息有target属性是因为要将消息分发给target指向的handler处理
  • 屏障消息会插入到MessageQueue中合适的位置,这个消息以后的普通消息将被屏蔽
  • postSyncBarrier返回一个int类型的数值,通过这个数值可以撤销屏障
  • postSyncBarrier方法是私有的,如果我们想调用它就得使用反射
  • 插入普通消息会唤醒消息队列,但是插入屏障不会

如何发送异步消息

通常我们发送的都是普通消息,如果想发送异步消息

  • 可以在创建handler时使用如下的构造器中的一种,同时将async参数设置为true,这样这个handler发送的消息就都是异步消息了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public Handler(boolean async) {
this(null, async);
}

public Handler(@NonNull Looper looper, @Nullable Callback callback) {
this(looper, callback, false);
}

public Handler(@Nullable Callback callback, boolean async) {
...
mLooper = Looper.myLooper();
if (mLooper == null) {
throw new RuntimeException(
"Can't create handler inside thread " + Thread.currentThread()
+ " that has not called Looper.prepare()");
}
mQueue = mLooper.mQueue;
mCallback = callback;
mAsynchronous = async;
}
  • 除了这种方式还可以直接设置消息的类型为异步消息
1
2
3
4
5
6
7
public void setAsynchronous(boolean async) {
if (async) {
flags |= FLAG_ASYNCHRONOUS;
} else {
flags &= ~FLAG_ASYNCHRONOUS;
}
}

消息处理的过程

MessageQueue是通过next方法来遍历消息的

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
@UnsupportedAppUsage
Message next() {
for (;;) {
nativePollOnce(ptr, nextPollTimeoutMillis);
synchronized (this) {
Message msg = mMessages;
//如果msg.target为空,也就是说是一个同步屏障消息,则进入这个判断里面
if (msg != null && msg.target == null) {
// Stalled by a barrier. Find the next asynchronous message in the queue.
//在这个while循环中,找到最近的一个异步消息
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
//找到了异步消息
if (msg != null) {
//如果消息的处理时间小于当前时间 则等待
if (now < msg.when) {
// Next message is not ready. Set a timeout to wake up when it is ready.
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
// Got a message.
//处理消息
mBlocked = false;
//将异步消息移除
if (prevMsg != null) {
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
}
msg.next = null;
if (DEBUG) Log.v(TAG, "Returning message: " + msg);
msg.markInUse();
//返回异步消息
return msg;
}
} else {
// No more messages.
//没有找到异步消息则进入阻塞状态,等待被唤醒
nextPollTimeoutMillis = -1;
}
...
}

在这段代码中,可以看到,处理过程中会先判断此消息是否为屏障消息,如果是屏障消息,则去循环遍历,直到寻找到异步消息为止。通过这种方式跳过了普通消息,直接执行异步消息。也就是说同步屏障为handler消息机制提供了一种优先级策略,异步消息的优先级要高于同步消息。

另外需要注意的是:同步屏障不会自动移除,使用完成之后需要手动移除,不然会造成同步消息无法处理。也就是上边提到的,通过removeSyncBarrier(int token)方法进行移除,token就是之前添加屏障时返回的token。

1
public void removeSyncBarrier(int token){}
  • 通过以上的分析,想必对于handler消息机制中的几种不同的消息有了一个更深入的了解了吧,对于绘制流程中,为什么要发送一个同步屏障并且发送异步消息,应该心中也有了答案,不错,就是为了让保证在vsync信号到来时,异步任务可以优先执行,从而绘制任务可以被及时执行,避免造成界面卡顿。

getLocationInWindow和getLocationOnScreen的区别

一个控件在其父窗口中的坐标位置

View.getLocationInWindow(int[] location)

一个控件在其整个屏幕上的坐标位置
View.getLocationOnScreen(int[] location)

Alt text

getLocationInWindow是以B为原点的C的坐标
getLocationOnScreen以A为原点。
下面是getLocationOnScreen示例

下面是getLocationOnScreen示例

1
2
3
4
5
start = (Button) findViewById(R.id.start);
int []location=new int[2];
start.getLocationOnScreen(location);
int x=location[0];//获取当前位置的横坐标
int y=location[1];//获取当前位置的纵坐标

getLocationOnScreen 的源码

1
2
3
4
5
6
7
8
9
public void getLocationOnScreen(@Size(2) int[] outLocation) {
getLocationInWindow(outLocation);

final AttachInfo info = mAttachInfo;
if (info != null) {
outLocation[0] += info.mWindowLeft;
outLocation[1] += info.mWindowTop;
}
}

下面是getLocationInWindow示例

1
2
3
4
5
start = (Button) findViewById(R.id.start);
int []location=new int[2];
start.getLocationInWindow(location);
int x=location[0];//获取当前位置的横坐标
int y=location[1];//获取当前位置的纵坐标

getLocationInWindow 的源码

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
public void getLocationInWindow(@Size(2) int[] outLocation) {
if (outLocation == null || outLocation.length < 2) {
throw new IllegalArgumentException("outLocation must be an array of two integers");
}

outLocation[0] = 0;
outLocation[1] = 0;

transformFromViewToWindowSpace(outLocation);
}


public void transformFromViewToWindowSpace(@Size(2) int[] inOutLocation) {
if (inOutLocation == null || inOutLocation.length < 2) {
throw new IllegalArgumentException("inOutLocation must be an array of two integers");
}

if (mAttachInfo == null) {
// When the view is not attached to a window, this method does not make sense
inOutLocation[0] = inOutLocation[1] = 0;
return;
}

float position[] = mAttachInfo.mTmpTransformLocation;
position[0] = inOutLocation[0];
position[1] = inOutLocation[1];

if (!hasIdentityMatrix()) {
getMatrix().mapPoints(position);
}

position[0] += mLeft;
position[1] += mTop;

ViewParent viewParent = mParent;
while (viewParent instanceof View) {
final View view = (View) viewParent;

position[0] -= view.mScrollX;
position[1] -= view.mScrollY;

if (!view.hasIdentityMatrix()) {
view.getMatrix().mapPoints(position);
}

position[0] += view.mLeft;
position[1] += view.mTop;

viewParent = view.mParent;
}

if (viewParent instanceof ViewRootImpl) {
// *cough*
final ViewRootImpl vr = (ViewRootImpl) viewParent;
position[1] -= vr.mCurScrollY;
}

inOutLocation[0] = Math.round(position[0]);
inOutLocation[1] = Math.round(position[1]);
}

Leakcanary原理浅析

LeakCanary是Android内存泄漏的框架,作为一个“面试常见问题”,它一定有值得学习的地方,今天我们就讲一下它。作为一名开发,我觉得给人讲框架或者库的原理,最好先把大概思路给读者讲一下,这样读者后面会按照这个框架往里填内容,理解起来也更容易一些,所以我先把LeakCanary的大致原理放出来:

其思路大致为:监听Activity生命周期->onDestroy以后延迟5秒判断Activity有没有被回收->如果没有回收,调用GC,再此判断是否回收,如果还没回收,则内存泄露了,反之,没有泄露。整个框架最核心的问题就是在什么时间点如何判断一个Activity是否被回收了。

下面开始按这个思路分析源码,直接从入口开始:

1
2
3
4
5
public static RefWatcher install(Application application) {
return refWatcher(application).listenerServiceClass(DisplayLeakService.class)
.excludedRefs(AndroidExcludedRefs.createAppDefaults().build())
.buildAndInstall();
}

builder模式构建了一个RefWatcher对象,listenerServiceClass()方法绑定了一个后台服务DisplayLeakService

这个服务主要用来分析内存泄漏结果并发送通知。你可以继承并重写这个类来进行一些自定义操作,比如上传分析结果等。

我们看最后buildAndInstall()方法:

1
2
3
4
5
6
7
8
public RefWatcher buildAndInstall() {
RefWatcher refWatcher = build();
if (refWatcher != DISABLED) {
LeakCanary.enableDisplayLeakActivity(context);
ActivityRefWatcher.installOnIcsPlus((Application) context, refWatcher);
}
return refWatcher;
}

build()方法,这个方法主要是配置一些东西,先大概了解一下,后面用到再说,下面是几个配置项目。

watchExecutor : 线程控制器,在 onDestroy()之后并且主线程空闲时执行内存泄漏检测

debuggerControl : 判断是否处于调试模式,调试模式中不会进行内存泄漏检测

gcTrigger : 用于GC

watchExecutor 首次检测到可能的内存泄漏,会主动进行GC,GC之后会再检测一次,仍然泄漏的判定为内存泄漏,进行后续操作

heapDumper : dump内存泄漏处的heap信息,写入hprof文件

heapDumpListener : 解析完hprof文件并通知DisplayLeakService弹出提醒

excludedRefs : 排除可以忽略的泄漏路径

LeakCanary.enableDisplayLeakActivity(context)

这行代码主要是为了开启LeakCanary的应用,显示其图标.

接下来是重点:

ActivityRefWatcher.installOnIcsPlus((Application) context, refWatcher)

它会进入:

1
2
ActivityRefWatcher activityRefWatcher = new ActivityRefWatcher(application, refWatcher);
activityRefWatcher.watchActivities();

接下来:

1
2
stopWatchingActivities();
application.registerActivityLifecycleCallbacks(lifecycleCallbacks);

第一行代码是为了确保不会重复绑定,第二行绑定生命周期,之后监听Activity的生命周期。

1
2
3
@Override public void onActivityDestroyed(Activity activity) {
ActivityRefWatcher.this.onActivityDestroyed(activity);
}

监听到Activity销毁时执行onActivityDestroyed方法,进入看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void watch(Object watchedReference, String referenceName) {
if (this == DISABLED) {
return;
}
checkNotNull(watchedReference, "watchedReference");
checkNotNull(referenceName, "referenceName");
final long watchStartNanoTime = System.nanoTime();
String key = UUID.randomUUID().toString();
retainedKeys.add(key);
final KeyedWeakReference reference =
new KeyedWeakReference(watchedReference, key, referenceName, queue);

ensureGoneAsync(watchStartNanoTime, reference);
}

整个LeakCanary最核心的思路就在这儿了。

前面几行是这样的,根据Activity生成一个随机Key,并将Key加入到一个Set中,然后讲key,activity传如一个包装的弱引用里。

这里引出了第一个知识点,弱引用和引用队列ReferenceQueue联合使用时,如果弱引用持有的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。即 KeyedWeakReference持有的Activity对象如果被垃圾回收,该对象就会加入到引用队列queue,我们看看RefreceQueue的javadoc:

1
2
3
4
5
6
7
8
/**
* Reference queues, to which registered reference objects are appended by the
* garbage collector after the appropriate reachability changes are detected.
*
* @author Mark Reinhold
* @since 1.2
*/
public class ReferenceQueue<T>

证实了上面的说法,另外看名字我们就知道,不光弱引用,软和虚引用也可以这样做。

重点是最后一句:ensureGoneAsyc,看字面意思,异步确保消失。这里我们先不看代码,如果要自己设计一套检测方案的话,怎么想?其实很简单,就是在Activiy onDestroy以后,我们等一会,检测一下这个Acitivity有没有被回收,那么问题来了,什么时候检测?怎么检测?这也是本框架的核心和难点。

LeakCanary是这么做的:onDestroy以后,一旦主线程空闲下来,延时5秒执行一个任务:先判断Activity有没有被回收?如果已经回收了,说明没有内存泄漏,如果还没回收,我们进一步确认,手动触发一下gc,然后再判断有没有回收,如果这次还没回收,说明Activity确实泄漏了,接下来把泄漏的信息展示给开发者就好了。

思路其实挺清晰的,我们看代码实现:

1
2
3
4
5
6
7
private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {
watchExecutor.execute(new Retryable() {
@Override public Retryable.Result run() {
return ensureGone(reference, watchStartNanoTime);
}
});
}

这里watchExecutor是AndroidWatchExecutor,看代码:

1
2
3
4
5
6
7
@Override public void execute(Retryable retryable) {
if (Looper.getMainLooper().getThread() == Thread.currentThread()) {
waitForIdle(retryable, 0);
} else {
postWaitForIdle(retryable, 0);
}
}

主线程和子线程其实一样,都要到主线程中执行,

1
2
3
4
5
6
7
8
9
  void waitForIdle(final Retryable retryable, final int failedAttempts) {
// This needs to be called from the main thread.
Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
@Override public boolean queueIdle() {
postToBackgroundWithDelay(retryable, failedAttempts);
return false;
}
});
}

这里有第二个知识点,IdleHandler,这个东西是干嘛的,其实看名字就知道了,就是当主线程空闲的时候,如果设置了这个东西,就会执行它的queueIdle()方法,所以这个方法就是在onDestory以后,一旦主线程空闲了,就会执行,然后我们看它执行了啥:

1
2
3
4
5
6
7
8
9
10
11
12
13
   private void postToBackgroundWithDelay(final Retryable retryable, final int failedAttempts) {
long exponentialBackoffFactor = (long) Math.min(Math.pow(2, failedAttempts), maxBackoffFactor);
long delayMillis = initialDelayMillis * exponentialBackoffFactor;
backgroundHandler.postDelayed(new Runnable() {
@Override public void run() {
Retryable.Result result = retryable.run();
if (result == RETRY) {
postWaitForIdle(retryable, failedAttempts + 1);
}
}
}, delayMillis);
}
}

很简单,延时5秒执行retryable的run(),注意,因为这里是backgroundHandler post出来的,所以是下面的run是在子线程执行的。这里的retryable就是前面传过来的:

1
2
3
4
5
6
7
private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {
watchExecutor.execute(new Retryable() {
@Override public Retryable.Result run() {
return ensureGone(reference, watchStartNanoTime);
}
});
}

ensureGone(reference,watchStartNanoTime),在看它干了啥之前,我们先理一下思路,前面onDestory以后,AndroidWatchExecutor这个东西执行excute方法,这个方法让主线程在空闲的时候发送了一个延时任务,该任务会在5秒延时后在一个子线程执行。理清了思路,我们看看这个任务是怎么执行的。

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
Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) {
long gcStartNanoTime = System.nanoTime();
long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);

removeWeaklyReachableReferences();

if (debuggerControl.isDebuggerAttached()) {
// The debugger can create false leaks.
return RETRY;
}
if (gone(reference)) {
return DONE;
}
gcTrigger.runGc();
removeWeaklyReachableReferences();
if (!gone(reference)) {
long startDumpHeap = System.nanoTime();
long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);

File heapDumpFile = heapDumper.dumpHeap();
if (heapDumpFile == RETRY_LATER) {
// Could not dump the heap.
return RETRY;
}
long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);
heapdumpListener.analyze(
new HeapDump(heapDumpFile, reference.key, reference.name, excludedRefs, watchDurationMs,
gcDurationMs, heapDumpDurationMs));
}
return DONE;
}

前面我们说过思路了,5秒延迟后先看看有没有回收,如果回收了,直接返回,没有发生内存泄漏,如果没有回收,触发GC,gc完成后,在此判断有没有回收,如果还没回收,说明泄漏了,收集泄漏信息,展示给开发者。而上面的代码完全按照这个思路来的。其中,removeWeaklyRechableReferences()和gone(reference)这两个方法配合,用来判断对象是否被回收了,看代码:

1
2
3
4
5
6
7
8
 private void removeWeaklyReachableReferences() {
// WeakReferences are enqueued as soon as the object to which they point to becomes weakly
// reachable. This is before finalization or garbage collection has actually happened.
KeyedWeakReference ref;
while ((ref = (KeyedWeakReference) queue.poll()) != null) {
retainedKeys.remove(ref.key);
}
}

通过知识点1知道:被回收的对象都会放到设置的引用队列queue中,我们从queue中拿出所有的ref,根据他们的key匹配retainedKeys集合中的元素并删除。然后在gone()函数里面判断key是否被移除.

1
2
3
private boolean gone(KeyedWeakReference reference) {
return !retainedKeys.contains(reference.key);
}

这个方法挺巧妙的,retainedKeys集合了所有destoryed了的但没有被回收的Activity的key,这个集合可以用来判断一个Activity有没有被回收,但是判断之前需要用removeWeaklyReachableReferences()这个方法更新一下。

一旦一个Activity检测出泄漏了,就收集泄漏信息然后通过前面配置的DisplayLeakService通知给用户并展示在DisplayLeakActivity中,后面的东西都是UI展示东西,就不是本文的重点了,有兴趣的可以自己查看。

稍微总结一下,我觉得这个框架中用到的一个很重要但冷门技巧就是弱引用的构造方法:传入一个RefrenceQueue,可以记录被垃圾回收的对象引用。说个题外话,一个对象都被回收了,他的弱引用咋办,总不能一直留着吧,(引用本身也是一个强引用对象,不要把引用和引用的对象搞混了,对象可以被回收了,但是它的引用,包括软,弱,虚引用都可以继续存在)。完全不用担心,这个引用在无用之后也会被GC回收的。

gradle新的依赖方式

在 gradle3.0之前,gradle 依赖项目配置有 compile,apk,provided三种方式

1.compile:指定编译时依赖项。Gradle 将此配置的依赖项添加到类路径和应用的 APK。这是默认配置。

2.apk: 指定 Gradle 需要将其与应用的 APK 一起打包的仅运行时依赖项。您可以将此配置与 JAR 二进制依赖项一起使用,而不能与其他库模块依赖项或 AAR 二进制依赖项一起使用。

3.provided:指定 Gradle 不与应用的 APK 一起打包的编译时依赖项。如果运行时无需此依赖项,这将有助于缩减 APK 的大小。您可以将此配置与 JAR 二进制依赖项一起使用,而不能与其他库模块依赖项或 AAR 二进制依赖项一起使用。

从上面截图可以看到,在AS 的 project structure的添加 dependency 界面,你会看到每个 dependency 后面可以致命 scope, 因为我的 gradle 是3.0版本,compile,apk,provided 这三种依赖方式已经 deprecated. 取而代之的implementation, api, compileOnly, and runtimeOnly几种方式

那新旧之间有什么不同呢?

gradle3.0之前的 build.gradle 文件是这样的,依赖项目默认都是通过compile

而gradle3.0后,module 下的build.gradle 项目依赖可以是这样子

gradle3.0或者以上版本 3.0之前(deprecated) 说明 作用
implementation compile gradle升级到3.0之后,新增了 implementation, 而compile 方式被标记为了deprecated, compile 在3.0之后仍然可以使用,但是 gradle 官方说会在 gradle 后续的某次重要升级后变为不可用. 如果我们使用了implementation方式来依赖项目的话,那么这个库就在编译时期,只对当前的module可见,对其他的module不可见,但是在运行使其是可见的,这种方式的好处是可以显著减少 build项目的时间,因为假如该依赖库有接口或者代码变动,那么Gradle只会去重新编译和它有直接依赖关系的module,也就是该库不存在传递性
api compile 同上 使用api方式来依赖项目或者库的话,那么这个库,在编译时期和运行时期都可以对其他module可见
compileOnly provided 3.0之后版本,使用compileOnly来替代provided 假如在项目中,对某些库你只是想要在编译时期使用,而在运行时期并不需要这个库,你可以使用这种方式!
runtimeOnly apk 3.0之后,使用 runtinmeOnly来替代apk Gradle 在运行时会将该库添加到 build 的 output 中去

也许到此刻,有些同学还是处于懵懵懂懂的状态,下面让我以几个例子来详细说明他们的作用

在我的项目里共有 app,common,factory,lang这4个module
他们的依赖关系是 [app->factory->common->lang]

那么此时如果我的 common这个 module中使用 implementation 来引入 gson 库,那么在 factory 和 app 这两个 module中,你是无法是用Gson 的,编译时期是无法找到这个类的,implementation 不具有传递性,如果使用 api 或者 compile 来引入 gson 库,便可以在 app 和 factory 中直接使用 gson 库,而不必再次引入.

什么时候用到 compileOnly呢?

我们在开发的时候,如果想要查看 PhoneWindow ,WindowManager 这些 framework 层的代码,可以将 sdk 中的 platforms中的 android.jar 放入 lib 文件夹中,然后add as Library,此时会在 build.gradle 文件中生成一句
implementation files(‘libs/android.jar’)
我们可以将 implementation替换为 compileOnly,此时就可以查看 PhoneWindow 这些 framework 层的源码了