Chromium for Android 编译过程和踩坑说明

最近试着玩Android上的chrome,并没有开源,试试去接触Chromium,官方的文档也写的很清楚了,
要做这些事情,免不了就是下载源码,进行编译,然后修改代码,进行提交。
官方文档在这里: Chromium Projects.

必备工具

  • Ubuntu 14.04 64bit
  • 梯子

踩坑纪要:

一开始就踩坑了,其实Google官方是推荐使用Ubuntu的,只有一处提到了说认为在这个平台有着最大的好运。但是其他地方都是使用Linux进行描述。
手里正好有Deepin15的系统,就装了这个进行,结果各种坑,因为从15以来deepin已经不再使用ubuntu了,而是基于debian…各种源不一样就不说了,
还提议编译Shadowsocks Qt for debian.。。
下载源码的过程都能出错,缺各种依赖,跑一些脚本也会报错,最后好不容易我装完了各种缺的文件,发现gclient sync死活成功不了。不能忍,果断换成Ubuntu14.04。

所以建议就老老实实的看完官方的所有说明,使用ubuntu吧。

JDK

openJdk和oracle的都可以,我试了都没问题,第一次成功时使用的openjdk1.7,后来换成了oracle 也可以。这一步应该很简单,大家可以apt-get,也可以自己下载了配置环境变量。

下载源码

完整过程可以参考 官方说明.

下载depot_tools

depot_tools是Google提供的为chromium进行代码管理的一套工具,包括gclient,gn,gyp等一系列脚本工具等,在以后和源码打交道时会一直使用到它。
首先是下载。

1
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git

下载后记得要配置环境变量,在.bashrc或者.bash_profile等。

1
export PATH = [your_depot_tools dir]/bin:$PATH

下载chromium源码

首先是创建一个目录存放源码,然后进入这个源码。

1
fetch --nohooks android

这一过程可能需要较长时间.如果中途出了什么问题,一定要进行以下步骤:

1
gclient sync

如果到这步没有问题,那么恭喜你,源代码下载成功了。

不过我是没这么幸运了,中间出了很多问题,fetch过程和sync过程都出过错,缺依赖,请走以下步骤:

1
./build/install-build-deps.sh

以及安装第三方库(src目录下)

1
./build/install-build-deps-android.sh

如果还不行,请检查网络代理后再sync。~

中途可能会出现某个py文件执行错误或者exit ,请不要担心,重新确认可以翻墙后再sync一次。

编译源代码

官方文档解释,已经放弃GYP了,现在推荐使用GN进行配置和build。

1
gn args out/Default

这里的out/Default目录就是以后编译的apk的输出的一层,apk会存放在apks中。

接着稳妥起见,可以再来一次sync.

1
gclient sync

接下来准备编译环境:

1
. build/android/envsetup.sh

官方对编译提供了3种编译方案,分别是完整的浏览器,content shell(提供了浏览器基本功能的一个壳),webview。

编译完整浏览器:

1
ninja -C out/Default chrome_public_apk

可以在out/Default/apks中看到相应的apk.
编译contentshell:

1
ninja -C out/Release content_shell_apk

编译webview:

1
ninja -C out/Release android_webview_apk

这几步都可以得到对应的apk。

可以将手机连接到电脑,然后可以push过去安装,或者直接install,当然也可以用官方文档提供的方法直接使用depot_tool进行安装。

好了,到这里就完成了chromium的编译,可以修改代码进行提交了。

invalidate和requestLayout流程认识

我们在之前已经分析了Android中view的绘制过程,知道了一个view是如何从父view往子view一层层的递归下去完成测量、布局和绘制的。
知道这些已经能完成基本的简单的自定义的view的开发了,但是在实际开发中我们往往会碰到或者使用两个同样很常见的方法—— invalidate和requestLayout.

invalidate

山不过来我就过去。话不多说,我们看看源码。

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
/**
* Invalidate the whole view. If the view is visible,
* {@link #onDraw(android.graphics.Canvas)} will be called at some point in
* the future.
* <p>
* This must be called from a UI thread. To call from a non-UI thread, call
* {@link #postInvalidate()}.
*/

public void invalidate() {
invalidate(true);
}

/**
* This is where the invalidate() work actually happens. A full invalidate()
* causes the drawing cache to be invalidated, but this function can be
* called with invalidateCache set to false to skip that invalidation step
* for cases that do not need it (for example, a component that remains at
* the same dimensions with the same content).
*
* @param invalidateCache Whether the drawing cache for this view should be
* invalidated as well. This is usually true for a full
* invalidate, but may be set to false if the View's contents or
* dimensions have not changed.
*/

void invalidate(boolean invalidateCache) {
invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
}

这段代码还是很简单,注释也说的很清楚,只需要注意的是这个方法只能在主线程中调用,invalidate方法最终都是调用了invalidateInternal方法。

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
void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
boolean fullInvalidate)
{

...

if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)
|| (invalidateCache && (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID)
|| (mPrivateFlags & PFLAG_INVALIDATED) != PFLAG_INVALIDATED
|| (fullInvalidate && isOpaque() != mLastIsOpaque)) {
if (fullInvalidate) {
mLastIsOpaque = isOpaque();
mPrivateFlags &= ~PFLAG_DRAWN;
}

mPrivateFlags |= PFLAG_DIRTY;

if (invalidateCache) {
mPrivateFlags |= PFLAG_INVALIDATED;
mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
}

// Propagate the damage rectangle to the parent view.
final AttachInfo ai = mAttachInfo;
final ViewParent p = mParent;
if (p != null && ai != null && l < r && t < b) {
final Rect damage = ai.mTmpInvalRect;
damage.set(l, t, r, b);
//往父view传递,调用父view的invalidateChild方法
p.invalidateChild(this, damage);
}

...
}
}

从上述代码中可以发现,View方法中的invalidateInternal实际上是将刷新区域往上传给了父viewGroup的invalidateChild方法,也就是一个从下往上从子到父的一个回溯过程,在每一层view或者viewGroup中都对自己的显示区域和传过来的刷新的 damage区域Rect做一个交集。我们可以看看ViewGroup中的invalidateChild方法。

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
/**
* Don't call or override this method. It is used for the implementation of
* the view hierarchy.
*/

public final void invalidateChild(View child, final Rect dirty) {
ViewParent parent = this;

final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
...
if (!childMatrix.isIdentity() ||
(mGroupFlags & ViewGroup.FLAG_SUPPORT_STATIC_TRANSFORMATIONS) != 0) {
RectF boundingRect = attachInfo.mTmpTransformRect;
boundingRect.set(dirty);
...
transformMatrix.mapRect(boundingRect);
dirty.set((int) (boundingRect.left - 0.5f),
(int) (boundingRect.top - 0.5f),
(int) (boundingRect.right + 0.5f),
(int) (boundingRect.bottom + 0.5f));
}

do {
View view = null;
if (parent instanceof View) {
view = (View) parent;
}
...

parent = parent.invalidateChildInParent(location, dirty);
if (view != null) {
// Account for transform on current parent
Matrix m = view.getMatrix();
if (!m.isIdentity()) {
RectF boundingRect = attachInfo.mTmpTransformRect;
boundingRect.set(dirty);
m.mapRect(boundingRect);
dirty.set((int) (boundingRect.left - 0.5f),
(int) (boundingRect.top - 0.5f),
(int) (boundingRect.right + 0.5f),
(int) (boundingRect.bottom + 0.5f));
}
}
} while (parent != null);
}
}

其中最重要的就是第30行,parent = parent.invalidateChildInParent(location, dirty)
不停地往上递归调用invalidateChildInParent方法,直到顶层view也即是ViewRootImpl.

我们看看ViewGroup的invalidateChildInParent方法。

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
/**
* Don't call or override this method. It is used for the implementation of
* the view hierarchy.
*
* This implementation returns null if this ViewGroup does not have a parent,
* if this ViewGroup is already fully invalidated or if the dirty rectangle
* does not intersect with this ViewGroup's bounds.
*/

public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) {
if ((mPrivateFlags & PFLAG_DRAWN) == PFLAG_DRAWN ||
(mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID) {
if ((mGroupFlags & (FLAG_OPTIMIZE_INVALIDATE | FLAG_ANIMATION_DONE)) !=
FLAG_OPTIMIZE_INVALIDATE) {
dirty.offset(location[CHILD_LEFT_INDEX] - mScrollX,
location[CHILD_TOP_INDEX] - mScrollY);
if ((mGroupFlags & FLAG_CLIP_CHILDREN) == 0) {
dirty.union(0, 0, mRight - mLeft, mBottom - mTop);
}

final int left = mLeft;
final int top = mTop;

if ((mGroupFlags & FLAG_CLIP_CHILDREN) == FLAG_CLIP_CHILDREN) {
if (!dirty.intersect(0, 0, mRight - left, mBottom - top)) {
dirty.setEmpty();
}
}
mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;

location[CHILD_LEFT_INDEX] = left;
location[CHILD_TOP_INDEX] = top;

if (mLayerType != LAYER_TYPE_NONE) {
mPrivateFlags |= PFLAG_INVALIDATED;
}

return mParent;

} else {
mPrivateFlags &= ~PFLAG_DRAWN & ~PFLAG_DRAWING_CACHE_VALID;

location[CHILD_LEFT_INDEX] = mLeft;
location[CHILD_TOP_INDEX] = mTop;
if ((mGroupFlags & FLAG_CLIP_CHILDREN) == FLAG_CLIP_CHILDREN) {
dirty.set(0, 0, mRight - mLeft, mBottom - mTop);
} else {
// in case the dirty rect extends outside the bounds of this container
dirty.union(0, 0, mRight - mLeft, mBottom - mTop);
}

if (mLayerType != LAYER_TYPE_NONE) {
mPrivateFlags |= PFLAG_INVALIDATED;
}

return mParent;
}
}

return null;
}

我们会发现其实这段代码主要是对传过来的Rect进行了运算,取了交集,对damage和自己的显示区域,返回的还是parent。

之前也讲到了,在invalidateChild中层层递归往父viewGroup回溯,直到ViewRootImpl才会停止,那我们看看ViewRootImpl中发生了什么。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
checkThread();
...
if (dirty == null) {
invalidate();
return null;
} else if (dirty.isEmpty() && !mIsAnimating) {
return null;
}

...

invalidateRectOnScreen(dirty);

return null;
}

最关键的一步在invalidateRectOnScreen中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private void invalidateRectOnScreen(Rect dirty) {
final Rect localDirty = mDirty;
...

// Add the new dirty rect to the current one
localDirty.union(dirty.left, dirty.top, dirty.right, dirty.bottom);
// Intersect with the bounds of the window to skip
// updates that lie outside of the visible region

...
if (!mWillDrawSoon && (intersected || mIsAnimating)) {
scheduleTraversals();
}
}

这里需要解释一下的就是,invalidateChildInParent中返回的是null。这个结果在ViewGroup中分析时有用到,就结束了自子view到父view的递归过程。
因为invalidateChild中的do-while循环会终止。

往下我们看到在ViewRootImpl中调用了scheduleTraversals方法。这一步就是整个invalidate的关键执行步骤了。

1
2
3
4
5
6
7
8
9
10
11
12
13
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}

原来是scheduleTraversals向handler发送了一个异步消息,会执行TraversalRunnable,这个TraversalRunnable的run方法中,执行的就是
doTraversal方法。

1
2
3
4
5
6
7
8
9
10
11
12
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

...

performTraversals();

...
}
}

看,doTraversal最后走到了我们熟悉的performTraversals了,performTraversals就是整个View树开始绘制的起始地方,所以说View调用invalidate方法的实质是层层回溯上传到父view,直到传递到ViewRootImpl后调用scheduleTraversals方法,然后整个View树开始
重新按照Android view的绘制过程分析分析的View绘制流程再来进行view的重绘任务。

postInvalidate

除了常见的invalidate外,我们还经常碰到postInvalidate,其实关于这个的特点我们在一开始有提到过invalidate只能在UI线程进行调用,所以如果想要在非主线程中进行
invalidate的效果,就需要使用postInvalidate。

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* <p>Cause an invalidate to happen on a subsequent cycle through the event loop.
* Use this to invalidate the View from a non-UI thread.</p>
*
* <p>This method can be invoked from outside of the UI thread
* only when this View is attached to a window.</p>
*
* @see #invalidate()
* @see #postInvalidateDelayed(long)
*/

public void postInvalidate() {
postInvalidateDelayed(0);
}

我们可以看到这里其实调用了以下的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* <p>Cause an invalidate to happen on a subsequent cycle through the event
* loop. Waits for the specified amount of time.</p>
*
* <p>This method can be invoked from outside of the UI thread
* only when this View is attached to a window.</p>
*
* @param delayMilliseconds the duration in milliseconds to delay the
* invalidation by
*
* @see #invalidate()
* @see #postInvalidate()
*/

public void postInvalidateDelayed(long delayMilliseconds) {
// We try only with the AttachInfo because there's no point in invalidating
// if we are not attached to our window
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds);
}
}

可以发现实际上是调用了ViewRootImpl.dispatchInvalidateDelayed方法,那么我们来看看这个方法:

1
2
3
4
public void dispatchInvalidateDelayed(View view, long delayMilliseconds) {
Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view);
mHandler.sendMessageDelayed(msg, delayMilliseconds);
}

这里也就是ViewRootImpl的handler发送了一条消息MSG_INVALIDATE,注意这里已经涉及到线程间消息传递了。

1
2
3
4
5
6
7
8
9
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_INVALIDATE:
((View) msg.obj).invalidate();
break;
...
}
}

到这里为止,postInvalidate的实质就是在UI Thread中调运了View的invalidate方法,那接下来View的invalidate方法就不再重复说了,上面已经分析过了。

小结

关于二者的具体流程,上面已经写的很详细了,对于invalidate,就是从view开始一层层往上层调用,直到ViewRootImpl,然后重新绘制一遍。
对于postInvalidate,就是在viewRootImpl中给handler发送了一个请求重绘的消息,然后接着走invalidate,只是这个起始是可以在非UI线程上进行。

需要注意的是,invalidate和postInvalidate方法请求重绘View,只会调用draw方法,如果View大小没有发生变化就不会再调用layout,并且只绘制那些需要重绘的View的脏
的Rect,也就是谁调用,重绘谁。

在平常开发中,可能会有以下情况引起view重绘:

  • 直接手动调用invalidate方法.请求重新draw,但只会绘制调用者本身的view。
  • 调用setSelection方法。请求重新draw,但只会绘制调用者本身。
  • 调用setVisibility方法。 当View可视状态在INVISIBLE转换VISIBLE时会间接调用invalidate方法,继而绘制该View。当View的可视状态在INVISIBLE\VISIBLE转换为GONE状态时会间接调用requestLayout和invalidate方法,同时由于View树大小发生了变化,所以会请求measure过程以及layout过程,同样只绘制需要重新绘制的视图。
  • 调用setEnabled方法。请求重新draw,但不会重新绘制任何View包括该调用者本身。
  • 调用requestFocus方法。请求View树的draw,只绘制需要重绘的View。

requestLayout

本文最初提到了,除了invalidate外,常见以及常用的方法还有requestLayout。我们来看看这是怎么回事。

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
/**
* Call this when something has changed which has invalidated the
* layout of this view. This will schedule a layout pass of the view
* tree. This should not be called while the view hierarchy is currently in a layout
* pass ({@link #isInLayout()}. If layout is happening, the request may be honored at the
* end of the current layout pass (and then layout will run again) or after the current
* frame is drawn and the next layout occurs.
*
* <p>Subclasses which override this method should call the superclass method to
* handle possible request-during-layout errors correctly.</p>
*/

@CallSuper
public void requestLayout() {
if (mMeasureCache != null) mMeasureCache.clear();

if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
// Only trigger request-during-layout logic if this is the view requesting it,
// not the views in its parent hierarchy
ViewRootImpl viewRoot = getViewRootImpl();
if (viewRoot != null && viewRoot.isInLayout()) {
if (!viewRoot.requestLayoutDuringLayout(this)) {
return;
}
}
mAttachInfo.mViewRequestingLayout = this;
}

mPrivateFlags |= PFLAG_FORCE_LAYOUT;
mPrivateFlags |= PFLAG_INVALIDATED;

if (mParent != null && !mParent.isLayoutRequested()) {
mParent.requestLayout();
}
if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
mAttachInfo.mViewRequestingLayout = null;
}
}

注意31-33行,是不是和invalidate的思路很像,也是一层层的回溯调用父view的requestLayout方法,直至顶级视图ViewRootImpl。
我们看一下ViewRootImpl的requstLayout方法:

1
2
3
4
5
6
7
8
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}

checkThread就是检查一下目标线程是不是当前线程。

1
2
3
4
5
6
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}

scheduleTraversals做的事情就是和invalidate最后的过程差不多了,向viewRootImpl的

1
2
3
4
5
6
7
8
9
10
11
12
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}

最后也是走到performTraversals了。

因此这些过程和invalidate一样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
private void performTraversals() {
...
boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw);
...
boolean insetsChanged = false;

if (layoutRequested) {

final Resources res = mView.getContext().getResources();

if (mFirst) {
// make sure touch mode code executes by setting cached value
// to opposite of the added touch mode.
mAttachInfo.mInTouchMode = !mAddedTouchMode;
ensureTouchModeLocally(mAddedTouchMode);
} else {
if (!mPendingOverscanInsets.equals(mAttachInfo.mOverscanInsets)) {
insetsChanged = true;
}
if (!mPendingContentInsets.equals(mAttachInfo.mContentInsets)) {
insetsChanged = true;
}
if (!mPendingStableInsets.equals(mAttachInfo.mStableInsets)) {
insetsChanged = true;
}
if (!mPendingVisibleInsets.equals(mAttachInfo.mVisibleInsets)) {
mAttachInfo.mVisibleInsets.set(mPendingVisibleInsets);
if (DEBUG_LAYOUT) Log.v(TAG, "Visible insets changing to: " + mAttachInfo.mVisibleInsets);
}
if (!mPendingOutsets.equals(mAttachInfo.mOutsets)) {
insetsChanged = true;
}
if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT || lp.height == ViewGroup.LayoutParams.WRAP_CONTENT) {
windowSizeMayChange = true;

if (lp.type == WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL
|| lp.type == WindowManager.LayoutParams.TYPE_INPUT_METHOD) {
// NOTE -- system code, won't try to do compat mode.
Point size = new Point();
mDisplay.getRealSize(size);
desiredWindowWidth = size.x;
desiredWindowHeight = size.y;
} else {
DisplayMetrics packageMetrics = res.getDisplayMetrics();
desiredWindowWidth = packageMetrics.widthPixels;
desiredWindowHeight = packageMetrics.heightPixels;
}
}
}

// Ask host how big it wants to be
windowSizeMayChange |= measureHierarchy(host, lp, res, desiredWindowWidth, desiredWindowHeight);
}

...
boolean windowShouldResize = layoutRequested && windowSizeMayChange
&& ((mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight())
|| (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT &&
frame.width() < desiredWindowWidth && frame.width() != mWidth)
|| (lp.height == ViewGroup.LayoutParams.WRAP_CONTENT &&
frame.height() < desiredWindowHeight && frame.height() != mHeight));
...

if (mFirst || windowShouldResize || insetsChanged || viewVisibilityChanged || params != null) {
...
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
}

final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
if (didLayout) {
...
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
...
}
...
}

可以看看上述的关键代码,由于在ViewRootImpl的requestLayout中设置了mLayoutRequested为true,在一些boolean值的计算后,所以在performTraversal中可以进入走measure和layout,但是从invalidate中进入的performTraversal不会进入measure和layout。

而且由于surface是valid,所以也不会走到performDraw。

小结

requestLayout方法会层层递归到父view中,直至viewRootImpl,调用measure过程和layout过程,不会调用draw过程,也不会重新绘制任何View包括该调用者本身。

你真的了解ListView的缓存吗

我们都知道ListView的baseAdapter中,使用了一个view的缓存回收机制,我们经常被告知会把不可见的view缓存起来,并且在新的view显示时会重用之前回收的view,实际中在开发时会使用convertView去进行相关处理。那么我们不禁会好奇,这个缓存回收机制到底是怎么实现的?我们不该仅仅会使用BaseAdapter重写各个方法就够了,我们需要往深处去挖点宝藏。今天我们就来看看listView和adapter在视图回收和缓存环节是怎么做到的。

AbsListView

ListView是一个继承自AbsListView的类,要想深入这一部分,我们需要看看AbsListView的源码。AbsListView还是比较复杂的,但是我们可以在ListView的scrollListItemsBy,layoutChildren等方法中看到几个叫mRecycler 和recycleBin的对象,看命名似乎是和视图回收机制重用等有关,mRecycler就是来自AbsListView的,我们可以继续看下去。

1
2
3
4
5
/**
* The data set used to store unused views that should be reused during the next layout
* to avoid creating new ones
*/

final RecycleBin mRecycler = new RecycleBin();

涉及到这个功能,recycleBin是一个RecycleBin对象,RecycleBin是AbsListView的内部类。那我们就来研究一下RecycleBin这个类。
我们先看看这个注释。

1
2
3
4
5
6
7
/**
* The RecycleBin facilitates reuse of views across layouts. The RecycleBin has two levels of
* storage: ActiveViews and ScrapViews. ActiveViews are those views which were onscreen at the
* start of a layout. By construction, they are displaying current information. At the end of
* layout, all views in ActiveViews are demoted to ScrapViews. ScrapViews are old views that
* could potentially be used by the adapter to avoid allocating views unnecessarily.
*/

大意就是RecycleBin实现了布局中view的重用。RecycleBin有两个层级的存储。

  1. ActiveViews , 布局开始时要在屏幕中显示的view
  2. ScrapViews, 布局结束后所有的ActiveViews就降级为ScrapViews。ScrapViews就是旧view,主要是可能被adapter为了避免不必要的视图分配空间而重用。
    好了,RecycleBin的结构我们搞懂了,那么关于ListView的核心问题就变成了RecycleBin是怎么运用ActiveViews和ScrapViews的了。换句话说就是ActiveViews和ScrapViews是怎么产生、怎么添加、怎么交换的?

嗯,继续看源码。
private View[] mActiveViews = new View[0];
private ArrayList[] mScrapViews;

mActiveViews就是一个ActiveViews堆,可以看到mActiveViews是一个View数组。
mScrapViews是一个ScrapViews堆,是一个ArrayList数组。这里为什么要这样设计存储?我们先卖个关子。
在研究这两个不同层级的View堆前,我们先看看在ListView中怎么使用RecycleBin的。

  1. setAdapter方法中,mRecycler.clear();
  2. setAdapter方法中,设计到了viewType的操作,因为会有不同的视图结构,mRecycler.setViewTypeCount(mAdapter.getViewTypeCount());
  3. onMeasure方法中,
1
2
3
if (recycleOnMeasure() && mRecycler.shouldRecycleViewType(((LayoutParams) child.getLayoutParams()).viewType)) {
mRecycler.addScrapView(child, 0);
}
  1. makeAndAddView方法中,mRecycler.getActiveView(position);
  2. layoutChildren方法中,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
final RecycleBin recycleBin = mRecycler;
if (dataChanged) {
for (int i = 0; i < childCount; i++) {
recycleBin.addScrapView(getChildAt(i), firstPosition+i);
}
} else {
recycleBin.fillActiveViews(childCount, firstPosition);
}

// Clear out old views
detachAllViewsFromParent();
recycleBin.removeSkippedScrap();
……
// Flush any cached views that did not get reused above
recycleBin.scrapActiveViews();

其中在layoutChildren中的用法特别典型,我们具体来看一看。
以上的这段代码可以大概看出一些逻辑思路:

  1. 先判断数据是否有改变,如果改变了就将当前的children加到ScrapViews中,否则加到ActiveViews中。
  2. removeSkippedScrap,把旧的view都删掉。
  3. 最后将以上没有被重用的缓存的view都回收掉。将当前的ActiveVies 移动到 ScrapViews。

以上是我们通过这一段代码的一个猜测分析,现在一步步看看源码。
dataChanged是一个AdapterView的boolean变量。
其中ListView 继承自AbsListView, AbsListView 继承自AdapterView,AdapterView继承自ViewGroup.
对dataChanged的赋值主要是在AdapterView中的内部类AdapterDataSetObserver中进行的。我们知道listView的adapter使用了观察者模式。这个是怎么做到的?
我们先看看AdapterDataSetObserver的源码:

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
class AdapterDataSetObserver extends DataSetObserver {

private Parcelable mInstanceState = null;

@Override
public void onChanged() {
mDataChanged = true;
mOldItemCount = mItemCount;
mItemCount = getAdapter().getCount();

// Detect the case where a cursor that was previously invalidated has
// been repopulated with new data.
if (AdapterView.this.getAdapter().hasStableIds() && mInstanceState != null
&& mOldItemCount == 0 && mItemCount > 0) {
AdapterView.this.onRestoreInstanceState(mInstanceState);
mInstanceState = null;
} else {
rememberSyncState();
}
checkFocus();
requestLayout();
}

@Override
public void onInvalidated() {
mDataChanged = true;

if (AdapterView.this.getAdapter().hasStableIds()) {
// Remember the current state for the case where our hosting activity is being
// stopped and later restarted
mInstanceState = AdapterView.this.onSaveInstanceState();
}

// Data is invalid so we should reset our state
mOldItemCount = mItemCount;
mItemCount = 0;
mSelectedPosition = INVALID_POSITION;
mSelectedRowId = INVALID_ROW_ID;
mNextSelectedPosition = INVALID_POSITION;
mNextSelectedRowId = INVALID_ROW_ID;
mNeedSync = false;

checkFocus();
requestLayout();
}

public void clearSavedState() {
mInstanceState = null;
}
}

代码很简单,继承自DataSetObserver,重写了onChanged和onInvalidated两个方法,对mDataChanged的操作都是在数据发生改变后将mDataChanged设为true,那么在哪里会变成false呢?AdapterView中已经没有了。我们还需要回到ListView中继续看。

在ListView中搜索这个变量会发现,将其变为false还是同样的在layoutChildren中,并且是完成了对mActiveViews和mScrapViews的各种操作之后才变为false。
并且在makeAndView中使用了false时的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft, boolean selected) {
View child;

if (!mDataChanged) {
// Try to use an existing view for this position
child = mRecycler.getActiveView(position);
if (child != null) {
// Found it -- we're using an existing child
// This just needs to be positioned
setupChild(child, position, y, flow, childrenLeft, selected, true);

return child;
}
}

// Make a new view for this position, or convert an unused view if possible
child = obtainView(position, mIsScrap);
// This needs to be positioned and measured
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
return child;
}

makeAndAddView能够获取一个view并且把它添加到child的list中,并且返回了这个child,这个child可以是新view,也可以是没有使用过的view
convert过来的,或者说是从缓存中重用的view。
这当中有一个getActiveView方法,就是我们在之前提到的RecycleBin的第四个用法,也是在mDataChanged为false时一个处理方法。
好了,对mDataChanged的分析和“寻找”先到这里,我们接着看看RecycleBin的用法。

既然是我们之前讲过的处理流程,我们先看看最初的一个RecycleBin使用情况。

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
void addScrapView(View scrap, int position) {
final AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams();
if (lp == null) {
return;
}

lp.scrappedFromPosition = position;
......

//当一个view有瞬态时不用被废弃
final boolean scrapHasTransientState = scrap.hasTransientState();
if (scrapHasTransientState) {
if (mAdapter != null && mAdapterHasStableIds) {
// 如果adapter有稳定的ids,那就能对相同的数据进行view的重用
if (mTransientStateViewsById == null) {
mTransientStateViewsById = new LongSparseArray<View>();
}
mTransientStateViewsById.put(lp.itemId, scrap);
} else if (!mDataChanged) {
// 如果绑定的数据没有改变,就能在旧位置重用view
if (mTransientStateViews == null) {
mTransientStateViews = new SparseArray<View>();
}
mTransientStateViews.put(position, scrap);
} else {
// 其他情况只能移除view并且从头来过
if (mSkippedScrap == null) {
mSkippedScrap = new ArrayList<View>();
}
mSkippedScrap.add(scrap);
}
}else{
if (mViewTypeCount == 1) {
//这里的mCurrentScrap就是mScrapViews[0]
mCurrentScrap.add(scrap);
} else {
mScrapViews[viewType].add(scrap);
}

if (mRecyclerListener != null) {
mRecyclerListener.onMovedToScrapHeap(scrap);
}
}
}

我们可以看到,这里的逻辑还算很清晰,在关于view的重用的判断时,涉及到一个概念还是需要解释一下,就是view的瞬态。
View.hasTransientState()的代码就不贴了,主要是几个flag的运算,虽然就一行,但是需要前后联系,感兴趣的朋友可以去看看源码。
我们这里主要理解瞬态,当我们说一个view有瞬态时,我们指app无需再关心这个view的保存与恢复,注释指出一般用来播放动画或者记录选择的位置等相似的行为。
当一个view标记位有瞬态时,在RecycleBin中,就有可能不会降级到ScrapView,而是mTransientStateViews或者mTransientStateViewsById将其保存起来,以便于
后来的重用。

1
2
private SparseArray<View> mTransientStateViews;
private LongSparseArray<View> mTransientStateViewsById;

在RecycleBin中涉及到瞬态的存储结构是上述代码中的两个,其实就是SparseArray,一个存的是position的key,一个存的是id。

这里介绍了mTransientStateViews的写,我们再看看mTransientStateViews的读。这里的读也是读的SparseArray,虽然功能类似于HashMap,但是读数据时使用
的是valueAt方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
View getTransientStateView(int position) {
if (mAdapter != null && mAdapterHasStableIds && mTransientStateViewsById != null) {
long id = mAdapter.getItemId(position);
View result = mTransientStateViewsById.get(id);
mTransientStateViewsById.remove(id);
return result;
}
if (mTransientStateViews != null) {
final int index = mTransientStateViews.indexOfKey(position);
if (index >= 0) {
View result = mTransientStateViews.valueAt(index);
mTransientStateViews.removeAt(index);
return result;
}
}
return null;
}

上面这份代码就是用来获取瞬时态的view的,首先从mTransientStateViewsById中读取,如果没有就从mTransientStateViews中读取。
这个方法是属于RecycleBin的,但是这个是在哪里调用的呢?答案是AbsListView的obtainView方法。

这里是一个比较关键的地方了。

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
View obtainView(int position, boolean[] isScrap) {
......

final View transientView = mRecycler.getTransientStateView(position);
if (transientView != null) {
final LayoutParams params = (LayoutParams) transientView.getLayoutParams();

// If the view type hasn't changed, attempt to re-bind the data.
if (params.viewType == mAdapter.getItemViewType(position)) {
final View updatedView = mAdapter.getView(position, transientView, this);

// 重新绑定数据失败,就废弃获取到的view
if (updatedView != transientView) {
setItemViewLayoutParams(updatedView, position);
mRecycler.addScrapView(updatedView, position);
}
}
......
return transientView;
}

final View scrapView = mRecycler.getScrapView(position);
final View child = mAdapter.getView(position, scrapView, this);
if (scrapView != null) {
if (child != scrapView) {
// Failed to re-bind the data, return scrap to the heap.
mRecycler.addScrapView(scrapView, position);
} else {
isScrap[0] = true;

child.dispatchFinishTemporaryDetach();
}
}

......

setItemViewLayoutParams(child, position);

......

return child;
}

阅读代码可以发现,在obtainView中,最重要的一个方法就是调用了adapter的getView方法,这个方法也是我们平常重写的方法,getView返回的是一个view。
而在对这个view的获取过程,有一个很明显的两层处理,首先就是尝试获取我们之前分析介绍的transientView,也就是拥有瞬时态的view,通过transientView
以完成复用。如果这一步走的失败了,也就是transientView为null时,或者说这个view并不拥有瞬时态,那么就从ScrapView中获取一个scrapView,这里可能要对
scrapView多做一些处理,我在上面都省略了,不影响逻辑大局,感兴趣的朋友可以去看看源码。最后返回的是scrapView。

getTransientStateView我们已经介绍过了,现在来看看失败之后的getScrapView。

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
View getScrapView(int position) {
if (mViewTypeCount == 1) {
return retrieveFromScrap(mCurrentScrap, position);
} else {
final int whichScrap = mAdapter.getItemViewType(position);
if (whichScrap >= 0 && whichScrap < mScrapViews.length) {
return retrieveFromScrap(mScrapViews[whichScrap], position);
}
}
return null;
}

private View retrieveFromScrap(ArrayList<View> scrapViews, int position) {
final int size = scrapViews.size();
if (size > 0) {
// 检查对某一个position或者id是否还有一个对应的view
for (int i = 0; i < size; i++) {
final View view = scrapViews.get(i);
final AbsListView.LayoutParams params = (AbsListView.LayoutParams) view.getLayoutParams();

if (mAdapterHasStableIds) {
final long id = mAdapter.getItemId(position);
if (id == params.itemId) {
return scrapViews.remove(i);
}
} else if (params.scrappedFromPosition == position) {
final View scrap = scrapViews.remove(i);
clearAccessibilityFromScrap(scrap);
return scrap;
}
}
final View scrap = scrapViews.remove(size - 1);
clearAccessibilityFromScrap(scrap);
return scrap;
} else {
return null;
}
}

上面这份代码还是写的很清楚了,根据不同的viewType来进行不同策略的取scrapView,但是实质上都是走到了retrieveFromScrap。
思路还是比较清晰,但是需要注意的是在get到scrapView后,在scrapView堆中这个view就会被移除掉。以便以后不停的循环往复的重用。

以上的分析都是在layoutChildren中判断数据改变后的过程,当数据没有改变时,会走到fillActiveViews方法。这个方法能够将AbsListView的所有子view都
装到activeViews中。代码很简单,就是一个for循环,将listView的所有子view一一存到activeViews中。

接着我们最初的分析,这些都走完之后,会执行recycleBin的removeSkippedScrap方法。还记得我们介绍的addScrapView方法吗,当一个view是有瞬时态的,但是
却没有一个保存到mTransientStateViewsById或者mTransientStateViews中时,会存在mSkippedScrap中。这是都会通过removeSkippedScrap全部清空。

在layoutChildren中,涉及到RecycleBin的最后还有一个方法就是scrapActiveViews。因为已经完成了children的布局layout位置的摆放等,所以这个时候需要刷新
缓存,scrapActiveViews这部分代码可能处理的过程有点多,但是最重要的一件事就是将现在mActiveViews还剩下的views都会移到mScrapViews中。

这个迁移过程也是和addScrapView的过程差不多。一开始是先对mActiveViews遍历,每次保存当前结点victim,并将mActiveViews[i]置空,然后对victim进行迁移
操作,如果符合条件就加到mTransientStateViewsById或者mTransientStateViews中,否则就加到mScrapViews中。

好了,至此在addScrapView中对RecycleBin进行的一系列操作就讲完了,我们看看下一步是什么。

ListView的makeAndAddView方法。
makeAndAddView的代码我们之前也贴过了,当判断数据没有发生改变时,会走到RecycleBin的getActiveView方法。
那我们来看看getActiveView。

1
2
3
4
5
6
7
8
9
10
View getActiveView(int position) {
int index = position - mFirstActivePosition;
final View[] activeViews = mActiveViews;
if (index >=0 && index < activeViews.length) {
final View match = activeViews[index];
activeViews[index] = null;
return match;
}
return null;
}

getActiveView的代码就很简单了,根据丢过来的position计算出一个真实有效的index,然后从activeViews中获取相应的view。没什么可讲的。

ok,关于RecycleBin的几个主要方法和执行流程就介绍完了。

我们可以暂时回顾小结一下,实际上RecycleBin的主要结构就是三个,一个是activeView堆,结构是一个View数组,另一个是scrapView堆,结构是一个
ArrayList数组。还有一个是transientViews,结构是SparseArray,主要通过Id或者position存取。
几个结构可以理解为层级不同,activeView比scrapView高一点,当触发了某种条件或者机制后,child的view就会从activieView中移到transientViews或者scrapView中进行缓存。
当ListView需要obtainView时,会先从有瞬时态的sparseArray中获取view,当失败时就会去scrapViews中获取view。
当然,这些过程又是和一个boolean变量mDataChanged进行配合的,具体的过程在上面的源码分析中已经解释过了,诸位可以回过去看看。
基本思路是在给子view布局时,如果数据没有发生改变,就使用当前已经存在ActiveViews的view。
在obtainView时,如果发生了改变,就addScrapView.否则就fill with activeView..

再次说到addScrapView,由于这是一个比较重要的方法,这里小结时我们也来看看哪些地方调用了addScrapView.
我们可以在ListView源码中搜索addScrapView看看。

  1. onMeasure
  2. measureHeightOfChildren , measure listView指定范围的高度, 在onMeasure中调用
  3. layoutChildren
  4. scrollListItemsBy , 以一定child数目滑动List,需要将滑出的child删掉,在最后添加view

其实前三个中用到的addScrapView我们之前也已经都讲到了,addScrapView的实现过程也不算复杂,主要是和activeView以及有瞬态的view的配合使用。
第四个我们接下来讲一下。

既然了解了RecycleBin的缓存结构和基本方法后,我们来实战看看,在一个Listview滑动过程中,到底是怎么实现view的回收的吧。

现在考虑滑动一个ListView的情况,也就是scrollListItemsBy方法。

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
private void scrollListItemsBy(int amount) {
...
final AbsListView.RecycleBin recycleBin = mRecycler;
if (amount < 0) { //上滑
...
View last = getChildAt(numChildren - 1);
while (last.getBottom() < listBottom) {
final int lastVisiblePosition = mFirstPosition + numChildren - 1;
if (lastVisiblePosition < mItemCount - 1) {
last = addViewBelow(last, lastVisiblePosition);
numChildren++;
}
...
}
...
View first = getChildAt(0);
while (first.getBottom() < listTop) {
AbsListView.LayoutParams layoutParams = (LayoutParams) first.getLayoutParams();
if (recycleBin.shouldRecycleViewType(layoutParams.viewType)) {
recycleBin.addScrapView(first, mFirstPosition);
}
...
}
...
}else{ //往下滑
...
View last = getChildAt(lastIndex);
...
if (recycleBin.shouldRecycleViewType(layoutParams.viewType)) {
recycleBin.addScrapView(last, mFirstPosition + lastIndex);
}
...
}
}

可以看到,每次ListView的滑动事件要将一个view滑出屏幕时,会将头部的或者尾部的(视方向而定)childView通过addScrapView缓存起来,
缓存的流程就是我们一开始分析的了,先保存有瞬时态的view,然后视情况存到scrapViews中。

等到每次obtainView时再依序从缓存的view中取出来。

这样就完成了一个滑动的缓存与回收。

好了,关于ListView的回收机制到这里就讲的差不多了,本质上就是对AbsListView的内部类RecycleBin的操作。
当我们弄懂了这个机制,才能更好的思考更多的问题。

例如,最后我问大家一个问题:

根据我之前所讲的,当一个ListView的有若干个viewType, 当滑出的view和滑入添加的view的type不一样,比如说滑出了一个TextView的item,
滑入了一个ImageView的Item, 那这种情况下还能复用刚才的view吗?

Android的view到底是怎么绘制出来的

我们都知道,在android中,一个Activity对应一个PhoneWindow,而在PhoneWindow中包含一个DecorView,并且这个DecorView就是页面的顶级视图,DecorView又分为TitleActionBar和contentView,我们平常所填充的布局一般都是在content中,好,这个是大的背景知识。

我们写了很多代码后也都会发现,其实在android中,每一个布局或者视图空间都是继承View实现的,这些控件或者说布局,都是通过View的绘制机制和流程才能在手机屏幕上显示。不管是看书也好还是看网上的资料也好,我们都会被告知说android系统中一个View的成功绘制过程都必须经过measure、layout和draw三个部分。

缘起

我们今天就来阅读源码来看看这三步到底是怎么走的。

既然绘制是按照measure,layout, draw三步走的,那有个疑问是measure又是从哪过来的呢?我们这里先给出答案:

整个View的视图绘制过程,是从ViewRootImpl类中的performTraversals()开始的,performTraversals有上千行代码,主要是根据之前设置的状态和标记,来判断是否要重新Measure,layout和draw视图。我们看看这里的核心源码。

1
2
3
4
5
6
7
8
9
10
11
12
private void performTraversals() {
......
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
......
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
......
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
......
performDraw();
......
}

可以看到,核心的过程就是根据各种状态和条件来进行相关的绘制流程,关键代码就是上面所示,我们再看看上面列的几个的具体实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {

case ViewGroup.LayoutParams.MATCH_PARENT:
// window不能重新改变大小,必须和窗口大小一样。
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window 能更改大小,最大可以到root view的大小。
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// Window 需要指定大小。并且将root view 设为这个大小.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}

这个方法主要是用来测量root view的大小的。

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
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
try {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}

private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, int desiredWindowHeight) {
final View host = mView;
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout");
try {
......
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
......
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}

private void performDraw() {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "draw");
try {
draw(fullRedrawNeeded);
} finally {
mIsDrawing = false;
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}

所以看到这里,大致的流程我们就可以掌握了。

从ViewRoorImpl的performTraversals()方法开始,然后通过状态标记的判断,进行performMeasure,performLayout,performDraw,分别调用view的measure, layout, draw方法进行视图的绘制。

发展

既然从performTraversals开始分析到了measure, layout, draw,那我们现在就来详细看看这三个方法。

measure分析

在上文讲到,在performMeasure中会调用view的measure方法。我们看看measure的方法。

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
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
boolean optical = isLayoutModeOptical(this); //当前的View控件是一个ViewGroup并且这个ViewGroup的边界模式是最佳的(刚刚好盖住一个子内容)
if (optical != isLayoutModeOptical(mParent)) { //如果当前ViewGroup和父view的模式不一样,就裁剪一下。
Insets insets = getOpticalInsets();
int oWidth = insets.left + insets.right;
int oHeight = insets.top + insets.bottom;
widthMeasureSpec = MeasureSpec.adjust(widthMeasureSpec, optical ? -oWidth : oWidth);
heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
}

// 将宽高存在一个64位long 数据中,低32位的与运算是去掉高32的符号
long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);

if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
widthMeasureSpec != mOldWidthMeasureSpec ||
heightMeasureSpec != mOldHeightMeasureSpec) {

// first clears the measured dimension flag
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;

resolveRtlPropertiesIfNeeded();

int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// measure ourselves, this should set the measured dimension flag back
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} else {
long value = mMeasureCache.valueAt(cacheIndex);
// Casting a long to int drops the high 32 bits, no mask needed
setMeasuredDimensionRaw((int) (value >> 32), (int) value);
mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
......
}

mOldWidthMeasureSpec = widthMeasureSpec;
mOldHeightMeasureSpec = heightMeasureSpec;

mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
(long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
}

这个方法主要是用来确定这个view到底应该多大,并且由父view提供宽高参数的限制大小的信息。也就是说每个View的实际宽高都是由父view和自身决定的。这个类是final的,也就是不能继承重写,实际的测量
是在第28行 onMeasure中实现的,自定义的控件或者view视图都是在onMeasure中实现,这个方法是可以重写的。

我们看一下measure有两个参数,是由父视图传过来的,是父对子的一个限制信息。这个spec的int值分为两个部分,高2位表示MODE,所以最多能表示4中MODE,而实际上在MeasureSpec类中只定义了三种,即

  1. MeasureSpec.EXACTLY 指定了确定的大小
  2. MeasureSpec.AT_MOST 最大的大小
  3. MeasureSpec.UNSPECIFIED 未指定的大小

低30位表示size,也就是父view的大小。对于DecorView的mode,一般都是MeasureSpec.EXACTLY, 而size是屏幕的宽高。对于子view来说,宽高就是父和自己一起决定的。

我们看看onMeasure的源码:

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
/**
* <p>
* Measure the view and its content to determine the measured width and the
* measured height. This method is invoked by {@link #measure(int, int)} and
* should be overridden by subclasses to provide accurate and efficient
* measurement of their contents.
* </p>
*
* <p>
* <strong>CONTRACT:</strong> When overriding this method, you
* <em>must</em> call {@link #setMeasuredDimension(int, int)} to store the
* measured width and height of this view. Failure to do so will trigger an
* <code>IllegalStateException</code>, thrown by
* {@link #measure(int, int)}. Calling the superclass'
* {@link #onMeasure(int, int)} is a valid use.
* </p>
*
* <p>
* The base class implementation of measure defaults to the background size,
* unless a larger size is allowed by the MeasureSpec. Subclasses should
* override {@link #onMeasure(int, int)} to provide better measurements of
* their content.
* </p>
*
* <p>
* If this method is overridden, it is the subclass's responsibility to make
* sure the measured height and width are at least the view's minimum height
* and width ({@link #getSuggestedMinimumHeight()} and
* {@link #getSuggestedMinimumWidth()}).
* </p>
*
* @param widthMeasureSpec horizontal space requirements as imposed by the parent.
* The requirements are encoded with
* {@link android.view.View.MeasureSpec}.
* @param heightMeasureSpec vertical space requirements as imposed by the parent.
* The requirements are encoded with
* {@link android.view.View.MeasureSpec}.
*
* @see #getMeasuredWidth()
* @see #getMeasuredHeight()
* @see #setMeasuredDimension(int, int)
* @see #getSuggestedMinimumHeight()
* @see #getSuggestedMinimumWidth()
* @see android.view.View.MeasureSpec#getMode(int)
* @see android.view.View.MeasureSpec#getSize(int)
*/

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

我们可以看看这段注释,其实写的很清楚了,就是测量视图的大小。这个方法是可以重写的。其中setMeasuredDimension是一个final方法,通过调用setMeasuredDimensionRaw来保存测量到的宽高值。

1
2
3
4
5
6
7
8
9
10
11
12
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int opticalWidth = insets.left + insets.right;
int opticalHeight = insets.top + insets.bottom;

measuredWidth += optical ? opticalWidth : -opticalWidth;
measuredHeight += optical ? opticalHeight : -opticalHeight;
}
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}

setMeasuredDimension能够通过setMeasuredDimensionRaw对mMeasuredWidth和mMeasuredHeight变量赋值。我们measure的主要目的就是对View树中的每个View的mMeasuredWidth和mMeasuredHeight进行赋值,所以当这两个变量被赋值意味着该View的测量工作就全部完成。

在这里我们还需要注意一个方法,在onMeasure中的setMeasuredDimension中传入的参数,是通过getDefaultSize获取的一个默认大小。我们看看getDefaultSize。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);

switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}

可以看到,specMode等于AT_MOST或EXACTLY就返回specSize,等于UNSPECIFIED就返回size。这些就是系统默认的尺寸。
回头看onMeasure方法,其中getDefaultSize参数的widthMeasureSpec和heightMeasureSpec都是由父View丢过来的。getSuggestedMinimumWidth与getSuggestedMinimumHeight都是View的方法,我们可以看看。

1
2
3
4
5
6
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

这两个方法分别是返回建议视图需要使用的最小宽高,并且这两个宽高是由背景尺寸和设置的最小宽(或高)共同决定的。

如果一个view不再是ViewGroup,那么通过这一步就可以得到view的测量的大小,如果是viewGroup,还需要进一步测量子view的大小。

当一个view是一个viewGroup,那么会走到measureChildren方法,主要就是通过一个循环来要求所有children测量自己的大小。

1
2
3
4
5
6
7
8
9
10
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}

可以看到,当中还传入了一个widthMeasureSpec和heightMeasureSpec。这就是对这个子view的宽高的要求。我们继续看到measureChild.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();

final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);

child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin + widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin + heightUsed, lp.height);

child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

我在这里列出了两个方法,分别是measureChild和measureChildWithMargins。其实这里的逻辑很简单,两者的区别就是measureChildWithMargins在测量时除了父视图提供的measureSpec参数外还会把margin以及
padding也考虑在内。而measureChild只考虑了padding。除此之外,这两个方法干的事情差不多,都是通过getChildMeasureSpec调整child的宽高的easureSpec,然后调用child视图的measure方法,也就是我们
之前分析的方法,去测量自身。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
//获取父视图传来的mode和size
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);

//计算size减去padding的大小,如果小于0就返回0
int size = Math.max(0, specSize - padding);

//最后返回的结果值。
int resultSize = 0;
int resultMode = 0;

switch (specMode) {
// 父视图指定了一个准确的大小
case MeasureSpec.EXACTLY:
if (childDimension >= 0) { //说明开发者在xml文件或者java中设置了一个具体的大于等于0的大小值,所以将这个值设为结果,并且mode为EXACTLY.
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// 子视图就设为当前值
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// 子视图自己决定大小,但是最大是当前这个值。所以mode设为AT_MOST, 大小设为size
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
//以下两种case的逻辑思路和上述的默认case差不多,这里就不再赘述了,大家看代码应该很容易理解。
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;

}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

通过上面的代码我们很容易知道,getChildMeasureSpec做的事情就是,对于传给特定子view的MeasureSpec解析出specMode和specSize,然后根据不同的mode进入不同的case,通过子view的宽高大小来计算自身正确的MeasureSpec,也就是说对这个view的宽高MeasureSpec进行调整和修改。

这里计算出的所有测量的结果,都是onMeasure的参数

讲到这里,我们从上面的分析可以看到,最终决定View的测量大小是View的setMeasuredDimension方法,所以我们在自定义view的时候可以直接通过setMeasuredDimension设定一个大小值来设置View的mMeasuredWidth和mMeasuredHeight的大小,但是这样缺少了灵活性,因此还是要尽量避免这种写法。

另外我们还可以发现,当通过setMeasuredDimension方法最终设置完成View的measure之后View的mMeasuredWidth和mMeasuredHeight才会有真实的数值,所以如果当一个View想通过getMeasuredWidth()和getMeasuredHeight()方法来获取View测量的宽高,必须保证这两个方法在onMeasure流程之后使用才能返回非0的值。

那么回过头回到measureChild和measureChildWithMargins中,在通过getChildMeasureSpec对传入的measureSpec做出调整修改后,就会
child.measure(childWidthMeasureSpec, childHeightMeasureSpec)进行正确的一个测量过程。

measure小结

好了,贴了这么多代码,现在来小结一下,其实在测量过程一句话概括就是从顶层父View向子View递归调用view.measure方法,每次measure方法通过onMeasure方法进行MeasureSpec的计算。
几个比较重要的地方就是:

  1. MeasureSpec 一个int值。高2位模式specMode和低30位尺寸specSize组成。其中specMode有三种值:
  2. MeasureSpec.EXACTLY 指定了确定值,父View希望子View的大小是确定的,由specSize决定
  3. MeasureSpec.AT_MOST 最大模式,父View希望子View的大小最多是specSize指定的值
  4. MeasureSpec.UNSPECIFIED 未指定,父View完全依据子View的自己值来决定

当然,

  1. 另外还需要注意的是,View.measure方法是final的,不能重写,view子类只能通过重写onMeasure实现自己的测量计算逻辑。
  2. 最顶层DecorView测量时的MeasureSpec是由ViewRootImpl中getRootMeasureSpec方法确定的,也即是LayoutParams宽高参数均为MATCH_PARENT,specMode是EXACTLY,specSize是屏幕大小。
  3. ViewGroup类需要通过一个for循环对所有的children 视图进行逐一measure。
  4. ViewGroup的子类的LayoutParams必须继承MarginLayoutParams。
  5. View的布局大小由父View和子View共同决定。
  6. 使用View的getMeasuredWidth()和getMeasuredHeight()方法来获取View测量的宽高,必须保证这两个方法在onMeasure流之后被调用才能得到想要的结果。

layout分析

在最初的背景缘起中我们看到了ViewRootImpl中的performLayout负责view的layout步骤。

实际上layout的过程和measure有点类似,也是从顶层view开始一步步的往下递归,其实也就是从viewGroup一直layout到view为止的过程。好了,先说这么多,下面我们看看源码。

我们看到layout时,performLayout中的关键一步是host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight()),可以看到其中传入了4个参数,分别是左上右下的四个坐标。

我们先说一个结论,View的layout方法和ViewGroupLayout方法略微不太一样。由于ViewGroup是View的子类,我们先看看View的layout方法。

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
public void layout(int l, int t, int r, int b) {
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}

//保存layout之前的四个坐标
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;

//本质上都是通过setFrame给四个坐标参数进行复制
boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
//关键方法
onLayout(changed, l, t, r, b);
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;

ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
ArrayList<OnLayoutChangeListener> listenersCopy = (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}

mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}

其中setOpticalFrame只是对parent和Child的Inset进行一个计算,最后还是调用了setFrame。

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
   protected boolean setFrame(int left, int top, int right, int bottom) {
boolean changed = false;
......
if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
changed = true;

// 保存drawn位
int drawn = mPrivateFlags & PFLAG_DRAWN;

int oldWidth = mRight - mLeft;
int oldHeight = mBottom - mTop;
int newWidth = right - left;
int newHeight = bottom - top;
boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);

// 将旧位置置为无效
invalidate(sizeChanged);
// 分配新的位置
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);

mPrivateFlags |= PFLAG_HAS_BOUNDS;


if (sizeChanged) {
sizeChange(newWidth, newHeight, oldWidth, oldHeight);
}

if ((mViewFlags & VISIBILITY_MASK) == VISIBLE || mGhostView != null) {
// If we are visible, force the DRAWN bit to on so that
// this invalidate will go through (at least to our parent).
// This is because someone may have invalidated this view
// before this call to setFrame came in, thereby clearing
// the DRAWN bit.
mPrivateFlags |= PFLAG_DRAWN;
invalidate(sizeChanged);
// 需要显示列表的父view可能因为子view的边界改变而需要重新创建
invalidateParentCaches();
}

// 重置drawn位
mPrivateFlags |= drawn;

mBackgroundSizeChanged = true;
if (mForegroundInfo != null) {
mForegroundInfo.mBoundsChanged = true;
}

notifySubtreeAccessibilityStateChangedIfNeeded();
}
return changed;
}

可以看到,在setFrame中始终是返回了changed,而changed就是根据新位置和旧位置的不同得出的。这样就可以在Veiw.layout中进行的适当的调用onLayout方法
以及位置更改的回调。记得这里的layout是可以重写的。
我们再看看View中的onLayout方法。

1
2
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}

看到没,View中的onLayout方法是一个空方法,虽然是在layout中调用,但是需要在自定义View时自己去实现。参数就是一个changed指明当前是否是新的位置或者大小,然后需要一个相对父view的
四个角的位置坐标。

我们再看看ViewGroup的layout方法。

1
2
3
4
5
6
7
8
9
10
11
12
@Override
public final void layout(int l, int t, int r, int b) {
if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
if (mTransition != null) {
mTransition.layoutChange(this);
}
super.layout(l, t, r, b);
} else {
// record the fact that we noop'd it; request layout when transition finishes
mLayoutCalledWhileSuppressed = true;
}
}

我们可以看到ViewGroup的layout方法是final的,也就是子类不能重写,其中super.layout方法是走到了View.layout方法。
接下来看看ViewGroup的onLayout方法。

1
2
@Override
protected abstract void onLayout(boolean changed, int l, int t, int r, int b);

什么情况? ViewGroup的onLayout方法是抽象方法,需要在子类ViewGroup中去实现。所一个可以预期的流程就是在一个自定义的ViewGroup中,onLayout需要和onMeasure一起实现视图的布局过程。
当一个自定义的view通过onMeasure获得了自身和子视图的宽高大小后,就通过onLayout来进行布局,主要是对children的位置布局等一一进行摆放,一般是一个for循环进行。我们可以看看一个具体的实
现,我们知道LinearLayout是直接继承ViewGroup的,我们看看它是如何实现父类的抽象方法onLayout的。

1
2
3
4
5
6
7
8
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mOrientation == VERTICAL) {
layoutVertical(l, t, r, b);
} else {
layoutHorizontal(l, t, r, b);
}
}

这里的逻辑很简单,我们平常其实也是这么用的,在使用LinearLayout时必须要在xml中指定它的orientation,就是在这里进行一个判断。
其实这水平和竖直的onLayout也差不多,我们看看我们平常用的比较多的竖直线性布局吧。

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

int childTop;
int childLeft;

// 计算子view在右边界的位置
final int width = right - left;
int childRight = width - mPaddingRight;

// 减去左右两边的padding后子view可以利用的空闲空间
int childSpace = width - paddingLeft - mPaddingRight;

final int count = getVirtualChildCount(); //实际是调用getChildCount方法返回的mChildrenCount,也就是这个group中子view的个数

final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
//根据majorGravity计算childTop,也就是子view的竖直的起始位置
switch (majorGravity) {
case Gravity.BOTTOM:
// mTotalLength contains the padding already
childTop = mPaddingTop + bottom - top - mTotalLength;
break;

// mTotalLength contains the padding already
case Gravity.CENTER_VERTICAL:
childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
break;

case Gravity.TOP:
default:
childTop = mPaddingTop;
break;
}
//for循环遍历子view进行位置的摆放布局
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child == null) {
childTop += measureNullChild(i);
} else if (child.getVisibility() != GONE) {
//当前view为子view,此处获得了在measure过程中得到的宽高大小信息
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();

final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();

int gravity = lp.gravity;
if (gravity < 0) {
gravity = minorGravity;
}
final int layoutDirection = getLayoutDirection();
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
//计算左边的起始位置
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.CENTER_HORIZONTAL:
childLeft = paddingLeft + ((childSpace - childWidth) / 2)
+ lp.leftMargin - lp.rightMargin;
break;

case Gravity.RIGHT:
childLeft = childRight - childWidth - lp.rightMargin;
break;

case Gravity.LEFT:
default:
childLeft = paddingLeft + lp.leftMargin;
break;
}

//可能有divider的, 也要考虑进去
if (hasDividerBeforeChildAt(i)) {
childTop += mDividerHeight;
}

childTop += lp.topMargin;
//熟悉的setFrame,在这里完成位置的设置和摆放
setChildFrame(child, childLeft, childTop + getLocationOffset(child), childWidth, childHeight);
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

i += getChildrenSkipCount(child, i);
}
}
}

以上就是LinearLayout的onLayout的一个竖直布局的layoutVertical的过程,可以看到,主要就是对传入的四个参数(左上右下)四个位置的处理,通过一个for循环遍历ViewGroup中所有的子view,然后
完成各个view的位置设置。实际上中间有获取在onMeasure中测量得到的宽高信息,但是在这里并不是必须的,比如一些自定义的view的大小位置是固定的情况下。

为什么说到这里就完成了布局了呢?我们看layoutVertical方法的第77行,setChildFrame方法。

1
2
3
private void setChildFrame(View child, int left, int top, int width, int height) {
child.layout(left, top, left + width, top + height);
}

通过这一步就回到了View的layout方法,然后走到onLayout方法,这四个参数就是在layoutVertical里丢过来的左上右下四个位置坐标。就是沿着我们之前的分析了。

layout小结

Layout步骤就到这儿了,逻辑还是比measure过程要简单一点的。整个过程是比较容易理解的。
整个过程也是和measure一样,从顶层父View向子view递归调用view的layout方法,每一层将子view放在合适的位置上。

回顾起来尤其是在我们自定义view的实现时需要注意以下几个方面:

  1. View的layout方法可被重写,但是ViewGroup的layout是final的不能被重写,ViewGroup.onLayout为abstract的,子类必须重写实现自己的布局逻辑。
  2. layout操作完成之后得到的是对每个View进行位置摆放布局后的左上右下四个坐标,这些都是相对于父View的。
  3. 在xml等设置的layout_xx的布局属性都是的是包含当前子View的ViewGroup的设置,对自己以及对没有父view的view是没有意义的。

draw分析

现在我们可以进行第三步了,draw过程。

我们在本文最初有提到过,在ViewRootImpl中的performTraversals方法里,会逐步调用measure, layout, draw的步骤。关于measure, layout我们已经讲的差不多了,第三步就是draw,在
performTraversals里是走到了performDraw执行canvas的绘制。

关于View的绘制,详细的过程可以参阅 方立的博客, 方立秉持了处女座的追究细节的特点,
讲的很细,我这里就不很详细讲了,只大致讲一下过程吧。

在performDraw中调用了draw(boolean fullRedrawNeeded)方法。

1
2
3
4
5
6
7
8
9
10
private void draw(boolean fullRedrawNeeded) {
Surface surface = mSurface;
......
final Rect dirty = mDirty;
......
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
return;
}
......
}

我们看到,在draw中关键一步是调用了ViewRootImpl的drawSoftware方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff, boolean scalingRequired, Rect dirty) {
final Canvas canvas;
try {
final int left = dirty.left;
final int top = dirty.top;
final int right = dirty.right;
final int bottom = dirty.bottom;

canvas = mSurface.lockCanvas(dirty);
......
}......
canvas.translate(-xoff, -yoff);
......
mView.draw(canvas);
......
surface.unlockCanvasAndPost(canvas);
......
}

我们看到,在drawSoftware中调用了View的draw(Canvas canvas)方法。需要指出的是,在View中有两个draw方法,分别是

  • draw(Canvas canvas)
  • draw(Canvas canvas, ViewGroup parent, long drawingTime)。

在drawSoftware中走到的实际是draw(Canvas canvas),另一个我们过会再看。
draw(Canvas canvas)的话,主要做的事情就是手动将一个已经完成了layout的view画到给定的canvas上去,在自定义view时一般不需要重写这个方法,而是用onDraw重写。

网上已经讨论了很多了,这里的draw主要分为6步。

  1. 画背景
  2. 如果必要的话,保存canvas的图层
  3. 画当前view的内容
  4. 画子view
  5. 如果必要的话,画渐弱边缘以及恢复图层,这一步主要是和第二步对应。
  6. 画一些装饰view,比如滚动条等。

其中2和5不是必需的

第三步画自身的内容,是通过onDraw(canvas)完成的。
第四步画子view,是通过dispatchDraw(canvas)完成的。

1
2
3
4
5
6
7
protected void onDraw(Canvas canvas) {

}

protected void dispatchDraw(Canvas canvas) {

}

可以看到View的onDraw是空方法,需要在自定义view时自己去实现。
dispatchDraw也是一个空方法,需要子类自己实现,例如ViewGroup.

那我们看看ViewGroup的dispatchDraw方法。

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
@Override
protected void dispatchDraw(Canvas canvas) {
......
final int childrenCount = mChildrenCount;
final View[] children = mChildren;
......
for (int i = 0; i < childrenCount; i++) {
......
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
more |= drawChild(canvas, child, drawingTime);
}
}
......
// Draw any disappearing views that have animations
if (mDisappearingChildren != null) {
......
for (int i = disappearingCount; i >= 0; i--) {
......
more |= drawChild(canvas, child, drawingTime);
}
}
......
if ((flags & FLAG_INVALIDATE_REQUIRED) == FLAG_INVALIDATE_REQUIRED) {
invalidate(true);
}
......
}

我们看一下ViewGroup的关键代码,主要是一个for循环遍历了所有的child视图,并且调用drawChild(canvas, child, drawingTime)方法进行每个子view的绘制。我们跟着这个走下去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* Draw one child of this View Group. This method is responsible for getting
* the canvas in the right state. This includes clipping, translating so
* that the child's scrolled origin is at 0, 0, and applying any animation
* transformations.
*
* @param canvas The canvas on which to draw the child
* @param child Who to draw
* @param drawingTime The time at which draw is occurring
* @return True if an invalidate() was issued
*/

protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}

可以看到,drawChild调用了View的draw方法,但是这里调用的View的draw方法还不是我们之前分析的draw方法,而是重载的一个draw。

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
/**
* This method is called by ViewGroup.drawChild() to have each child view draw itself.
*
* This is where the View specializes rendering behavior based on layer type,
* and hardware acceleration.
*/

boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
final boolean hardwareAcceleratedCanvas = canvas.isHardwareAccelerated();
......
if (!drawingWithDrawingCache) {
if (drawingWithRenderNode) {
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
((DisplayListCanvas) canvas).drawRenderNode(renderNode);
} else {
// Fast path for layouts with no backgrounds
if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
dispatchDraw(canvas);
} else {
draw(canvas);
}
}
}
......
}

在这个draw方法中,主要是干了两件事,一件事是对画布canvas中做了调整,多次调用了translate方法进行移动和裁剪,然后如果子视图还是ViewGroup那就走到dispatchDraw,
如果子视图是View,那就走到View的draw(canvas)方法。

当然,这里的canvas都是ViewRootImpl中传过来的

这个draw方法只能是由ViewGroup来调用,所以本质上最后还是要走到draw(canvas)方法,然后调用onDraw方法,在传入的canvas上进行绘制。

好了,draw的过程到这儿也算大致讲清楚了。之前有说,如果对具体的canvas等绘制的过程也很感兴趣,可以看看
方立的博客 的博客阅读。

draw小结

其实这样分析下来,发现整个的逻辑流程和measure和layout是差不多的。都是从父view开始一层层往子view递归进行绘制。那么我们可以总结出什么呢?

  1. View本身不进行绘制,绘制的内容是要在子view中进行实现的。
  2. View动画和ViewGroup动画并不是一回事,View动画是自身的动画,而ViewGroup动画是显示子元素时的动画,是layoutAnimation.

好了,明白了Android上的view的绘制,才能方便我们更好的去在自定义view以及一些布局的处理上有着更好的理解和实现,并且对理解其他相关机制有着更好的掌握。

互斥锁ReentrantLock一探究竟

讲互斥锁之前,我们先学习一下基础的框架。

Lock接口

我们知道,从jdk 1.5之后在juc包中新增了Lock接口,通过Lock接口实现锁的功能,可以提供和synchronized关键字差不多的多线程同步功能。
Lock操作使用还是非常简单,进行lock操作后,只需要记得在finally中进行unlock操作即可,虽然不像synchronized那样可以更隐秘的进行同步操作,但是也很便捷。

阅读源码可以发现, Lock接口包括6个基本操作,如下面所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//获取锁操作,调用这个方法当前线程将会获取锁。
void lock();

//可中断的获取锁,和lock方法的不同是这个方法可以响应中断,也就是说当前线程在获取锁时可能中断。
void lockInterruptibly() throws InterruptedException;

//尝试非阻塞地获取锁,如果成功获取就返回true,否则返回false
boolean tryLock();

//超时获取锁。三种情况下会返回。1: 当前线程获取了锁,返回true,2: 当前线程在time内被中断,3: time时间到,返回false
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

//释放锁
void unlock();

//获取等待通知的instance,并且和当前线程绑定,当前线程只有获得了锁,才能调用wait方法,调用后当前线程就会释放锁。
Condition newCondition();

Lock接口主要就讲到这里,接下来我们就看看互斥锁的核心部分AbstractQueuedSynchronizer。其实Lock的实现都是通过AbstractQueuedSynchronizer的子类来完成的多线程加锁释放锁的。

AbstractQueuedSynchronizer队列同步器

AbstractQueuedSynchronizer是一个抽象类(以下我们简称AQS),主要是实现锁和其他同步功能组件的一个基本模板框架,维护了一个volatile类型的int变量state表示同步的状态,通过一个FIFO的队列来进行线程的排队管理。

AQS的基本框架和方法

当我们要实现一个同步的锁时,我们只需要集成AbstractQueuedSynchronizer并且实现它的抽象方法来对state进行操作,它提供了三个方法来操作state。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 同步状态
*/

private volatile int state;

//获取当前同步状态
protected final int getState() {
return state;
}

//设置当前同步状态
protected final void setState(int newState) {
state = newState;
}

//使用CAS设置当前状态,保证原子性
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

其实在使用CAS进行原子操作时,用到的Unsafe类,本质上就和我们之前介绍原子类AtomicInteger等一系列中讲到的就一样啦,这里就不再赘述了,主要关心AQS的框架和模板方法。说到模板方法,AQS本身是一个抽象类,也提供了一些方法供继承实现的类来实现这些方法,这些方法包括可重写的与不可重写的(final),这里我们先看个大概,接下来研究ReentrantLock时会详细看到。

1
2
3
4
5
6
7
8
9
10
11
//可重写的:
//独占的获取同步状态
protected boolean tryAcquire(int arg);
//独占的释放同步状态
protected boolean tryRelease(int arg)
//共享的获取同步状态
protected int tryAcquireShared(int arg);

//共享的释放同步状态
protected boolean tryReleaseShared(int arg);
//判断当前同步器是否被当前线程所独占
protected boolean isHeldExclusively();

我们可以看到,其实AQS提供了独占式和共享式两种对同步状态的操作,独占锁是一个锁在同一个时间点只能有一个线程占有和操作同步状态,并且又分为公平锁和非公平锁,比如我们接下来会讲到的ReentrantLock。共享锁是指能被多个线程同时拥有,能被共享的锁,例如ReentrantReadWriteLock。并且自定义的同步组件可以使用重写这些基本方法以及final的模板方法来实现自己的同步语义。

CLH队列

我们之前也介绍过了,AQS中维护一个FIFO的双向队列,实际上这个队列就是CLH队列。在独占锁的情况下,竞争资源和锁在一个时间点只能被一个线程访问并获取占有,其它的线程则需要等待。CLH就是管理这些“等待锁的线程”的队列。所以说,CLH队列主要用来管理AQS的同步状态。当前的线程获取同步状态失败时,AQS会将当前线程以及等待信息构造为一个Node,并且将这个Node加入CLH队列,并且将这个线程阻塞掉,等待下次同步状态被释放时被首节点中的线程唤醒。

Node的属性类型和名称,我们先列出一些基本的。

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
/**
*等待的状态。主要包括5个状态,1. CANCELLED =1, 同步队列汇总等待的线程等待超时或者被中断,需要从同步队列中取消等待,Node进入这个状态并不会发生什么变化。
2. SIGNAL = -1。 表明当前Node的下一个后继结点需要被唤醒,一般是当前结点的线程释放了同步状态或者被取消后的事情。
3. CONDITION = -2. 表明结点是等待在Condition上,只有其他线程对这个condition调用了signal后,这个结点才会从等待队列转移到同步队列。
4. PROPAGATE = -3. 表镜下一个共享的获取同步状态的获取会无条件的传播下去。
5. 0 初始化状态,不符合以上任何条件的。
*/

volatile int waitStatus;

/*
* 后继结点,也就是当前结点会唤醒的下一个结点
*/

volatile Node next;

/*
* 前驱结点, 在结点加入到同步队列后进行设置的。
*/

volatile Node prev;

/*
* 等待队列中的后继结点。如果当前结点是共享的,那么这个字段就是一个SHARED常量,也就是说结点类型和等待队列中的后继结点公用一个字段。
*/

Node nextWaiter;

/*
* 获取同步状态的线程
*/

volatile Thread thread;

Node是构成同步队列的基础,在AQS中拥有一个head结点和tail结点,凡是没有成功获取到同步状态的线程都将会构造为Node加入到该队列的尾部。

需要指出的是,加入队列时,是通过CAS操作进行的。这一过程我们在后面会详细介绍,先还是关注一下CLH队列。

我们已经知道了,AQS包括两个引用,一个指向头结点,另一个指向尾结点,当有新的结点要加入后,通过CAS操作设置尾结点,并且将尾结点的prev指向之前的尾结点,这样就完成了结点的插入。

首节点是获取到同步状态的结点,当首结点的线程在释放同步状态后,就会唤醒后继结点,而此时后继结点如果也能成功的获取到同步状态,就会将自己设置为首结点。这时候AQS也会更新自己的head结点的引用。

以上大致就是CLH队列的工作原理和过程,知道了这些之后我们结合ReentrantLock来具体看看同步状态的获取和释放。

ReentrantLock细究

ReentrantLock实现了公平锁和非公平锁。我们前面已经介绍了,ReentrantLock的核心在于AQS,在ReentrantLock中有一个抽象静态内部类Sync继承了AbstractQueuedSynchronizer来进行相关的同步状态的管理。而公平锁和非公平锁则是分别通过 FairSync和NonfairSync来继承Sync来进行锁的实现的,因此我们只需要搞懂FairSync和NonfairSync即可。

公平锁FairSync

我们知道,在使用ReentrantLock时获取锁是通过lock()函数。下面,我们就通过lock()对获取公平锁的过程进行一步步的探究学习。

1
2
3
final void lock() {
acquire(1);
}

这段代码很简单,就是通过acquire(1)获取锁。这里为什么传的参数是1呢,只是设置“锁的状态”的参数。对于“独占锁”而言,锁处于可获取状态时,它的状态值是0;锁被线程首次获取到了,它的状态值就变成了1。于ReentrantLock是可重入锁,所以独占锁可以被同一个线程多此获取,每获取1次就将锁的状态+1。也就是说,第一次获取锁时,通过acquire(1)将锁的状态值设为1;再次获取锁时,将锁的状态值设为2;依次类推。

acquire

acquire是在AQS中实现的。

1
2
3
4
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}

上面这段代码看似很简单,其实经历了好几个过程。我们先将一下逻辑。

  1. 当前线程首先通过tryAcquire()尝试独占的获取锁。获取成功的话,直接返回,尝试失败的话,进入到等待队列排序等待。
  2. 当前线程尝试失败的情况下,先通过addWaiter(Node.EXCLUSIVE)来将此线程加入到CLH队列末尾。
  3. 执行完addWaiter(Node.EXCLUSIVE)之后,会调用acquireQueued()来获取锁。由于是公平锁,它会根据公平性原则来获取锁。
  4. 当前线程在执行acquireQueued()时,会进入到CLH队列中休眠等待,直到获取锁了才返回,如果当前线程在休眠等待过程中被中断过,acquireQueued会返回true,此时if条件符合,会使当前线程调用selfInterrupt()来自己给自己产生一个中断。

好,我们先了解一下这个代码的基本逻辑,接下来我们对这四个方法一一研究。

tryAcquire

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
protected final boolean tryAcquire(int acquires) {
//获取当前要获取同步状态的线程
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) { //没有任何线程获取锁
//如果是头结点,那么通过CAS自旋去设置同步的状态,并将当前线程设置为锁的独占拥有者。
if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) { //如果当前线程已经是锁的独占拥有者,那么就更新一下同步状态。(即为可重入)
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}

我们看看hasQueuedPredecessors来做什么的:

1
2
3
4
5
6
7
public final boolean hasQueuedPredecessors() {

Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t && ((s = h.next) == null || s.thread != Thread.currentThread());
}

我们可以看到,返回的结果也是多个条件的判断,我们看看返回true的情况:

  1. h!=h, 也就是说CLH队列不是空,有线程在等待获取锁。
  2. h.next == null ,只有一个线程在等待,s.thread != Thread.currentThread()则是有比当前线程更早的线程在等待。
    因此可以发现hasQueuedPredecessors()通过判断当前线程的结点是不是在CLH队列的队首,来返回AQS中是不是有比当前线程等待更久的线程。和我们的分析是一致的。
    接下来我们看看CAS操作设置状态。
1
2
3
4
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

compareAndSwapInt是unsafe类中的一个本地方法,这种类型的操作我们在AtomicInteger中已经介绍了很多次了,这里同样的,采用一个原子操作来修改state的状态值,保证了原子性。
我们接着看setExclusiveOwnerThread,这个方法是在AbstractOwnableSynchronizer中,AbstractOwnableSynchronizer是一个抽象类,AQS就是继承AbstractOwnableSynchronizer实现的。

1
2
3
4
5
6
//独占锁的当前拥有者
private transient Thread exclusiveOwnerThread;

protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}

所以setExclusiveOwnerThread能够设置当前线程为拥有锁的独占拥有者。
好了,tryAcquire内容大致就是这些了,我们可以看到就像名称所说,只是尝试获得锁,如果获取成功,就返回true,如果获取失败,可以通过其他方式再去获得锁,这些方式我们下面会讲。

addWaiter

还记得我们之前看的acquire吗,当tryAcquire获取成功返回true后,acquire就直接结束了,当tryAcquire返回false时,就会进入acquireQueued(addWaiter(Node.EXCLUSIVE), arg),我们先看看addWaiter。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private Node addWaiter(Node mode) {
//新建一个Node,传入新的两个参数,一个是当前线程,一个是模式,这里是独占模式
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
// 如果CLH队列不为空,则通过CAS操作将当前线程插入到队列的尾部
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//如果队列为空,就先创建队列,再插入结点
enq(node);
return node;
}

这里的思路还是很清晰的,主要就是将当前线程构造为结点后插入到等待队列中。其中在队列为空时走到了enq,我们看看这里面发生了什么。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}

我们可以看到,在enq中是通过一个死循环来保证结点的正确添加,在死循环中只有通过CAS将结点成功设置为尾结点后当前线程才能从enq中返回。
事实上当结点添加到队列中后,就会不停的自旋,直到条件满足获取到了同步状态才会结束自旋过程。

所以总结一下addWaiter的作用就是将当前线程构造成结点添加到CLH队列的尾部,也就是说当当前线程在tryAcquire失败时,会添加到等待队列中进行等待。

acquireQueued

当一个线程被构造给Node并且加到等待队列后,接下来就是acquireQueued来进行操作了。acquireQueued会逐步检查队列中的线程去获取同步状态,废话不说了,我们先看看代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // 有助于GC回收
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}

看了源码,对于传入的node,也就是当前的线程,事实上操作的是它的前驱结点
这是为什么?

  1. 首先, 头结点是成功获取到同步状态的结点,当头结点释放了同步状态后才会唤醒后继结点。并且后继结点的线程被唤醒后还需要检查自己的结点是否是头结点。
  2. 因为结点的线程在等待过程中可能会发生中断从而返回,或者前驱结点出队,只有前置结点是头结点时才可以tryAcquire,这样能够保证CLH队列的FIFO的特性。

我们看看具体的流程,只有当前置结点是头结点并且尝试获取锁成功后,会将当前的线程的结点设置为头结点,并且直接返回false,也就是没有中断过。

那么中断过的是什么过程呢?我们先看看上面有两个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//当前结点获取锁失败时检查并更新状态,返回当前线程是否要阻塞
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 前继节点的状态
int ws = pred.waitStatus;
// 如果前继节点是SIGNAL状态,说明这当前线程需要被unpark唤醒。所以可以安全的阻塞。
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
//如果前继节点是取消状态,则设置当前节点的当前前继节点为原前继节点的前继节点。
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//如果前继节点为“0”或者“PROPAGATE”,则设置前继节点为SIGNAL状态,但是不需要阻塞。
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}

如果这一步返回的是true, 即当前结点的前置结点的状态为SIGNAL,那么当前线程需要被阻塞掉。阻塞的过程通过parkAndCheckInterrupt来实现。

1
2
3
4
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}

parkAndCheckInterrupt就是通过LockSupport来阻塞线程,并且返回线程是否中断过,同时还会清除线程中断的标记。这也是为什么在acquireQueued中会对interrupted进行操作的原因,因为需要保存线程的中断标记,关于这个我们稍后再说。
结合我们之前所讲的,我们可以先思考一下当线程被阻塞后什么时候会被唤醒?

  1. 前置结点在释放锁时会通过unpark唤醒下一个结点也就是当前线程
  2. 其他线程通过interrupt中断当前线程时会唤醒当前线程

所以我们回到acquireQueued中,看for循环中,会先判断前继结点是否是队列头结点,如果是就会尝试获得锁。
因为我们也在shouldParkAfterFailedAcquire中知道了当前线程是否会被阻塞,如果会阻塞就将其阻塞掉,并且通过变量保存一下这个线程是否被中断过。
正是有这种情况存在,所以在线程被唤醒时,必须先检查唤醒自己的线程是不是队列的头结点,这样才符合FIFO的公平性,这也是公平锁的意义。

总之,acquireQueued实现了当前线程会根据公平性原则进行阻塞等待,直到获取锁为止,并且会返回当前线程在等待过程中有没有并中断过的标记。

selfInterrupt

回到AQS中acquire的if判断,当线程获取锁失败,并且被加到队列尾部,也返回了这个线程是否要阻塞以及阻塞后返回了是否中断过的标记后,这时候再把自己中断一下。

1
2
3
static void selfInterrupt() {
Thread.currentThread().interrupt();
}

根据我们之前的分析,在acquireQueued中,只有当前线程被中断过,才会走到这一步。
那么思考一下,即使是线程在阻塞状态中被中断唤醒,但是如果前面还有其他等待的线程,那么还是无法获取到锁,会再次阻塞,直到前一个结点唤醒自己。
而这个线程在真正执行之前由于走到了interrupted,所以会清除这个标记,因此需要重新中断一下当前的线程。

好了,关于公平锁的基本框架和原理就是这些了,其实回过头看看acquire,主要就是做了tryAcquire,addWaiter,acquireQueued和selfInterrupt。其中tryAcquire是一定会做的事情。

而由于ReentrantLock是可重入锁,在获取同步状态时,传入的arg就是当前线程获取锁的次数,当重入了多少次,就得unlock多少次。说到了unlock,那我们就来看一看unlock.

unlock

1
2
3
public void unlock() {
sync.release(1);
}

可以看到,unlock非常简单,就是执行了AQS的release. 我们看到参数是1,也就是每次释放时只对同步状态进行减1操作。unlock调用了AQS的release,让我们看一眼。

release

1
2
3
4
5
6
7
8
9
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}

这种形式和思路是很像acquire的。首先会执行sync的tryRelease,如果成功就返回true,并且如果头结点不为空并且状态不为0,就唤醒下一个结点.

1
2
3
4
5
6
7
8
9
10
11
12
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}

我们看看源码,首先会计算当前线程的结点在释放后的状态。然后检查当前线程,如果不是锁的独占拥有者,就会抛出异常。

当状态为减到0后,就采用setExclusiveOwnerThread,将锁的拥有者置为null,并且通过setState设置同步状态。这里的种种方法在上文已经介绍过,这里就不再赘述。
只有c==0时,意味着这个重入锁的线程已经完全释放了当前的锁,所以就返回true,其他情况返回false.

我们回到release, 当完全释放锁成功后,就会进入unparkSuccessor.

unparkSuccessor

顾名思义,应该是唤醒当前结点的后继结点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void unparkSuccessor(Node node) {

int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);

Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}

首先,我们需要明确的是,正在进行操作的当前线程,一定是头结点。

  1. 先获取node的等待状态,如果是小于0的,首先会把waitStatus变成0。
  2. 获取当前结点的后继结点,如果结点的waitStatus>0 ,则通过for循环往下继续进行获取直到找到一个有效的(waitStatus <=0)。
  3. 如果找到了第一个有效的后继结点,就通过LockSupport的unpark唤醒它。

好了,看到这里其实对公平锁的获取与释放就很清楚了。接下来我们看看非公平锁。

NonfairSync 非公平锁

一样的学习思路,我们先看看锁的获取过程。

lock

1
2
3
4
5
6
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}

从上面这个代码可以看到,lock会先通过compareAndSet(0, 1)来判断锁是不是空闲状态,如果是的,那么当前线程可以直接获取锁,否则调用acquire(1)获取锁。

  1. compareAndSetState()是CAS函数,用来比较并设置当前锁的状态。若锁的状态值为0,就设置锁的状态值为1。
  2. setExclusiveOwnerThread(Thread.currentThread())的作用是,设置当前线程为这个锁的拥有者。

acquire

直接看代码

1
2
3
4
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}

这里的这段代码还是来自AQS,可以看到是和公平锁是公用的一份。但是不同点在于tryAcquire是不同的。我们看看NonfairSync中尝试获取锁的实现。

1
2
3
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//AQS中
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}

在tryAcquire中是调用了nonfairTryAcquire,执行非公平策略的尝试获取。 同样的,先获取同步状态,如果为0,说明还没有线程占有当前的锁,因此就通过CAS来设置重入的参数,并将当前线程设置为锁的
拥有者。之后就直接从这个方法中返回。

另外,如果锁的拥有者就是当前的线程,就可以实现重入的效果,即可以继续设置新的同步状态然后返回。

以上就是非公平锁的获取锁的过程,在研究了公平锁的实现后,再到这里理解这个就相对比较轻松。

至于非公平锁的释放,就是和公平锁一模一样了。

总结

其实可重入锁ReentrantLock只是对锁套了一层皮,对获取锁的过程中多了一个对当前线程和锁的拥有者的比较判断,然后以此为基础实现了公平锁和非公平锁。
核心就是我们在上文中主要研究的AQS,AbstractQueuedSynchronizer。
而要弄清楚AbstractQueuedSynchronizer,主要需要理解其中的CLH队列,即一个FIFO双向队列,实现是将每一个需要等待的线程构造成为一个结点Node,然后插入到队列中。
队列的头结点就是获取到同步状态的线程。

每次操作就是对队列的结点进行相应的操作。具体过程就参考上述的步骤吧。

关于锁和AQS也介绍的差不多了,先写到这里吧。 = =

java-juc-原子类-AtomicReference和AtomicIntegerFieldUpdater初探

AtomicReference

我们之前已经比较深入的学习了AtomicInteger和AtomicIntegerArray了,现在来看看第三种不同的原子类,AtomicReference。

AtomicReference 基本用法

顾名思义,AtomicReference就是可以对相较基本数据更复杂的对象进行原子操作,例如用户自定义的类等,我们先看看基本用法吧。

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 AtomicReferenceTest {

public static void main(String[] args){

Student s1 = new Student(1);
Student s2 = new Student(2);
AtomicReference<Student> ar = new AtomicReference<>(s1);
ar.compareAndSet(s1, s2);
Student s3 = ar.get();

System.out.println("s3 is " + s3); // 2
System.out.println("s3.equals(s1) = " + s3.equals(s1)); // false
}
}

static class Student {
volatile long id;
public Student(long id) {
this.id = id;
}
public String toString() {
return "student id = "+id;
}
}

用法也很简单,在jdk 1.7及以前,对一个对象的原子性存取的保证,主要是采用compareAndSet方法进行对象的原子操作。

我们先看看构造方法- -

1
2
3
4
5
6
7
8
9
10
11
12
private volatile V value;

public AtomicReference(V initialValue) {
value = initialValue;
}

/**
* Creates a new AtomicReference with null initial value.
*/

public AtomicReference() {

}

和前面讲到的所有的AtomicInteger等类似,AtomicReference也有两个构造方法。一个给定一个初始值传入,一个是初始化一个为null的值。其中value是使用了泛型,因此在构造时需要给AtomicReference指定传入的type。

研究了几次Atomic类后,我们会发现其实操作的套路几乎是一样的。

1
2
3
4
5
6
7
8
9
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;

static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicReference.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}

同样的使用一个Unsafe类来提供CAS操作,使用valueOffset保存value在内存中的地址偏移,便于CAS操作时读取内存中的值。

在无参数的构造方法构造后,可以使用set(newValue)方法继续赋值,也可以用compareAndSet进行原子赋值。

既然说到了compareAndSet,我们还是提一下吧,这个也是用了CAS函数进行原子赋值,方法如下:

1
2
3
4
5
6
7
8
9
10
11
/**
* Atomically sets the value to the given updated value
* if the current value {@code ==} the expected value.
* @param expect the expected value
* @param update the new value
* @return {@code true} if successful. False return indicates that
* the actual value was not equal to the expected value.
*/

public final boolean compareAndSet(V expect, V update) {
return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
}

其又调用了native方法:

1
public final native boolean compareAndSwapObject(Object object, long valueOffset, Object expect, Object update);

这里就和AtomicInteger等原子类一样了。同样的方法还有如下:

1
2
3
public final V getAndSet(V newValue) {
return (V)unsafe.getAndSetObject(this, valueOffset, newValue);
}

本质上也是调用了Unsafe类中的compareAndSwapObject方法。

另外,在jdk 1.8中也添加了对lambda表达式的支持,新增了getAndUpdate, updateAndGet等方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public final V getAndUpdate(UnaryOperator<V> updateFunction) {
V prev, next;
do {
prev = get();
next = updateFunction.apply(prev);
} while (!compareAndSet(prev, next));
return prev;
}

public final V updateAndGet(UnaryOperator<V> updateFunction) {
V prev, next;
do {
prev = get();
next = updateFunction.apply(prev);
} while (!compareAndSet(prev, next));
return next;
}

大家可以看到其实这里也是调用了compareAndSet方法,只是支持了UnaryOperator,这也是java 8的新特性之一。

好了,关于AtomicReference就介绍到这儿了。

AtomicIntegerFieldUpdater

在juc的原子类包里,还有第四种原子类,就是field updater。顾名思义,通过这个类我们可以对某个对象的integer的成员变量进行原子更新操作。

我们看看基本用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
AtomicIntegerFieldUpdater<Student> mAtoLong = AtomicIntegerFieldUpdater.newUpdater(Student.class, "id");
Student person = new Student(123456);
mAtoLong.compareAndSet(person, 123456, 1000);
System.out.println("id=" + person.toString()); //1000
mAtoLong.getAndAdd(person,10);


static class Student {
volatile int id;

public Student(int id) {
this.id = id;
}

public String toString() {
return "student id = " + id;
}
}

我们可以看到,AtomicIntegerFieldUpdater可以保证更新一个对象中int属性值的原子性。这里的使用方法可能和我们之前分析的三种原子类还不太一样,因为AtomicIntegerFieldUpdater
是一个抽象类,让我们看看源码。

1
2
3
4
5
6
7
8
9
10
11
public static <U> AtomicIntegerFieldUpdater<U> newUpdater(Class<U> tclass, String fieldName) {
return new AtomicIntegerFieldUpdaterImpl<U>
(tclass, fieldName, Reflection.getCallerClass());
}

/**
* Protected do-nothing constructor for use by subclasses.
*/

protected AtomicIntegerFieldUpdater() {

}

先不管其他的修改值的方法,事实上我们在代码里使用时也是走到了newUpdater方法,传进一个类名参数和要修改的属性名(这里是不是有点像反射的感觉?别着急,我们慢慢往下看看),返回了一个AtomicIntegerFieldUpdaterImpl对象。

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
 AtomicIntegerFieldUpdaterImpl(final Class<T> tclass, final String fieldName, final Class<?> caller) {
final Field field;
final int modifiers;
try {
field = AccessController.doPrivileged(
new PrivilegedExceptionAction<Field>() {
public Field run() throws NoSuchFieldException {
return tclass.getDeclaredField(fieldName);
}
});
modifiers = field.getModifiers();
sun.reflect.misc.ReflectUtil.ensureMemberAccess(
caller, tclass, null, modifiers);
ClassLoader cl = tclass.getClassLoader();
ClassLoader ccl = caller.getClassLoader();
if ((ccl != null) && (ccl != cl) && ((cl == null) || !isAncestor(cl, ccl))) {
sun.reflect.misc.ReflectUtil.checkPackageAccess(tclass);
}
} catch (PrivilegedActionException pae) {
throw new RuntimeException(pae.getException());
} catch (Exception ex) {
throw new RuntimeException(ex);
}

Class<?> fieldt = field.getType();
if (fieldt != int.class)
throw new IllegalArgumentException("Must be integer type");

if (!Modifier.isVolatile(modifiers))
throw new IllegalArgumentException("Must be volatile type");

this.cclass = (Modifier.isProtected(modifiers) && caller != tclass) ? caller : null;
this.tclass = tclass;
offset = unsafe.objectFieldOffset(field);
}

可以看到,我们之前的猜测没有错,传进的类名和field名用来进行反射通过getDeclaredField获取到了这个类的特定的成员域。

在通过反射获取到int属性成员后,检查了调用的原类的安全权限,接着检查了这个field是否是一个int,是否是一个volatile变量,当这一切都没有问题后才通过unsafe类来进行内存地址的获取,
一起后续的一系列原子操作。

see? 又看到unsafe啦,这个真是我们的老朋友了,这里的原子操作也是unsafe提供的CAS函数进行的。

而比较有意思的一点是,在AtomicIntegerFieldUpdater中的compareAndSet也是abstract的,具体的实现在AtomicIntegerFieldUpdaterImpl中。

1
2
3
4
public boolean compareAndSet(T obj, int expect, int update) {
if (obj == null || obj.getClass() != tclass || cclass != null) fullCheck(obj);
return unsafe.compareAndSwapInt(obj, offset, expect, update);
}

我想关于compareAndSet我们也算比较熟悉了,这里就不再详细介绍了。

事实上,在AtomicIntegerFieldUpdaterImpl中的构造方法里取出了对应对象的int field并检查一切没有问题后,接下来的处理就和AtomicInteger的思路以及实现方法一样了。对这个int进行诸如getAndAdd、addAndGet、getAndUpdate和updateAndGet等就是一样的过程,这里也不再细述了。

只不过AtomicIntegerFieldUpdater的特殊性在于有些基本方法是abstract,需要在AtomicIntegerFieldUpdaterImpl中实现,也就是说一些原子操作实际上是在AtomicIntegerFieldUpdaterImpl中完成。

1
2
3
4
5
6
7
8
9
public abstract boolean compareAndSet(T obj, int expect, int update);

public abstract boolean weakCompareAndSet(T obj, int expect, int update);

public abstract void set(T obj, int newValue);

public abstract void lazySet(T obj, int newValue);

public abstract int get(T obj);

这些方法都是在AtomicIntegerFieldUpdaterImpl中进行了实现,本质上还是Unsafe类走原子操作。

另外,像getAndAdd , addAndGet等方法在AtomicIntegerFieldUpdater中和AtomicIntegerFieldUpdaterImpl都有实现,但是本质上都是CAS函数,也和AtomicInteger一样。

像getAndUpdate和updateAndGet是java 8新增的方法,用法也和也和AtomicInteger一样。

总之,AtomicIntegerFieldUpdater自身定义一个abstract类,通过子类实现反射获得对应的属性后,接下来的原子操作就和AtomicInteger等一样啦。这里就不再继续到unsafe中啦。

好了,关于java.util.concurrent中的原子类的包的分析就大概这三篇文章讲一下啦。往后我们就继续学习其他并发知识吧。

java-juc-原子类-AtomicIntegerArray初探

上一篇文章我们已经大致了解了AtomicInteger的实现机制以及在jdk 1.8上的新特点,现在我们趁热打铁来看看AtomicIntegerArray类。

同样的,我们先看看AtomicIntegerArray的简单用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
int[] intArray = new int[]{1, 2, 3, 4, 5};
AtomicIntegerArray aia = new AtomicIntegerArray(intArray);
aia.set(0, 5);
aia.getAndDecrement(0); // 5
aia.decrementAndGet(1); // 1
aia.getAndIncrement(2); // 3
aia.incrementAndGet(3); // 5
aia.addAndGet(0, 10); // 14
aia.getAndAdd(1, 10); // 1
aia.compareAndSet(2, 3, 10); // false
aia.get(2); // 4
aia.compareAndSet(2, 4, 10); // true
aia.get(2); // 10

用法同样非常简单,操作的数据类型类似int[], 只不过封装好套了一层atomin操作。经过上一篇AtomicInteger的介绍,上面的各个基本的方法使用的结果相信应该不会有什么问题,
并且实际上都是可以根据方法名“顾名思义”的。

同样的,我们看看源码, 基于jdk 1.8.0_05 。

1
2
3
4
5
6
7
8
9
private final int[] array;

public AtomicIntegerArray(int length) {
array = new int[length];
}

public AtomicIntegerArray(int[] array) {
this.array = array.clone();
}

这里的构造方法也很简单,一种是传入数组的长度,默认创建一个length长度的各个元素为0的数组,一种是直接在外面创建好一个数组,然后传给构造方法。

这里保存数组的值是全局维护了一个int[] array. 有没有发现这里和AtomicInteger的不同?AtomicInteger保存值是维护了一个volatile来保证可见性,这里为什么没有采取同样的方法?

仔细看看一下,array使用的是final修饰,变成了常量数组,引用不可变,这个array数组就保存到了方法区,同样的可以保证多线程访问时的可见性,避免使用volatile也减少了开销。

类似AtomicInteger,AtomicIntegerArray也是采用了Unsafe特殊类来提供CAS函数进行原子性的操作,这块暂且按下不表,我们看看AtomicIntegerArray的内部实现中相比AtomicInteger多了一些有意思的存在,
一个是base, 一个是shift。

1
2
3
4
5
6
7
8
9
10
11
private static final Unsafe unsafe = Unsafe.getUnsafe();

private static final int base = unsafe.arrayBaseOffset(int[].class);
private static final int shift;

static {
int scale = unsafe.arrayIndexScale(int[].class);
if ((scale & (scale - 1)) != 0)
throw new Error("data type scale not a power of two");
shift = 31 - Integer.numberOfLeadingZeros(scale);
}
1
2
3
4
Unsafe.class
public native int arrayBaseOffset(Class<?> var1);

public native int arrayIndexScale(Class<?> var1);

arrayBaseOffset方法一般是配合arrayIndexScale方法使用,两个都是属于Unsafe类中的native方法。这两个native方法需要传入的参数都是一个array类型的class。arrayBaseOffset是能获取数组首个元素的首地址偏移,arrayIndexScale可以用来获取数组元素的增量地址的方法。上面那段代码和内容可能有点不太好理解,我们先形成这个印象,接下来可以先看一个小例子。

1
2
3
4
5
int base = unsafe.arrayBaseOffset(int[].class);
int scale = unsafe.arrayIndexScale(int[].class);
int elementIdx = 3;
int[] array = { 0, 1, 23, 4 };
long offsetForIdx = base + (elementIdx * scale);

我们用来计算offsetForIdx的过程,就是先计算基址加上一个偏移的增量。其中偏移的增量又是根据偏移的索引和元素增量地址的乘积获得。

如果还不清楚,我们更深入一点来研究这两个native方法: -)。

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 int arrayBaseOffset(Class clazz) {
Class<?> component = clazz.getComponentType();
if (component == null) {
throw new IllegalArgumentException("Valid for array classes only: " + clazz);
}
// TODO: make the following not specific to the object model.
int offset = 12;
if (component == long.class || component == double.class) {
offset += 4; // 4 bytes of padding.
}
return offset;
}

public int arrayIndexScale(Class clazz) {
Class<?> component = clazz.getComponentType();
if (component == null) {
throw new IllegalArgumentException("Valid for array classes only: " + clazz);
}
// TODO: make the following not specific to the object model.
if (!component.isPrimitive()) {
return 4;
} else if (component == long.class || component == double.class) {
return 8;
} else if (component == int.class || component == float.class) {
return 4;
} else if (component == char.class || component == short.class) {
return 2;
} else {
// component == byte.class || component == boolean.class.
return 1;
}
}

上面一段是我抠的一段源码,可以看到,arrayBaseOffset实质上做的是获取一个数组对象在内存中从数组的地址到首个元素的地址的偏移量。为什么offset会先加12呢,这里涉及到的是java内存中对象存储的知识。
我们要知道,每个类对象在内存中存储时除了数据内容,其实还要包含一个头部信息的,主要是8字节大小的元信息,存储一些标识符号等信息,如果这个类是数组类型的话,还需要4字节来存储数组的大小,所以
这里是12字节。接下来又涉及到了字节对齐,我们知道在jvm中,是要以8字节为单位进行对齐的,这里的头部12字节肯定是无法对齐了,但是如果是long,double等8字节的类型,就是在开始存时就进行对齐操作,
这样就能保证接下来的每一个元素都是8的倍数,而如果是其他的对象比如int 4字节,就在数组末尾进行对齐,这样就能缺多少补多少。

arrayIndexScale实际上是能获取数组中每个元素在内存中的大小,是不是有点像cpp里sizeof的感觉了?

分析到这里我们应该对AtomicIntegerArray的base和scale有了一个清晰的认识了,但是我们会发现,在最初我们贴出的那段static代码块中,将scale转为了一个final int值shift。

首先通过scale & (scale-1) !=0 进行一下检查,事实上n & (n-1) ==0 就说明n要么是0,要么是2的幂次方,这里的这个检查处理结合上面贴出的arrayIndexScale中对基本类型的处理就非常好理解了。

接着用到了Integer.numberOfLeadingZeros(scale)方法,这个方法能够获取scale中高位的0的个数,因为一个int是以32位二进制存储的,当高位没有时,都会补0。所以shift就是第一个不为0的index,这里非常重要,
也就是说,shift可以理解为scale的2幂次方的这个幂。例如scale为16,那么shift就是4, scale为8,shift就是3。好了,关于AtomicIntegerArray的初始化构建到这儿就有一个了解了。但是知道这个有什么用呢?我们接下来会慢慢用到。

我们再来看两个重要的基础方法。

1
2
3
4
5
6
7
8
9
10
private long checkedByteOffset(int i) {
if (i < 0 || i >= array.length)
throw new IndexOutOfBoundsException("index " + i);

return byteOffset(i);
}

private static long byteOffset(int i) {
return ((long) i << shift) + base;
}

checkedByteOffset接到的参数就是array中的index,检查一下没有数组下标越界后实际上做的事到了byteOffset。i << shift其实就是i * scale。这也符合我们开始时解释arrayBaseOffset时举的例子。

本质上还是计算了CAS中需要的那个内存中的旧值。所以这里的转化就非常巧妙,可以再多回味一下。

好了,基础工作我们基本分析完了,现在像学习AtomicInteger一样来学习一下存取方法吧。

1
2
3
4
5
6
7
public final int getAndAdd(int i, int delta) {
return unsafe.getAndAddInt(array, checkedByteOffset(i), delta);
}

public final int addAndGet(int i, int delta) {
return getAndAdd(i, delta) + delta;
}

这两个核心方法的思想和实质几乎和AtomicInteger一模一样,除了修改值时需要传入一个数组的index,最后都是进了Unsafe类中去getAndAddInt,然后走compareAndSwapInt方法,到这里的过程就和AtomicInteger一模一样了。

同样的,在jdk 1.8中加入了单值运算操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public final int getAndUpdate(int i, IntUnaryOperator updateFunction) {
long offset = checkedByteOffset(i);
int prev, next;
do {
prev = getRaw(offset);
next = updateFunction.applyAsInt(prev);
} while (!compareAndSetRaw(offset, prev, next));
return prev;
}

public final int updateAndGet(int i, IntUnaryOperator updateFunction) {
long offset = checkedByteOffset(i);
int prev, next;
do {
prev = getRaw(offset);
next = updateFunction.applyAsInt(prev);
} while (!compareAndSetRaw(offset, prev, next));
return next;
}

用法和AtomicInteger差不多,多传入一个index.

1
2
3
int[] intArray = new int[]{1, 2, 3, 4, 5};
AtomicIntegerArray aia = new AtomicIntegerArray(intArray);
aia.updateAndGet(1, n -> (n % 2 == 0 ? n - 2 : n - 1));

那么aia变成了[1, 0,3,4,5]。

除了直接更新值操作,也和AtomicInteger一样新增提供了getAndAccumulate和accumulateAndGet方法,都可以传入一个IntBinaryOperator进行java 8特性的编写。这也是非常方便的了。

ok,关于AtomicIntegerArray的探究就到这儿吧,AtomicLongArray等原子数组类型和这个就差不多了,只是数据类型对象更换一下,具体的机制是想通的,大家有兴趣可以自己再看看相关的源码。

java-juc-原子类-AtomicInteger初探

研究原子类型,我们先从最常用的AtomicInteger开始看起。

我们可以先看看AtomicInteger的使用方法:

1
2
3
4
AtomicInteger i = new AtomicInteger(0);
i.addAndGet(10); // print 10
i.getAndAdd(10); // print 10
i.get(); // 20

用法还是很简单的,我们也可以看到AtomicInteger等一系列原子数据类是为了解决多线程访问Integer变量导致可能不出现的结果所设计实现的一个基于原子操作的Integer类。

那么老规矩,研究源码, 基于jdk 1.8.0_05 。

看看构造方法。

1
2
3
4
5
6
7
8
private volatile int value;

public AtomicInteger(int initialValue) {
value = initialValue;
}

public AtomicInteger() {
}

可以看到实际上AtomicInteger用一个volatile int保存了当前要进行一系列操作的int对象。当然也有一个无参数的构造方法,可以通过set()方法给value赋值,实现的功能是一样的。

对于这个value的操作就涉及到了一个Unsafe类。

1
private static final Unsafe unsafe = Unsafe.getUnsafe();

Unsafe类主要是用来实现一个不安全的操作的比较特殊的类,Unsafe能够提供很多保证安全的方法,在这里主要是提供了CAS操作。

CAS对int类型的数据进行操作时,主要使用了这个方法:

1
public final native boolean compareAndSwapInt(Object object, long valueOffset, int expect, int update);

而这里的valueOffset是一个全局的final long 数据,表示一个内存位置。

1
2
3
4
5
6
7
8
private static final long valueOffset;

static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}

这里的value就是之前提到的volatile int 的值value,这里主要是为了保证这个value的可见性,某个线程修改了value后,其他线程也能读取到正确的值。

另外,通过unsafe提供的CAS方法,可以在AtomicInteger中修改值。

1
2
3
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

所以我们可以看到通过CAS来保证原子性,通过volatile保证了可见性。

阅读源码发现,AtomicInteger还有几个很有意思的方法。也是我们一开始就见到的常用用法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public final int getAndAdd(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta);
}

public final int addAndGet(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
}

/*这段代码位于Unsafe*/
public final int getAndAddInt(Object object, long valueOffset, int delta) {
int ans;
do {
ans = this.getIntVolatile(object, valueOffset);
} while(!this.compareAndSwapInt(object, valueOffset, ans, ans + delta));

return ans;
}

用法我在一开始也通过例子解释了,getAndAdd是先通过unsafe.getAndAddInt获取当前的安全值然后再原子操作加上传进来的参数delta,而addAndGet是同样的unsafe.getAndAddInt获取当前的
安全值,并且原子操作加上delta。这里使用的同样的原子操作,但是有一个trick是同样的原子操作进行先取值再加值,addAndGet是返回了取出的值直接加上delta。这里真的很巧妙啊。为什么我要说这里很有意思呢? 我们可以看看在jdk 1.7的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public final int getAndAdd(int delta) {
for (;;) {
int current = get();
int next = current + delta;
if (compareAndSet(current, next))
return current;
}
}

public final int addAndGet(int delta) {
for (;;) {
int current = get();
int next = current + delta;
if (compareAndSet(current, next))
return next;
}
}

public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

可以看到,1.8到1.7,将AtomicInteger中暴露的Unsafe的方法使用的更少了。代码看着更简洁了。
并且for循环也换成了do{}while(), 虽然性能没什么改变,但是更直观了。

好,回到原子性上来。既然提到了CAS,我们也看到了Unsafe中的CAS的方法使用,主要在native层使用原子指令实现对值的修改,需四个参数,其中valueOffSet是内存地址,expect是旧的期望正确的值,update是新的值。

只有旧值匹配时,才会将旧值更新为新值。关于CAS的具体学习,我们以后再继续深入,今天的主角是AtomicInteger。

除了getAndAdd,还有getAndIncrement等方法,其实本质都是一样的,只不过delta确认为1而已。

另外,相比较jdk 1.7, 在jdk1.8中新拓展了两个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public final int getAndUpdate(IntUnaryOperator updateFunction) {
int prev, next;
do {
prev = get();
next = updateFunction.applyAsInt(prev);
} while (!compareAndSet(prev, next));
return prev;
}

public final int updateAndGet(IntUnaryOperator updateFunction) {
int prev, next;
do {
prev = get();
next = updateFunction.applyAsInt(prev);
} while (!compareAndSet(prev, next));
return next;
}

简单用法如下:

1
2
AtomicInteger ai = new AtomicInteger(10);
ai.updateAndGet(n -> (n %2 == 0 ? n-2:n -1));

IntUnaryOperator是一个jdk 1.8的新增类,主要完成对单一值运算操作得到一个新结果的过程。例如上面的例子中就是将偶数减2,奇数减1的一个“side-effect-free” 函数,也就是对单一值进行运算。

除了以上对各种值的原子性修改以外,AtomicInteger还提供了一些方法将int转为其他类型。

1
2
3
4
5
6
7
8
9
10
11
public long longValue() {
return (long)get();
}

public float floatValue() {
return (float)get();
}

public double doubleValue() {
return (double)get();
}

好了,关于AtomicInteger的学习研究先到这儿吧,主要需要弄懂Unsafe在其中起的作用以及jdk 1.8的一些新变化。其实其他几个原子基本数据类的方法和实现与AtomicInteger差不多,就不一一介绍了,搞懂这一个,其他就触类旁通了。

java-juc-原子包解析概要

谈java永远绕不开并发,谈并发永远绕不开java.util.concurrent包。

接下来一段较长时间,我们将一起学习一下java并发包的一些重要内容。

我们先从atomic包开始,atomic下主要包括一系列原子类。

  1. 基本类型: AtomicInteger, AtomicLong, AtomicBoolean ;
  2. 数组类型: AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray;
  3. 引用类型: AtomicReference, AtomicStampedRerence, AtomicMarkableReference;
  4. 对象的属性修改类型: AtomicIntegerFieldUpdater, AtomicLongFieldUpdater, AtomicReferenceFieldUpdater。

Android GC 从dalvik到ART的改进分析

自从android4.4开始加入ART虚拟机,从5.0开始完全用ART代替Dalvik虚拟机,ART的优点越来越被开发者和用户接受。很多用户会觉得android从5.0开始很流畅,很少卡顿,其实从GC角度看,是因为系统GC的过程得到了优化,GC过程中的几次暂停时间缩短了。接下来我主要从dalvik和ART二者的GC的改进的地方来分析一下,为什么ART的使用会使android系统性能和流畅有着如此之大的提升。
我将分别从Dalvik和ART两方面来介绍相关的详细机制和实现,然后再对比二者的改进,结合实验数据来探讨android系统在ART的GC的改进优势。

1. Dalvik的垃圾收集

在Dalvik时代,Android更是在这方面深受困扰。由于内存较小,每次APP应用程序需要分配内存时,如果堆空间不能提供一个足够大的空间时就会启动Dalvik的垃圾收集器。GC收集器会遍历整个堆地址空间,然后查看每个app分配的对象,并进行相应的标记,然后进行回收过程。
Dalvik除了要给新的应用程序分配内存空间外,还需要对已经分配的内存空间的对象进行内存管理,主要就是我们之前提到的自动垃圾收集。这个垃圾收集主要是mark sweep算法实现的。简单点说,dalvik虚拟机的mark-sweep算法就分为两个阶段,即mark阶段和sweep阶段。Dalvik的GC过程,可以大致归纳为如下过程:
整个标记开始之前,首先遍历一次堆地址空间。
Mark阶段: 从对象的根集对象开始标记引用对象。
Sweep阶段: 回收没有被标记的对象占用的内存。

Mark阶段就是从根集对象开始标记被引用的对象,在完成了mark标记后,就进入sweep阶段,即回收那些没有被标记的对象占用的内存。接下来我们主要分析一下这个算法,才能更好的比较分析新的ART和dalvik的区别。其实sweep阶段很简单了,现在的重点是讲解一下mark阶段。

1.1 dalvik的mark阶段

Mark阶段最主要的工作就是标记已经被引用的对象。用到的最主要的一个数据结构叫heap bitmap。事实上,总共使用了两个bitmap,一个是live bitmap,一个是mark bitmap。这个bitmap里的每一位对应一个对象,某个对象被引用了,就标1,没引用就标0。Livebitmap主要用来标记上一次GC时被引用的对象,也就是那些没有被回收的对象,而markbitmap主要用来标记当前GC有被引用的对象。因此在判断需要回收哪些对象时,就是那些在Live bitmap中标为1,而在mark bitmap中标为0的对象。
仔细思考这个流程可以发现,mark bitmap实际上就是live bitmap的一个子集。
在mark阶段,要求除了GC线程外,其他的线程都需要停止,否则就可能不能正确的标记每个对象,因为可能刚标记完又被修改引用等等情况的发生。这种现象叫stop the world,会导致该应用程序中止执行,造成停顿,为了减少这种停顿的发生,dalvik采用了并行的垃圾回收算法,即Concurrent GC。
在整个mark开始时,GC会先不得不中止一次程序的运行,从而对堆地址空间进行一次遍历,这次遍历可以获得每一个应用程序分配的对象,就能确认每个对象在内存堆中的大小、起始地址等等信息。 这个停顿在dalvik里是不得不做的事情,每次GC都会必须触发一次堆地址空间的遍历引起的停顿。
接下来就是真正的标记阶段了。为了减少stop the world带来的负面影响,dalvik的GC采用了Concurrent方法,实现时,GC将mark阶段又分为了两个子阶段,一个是标记根集对象,一个是标记根集对象引用的对象。所谓根集对象,就是指在GC线程开始的时候,那些被全局变量、栈变量和寄存器等引用的对象。通过这些根集变量,可以顺着它们找到其余的被引用变量。比如说,假如发现了一个栈变量引用一个对象,而这个对象又引用了另外一个对象,那这个被引用的对象也会被标记为正在使用。这个标记“被根集对象引用的对象”的过程就是第二个子阶段。在ConcurrentGC中,mark的第一个阶段,在标记根集对象的阶段是不允许GC线程之外的线程运行的,但是mark的第二个阶段允许其他线程运行。这样就可能带来一个问题是,在第二个阶段执行的过程中,如果某个运行的线程修改了一个对象了内容,由于很有可能引用了新的对象,所以这个对象也必须要记录起来。否则会发生被引用对象还在使用却被回收。这种情况出现在只进行部分GC的情况,这时候Card Table的作用就是用来记录非GC堆对象对GC的堆对象的引用。Dalvik的堆空间,分为zygote heap 和 active heap。 前者主要存放一些在zygote时就分配的对象,后者主要用于之后新分配的空间,Dalvik虚拟机进行部分垃圾收集时,实际上就是只收集在Active heap上分配的对象。Card Table就是用来记录在Zygote heap上分配的对象在GC执行过程中对在Active heap上分配的对象的引用。
Card table由一系列card组成,一个card实际上就是一个字节,它的值分为clean和dirty两种。如果一个Card的值是CLEAN,就表示与它对应的对象在Mark第二个子阶段没有被程序修改过。如果一个Card的值是DIRTY,就意味着被程序修改过,对于这些被修改过的对象。需要在Mark第二子阶段结束之后,再次禁止GC线程之外的其它线程执行,以便GC线程再次根据Card Table记录的信息对被修改过的对象引用的其它对象进行重新标记。由于Mark第二子阶段执行的时间不会太长,因此在这个阶段被修改的对象不会很多,这样就可以保证第二次子阶段结束后,再次执行标记对象的过程是很快的,因而此时对程序造成的停顿非常小。
在mark阶段,主要是标记的第二个子阶段,dalvik是采用递归的方式来遍历标记对象。但是这个递归并不是像一般的递归一样借助一个递归函数来实现,而是使用一个叫做mark stack的栈空间实现。大致过程是:一个被引用的对象在标记的过程中,先被标记,然后放在栈中,等该对象的父对象全部被标记完成后,依次弹出栈中的每一个对象,并标记其引用,然后把其引用再丢到栈中。
这里可能会有一个疑问,一般在递归时都采用的函数递归的方法,但是为什么这里要采用mark stack呢?可以思考,采用mark stack栈而不是函数递归的好处是:首先可以采用并行的方式来做,将栈进行分段,多个线程共同将栈中的数据递归标记。其次,可以避免函数堆栈占用较深。
至此,差不多介绍了dalvik的GC的mark阶段的过程。我们可以发现,在mark阶段,一共有3次停顿,一次是在标记开始前遍历堆地址空间的停顿,一次是在标记的第一个阶段标记所有根集对象时的停顿,还有一次是在标记的第二个子阶段完成后处理card table时的停顿。这3次停顿的时间直接影响了android上应用程序的表现,尤其是卡顿现象,因此ART在这块有重点改进,下文会介绍ART上的过程。

1.2 dalvik的sweep阶段

其实sweep阶段就很简单了,在mark阶段已经提到过,GC时回收的是在live bitmap里标为1而在mark bitmap里标为0的对象。而这个mark bitmap实际上就是live bitmap的子集,因此在sweep阶段只需要处理二者的差集即可,在回收掉相应的对象后,只需要再把live bitmap和mark bitmap的指针调换一下,即这次的mark bitmap作为下一次GC时的live bitmap,然后清空live bitmap。
其过程和ART的没什么太大变化,而由于在android 5.0源码中已经去掉了dalvik,这环节的具体解释就在ART部分分析,但是实际上在sweep阶段dalvik和ART二者没有太大区别,因为主要只是处理相应的bitmap的对应的对象的内存,ART也没有什么优化的地方。

2 ART的垃圾收集

ART同样采用了自动GC的策略,并且同样不可避免的使用到了经典的mark-sweep算法。

2.1 mark-sweep收集器

在android源码中,ART的部分的GC在使用mark-sweep算法进行自动垃圾收集时,根据轻重程度不同,分为三类,sticky,partial,full。可以看到,ART里的GC的改进,首先就是收集器的多样化。
而根据GC时是否暂停所有的线程分类并行和非并行两类。所以在ART中一共定义了6个mark-sweep收集器。参看art/runtime/gc/heap.cc可见。根据不同情况,ART会选择不同的GC collector进行GC工作。其实最复杂的就是Concurrent Mark Sweep 收集器。如果理解了最复杂的Concurrent Mark Sweep算法,其他5种GC收集器的工作原理就也理解了。同样的,垃圾回收工作从整体上可以划分两个大的阶段:Mark 和 Sweep。

1) Mark阶段

最重要的提升就是这个阶段只暂停线程一次。将dalvik的三次缩短到一次,得到了较大的优化。和dalvik一样,标记阶段完成的工作也是完成从根集对象出发,进行递归遍历标记被引用的对象的整个过程。用到的主要的数据结构也是同样的live bitmap和mark bitmap, 以及card table和在递归遍历标记时用到的mark stack。
一个被引用的对象在标记的过程中先被标记,然后存入mark stack中,等待该对象的父对象全部被标记完成,再依次弹出栈中每一个对象然后,标记这个对象的引用,再把引用存入mark stack,重复这个过程直至整个栈为空。这个过程对mark stack的操作使用以及递归的方法和dalvik的递归过程是一样的。但是在dalvik小节里提到了,在标记时mark阶段划分成了两个阶段,第一小阶段是禁止其他线程执行的,在mark两个阶段完成后处理card table时也是禁止其他线程执行的。但是在ART里做出了改变,即先Concurrent标记两遍,也就是说两个子阶段都可以允许其他线程运行了。然后再Non-Concurrent标记一遍。这样就大大缩短了dalvik里的第二次停顿的带来的卡顿时间。这个改进非常重要。
在对mark stack使用时,在初始阶段,为后面的mark准备好markstack
但是值得一提的是,在标记开始阶段,有别于dalvik的要暂停所有线程进行堆地址空间的遍历,ART去掉了这个过程,替代的是增加了一个叫作allocation stack结构,所有新分配的对象会记录到allocation stack,然后在Mark的时候,再在Live Bitmap中打上live的标记。Allocation stack和live stack其实是一个工作栈和备份栈。当在GC的时候,需要处理allocation stack,那么会把两个stack互换。新分配的对象会压到备份栈中,这个时候备份栈就当作新的工作栈。这样一来,dalvik在GC时产生的第一次停顿就完全消除了,从而产生了巨大的性能提升。
关于card table,和dalvik依旧类似,每个card用一个字节来描述。ART里多了一个结构ModUnionTable,是和card table配合使用的。
前面在ConCurrent的情况下,经过了两轮的递归遍历,基本上已经标记扫描的差不多了。但由于应用程序主线程是在一直运行的,不可避免地会修改之前已经mark过的bitmap。因此,需要第三遍扫描,这次就需要在stop the world的情况下进行遍历,主要过程也就是上文提到的对card table的操作等等。
这次遍历扫的时候,除了重新标记根集以外,还需要扫描card table中Dirty Card的部分。关于live bitmap和mark bitmap的使用,ART和dalvik在这一块没有多少区别。Live Bitmap记录了当前存在于VM进程中所有的未标记的对象和标记过的对象。Mark Bitmap经过了Mark 的过程,记录了当前VM进程中所有被引用的object。Live Bitmap和Mark Bitmap中间的差集,便是所有为被系统引用的object,即是可以回收的垃圾了。
经过了前面3次扫描以及Mark,我们的mark bitmap已经很完整了。但是值得注意的是,由于Sweep的操作是对应于live bitmap,即那些在live bitmap中标记过,却在mark bitmap中没有标记的对象。也就是说,mark bitmap中标记的对象是live bitmap中标记对象的子集。但目前为止live bitmap标记的对象还不是最全,因为前文有提到过,为了消除dalvik的第一次停顿,ART计入了allocation stack中的对象,还没有标记。Allocation stack先“搁置”起来不让后面的主线程使用,启用备份的的live stack。

1
2
3
void Heap::SwapStacks() {
allocation_stack_.swap(live_stack_);
}

2) Sweep阶段

在完成了mark阶段后,对应已经标好的live bitmap和mark bitmap,需要进入sweep来回收相应的垃圾。Sweep阶段就是把那些二者的差集所占用的内存回收掉。

3) Finish阶段

在dalvik中没有发现的是,ART中可以归纳为有一个第三个阶段,就是类似的一个finish阶段。

1
2
3
4
5
6
7
8
9
void MarkSweep::FinishPhase() {
base::TimingLogger::ScopedSplit split("FinishPhase", &timings_);
// Can't enqueue references if we hold the mutator lock.
Object* cleared_references = GetClearedReferences();
Heap* heap = GetHeap();
timings_.NewSplit("EnqueueClearedReferences");
heap->EnqueueClearedReferences(&cleared_references);
......
}

因为之前说过mark bitmap是live bitmap的一个子集,而mark bitmap中包含了所有的正在被引用的的非垃圾的对象,因此需要交换mark bitmap和live bitmap的指针,使mark bitmap作为下一次GC的live bitmap,并且重置新的mark bitmap。

1
2
3
4
5
6
7
//Clear all of the spaces' mark bitmaps.
for (const auto& space : GetHeap()->GetContinuousSpaces()) {
if (space->GetGcRetentionPolicy() != space::kGcRetentionPolicyNeverCollect) {
space->GetMarkBitmap()->Clear();
}
}
mark_stack_->Reset();

另外,需要指出的是,由于mark stack的目的是为了方便标记的递归,所以在Finish阶段,也需要把mark stack给清空,至于实现可以看以上代码行。

2.2 sticky mark sweep收集器

其实sticky mark sweep的主要步骤也是和mark sweep的过程大致一样,主要完成三次并发的mark阶段,然后进行一个stop the world的非并发进行一次对堆对象的遍历。

1
2
3
4
5
6
7
8
9
10
11
12
13
void StickyMarkSweep::BindBitmaps() {
PartialMarkSweep::BindBitmaps();
WriterMutexLock mu(Thread::Current(), *Locks::heap_bitmap_lock_);
// For sticky GC, we want to bind the bitmaps of all spaces as the allocation stack lets us
// know what was allocated since the last GC. A side-effect of binding the allocation space mark
// and live bitmap is that marking the objects will place them in the live bitmap.
for (const auto& space : GetHeap()->GetContinuousSpaces()) {
if (space->GetGcRetentionPolicy() == space::kGcRetentionPolicyAlwaysCollect) {
BindLiveToMarkBitmap(space);
}
}
GetHeap()->GetLargeObjectsSpace()->CopyLiveToMarked();
}

但是可以通过实现方法发现,有一个getGcRetenionPolicy,获取的是一个枚举。

只有符合总是收集的条件的,就把live bitmap和mark bitmap绑定起来。其余的过程和full是一样的。Sticky Mark sweep只扫描自从上次GC后被修改过的堆空间,并且也只回收自从上次GC后分配的空间。Sticky是只回收kGcRetentionPolicyAlwaysCollect的space。不回收其他两个,因此sticky的回收的力度是最小的。作为最全面的full mark sweep, 上面的三个策略都是会回收的。

2.3 partial mark sweep收集器

这是mark sweep收集器里使用的最少的GC收集策略。按照官方文档,一般是使用sticky mark sweep较多。这里有一个概念就是吞吐率,即一次GC释放的字节数和GC持续的时间(秒)的比值。由于一般是sticky mark sweep进行GC,所以当上次GC的吞吐率小于同时的partial mark sweep的吞吐率时,就会把下次GC收集器从sticky变成partial。但是在partial执行一次GC后,就仍然会恢复到stick mark sweep收集器。
阅读源码发现,partial重写了父类的成员函数。
其实分析这些可以发现,从full mark sweep到partial mark sweep到stick mark sweep,GC的力度是越来越小的,因为可以回收的越来越少。之所以说回收力度大,就是指可以回收的space多,比如上图的partial, 是不回收kGcRetentionPolicyFullCollect,但是是会回收kGcRetentionPolicyAlwaysCollect的space的。
因此partial mark sweep每次执行一次GC后,就会自动切换到sticky策略,这样才能使系统更流畅得进行GC,并减少了GC带来的消耗。。

2.4 其他区别

其实观察space目录的文件可以发现,有一个新的概念就是large object space。事实上,ART还引入了一个新的的方法就是大对象存储空间(large object space,LOS),这个空间与堆是相互独立的,但是仍然是驻留在应用程序的内存空间中。方便让ART可以更好的管理较大的对象,比如android里的bitmap。在dalvik中,在对堆空间进行分段时,占用空间较大的对象会带来一些问题。例如,在分配一个bitmap大对象时,由于占用空间较大,可能引起GC的启动次数也会增加,从而增加了开销。有了LOS,GC收集器因堆空间分段而引发调用次数将会大大降低,这样垃圾收集器就能做更加合理的内存分配,从而降低运行时开销。

3 实验对比证明

为了更形象的了解ART在GC这个环节对比Dalvik到底有了多大的改善,我同样的进行的实验。实验使用的手机,保证了总的RAM内存是相同的,安装的应用程序也都是相同的。我们分别在dalvik模式下和ART模式下观察运行支付宝APP的GC表现。

在ART下启动运行开始时的GC表现。

03-04 10:07:20.524: I/art(5703): Background partial concurrent mark sweep GC freed 65419(7MB) AllocSpace objects, 136(11MB) LOS objects, 20% free, 60MB/76MB, paused 6.695ms total 107.116ms
03-04 10:08:05.302: I/art(2176): Background partial concurrent mark sweep GC freed 63122(4MB) AllocSpace objects, 2(40KB) LOS objects, 24% free, 50MB/66MB, paused 1.289ms total 123.983ms
我从GC 的log中截取了上述一段。其中的显式(GC_EXPLICIT)和并发(GC_CONCURRENT)是GC中比较通用的清除垃圾的调用。GC_FOR_ALLOC则是在内存分配器尝试分配新的内存空间,但堆空间不够用时调用的。可以看到,在dalvik模式下刚启动支付宝的几秒内,触发了28次GC事件,总共停顿耗时4657ms。而在ART模式下,可以看到一共触发了2次GC事件,共耗时231.099ms。
我们还可以拿之前的一次测试来看看结果,这次测试是当时操作系统课堂报告时做的测试,测试观察百度地图这个应用程序的GC表现,结果可以从下面看出,分别是dalvik和ART的log。

12-24 15:34:21.046: D/dalvikvm(577): GC_FOR_ALLOC freed 3690K, 19% free 30176K/36844K, paused 517ms, total 517ms
12-24 15:34:21.062: D/dalvikvm(30509): GC_CONCURRENT freed 1968K, 49% free 11561K/22312K, paused 28ms+5ms, total 224ms
12-24 15:34:21.062: D/dalvikvm(30509): WAIT_FOR_CONCURRENT_GC blocked 179ms
12-24 15:34:21.070: D/dalvikvm(30509): WAIT_FOR_CONCURRENT_GC blocked 183ms
12-24 15:34:21.078: D/dalvikvm(30509): WAIT_FOR_CONCURRENT_GC blocked 188ms
12-24 15:34:21.093: D/dalvikvm(30509): WAIT_FOR_CONCURRENT_GC blocked 124ms
12-24 15:34:21.523: D/dalvikvm(577): GC_FOR_ALLOC freed 6K, 17% free 33769K/40448K, paused 452ms, total 452ms
12-24 15:34:21.585: D/dalvikvm(30509): GC_CONCURRENT freed 1005K, 49% free 11554K/22312K, paused 30ms+19ms, total 261ms
12-24 15:34:21.585: D/dalvikvm(30509): WAIT_FOR_CONCURRENT_GC blocked 192ms
12-24 15:34:21.585: D/dalvikvm(30509): WAIT_FOR_CONCURRENT_GC blocked 199ms
12-24 15:34:22.171: D/dalvikvm(577): GC_CONCURRENT freed 110K, 17% free 33744K/40448K, paused 99ms+52ms, total 612ms
12-24 15:34:22.171: D/dalvikvm(577): WAIT_FOR_CONCURRENT_GC blocked 336ms
12-24 15:34:22.421: D/dalvikvm(877): GC_CONCURRENT freed 1203K, 62% free 7760K/20000K, paused 147ms+20ms, total 354ms
12-24 15:34:22.437: D/dalvikvm(577): GC_FOR_ALLOC freed 68K, 16% free 35701K/42476K, paused 255ms, total 255ms
12-24 15:34:23.101: D/dalvikvm(577): GC_CONCURRENT freed 661K, 16% free 35720K/42476K, paused 23ms+83ms, total 668ms
12-24 15:34:24.632: D/dalvikvm(758): GC_CONCURRENT freed 387K, 33% free 10427K/15496K, paused 4ms+17ms, total 135ms
12-24 15:34:24.640: D/dalvikvm(30509): GC_CONCURRENT freed 691K, 48% free 11804K/22312K, paused 5ms+33ms, total 279ms
12-24 15:34:24.906: D/dalvikvm(30825): GC_CONCURRENT freed 305K, 15% free 8710K/10160K, paused 3ms+2ms, total 63ms
12-24 15:34:26.179: D/dalvikvm(30509): GC_CONCURRENT freed 875K, 47% free 11998K/22312K, paused 147ms+6ms, total 498ms
12-24 15:34:26.179: D/dalvikvm(30509): WAIT_FOR_CONCURRENT_GC blocked 362ms
12-24 15:34:26.453: D/dalvikvm(30509): GC_CONCURRENT freed 696K, 45% free 12430K/22312K, paused 21ms+27ms, total 229ms
12-24 15:34:26.992: D/dalvikvm(30509): GC_CONCURRENT freed 1626K, 47% free 12018K/22312K, paused 8ms+5ms, total 62ms
12-24 15:34:27.062: D/dalvikvm(30509): GC_CONCURRENT freed 1038K, 47% free 12016K/22312K, paused 5ms+5ms, total 53ms
12-24 15:34:27.140: D/dalvikvm(30509): GC_CONCURRENT freed 1051K, 47% free 12016K/22312K, paused 7ms+5ms, total 57ms
12-24 15:34:27.257: D/dalvikvm(30509): GC_CONCURRENT freed 1038K, 47% free 12017K/22312K, paused 5ms+6ms, total 101ms
12-24 15:34:27.429: D/dalvikvm(30509): GC_CONCURRENT freed 1050K, 47% free 12017K/22312K, paused 5ms+6ms, total 55ms
12-24 15:34:27.531: D/dalvikvm(30509): GC_CONCURRENT freed 1043K, 47% free 12016K/22312K, paused 5ms+5ms, total 57ms
12-24 15:34:27.601: D/dalvikvm(30509): GC_CONCURRENT freed 1043K, 47% free 12016K/22312K, paused 5ms+6ms, total 57ms
12-24 15:34:27.710: D/dalvikvm(30509): GC_CONCURRENT freed 1037K, 47% free 12016K/22312K, paused 5ms+7ms, total 78ms
12-24 15:34:27.796: D/dalvikvm(30509): GC_CONCURRENT freed 1042K, 47% free 12016K/22312K, paused 5ms+6ms, total 58ms
12-24 15:34:27.882: D/dalvikvm(30509): GC_CONCURRENT freed 1036K, 47% free 12016K/22312K, paused 5ms+5ms, total 51ms
12-24 15:34:27.960: D/dalvikvm(30509): GC_CONCURRENT freed 1041K, 47% free 12016K/22312K, paused 5ms+5ms, total 53ms
12-24 15:34:28.039: D/dalvikvm(30509): GC_CONCURRENT freed 1041K, 47% free 12016K/22312K, paused 5ms+5ms, total 56ms
12-24 15:34:28.414: D/dalvikvm(30509): GC_CONCURRENT freed 1153K, 47% free 11987K/22312K, paused 9ms+6ms, total 137ms
12-24 15:34:28.414: D/dalvikvm(30509): WAIT_FOR_CONCURRENT_GC blocked 106ms
12-24 15:34:28.468: D/dalvikvm(30509): GC_CONCURRENT freed 766K, 45% free 12293K/22312K, paused 7ms+6ms, total 44ms
12-24 15:34:28.468: D/dalvikvm(30509): WAIT_FOR_CONCURRENT_GC blocked 37ms
12-24 15:34:28.523: D/dalvikvm(30509): GC_FOR_ALLOC freed 1063K, 45% free 12421K/22312K, paused 46ms, total 46ms
12-24 15:34:28.921: D/dalvikvm(30509): GC_CONCURRENT freed 1044K, 44% free 12548K/22312K, paused 11ms+8ms, total 174ms
12-24 15:34:31.210: D/dalvikvm(12328): GC_FOR_ALLOC freed 345K, 15% free 10241K/12048K, paused 109ms, total 109ms
12-24 15:34:31.359: D/dalvikvm(12328): GC_FOR_ALLOC freed <1K, 16% free 10249K/12060K, paused 153ms, total 153ms
12-24 15:34:31.437: D/dalvikvm(12328): GC_FOR_ALLOC freed 11K, 16% free 10237K/12060K, paused 76ms, total 76ms
12-24 15:34:31.445: D/dalvikvm(30509): GC_CONCURRENT freed 549K, 41% free 13325K/22312K, paused 14ms+20ms, total 196ms
12-24 15:34:31.445: D/dalvikvm(30509): WAIT_FOR_CONCURRENT_GC blocked 184ms
12-24 15:34:31.445: D/dalvikvm(30509): WAIT_FOR_CONCURRENT_GC blocked 164ms
12-24 15:34:31.445: D/dalvikvm(30509): WAIT_FOR_CONCURRENT_GC blocked 88ms
12-24 15:34:31.507: D/dalvikvm(12328): GC_FOR_ALLOC freed 0K, 16% free 10245K/12072K, paused 66ms, total 66ms
12-24 15:34:31.570: D/dalvikvm(30509): GC_FOR_ALLOC freed 2347K, 45% free 12289K/22312K, paused 61ms, total 61ms
12-24 15:34:31.640: D/dalvikvm(30509): GC_FOR_ALLOC freed 689K, 45% free 12414K/22312K, paused 44ms, total 44ms
12-24 15:34:32.101: D/dalvikvm(12328): GC_FOR_ALLOC freed 23K, 16% free 10240K/12072K, paused 36ms, total 36ms
12-24 15:34:32.140: D/dalvikvm(12328): GC_FOR_ALLOC freed <1K, 16% free 10247K/12084K, paused 37ms, total 37ms
12-24 15:34:32.210: D/dalvikvm(12328): GC_FOR_ALLOC freed <1K, 16% free 10247K/12084K, paused 71ms, total 71ms
12-24 15:34:32.398: D/dalvikvm(12328): GC_FOR_ALLOC freed 0K, 16% free 10255K/12096K, paused 188ms, total 188ms
12-24 15:34:32.515: D/dalvikvm(12328): GC_FOR_ALLOC freed 31K, 16% free 10239K/12096K, paused 45ms, total 45ms
12-24 15:34:32.578: D/dalvikvm(12328): GC_FOR_ALLOC freed 32K, 16% free 10239K/12096K, paused 30ms, total 30ms
12-24 15:34:32.695: D/dalvikvm(12328): GC_FOR_ALLOC freed 79K, 16% free 10247K/12096K, paused 49ms, total 49ms
12-24 15:34:35.054: D/dalvikvm(30509): GC_CONCURRENT freed 1760K, 47% free 11834K/22312K, paused 5ms+13ms, total 106ms
12-24 15:34:38.265: D/dalvikvm(30509): GC_FOR_ALLOC freed 606K, 46% free 12058K/22312K, paused 55ms, total 56ms
12-24 15:34:38.351: D/dalvikvm(30509): GC_FOR_ALLOC freed 254K, 45% free 12308K/22312K, paused 68ms, total 70ms
12-24 15:34:38.429: D/dalvikvm(30509): GC_FOR_ALLOC freed 521K, 43% free 12803K/22312K, paused 64ms, total 64ms
12-24 15:34:38.500: D/dalvikvm(30509): GC_FOR_ALLOC freed 1952K, 46% free 12112K/22312K, paused 46ms, total 46ms
12-24 15:34:38.554: D/dalvikvm(30509): GC_FOR_ALLOC freed 276K, 45% free 12338K/22312K, paused 44ms, total 44ms
12-24 15:34:38.609: D/dalvikvm(30509): GC_FOR_ALLOC freed 514K, 43% free 12833K/22312K, paused 44ms, total 44ms
12-24 15:34:38.664: D/dalvikvm(30010): GC_CONCURRENT freed 402K, 15% free 8869K/10416K, paused 3ms+2ms, total 31ms
12-24 15:34:38.671: D/dalvikvm(30509): GC_FOR_ALLOC freed 1193K, 43% free 12796K/22312K, paused 51ms, total 52ms
12-24 15:34:38.726: D/dalvikvm(30509): GC_FOR_ALLOC freed 269K, 42% free 13023K/22312K, paused 45ms, total 45ms
12-24 15:34:38.789: D/dalvikvm(30509): GC_FOR_ALLOC freed 521K, 40% free 13517K/22312K, paused 57ms, total 57ms

ART 结果:
12-24 15:37:51.500: I/art(4735): Background sticky concurrent mark sweep GC freed 27480(1767KB) AllocSpace objects, 35(660KB) LOS objects, 4% free, 39MB/40MB, paused 7.802ms total 68.710ms
12-24 15:37:57.229: I/art(27369): Explicit concurrent mark sweep GC freed 82830(3MB) AllocSpace objects, 10(160KB) LOS objects, 39% free, 19MB/32MB, paused 744us total 84.814ms
12-24 15:37:57.341: I/art(745): Explicit concurrent mark sweep GC freed 47032(2MB) AllocSpace objects, 4(64KB) LOS objects, 24% free, 49MB/65MB, paused 1.161ms total 78.172ms

12-24 15:38:00.259: I/art(5776): Background sticky concurrent mark sweep GC freed 29233(2MB) AllocSpace objects, 34(5MB) LOS objects, 14% free, 28MB/33MB, paused 3.255ms total 265.466ms

从两张图可以清楚的发现,在dalvik模式下刚启动百度地图的几秒内,触发了26次GC事件,总共停顿耗时5371ms。而在ART模式下,可以看到一共触发了4次GC事件,共耗时497.162ms。
对比可以看到,ART下的GC的性能明显提升了,几乎可以说是提升了十倍左右,这是一个数量级的提升,GC环节带来的性能提升还是非常明显。

4 总结

首先通过对dalvik的GC的过程的分析,我们可以发现dalvik的在GC时出现的几个主要问题,首先即在GC时会有三次暂停其他进程运行,三次停顿导致的总的时间太长会导致丢帧卡顿现象严重。其次,就是在堆空间中给较大的对象分配空间后会导致碎片化比较严重,并且可能会导致GC调用次数变多增加开销。
我们可以发现,针对dalvik的以上两个问题,ART都有做了对应的优化来解决这些问题。针对第一个问题,ART在标记阶段做了非常大的优化,消除了第一次遍历堆地址空间的停顿,和第二次标记根集对象的停顿,并缩短了第三次处理card table的停顿,因此大大的缩短了应用程序在执行时的卡顿时间。针对第二个问题,提出了LOS的管理方法。
除此以外,还提供了丰富的GC收集器,例如继承自mark sweep的sticky mark sweep和partial mark sweep,二者的回收力度都要比full mark sweep小,因此性能上也得到了一些提升。一般情况下的收集器的主力就是sticky mark sweep, 这是对应用程序的性能影响最小的一种方式,因此大多数情况的GC表现,都要比dalvik的GC表现更好。
并且,通过实验数据的显示我们也可以看到,ART的GC的性能确实有了显著的提升,应用程序的流畅性得到了较好的保证。
以上都只是一个比较初步的分析比较,进一步的原理研究还需要详细学习源码才能融会贯通。

5 参考文档

  1. https://source.android.com/devices/tech/dalvik/gc-debug.html;
  2. http://www.cnblogs.com/jinkeep/p/3818180.html
  3. https://infinum.co/the-capsized-eight/articles/art-vs-dalvik-introducing-the-new-android-runtime-in-kit-kat
  4. http://blog.csdn.net/luoshengyang/article/details/42555483