符合中小企业对网站设计、功能常规化式的企业展示型网站建设
本套餐主要针对企业品牌型网站、中高端设计、前端互动体验...
商城网站建设因基本功能的需求不同费用上面也有很大的差别...
手机微信网站开发、微信官网、微信商城网站...
对于Android开发者来说,我们或多或少有了解过Android图像显示的知识点,刚刚学习Android开发的人会知道,在Actvity的onCreate方法中设置我们的View后,再经过onMeasure,onLayout,onDraw的流程,界面就显示出来了;对Android比较熟悉的开发者会知道,onDraw流程分为软件绘制和硬件绘制两种模式,软绘是通过调用Skia来操作,硬绘是通过调用Opengl ES来操作;对Android非常熟悉的开发者会知道绘制出来的图形数据最终都通过GraphiBuffer内共享内存传递给SurfaceFlinger去做图层混合,图层混合完成后将图形数据送到帧缓冲区,于是,图形就在我们的屏幕显示出来了。
创新互联是一家企业级云计算解决方案提供商,超15年IDC数据中心运营经验。主营GPU显卡服务器,站群服务器,成都服务器托管,海外高防服务器,服务器机柜,动态拨号VPS,海外云手机,海外云服务器,海外服务器租用托管等。
但我们所知道的Activity或者是应用App界面的显示,只属于Android图形显示的一部分。同样可以在Android系统上展示图像的WebView,Flutter,或者是通过Unity开发的3D游戏,他们的界面又是如何被绘制和显现出来的呢?他们和我们所熟悉的Acitvity的界面显示又有什么异同点呢?我们可以不借助Activity的setView或者InflateView机制来实现在屏幕上显示出我们想要的界面吗?Android系统显示界面的方式又和IOS,或者Windows等系统有什么区别呢?……
去探究这些问题,比仅仅知道Acitvity的界面是如何显示出来更加的有价值,因为想要回答这些问题,就需要我们真正的掌握Android图像显示的底层原理,当我们掌握了底层的显示原理后,我们会发现WebView,Flutter或者未来会出现的各种新的图形显示技术,原来都是大同小异。
我会花三篇文章的篇幅,去深入的讲解Android图形显示的原理,OpenGL ES和Skia的绘制图像的方式,他们如何使用,以及他们在Android中的使用场景,如开机动画,Activity界面的软件绘制和硬件绘制,以及Flutter的界面绘制。那么,我们开始对Android图像显示原理的探索吧。
在讲解Android图像的显示之前,我会先讲一下屏幕图像的显示原理,毕竟我们图像,最终都是在手机屏幕上显示出来的,了解这一块的知识会让我们更容易的理解Android在图像显示上的机制。
图像显示的完整过程,分为下面几个阶段:
图像数据→CPU→显卡驱动→显卡(GPU)→显存(帧缓冲)→显示器
我详细介绍一下这几个阶段:
实际上显卡驱动,显卡和显存,包括数模转换模块都是属于显卡的模块。但为了能能详细的讲解经历的步骤,这里做了拆分。
当显存中有数据后,显示器又是怎么根据显存里面的数据来进行界面的显示的呢?这里以LCD液晶屏为例,显卡会将显存里的数据,按照从左至右,从上到下的顺序同步到屏幕上的每一个像素晶体管,一个像素晶体管就代表了一个像素。
如果我们的屏幕分辨率是1080x1920像素,就表示有1080x1920个像素像素晶体管,每个橡素点的颜色越丰富,描述这个像素的数据就越大,比如单色,每个像素只需要1bit,16色时,只需要4bit,256色时,就需要一个字节。那么1080x1920的分辨率的屏幕下,如果要以256色显示,显卡至少需要1080x1920个字节,也就是2M的大小。
刚刚说了,屏幕上的像素数据是从左到右,从上到下进行同步的,当这个过程完成了,就表示一帧绘制完成了,于是会开始下一帧的绘制,大部分的显示屏都是以60HZ的频率在屏幕上绘制完一帧,也就是16ms,并且每次绘制新的一帧时,都会发出一个垂直同步信号(VSync)。我们已经知道,图像数据都是放在帧缓冲中的,如果帧缓冲的缓冲区只有一个,那么屏幕在绘制这一帧的时候,图像数据便没法放入帧缓冲中了,只能等待这一帧绘制完成,在这种情况下,会有很大了效率问题。所以为了解决这一问题,帧缓冲引入两个缓冲区,即 双缓冲机制 。双缓冲虽然能解决效率问题,但会引入一个新的问题。当屏幕这一帧还没绘制完成时,即屏幕内容刚显示一半时,GPU 将新的一帧内容提交到帧缓冲区并把两个缓冲区进行交换后,显卡的像素同步模块就会把新的一帧数据的下半段显示到屏幕上,造成画面撕裂现象。
为了解决撕裂问题,就需要在收到垂直同步的时候才将帧缓冲中的两个缓冲区进行交换。Android4.1黄油计划中有一个优化点,就是CPU和GPU都只有收到垂直同步的信号时,才会开始进行图像的绘制操作,以及缓冲区的交换工作。
我们已经了解了屏幕图像显示的原理了,那么接着开始对Android图像显示的学习。
从上一章已经知道,计算机渲染界面必须要有GPU和帧缓冲。对于Linux系统来说,用户进程是没法直接操作帧缓冲的,但我们想要显示图像就必须要操作帧缓冲,所以Linux系统设计了一个虚拟设备文件,来作为对帧缓冲的映射,通过对该文件的I/O读写,我们就可以实现读写屏操作。帧缓冲对应的设备文件于/dev/fb* ,*表示对多个显示设备的支持, 设备号从0到31,如/dev/fb0就表示第一块显示屏,/dev/fb1就表示第二块显示屏。对于Android系统来说,默认使用/dev/fb0这一个设帧缓冲作为主屏幕,也就是我们的手机屏幕。我们Android手机屏幕上显示的图像数据,都是存储在/dev/fb0里,早期AndroidStuio中的DDMS工具实现截屏的原理就是直接读取/dev/fb0设备文件。
我们知道了手机屏幕上的图形数据都存储在帧缓冲中,所以Android手机图像界面的原理就是将我们的图像数据写入到帧缓冲内。那么,写入到帧缓冲的图像数据是怎么生成的,又是怎样加工的呢?图形数据是怎样送到帧缓冲去的,中间经历了哪些步骤和过程呢?了解了这几个问题,我们就了解了Android图形渲染的原理,那么带着这几个疑问,接着往下看。
想要知道图像数据是怎么产生的,我们需要知道 图像生产者 有哪些,他们分别是如何生成图像的,想要知道图像数据是怎么被消费的,我们需要知道 图像消费者 有哪些,他们又分别是如何消费图像的,想要知道中间经历的步骤和过程,我们需要知道 图像缓冲区 有哪些,他们是如何被创建,如何分配存储空间,又是如何将数据从生产者传递到消费者的,图像显示是一个很经典的消费者生产者的模型,只有对这个模型各个模块的击破,了解他们之间的流动关系,我们才能找到一条更容易的路径去掌握Android图形显示原理。我们看看谷歌提供的官方的架构图是怎样描述这一模型的模块及关系的。
如图, 图像的生产者 主要有MediaPlayer,CameraPrevier,NDK,OpenGl ES。MediaPlayer和Camera Previer是通过直接读取图像源来生成图像数据,NDK(Skia),OpenGL ES是通过自身的绘制能力生产的图像数据; 图像的消费者 有SurfaceFlinger,OpenGL ES Apps,以及HAL中的Hardware Composer。OpenGl ES既可以是图像的生产者,也可以是图像的消费者,所以它也放在了图像消费模块中; 图像缓冲区 主要有Surface以及前面提到帧缓冲。
Android图像显示的原理,会仅仅围绕 图像的生产者 , 图像的消费者 , 图像缓冲区 来展开,在这一篇文章中,我们先看看Android系统中的图像消费者。
SurfaceFlinger是Android系统中最重要的一个图像消费者,Activity绘制的界面图像,都会传递到SurfaceFlinger来,SurfaceFlinger的作用主要是接收图像缓冲区数据,然后交给HWComposer或者OpenGL做合成,合成完成后,SurfaceFlinger会把最终的数据提交给帧缓冲。
那么SurfaceFlinger是如何接收图像缓冲区的数据的呢?我们需要先了解一下Layer(层)的概念,一个Layer包含了一个Surface,一个Surface对应了一块图形缓冲区,而一个界面是由多个Surface组成的,所以他们会一一对应到SurfaceFlinger的Layer中。SurfaceFlinger通过读取Layer中的缓冲数据,就相当于读取界面上Surface的图像数据。Layer本质上是 Surface和SurfaceControl的组合 ,Surface是图形生产者和图像消费之间传递数据的缓冲区,SurfaceControl是Surface的控制类。
前面在屏幕图像显示原理中讲到,为了防止图像的撕裂,Android系统会在收到VSync垂直同步时才会开始处理图像的绘制和合成工作,而Surfaceflinger作为一个图像的消费者,同样也是遵守这一规则,所以我们通过源码来看看SurfaceFlinger是如何在这一规则下,消费图像数据的。
SurfaceFlinger专门创建了一个EventThread线程用来接收VSync。EventThread通过Socket将VSync信号同步到EventQueue中,而EventQueue又通过回调的方式,将VSync信号同步到SurfaceFlinger内。我们看一下源码实现。
上面主要是SurfaceFlinger初始化接收VSYNC垂直同步信号的操作,主要有这几个过程:
经过上面几个步骤,我们接收VSync的初始化工作都准备好了,EventThread也开始运转了,接着看一下EventThread的运转函数threadLoop做的事情。
threadLoop主要是两件事情
mConditon又是怎么接收VSync的呢?我们来看一下
可以看到,mCondition的VSync信号实际是DispSyncSource通过onVSyncEvent回调传入的,但是DispSyncSource的VSync又是怎么接收的呢?在上面讲到的SurfaceFlinger的init函数,在创建EventThread的实现中,我们可以发现答案—— mPrimaryDispSync 。
DispSyncSource的构造方法传入了mPrimaryDispSync,mPrimaryDispSync实际是一个DispSyncThread线程,我们看看这个线程的threadLoop方法
DispSyncThread的threadLoop会通过mPeriod来判断是否进行阻塞或者进行VSync回调,那么mPeriod又是哪儿被设置的呢?这里又回到SurfaceFlinger了,我们可以发现在SurfaceFlinger的 resyncToHardwareVsync 函数中有对mPeriod的赋值。
可以看到,这里最终通过HWComposer,也就是硬件层拿到了period。终于追踪到了VSync的最终来源了, 它从HWCompser产生,回调至DispSync线程,然后DispSync线程回调到DispSyncSource,DispSyncSource又回调到EventThread,EventThread再通过Socket分发到MessageQueue中 。
我们已经知道了VSync信号来自于HWCompser,但SurfaceFlinger并不会一直监听VSync信号,监听VSync的线程大部分时间都是休眠状态,只有需要做合成工作时,才会监听VSync,这样即保证图像合成的操作能和VSync保持一致,也节省了性能。SurfaceFlinger提供了一些主动注册监听VSync的操作函数。
可以看到,只有当SurfaceFlinger调用 signalTransaction 或者 signalLayerUpdate 函数时,才会注册监听VSync信号。那么signalTransaction或者signalLayerUpdate什么时候被调用呢?它可以由图像的生产者通知调用,也可以由SurfaceFlinger根据自己的逻辑来判断是否调用。
现在假设App层已经生成了我们界面的图像数据,并调用了 signalTransaction 通知SurfaceFlinger注册监听VSync,于是VSync信号便会传递到了MessageQueue中了,我们接着看看MessageQueue又是怎么处理VSync的吧。
MessageQueue收到VSync信号后,最终回调到了SurfaceFlinger的 onMessageReceived 中,当SurfaceFlinger接收到VSync后,便开始以一个图像消费者的角色来处理图像数据了。我们接着看SurfaceFlinger是以什么样的方式消费图像数据的。
VSync信号最终被SurfaceFlinger的onMessageReceived函数中的INVALIDATE模块处理。
INVALIDATE的流程如下:
handleMessageTransaction的处理比较长,处理的事情也比较多,它主要做的事情有这些
handleMessageRefresh函数,便是SurfaceFlinger真正处理图层合成的地方,它主要下面五个步骤。
我会详细介绍每一个步骤的具体操作
合成前预处理会判断Layer是否发生变化,当Layer中有新的待处理的Buffer帧(mQueuedFrames0),或者mSidebandStreamChanged发生了变化, 都表示Layer发生了变化,如果变化了,就调用signalLayerUpdate,注册下一次的VSync信号。如果Layer没有发生变化,便只会做这一次的合成工作,不会注册下一次VSync了。
重建Layer栈会遍历Layer,计算和存储每个Layer的脏区, 然后和当前的显示设备进行比较,看Layer的脏区域是否在显示设备的显示区域内,如果在显示区域内的话说明该layer是需要绘制的,则更新到显示设备的VisibleLayersSortedByZ列表中,等待被合成
rebuildLayerStacks中最重要的一步是 computeVisibleRegions ,也就是对Layer的变化区域和非透明区域的计算,为什么要对变化区域做计算呢?我们先看看SurfaceFlinger对界面显示区域的分类:
还是以这张图做例子,可以看到我们的状态栏是半透明的,所以它是一个opaqueRegion区域,微信界面和虚拟按键是完全不透明的,他是一个visibleRegion,除了这三个Layer外,还有一个我们看不到的Layer——壁纸,它被上方visibleRegion遮挡了,所以是coveredRegion
对这几个区域的概念清楚了,我们就可以去了解computeVisibleRegions中做的事情了,它主要是这几步操作:
您好,
1、用美图看看打开照片的原文件,右击百鼠标出现菜单,点击最下方的“图片信息”,就可以看到此图片是用哪个品牌的度设备(相机与手机)拍摄的,还有具体型号,拍摄日期,焦距感。
2、在电脑上用专业的图像浏览软件打开照片,看属性,里面的拍摄信息很详细,包括相机的型号,拍摄时间等等,一目了然问。这样的软件很多,例如ACDSee等。
3、现在数码技术是光电转化,核心是一个感光元件答,手机上的可能都没小拇指甲盖大,单反的大拇指头那么大把,比如同样是一千两百万像素,这么多像素点分在两颗不同的原件上。
4、单专反上的就大,就可以更好地抑制强光的紫边,高感光度的噪点,同时色彩还原也好。当然了,单反画质好,镜头是更主要的因素。
解释:
1、首先创建一个Bitmap图片,并指定大小;
2、在该图片上创建一个新的画布Canvas,然后在画布上绘制,并保存即可;
3、需要保存的目录File,注意如果写的目录如“/sdcard/akai/”如果不存在的话,要先创建(file.mkdirs()),否则FileOutputStream会报错No found;
4、需要添加权限:uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/
什么是画布呢 其实通过字面意思的理解就是用来绘画的地方,那么android里的画布是神马样子的呢?
在自定义画布中常用到下面3个类
Canvas
这些绘图方法中的每一个都需要指定一个Paint对象来渲染它
Paint
Paint也称为"刷子",Paint可以指定如何将基本图形绘制到位图上。
Paint类相当于一个笔刷和调色板。它可以选择如何使用上面描述的draw方法来渲染绘 制在画布上的基本图形。通过修改Paint对象,可以在绘图的时候控制颜色、样式、字体和特殊效果。最简单地,setColor可以让你选择一个Paint的颜色,而Paint对象的样式(使用setStyle控制)则可以决定是绘制绘图对象的轮廓(STROKE),还是只填充每一部 分(FILL),或者是两者都做(STROKE_AND_FILL)除了这些简单的控制之外,Paint类还支持透明度,另外,它也可以通过使用各种各样的阴影、过滤器和效果进行修改,从而提供由更丰富的、复杂的画笔和颜料组成的调色板。
从继承View类(或其子类)开始,并定义onDraw()回调方法。系统会调用该方法来完 成View对象自己的绘制请求。这也是通过Canvas对象来执行所有的图形绘制调用的地方,这个Canvas对象是由onDraw()回调方法传入的。
Android框架只在必要的时候才会调用onDraw()方法,每次请求应用程序准备完成图形 绘制任务时,必须通过调用invalidate()方法让该View对象失效。这表明可以在该View 对象上进行图形绘制处理了,然后Android系统会调用该View对象的onDraw()方(尽 管不保证该回调方法会立即被调用)。
在定制的View组件的onDraw()方法内部,使用给定的Canvas对象来完成所有的图形绘制处理(如Canvas.draw…()方法或把该Canvas对象作为参数传递给其他类的draw() 方法)。一旦onDraw()方法被执行完成,Android框架就会使用这个Canvas对象来绘制一个有系统处理的Bitmap对象。
下面是Paint一些常用方法:
Bitmap
Bitmap绘图的表面也称位图(这里详细说哈位图的功能)。
从资源中获取位图:
通过Resource的函数:InputStream openRawResource(int id)获取得到资源文件的数据流后,可以通过2种方式获得bitmap
使用BitmapDrawable :
使用BitmapDrawable(InputStream is)构造一个BitmapDrawable;
使用BitmapDrawable类的getBitmap()获取得到位图;
使用BitmapFactory使用BitmapFactory类decodeStream(InputStream is)解码位 图资源,获取位图BitmapFactory的所有函数都是static,这个辅助类可以通过资 源ID、路径、文件、数据流等方式来获取位图。
获取位图的信息
一般获取位图信息包括:位图大小、透明度、颜色格式等等,这些信息呢可以通过 三-一方法获取得到Bitmap就迎刃而解了,Android SDK中对Bitmap有详细说明,大家可以去详细了解哈。
显示位图
显示位图需要使用核心类Canvas,可以直接通过Canvas类的drawBirmap()显示位图,或者借助于BitmapDrawable来将Bitmap绘制到Canvas,下面的实例中会详细列举到
位图的缩放
位图的缩放,在Android SDK中提供了2种方法:
1:将一个位图按照需求重画一遍,画后的位图就是我们需要的了,与位图的显示几乎 一样:
drawBitmap(Bitmap bitmap, Rect src, Rectdst, Paint paint)
2:在原有位图的基础上,缩放原位图,创建一个新的位图:
createBitmap(Bitmap source, int x, int y,int width, int height, Matrix m, boolean filter)
位图旋转
位图的旋转,离不开Matrix。Android SDK提供了Matrix类,可以通过各种接口来设置 矩阵
android 处理图片工具
截取视频帧并转化为Bitmap
SVG 图片是一种可支持任意缩放的图片格式,使用 xml 定义,使用 canvas 中 path 路径来完成绘制,和我们传统使用的 BitMap位图有很大的区别。
SVG 在前端早就普及了,在android 中是 google 是在5.0之后开始支持的,14年出来之后兼容是个大问题,随着2016.2 V7包 23.2.0版本的发布才算是有个相对完善的兼容使用方案。
SVG 的概念我就不写了,拿来主义啦,原文: Android Vector曲折的兼容之路
不瞎逼逼,我们先来看一看 android 中的 SVG 矢量图是个什么东东
看到没有,这就是一个 SVG 矢量图片,就是一个 xml 文件,右边是预览,先说下,这东西的好处:缩放不失真,体积小。这一个 SVG 图片只有970个字节...强大吧,比 png 格式的图片强的没边了吧,png 我们还得适配,做多套,然后一个一个改名字复制到工程里,有了 SVG 妈妈再也不担心我们写作业啦...
这里需要解释下这里的几个标签:
这里有一分详细的属性说明:
好了下面开始介绍 SVG 啦
首先,需要讲解两个概念——SVG和Vector。
SVG,即Scalable Vector Graphics 矢量图,这种图像格式在前端中已经使用的非常广泛了
Vector,在Android中指的是Vector Drawable,也就是Android中的矢量图
因此,可以说Vector就是Android中的SVG实现,因为Android中的Vector并不是支持全部的SVG语法,也没有必要,因为完整的SVG语法是非常复杂的,但已经支持的SVG语法已经够用了,特别是Path语法,几乎是Android中Vector的标配
Android以一种简化的方式对SVG进行了兼容,这种方式就是通过使用它的Path标签,通过Path标签,几乎可以实现SVG中的其它所有标签,虽然可能会复杂一点,但这些东西都是可以通过工具来完成的,所以,不用担心写起来会很复杂。
Path指令解析如下所示:
支持的指令:
M = moveto(M X,Y) :将画笔移动到指定的坐标位置
L = lineto(L X,Y) :画直线到指定的坐标位置
H = horizontal lineto(H X):画水平线到指定的X坐标位置
V = vertical lineto(V Y):画垂直线到指定的Y坐标位置
C = curveto(C X1,Y1,X2,Y2,ENDX,ENDY):三次贝赛曲线
S = smooth curveto(S X2,Y2,ENDX,ENDY)
Q = quadratic Belzier curve(Q X,Y,ENDX,ENDY):二次贝赛曲线
T = smooth quadratic Belzier curveto(T ENDX,ENDY):映射
A = elliptical Arc(A RX,RY,XROTATION,FLAG1,FLAG2,X,Y):弧线
Z = closepath():关闭路径
坐标轴为以(0,0)为中心,X轴水平向右,Y轴水平向下
所有指令大小写均可。大写绝对定位,参照全局坐标系;小写相对定位,参照父容器坐标系
指令和数据间的空格可以省略
同一指令出现多次可以只用一个
注意,’M’处理时,只是移动了画笔, 没有画任何东西。 它也可以在后面给出上同时绘制不连续线。
关于这些语法,开发者需要的并不是全部精通,而是能够看懂即可,其它的都可以交给工具来实现。
这里有一篇 Android vector标签 PathData 画图超详解 详细描述了 SVG 中 path 的绘制
好了概念性的东西说完了,我们来看看
SVG 的使用分2种,一种是静态 SVG 矢量图,就是本文的主角,本章节主要谈论的东西,另一种是 SVG 矢量动画,是SVG 的高级应用,是给静态 SVG 加上objectAnimator 动画,应用的很广泛,是实现 android icon 动态交互的核心做法。
上面的SVG 图大家都看到了,我们就是写一个 xml 的文件,里面承载的标签都是描述如何绘制我们想要的图案的,画布大小,颜色,路径等,然后交给系统去绘制。
现在让我们来看看 SVG 在 andorid 中如何应用,如何兼容5.0以下版本。
SVG 虽然早早就在前端使用了,但是 android 上开始支持 SVG 的使用还是从5.0开始的,在5.0以上系统的使用很简单,和之前一样使用 PNG 图片一样
首先 android 中的 SVG 图片的承载方式是一个 xml 文件,所以UI 给我们的 SVG 图片是不能直接使用的,这里 google 给我们提供加载方式
Android studio 在 2.3.3 的版本中可以直接使用 svg,新建一个 SVGDemo项目,新建 Vector Asset 文件:app- main - New - Vector Asset 如图所示:
我们选择 Local File 就是选择本地svg文件进行导入,对文件命名后点击 Next -Finish 在 drawable目录 下就添加了一个.xml的文件
好了这样一个 svg 图片我们算是加入到我们的工程里里了,可以直接使用了。当然在此之前我们把 SVG 图片放在那个 drawable 文件夹呢。对于这个问题就要说一下了:
有一点需要解释一下,svg 矢量图文件我们放在drawable 根目录即可。android 系统不会根据你把 svg 矢量图存放在不同的 drawable 文件夹,对图片进行分辨率上的缩放,因此我们不用像使用 PNG 图片时准备多套图片了。我们导入 SVG 图片默认存放的地址就是 drawable根目录,所以我们就放这里就好了,当然也可以自己写SVG 图片,都是 xml 的,自己写完 path 路径后都是可以查看预览的,一般也不会自己写,都是UI 的活。
这样就 ok啦,5.0以上的系统SVG你就像一般 png 图片一样使用就好啦,你可以试一下。
SVG 在 4.x 版本上的兼容根据 SVG 使用范围的变化,配置也是逐步增加的
这时 imageview 就不行了,我们需要使用 AppCompatActivity 或是 AppCompatImageView,这时我们需要导入 V7 包
gradle 需要如下配置:
系统会在 4.x 版本时对 SVG 自动生成相应的 drawable 图,此时 SVG 是没有无限拉伸特性的,gradle 的配置目的是去这个
举个例子:
资源设置不能用 src 了,必须使用 srcCompat ,这时我们能看到图而不是去 SVG 的特性了
这时上面的设置就不够了,我们在 view 所在的 activity 或是全局添加下面的设置
然后这还不够,我们必须给 SVG 图片添加一个容器,比如 selector,这样我们才能正常使用,比如给 textview 设置图片,自定义属性设置图片
这个 vc_halfstart_24dp 就是 SVG 图片
这个我们必须要添加官方的 vectorDrawable 支持库了,最低支持到 23.2.0
这样基本就没啥问题了
SVG 配合自定义 view 的话,就得我们读取 SVG 然后转换成 path 路径来画了,SVG 实质上也是 xml 文件,所以解析 xml 文件的思路也使用,当然还有其他一些 SVG 转 path 的思路
SVG前戏—让你的View多姿多彩 一文中提供了一些思路,大家不妨去看看
以前在实际项目中使用拍照和从图库中获取图片时,不知道以何种方式从回调中取得图片资源,以Bitmap方式还是Uri的形式?如果是使用Bitmap,应该注意些什么,Uri又是一种什么样的格式?有时会出现拍照时回调data数据为空的情况,又该如何定位问题呢?图片裁剪又是怎样决定方案的?以下将针对这几个问题阐述自己的见解。
在Android中,Intent触发 Camera程序,拍好照片后会返回数据,比如摄像头800万像素,拍出来的图片尺寸为 3200x2400,占据内存大小=3200 x 2400 x 4bytes / (1024 x 1024) = 30MB 【图像设置ARGB_8888一个像素点占据4字节内存】,这个尺寸对应的 Bitmap会耗光应用程序的内存,出于安全方面的考虑,Android会给你一个缩略图,比如 160 x 120 px。
Q:为何要返回图缩略?
缩略图是指从onActivityForResullt回调方法中 intent保存的参数图片。这是因为在启动相机程序拍摄图片,为了让Bitmap数据能在Activity之间传递,不得不将拍摄后的Bitmap进行压缩再传递,因此通过回调从intent中取得的是缩略图在于拍摄的Bitmap太大,Activity之间Bundle存放的数据不能太大,会导致程序无响应。高清原图是指直接将拍摄的图片以文件/Uri形式保存到手机。
注:Bitmap实现了Parcelable 接口,所有可以在Activity间使用Intent传递。
Q:使用Bitmap需要注意哪些问题?
1、Android 裁剪图片 Intent 附加数据的含义
| setExtra | DataType | Desciption | Extra |
|:-------- |:--------:| :------: |
|crop| String | Signals the crop feature | value="true" |
|aspectX|int|Aspect Ratio|裁剪横向比例|
|aspectY|int|Aspect Ratio|裁剪纵向比例|
|outputX|int|width od output created from this intent|输出宽度|
|outputY|int|height od output created from this intent|输出高度|
|scale|boolean|should it scale|是否伸缩|
|return-date|boolean|Return the bitmap with Action-inline-data by using the data|是否返回Bitmap数据|
|data|Parcelable|Bitmap to process, you may provide it a bitmap (not tested)|可设置data为Bitmap或者将相应数据同uri联系起来|
|circleCrop|String|if this string is not null, it will provide some cicular cr||
|MediaStore.
EXTRA_OUTPUT("output")|URI|set this URI to a File|输出路径到uri中|
2、图片裁剪终极方案 — 图片来源有拍照和图库,可采取的操作有:
3、剪切图片:
我们每天花很多时间盯着手机屏幕,不知道你有没有好奇过:
这时候来了一位Android程序员(当然也可以是iOS或者是前端程序员)说: 这里显示的其实是一个View树,我们看到的都是大大小小的View。
。。。听起来很有道理,我们也经常指着屏幕说这个View怎么怎么样,可问题又来了:
程序员老兄又来了: 屏幕当然不能识别View,它作为一个硬件,只能根据收到的数据改变每个像素单元的数据,这样整体来看,用户就发现屏幕上的内容变化了。至于View的内容是如何一步一步转化成屏幕可是识别的数据的,简单讲可以分成三步:
。。。听起来很有道理,可问题又来了:
那可就说来话长了。。。
对于 measure layout 和 draw ,Android工程师(大都)非常熟悉,我们常常在执行了 onDraw() 方法后,一个让人自豪的自定义View就显示出来了。在实际的Android绘制流程中,第一步就是通过 measure layout 和 draw 这些步骤准备了下面的材料:
在Android的绘制中,我们使用Canvas API进行来告诉表示画的内容,如 drawCircle() drawColor() drawText() drawBitmap() 等,也是这些内容最终呈现在屏幕上。
在当前应用中,View树中所有元素的材料最终会封装到 DisplayList 对象中(后期版本有用 RenderNode 对 DisplayList 又做了一层封装,实现了更好的性能),然后发送出去,这样第一阶段就完成了。
当然就有一个重要的问题:
会将Bitmap复制到下一个阶段(准确地讲就是复制到GPU的内存中)。
现在大多数设备使用了GPU硬件加速,而GPU在渲染来自Bitmap的数据时只能读取GPU内存中的数据, 所以需要赋值Bitmap到GPU内存,这个阶段对应的名称叫 Syncupload 。另外,硬件加速并不支持所有Canvas API,如果自定义View使用了不支持硬件加速的Canvas API(参考 Android硬件加速文档 ),为了避免出错就需要对View进行软件绘制,其处理方式就是生成一个Bitmap,然后复制到GPU进行处理。
这时可能会有问题:如果Bitmap很多或者单个Bitmap尺寸很大,这个过程可能会时间比较久,那有什么办法吗?
当然有(做作。。。)
关于Bitmap这里再多说一句:
Bitmap的内存管理一直是Android程序员很关心的问题,毕竟它是个很占内存的大胖子,在Android3.0~Android7.0,Bitmap内存放在Java堆中,而android系统中每个进程的Java堆是有严格限制的,处理不好这些Bitmap内存,容易导致频繁GC,甚至触发Java堆的 OutOfMemoryError 。从Android8.0开始,bitmap的像素数据放入了native内存,于是Java Heap的内存问题暂时缓解了。
Tip:
现在材料已经备好,我们要真正地画东西了。
接下来就要把东西画出来了,画出来的过程就是把前面的材料转化成一个堆像素数据的过程,也叫 栅格化 ,那这个活儿谁来干呢?
候选人只有两个:
大部分情况下,都是GPU来干这个活儿,因为GPU真的特别快!!!
所谓的“画”,对于计算机来讲就是处理图像,其实就是根据需要(就是DisplayList中的命令)对数据做一些特定类型的数学运算,最后输出结果的过程。我们看到的每一帧精美界面,(几乎)都是GPU吭哧吭哧"算"出来的,这个就有疑问了:
我们简单地聊聊CPU与GPU的区别:
CPU的核心数通常是几个,单个核心的主频高,功能强大,擅长串行处理复杂的流程;
GPU ( Graphics Processing Unit ) 有成百上千个核心,单个核心主频低,功能有限,擅长(利用超多核心)大量并行简单运算;正如它的名字一样,GPU就是为图像绘制这个场景量身定做的硬件(所以使用GPU也叫硬件加速),后来也被用到挖矿和神经网络中。
图片肯定没有视频直观,我们从感性的角度感受一下GPU到底有多快,我想下面的视频看过就不会忘掉,你会被GPU折服:
Mythbusters Demo GPU versus CPU
看这个视频,我们对于“加速”应该有了更深刻的印象,这里不再进一步分析CPU和GPU更微观的差别(因为不懂),我想已经讲明白为什们GPU更快了。
另外,在GPU开始绘制之前,系统也做了一些优化(对DisplayList中的命令进行预处理),让整个绘制流程更加高效:
第二步的具体过程还是很复杂的,比如涉及到Alpha绘制,相关的优化会失效,详情查看文章 为什么alpha渲染性能低 .
至于画在哪里,我们现在理解为一个缓冲(Buffer)中就可以了,具体的机制放在第三步讲。
到此,我们已经画(绘制)完了图像内容,把这个内容发送出去,第二步的任务就完成了。
Tip:
我们知道,除了我们的应用界面,手机屏幕上同时显示着其他内容,比如SystemUI(状态栏、导航栏)或者另外的悬浮窗等,这些内容都需要显示到屏幕上。所以要先 把这些界面的内容合成,然后再显示到屏幕 。
在讲合成图像之前,我们有必要知道这些界面图像(Buffer)是怎么传递的:
Android图形架构中,使用生产者消费者模型来处理图像数据,其中的图像缓冲队列叫 BufferQueue , 队列中的元素叫 Graphic Buffer ,队列有生产者也有消费者;每个应用通常会对应一个 Surface ,一个 Surface 对应着一个缓冲队列,每个队列中 Graphic Buffer 的数量不超过3个, 上面两步后绘制的图像数据最终会放入一个 Graphic Buffer ,应用自身就是队列的生产者( BufferQueue 在Android图形处理中有广泛的应用,当前只讨论界面绘制的场景)。
每个 Graphic Buffer 本身体积很大,在从生产者到消费者的传递过程中不会进行复制的操作,都是用匿名共享内存的方式,通过句柄来跨进程传递。
我们可以通过以下命令来查看手机当前用到的 Graphic Buffer 情况:
关于上面的命令,你可能会好奇这个 SurfaceFlinger 是什么东西啊?
上文提到过每个应用(一般)对应一个 Surface ,从字面意思看, SurfaceFlinger 就是把应用的 Surface 投射到目的地。
实际上, SurfaceFlinger 就是界面(Buffer)合成的负责人,在应用界面绘制的场景, SurfaceFlinger 充当了 BufferQueue 的消费者。绘制好的 Graphic Buffer 会进入(queue)队列, SurfaceFlinger 会在合适的时机(这个时机下文讨论),从队列中取出(acquire)Buffer数据进行处理。
我们知道,除了我们的应用界面,手机屏幕上同时显示着其他内容,比如SystemUI(状态栏、导航栏)或者另外的悬浮窗等,这些部分的都有各自的Surface,当然也会往对应的 BufferQueue 中生产 Graphic Buffer 。
如下图所示, SurfaceFlinger 获取到所有Surface的最新Buffer之后,会配合HWComposer进行处理合成,最终把这些Buffer的数据合成到一个 FrameBuffer 中,而FrameBuffer的数据会在另一个合适的时机(同样下文讨论)迅速地显示到屏幕上,这时用户才观察到屏幕上的变化。
关于上图中的 HWComposer ,它是Android HAL接口中的一部分,它定义了上层需要的能力,让由硬件提供商来实现,因为不同的屏幕硬件差别很大,让硬件提供商驱动自己的屏幕,上层软件无需关心屏幕硬件的兼容问题。
事实上,如果你观察足够仔细的话,可能对上图还有疑问:
同学你观察很仔细(...),事实上,这是 SurfaceFlinger 合成过程中重要的细节,对于不同 Surface 的Buffer, 合成的方法有两种:
显然第一种方法是最高效的,但为了保证正确性,Android系统结合了两种方法。具体实现上, SurfaceFlinger 会询问( prepare ) HWComposer 是否支持直接合成,之后按照结果做对应处理。
有的朋友憋不住了:
Good question! (太做作了。。。)
为了保证最好的渲染性能,上面各个步骤之间并不是串行阻塞运行的关系,所以有一个机制来调度每一步的触发时机,不过在此之前,我们先讲介绍一个更基础的概念:
屏幕刷新率
刷新率是屏幕的硬件指标,单位是Hz(赫兹),意思是屏幕每秒可以刷新的次数。
回到问题,既然屏幕这个硬件每隔一段时间(如60Hz屏幕是16ms)就刷新一次,最佳的方案就是屏幕刷新时开始新一轮的绘制流程,让一次绘制的流程尽可能占满整个刷新周期,这样掉帧的可能性最小。基于这样的思考,在Android4.1(JellyBean)引入 VSYNC(Vertical Synchronization - 垂直同步信号)
收到系统发出的VSYNC信号后, 有三件事会同时执行(并行) :
下图描述了没有掉帧时的VSYNC执行流程,现在我们可以直接回答问题了: 合适的时机就是VSYNC信号 。
从上图可以看出,在一次VSYNC信号发出后,屏幕立即显示2个VSYNC周期(60Hz屏幕上就是32ms)之前开始绘制的图像,这当然是延迟,不过这个延迟非常稳定, 只要前面的绘制不掉链子 ,界面也是如丝般顺滑。当然,Android还是推出一种机制让延迟可以缩小到1个VSYNC周期,详情可参考 VSYNC-offset 。
实际上,系统只会在需要的时候才发出VSYNC信号,这个开关由SurfaceFlinger来管理。应用也只是在需要的时候才接收VSYNC信号,什么时候需要呢?也就是应用界面有变化,需要更新了,具体的流程可以参考 View.requestLayout() 或 View.invalidate() 到 Choreographer (编舞者)的调用过程。这个过程会注册一次VSYNC信号,下一次VSYNC信号发出后应用就能收到了,然后开始新的绘制工作;想要再次接收VSYNC信号就需要重新注册,可见,应用界面没有改变的时候是不会进行刷新的。
我们可以看到,无论是VSYNC开关,还是应用对VSYNC信号的单次注册逻辑,都是秉承着按需分配的原则,这样的设计能够带来Android操作系统更好的性能和更低的功耗。
Tip:
终于。。。说完了
我们简单回顾一下,
更形象一点就是:
之所以有这一节,是因为随着Android版本的更替,渲染方案也发生了很多变化。为了简化表达,我们前文都以当前最新的方案来讲解,事实上,部分流程的实现方式在不同版本可能会有较大的变化,甚至在之前版本没有实现方案,这里我尽可能详细地列出Android版本更迭过程中与渲染相关的更新(包括监控工具)。
如果你居然能读到这里,那我猜你对下面的参考文章也会感兴趣:
;feature=emb_logo
;t=177s
;index=64list=PLWz5rJ2EKKc9CBxr3BVjPTPoDPLdPIFCE
Android Developer Backstage - Android Rendering
Android Developer Backstage - Graphics Performance