Android屏幕截圖研究

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容