高效計(jì)算——****RenderScript
RenderScript是安卓平臺上很受谷歌推薦的一個(gè)高效計(jì)算平臺,它能夠自動(dòng)把計(jì)算任務(wù)分配到各個(gè)可用的計(jì)算核心上,包括CPU,GPU以及DSP等,提供十分高效的并行計(jì)算能力??赡苁怯捎趹?yīng)用開發(fā)時(shí)的需求不夠,關(guān)于RenderScript的相關(guān)文章很少,剛好我在工作中應(yīng)用到此平臺,做了一些筆記,因此決定整理成博文分享給大家。內(nèi)容主要來源于官方文檔、StackOverflow以及自己的理解,如有錯(cuò)誤,請大家指正。本篇主要介紹RenderScript的基本概念。
1 RenderScript****簡介
RenderScript是安卓提供的一個(gè)高效計(jì)算平臺。它顯著的特點(diǎn)在于:
- 能夠自動(dòng)利用各種核心,包括CPU,GPU以及DSP等,來進(jìn)行并行計(jì)算,能大大提高在圖片處理、數(shù)學(xué)模型等領(lǐng)域提供高效的計(jì)算能力;
- 不需要針對不同的核心平臺而編寫不同的代碼,因?yàn)镽enderScript是在設(shè)備上進(jìn)行運(yùn)行時(shí)編譯的。
使用了RenderScript的應(yīng)用與一般的安卓應(yīng)用在代碼編寫上與并沒有太大區(qū)別。使用了RenderScript的應(yīng)用依然像傳統(tǒng)應(yīng)用一樣運(yùn)行在VM中,但是你需要給你的應(yīng)用編寫你所需要的RenderScript代碼,且這部分代碼運(yùn)行在native層。
RenderScript采用從屬控制架構(gòu):底層RenderScript被運(yùn)行在虛擬機(jī)中的上層安卓系統(tǒng)所控制。安卓VM負(fù)責(zé)所有內(nèi)存管理并把它分配給RenderScript的內(nèi)存綁定到RenderScript運(yùn)行時(shí),所以RenderScript代碼能夠訪問這些內(nèi)存。安卓框架對RenderScript進(jìn)行異步調(diào)用,每個(gè)調(diào)用都放在消息隊(duì)列中,并且會被盡快處理。

RenderScript工作流程需要經(jīng)歷三層:
- RenderScript運(yùn)行時(shí)API:提供進(jìn)行運(yùn)算的API
- 反射層:相當(dāng)于NDK中的JNI膠水代碼,它是一些由安卓編譯工具自動(dòng)生成的類,對你寫的RenderScript代碼進(jìn)行包裝,使得安卓層能夠和RenderScript進(jìn)行交互
- 安卓框架:通過調(diào)用反射層來訪問RenderScript運(yùn)行時(shí)
RenderScript的主要優(yōu)點(diǎn):
- 可移植性:對于不同架構(gòu),不同的處理器都不需要考慮代碼的差異化,因?yàn)槎际沁\(yùn)行時(shí)在設(shè)備上進(jìn)行編譯的;
- 高性能:提供充分利用所有核心的無縫的并行化計(jì)算
- 易用性:簡化編碼,不需要像JNI一樣寫膠水代碼
缺點(diǎn):
- 開發(fā)復(fù)雜:需要去學(xué)習(xí)新的api
- 調(diào)試可見性:因?yàn)镽enderScript可能運(yùn)行在除了主cpu之外的處理器上,所以調(diào)試?yán)щy
2 ****使用****RenderScript
使用RenderScript需要對編譯或者開發(fā)環(huán)境進(jìn)行一定的配置。
使用RenderScript主要分為兩個(gè)步驟:編寫.rs文件以及在Android framework中使用RenderScript,下面分別介紹。
2.1 ****環(huán)境配置
- RenderScript的API可以有兩種來源方式:
對于Android 3.0 (API level 11)及以上的可以在android.renderscript包中獲取
通過android.support.v8.renderscript包獲取,可以支持API level 8及以上的平臺,官方強(qiáng)烈建議使用此支持包的方式來獲取API
- 編譯環(huán)境要求:
Android SDK Tools revision 需要22.2及以上
Android SDK Build-tools revision 需要18.1.0及以上
- 在project.properties文件中寫入如下屬性:
renderscript.target=18
renderscript.support.mode=true
或者在AS中的build.gradle的defaultConfig中添加
renderscriptTargetApi 18
renderscriptSupportModeEnabled true
注意:target的值應(yīng)該為11及以上,但推薦使用18.如果在Manifest中配置的minSDK的值與target的值不相同,那么在編譯的時(shí)候,將使用target的值替代Mainfest中的minSDK值。
2.2 ****編寫****RenderScript****文件
RenderScript代碼放在.rs或者.rsh文件中,在RenderScript代碼中包含計(jì)算邏輯以及聲明所有必須的變量和指針,通常一個(gè).rs文件包含如下幾個(gè)部分:
- 編譯聲明:#pragma rs java_package_name(package.name),比如#pragma rs java_package_name (com.willhua.RenderScript),用來聲明本rs所在的java包。注意:.rs文件只能在應(yīng)用程序包中,而不能在library項(xiàng)目中。
- 編譯聲明:#pragma version(1).聲明RenderScript版本,現(xiàn)在都是1
- 主工作函數(shù)root().它會被RenderScript層的reForEach函數(shù)調(diào)用,實(shí)現(xiàn)多處理器對root工作的并行處理。Root函數(shù)必須返回void以及接受如下參數(shù)
1.分配給RenderScript的輸入輸出地址的指針。在Android3.2以及更低版本中,輸入輸出的指針都需要,在Android4.0及以后的版本中,給出其中一個(gè)或者兩個(gè)都可以
- 2.下面兩個(gè)參數(shù)是可選的,但是只要用了其中一個(gè)就必須兩個(gè)都提供
a) 指向用戶數(shù)據(jù)的指針。該數(shù)據(jù)會在RenderScript的計(jì)算中用到。該數(shù)據(jù)可以指向原始類型或者復(fù)雜結(jié)構(gòu)類型
b) 用戶數(shù)據(jù)的大小
從官方文檔來看,老版本的文檔中有介紹root,而新版本的則用kernel替代。官方在弱化root函數(shù)的概念,而是推薦使用kernel概念。本質(zhì)上來說,root僅僅是一個(gè)寫法形式上特殊的kernel而已。
- 可選init()函數(shù)??梢杂脕碜鋈魏纬跏蓟ぷ?,比如初始化變量。它將會在每次RenderScript啟動(dòng)的時(shí)候,在其他任何代碼之前執(zhí)行一次
- 一些invokable函數(shù)。這些函數(shù)都是單線程函數(shù)(kernel函數(shù)的工作則是并行工作的),你可以給這些函數(shù)傳遞任意數(shù)量的參數(shù)。這些函數(shù)將會在反射層中生成對應(yīng)的版本,可以從Android framework中調(diào)用。這些函數(shù)一般用來做一些初始化工作或者當(dāng)做計(jì)算任務(wù)中的一個(gè)串行計(jì)算單元任務(wù)。注意:invokable函數(shù)不能是static的。
- 一些計(jì)算內(nèi)核(compute kernel)。計(jì)算內(nèi)核是并行執(zhí)行的,它將并行處理輸入Allocation中的每一個(gè)Element。一個(gè)簡單的compute kernel如下:
uchar4 __attribute__((kernel)) invert(uchar4 in, uint32_t x, uint32_t y) {
uchar4 out = in;
out.r = 255 - in.r;
out.g = 255 - in.g;
out.b = 255 - in.b;
return out;
}
compute kernel基本與一個(gè)C函數(shù)一樣,但是有如下特征:
a) attribute((kernel))標(biāo)志。該標(biāo)志表示該函數(shù)是一個(gè)RenderScript kernel函數(shù),而不是一個(gè)invokable函數(shù)
b) in參數(shù)及其類型。在RenderScript kernel中,這個(gè)參數(shù)將會基于傳給kernel的輸入Allocation而自動(dòng)賦值,且默認(rèn)情況下,對于Allocation中每一個(gè)Element都將會執(zhí)行一遍kernel函數(shù)
c) 返回值及其類型。每次kernel函數(shù)執(zhí)行的返回值將會自動(dòng)寫入到輸出Allocation的正確位置。RenderScript將會對輸入輸出Allocation進(jìn)行檢查,如果他們與kernel函數(shù)聲明不匹配則將拋出異常。
每個(gè)kernel都應(yīng)該有一個(gè)輸入Allocation或者一個(gè)輸出Allocation或者二者都有,但不能有兩個(gè)及以上的輸入或者輸出Allocation。如果需要在kernel中訪問多個(gè)輸入或者輸出,則需要聲明rs_allocation全局變量來擔(dān)任多余一個(gè)的輸入或者輸出角色,然后再kernel函數(shù)或者invokable函數(shù)中通過rsGetElementAt_type()或者rsSetElementAt_type()來訪問或者設(shè)置相應(yīng)的Allocation,其中type為對應(yīng)Allocation的Element類型對應(yīng)的數(shù)據(jù)類型,比如uchar4。
在kernel中,可以通過可選的xyz參數(shù)來獲取當(dāng)前Element在整個(gè)Allocation中的坐標(biāo)值,比如上面的invert中就通過xy來獲取了xy坐標(biāo)值。注意xyz的參數(shù)名不能設(shè)置為其他名稱,且類型必須為uint32_t。
- 任何要在RenderScript中用到的變量,指針以及結(jié)構(gòu)。這些聲明也可以在.rsh文件中
- 所需要的script變量。就和C中的全局變量一樣,這些全局變量一般用來傳遞參數(shù)給計(jì)算kernel。
- 一些靜態(tài)變量以及函數(shù)。靜態(tài)的變量與普通全局變量的區(qū)別在于:靜態(tài)變量不會在映射在反射層,也即無法從Android framework中調(diào)用;靜態(tài)函數(shù)就是一個(gè)標(biāo)準(zhǔn)的C函數(shù),但是不會映射到反射層,也無法從Android framework中調(diào)用,但是可以在RenderScript中的kernel或者invokable中調(diào)用。如果有變量或者函數(shù)需要在RenderScript中使用但是不需要在Java中使用,強(qiáng)烈推薦設(shè)置為static的。
- 可選的精度控制配置,主要有三個(gè)等級:
a) #pragma rs_fp_full:默認(rèn)的等級。表示的完全遵守IEEE 754-2008 standard的精度要求
b) #pragma rs_fp_relaxed:不嚴(yán)格的IEEE 754-2008 standard的精度要求
c) #pragma rs_fp_imprecise:比relaxed更低的精度要求
對于大部分應(yīng)用來說,使用relaxed精度要求都可以滿足要求而無任何副作用
example.rs :
#pragma version(1)
#pragma rs java_package_name(com.willhua.rgbtoyuv)
#pragma rs_fp_relaxed
typedef struct Point_T{
int x;
int y;
}Point;
//script variable
uint32_t inW;
uint32_t inH;
uint32_t inCount;
rs_allocation outYUV;
struct Point point;
//root
void root(const uchar4 *in, uint32_t x, uint32_t y){
struct myStruct my;
my.x = 0;
struct myStruct my2 = my;
int u = my.x;
uchar R,G,B;
int Y,U,V;
R = (*in).r;
G = (*in).g;
B = (*in).b;
Y = ( ( 66 * R + 129 * G + 25 * B + 128) >> 8) + 16;
uint32_t yIndex = y * inW + x;
Y = ((Y < 0) ? 0 : ((Y > 255) ? 255 : Y));
rsSetElementAt_uchar(outYUV, ((uchar)Y), yIndex);
if((x & 1) == 0 && (y & 1) == 0) {
U = ( ( -38 * R - 74 * G + 112 * B + 128) >> 8) + 128;
V = ( ( 112 * R - 94 * G - 18 * B + 128) >> 8) + 128;
uint32_t index = (y >> 1) * inW + x + inCount;
U = ((U < 0) ? 0 : ((U > 255) ? 255 : U));
V = ((V < 0) ? 0 : ((V > 255) ? 255 : V));
rsSetElementAt_uchar(outYUV, ((uchar)U), index + 1);
rsSetElementAt_uchar(outYUV, ((uchar)V), index );
}
}
//compute kernel
__attribute__((kernel)) invert(uchar4 in, uint32_t x, uint32_t y) {
uchar4 out = in;
out.r = 255 - in.r;
out.g = 255 - in.g;
out.b = 255 - in.b;
return out;
}
//invokable function
void setInPara(uint32_t w, uint32_t h){
inW = w;
inH = h;
inCount = w * h;
}
//init
void init(){
}
View Code
2.3 ****在****Android framework****層調(diào)用****RenderScript
雖然各個(gè)應(yīng)用使用RenderScript細(xì)節(jié)各不相同,但大體有著這樣的模式:
- 初始化RenderScript context。通過RenderScript.create函數(shù)可以創(chuàng)建相應(yīng)的context,有了該context才可以進(jìn)行RenderScript的其他動(dòng)作,并通過該context可以控制其他RenderScript對象的生命周期。因?yàn)閏ontext的創(chuàng)建需要在不同的硬件設(shè)備上創(chuàng)建資源,所以可能會比較耗時(shí)。因此,最好不要讓context的創(chuàng)建在響應(yīng)速度要求比較高的時(shí)間點(diǎn)上。一般來說,一個(gè)應(yīng)用應(yīng)該只有一個(gè)context。
- 至少需要?jiǎng)?chuàng)建一個(gè)Allocation。用的比較多的方法是createTyped(RenderScript, Type) 或createFromBitmap(RenderScript, Bitmap)。
- 創(chuàng)建需要的scripts。可以分為兩種,一種就是通過自定義.rs文件,然后在反射層自動(dòng)生成的ScriptC子類,名字為ScriptC_rsfilename;還有一種就是系統(tǒng)定義的一些scripts,比如高斯模糊等,他們都是ScriptIntrinsic的子類。
- 給Allocation填充數(shù)據(jù)。使用Allocation的copy系類方法
- 設(shè)置必要的script變量。在ScriptC_rsfilename會給script變量生成相應(yīng)的set方法,比如int型名為num的script變量則會在反射層生成set_num(int)方法。
- 啟動(dòng)計(jì)算。.rs中定義的kernel函數(shù)都會在反射層的ScriptC_rsfilename類中生成forEach_kernalname方法。該方法的執(zhí)行是異步的,且會在RenderScript中按照kernel調(diào)用順序來執(zhí)行。前面提到過,forEach_kernalname方法默認(rèn)對對應(yīng)Allocation中所有的Element執(zhí)行計(jì)算,但是可以在forEach_kernalname參數(shù)的最后傳入一個(gè)Scrpit.LunchOptions參數(shù)來指定Allocation中需要被計(jì)算的子集。.rs中定義的invokable函數(shù)則會在反射層生成invoke_functionname的方法,也可以按需調(diào)用
- 從Allocation中得到數(shù)據(jù)。通過Allocation的copyTo系類方法,可以把Allocation中的數(shù)據(jù)放到Java數(shù)據(jù)中。這些copyTo方法與forEach方法保持同步,forEach的計(jì)算完成之后就會自動(dòng)啟動(dòng)copy過程。目前只支持基本類型的copyTo,暫不支持比如自定義的struct等數(shù)據(jù)。
- 釋放資源。通過RenderScript.destory方法來釋放相關(guān)資源。
2.4 RenderScript****工作流程
最開始就提到,RenderScript是一個(gè)主從架構(gòu),底層的RenderScript被上層的Android framework所控制。其工作流程也正是如此。從在Android framework創(chuàng)建RenderScript的context開始,然后給RenderScript層分配、綁定相關(guān)內(nèi)存,對script變量進(jìn)行初始化,然后調(diào)用forEach函數(shù)通知啟動(dòng)RenderScript計(jì)算。RenderScript將會自動(dòng)把它的計(jì)算任務(wù)分配到各個(gè)可用的核心上來完成計(jì)算任務(wù)(現(xiàn)在還只能支持CPU,以后將會支持到GPU以及DSP,且代碼不需要變動(dòng))。RenderScript計(jì)算完成以后將會自動(dòng)把計(jì)算結(jié)果放到相應(yīng)的Allocation內(nèi)存,然后在Android framework層再從Allocation中copy出數(shù)據(jù),最后Android framework層命令RenderScript釋放資源,流程介紹。下圖展示了RenderScript的工作流程: