1. invalidate 和 postInvalidate 的关系

postInvalidate 是通过 Handler 切换回到主线程,然后在调用 invalidate 的,源码:

public void postInvalidate() {

postInvalidateDelayed(0);

}

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 中

public void dispatchInvalidateDelayed(View view, long delayMilliseconds) {

Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view);

mHandler.sendMessageDelayed(msg, delayMilliseconds);

}

final class ViewRootHandler extends Handler {

@Override

public void handleMessage(Message msg) {

switch (msg.what) {

case MSG_INVALIDATE:

((View) msg.obj).invalidate();

break;

...

}

2. 子线程是否可以更新 UI ?

可以的,在 Activity 的 onCreate 中直接开启子线程并在子线程中更新 UI 是没问题的:

public class MainActivity extends Activity {

private TextView tvText;

@Override

public void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

tvText = (TextView) findViewById(R.id.main_tv);

new Thread(new Runnable() {

@Override

public void run() {

try {

Thread.sleep(200);

} catch (InterruptedException e) {

e.printStackTrace();

}

tvText.setText("OtherThread");

}

}).start();

}

}

原因:校验线程是 ViewRootImpl 来做的,但是它的创建流程是在 Activity 的 onResume 的时候:

// ActivityThread 中

final void handleResumeActivity(IBinder token,

boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {

ActivityClientRecord r = mActivities.get(token);

...

if (r != null) {

final Activity a = r.activity;

if (r.window == null && !a.mFinished && willBeVisible) {

r.window = r.activity.getWindow();

View decor = r.window.getDecorView();

decor.setVisibility(View.INVISIBLE);

ViewManager wm = a.getWindowManager();

WindowManager.LayoutParams l = r.window.getAttributes();

...

if (a.mVisibleFromClient) {

if (!a.mWindowAdded) {

a.mWindowAdded = true;

// 关键代码

wm.addView(decor, l);

} else {

a.onWindowAttributesChanged(l);

}

}

...

}

// WindowManagerGlobal 中

public void addView(View view, ViewGroup.LayoutParams params,

Display display, Window parentWindow) {

...

ViewRootImpl root;

View panelParentView = null;

synchronized (mLock) {

...

// 在这里创建 ViewRootImpl

root = new ViewRootImpl(view.getContext(), display);

view.setLayoutParams(wparams);

...

}

}

// 在 ViewRootImpl 中有这么段代码,所有更新 UI 都会走到这里

void checkThread() {

if (mThread != Thread.currentThread()) { // mThread 就是主线程

throw new CalledFromWrongThreadException(

"Only the original thread that created a view hierarchy can touch its views.");

}

}

所以子线程只要在 ViewRootImpl 创建之前更新 UI 就没问题!

3. invalidate 的源码分析

先看一张图:

invalidate 的流程

于是自己尝试走走源码:

// view 中

public void invalidate() {

invalidate(true);

}

public void invalidate(boolean invalidateCache) {

invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);

}

void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,

boolean fullInvalidate) {

if (mGhostView != null) {

mGhostView.invalidate(true);

return;

}

if (skipInvalidate()) {

return;

}

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);

// 调用父类的 invalidateChild 方法

p.invalidateChild(this, damage);

}

// Damage the entire projection receiver, if necessary.

if (mBackground != null && mBackground.isProjected()) {

final View receiver = getProjectionReceiver();

if (receiver != null) {

receiver.damageInParent();

}

}

}

}

看到 View 的 invalidate 最后是调用了 p.invalidateChild(this, damage); p 是 ViewParent 的对象,具体实现是 ViewGroup

// ViewGroup 中

@Override

public final void invalidateChild(View child, final Rect dirty) {

final AttachInfo attachInfo = mAttachInfo;

...

ViewParent parent = this;

do {

View view = null;

...

// 关键代码

parent = parent.invalidateChildInParent(location, dirty);

...

} while (parent != null);

}

@Override

public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) {

if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID)) != 0) {

// either DRAWN, or 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();

}

}

location[CHILD_LEFT_INDEX] = left;

location[CHILD_TOP_INDEX] = top;

} else {

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);

}

location[CHILD_LEFT_INDEX] = mLeft;

location[CHILD_TOP_INDEX] = mTop;

mPrivateFlags &= ~PFLAG_DRAWN;

}

mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;

if (mLayerType != LAYER_TYPE_NONE) {

mPrivateFlags |= PFLAG_INVALIDATED;

}

return mParent;

}

return null;

}

上面 invalidateChildInParent 开始时会调用 ViewGroup 自己的 invalidateChildInParent 方法,但到最后还是会调用到 ViewRootImpl 中的 invalidateChildInParent,看下 ViewRootImpl 中的具体实现

// ViewRootImpl 中

@Override

public ViewParent invalidateChildInParent(int[] location, Rect dirty) {

checkThread();

if (DEBUG_DRAW) Log.v(mTag, "Invalidate child: " + dirty);

if (dirty == null) {

invalidate();

return null;

} else if (dirty.isEmpty() && !mIsAnimating) {

return null;

}

if (mCurScrollY != 0 || mTranslator != null) {

mTempRect.set(dirty);

dirty = mTempRect;

if (mCurScrollY != 0) {

dirty.offset(0, -mCurScrollY);

}

if (mTranslator != null) {

mTranslator.translateRectInAppWindowToScreen(dirty);

}

if (mAttachInfo.mScalingRequired) {

dirty.inset(-1, -1);

}

}

// 又调用了这个方法

invalidateRectOnScreen(dirty);

return null;

}

private void invalidateRectOnScreen(Rect dirty) {

final Rect localDirty = mDirty;

if (!localDirty.isEmpty() && !localDirty.contains(dirty)) {

mAttachInfo.mSetIgnoreDirtyState = true;

mAttachInfo.mIgnoreDirtyState = true;

}

// 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

final float appScale = mAttachInfo.mApplicationScale;

final boolean intersected = localDirty.intersect(0, 0,

(int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));

if (!intersected) {

localDirty.setEmpty();

}

if (!mWillDrawSoon && (intersected || mIsAnimating)) {

// 关键又调用了这个方法

scheduleTraversals();

}

}

void scheduleTraversals() {

if (!mTraversalScheduled) {

mTraversalScheduled = true;

mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();

// 会调用 mTraversalRunnable 中的 run 方法

mChoreographer.postCallback(

Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);

if (!mUnbufferedInputDispatch) {

scheduleConsumeBatchedInput();

}

notifyRendererOfFramePending();

pokeDrawLockIfNeeded();

}

}

final class TraversalRunnable implements Runnable {

@Override

public void run() {

doTraversal();

}

}

void doTraversal() {

if (mTraversalScheduled) {

mTraversalScheduled = false;

mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

if (mProfile) {

Debug.startMethodTracing("ViewAncestor");

}

// 终于到了关键方法了:

performTraversals();

if (mProfile) {

Debug.stopMethodTracing();

mProfile = false;

}

}

}

ViewRootImpl 最终调用到了performTraversals 中,这个方法巨长,涉及到了 onMeasure/onLayout/onDraw 等重要方法的起源:

private void performTraversals() {

...

// 这里最终会触发 view 的 onMeasure

performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

performLayout(lp, mWidth, mHeight);

performDraw();

...

mIsInTraversal = false;

}

看到上面就是对应着 View 的绘制流程了,继续看 performDraw 的实现:

private void performDraw() {

...

try {

draw(fullRedrawNeeded);

} finally {

mIsDrawing = false;

Trace.traceEnd(Trace.TRACE_TAG_VIEW);

}

...

}

private void draw(boolean fullRedrawNeeded) {

if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {

return;

}

}

private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,

boolean scalingRequired, Rect dirty) {

...

try {

canvas.translate(-xoff, -yoff);

if (mTranslator != null) {

mTranslator.translateCanvas(canvas);

}

canvas.setScreenDensity(scalingRequired ? mNoncompatDensity : 0);

attachInfo.mSetIgnoreDirtyState = false;

// 最终调用了 View 的 draw 方法了

mView.draw(canvas);

} finally {

...

}

return true;

}

看到终于调用到 View 的 draw 方法来了,继续看下 ViewGroup 和 View 在这方法中的处理方式:

// View 中的 draw 方法:

public void draw(Canvas canvas) {

final int privateFlags = mPrivateFlags;

final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&

(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);

mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;

/*

* Draw traversal performs several drawing steps which must be executed

* in the appropriate order:

*

* 1. Draw the background

* 2. If necessary, save the canvas' layers to prepare for fading

* 3. Draw view's content

* 4. Draw children

* 5. If necessary, draw the fading edges and restore layers

* 6. Draw decorations (scrollbars for instance)

*/

// Step 1, draw the background, if needed

int saveCount;

if (!dirtyOpaque) {

drawBackground(canvas);

}

// skip step 2 & 5 if possible (common case)

final int viewFlags = mViewFlags;

boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;

boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;

if (!verticalEdges && !horizontalEdges) {

// Step 3, draw the content

if (!dirtyOpaque) onDraw(canvas);

// Step 4, draw the children

dispatchDraw(canvas);

drawAutofilledHighlight(canvas);

// Overlay is part of the content and draws beneath Foreground

if (mOverlay != null && !mOverlay.isEmpty()) {

mOverlay.getOverlayView().dispatchDraw(canvas);

}

// Step 6, draw decorations (foreground, scrollbars)

onDrawForeground(canvas);

// Step 7, draw the default focus highlight

drawDefaultFocusHighlight(canvas);

if (debugDraw()) {

debugDrawFocus(canvas);

}

// we're done...

return;

}

/*

* Here we do the full fledged routine...

* (this is an uncommon case where speed matters less,

* this is why we repeat some of the tests that have been

* done above)

*/

boolean drawTop = false;

boolean drawBottom = false;

boolean drawLeft = false;

boolean drawRight = false;

float topFadeStrength = 0.0f;

float bottomFadeStrength = 0.0f;

float leftFadeStrength = 0.0f;

float rightFadeStrength = 0.0f;

// Step 2, save the canvas' layers

int paddingLeft = mPaddingLeft;

final boolean offsetRequired = isPaddingOffsetRequired();

if (offsetRequired) {

paddingLeft += getLeftPaddingOffset();

}

int left = mScrollX + paddingLeft;

int right = left + mRight - mLeft - mPaddingRight - paddingLeft;

int top = mScrollY + getFadeTop(offsetRequired);

int bottom = top + getFadeHeight(offsetRequired);

if (offsetRequired) {

right += getRightPaddingOffset();

bottom += getBottomPaddingOffset();

}

final ScrollabilityCache scrollabilityCache = mScrollCache;

final float fadeHeight = scrollabilityCache.fadingEdgeLength;

int length = (int) fadeHeight;

// clip the fade length if top and bottom fades overlap

// overlapping fades produce odd-looking artifacts

if (verticalEdges && (top + length > bottom - length)) {

length = (bottom - top) / 2;

}

// also clip horizontal fades if necessary

if (horizontalEdges && (left + length > right - length)) {

length = (right - left) / 2;

}

if (verticalEdges) {

topFadeStrength = Math.max(0.0f, Math.min(1.0f, getTopFadingEdgeStrength()));

drawTop = topFadeStrength * fadeHeight > 1.0f;

bottomFadeStrength = Math.max(0.0f, Math.min(1.0f, getBottomFadingEdgeStrength()));

drawBottom = bottomFadeStrength * fadeHeight > 1.0f;

}

if (horizontalEdges) {

leftFadeStrength = Math.max(0.0f, Math.min(1.0f, getLeftFadingEdgeStrength()));

drawLeft = leftFadeStrength * fadeHeight > 1.0f;

rightFadeStrength = Math.max(0.0f, Math.min(1.0f, getRightFadingEdgeStrength()));

drawRight = rightFadeStrength * fadeHeight > 1.0f;

}

saveCount = canvas.getSaveCount();

int solidColor = getSolidColor();

if (solidColor == 0) {

final int flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG;

if (drawTop) {

canvas.saveLayer(left, top, right, top + length, null, flags);

}

if (drawBottom) {

canvas.saveLayer(left, bottom - length, right, bottom, null, flags);

}

if (drawLeft) {

canvas.saveLayer(left, top, left + length, bottom, null, flags);

}

if (drawRight) {

canvas.saveLayer(right - length, top, right, bottom, null, flags);

}

} else {

scrollabilityCache.setFadeColor(solidColor);

}

// Step 3, draw the content

if (!dirtyOpaque) onDraw(canvas);

// Step 4, draw the children

dispatchDraw(canvas);

// Step 5, draw the fade effect and restore layers

final Paint p = scrollabilityCache.paint;

final Matrix matrix = scrollabilityCache.matrix;

final Shader fade = scrollabilityCache.shader;

if (drawTop) {

matrix.setScale(1, fadeHeight * topFadeStrength);

matrix.postTranslate(left, top);

fade.setLocalMatrix(matrix);

p.setShader(fade);

canvas.drawRect(left, top, right, top + length, p);

}

if (drawBottom) {

matrix.setScale(1, fadeHeight * bottomFadeStrength);

matrix.postRotate(180);

matrix.postTranslate(left, bottom);

fade.setLocalMatrix(matrix);

p.setShader(fade);

canvas.drawRect(left, bottom - length, right, bottom, p);

}

if (drawLeft) {

matrix.setScale(1, fadeHeight * leftFadeStrength);

matrix.postRotate(-90);

matrix.postTranslate(left, top);

fade.setLocalMatrix(matrix);

p.setShader(fade);

canvas.drawRect(left, top, left + length, bottom, p);

}

if (drawRight) {

matrix.setScale(1, fadeHeight * rightFadeStrength);

matrix.postRotate(90);

matrix.postTranslate(right, top);

fade.setLocalMatrix(matrix);

p.setShader(fade);

canvas.drawRect(right - length, top, right, bottom, p);

}

canvas.restoreToCount(saveCount);

drawAutofilledHighlight(canvas);

// Overlay is part of the content and draws beneath Foreground

if (mOverlay != null && !mOverlay.isEmpty()) {

mOverlay.getOverlayView().dispatchDraw(canvas);

}

// Step 6, draw decorations (foreground, scrollbars)

onDrawForeground(canvas);

}

这个方法的大体意思是这样的:

@Override

public void draw(Canvas canvas) {

...

drawBackground(canvas); // 绘制背景

onDraw(canvas); // 调用自己的 onDraw 方法来绘制内容

dispatchDraw(canvas); // 分发绘制

onDrawForeground(canvas); // 绘制前景

...

}

上面几个方法中,只有 dispatchDraw 涉及到分发绘制,其他的都是对自身的绘制,所以继续看 dispatchDraw

// View 中的实现,是个空方法,也就是 View 没有孩子,不需要什么分发

protected void dispatchDraw(Canvas canvas) {

}

View 中的实现,是个空方法,也就是 View 没有孩子,不需要什么分发。那么看下 ViewGroup 中是怎么分发的:

@Override

protected void dispatchDraw(Canvas canvas) {

...

while (transientIndex >= 0) {

// there may be additional transient views after the normal views

final View transientChild = mTransientViews.get(transientIndex);

if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||

transientChild.getAnimation() != null) {

// 看到这里,会去绘制 子View

more |= drawChild(canvas, transientChild, drawingTime);

}

transientIndex++;

if (transientIndex >= transientCount) {

break;

}

}

...

}

protected boolean drawChild(Canvas canvas, View child, long drawingTime) {

// 又调回了 View 的 draw 来了

return child.draw(canvas, this, drawingTime);

}

上面又调会了 View 的 draw 来了,如此递归调用下去,直到遍历完所有的 VIew 。

4. 总结

1 invalidate 和 postInvalidate 的关系:

postInvalidate 最终通过 Handler 切换到主线程,调用 invalidate

2 能否在子线程中更新 UI ?

只要在校验 UI 线程前,子线程是可以更新 UI 的,也就是 Activity 的 onResume 方法前。因为在 onResume 中创建了 ViewRootImpl。

3 invalidate 源码

invalidate 会先找到父类去走绘制流程,最终遍历所有相关联的 View ,触发它们的 onDraw 方法进行绘制

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。

android invalidate 不刷新,浅谈Android invalidate 分析相关推荐

  1. android 换行乱_浅谈Android textview文字对齐换行的问题

    今天忽然发现android项目中的文字排版参差不齐的情况非常严重,不得不想办法解决一下. 经过研究之后,终于找到了textview自动换行导致混乱的原因了----半角字符与全角字符混乱所致!一般情况下 ...

  2. [Android 泥水匠] Android基础 之一:浅谈Android架构到HelloWorld案例的剖析

    作者:泥沙砖瓦浆木匠 网站:http://blog.csdn.net/jeffli1993 个人签名:打算起手不凡写出鸿篇巨作的人,往往坚持不了完成第一章节. 交流QQ群:[编程之美 36523458 ...

  3. android 清屏函数,浅谈android截屏问题

    做了几个月的截屏开发,稍微了解了一下这方面的知识,于是拿来分享一下,也许对你有一些帮助吧. 我是基于android2.3.3系统之上的,想必大家应该知道在android源码下面有个文件叫做screen ...

  4. 浅谈Android保护技术__代码混淆

    浅谈Android保护技术__代码混淆 浅谈Android保护技术__代码混淆 代码混淆 代码混淆(Obfuscated code)亦称花指令,是将计算机程序的代码,转换成一种功能上等价,但是难于阅读 ...

  5. android fps 垂直同步,浅谈Android流畅度

    原标题:浅谈Android流畅度 哈哈 讲个故事 白 1 流畅度 关于流畅度谷歌官方给出的解释为:running at a consistent 60 frames per second, witho ...

  6. android获取存储设备根目录,浅谈android获取存储目录(路径)的几种方式和注意事项...

    通常, 我们创建文件/目录, 或者存储图片什么的, 我们都需要拿到手机的存储路径, 现在我们就来看一下获取手机存储路径的几种方式(作为工具类方法调用即可): 第一种: 获取 /storage/emul ...

  7. 浅谈Android Architecture Components

    浅谈Android Architecture Components 浅谈Android Architecture Components 简介 Android Architecture Componen ...

  8. 浅谈Android文件管理器的几种实现方式(原理篇)--对我有帮助

    转自 https://blog.csdn.net/weixin_33698823/article/details/87269955 浅谈Android文件管理器的几种实现方式 为了完成毕业设计,我花费 ...

  9. 浅谈Android游戏开发基础和经验

    Android游戏开发基础和经验是本文要介绍的内容,主要是来了解并学习Android游戏开发的内容实例,具体关于Android游戏开发内容的详解来看本文. 做一个类似俄罗斯方块的android游戏开发 ...

最新文章

  1. 机器物联网的四大价值流
  2. php 小数点 乘法,js小数点数字相乘、把小数点四舍五入保留两位小数
  3. Linux-Android启动之Init进程前传
  4. 二分查找及一般拓展总结
  5. 双一流大学毕业的我,应该何去何从?
  6. linux服务器之间做ssh,Linux 服务器之间怎么样 SSH 不需密码
  7. 在C / C ++中使用INT_MAX和INT_MIN
  8. linux jmeter 内存,怎么在Linux下改变JMeter内存
  9. Atitit 音频资料与音乐库管理系统功能 目录 1. 通用功能区 2 1.1. 批量处理功能文件夹遍历 2 1.2. Zip文件遍历与读取 2 1.3. Rar文件遍历与读取 2 1.4. She
  10. 09 Python 利用爱心曲线函数打印自定义内容为爱心形状
  11. 分享5个国外较好的图片网站
  12. python性能测试框架_python性能测试框架locust(一)
  13. Pycharm 报错 Environment location directory is not empty 解决
  14. 软件测试基础篇(3)
  15. 字符串的使用(JavaScript)
  16. c#飞行棋游戏(控制台)
  17. “技术男”升为“管理者”,角色一定要转变
  18. 真正爱一个人,是可以无私的付出
  19. html音乐播放器歌单,H5音乐播放器【歌单列表】
  20. 袁国宝:恒大“押宝”,房市车市真要“里外通吃”?

热门文章

  1. mysql case when
  2. html5滚动条样式修改,css修改滚动条样式
  3. CSS中 滚动条样式设置
  4. java stringbuilder 清空问题
  5. Java中StringBuilder清空数据方法比较
  6. oracle中常使用到的函数,oracle中经常用到的函数
  7. 情感伤感语录标题文案
  8. 曼达洛人对机器人的评价_为什么曼达洛人如此厌恶机器人?硬核奶爸原来也有童年阴影...
  9. Android手机街霸4出招表,安卓手机版《街霸4》出招表
  10. Java与算法之(5) - 老鼠走迷宫(深度优先算法)