Android UI-测量-布局-绘制-机制

Window

  • android中 Window 是 View 的容器
  • 一个window 有一个surface

window 的类型

每一种窗口类型定义了一种对应的type

  • 系统Window (type范围是2000以上)
    常见的系统Window有哪些呢?比如在手机电量低的时候,会有一个提示电量低的Window,我们输入文字的时候,会弹出输入法Window,还有搜索条Window,来电显示Window,Toast对应的Window,可以总结出来,系统Window是独立与我们的应用程序的,对于应用程序而言,我们理论上是无法创建系统Window,因为没有权限,这个权限只有系统进程有。

  • 应用程序Window(type范围是1~99)
    所谓应用窗口指的就是该窗口对应一个Activity,因此,要创建应用窗口就必须在Activity中完成了。本节后面会分析Activity对应的Window的创建过程。

  • 子Window(type范围是1000~1999)
    所谓的子Window,是说这个Window必须要有一个父窗体,比如PopWindow,Dialog是属于应用程序Window,这个比较特殊。

window 的 Z-order

  • 在wms中,用windowState来保存window的所有属性,其中mLayer表示窗口在Z轴的位置,mLayer值越小,窗口越靠后,mLayer值越大,窗口越靠前,最前面的一个窗口就作为焦点窗口,可以接收触摸事件。因为窗口的切换,切换后的Z序(窗口的显示次序称为 Z 序)就可能不同,所以mLayer的值不是固定不变的。mLayer是通过WindowState的另一个成员变量mBaseLayer的值计算得到,mBaseLayer的值是固定不变的,只和窗口类型有关。mBaseLayer(称为主序)是WindowState的构造方法中赋值,等价与:mBaseLayer =窗口类型×10000+1000。
  • SubLayer(称为子序):SubLayer值是用来描述一个窗口是否属于另外一个窗口的子窗口,或者说SubLayer值是用来确定子窗口和父窗口之间的相对位置的。

window 的添加过程

三大类窗口的添加过程会有所不同,但是后续的过程如下

  1. WindowManagerImpl -> addView
  2. WindowManagerGlobal -> addView
  3. ViewRootImpl -> setView
  4. WindowSession -> addToDisplay Binder方式和wms通信
  5. wms -> addWindow
    • 窗口添加权限校验
    • 检查特殊窗口attr.token和attr.type的一致性
    • 创建窗口对象
    • 调用adjustWindowParamsLw对窗口参数进行调整
    • 创建pipe,用于输入消息的传递 (向InputManagerService注册InputChannel)
    • 调用窗口的attach,初始化SurfaceSession相关的变量,将窗口win放到mWindowMap中
    • 如果type == TYPE_APPLICATION_STARTING ,说明这个是启动窗口,把win赋值给token.appWindowToken.startingWindow
    • 添加窗口到Windows列表,确定窗口的位置
    • 窗口已经添加了,调用assignLayersLocked调整一下层值

ViewRootImpl 职责

  • 负责为应用程序窗口视图创建Surface

调用链

测量

测量模式

  • MeasureSpec.EXACTLY:精确大小,父容器已经测量出所需要的精确大小,这也是我们 childView 的最终大小 — MATCH_PARENT
  • MeasureSpec.AT_MOST: 最终的大小不能超过我们的父容器 — WRAP_CONTENT
  • UNSPECIFIED:不确定的,源码内部使用,一般在 ScorllView、ListView 中能看到这些,需要动态测量

measureSpec 的定义

总共为32bit,4字节,高位2bit为测量模式,低位30bit为size大小

measure.png
mesure-talbe.jpg

避免二次测量

布局

绘制

绘制分为软件绘制和硬件加速绘制, 4.+之后的手机一般都支持硬件加速,而且在添加窗口的时候,ViewRootImpl会enableHardwareAcceleration开启硬件加速,new HardwareRenderer,并初始化硬件加速环境。

private void enableHardwareAcceleration(WindowManager.LayoutParams attrs) {

    
    // Try to enable hardware acceleration if requested
    final boolean hardwareAccelerated =
            (attrs.flags & WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED) != 0;
   if (hardwareAccelerated) {
        ...
            
            mAttachInfo.mHardwareRenderer = HardwareRenderer.create(mContext, translucent);
            if (mAttachInfo.mHardwareRenderer != null) {
                mAttachInfo.mHardwareRenderer.setName(attrs.getTitle().toString());
                mAttachInfo.mHardwareAccelerated =
                        mAttachInfo.mHardwareAccelerationRequested = true;
            }
        ...
软件绘制

利用Surface.lockCanvas,向SurfaceFlinger申请一块匿名共享内存内存分配,同时获取一个普通的SkiaCanvas,用于调用Skia库,进行图形绘制,遍历view节点树调用他们的onDraw方法

private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
            boolean scalingRequired, Rect dirty) {
        final Canvas canvas;
        try {
            
            canvas = mSurface.lockCanvas(dirty);
            ..
            
                 mView.draw(canvas);
             ..
             关键点3 通知SurfaceFlinger进行图层合成
                surface.unlockCanvasAndPost(canvas);
            }   ...         
           return true;  }

默认情况下Skia的绘制没有采用GPU渲染的方式(虽然Skia也能用GPU渲染),也就说默认drawSoftware工作完全由CPU来完成,不会牵扯到GPU的操作,但是8.0之后,Google逐渐加重了Skia,开始让Skia接手OpenGL,间接统一调用,将来还可能是Skia同Vulkan的结合。

硬件绘制

硬件加速绘制包括两个阶段:构建阶段+绘制阶段,所谓构建就是递归遍历所有视图,将需要的操作缓存下来,之后再交给单独的Render线程利用OpenGL渲染。在Android硬件加速框架中,View视图被抽象成RenderNode节点,View中的绘制都会被抽象成一个个DrawOp(DisplayListOp),比如View中drawLine,构建中就会被抽象成一个DrawLintOp,drawBitmap操作会被抽象成DrawBitmapOp,每个子View的绘制被抽象成DrawRenderNodeOp,每个DrawOp有对应的OpenGL绘制命令,同时内部也握着绘图所需要的数据。如下所示:


drawop.png

在UI线程中完成DrawOp集构建

render-node-arch.png

RenderNode 存在于View中,且缓存了了DisplayOp。

ThreadedRenderer(Context context, boolean translucent) {
    ...
    
    long rootNodePtr = nCreateRootRenderNode();
    mRootNode = RenderNode.adopt(rootNodePtr);
    mRootNode.setClipToBounds(false);
    
    mNativeProxy = nCreateProxy(translucent, rootNodePtr);
    ProcessInitializer.sInstance.init(context, mNativeProxy);
    loadSystemProperties();
}

从上面代码看出,ThreadedRenderer中有一个RootNode用来标识整个DrawOp树的根节点,有个这个根节点就可以访问所有的绘制Op,同时还有个RenderProxy对象,这个对象就是用来跟渲染线程进行通信的句柄,看一下其构造函数:

RenderProxy::RenderProxy(bool translucent, RenderNode* rootRenderNode, IContextFactory* contextFactory)
        : mRenderThread(RenderThread::getInstance())
        , mContext(nullptr) {
    SETUP_TASK(createContext);
    args->translucent = translucent;
    args->rootRenderNode = rootRenderNode;
    args->thread = &mRenderThread;
    args->contextFactory = contextFactory;
    mContext = (CanvasContext*) postAndWait(task);
    mDrawFrameTask.setContext(&mRenderThread, mContext);  
   }

从RenderThread::getInstance()可以看出,RenderThread是一个单例线程,也就是说,每个进程最多只有一个硬件渲染线程,这样就不会存在多线程并发访问冲突问题,到这里其实环境硬件渲染环境已经搭建好好了。下面就接着看ThreadedRenderer的draw函数,如何构建渲染Op树:

@Override
void draw(View view, AttachInfo attachInfo, HardwareDrawCallbacks callbacks) {
    attachInfo.mIgnoreDirtyState = true;

    final Choreographer choreographer = attachInfo.mViewRootImpl.mChoreographer;
    choreographer.mFrameInfo.markDrawStart();
    
    updateRootDisplayList(view, callbacks);

    
    int syncResult = nSyncAndDrawFrame(mNativeProxy, frameInfo, frameInfo.length);
    ...
}

只关心关键点1 updateRootDisplayList,构建RootDisplayList,其实就是构建View的DrawOp树,updateRootDisplayList会进而调用根View的updateDisplayListIfDirty,让其递归子View的updateDisplayListIfDirty,从而完成DrawOp树的创建,简述一下流程:

private void updateRootDisplayList(View view, HardwareDrawCallbacks callbacks) {
    
    updateViewTreeDisplayList(view);
   if (mRootNodeNeedsUpdate || !mRootNode.isValid()) {
      
        DisplayListCanvas canvas = mRootNode.start(mSurfaceWidth, mSurfaceHeight);
        try {
        
            final int saveCount = canvas.save();
            canvas.translate(mInsetLeft, mInsetTop);
            callbacks.onHardwarePreDraw(canvas);

            canvas.insertReorderBarrier();
            canvas.drawRenderNode(view.updateDisplayListIfDirty());
            canvas.insertInorderBarrier();

            callbacks.onHardwarePostDraw(canvas);
            canvas.restoreToCount(saveCount);
            mRootNodeNeedsUpdate = false;
        } finally {
        
            mRootNode.end(canvas);
        }
    }
}

利用View的RenderNode获取一个DisplayListCanvas
利用DisplayListCanvas构建并缓存所有的DrawOp
将DisplayListCanvas缓存的DrawOp填充到RenderNode
将根View的缓存DrawOp设置到RootRenderNode中,完成构建


render-sequence.png

简单看一下View递归构建DrawOp

@NonNull
    public RenderNode updateDisplayListIfDirty() {
        final RenderNode renderNode = mRenderNode;
        ...
            // start 获取一个 DisplayListCanvas 用于绘制 硬件加速 
            final DisplayListCanvas canvas = renderNode.start(width, height);
            try {
                // 是否是textureView
                final HardwareLayer layer = getHardwareLayer();
                if (layer != null && layer.isValid()) {
                    canvas.drawHardwareLayer(layer, 0, 0, mLayerPaint);
                } else if (layerType == LAYER_TYPE_SOFTWARE) {
                    // 是否强制软件绘制
                    buildDrawingCache(true);
                    Bitmap cache = getDrawingCache(true);
                    if (cache != null) {
                        canvas.drawBitmap(cache, 0, 0, mLayerPaint);
                    }
                } else {
                      // 如果仅仅是ViewGroup,并且自身不用绘制,直接递归子View
                    if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
                        dispatchDraw(canvas);
                    } else {
                        
                        draw(canvas);
                    }
                }
            } finally {
                  
                renderNode.end(canvas);
                setDisplayListProperties(renderNode);
            }
        }  
        return renderNode;
    }

TextureView跟强制软件绘制的View比较特殊,有额外的处理,这里不关心,直接看普通的draw,假如在View onDraw中,有个drawLine,这里就会调用DisplayListCanvas的drawLine函数,DisplayListCanvas及RenderNode类图大概如下


render-func-call.png

DisplayListCanvas的drawLine函数最终会进入DisplayListCanvas.cpp的drawLine

void DisplayListCanvas::drawLines(const float* points, int count, const SkPaint& paint) {
    points = refBuffer(points, count);

    addDrawOp(new (alloc()) DrawLinesOp(points, count, refPaint(&paint)));
}

可以看到,这里构建了一个DrawLinesOp,并添加到DisplayListCanvas的缓存列表中去,如此递归便可以完成DrawOp树的构建,在构建后利用RenderNode的end函数,将DisplayListCanvas中的数据缓存到RenderNode中去:

public void end(DisplayListCanvas canvas) {
    canvas.onPostDraw();
    long renderNodeData = canvas.finishRecording();
    
    nSetDisplayListData(mNativeRenderNode, renderNodeData);
    // canvas 回收掉]
    canvas.recycle();
    mValid = true;
}

如此,便完成了DrawOp树的构建,之后,利用RenderProxy向RenderThread发送消息,请求OpenGL线程进行渲染。

负责跟渲染线程通信,RenderThread渲染UI到Graphic Buffer

DrawOp树构建完毕后,UI线程利用RenderProxy向RenderThread线程发送一个DrawFrameTask任务请求,RenderThread被唤醒,开始渲染,大致流程如下:

  1. 首先进行DrawOp的合并
  2. 接着绘制特殊的Layer
  3. 最后绘制其余所有的DrawOpList
  4. 调用swapBuffers将前面已经绘制好的图形缓冲区提交给Surface Flinger合成和显示。
硬件渲染绘制内存的申请

不过再这之前先复习一下绘制内存的由来,毕竟之前DrawOp树的构建只是在普通的用户内存中,而部分数据对于SurfaceFlinger都是不可见的,之后又绘制到共享内存中的数据才会被SurfaceFlinger合成,之前分析过软件绘制的UI是来自匿名共享内存,那么硬件加速的共享内存来自何处呢?到这里可能要倒回去看看ViewRootImlp

private void performTraversals() {
        ...
        if (mAttachInfo.mHardwareRenderer != null) {
            try {
                hwInitialized = mAttachInfo.mHardwareRenderer.initialize(
                        mSurface);
                if (hwInitialized && (host.mPrivateFlags
                        & View.PFLAG_REQUEST_TRANSPARENT_REGIONS) == 0) {
                    mSurface.allocateBuffers();
                }
            } catch (OutOfResourcesException e) {
                handleOutOfResourcesException(e);
                return;
            }
        }
      ....
      
/**
 * Allocate buffers ahead of time to avoid allocation delays during rendering
 * @hide
 */
public void allocateBuffers() {
    synchronized (mLock) {
        checkNotReleasedLocked();
        nativeAllocateBuffers(mNativeObject);
    }
}

可以看出,对于硬件加速的场景,请求SurfaceFlinger内存分配的时机会稍微提前,而不是像软件绘制,由Surface的lockCanvas发起,主要目的是:预先分配slot位置,避免在渲染的时候再申请,一是避免分配失败,浪费了CPU之前的准备工作,二是也可以将渲染线程个工作简化,减少延时。不过,还是会存在另一个问题,一个APP进程,同一时刻会有多个Surface绘图界面,但是渲染线程只有一个,那么究竟渲染那个呢?这个时候就需要将Surface与渲染线程(上下文)绑定。

static jboolean android_view_ThreadedRenderer_initialize(JNIEnv* env, jobject clazz,
        jlong proxyPtr, jobject jsurface) {
    RenderProxy* proxy = reinterpret_cast(proxyPtr);
    sp window = android_view_Surface_getNativeWindow(env, jsurface);
    return proxy->initialize(window);
}

首先通过android_view_Surface_getNativeWindowSurface获取Surface,在Native层,Surface对应一个ANativeWindow,接着,通过RenderProxy类的成员函数initialize将前面获得的ANativeWindow绑定到RenderThread

bool RenderProxy::initialize(const sp& window) {
    SETUP_TASK(initialize);
    args->context = mContext;
    args->window = window.get();
    return (bool) postAndWait(task);
}

仍旧是向渲染线程发送消息,让其绑定当前Window,其实就是调用CanvasContext的initialize函数,让绘图上下文绑定绘图内存:

bool CanvasContext::initialize(ANativeWindow* window) {
    setSurface(window);
    if (mCanvas) return false;
    mCanvas = new OpenGLRenderer(mRenderThread.renderState());
    mCanvas->initProperties();
    return true;
}

CanvasContext通过setSurface将当前要渲染的Surface绑定到到RenderThread中,大概流程是通过eglApi获得一个EGLSurface,EGLSurface封装了一个绘图表面,进而,通过eglApi将EGLSurface设定为当前渲染窗口,并将绘图内存等信息进行同步,之后通过RenderThread绘制的时候才能知道是在哪个窗口上进行绘制。这里主要是跟OpenGL库对接,所有的操作最终都会归结到eglApi抽象接口中去。假如,这里不是Android,是普通的Java平台,同样需要相似的操作,进行封装处理,并绑定当前EGLSurface才能进行渲染,因为OpenGL是一套规范,想要使用,就必须按照这套规范走。之后,再创建一个OpenGLRenderer对象,后面执行OpenGL相关操作的时候,其实就是通过OpenGLRenderer来进行的。


render-egl-sequence.png

上面的流程走完,有序DrawOp树已经构建好、内存也已分配好、环境及场景也绑定成功,剩下的就是绘制了,不过之前说过,真正调用OpenGL绘制之前还有一些合并操作,这是Android硬件加速做的优化,回过头继续走draw流程,其实就是走OpenGLRenderer的drawRenderNode进行递归处理:

void OpenGLRenderer::drawRenderNode(RenderNode* renderNode, Rect& dirty, int32_t replayFlags) {
       ... 
        
        DeferredDisplayList deferredList(mState.currentClipRect(), avoidOverdraw);
        DeferStateStruct deferStruct(deferredList, *this, replayFlags);
        
        renderNode->defer(deferStruct, 0);
        
        flushLayers();
        startFrame();
      
        deferredList.flush(*this, dirty);
        ...
    }
sd-sequence.png

先看下renderNode->defer(deferStruct, 0),合并操作,DrawOp树并不是直接被绘制的,而是首先通过DeferredDisplayList进行一个合并优化,这个是Android硬件加速中采用的一种优化手段,不仅可以减少不必要的绘制,还可以将相似的绘制集中处理,提高绘制速度。

void RenderNode::defer(DeferStateStruct& deferStruct, const int level) {  
    DeferOperationHandler handler(deferStruct, level);  
    issueOperations(deferStruct.mRenderer, handler);  
}

RenderNode::defer其实内含递归操作,比如,如果当前RenderNode代表DecorView,它就会递归所有的子View进行合并优化处理,简述一下合并及优化的流程及算法,其实主要就是根据DrawOp树构建DeferedDisplayList,defer本来就有延迟的意思,对于DrawOp的合并有两个必要条件,

  • 两个DrawOp的类型必须相同,这个类型在合并的时候被抽象为Batch ID,取值主要有以下几种
  enum OpBatchId {  
      kOpBatch_None = 0, // Don't batch  
      kOpBatch_Bitmap,  
      kOpBatch_Patch,  
      kOpBatch_AlphaVertices,  
      kOpBatch_Vertices,  
      kOpBatch_AlphaMaskTexture,  
      kOpBatch_Text,  
      kOpBatch_ColorText,  
      kOpBatch_Count, // Add other batch ids before this  
  }; 
  • DrawOp的Merge ID必须相同,Merge ID没有太多限制,由每个DrawOp自定决定,不过好像只有DrawPatchOp、DrawBitmapOp、DrawTextOp比较特殊,其余的似乎不需要考虑合并问题,即时是以上三种,合并的条件也很苛刻
    在合并过程中,DrawOp被分为两种:需要合的与不需要合并的,并分别缓存在不同的列表中,无法合并的按照类型分别存放在Batch* mBatchLookup[kOpBatch_Count]中,可以合并的按照类型及MergeID存储到TinyHashMap mMergingBatches[kOpBatch_Count]中,示意图如下:
    render-merge.png

    合并之后,DeferredDisplayList Vector mBatches 包含全部整合后的绘制命令,之后渲染即可,需要注意的是这里的合并并不是多个变一个,只是做了一个集合,主要是方便使用各资源纹理等,比如绘制文字的时候,需要根据文字的纹理进行渲染,而这个时候就需要查询文字的纹理坐标系,合并到一起方便统一处理,一次渲染,减少资源加载的浪费,当然对于理解硬件加速的整体流程,这个合并操作可以完全无视,甚至可以直观认为,构建完之后,就可以直接渲染,它的主要特点是 在另一个Render线程使用OpenGL进行绘制,这个是它最重要的特点。而mBatches中所有的DrawOp都会通过OpenGL被绘制到GraphicBuffer中,最后通过swapBuffers通知SurfaceFlinger合成。

Surfaceflinger 系统服务

  • SF是整个Android系统渲染的核心进程。所有应用的渲染逻辑最终都会来到SF中进行处理,最终会把处理后的图像数据交给CPU或者GPU进行绘制。
  • 在每一个应用中都以Surface作为一个图元传递单元,向SF这个服务端传递图元数据。
  • SF是以生产者以及消费者为核心设计思想,把每一个应用进程作为生产者生产图元保存到SF的图元队列中,SF则作为消费者依照一定的规则把生产者存放到SF中的队列一一处理。
  • 为了能够跨进程的传输大容量的图元数据,使用了匿名共享内存内存作为工具把图元数据都输送到SF中处理。 我们需要从应用进程跨进程把图元数据传输到SF进程中处理,就需要跨进程通信,能考虑的如socket这些由于本身效率以及数据拷贝了2份(从物理内存页层面上来看),确实不是很好的选择。一个本身拷贝大量的数据就是一个疑问。那么就需要那些一次拷贝的进程间通信方式,首先能想到的当然是Binder,然而Binder进程间通信,特别是应用的通信数据总量只有1M不到的大小加上应用其他通信,势必会出现不足的问题。为了解决这个问题,Android使用共享内存,使用的是匿名共享内存(Ashmem)。匿名共享内存也是一种拷贝一次的进程间通信方式,其核心比起binder的复杂的mmap更加接近Linux的共享内存的概念。
  • SF底层有一个时间钟在不断的循环,或从硬件中断发出,或从软件模拟发出计时唤起,每隔一段时间都会获取SF中的图元队列通过CPU/GPU绘制在屏幕。

SurfaceView 与 TextureView区别

SurfaceView:

1.可以在一个独立的线程中进行绘制,不会影响主线程
2.使用双缓冲机制,播放视频时画面更流畅
3.Surface不在View hierachy中,它的显示也不受View的属性控制,所以不能进行平移,缩放等变换,也不能放在其它ViewGroup中。
4.SurfaceView 不能嵌套使用

TextureView:
  • SurfaceTexture.OnFrameAvailableListener用于通知TextureView内容流有新图像到来。SurfaceTextureListener接口用于让TextureView的使用者知道SurfaceTexture已准备好,这样就可以把SurfaceTexture交给相应的内容源。Surface为BufferQueue的Producer接口实现类,使生产者可以通过它的软件或硬件渲染接口为SurfaceTexture内部的BufferQueue提供graphic buffer。
  • 支持移动、旋转、缩放等动画,支持截图
  • 必须在硬件加速的窗口中使用,占用内存比SurfaceView高,在5.0以前在主线程渲染,5.0以后有单独的渲染线程。
  • 相比SurfaceView 有1~3帧的延迟

Choreographer

  • 每个线程中有一个Choreographer实例对象
  • 负责获取Vsync同步信号并控制App线程(主线程)完成图像绘制的类。
  • 同一个App的每个窗体旗下ViewRootImpl使用的同一个Choregrapher对象,他控制者整个App中大部分视图的绘制及其他节奏。
  • CALLBACK_INPUT:处理输入事件
  • CALLBACK_ANIMATION:处理动画计算
  • CALLBACK_TRAVERSAL:窗口刷新,执行measure/layout/draw操作
  • CALLBACK_COMMIT:遍历完成的提交操作,用来修正动画启动时间

refrence

[Android 重学系列 SurfaceFlinger的概述]
https://www.jianshu.com/p/c954bcceb22a

[Android 之 Choreographer 详细分析] https://www.jianshu.com/p/86d00bbdaf60

[理解Android硬件加速原理的小白文]
https://www.jianshu.com/p/40f660e17a73
[深入 Activity 三部曲(3)之 View 绘制流程] https://www.jianshu.com/p/7e2a4eff2641!

https://www.jianshu.com/p/7e2a4eff2641

[Android 中的 Window] https://www.jianshu.com/p/4cbcd2a01464

[Android窗口系统第一篇---Window的类型与Z-Order确定] https://www.jianshu.com/p/f3ec3f6e0f6b

[Android窗口系统第三篇---WindowManagerService中窗口的组织方式]
https://www.jianshu.com/p/38bf943766b3
[Android窗口系统第四篇---Activity动画的设置过程] https://www.jianshu.com/p/c2e48b3e33a0

[SurfaceView及TextureView区别]
https://blog.csdn.net/while0/article/details/81481771

你可能感兴趣的:(Android UI-测量-布局-绘制-机制)