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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容