對于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