结合Android原生看Flutter渲染流程

写在前面

作为一名android开发人员接触学习flutter已有小半年了,无论是widget配置构建响应式ui的思想,dart vm hotReload的快捷,还是各种devtools带来的开发便利都让我体会到flutter不同于其他跨平台框架的强大的魅力。深入了解了flutter的渲染流程才大致明白了flutter宣称的媲美原生的流畅度是如何做到的。下面对比着android原生看下flutter framework是如何做ui渲染的。

UI渲染

Android 上无论是原生应用,视频解码播放还是 FLutter 渲染都离不开Surface。UI 渲染本质上就是图像的生产者生产图像流,图像的消费方消费图像流,由于两端处于不同进程有不同的运行速度两者数据传递就需要用到缓冲区,在安卓上这个缓冲区对应的就是BufferQueueSurface是一个接口,供生产方与使用方交换缓冲区。下图展示了生产者消费者通过对缓冲区的操作实现数据的传递。

图片

一个 android 应用一个典型的渲染流程就是:当 app 进入前台时,WindowManager服务会向SurfaceFlinger请求一个绘图SurfaceSurfaceFlinger会创建一个其主要组件为BufferQueue的层,而SurfaceFlinger是其消耗方。生产方端的Binder对象通过WindowManager传递到应用,然后应用可以开始直接将帧发送到SurfaceFlinger。应用通过 Vysnc 触发绘制流程,经过measure , layout, draw等流程最后通过栅格化把代码表示的view 结构对应的display list转化为像素数据(支持硬件加速栅格化会通过 opengl es 调用 gpu 执行,不持支的通过 skia 在 cpu 中执行,现在的手机都有硬件加速了所以文章后面都是围绕着有硬件加速展开的),随后提交到BufferQueue中等待消费。大多数应用在屏幕上一次显示三个层:屏幕顶部的状态栏、底部或侧面的导航栏以及应用界面,SurfaceFlingers收到所有层后会和Hardware Composer一起完成层的合成工作最后交给显示屏显示。

图片

Flutter 是使用SurfaceView依附于 android 原生之上,他的绘制流程不同的地方是在于Vysnc 触发后进行自己的animate, build, layout, paint等步骤根据我们构建的widget tree生成的rendering tree转化为一系列绘制指令最后根据指令栅格化为像素数据。也就是说更换了生产图片流的方式,视频播放也是一样不过生产方是用对应解码器从视频文件流中解码出一帧桢图片像素数据。

总的来说 android/flutter 渲染就是要把我们代码里写的 view/widget 对应的 ui 结构树转化为绘制指令集,再经过 gpu 或者 cpu 栅格化转化为像素数据用于显示。

开始渲染!

我所理解的 UI 渲染一般分为EventLayoutDraw三个阶段:

  • Event:用户做了某些操作或收到系统某些指令触发绘制流程,如我们点击按钮,修改控件显示文本等等。
  • Layout:绘制之前对控件树进行尺寸测量和位置布局等。
  • Draw:更新或重制绘制指令集,格栅化,显示等

Android原生

在android中渲染都是由 Vysnc 驱动的。Vysnc 信号由硬件产生经过SurfaceFlinger转发到应用线程的Choreographer中,通过 ui 线程的 handler 切换线程调用doFrame方法开始每一帧的渲染流程:

void doFrame(long frameTimeNanos, int frame) {
    ...
    doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
    doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
    doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
    doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
    ...
}

该方法会依次处理4个 callback

  • INPUT:输入事件
  • ANIMATION:动画
  • TRAVERSAL:窗口刷新,执行 measure/layout/draw 操作
  • COMMIT:遍历完成的提交操作,用来修正动画启动时间

渲染主要看的是TRAVERSAL,这个 callback 是由ViewRootImpl添加的,对应的是ViewRootImpl中的TraversalRunnable

final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        doTraversal();
    }
}

doTraversal最终会调用performTraversals方法

private void performTraversals() {
  ...
  performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
  ...
  performLayout(lp, mWidth, mHeight);
  ...
  performDraw();
}

该方法中会依次调用performMeasure/Layout/Draw方法开始绘制流程。

Event 阶段

Chorepgrapher并不会一直接收到Vsync信号进行重绘,只有一定的事件发生才会去接收信号这样避免了无用的渲染。在Event阶段要做的就是告诉系统我需要在下一帧重绘,下一个Vsync信号到来时才会触发ChoreographerdoFrame方法。

看一个简单的例子

图片

最外层是DecorView,他是所有View流程的起点。navigationbarBackgorundstatusBarBackground就像前面所说的一样不属于app层,是由 systemui 渲染后 和 app 的层合并显示的。

当我们调用setText重新设置TextView显示文字

图片

根据调用栈可以发现settext后会分别调用requestLayoutinvalidateInternal方法

public void requestLayout() {
    ...
    mPrivateFlags |= PFLAG_FORCE_LAYOUT;
    mPrivateFlags |= PFLAG_INVALIDATED;
    if (mParent != null && !mParent.isLayoutRequested()) {
        mParent.requestLayout();
    }
    ...
}
void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,boolean fullInvalidate) {
    ...
    if (...) {
        if (fullInvalidate) {
            mLastIsOpaque = isOpaque();
            mPrivateFlags &= ~PFLAG_DRAWN;
        }
        mPrivateFlags |= PFLAG_DIRTY;
        if (invalidateCache) {
            mPrivateFlags |= PFLAG_INVALIDATED;
            mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
        }
        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);
            p.invalidateChild(this, damage);
        }
        ...
    }
}

这2个方法基本都是对 view 的mPrivateFlags标志位进行标记,会标记这个view及其父类,子类是否需要重新布局和重新绘制。

图片

继续往里走发现调用了DisplayEventReceivernativeScheduleVsync方法,往下就是C++层的代码,后面的具体作用就是通过binder和SurfaceFlinger通信,告诉 SurfaceFlinger 我需要在下一帧重绘,当硬件发送Vsync消息给SurfaceFlinger后SurfaceFlinger会通知应用,最后就到了ChorepgrapherdoFrame方法开始绘制流程。完成了Event阶段到Layout阶段的切换。

图片

Layout 阶段

Android原生上这个阶段对应的就是 View 的 Measure 和 Layout 阶段。

  • measure 确定 view 的测量宽/高(mMeasureWidth/mMeasureHeight)
  • layout 确定 view 的最终宽/高和四个顶点位置(mLeft,mTop,mRight.mBottom)

Measure

measure要分view和viewgroup两种情况,view通过measure完成自己的测量,viewgroup除了完成自己的测量外还要遍历调用所有子view的measure方法,各个child view/viewgroup 递归去执行这个流程。

View measure

先看下View的measure方法:

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    ...
    final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
    ...
    final boolean needsLayout = specChanged
            && (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);
    ...
    if (forceLayout || needsLayout) {
        ...
        onMeasure(widthMeasureSpec, heightMeasureSpec);
        ...
        mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
    }
    ...
}
  • 参数widthMeasureSpecheightMeasureSpec,是 parent view 根据 child view 的LayoutParam 和自身逻辑对 view 的尺寸要求,如要求指定尺寸或者最大尺寸等等
  • forceLayout:这里可以看到是对mPrivateFlags标志位PFLAG_FORCE_LAYOUT的判断,其实这个PFLAG_FORCE_LAYOUT就是Event阶段我们在requestLayout方法时候设置的,
  • onMeasure方法:forcelayout为true或者满足一些条件触发,如parent view传的measureSpec有变换等等
  • 设置PFLAG_LAYOUT_REQUIRED标志位用于后面的layout方法

onMeasure方法:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
protected int getSuggestedMinimumWidth() {
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
    mMeasuredWidth = measuredWidth;
    mMeasuredHeight = measuredHeight;
    mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}

onMeasure方法大部分子类都会重写,靠自己的规则测量出view的大小(如TextView会根据文字的排版确定尺寸),如果没有重写就比较简单根据背景图宽高和设置的最小宽高属性和measureSpec确定最终的测量大小并给mMeasuredWidth/mMeasuredHeight(这里只是测量尺寸,具体的尺寸还要layout后才能确定,但是绝大分测量尺寸就是view的最终尺寸,getMeasureWidth/getWidth)赋值,单个view的measure流程就完成了。

ViewGroup measure

再看看拥有子view的ViewGroup的measure流程,由于measure方法是final的子view并不能重写,所以measure方法都是一样的,所以直接从onMeasure开始看,这里以FramLayout为例:

图片

onMeasure主要做2件事

  • 传递尺寸要求调用所有child viewmeasure方法确定所有view的大小(每个child view又会调用自己的onMeasure方法测量自己的)
  • 根据子view的最大宽高来调用setMeaureDimension来确定FrameLayout的宽高

onMeasure会遍历所有child view并把child view 作为参数调用到ViewGroup 的 measureChildWithMargins方法:

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

可以看到根据getChildMeasureSpec方法生成childWidthMeasureSpec/childHeightMeasureSpec作为参数调用child view的measure方法开始递归完成所有view树所有view的measure方法。
(getChildMeasureSpec方法参数传递了LayoutParam的width/height,也就是上文说说的 parent view 根据 child view 的LayoutParam 和自身逻辑生成对 view 的尺寸要求 measureSpec)

所有measure的起点则是上文所说的doFrame后调用的ViewRootImpl的performMeasure方法

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
  ...
        mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
  ...
}

mView其实就是DecorView,DecorView就是一个FramLayout,就是从它开始整个measure流程。

Layout

Layout的作用是确定view的位置,通过调用view的layout方法来确定该view的位置,layout会调用onLayout,view通过重写onLayout方法就可以响应view位置的变化,如果是ViewGroup还要负责在onLayout中遍历所有的子元素并调用其layout方法种当ViewGroup的位置被确定后,它 onLayout 中会遍历所有的子元素并调用其layout方法。完成所有view 的layout

先看下layout方法

public void layout(int l, int t, int r, int b) {
  ...
  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;
  }
  ...
  mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
  ...
}
  • l,t,r,b 就是parent view对这个view确定的位置
  • 调用setFrame方法对比定点位置和原来的位置有没有变化
  • 如果顶点位置有变化或者设置PFLAG_LAYOUT_REQUIRED标志位(measure方法里设置的)触发onLayout方法
  • mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED, mPrivateFlags &= ~PFLAG_FORCE_LAYOUT,应为到这layout方法已经走完了所以得还原以该view前设置的标记位

继续看onLayout方法,onLayout要区分view和viewgroup,view的onLayout是根据确定的大小做些内部逻辑的调整,viewgroup还得在onLayout中调用child view 的layout方法,以FrameLayout的onLayout方法为例:

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}
void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
        final int count = getChildCount();
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                final int width = child.getMeasuredWidth();
                final int height = child.getMeasuredHeight();
                int childLeft;
                int childTop;
                int gravity = lp.gravity;
                if (gravity == -1) {
                    gravity = DEFAULT_CHILD_GRAVITY;
                }
                final int layoutDirection = getLayoutDirection();
                final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
                ...
                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                    case Gravity.CENTER_HORIZONTAL:
                        childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
                        lp.leftMargin - lp.rightMargin;
                        break;
                    case Gravity.RIGHT:
                        if (!forceLeftGravity) {
                            childLeft = parentRight - width - lp.rightMargin;
                            break;
                        }
                    case Gravity.LEFT:
                    default:
                        childLeft = parentLeft + lp.leftMargin;
                }
                ...
                child.layout(childLeft, childTop, childLeft + width, childTop + height);
            }
        }
    }

在layoutChildren方法中我们可以看到FrameLayout根据child view 在measure的时候确定的测量大小和gravity确定child view的4个顶点,然后调用child 的 layout方法开始child view 的layout流程,因为view的实际大小其实是由4个顶点决定的所以说view的实际大小不一定等于测量尺寸mMeasureWidth/Height。
layout的起点则是后调用的ViewRootImpl的performLayout方法

 private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, int desiredWindowHeight) {
    ...
    final View host = mView;
    if (host == null) {
      return;
    }
    ...
    host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
    ...
  }

用0,0点DecoerView的测量宽高作为参数调用DecoerView的layout方法开始整个view 的layout流程。
经过上诉2个过程就确定了每个view的大小和位置就可以开始绘制流程了,通过ViewRootImpl的perfornDraw进入到Draw阶段

Draw 阶段

图片

从上面的调用栈可以看到有涉及到ThreadedRenderer,RenderNode,****RecordingCanvas,****DisplayList 等偏底层类,这几个类的大致作用如下:

  • ThreadedRenderer是连接java/c++层,ui线程和渲染线程的代理类
  • RecordingCanvas记录绘制过程最后生成DisplayList
  • DisplayList就是实际存放绘制指令的类
  • RenderNode则是连接View和对应的DisplayList,每个View都有自己对应的RenderNode

触发了ViewRootImpl的draw后会调用到ThreadedRendererdraw方法,然后调用updateRootDisplayList把DecorView作为参数传递到updateViewTreeDisplayList开始view的重绘过程。

private void updateViewTreeDisplayList(View view) {
    view.mPrivateFlags |= View.PFLAG_DRAWN;
    view.mRecreateDisplayList = (view.mPrivateFlags & View.PFLAG_INVALIDATED)
            == View.PFLAG_INVALIDATED;
    view.mPrivateFlags &= ~View.PFLAG_INVALIDATED;
    view.updateDisplayListIfDirty();
    view.mRecreateDisplayList = false;
}

设置一些flag然后调用view的updateDisplayListIfDirty

public RenderNode updateDisplayListIfDirty() {
    final RenderNode renderNode = mRenderNode;
    ...
    if ((mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == 0
            || !renderNode.hasDisplayList()
            || (mRecreateDisplayList)) {
        if (renderNode.hasDisplayList()
                && !mRecreateDisplayList) {
            mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID;
            mPrivateFlags &= ~PFLAG_DIRTY_MASK;
            dispatchGetDisplayList();
            return renderNode; 
        }
        mRecreateDisplayList = true;
        ...
        final RecordingCanvas canvas = renderNode.beginRecording(width, height);
        try {
        ...
                computeScroll();

                canvas.translate(-mScrollX, -mScrollY);
                mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID;
                mPrivateFlags &= ~PFLAG_DIRTY_MASK;
                if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
                    dispatchDraw(canvas);
                    drawAutofilledHighlight(canvas);
                    if (mOverlay != null && !mOverlay.isEmpty()) {
                        mOverlay.getOverlayView().draw(canvas);
                    }
                    if (debugDraw()) {
                        debugDrawFocus(canvas);
                    }
                } else {
                    draw(canvas);
                }
        ...
        } finally {
            renderNode.endRecording();
            setDisplayListProperties(renderNode);
        }
    } else {
        mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID;
        mPrivateFlags &= ~PFLAG_DIRTY_MASK;
    }
    return renderNode;
}
  • 根据view的RenderNode是否已经持有DisplayListmRecreateDisplayList(是否需要重建DisplayList标记) 判断是否需要触发重建DiplayList
  • beginRecording新建用于记录绘制的RecordingCanvas
  • RecordingCanvas作为参数调用view.draw(canvas)
  • endRecording结束绘制记录

draw方法是被beginRecordingendRecording包围,就有点像由系统beginRecording给view一张纸(RecordingCanvas),view通过draw方法在上面写如需要在什么位置需要绘制某个图形的指令等等,最后endRecording时后系统回收纸生成对应的DisplayList

再回头看下draw方法,和measure方法类似,子view的draw都是由parent view调用的,parent view需要通过dispatchDraw调用所有child view的draw方法,

view的draw方法是有两种

boolean draw(Canvas canvas, ViewGroup parent, long drawingTime){
 ...
if (hardwareAcceleratedCanvas) {
    mRecreateDisplayList = (mPrivateFlags & PFLAG_INVALIDATED) != 0;
    mPrivateFlags &= ~PFLAG_INVALIDATED;
}
...
if (drawingWithRenderNode) {
    renderNode = updateDisplayListIfDirty();
...
}
...
}
public void draw(Canvas canvas){
  ...
  drawBackground(canvas);
  ...
  onDraw(canvas);
  ...
  dispatchDraw(canvas);
  ...
}
  • 第一个是由ViewGroup.drawChild()调用,主要处理和RenderNode等渲染有关的东西,并调用updateDisplayListIfDirty方法。可以看到在updateDisplayListIfDirty中触发重建displayList的mRecreateDisplayList是由 PFLAG_INVALIDATED决定的,这个标志位正好就是Event 阶段调用requestLayout或者in****validate设置的
  • 第二个是view在updateDisplayListIfDirty中被调用的。我们熟知的onDraw也是在这里被调用的为了防止歧义下文中暂且命名这个方法为draw_inner

用下图表述下2个draw之间的关系:
图片

ViewGroup的draw被parent view调用后根据mPrivateFlags等决定是否要重建DisplayList,如果需要就会新建RecordingCanvas作为参数调用draw_inner,然后调用onDraw,平常做自定义控件会重写onDraw方法时调用canvas.drawXXX等等api,这个canvas就是RecordingCanvas,它的左右就是记录绘制的流程,onDraw结束后会调用dispatchDraw遍历child view调用每个view的draw方法,完成所有view的绘制,整个流程的起点还是DecoverView,在上文中updateViewTreeDisplayList方法中开始的。

当所有view 完成绘制了后会调用endRecording方法开始生成对应的DisplayList

RenderNode.java

public void endRecording() {
    RecordingCanvas canvas = mCurrentRecordingCanvas;
    mCurrentRecordingCanvas = null;
    long displayList = canvas.finishRecording();
    nSetDisplayList(mNativeRenderNode, displayList);
    canvas.recycle();
}

RecordingCanvas.java

long finishRecording() {
    return nFinishRecording(mNativeCanvasWrapper);
}

结束绘制返回生成的DisplayList

RecordingCanvas.cpp

DisplayList* RecordingCanvas::finishRecording() {
    restoreToCount(1);
    mPaintMap.clear();
    mRegionMap.clear();
    mPathMap.clear();
    DisplayList* displayList = mDisplayList;
    mDisplayList = nullptr;
    mSkiaCanvasProxy.reset(nullptr);
    return displayList;
}

finishRecording后调用nSetDisplayList方法关联RenderNode(Java RenderNode 对应的C++类)和对应的DisplayList(C++)

nSetDisplayList方法

android_view_RenderNode.cpp

static void android_view_RenderNode_setDisplayList(JNIEnv* env,
        jobject clazz, jlong renderNodePtr, jlong displayListPtr) {
     RenderNode* renderNode = reinterpret_cast<RenderNode*>(renderNodePtr);
     DisplayList* newData = reinterpret_cast<DisplayList*>(displayListPtr);
     renderNode->setStagingDisplayList(newData);
 }

记录完绘制指令后会调用syncAndDrawFrame开始格栅化和交换buffer与SurfaceFlinger通信阶段

图片
public int syncAndDrawFrame(@NonNull FrameInfo frameInfo) {
    return nSyncAndDrawFrame(mNativeProxy, frameInfo.frameInfo, frameInfo.frameInfo.length);
}

然后会依次调用如下c++类的方法

void CanvasContext::draw()
{
    ...
    bool drew = mRenderPipeline->draw(frame, windowDirty, dirty, mLightGeometry, &mLayerUpdateQueue,
                                        mContentDrawBound, mOpaque, mLightInfo, mRenderNodes, &(profiler()));
    ...
    bool didSwap = mRenderPipeline->swapBuffers(frame, drew, windowDirty, mCurrentFrameInfo,
                                                    &requireSwap);
    ...
}

mRenderPipelinedrawswapBuffers方法就是对应的格栅化和交换 buffer了。到此 Android 原生端的绘制流程就算结束了。

图片

Flutter

FrameWork构架

Flutter拥有自己的开发工具,开发语言、虚拟机,编译机制,线程模型和渲染管线,和Android相比,它也可以看做一个小型的OS了。

先看下flutter framework的构架图

图片

从下到上依次:

Embedder

不同的操作系统有不同的embedder,它负责为fluuter提供运行的入口,提供像rendering surface,输入系统,管理message event loop等服务,甚至可以根据embedder api移植到不同的系统上,如非官方支持desktop的https://github.com/go-flutter-desktop/go-flutter

Flutter engine

作为flutter app的基石,提供dart runtime ,文字图像绘制,栅格化,文件,网络等功能,通过dart:ui像framework层提供一些底层的api

Framework

我们开发过程中接触最多的就是framework层,给我们提供了不同风格的控件,动画。手势,绘制等支持,管理我们配置的widget tree,处理一部分渲染流程,本文主要也是在这层展开。

未完待续。。。

参考:

一颗像素的诞生:

https://mp.weixin.qq.com/s/QoFrdmxdRJG5ETQp5Ua3-A

Flutter architectural-overview:

https://flutter.cn/docs/resources/architectural-overview#widgets

Choreographer原理

http://gityuan.com/2017/02/25/choreographer/

Android N中UI硬件渲染(hwui)的HWUI_NEW_OPS(基于Android 7.1)

https://blog.csdn.net/jinzhuojun/article/details/54234354

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,393评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,790评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,391评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,703评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,613评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,003评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,507评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,158评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,300评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,256评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,274评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,984评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,569评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,662评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,899评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,268评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,840评论 2 339