阅读视图

发现新文章,点击刷新页面。

假装是个设计师

每个 Web 开发人员都会遇到要做 UI 的情况,无论他喜欢与否。

也许你公司没有全职 UI 设计,所以你得自己来。也或许你在做一个 side-project,但你想让它比 Bootstrap 好看点。

你可以说「我又不是艺术家,别要求我太多」,但事实表明,你还是有很多技巧来提高自己的设计水平,当然了,这并不需要你有很好的 UI 技术。

以下是七个提升设计感的简单实践。

一、 用颜色和粗细来划分重点,而不是字体大小

文本样式的一个常见错误是过度依赖字体大小来划分重点结构。

「这段文字重要吗,字大点。」

「这段文字不太重要,字小点。」

不要总用字体来控制这些,尝试使用颜色和 font-wight 也可以完成同样的工作。比如:

「这段文字重要,加粗。」

「这段文字不重要,细一点。」

用两种或三种颜色划分你的内容:

  • 重要内容用深色(但不是黑色)(如文章标题)
  • 次重要内容用灰色(如文章发布日期)
  • 不重要内容用浅灰色(如页脚的版权声明)

大多情况,两种 font-weight 就够了:

  • 大部分的文字用普通 weight(400或500)
  • 你强调的文字 weight(600或700)

400 以下的 weight 就别用了;大标题下还可以看清,小字体基本看不清。如果你想用更小的 weight 来弱化你的文字,用更淡的颜色或更小的字体吧。

二、彩色背景别用灰色字体

在白色背景下使用浅灰色字体可以弱化内容,但是在彩色背景下就不好了。

这是因为我们在白色背景下看灰色其实是降低了文字的对比度。

相比于使用灰色字体,让文本接近背景色更能突出文字重点。

彩色背景下,你可以通过两种方法降低对比度。

1.降低白字不透明度(opacity)

白色,低不透明度,再让背景色透过来一点,这样可以弱化文字,又不会在彩色背景下显得很违和。

2.根据背景色自己选一个合适的颜色

当你背景是图片或图案时,这种方法比降低不透明度要好一些。而且降低不透明度显得文字太暗了,就像水洗的一样。

选个和背景色相同的颜色,再慢慢调饱和度和亮度,直到看起来还不错。

3.正确使用阴影

相比于使用大阴影(blur)和暗角来让阴影更明显,不如添加垂直偏移 (vertical offset)。

这样看起来会更自然,因为它模拟了光源从上面照下来的效果,这和我们在现实世界看到的一样。

输入框和表格也可以这么用:

阴影设计是门艺术,感兴趣可以看看 Material Design Guidelines

四、少用 border

当你想划分两个元素的边界时,先别考虑 border。

border 确实在划分边界上做的很不错,但这不是唯一的方法,滥用 border 会让你的设计特别杂乱。

你可以试试下面的方法:

1.box shadow

box shadow 不仅可以像 border那样做到划分边界,而且这种方法更精细,也不会太分散你的注意力。

2.两种背景色

相邻元素用不同的背景色就可以划分它们。

3.加空隙

有没有更好的办法划分不同元素的边界,而不只是简单地增加分割部分?当然有,把元素隔得更远就行了。

五、少用大图标,如果它们本身意义不大的话

如果你在设计一些大图标(比如登录页面的「功能」部分),试试 Font Awesome 或 Zondicons 这样的免费图标集,然后加大尺寸直到满足你的要求。

它们都是矢量图,所以不会在放大的过程中丢失细节。

虽然矢量图不会在拉伸时丢失细节,但本来在 16-24px绘制的图标非要拉伸到三倍四倍之大,真的不会显得很好看,他们会缺乏一些细节,比如不成比例的「矮胖」。

如果小图标可以满足你的要求,可以将它们包在另外一个形状中,并且为形状添加背景色。

这可以让实际图标更加接近预期的大小,同时可以填充更大的空间。

如果有预算,可以用大尺寸的高级矢量图,比如Heroicons or Iconic

六、边框特写,给平淡的设计增加颜色

如果你不是 UI,怎么才能从你的用户界面中和其他那些好看的设计中脱颖而出呢。

简单却有效的一个技巧就是为某些部分添加色彩鲜艳的边框,这样不会感到乏味。

举个例子,在提醒信息的左边做文章。

高亮导航栏

顶端都加上也行。。

它不需要任何专业的 UI 设计人员帮你,但可以让你网站看起来更「大气」。

是不是不好选颜色,Dribbble’s color search 可以帮你。

七、不是每个按钮都需要颜色

当用户可以在一个页面有很多的选择时,很容易陷入语义化设计的陷阱。

比如 Bootstrap,无论何时添加新按钮,都可以选择不同的语义。

「积极动作,绿按钮。」

「删除数据,红按钮。」

语义是按钮设计的一个重要部分,但有一个更常见的重要维度被忽略了:重要性。

页面上的每个操作都处于金字塔的某个重要位置。但大多数页面只有一个真正重要的操作,一些不太重要的次要操作,以及一些很少使用的三级操作。

设计这些动作的时候,更重要的是理清这些按钮谁更重要。

  • 重要动作应该突出,加粗,高对比度。
  • 不太重要的动作应该清楚显示,但别太主导。用轮廓线,低对比度背景色。
  • 三级操作可见即可,但不要引人注意。像链接一样处理吧。

「删除按钮呢,应不应该总是红色?」

没必要,如果它们不是页面中的主要操作,处理成二级或三级按钮即可。

保留大、红,粗样式在那些以负面操作为主的页面上,比如确认对话框。

原文链接:7 Practical Tips for Cheating at Design – Refactoring UI – Medium

作者:Adam Wathan & Steve Schoger

编译:@王隐在录音

小试自定义GPT

最近不是在折腾LLM嘛,于是就试了两条路子:用openai的api,以及直接在openai的界面里面创建GPT。

前者没啥特别的,chatgpt的api做的很成熟了,from openai import OpenAI 之后直接在python里面调用几个现成的函数就好了。可选的参数其实也不多,主要就是prompt写的好一点就行。我的要求也不高,试了试基本满足。此外我还用到了微软 azure api,也很方便,两者一结合基本一个app就搓出来了,只是暂时还只能在命令行运行,没写前端ui罢了。

后者就麻烦了。我想着自己写前端ui还挺麻烦的,就想偷个懒直接在GPT里面弄弄看看行不。结果呢,现在这个版本实在是太挫了,只支持最最基本的action,虽然可以调用其他api,但还没研究出来怎么实现用户上传的文件扔到action api call里面。搜了搜他们的论坛也没啥结果,然后心累就到此为止了。

最后贴一下如何在openai 的GPT里面调用azure api。主要是api key那里实在是反用户直觉,我找了好久……一定要选 custom 然后把自定义的名字设为 Ocp-Apim-Subscription-Key 才可以。贴个图。

自定义 action -> authentication -> custom header name

当然azure api的文档做的也很差就是了,经常搜出来的是过时的文档,试一试都是404错误。哎,时间都花在这些琐碎的调试bug上了。

最后的结论是,在现在这个阶段,openai GPT的多模态做的还是太封闭,只适用于比较基础的交互需求,得等到后面允许自定义编程更丰富一些才可以。想做的稍稍复杂一点,写ui是逃不掉的了。web版还可以写个python+js凑和一下(flask这么轻量级的web开发框架真的是效率提升利器),app版xcode看了半天发现也是一等一的复杂……说好的ai改变程序开发呢?叹口气……

Android hwui 中 RenderThread 工作流程

前言

本篇文章是自己的一个学习笔记,记录了 Android 5.0 中 hwui 中的 RenderThread 的简单工作流程。由于是学习笔记,所以其中一些细节不会太详细,我只是将大概的流程走一遍,将其工作流标注出来,下次遇到问题的时候就可以知道去哪里查。

下图是我用 Systrace 抓取的一个应用启动的时候 RenderThread 的第一次 Draw 的 Trace 图,从这里面的顺序来看 RenderThread 的流程。熟悉应用启动流程的话应该知道,只有当第一次 DrawFrame 完成之后,整个应用的界面才会显示在手机上,在这之前,用户看到的是应用的 StartingWindow 的界面。

RenderThread Draw first frame

从Java层说起

应用程序的每一帧是从接收到 VSYNC 信号开始进行计算和绘制的,这要从 Choreographer 这个类说起了,不过由于篇幅原因,我们直接看一帧的绘制调用关系链即可:

绘制关系链

Choreographer 的 drawFrame 会调用到 ViewRootImpl 的 performTraversals 方法,而 performTraversals 方法最终会调用到performDraw() 方法, performDraw 又会调用到 draw(boolean fullRedrawNeeded) 方法,这个 draw 方法是 ViewRootImpl 的私有方法,和我们熟知的那个draw并不是同一个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
            if (mAttachInfo.mHardwareRenderer != null && mAttachInfo.mHardwareRenderer.isEnabled()) {
mIsAnimating = false;
boolean invalidateRoot = false;
if (mHardwareYOffset != yOffset || mHardwareXOffset != xOffset) {
mHardwareYOffset = yOffset;
mHardwareXOffset = xOffset;
mAttachInfo.mHardwareRenderer.invalidateRoot();
}
mResizeAlpha = resizeAlpha;

dirty.setEmpty();

mBlockResizeBuffer = false;
mAttachInfo.mHardwareRenderer.draw(mView, mAttachInfo, this);
}

如果是走硬件绘制路线的话,则会走这一条先,之后就会调用 mHardwareRenderer 的 draw 方法,这里的 mHardwareRenderer 指的是 ThreadedRenderer ,其 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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@Override
void draw(View view, AttachInfo attachInfo, HardwareDrawCallbacks callbacks) {
attachInfo.mIgnoreDirtyState = true;
long frameTimeNanos = mChoreographer.getFrameTimeNanos();
attachInfo.mDrawingTime = frameTimeNanos / TimeUtils.NANOS_PER_MS;

long recordDuration = 0;
if (mProfilingEnabled) {
recordDuration = System.nanoTime();
}

updateRootDisplayList(view, callbacks);

if (mProfilingEnabled) {
recordDuration = System.nanoTime() - recordDuration;
}

attachInfo.mIgnoreDirtyState = false;

// register animating rendernodes which started animating prior to renderer
// creation, which is typical for animators started prior to first draw
if (attachInfo.mPendingAnimatingRenderNodes != null) {
final int count = attachInfo.mPendingAnimatingRenderNodes.size();
for (int i = 0; i < count; i++) {
registerAnimatingRenderNode(
attachInfo.mPendingAnimatingRenderNodes.get(i));
}
attachInfo.mPendingAnimatingRenderNodes.clear();
// We don't need this anymore as subsequent calls to
// ViewRootImpl#attachRenderNodeAnimator will go directly to us.
attachInfo.mPendingAnimatingRenderNodes = null;
}

int syncResult = nSyncAndDrawFrame(mNativeProxy, frameTimeNanos,
recordDuration, view.getResources().getDisplayMetrics().density);
if ((syncResult & SYNC_INVALIDATE_REQUIRED) != 0) {
attachInfo.mViewRootImpl.invalidate();
}
}

这个函数里面的 updateRootDisplayList(view, callbacks) ;即 getDisplayList 操作。接下来就是比较重要的一个操作:

1
2
int syncResult = nSyncAndDrawFrame(mNativeProxy, frameTimeNanos,
recordDuration, view.getResources().getDisplayMetrics().density);

可以看出这是一个阻塞操作,等Native层完成后,拿到返回值后才会进行下一步的操作。

Native层

其Native代码在android_view_ThreadedRenderer.cpp中,对应的实现代码如下:

1
2
3
4
5
static int android_view_ThreadedRenderer_syncAndDrawFrame(JNIEnv* env, jobject clazz,
jlong proxyPtr, jlong frameTimeNanos, jlong recordDuration, jfloat density) {
RenderProxy* proxy = reinterpret_cast<RenderProxy*>(proxyPtr);
return proxy->syncAndDrawFrame(frameTimeNanos, recordDuration, density);
}

RenderProxy的路径位于frameworks/base/libs/hwui/renderthread/RenderProxy.cpp

1
2
3
4
5
int RenderProxy::syncAndDrawFrame(nsecs_t frameTimeNanos, nsecs_t recordDurationNanos,
float density) {
mDrawFrameTask.setDensity(density);
return mDrawFrameTask.drawFrame(frameTimeNanos, recordDurationNanos);
}

其中 mDrawFrameTask 是一个 DrawFrameTask 对象,其路径位于frameworks/base/libs/hwui/renderthread/DrawFrameTask.cpp,其中drawFrame代码:

1
2
3
4
5
6
7
8
9
10
11
12
int DrawFrameTask::drawFrame(nsecs_t frameTimeNanos, nsecs_t recordDurationNanos) {
mSyncResult = kSync_OK;
mFrameTimeNanos = frameTimeNanos;
mRecordDurationNanos = recordDurationNanos;
postAndWait();

// Reset the single-frame data
mFrameTimeNanos = 0;
mRecordDurationNanos = 0;

return mSyncResult;
}

其中 postAndWait() 的实现如下:

1
2
3
4
5
void DrawFrameTask::postAndWait() {
AutoMutex _lock(mLock);
mRenderThread->queue(this);
mSignal.wait(mLock);
}

就是将一个 DrawFrameTask 放入到了 mRenderThread 中,其中 queue 方法实现如下:

1
2
3
4
5
6
7
8
void RenderThread::queue(RenderTask* task) {
AutoMutex _lock(mLock);
mQueue.queue(task);
if (mNextWakeup && task->mRunAt < mNextWakeup) {
mNextWakeup = 0;
mLooper->wake();
}
}

其中 mQueue 是一个 TaskQueue 对象,其

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 TaskQueue::queue(RenderTask* task) {
// Since the RenderTask itself forms the linked list it is not allowed
// to have the same task queued twice
LOG_ALWAYS_FATAL_IF(task->mNext || mTail == task, "Task is already in the queue!");
if (mTail) {
// Fast path if we can just append
if (mTail->mRunAt <= task->mRunAt) {
mTail->mNext = task;
mTail = task;
} else {
// Need to find the proper insertion point
RenderTask* previous = 0;
RenderTask* next = mHead;
while (next && next->mRunAt <= task->mRunAt) {
previous = next;
next = next->mNext;
}
if (!previous) {
task->mNext = mHead;
mHead = task;
} else {
previous->mNext = task;
if (next) {
task->mNext = next;
} else {
mTail = task;
}
}
}
} else {
mTail = mHead = task;
}
}

接着看 RenderThread 之前的 queue 方法,

1
2
3
4
5
6
7
8
9
10
11
12
void Looper::wake() {
ssize_t nWrite;
do {
nWrite = write(mWakeWritePipeFd, "W", 1);
} while (nWrite == -1 && errno == EINTR);

if (nWrite != 1) {
if (errno != EAGAIN) {
ALOGW("Could not write wake signal, errno=%d", errno);
}
}
}

wake 函数则更为简单,仅仅向管道的写端写入一个字符“W”,这样管道的读端就会因为有数据可读而从等待状态中醒来。

HWUI-RenderThread

接下来会到哪里去,我们首先要熟悉一下RenderThread,RenderThread是继承自Thread的,这个Thread是utils/Thread.h,RenderThread的初始化函数

1
2
3
4
5
6
7
8
9
10
11
12
RenderThread::RenderThread() : Thread(true), Singleton<RenderThread>()
, mNextWakeup(LLONG_MAX)
, mDisplayEventReceiver(0)
, mVsyncRequested(false)
, mFrameCallbackTaskPending(false)
, mFrameCallbackTask(0)
, mRenderState(NULL)
, mEglManager(NULL) {
mFrameCallbackTask = new DispatchFrameCallbacks(this);
mLooper = new Looper(false);
run("RenderThread");
}

其 run 方法在 Thread 中有说明:

1
2
3
4
// Start the thread in threadLoop() which needs to be implemented.
virtual status_t run( const char* name = 0,
int32_t priority = PRIORITY_DEFAULT,
size_t stack = 0);

即启动 threadLoop 函数,我们来看 RenderThread 的 threadLoop 函数,这个函数比较重要:

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
bool RenderThread::threadLoop() {
#if defined(HAVE_PTHREADS)
setpriority(PRIO_PROCESS, 0, PRIORITY_DISPLAY);
#endif
initThreadLocals();

int timeoutMillis = -1;
for (;;) {
int result = mLooper->pollOnce(timeoutMillis);
LOG_ALWAYS_FATAL_IF(result == Looper::POLL_ERROR,
"RenderThread Looper POLL_ERROR!");

nsecs_t nextWakeup;
// Process our queue, if we have anything
while (RenderTask* task = nextTask(&nextWakeup)) {
task->run();
// task may have deleted itself, do not reference it again
}
if (nextWakeup == LLONG_MAX) {
timeoutMillis = -1;
} else {
nsecs_t timeoutNanos = nextWakeup - systemTime(SYSTEM_TIME_MONOTONIC);
timeoutMillis = nanoseconds_to_milliseconds(timeoutNanos);
if (timeoutMillis < 0) {
timeoutMillis = 0;
}
}

if (mPendingRegistrationFrameCallbacks.size() && !mFrameCallbackTaskPending) {
drainDisplayEventQueue(true);
mFrameCallbacks.insert(
mPendingRegistrationFrameCallbacks.begin(), mPendingRegistrationFrameCallbacks.end());
mPendingRegistrationFrameCallbacks.clear();
requestVsync();
}
}

return false;
}

可以看到,一个 for 循环是一个无限循环,而其中 pollOnce 是一个阻塞函数,直到我们上面调用了 mLooper->wake() 之后,会继续往下走,走到 while 循环中:

1
2
3
4
while (RenderTask* task = nextTask(&nextWakeup)) {
task->run();
// task may have deleted itself, do not reference it again
}

会将 RenderTask 取出来执行其 run 方法,经过前面的流程我们知道这个 RenderTask 是一个 DrawFrameTask ,其run方法如下:

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
void DrawFrameTask::run() {
ATRACE_NAME("DrawFrame");

mContext->profiler().setDensity(mDensity);
mContext->profiler().startFrame(mRecordDurationNanos);

bool canUnblockUiThread;
bool canDrawThisFrame;
{
TreeInfo info(TreeInfo::MODE_FULL, mRenderThread->renderState());
canUnblockUiThread = syncFrameState(info);
canDrawThisFrame = info.out.canDrawThisFrame;
}

// Grab a copy of everything we need
CanvasContext* context = mContext;

// From this point on anything in "this" is *UNSAFE TO ACCESS*
if (canUnblockUiThread) {
unblockUiThread();
}

if (CC_LIKELY(canDrawThisFrame)) {
context->draw();
}

if (!canUnblockUiThread) {
unblockUiThread();
}
}

RenderThread.DrawFrame

上面说到了 DrawFrameTask 的 run 方法,这里 run 方法中的执行的方法即我们在最前面那张图中所示的部分(即文章最前面那张图),下面的流程就是那张图中的函数调用,我们结合代码和图,一部分一部分来走整个 DrawFrame 的流程:

1. syncFrameState

第一个比较重要的函数是 syncFrameState ,从函数名就可以知道, syncFrameState 的作用就是同步 frame 信息,将 Java 层维护的 frame 信息同步到 RenderThread中。

Main Thread 和Render Thread 都各自维护了一份应用程序窗口视图信息。各自维护了一份应用程序窗口视图信息的目的,就是为了可以互不干扰,进而实现最大程度的并行。其中,Render Thread维护的应用程序窗口视图信息是来自于 Main Thread 的。因此,当Main Thread 维护的应用程序窗口信息发生了变化时,就需要同步到 Render Thread 去。

所以查看代码就可以知道有两个 RenderNode,一个在 hwui 中,一个在 View 中。简单来说,同步信息就是将 Java 层的 RenderNode 中的信息同步到 hwui 中的 RenderNode 中。 注意syncFrameState的返回值赋给了 canUnblockUiThread ,从名字可以看出这个 canUnblockUiThread 的作用是判断是否唤醒 Main Thread ,也就是说如果返回为 true 的话,会提前唤醒主线程来执行其他的事情,而不用等到 draw 完成后再去唤醒 Main Thread。 这也是 Android 5.0 和 Android 4.x 最大的区别了。

syncFrameState

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
bool DrawFrameTask::syncFrameState(TreeInfo& info) {
mRenderThread->timeLord().vsyncReceived(mFrameTimeNanos);
mContext->makeCurrent();
Caches::getInstance().textureCache.resetMarkInUse();

for (size_t i = 0; i < mLayers.size(); i++) {
mContext->processLayerUpdate(mLayers[i].get());
}
mLayers.clear();
mContext->prepareTree(info);

if (info.out.hasAnimations) {
if (info.out.requiresUiRedraw) {
mSyncResult |= kSync_UIRedrawRequired;
}
}
// If prepareTextures is false, we ran out of texture cache space
return info.prepareTextures;
}

首先是makeCurrent,这里的mContext是一个CanvasContext对象,其makeCurrent实现如下:

1
2
3
4
void CanvasContext::makeCurrent() {
// In the meantime this matches the behavior of GLRenderer, so it is not a regression
mHaveNewSurface |= mEglManager.makeCurrent(mEglSurface);
}

mEglManager是一个EglManager对象,其实现为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
bool EglManager::makeCurrent(EGLSurface surface) {
if (isCurrent(surface)) return false;

if (surface == EGL_NO_SURFACE) {
// If we are setting EGL_NO_SURFACE we don't care about any of the potential
// return errors, which would only happen if mEglDisplay had already been
// destroyed in which case the current context is already NO_CONTEXT
TIME_LOG("eglMakeCurrent", eglMakeCurrent(mEglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT));
} else {
EGLBoolean success;
TIME_LOG("eglMakeCurrent", success = eglMakeCurrent(mEglDisplay, surface, surface, mEglContext));
if (!success) {
LOG_ALWAYS_FATAL("Failed to make current on surface %p, error=%s",
(void*)surface, egl_error_str());
}
}
mCurrentSurface = surface;
return true;
}

这里会判断mCurrentSurface == surface,如果成立,则不用再初始化操作,如果是另外一个surface。,则会执行eglMakeCurrent,来重新创建上下文。

makeCurrent之后,会调用mContext->prepareTree(info),其实现如下:

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
void CanvasContext::prepareTree(TreeInfo& info) {
mRenderThread.removeFrameCallback(this);

info.damageAccumulator = &mDamageAccumulator;
info.renderer = mCanvas;
if (mPrefetechedLayers.size() && info.mode == TreeInfo::MODE_FULL) {
info.canvasContext = this;
}
mAnimationContext->startFrame(info.mode);
mRootRenderNode->prepareTree(info);
mAnimationContext->runRemainingAnimations(info);

if (info.canvasContext) {
freePrefetechedLayers();
}

int runningBehind = 0;
// TODO: This query is moderately expensive, investigate adding some sort
// of fast-path based off when we last called eglSwapBuffers() as well as
// last vsync time. Or something.
TIME_LOG("nativeWindowQuery", mNativeWindow->query(mNativeWindow.get(),
NATIVE_WINDOW_CONSUMER_RUNNING_BEHIND, &runningBehind));
info.out.canDrawThisFrame = !runningBehind;

if (info.out.hasAnimations || !info.out.canDrawThisFrame) {
if (!info.out.requiresUiRedraw) {
// If animationsNeedsRedraw is set don't bother posting for an RT anim
// as we will just end up fighting the UI thread.
mRenderThread.postFrameCallback(this);
}
}
}

其中 mRootRenderNode->prepareTree(info) 又是最重要的。回到Java层,我们知道 ThreadedRenderer 在初始化时,初始化了一个指针

1
long rootNodePtr = nCreateRootRenderNode();

这个RootRenderNode也就是一个根Node,

1
mRootNode = RenderNode.adopt(rootNodePtr);

然后会创建一个 mNativeProxy 指针,在 Native 层初始化一个 RenderProxy 对象,将 rootNodePtr 传给 RenderProxy 对象,这样在 RenderProxy 我们就可以得到这个对象的指针了。其中 CanvasContext 也是在 RenderProxy 对象初始化的时候被初始化的,初始化的时候将 rootNodePtr 传给了 CanvasContext 对象。

我们之前提到 ThreadedRenderer 的 draw 方法中首先会调用updateRootDisplayList,即我们熟悉的 getDisplayList 。这个方法中,其实也分为两个步骤,第一个步骤是 updateViewTreeDisplayList,第二个步骤是将根 Node 加入到 DrawOp 中:

1
2
3
canvas.insertReorderBarrier();
canvas.drawRenderNode(view.getDisplayList());
canvas.insertInorderBarrier();

其最终实现在

1
2
3
4
5
6
7
8
9
10
status_t DisplayListRenderer::drawRenderNode(RenderNode* renderNode, Rect& dirty, int32_t flags) {
LOG_ALWAYS_FATAL_IF(!renderNode, "missing rendernode");

// dirty is an out parameter and should not be recorded,
// it matters only when replaying the display list
DrawRenderNodeOp* op = new (alloc()) DrawRenderNodeOp(renderNode, flags, *currentTransform());
addRenderNodeOp(op);

return DrawGlInfo::kStatusDone;
}

再回到我们之前的 CanvasContext.prepareTree 中提到的 mRootRenderNode->prepareTree(info),这时候这里的 mRootRenderNode 就是 CanvasContext 初始化是传进来的。

其实现在 RenderNode.cpp 中:

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
void RenderNode::prepareTree(TreeInfo& info) {
prepareTreeImpl(info);
}

void RenderNode::prepareTreeImpl(TreeInfo& info) {
TT_START_MARK(getName());
info.damageAccumulator->pushTransform(this);

if (info.mode == TreeInfo::MODE_FULL) {
pushStagingPropertiesChanges(info); //同步当前正在处理的Render Node的Property
}
uint32_t animatorDirtyMask = 0;
if (CC_LIKELY(info.runAnimations)) {
animatorDirtyMask = mAnimatorManager.animate(info);//执行动画相关的操作
}
prepareLayer(info, animatorDirtyMask);
if (info.mode == TreeInfo::MODE_FULL) {
pushStagingDisplayListChanges(info); //同步当前正在处理的Render Node的Display List
}
prepareSubTree(info, mDisplayListData); //同步当前正在处理的Render Node的Display List引用的Bitmap,以及当前正在处理的Render Node的子Render Node的Display List等信息
pushLayerUpdate(info); //检查当前正在处理的Render Node是否设置了Layer。如果设置了的话,就对这些Layer进行处理

info.damageAccumulator->popTransform();
TT_END_MARK();
}

这里所涉及到的进一步的具体操作大家可以自行去看代码。

2. draw

Draw

执行完syncFrameState之后,接下来就是执行draw

1
2
3
if (CC_LIKELY(canDrawThisFrame)) {
context->draw();
}

CanvasContext的draw函数是一个核心函数,其位置在 frameworks/base/libs/hwui/OpenGLRenderer.cpp ,其实现如下:

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
void CanvasContext::draw() {
profiler().markPlaybackStart();

SkRect dirty;
mDamageAccumulator.finish(&dirty);

......

status_t status;
if (!dirty.isEmpty()) {
status = mCanvas->prepareDirty(dirty.fLeft, dirty.fTop,
dirty.fRight, dirty.fBottom, mOpaque);
} else {
status = mCanvas->prepare(mOpaque);
}

Rect outBounds;
status |= mCanvas->drawRenderNode(mRootRenderNode.get(), outBounds);

profiler().draw(mCanvas);

mCanvas->finish();

profiler().markPlaybackEnd();

if (status & DrawGlInfo::kStatusDrew) {
swapBuffers();
}

profiler().finishFrame();

/// M: enable to get overdraw count
if (CC_UNLIKELY(g_HWUI_debug_overdraw)) {
if (!mDebugOverdrawLayer) {
mDebugOverdrawLayer = LayerRenderer::createRenderLayer(mRenderThread.renderState(),
mCanvas->getWidth(), mCanvas->getHeight());
} else if (mDebugOverdrawLayer->layer.getWidth() != mCanvas->getWidth() ||
mDebugOverdrawLayer->layer.getHeight() != mCanvas->getHeight()) {
if (!LayerRenderer::resizeLayer(mDebugOverdrawLayer, mCanvas->getWidth(), mCanvas->getHeight())) {
LayerRenderer::destroyLayer(mDebugOverdrawLayer);
mDebugOverdrawLayer = NULL;
}
}

......
}

2.1 eglBeginFrame

首先来看eglBeginFrame的实现

1
2
3
4
5
6
7
8
9
10
void EglManager::beginFrame(EGLSurface surface, EGLint* width, EGLint* height) {
makeCurrent(surface);
if (width) {
eglQuerySurface(mEglDisplay, surface, EGL_WIDTH, width);
}
if (height) {
eglQuerySurface(mEglDisplay, surface, EGL_HEIGHT, height);
}
eglBeginFrame(mEglDisplay, surface);
}

makeCurrent是用来管理上下文,eglBeginFrame主要是校验参数的合法性。

2.2 prepareDirty

1
2
3
4
5
6
7
8
status_t status;
if (!dirty.isEmpty()) {
status = mCanvas->prepareDirty(dirty.fLeft, dirty.fTop,
dirty.fRight, dirty.fBottom, mOpaque);
} else {
status = mCanvas->prepare(mOpaque);
}

这里的mCanvas是一个OpenGLRenderer对象,其prepareDirty实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//TODO:增加函数功能描述
status_t OpenGLRenderer::prepareDirty(float left, float top,
float right, float bottom, bool opaque) {
setupFrameState(left, top, right, bottom, opaque);

// Layer renderers will start the frame immediately
// The framebuffer renderer will first defer the display list
// for each layer and wait until the first drawing command
// to start the frame
if (currentSnapshot()->fbo == 0) {
syncState();
updateLayers();
} else {
return startFrame();
}

return DrawGlInfo::kStatusDone;
}

2.3 drawRenderNode

1
2
Rect outBounds;
status |= mCanvas->drawRenderNode(mRootRenderNode.get(), outBounds);

接下来就是调用OpenGLRenderer的drawRenderNode方法进行绘制

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
status_t OpenGLRenderer::drawRenderNode(RenderNode* renderNode, Rect& dirty, int32_t replayFlags) {
status_t status;
// All the usual checks and setup operations (quickReject, setupDraw, etc.)
// will be performed by the display list itself
if (renderNode && renderNode->isRenderable()) {
// compute 3d ordering
renderNode->computeOrdering();
if (CC_UNLIKELY(mCaches.drawDeferDisabled)) { //判断是否不重排序
status = startFrame();
ReplayStateStruct replayStruct(*this, dirty, replayFlags);
renderNode->replay(replayStruct, 0);
return status | replayStruct.mDrawGlStatus;
}

// 需要重新排序
bool avoidOverdraw = !mCaches.debugOverdraw && !mCountOverdraw; // shh, don't tell devs!
DeferredDisplayList deferredList(*currentClipRect(), avoidOverdraw);
DeferStateStruct deferStruct(deferredList, *this, replayFlags);
renderNode->defer(deferStruct, 0); //递归进行重排操作

flushLayers(); // 首先执行设置了 Layer 的子 Render Node 的绘制命令,以便得到一个对应的FBO
status = startFrame(); //执行一些诸如清理颜色绘冲区等基本操作
status = deferredList.flush(*this, dirty) | status;
return status;
}

// Even if there is no drawing command(Ex: invisible),
// it still needs startFrame to clear buffer and start tiling.
return startFrame();
}

这里的 renderNode 是一个 Root Render Node,

可以看到,到了这里虽然只是开始,但是其实已经结束了,这个函数里面最重要的几步:

1
2
3
4
5
6
renderNode->defer(deferStruct, 0); //进行重排序

flushLayers(); 首先执行设置了 Layer 的子 Render Node 的绘制命令,以便得到一个对应的FBO

status = deferredList.flush(*this, dirty) | status; //对deferredList中的绘制命令进行真正的绘制操作

这几个是渲染部分真正的核心部分,其中的代码细节需要自己去研究。老罗在这部分讲的很细,有空可以去看看他的文章Android应用程序UI硬件加速渲染的Display List渲染过程分析.

2.4 swapBuffers

1
2
3
if (status & DrawGlInfo::kStatusDrew) {
swapBuffers();
}

其核心就是调用EGL的 eglSwapBuffers(mEglDisplay, surface), duration)函数。

2.5 FinishFrame

1
profiler().finishFrame();

主要是记录时间信息。

总结

鉴于我比较懒,而且总结能力不如老罗,就直接把他的总结贴过来了。
RenderThread的总的流程如下:

  1. 将Main Thread维护的Display List同步到Render Thread维护的Display List去。这个同步过程由Render Thread执行,但是Main Thread会被阻塞住。
  1. 如果能够完全地将Main Thread维护的Display List同步到Render Thread维护的Display List去,那么Main Thread就会被唤醒,此后Main Thread和Render Thread就互不干扰,各自操作各自内部维护的Display List;否则的话,Main Thread就会继续阻塞,直到Render Thread完成应用程序窗口当前帧的渲染为止。
  1. Render Thread在渲染应用程序窗口的Root Render Node的Display List之前,首先将那些设置了Layer的子Render Node的Display List渲染在各自的一个FBO上,接下来再一起将这些FBO以及那些没有设置Layer的子Render Node的Display List一起渲染在Frame Buffer之上,也就是渲染在从Surface Flinger请求回来的一个图形缓冲区上。这个图形缓冲区最终会被提交给Surface Flinger合并以及显示在屏幕上。

第2步能够完全将Main Thread维护的Display List同步到Render Thread维护的Display List去很关键,它使得Main Thread和Render Thread可以并行执行,这意味着Render Thread在渲染应用程序窗口当前帧的Display List的同时,Main Thread可以去准备应用程序窗口下一帧的Display List,这样就使得应用程序窗口的UI更流畅。

注意最后一段,在 Android 4.x 时代,没有RenderThread的时代,只有 Main Thread ,也就是说 必须要等到 Draw 完成后,才会去准备下一帧的数据,如下图:

Paste_Image.png

Android5.0 之后,如老罗所说,有两种情况,

Main Thread 和 Render Thread

Render Thread 提前唤醒了 Main Thread

可以看到第二张图中,Render Thread 并没有绘制完成,但是由于其提前唤醒了 Main Thread ,所以 Main Thread 在下一个Vsync信号到来的时候,响应了Vsync事件,开始准备下一帧。
此时虽然由于第一帧绘制时间过长,导致掉了一帧,但是第二帧没有收到任何影响。

关于我 && 博客

下面是个人的介绍和相关的链接,期望与同行的各位多多交流,三人行,则必有我师!

  1. 博主个人介绍 :里面有个人的微信和微信群链接。
  2. 本博客内容导航 :个人博客内容的一个导航。
  3. 个人整理和搜集的优秀博客文章 - Android 性能优化必知必会 :欢迎大家自荐和推荐 (微信私聊即可)
  4. Android性能优化知识星球 : 欢迎加入,多谢支持~

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

白嫖阿里云99元服务器,初尝 Portainer 部署 Docker

没错,这篇文章的标题是小胡同学开玩笑时帮我起的。双十一阿里的活动机最为火爆款应该就是2C2G3M的这款99元机了。这么便宜的小鸡还能白嫖?是真的可以白嫖。这个信息是韩宇发面上群里的,有兴趣的可以看一下,https://docs.qq.com/doc/DTEFhbU5CanZoTG1o 。这位大佬是推广500台,目前好像还有不到一百台了。开始我对这项活动表示怀疑的,哪不知上午购买的,下午就返现给我了。白嫖的东西,不拿白不拿。

因为手里没有备案的域名,那这白嫖的阿里云服务器拿来干什么?那就拿来跑Docker项目吧!

大家知道Docker是全命令,没有UI界面的,像我这样的小白,当然是想搞一个Docker中文管理面板了,

一.目前主流的Docker中文管理面板及区别

  1. Portainer:Portainer是一款知名的开源Docker管理工具,提供了友好的用户界面,支持多种语言,包括中文。它支持多种Docker环境,包括Docker Swarm、Kubernetes和Docker单机模式。Portainer可以与Docker轻松安装在独立的Linux/Windows服务器/集群上,可以管理注册表、网络、卷、镜像和容器,还可以保存配置,并配置Docker Swarm和堆栈
  2. FAST OS DOCKER:FAST OS DOCKER是一款国产中文面板,界面简洁,功能不错,尚在完善开发中。它可以为用户提供docker总览、本地容器管理、远程镜像拉取、服务器磁盘映射、服务器网络管理等功能,基本能满足中小型单位对容器管理的全部需求
  3. Docker UI:Docker UI是一个使用Docker Remote API的web接口,目的是提供一个简洁纯净的客户端实现,为了连接和管理Docker。然而,该工具目前已经无人维护,因此建议使用其他工具如Portainer。

综上所述,Portainer是一个功能齐全的开源Docker管理工具,FAST OS DOCKER是一款国产中文面板,而Docker UI则是一个已经无人维护的工具。

二.Docker环境安装

我的服务器环境是Ubuntu,下面介绍的是如何在Ubuntu上安装 Docker。

1.更新软件包索引,并且安装必要的依赖软件,来添加一个新的 HTTPS 软件源:

sudo apt update sudo apt install apt-transport-https ca-certificates curl gnupg-agent software-properties-common

2.使用下面的 curl 导入源仓库的 GPG key:

curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
      3.将 Docker APT 软件源添加到你的系统:
sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"

4.安装 Docker 最新版本

sudo apt update sudo apt install docker-ce docker-ce-cli containerd.io

5.一旦安装完成,Docker 服务将会自动启动。你可以输入下面的命令,验证它:

sudo systemctl status docker

输出将会类似下面这样:

● docker.service - Docker Application Container Engine
     Loaded: loaded (/lib/systemd/system/docker.service; enabled; vendor preset: enabled)
     Active: active (running) since Thu 2020-05-21 14:47:34 UTC; 42s ago
...

三.安装portainer-ce中文版

大家一定要注意portainer-ce并没有原生的中文版,而是国内大佬进行汉化的。那安装方式有两种,一种是先安装英语版的portainer-ce,然后再安装汉化补丁。另外一种方式就是直接安装大佬做好的集成的portainer-ce中文版。我在这里踩了个大坑,白嫖阿里云99元服务器,初尝 Portainer 部署 Docker - 第1张图片

安装好英文版portainer-ce进行打汉化补丁,但是问题出来了,如上图出错提示。意思是portainer-ce连接不上Docker环境。英文版的可以连接,汉化版的却不行,那问题一定出现在汉化上面。后来通过群里往记同学的帮助,才找到问题所在。原来是汉化版数据文件路径和英文版的冲突而导致的。

所以,老张在这里推荐新手小白们,还是直接安装portainer-ce集成中文版。仓库地址:https://hub.docker.com/r/6053537/portainer-ce 。一键代码安装,傻瓜无脑操作。

docker run -d --restart=always --name="portainer" -p 9000:9000 -v /var/run/docker.sock:/var/run/docker.sock 6053537/portainer-ce

OK了,开始你的Docker折腾之旅吧!

❌