周围有位罗总的忠实粉丝,于是乎平时有意无意的被灌注了大量“锤子”的信息。也许就是这些无意,我昨晚也就无意中点进了锤子手机发布会的直播,看着看着最后就看完了。
锤子的硬件设计是超出期待的,软件是稍微有点没有感觉的,整个发布会后,我是融入的,感动了。罗总为人良心,整个发布确实让人很觉得他们在认真的做,认真的想,认真的夸人。当做手机变成了一种态度,一种做人,自然带入感就比较强烈。比较能够相信罗总的支持者们是能够举得起雷神般的锤子不停的tuning,只等那最后一道闪电解救世界。
闲话一大段,有人下锤了,必须进入话题。
锥子手机的系统是Android系统,本文自然说的是Android的自由截屏实现。
说到截屏,我们首先必须讲屏幕上的内容是怎么显示出来的,接下来我们以下图为例来讲解。
总的来说Android通过两级绘制系统来实现内容输出的
1)程序的绘制
这一级别的绘制的基本单元是view,各种view的组合最后绘制成了丰富多彩的程序内容。如下图
图(1)
这一过程的绘制采用的是传统的canvas绘制方法。
程序内容组合就是将系统中的所有可见的程序组合显示,这又是一个绘制过程。
这一级别绘制的基本单元是window(另一层面上叫layer),绝大多数程序只有一个window,但是也有程序有多个window,比如下图的”系统程序”就有两个window.
这一级别的绘制是由surfaceflinger来执行的,而这一级别的绘制也使用了更高级图形绘制系统—OpenGl,OpenGl有很好的composer能力,比如下图的dim效果(全屏灰色部分)。这一级别的绘制对象很少,比如下图只有四个window, 正常情况只有3个(没有关机程序),当手机有实体back, home, menu键后,又少一个navigationBar(最下面那个window),所以大部分手机的大部分时间只有2个window,也很适合opengl的特性。但是opengl操作比较耗电,android的大部分平台上又采用了overlay技术。Overlay技术是一种芯片级别的内存拷贝,lcd 控制器负责将overlay的内存和主显示的内容做composer效果(比如alpha,缩放等),效率和效能都高。你可以将overlay看成是一个轻型的GPU,对于android等移动设备(layer基本都是全屏,且数量少),video应用(特殊格式yuv)场景,有很大的提升作用。这个我可能在后面合适的时机单独讲一章。
图二
大部分情况下两个layer
从上面的绘制分层看来截屏肯定也有3种:应用截屏,整个屏幕的截屏,读取显示芯片内存截屏
1)应用截屏
应用截屏就是应用截取自己的内容。
要实现截取应用的内容,从上图可以很容易看出,只要将图(1)中的buffer替换为我们可以控制的buffer,然后让根view重新draw一次就可以了。代码告诉我们Android就是这么做的。
//这个就是根节点view View decorView = this.getWindow().getDecorView(); //开启view的drawing cache decorView.setDrawingCacheEnabled(true); //这个就是截屏的关键接口 //getDrawingCache负责创建bm的buffer,并让view都在上面绘制 Bitmap bm = decorView.getDrawingCache(); 文件View.java: public Bitmap getDrawingCache(boolean autoScale) { if ((mViewFlags & DRAWING_CACHE_ENABLED) == DRAWING_CACHE_ENABLED) { //这个会更新截屏的mDrawingCache buildDrawingCache(autoScale); } return autoScale ? mDrawingCache : mUnscaledDrawingCache; } public void buildDrawingCache(boolean autoScale) { Canvas canvas; Bitmap bitmap = autoScale ? mDrawingCache : mUnscaledDrawingCache; if (attachInfo != null) { canvas = attachInfo.mCanvas; if (canvas == null) { canvas = new Canvas(); } canvas.setBitmap(bitmap); // Temporarily clobber the cached Canvas in case one of our children // is also using a drawing cache. Without this, the children would // steal the canvas by attaching their own bitmap to it and bad, bad // thing would happen (invisible views, corrupted drawings, etc.) attachInfo.mCanvas = null; } else { // This case should hopefully never or seldom happen canvas = new Canvas(bitmap); } //让该view及子view绘制,程序的内容就更新在mDrawingCache上了 draw(canvas); } } 下面就是将bitmap转化为图片了,在此略去….
从上面的代码分析可以看出,其实我们可以截取任一view而不是整个屏幕,关键看你调用哪个view的getDrawingCache函数。
该方法的局限性在于只有程序自己才能截取,也就是说必须在程序内部显式的实现截屏逻辑才能截屏。该方法还有另外一个问题,如果应用中包含SurfaceView(比如camera, video播放等应用),截取后,你会发现SurfaceView所在的区域是黑色的。这是因为SurfaceView是一种特殊的view,它有自己的buffer,它的draw函数不会在canvas上绘制任何东西,自然就没法截取到它了。
Android中的著名的调试利器hierachviewer就是使用类似的机制来显示程序的各个view的。调用流程如下:
上面红色部分view的createSnapShot函数就和上面的getDrawingCache函数功能一样,核心就是创建buffer,重新draw.
2)整个屏幕截屏
从上面图线绘制逻辑分析来看,要实现全部屏幕截屏,只需将上图2中的buffer替换为截屏程序可控的buffer,然后重新绘制一次就可以了。我们来看源码:
Screencap.cpp { //heap就是内存的生产者 sp<IMemoryHeap> heap; uint32_t w, h; PixelFormat f; sp<IBinder> display(composer->getBuiltInDisplay(ISurfaceComposer::eDisplayIdMain)); status_t err = composer->captureScreen(display, &heap, &w, &h, &f, 0, 0); //转化为了大家熟悉的bitmap了 SkBitmap b; b.setConfig(SkBitmap::kARGB_8888_Config, w, h); b.setPixels(heap->getBase()); } SurfaceFlinger.cpp //重要的参数就是buffer的producer status_t SurfaceFlinger::captureScreenImplLocked( const sp<const DisplayDevice>& hw, const sp<IGraphicBufferProducer>& producer, uint32_t reqWidth, uint32_t reqHeight, uint32_t minLayerZ, uint32_t maxLayerZ) { //producer转化为surface,你可以看成是buffer的另外一种形式 sp<Surface> sur = new Surface(producer, false); //surface转化为ANativeWindow,你可以把它看成是buffer的另外一种包装形式 ANativeWindow* window = sur.get(); if (native_window_api_connect(window, NATIVE_WINDOW_API_EGL) == NO_ERROR) { if (err == NO_ERROR) { ANativeWindowBuffer* buffer; //从window中拿出buffer,这个地方涉及直接的buffer了 result = native_window_dequeue_buffer_and_wait(window, &buffer); if (result == NO_ERROR) { //buffer的又一种形式,转化为EGLImageKHR,这样opengl系统就认得了 EGLImageKHR image = eglCreateImageKHR(mEGLDisplay, EGL_NO_CONTEXT, EGL_NATIVE_BUFFER_ANDROID, buffer, NULL); if (image != EGL_NO_IMAGE_KHR) { //绑定到绘制引擎上,你可以看成是将bitmap绑定到canvas一样 //还是buffer的一种形式 RenderEngine::BindImageAsFramebuffer imageBond(getRenderEngine(), image); if (imageBond.getStatus() == NO_ERROR) { //开始进入绘制逻辑了,hw已经绑定了buffer renderScreenImplLocked(hw, reqWidth, reqHeight, minLayerZ, maxLayerZ, true); //这个会通知上面的buffer produce buffer已经绘制好了,可以用了 window->queueBuffer(window, buffer, -1); } } else { result = BAD_VALUE; } native_window_api_disconnect(window, NATIVE_WINDOW_API_EGL); } return result; } void SurfaceFlinger::renderScreenImplLocked( const sp<const DisplayDevice>& hw, uint32_t reqWidth, uint32_t reqHeight, uint32_t minLayerZ, uint32_t maxLayerZ, bool yswap) { const LayerVector& layers( mDrawingState.layersSortedByZ ); const size_t count = layers.size(); for (size_t i=0 ; i<count ; ++i) { const sp<Layer>& layer(layers[i]); const Layer::State& state(layer->getDrawingState()); if (state.layerStack == hw->getLayerStack()) { if (state.z >= minLayerZ && state.z <= maxLayerZ) { if (layer->isVisible()) { //核心:每个window(layer)将自己的内容绘制到buffer上 layer->draw(hw); } } } } }
有了全屏的bitmap,接下来做自由剪切就容易了吧。截完屏幕,用service弹出dialog,dialog里面加一个imageView显示截取的bitmap,再在外面套一个剪切域view即可实现锥子的自由切屏功能。
当然,上面为了简化逻辑,使用了native的代码来实现调用surfaceFlinger的captureScreenImplLocked函数。事实上,android的apk的ndk代码只能使用ndk里library提供的接口,是没法实现上面sample代码。再事实上,android的一般的apk的java层也是没法调用到captureScreenImplLocked接口的,只有WindowManagerService等system的java逻辑才可以调用的。
所以你要实现自由截屏,你必须是要是系统级别的程序。要不你是一个系统级别的native程序,要么你是系统级别的java逻辑,比如在WindowManagerService添加逻辑,java层实现可以参考如下代码:
文件WindowManagerService.java @Override public Bitmap screenshotApplications(IBinder appToken, int displayId, int width, int height, boolean force565) { if (!checkCallingPermission(android.Manifest.permission.READ_FRAME_BUFFER, "screenshotApplications()")) { throw new SecurityException("Requires READ_FRAME_BUFFER permission"); } Bitmap rawss = null; do { //这个代码就会最终调用到surfaceFlinger的renderScreenImplLocked //来截取指定layer的内容 //SurfaceControl的逻辑会提供buffer rawss = SurfaceControl.screenshot(dw, dh, minLayer, maxLayer); } if (rawss == null) { return null; } //有了bitmap,一切就完美了,你懂得 return bm; }
Android中最近程序历史记录里的程序截屏就是调用这个接口做到的,如下图:
Java贯穿到native的surfaceflinger的逻辑如下:
3)读取显示芯片内存截屏
即直接读取显示设备(通常为/dev/graphics/fb0)的数据,有overlay的还有可能有/dev/graphics/fb1。较早版本的DDMS截屏就是通过这种方式,ddms调用adbd,adbd(system/core/adb/framebuffer_service.c)直接读取/dev/graphics/fb0来实现截屏。但是由于有些设备出于安全,产权保护(避免录制高清视频)在出厂时是禁止直接读取显示设备内容的,故有些机型ddms是没法截取到屏幕的。故新版本的DDMS直接调用上面我提到过得screencap程序来实现的,由于这个方式是通过直接draw layer来实现的,所有机型都是可以截取到屏幕的。
目前主流手机助手的截屏都是通过ddms的方式实现的。Android 4.0+才开始内置了截屏幕功能,可以通过快捷键快速截屏。
Bitmap保存为文件的代码如下:
public void saveBitmap(Bitmap bitmap, String fileName) { if (bitmap == null) { return; } File file = File("/sdcard", fileName); file.createNewFile(); try { FileOutputStream out = new FileOutputStream(file); bitmap.compress(Bitmap.CompressFormat.PNG, 90, out); out.flush(); out.close(); } catch (Exception e) { e.printStackTrace(); } }
/********************************
* 本文来自博客 “爱踢门”
* 转载请标明出处:http://blog.csdn.net/itleaks
******************************************/