作者

大家好,我叫大圣;

自己于2018年5月加入37手游安卓团队,曾经上任于爱拍等互联网公司;

目前是37手游安卓团队的国内担任人,首要担任相关业务开发和一些日常业务统筹等。

布景

最近在处理一个游戏内嵌社区页面卡顿问题的进程中,发现在Manifest的Activity中有将硬件加速封闭的配置,假如将其敞开,那么页面卡顿的问题就处理了。对这一块的知识还不是很了解,也不知道直接打开硬件加速对游戏是否有影响。所以乎在网上查了一波他人的文章,做了一个简单的总结。

CPU与GPU

在了解什么是硬件加速之前,先了解一下什么是CPU和GPU。 CPU(Centr(l Processing Unit,中央处理器)是核算机设备核⼼器材,⽤于执⾏ 程序代码,软件开发者对此都很了解;GPU(Gr(phics Processing Unit,图形处理 器)首要⽤于处理图形运算,一般所说“显卡”的核⼼部件便是GPU。

硬件加速浅谈

  • 黄色的Control为操控器,用于和谐操控整个CPU的运转,包括取出指令、操控其他模块的运转等;
  • 绿色的ALU(Arithmetic Logic Unit)是算术逻辑单元,用于进行数学、逻辑运算;
  • 橙色的Cache和DRAM分别为缓存和RAM,用于存储信息。 从上面的结构图能够看出,CPU的操控器较为复杂,拿手各种复杂的逻辑运算;GPU的操控器比较简单,但包括了大量ALU,拿手大量的数学运算。

硬件加速的首要原理,便是通过底层软件代码,将CPU不拿手的图形核算转换成GPU专用指令,由GPU完结。

显卡和GPU不是同一个概念,显卡指的是包括GPU的一个完整的卡,GPU是单指显卡的核心芯片,是一颗芯片;
听说比特币挖矿的机器都是采用GPU运算的,是不是和上面的原理相同,挖矿是大量的数学核算,GPU愈加拿手呢?

Android中的硬件加速

接下来看看Android中怎么使用硬件加速的。

怎么敞开硬件加速

从 Android 3.0(API 等级 11)开端,Android 2D 烘托管道⽀持硬件加速,也 便是说,在 View 的画布上执⾏的一切制作操作都会使⽤ GPU。启⽤硬件加速需求 更多资源,因此应⽤会占⽤更多内存。

假如您的⽬标 API 等级为 14 及更⾼等级,则硬件加速默许处于启⽤状态,但也能够 清晰启⽤该功用。假如您的应⽤仅使⽤规范视图和 Dr(w(ble,则大局启⽤硬件加速 不会形成任何不良制作效果。

您能够在以下等级操控硬件加速:

  • 应⽤
  • Activity
  • 窗⼝
  • 视图

应⽤等级

在 Android 清单⽂件中,将以下特点添加到 标记中,为整个应⽤启⽤硬件加速:

<application android:hardwareAccelerated="true" ...>

Activity 等级

假如大局启用硬件加速后,您的使用无法正常运转,则您也能够针对各个 Activity 操控硬件加速。要在 Activity 等级启用或停用硬件加速,您能够使用 元素的 android:hardwareAccelerated 特点。以下示例展现了怎么为整个使用启用硬件加速,但为一个 Activity 停用硬件加速:

<application android:hardwareAccelerated="true">
	<activity ... />
	<activity android:hardwareAccelerated="false" />
</application>

窗口等级

假如您需求完成更精细的操控,能够使用以下代码为给定窗口启用硬件加速:

getWindow().setFlags(
	WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
    WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);

视图等级

您能够使用以下代码在运转时为单个视图停用硬件加速:

myView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);

制作流程上面的优化

在说完以上CPU和GPU的特性,以及Android怎么操控硬件加速之后,来看看在界面View在改写和制作进程中,详细是哪些要害点来做了硬件加速的操控。

硬件加速情况下Canvas生成的是DisplayListCanvas目标

在View的制作进程中会沿着draw(canvas,parent,drawingTime) –> draw(canvas) –> onDraw –> dispachDraw 这条递归途径往下走,在draw(canvas,parent,drawingTime)中,假如是硬件加速情况下,会走updateDisplayListIfDirty()。

if (drawingWithRenderNode) {
    renderNode = updateDisplayListIfDirty();
    if (!renderNode.isValid()) {
        // ...
        renderNode = null;
        drawingWithRenderNode = false;
    }
}

在updateDisplayListIfDirty()办法中,生成了DisplayListCanvas目标,然后调用draw(canvas)办法,此刻Canvas已经不是一般的Canvas了,而是DisplayListCanvas。

final DisplayListCanvas canvas = renderNode.start(width, height);
canvas.setHighContrastText(mAttachInfo.mHighContrastText);
try {
        //...
            if (debugDraw()) {
                debugDrawFocus(canvas);
            }
        } else {
            draw(canvas);
        }
    }
} finally {
    renderNode.end(canvas);
    setDisplayListProperties(renderNode);
}

在调用DisplayListCanvas的drawXXX办法进程中,并没有履行真实的制作,而是用构建DisplayList,并保存在RenderNode中,通过renderNode.end(canvas)进行该操作。

/**
* Ends the recording for this display list. A display list cannot be
* replayed if recording is not finished. Calling this method marks
* the display list valid and {@link #isValid()} will return true.
*
* @see #start(int, int)
* @see #isValid()
*/
public void end(DisplayListCanvas canvas) {
    long displayList = canvas.finishRecording();
    nSetDisplayList(mNativeRenderNode, displayList);
    canvas.recycle();
}

JNI层将DisplayListCanvas的操作转化成DisplayList

一个RenderNode包括若干个DisplayList,一般一个RenderNode对应一个View,包括View本身及其子View的一切DisplayList。

在ViewRootImpl中,假如是支撑硬件加速,会履行mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this)办法。

// ViewRootImpl.java
if (mAttachInfo.mThreadedRenderer != null && mAttachInfo.mThreadedRenderer.isEnabled()) {
    mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this);
} else {
    //...
}

ThreadedRenderer的draw进程,会调用到updateRootDisplayList,然后调用updateViewTreeDisplayList,此进程会更新整个View树的DisplayList,终究履行ThreadedRenderer.nSyncAndDrawFrame来发动线程对DisplayList进行终究的制作操作

// ThreadedRenderer.java
void draw(View view, AttachInfo attachInfo, DrawCallbacks callbacks) {
    ...
    updateRootDisplayList(view, callbacks);
    ...
    int syncResult = nSyncAndDrawFrame(mNativeProxy, frameInfo, frameInfo.length);
}

因为制作流程的不同,硬件加速在界面内容产生重绘的时分制作流程能够得到优化,避免了一些重复操作,然后大幅提高制作功率。在updateDisplayListIfDirty办法从姓名能够看出,更新Displaylist是产生在脏区域。

在硬件加速封闭时,制作内容会被 CPU 转换成实践的像素,然后直接烘托到屏幕。详细来说,这个「实践的像素」,它是由 Bitmap 来承载的。在界面中的某个 View 因为内容产生改动而调用 invalidate() 办法时,假如没有敞开硬件加速,那么为了正确核算 Bitmap 的像素,这个 View 的父 View、父 View 的父 View 乃至一直向上直到最尖端 View,以及一切和它相交的兄弟 View,都需求被调用 invalidate()来重绘。一个 View 的改动使得大半个界面甚至整个界面都重绘一遍,这个工作量是非常大的。

而在硬件加速敞开时,前面说过,制作的内容会被转换成 GPU 的操作保存下来(承载的方式称为 display list,对应的类也叫做 DisplayList),再转交给 GPU。因为一切的制作内容都没有变成终究的像素,所以它们之间是彼此独立的,那么在界面内容产生改动的时分,只要把产生了改动的 View 调用 invalidate() 办法以更新它所对应的 GPU 操作就好,至于它的父 View 和兄弟 View,只需求保持原样。那么这个工作量就很小了。

硬件加速浅谈

在默许情况下,View的clipChildren的特点为true,即每个View制作区域不能超出其父View的规模。假如设置一个页面根布局的clipChildren特点为false,则子View能够超出父View的制作区域。

上图中右边的ViewGroup的clipChildren特点即为false,其子View的制作超出了本身规模。

当一个View触发invalidate,且没有播放动画、没有触发layout的情况下:

  • 关于全不通明的View,其本身会设置标志位PFLAG_DIRTY,其父View会设置标志位PFLAG_DIRTY_OPAQUE。在draw(canvas)办法中,只有这个View本身重绘。

    上图中的TextView布景全不通明,此刻调用TextView的invalidate,自由其本身重制作。

  • 关于可能有通明区域的View,其本身和父View都会设置标志位PFLAG_DIRTY

    • clipChildren为true时,脏区会被转换成ViewRoot中的Rect,改写时层层向下判断,当View与脏区有堆叠则重绘。假如一个View超出父View规模且与脏区堆叠,但其父View不与脏区堆叠,这个子View不会重绘。

      上图中假如TextView布景通明,那么脏区域会从TextView投影至ViewRoot上面的Rect,也便是绿色区域,此刻在这个投影之上的View都会被触发制作(即TextView、左边ViewGroup、ViewRoot)

    • clipChildren为false时,ViewGroup.invalidateChildInParent()中会把脏区扩大到本身整个区域,所以与这个区域堆叠的一切View都会重绘。

      上图中右边ViewGroup的clipChildren为false,那么脏区域即为右边ViewGroup的整个投影的区域,与此堆叠的View都会被重新制作。

实践问题处理

布景中说到的游戏内嵌web页面卡顿的问题, 在敞开硬件加速后卡顿的问题处理了。但是因为对游戏引擎不了解,所以不清楚敞开硬件加速是否会影响到游戏的运转,比方游戏界面制作会不会错乱,耗电量会不会添加等等问题。其实从上面第三末节说到的,Android不只仅能够对整个使用敞开硬件加速还支撑对单个Activity进行设置,那么这个问题其实就很好处理了, 直接将显示Web的页面写成一个Activity单独敞开硬件急速即可,这样完全不会影响到游戏。

总结

在硬件加速进程中,充沛利用CPU和GPU各自的特性,将逻辑部分交个CPU处理,将大量的数学核算交个GPU处理,在硬件层面对整个进程功率提高。界面改写进程中,CPU只会更新需求重绘的DisplayList,进一步提高制作功率。

上文的介绍只是在一些要害的代码上面做了讲解,未深化介绍细节;关于DisplayListCanvas怎么生成DisplsyList的进程,属于JNI层的操作,文档并未触及。关于RenderNode是怎么安排DiaplayList,以及View树的各个RenderNode是怎么安排的都未说明,文章充沛学习了一些大神文章的内容,文末有参阅链接,需求深化了解能够自行查阅。

结束语

进程中有问题或者需求沟通的同学,能够添加微信号AndroidAssistant37,然后进群进行问题和技术的沟通等。

参阅链接

  1. /post/684490…
  2. developer.android.com/guide/topic…
  3. www.codeleading.com/article/337…
  4. /post/684490…