1. FrameBuffer文件介紹
FrameBuffer 文件是 Linux (Android是基于Linux的) 對顯示設備的一種抽象設備,相當于顯存。Android 的 SurfaceFlinger 想更新屏幕的時候,就會把相應的改變寫入到FrameBuffer里。Android 2.x 的時代,顯示開機畫面的功能也是通過把圖像數據寫入到FrameBuffer實現的。所以,你可以認為,FrameBuffer里頭一定有當前屏幕內容的圖像數據。
Android平臺上,FrameBuffer 文件的絕對路徑一般是: /dev/graphics/fb0 。
所以,如果我們想截圖,其中一種方法就是把FrameBuffer里頭的圖像數據取出來,轉換成bitmap,然后存儲起來或者給ImageView來顯示出來。
2. FrameBuffer文件格式
現在我們知道FrameBuffer (/dev/graphics/fb0) 文件里頭會有當前屏幕的圖像數據,取出來就可以了。但是,如果你直接運行這段代碼:
public void test() {
byte[] fb_data = new byte[5000000];
FileInputStream fis = null;
try {
fis = new FileInputStream(new File("/dev/graphics/fb0"));
DataInputStream dStream = new DataInputStream(fis);
dStream.readFully(fb_data);
dStream.close();
Bitmap bm = BitmapFactory.decodeByteArray(fb_data, 0, fb_data.length);
mImageView.setBackground(new BitmapDrawable(bm));
} catch (Exception e) {
e.printStackTrace();
}
}
會遇到兩個問題:
1. /dev/graphics/fb0 文件會拒絕訪問,所以你要讓你的程序獲取root權限后,才能取到/dev/graphics/fb0里頭的數據 ,或者獲取 root 權限后把 /dev/graphics/fb0 文件改為所有用戶可讀 (如何獲取root權限,這篇文章暫不討論)
2. 取到數據后,decode成bitmap,讓imageview顯示,會花屏,或者索性什么都沒顯示。
花屏或者什么都不顯示,那是因為FrameBuffer 里頭的數據并不是常見的圖像數據,直接丟給 BitmapFactory 顯示,BitmapFactory 也不知道你這一堆什么玩意兒,所以出來的圖像要么花屏,要么什么都不顯示。
那我們現在進入正題:FrameBuffer 里頭的數據到底是怎么樣的?
要弄清這個問題,我們需要在我們的jni代碼里執行這段代碼:
int fd, ret;
struct fb_fix_screeninfo finfo;
// 打開Framebuffer設備
fd = open("/dev/graphics/fb0", O_RDONLY);
if(fd < 0)
{
LOGD("======Cannot open /dev/graphics/fb0!");
return -1;
}
// 獲取Framebuffer 的 fixed info 不變信息
ret = ioctl(fd, FBIOGET_FSCREENINFO, &finfo);
if(ret < 0 )
{
LOGD("Cannot get fixed screen information.");
close(fd);
return -1;
}
通過這段代碼,我們獲取到了Framebuffer 設備的 “不變信息” :其實就是一個名叫 fb_fix_screeninfo 的結構體,這個結構體里包含了我們的 Framebuffer 數據的格式。
fb_fix_screeninfo這個結構體定義在 linux/include/linux/fb.h 頭 文件里頭 ( 雖然這個頭文件是linux源碼里頭找的,但是 fb.h 里頭定義的很多東西,Android 都直接沿用了)
定義如下:
struct fb_fix_screeninfo {
char id[16];? ? ? ? ? ? ? ? ? ? /* identification string eg "TT Builtin" */
unsigned long smem_start;? ? ? /* Start of frame buffer mem */
/* (physical address) */
__u32 smem_len;? ? ? ? ? ? ? ? /* Length of frame buffer mem */
__u32 type;? ? ? ? ? ? ? ? ? ? /* see FB_TYPE_*? ? ? ? ? ? ? ? */
__u32 type_aux;? ? ? ? ? ? ? ? /* Interleave for interleaved Planes */
__u32 visual;? ? ? ? ? ? ? ? ? /* see FB_VISUAL_*? ? ? ? ? ? ? */
__u16 xpanstep;? ? ? ? ? ? ? ? /* zero if no hardware panning? */
__u16 ypanstep;? ? ? ? ? ? ? ? /* zero if no hardware panning? */
__u16 ywrapstep;? ? ? ? ? ? ? ? /* zero if no hardware ywrap? ? */
__u32 line_length;? ? ? ? ? ? ? /* length of a line in bytes? ? */
unsigned long mmio_start;? ? ? /* Start of Memory Mapped I/O? */
/* (physical address) */
__u32 mmio_len;? ? ? ? ? ? ? ? /* Length of Memory Mapped I/O? */
__u32 accel;? ? ? ? ? ? ? ? ? ? /* Indicate to driver which? ? */
/*? specific chip/card we have? */
__u16 reserved[3];? ? ? ? ? ? ? /* Reserved for future compatibility */
};
里面有一個__u32 type 成員,就是這個成員會告訴我們,我們的FrameBuffer里頭的數據的格式。
這個__ur32 type 可能的取值有5個,分別如下:
#define FB_TYPE_PACKED_PIXELS? ? ? ? ? 0? ? ? /* Packed Pixels? ? ? ? */
#define FB_TYPE_PLANES? ? ? ? ? ? ? ? ? 1? ? ? /* Non interleaved planes */
#define FB_TYPE_INTERLEAVED_PLANES? ? ? 2? ? ? /* Interleaved planes? */
#define FB_TYPE_TEXT? ? ? ? ? ? ? ? ? ? 3? ? ? /* Text/attributes? ? ? */
#define FB_TYPE_VGA_PLANES? ? ? ? ? ? ? 4? ? ? /* EGA/VGA planes? ? ? */
于是我們回到剛剛那段jni 代碼,最后加一句打印:
LOGD("====== type : %d",? finfo.type);
運行下,會看到我的三星i9300 運行的結果是:
D/termExec(21787): ====== type : 0
其實,大多數Android 設備的 fb0 都應該是 type == 0 的,type 為0 意思說 Framebuffer 里頭存的是每個像素點的 ARGB 信息。
但是既然存的是每個像素點的ARGB 信息,為何 BitmapFactory 會解析出花屏圖像出來呢? 這是因為 Framebuffer 里頭每個像素點的 ARGB 信息是按照 little endian 方式存儲的 也就是說,假如屏幕中一個像素點的格式的 ARGB 顏色值是 #FFBBCCDD 的話,存到 Framebuffer 的時候,是存成這樣的:DDCCBBFF。所以現在你明白為何你把 Framebuffer 的數據直接丟給 BitmapFactory 的時候會花屏了吧? 因為每個像素點的 ARGB 信息都倒過來了啊,變成 BGRA 了。
那是如果確定每個像素點存的時候存的是BGRA 的呢?要確定每個像素點的存儲格式,需要再運行一段 jni 代碼,如下:
int fd, ret;
static struct fb_var_screeninfo vinfo;
// 打開Framebuffer設備
fd = open("/dev/graphics/fb0", O_RDONLY);
if(fd < 0)
{
LOGD("======Cannot open /dev/graphics/fb0!");
return -1;
}
// 獲取FrameBuffer 的 variable info 可變信息
ret = ioctl(fd, FBIOGET_VSCREENINFO, &vinfo);
if(ret < 0 )
{
LOGD("======Cannot get variable screen information.");
close(fd);
return -1;
}
這是獲取到的是一個叫做fb_var_screeninfo 的結構體的實例,這個結構體同樣定義在 linux/include/linux/fb.h 里頭,定義如下:
struct fb_var_screeninfo {
__u32 xres;? ? ? ? ? ? ? ? ? ? /* visible resolution? ? ? ? ? */
__u32 yres;
__u32 xres_virtual;? ? ? ? ? ? /* virtual resolution? ? ? ? ? */
__u32 yres_virtual;
__u32 xoffset;? ? ? ? ? ? ? ? ? /* offset from virtual to visible */
__u32 yoffset;? ? ? ? ? ? ? ? ? /* resolution? ? ? ? ? ? ? ? ? */
__u32 bits_per_pixel;? ? ? ? ? /* guess what? ? ? ? ? ? ? ? ? */
__u32 grayscale;? ? ? ? ? ? ? ? /* != 0 Graylevels instead of colors */
struct fb_bitfield red;? ? ? ? /* bitfield in fb mem if true color, */
struct fb_bitfield green;? ? ? /* else only length is significant */
struct fb_bitfield blue;
struct fb_bitfield transp;? ? ? /* transparency? ? ? ? ? ? ? ? */
__u32 nonstd;? ? ? ? ? ? ? ? ? /* != 0 Non standard pixel format */
__u32 activate;? ? ? ? ? ? ? ? /* see FB_ACTIVATE_*? ? ? ? ? ? */
__u32 height;? ? ? ? ? ? ? ? ? /* height of picture in mm? ? */
__u32 width;? ? ? ? ? ? ? ? ? ? /* width of picture in mm? ? */
__u32 accel_flags;? ? ? ? ? ? ? /* (OBSOLETE) see fb_info.flags */
/* Timing: All values in pixclocks, except pixclock (of course) */
__u32 pixclock;? ? ? ? ? ? ? ? /* pixel clock in ps (pico seconds) */
__u32 left_margin;? ? ? ? ? ? ? /* time from sync to picture? ? */
__u32 right_margin;? ? ? ? ? ? /* time from picture to sync? ? */
__u32 upper_margin;? ? ? ? ? ? /* time from sync to picture? ? */
__u32 lower_margin;
__u32 hsync_len;? ? ? ? ? ? ? ? /* length of horizontal sync? ? */
__u32 vsync_len;? ? ? ? ? ? ? ? /* length of vertical sync? ? ? */
__u32 sync;? ? ? ? ? ? ? ? ? ? /* see FB_SYNC_*? ? ? ? ? ? ? ? */
__u32 vmode;? ? ? ? ? ? ? ? ? ? /* see FB_VMODE_*? ? ? ? ? ? ? */
__u32 rotate;? ? ? ? ? ? ? ? ? /* angle we rotate counter clockwise */
__u32 reserved[5];? ? ? ? ? ? ? /* Reserved for future compatibility */
};
注意到這個結構體里面的四個結構體成員了么?red、green、blue 和 transp 這四個成員。它們的類型是 fb_bitfield,這個fb_bitfield 就是用來告訴我們每個像素點的格式的。
fb_bitfield 也是定義在 linux/include/linux/fb.h 里頭,定義如下:
struct fb_bitfield {
__u32 offset;? ? ? ? ? ? ? ? ? /* beginning of bitfield? ? ? ? */
__u32 length;? ? ? ? ? ? ? ? ? /* length of bitfield? ? ? ? ? */
__u32 msb_right;? ? ? ? ? ? ? ? /* != 0 : Most significant bit is */
/* right */
};
這個結構體里頭:
offset? ------ 顏色值的在整個ARGB二進制數據中的偏移量
length ------ 顏色值二進制位數
msb_right ------ 0 代表Big endian
現在我們在剛剛那段獲取fb_var_screeninfo 結構體實例的 jni 代碼的末尾,加上這幾句打印:
// 下面這一段是每個像素點的格式
LOGD("====== fb_bitfield red.offset : %d",? vinfo.red.offset);
LOGD("====== fb_bitfield red.length : %d",? vinfo.red.length);
// 如果 == 0,就是Big endian
LOGD("====== fb_bitfield red.msb_right : %d",? vinfo.red.msb_right);
LOGD("====== fb_bitfield green.offset : %d",? vinfo.green.offset);
LOGD("====== fb_bitfield green.length : %d",? vinfo.green.length);
LOGD("====== fb_bitfield green.msb_right : %d",? vinfo.green.msb_right);
LOGD("====== fb_bitfield blue.offset : %d",? vinfo.blue.offset);
LOGD("====== fb_bitfield blue.length : %d",? vinfo.blue.length);
LOGD("====== fb_bitfield blue.msb_right : %d",? vinfo.blue.msb_right);
LOGD("====== fb_bitfield transp.offset : %d",? vinfo.transp.offset);
LOGD("====== fb_bitfield transp.length : %d",? vinfo.transp.length);
LOGD("====== fb_bitfield transp.msb_right : %d",? vinfo.transp.msb_right);
看看我的i9300 的運行結果:
D/termExec(21988): ====== fb_bitfield red.offset : 16
D/termExec(21988): ====== fb_bitfield red.length : 8
D/termExec(21988): ====== fb_bitfield red.msb_right : 0
D/termExec(21988): ====== fb_bitfield green.offset : 8
D/termExec(21988): ====== fb_bitfield green.length : 8
D/termExec(21988): ====== fb_bitfield green.msb_right : 0
D/termExec(21988): ====== fb_bitfield blue.offset : 0
D/termExec(21988): ====== fb_bitfield blue.length : 8
D/termExec(21988): ====== fb_bitfield blue.msb_right : 0
D/termExec(21988): ====== fb_bitfield transp.offset : 24
D/termExec(21988): ====== fb_bitfield transp.length : 8
D/termExec(21988): ====== fb_bitfield transp.msb_right : 0
從打印結果我們可以看到:
1. 每個像素點的單個顏色值占8 bits 也就是一個字節,一個像素是 8 * 4 = 32 bits。
2. blue offset 是0 也就是 Framebuffer里頭存的每個像素點的 前8 bits 是藍色值
3. green offset 是 8 ,8 到 15 bits 是綠色值
4. red offset 是 16 ,16 到 23 bits 是紅色值
5. transp offset 是 24,24 到 31 bits 是透明度值
綜合上面所述,Framebuffer 中的數據肯定是這樣的 :
BGRA BGRA BGRA BGRA BGRA BGRA BGRA BGRA BGRA BGRA BGRA BGRA BGRA BGRA BGRA BGRA BGRA BGRA ...
啊!Framebuffer 的數據格式現在是搞清楚了,但是當你以為你簡簡單單的把 每個像素的 BGRA 信息 轉換成 ARGB 后,丟給BitmapFactory就能得到屏幕截圖了么?
其實還不夠。
首先,回頭再看看fb_var_screeninfo 的定義,我們注意到里頭有六個這樣的成員:
__u32 xres;? ? ? ? ? ? ? ? ? ? /* visible resolution? ? ? ? ? */
__u32 yres;
__u32 xres_virtual;? ? ? ? ? ? /* virtual resolution? ? ? ? ? */
__u32 yres_virtual;
__u32 xoffset;? ? ? ? ? ? ? ? ? /* offset from virtual to visible */
__u32 yoffset;
前兩個個的的含義如下:
1. xres -------------- 你可以認為這個就是屏幕的寬(單位:像素)
2.yres --------------- 你可以認為這個就是屏幕的高(單位:像素)
前兩個很好理解,后面四個就麻煩一點點。稍微查下資料我們就能知道,Android 的 Framebuffer 一般是“雙緩沖”的,就是說 Framebuffer里頭不止緩存了一個屏幕的像素點數據,而是緩存了兩個屏幕的像素點數據。而且兩屏數據,是上下擺放的,假設我們的手機屏幕橫向有 720 個像素,縱向有 1280 個像素,那 fb0 的實際格式將會是如下所示:
第一列是fb0 文件的相對地址(十進制表示) 720個像素點 * 每個像素點占用4字節 = 2880
0 ? ? BGRA BGRA BGRA BGRA BGRA BGRA BGRA ...
2880 ? ? BGRA BGRA BGRA BGRA BGRA BGRA BGRA ...
2880*2 ? ? BGRA BGRA BGRA BGRA BGRA BGRA BGRA ...
.
. 緩沖的第一屏
.
2880*1279? BGRA BGRA BGRA BGRA BGRA BGRA BGRA ...
2880*1280? BGRA BGRA BGRA BGRA BGRA BGRA BGRA ...
2880*1281? BGRA BGRA BGRA BGRA BGRA BGRA BGRA ...
2880*1282? BGRA BGRA BGRA BGRA BGRA BGRA BGRA ...
.
. 緩沖的第二屏
.
2880*2559? BGRA BGRA BGRA BGRA BGRA BGRA BGRA ...
把這兩屏數據當成一副圖,那橫向像素點個數就是xres_virtual,縱向像素點個數就是 yres_virtual。由于Android一般是“雙緩沖”,所以下面這個公式對很多手機都應該成立:
xres == xres_virtual
yres * 2 == yres_virtual
既然有兩屏數據,那哪一屏才是當前的屏幕內容呢?這就是xoffset 和 yoffset 會告訴你的,我的 i9300 獲取到的這樣的:
D/termExec(21988): ====== xres : 720
D/termExec(21988): ====== yres : 1280
D/termExec(21988): ====== xres_virtual : 720
D/termExec(21988): ====== yres_virtual : 2560
D/termExec(21988): ====== xoffset : 0
D/termExec(21988): ====== yoffset : 0
所以我的i9300 的 Framebuffer 的第一屏就是當前屏幕的內容。 假如我的 i9300 yoffset == 1280 的話,那第二屏才是真正屏幕的當前內容。
fb_fix_screeninfo 里頭,另外還有一個需要注意的成員是 line_length。其實我們的 Framebuffer 里頭保存的一屏數據并不一定剛好就是我們的屏幕分辨率大小,它的橫向像素值有可能比屏幕的橫向像素值多!假如你發現你截出來的圖片屏幕外有黑邊,那原因就在這里了。
Framebuffer里頭的圖像數據的一行,不應該是 屏幕橫向分辨率 * 4,而應該是 line_length (它的單位不是像素點,而是字節)
3. 從Framebuffer中獲取圖像數據
現在我們已經知道了,Framebuffer 中的數據格式,那到底如何中的屏幕圖像數據顯示在ImageView中呢?
當然,你已經知道了Framebuffer中的數據格式了,你大可以很自信的自己把一屏的數據截取出來,把BGRA轉換成ARGB,然后轉換成bitmap,然后丟給ImageView顯示出來。比如,下面這段代碼:
long stat2 = System.currentTimeMillis();
for (int i = 0; i < pixels.length; i+=4) {
row = i / line_length;
if (row >= h) break;
if ((i - row * line_length) >= widthBytes) continue;
// fb0里面存儲的BGRA中的A都是FF
pixels[offset] =? (0xFF << 24) | ((Fb0Bytes[i + 2] & 0xFF) << 16) | ((Fb0Bytes[i + 1] & 0xFF) << 8) | (Fb0Bytes[i] & 0xFF);
offset++;
}
Logger.d("Moce time =? " + (System.currentTimeMillis() - stat2));
這段代碼你可以不用細看,你只要知道我只讀取一屏的數據,然后每次讀取4bytes,把BGRA轉換成了ARGB。但是這段代碼的平均執行時間是 250ms 。這實在是太慢了。為了提速,我第一個想到的是改用C語言來寫,但是并沒有質的提升。最后經過一番搜索,發現了一個很有名的開源庫:turbo-jpeg !turbo-jpeg 會調用Arm cpu 的 Neon 協處理器的 SIMD 指令集,效率非常高!
4. 實例截圖功能的完整Android demo項目
我上傳了一個通過jni 實現截圖的功能完整demo項目到Github了,地址如下:https://github.com/faip520/AndroidFramebufferScreenshot
我簡單描述下實現的過程:
1. 打開 /dev/graphics/fb0 設備
2.把 fb0 設備內容中的一屏數據,通過 mmap 映射到自己的內存區
3.通過 turbo-jpeg 的接口,直接讀取 fb0 的信息,生成 jpeg 圖片數據
4.把得到的 jpeg 圖片數據返回到 java 層
5.通過BitmapFactory.decode 方法把 jpeg 圖片數據轉換成 bm,然后轉換成 BitmapDrawable 給 ImageView 顯示。
需要說明的是:我這份代碼,肯定不是適配所有機型的,你的機型有可能是
“三緩沖”,有可能不是 32位色,而是 RGB565 或者其他格式,xoffset 和 yoffset 的值也有可能
比較特別,甚至有可能Framebuffer 都不是 fb0
文件。這些情況下,你就要自己去修改我的代碼了。我這里只是介紹一個解決這種問題的分析模型。
5. 黑邊問題的處理 (圖像裁剪)
其實如果你的截圖出來有黑邊,或者只想截取其中一部分。可以看看我源碼里頭的這個地方:
tjCompress2(handle, framebuffer_memory,
// 希望生成的jpeg圖片的寬 源數據里頭屏幕每行的字節數
300, finfo.line_length,
// 希望生成的jpeg圖片的高
100, TJPF_BGRA,
&jpeg_data, &jpegSize,
TJSAMP_444, 10,
TJFLAG_NOREALLOC);
其中第三個和第五個參數,就是你希望生成的jpeg 的寬高。比如 100 100,那就是只截取屏幕左上角的 100 * 100 個像素。配置這里就可以去掉黑邊,或者圖片裁剪。
? RAS