周围有位罗总的忠实粉丝,于是乎平时有意无意的被灌注了大量“锤子”的信息。也许就是这些无意,我昨晚也就无意中点进了锤子手机发布会的直播,看着看着最后就看完了。
锤子的硬件设计是超出期待的,软件是稍微有点没有感觉的,整个发布会后,我是融入的,感动了。罗总为人良心,整个发布确实让人很觉得他们在认真的做,认真的想,认真的夸人。当做手机变成了一种态度,一种做人,自然带入感就比较强烈。比较能够相信罗总的支持者们是能够举得起雷神般的锤子不停的tuning,只等那最后一道闪电解救世界。
闲话一大段,有人下锤了,必须进入话题。
锥子手机的系统是Android系统,本文自然说的是Android的自由截屏实现。
说到截屏,我们首先必须讲屏幕上的内容是怎么显示出来的,接下来我们以下图为例来讲解。
绘制原理
总的来说Android通过两级绘制系统来实现内容输出的
1)程序的绘制
这一级别的绘制的基本单元是view,各种view的组合最后绘制成了丰富多彩的程序内容。如下图
图(1)
这一过程的绘制采用的是传统的canvas绘制方法。
2)程序的组合
程序内容组合就是将系统中的所有可见的程序组合显示,这又是一个绘制过程。
这一级别绘制的基本单元是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
******************************************/