FrameBuffer中获取Android屏幕截图

时间:2022-04-19 15:24:28
引子 我们知道,DDMS可以很容易的获取Android 手机 的屏幕截图,那么它是怎么做到的呢?  其实,android手机上有一个叫做FrameBuffer的设备,图像信息都是通过FrameBuffer写到手机屏幕上去的。因此可以通过读取此设备中的数据来获取当前正在显示的图像。当然DDMS也是这么做到的。 FrameBuffer 对应的设备文件就是/dev/graphics/fb0。因此我们可以通过下面的代码读取屏幕图像数据。其中传入的参数fd为一个文件描述符,也可以是 socket描述符。这样我们就可以把从fb中读取的屏幕图像信息传递给我们自己的应用,从而获取手机屏幕信息。 [cpp] view plaincopy
  1. void framebuffer_service(int fd)  
  2. {  
  3.     struct fb_var_screeninfo vinfo;  
  4.     int fb, offset;  
  5.     char x[256];  
  6.    
  7.     struct fbinfo fbinfo;  
  8.     unsigned i, bytespp;  
  9.    
  10.     fb = open("/dev/graphics/fb0", O_RDONLY);  
  11.     if(fb < 0) goto done;  
  12.    
  13.     if(ioctl(fb, FBIOGET_VSCREENINFO, &vinfo) < 0) goto done;  
  14.     fcntl(fb, F_SETFD, FD_CLOEXEC);  
  15.    
  16.     bytespp = vinfo.bits_per_pixel / 8;  
  17.    
  18.     fbinfo.version = DDMS_RAWIMAGE_VERSION;  
  19.     fbinfo.bpp = vinfo.bits_per_pixel;  
  20.     fbinfo.size = vinfo.xres * vinfo.yres * bytespp;  
  21.     fbinfo.width = vinfo.xres;  
  22.     fbinfo.height = vinfo.yres;  
  23.     fbinfo.red_offset = vinfo.red.offset;  
  24.     fbinfo.red_length = vinfo.red.length;  
  25.     fbinfo.green_offset = vinfo.green.offset;  
  26.     fbinfo.green_length = vinfo.green.length;  
  27.     fbinfo.blue_offset = vinfo.blue.offset;  
  28.     fbinfo.blue_length = vinfo.blue.length;  
  29.     fbinfo.alpha_offset = vinfo.transp.offset;  
  30.     fbinfo.alpha_length = vinfo.transp.length;  
  31.    
  32.     /* HACK: for several of our 3d cores a specific alignment 
  33.      * is required so the start of the fb may not be an integer number of lines 
  34.      * from the base.  As a result we are storing the additional offset in 
  35.      * xoffset. This is not the correct usage for xoffset, it should be added 
  36.      * to each line, not just once at the beginning */  
  37.     offset = vinfo.xoffset * bytespp;  
  38.    
  39.     offset += vinfo.xres * vinfo.yoffset * bytespp;  
  40.     printf("offset %d/n", offset);  
  41.    
  42.     if(writex(fd, &fbinfo, sizeof(fbinfo))) goto done;  
  43.    
  44.     lseek(fb, offset, SEEK_SET);  
  45.     for(i = 0; i < fbinfo.size; i += 256) {  
  46.       if(readx(fb, &x, 256)) goto done;  
  47.       if(writex(fd, &x, 256)) goto done;  
  48.     }  
  49.    
  50.     if(readx(fb, &x, fbinfo.size % 256)) goto done;  
  51.     if(writex(fd, &x, fbinfo.size % 256)) goto done;  
  52.    
  53. done:  
  54.     if(fb >= 0) close(fb);  
  55.     close(fd);  
  56. }  
(evilcode)
 

简单介绍:

截屏一般有三种方法:

1.           直接获取到一个view,然后通过ViewCache来获取一个bitmap对象,然后将bitmap对象写到图像文件。

2.           ddmslib截屏,ddmslibandroid内置的一个库,在pc端的截屏都是使用的这种方式,但在网上找不到使用这种方法的资料。

3.           framebuffer截屏,网上流传的都是这种方法,不过只有root后的设备能使用

根据各种方法的特点,我们选择的是framebuffer的方式,View的方式也简单介绍一下。由于截屏需要一定的处理时间,而且需要在后台运行,用service来实现是比较好的选择。                          

 

View截屏的简单实现

使用view截屏的话首先要获取当前的界面的view

Activity中获取view是非常容易的,但是仅限于当前Activityview,这种方法截屏是有限制的,截取的图像没有状态栏。在Activity中截屏的代码如下:

取得bitmap

View view = v.getRootView();

view.setDrawingCacheEnabled(true);

view.buildDrawingCache();

Bitmap bitmap = view.getDrawingCache();

 

然后将bitmap保存为png格式:

    FileOutputStream out new FileOutputStream(file_name);

    bitmap.compress(Bitmap.CompressFormat.PNG, 100, out);

 

framebuffer截屏的实现

framebufferlinux内核对显示的最底层驱动。在一般的linux文件系统中,通过/dev/fb0设备文件来提供给应用程序对framebuffer进行读写的访问。这里,如果有多个显示设备,就将依次出现fb1,fb2,…等文件。而在我们所说的android系统中,这个设备文件被放在了/dev/graphics/fb0,而且往往只有这一个。

 

这种方法使用的最为普遍,linux系统中经常使用这种方法实现截屏,一般步骤是:

1.      读取framebuffer

2.      Framebuffer转换为bitmap

3.      bitmap生成图像文件

 

读取framebuffer

android上使用这种方法第一个难题是获取framebuffer,因为默认的配置中framebuffer的读取权限是“root”,而Apk的权限最高只能提升到“system”,framework工作的权限也是“system”,也因此网上提供的截屏软件都只能在root过的手机上使用。对拥有源码的人来说,最简单的方法是直接改变framebuffer的权限,普通用户也有权限读取framebuffer

然后就可以通过读文件的方式直接读取framebuffer,利用如下代码打开一个指向framebuffer的输入流就可以读取了。

    public static InputStream getInputStream() throws Exception {

       FileInputStream buf = new  FileInputStream(

new File("/dev/graphics/fb0"));

       return buf;

}//get the InputStream from framebuffer

 

framebufferbitmap的转换

framebuffer的数据是直接送入显示设备的,这些数据没有文件头,而且由于framebuffer读到数据跟显示方式关系很大,在不同设备上framebuffer的大小和数据格式不一样,读取前确定framebuffer的数据格式。

       获取屏幕大小:

       DisplayMetrics metrics = new DisplayMetrics();

    WindowManager WM = (WindowManager) mContext

                  .getSystemService(Context.WINDOW_SERVICE);

    Display display = WM.getDefaultDisplay();

    display.getMetrics(metrics);

    int height = metrics.heightPixels; //屏幕高

    int with = metrics.widthPixels    //屏幕的宽

 

    获取显示方式

    int pixelformat = display.getPixelFormat();

    PixelFormat localPixelFormat1 = new PixelFormat();

    PixelFormat.getPixelFormatInfo(pixelformat, localPixelFormat1);

    int deepth = localPixelFormat1.bytesPerPixel;//位深

 

pixelformat代表的意义是在PixelFormat.java定义的:

    public static final int RGBA_8888   = 1;

    public static final int RGBX_8888   = 2;

    public static final int RGB_888     = 3;

    public static final int RGB_565     = 4;

 

    public static final int RGBA_5551   = 6;

    public static final int RGBA_4444   = 7;

    public static final int A_8          = 8;

    public static final int L_8          = 9;

    public static final int LA_88        = 0xA;

    public static final int RGB_332     = 0xB;

      

pixelformat是下面进行判断处理的依据,根据pixelformat计算出实际的深度。

一般

       Framebuffer大小 height* with* deepth*2

之所以“*2”,是因为Framebuffer包含两帧画面,我们使用任何一帧都可以。

 

piex = new byte[height * with * deepth];

InputStream stream = getInputStream(

new File("/dev/graphics/fb0"));

       DataInputStream dStream = new DataInputStream(stream);

       dStream.readFully(piex);

              这样framebuffer的数据就被写进了piex

piex 生成bitmap

       bitmap = Bitmap.createBitmap(datamwidthmheightconfig);

        data:像素数据,data的一个元素表示一个像素,所以这里不能直接使用piex,必须经过转换。

              mwidthmheight:图像大小

       config:图像格式,可以取以下数值

        Bitmap.Config.ALPHA_8     (2),

        Bitmap.Config.RGB_565     (4),

        Bitmap.Config.ARGB_4444   (5),

        Bitmap.Config.ARGB_8888   (6);

 

bitmap保存为png格式

这是跟上面一样的

    FileOutputStream out new FileOutputStream(file_name);

    bitmap.compress(Bitmap.CompressFormat.PNG, 100, out);

 

这样截取的一幅图片就被保存到了固定位置。

 

快捷键的添加

    由于快捷键是全局的,所以要在PhoneWindowManager中添加

public int interceptKeyBeforeQueueing(

long whenNanos, int action, int flags,

int keyCode, int scanCode, int policyFlags,

boolean isScreenOn)

 

这个函数是在按键事件进入消息队列之前调用的,所以先与所有app相应事件。但是在这个函数对于每个按键事件是单独处理的,组合键需要我们自己来识别。

下图分别是power键和volumedown键的按键处理,黄色区域为新增加代码。由于现在power键的长按和短按都是有事件响应的,所以设计的关键在于必须在长按和短按的间隙处理截屏事件,而且截屏后保证power的长按不在响应。

 

 

service设计:

这个服务的功能比较单一,只在启动时响应请求就可以,不需要驻留在后台,也不需要调用接口,所以不需要AIDL,也不需要远程连接。

客户端,使用intent启动服务

Intent intent = new Intent();

intent.setAction("com.android.ScreenServer.SHOT");

mContext.startService(intent);

在应用程序端

       声明权限使用SD

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

 

       Intentfilter过滤事件

    <application

        android:icon="@drawable/ic_launcher"

        android:label="@string/app_name" >

        <service

            android:name="shotservice" >

            <intent-filter >

                <action android:name="com.android.ScreenServer.SHOT"/>

            </intent-filter>  

        </service>

</application>

 

程序代码:

    public void onStart(Intent intent, int startId) {

       // TODO Auto-generated method stub

       super.onStart(intent, startId);

       mContext this;

       。。。

       使用framebuffer截屏

       。。。

}

这样截屏结束后,Service的生命周期就结束了,系统会自动销毁Service