Android Surface截图方法总结

前言

说起Surface截图,很多人一个惯性思维是使用MediaProjection框架,但是有点杀鸡使用宰牛刀的问题,实际上,MediaProjection往往需要申请权限,其录制范围包括第三方app,但是如果是自身app,实际上完全没有必要,仅仅使用DisplayManager创建虚拟屏即可,连权限都不需要申请。

实际上,Android Surface截图实际上不算什么难事,在Android N版本开始,系统就已经提供了PixelCopy类来截取Surface,同时还支持缩放,也就是会根据传入的Bitmap大小进行缩放,这种方式可以辅助我们实现画面调整。

关于截屏

在之前一篇文章中我们提到过,截取自身屏幕实际上也有最简单的方法,技术使用DecorView#draw方法将UI绘制到Bitmap上,然后再将Bitmap绘制到android.media.MediaCodec#createInputSurface创建的Surface上即可。

SurfaceView截图问题

但是,有一种情况比较特殊,那就是UI中包含SurfaceView和GLSurfaceView,那么其View是无法截取到的,SurfaceView绘制完之后会直接将GraphicBuffer提交给SurfaceFlinger,因此是无法绘制到的。

TextureView截图问题

SurfaceView存在问题,那么TextureView是不是可以使用DecorView#draw,实际上并不是,这个时候我们要使用TexureView#gitBitmap + DecorView#Bitmap 合成,或者使用PixelCopy的新方法

java 复制代码
public static void request(@NonNull Window source, @NonNull Bitmap dest,
        @NonNull OnPixelCopyFinishedListener listener, @NonNull Handler listenerThread) {
    request(source, null, dest, listener, listenerThread);
}

上面的方法仅仅支持Android 8.0 版本,实际上我们在Android 7上也可以参考Android 8的实现,也能获得截图,理论上是合成后的画面。

PixelCopy的缺陷

PixelCopy最大的缺陷是不支持Android 7之前的版本。

PixelCopy性能如何?

一般来说,我们可能认为PixelCopy性能较差,但实际上他的代码却是【相反的表达】,主要是下面的逻辑中,线程涉及是

主线程截取图片,在子线程中发送图片

用中国思维来理解,其表达的意思是:【拿走你的东西,别挡着老子干活】。道理很简单,此方法截图很快,不要低估他的性能。

下面是代码

java 复制代码
public static void request(@NonNull Surface source, @Nullable Rect srcRect,
        @NonNull Bitmap dest, @NonNull OnPixelCopyFinishedListener listener,
        @NonNull Handler listenerThread) {
    validateBitmapDest(dest);
    if (!source.isValid()) {
        throw new IllegalArgumentException("Surface isn't valid, source.isValid() == false");
    }
    if (srcRect != null && srcRect.isEmpty()) {
        throw new IllegalArgumentException("sourceRect is empty");
    }
    // TODO: Make this actually async and fast and cool and stuff
    int result = ThreadedRenderer.copySurfaceInto(source, srcRect, dest);  //在主线程截图
    listenerThread.post(new Runnable() {
        @Override
        public void run() {
            listener.onPixelCopyFinished(result); //发送到其他线程
        }
    });
}

总之,这个方法性能还是可以的,然而,如果我们要实现定时截图,那么需要对Bitmap进行池化,使得Bitmap能够被复用。

Surface截取

实际上,截屏中最困难的就是SurfaceView和GLSurfaceView的截屏,不过后者可以使用glReadPixels来读取argb缓存。

java 复制代码
private Bitmap captureBitmap() {
    int width = getWidth();
    int height = getHeight();
    int size = width * height * 4;
    if(size <= 0) return null;
    try {
        if(mPBufferPixels == null || mPBufferPixels.capacity() != size) {
            mPBufferPixels = ByteBuffer.allocateDirect(size)
                    .order(ByteOrder.nativeOrder());
        }
        GLES20.glReadPixels(/*x*/ 0, /*y*/ 0, width, height,
                GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, mPBufferPixels);
        checkGlError("glReadPixels");
        Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        bitmap.copyPixelsFromBuffer(mPBufferPixels);
        return bitmap;
    }catch (Throwable e){
        e.printStackTrace();
    }
    return null;
}

那么SurfaceView怎么办呢,前面说过,Android 7之后可以使用PixelCopy工具,但是Android 7之前的版本呢?

虚拟屏方案

Android 7之前对于Surface可以说几乎无能为力,不过Android 4.4开始提供DisplayManager#createVirtualDisplay可以解决此问题,我们可以创建一个虚拟屏

java 复制代码
public VirtualDisplay createVirtualDisplay(@NonNull String name,
        int width, int height, int densityDpi, @Nullable Surface surface, int flags,
        @Nullable VirtualDisplay.Callback callback, @Nullable Handler handler) {
    final VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder(name, width,
            height, densityDpi);
    builder.setFlags(flags);
    if (surface != null) {
        builder.setSurface(surface);
    }
    return createVirtualDisplay(null /* projection */, builder.build(), callback, handler);
}

我们知道Android系统中的虚拟屏默认是会投影DefaultDisplay上的画面,上面方法中的Surface我们可以使用ImageReader来创建,从而实现截屏。

java 复制代码
imageReader = ImageReader.newInstance(1280, 720, ImageFormat.YUV_420_888, 3);
imageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {
  @Override
  public void onImageAvailable(ImageReader reader) {
    Image image = reader.acquireLatestImage();
    if(image == null) return;
    Surface surface = videoSurface;
    if(surface == null) {
      image.close();
      return;
    }
    synchronized (surface){
      if(surface.isValid()){
        byte[] yuv420SP = ImageBitmapUtil.getBytesFromImageAsType(image, ImageBitmapUtil.NV21);
        int[] rgb = ImageBitmapUtil.decodeYUV420SP(yuv420SP, image.getWidth(), image.getHeight());
        Bitmap bitmap = Bitmap.createBitmap(rgb, 0, image.getWidth(), image.getWidth(),
            image.getHeight(),
            Bitmap.Config.ARGB_8888);

        Canvas canvas = surface.lockCanvas(null);
        canvas.drawBitmap(bitmap,0,0,null);
        surface.unlockCanvasAndPost(canvas);
        bitmap.recycle();
      }
    }
    image.close();
  }
},new Handler(thread.getLooper()));

将imageReader#getSurface传入下面方法接口

java 复制代码
virtualDisplay = displayManager.createVirtualDisplay("AutoProjection", displayMetrics.widthPixels, displayMetrics.heightPixels, displayMetrics.densityDpi,surface,DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION);

综上,Android 7.0我们使用PixelCopy ,而低版本可以使用虚拟屏方案

引申: 如何实现副屏录屏

我们知道,Android 系统只能录制主屏幕,那如果想录制指定的副屏如何实现?

scrcpy方案

看过scrcpy 源码的应该知道,scrcpy利用SurfaceControl和日志嗅探出副屏的displayId,通过通过SurfaceControl方法SurfaceFlinger从而实现了副屏录制

但是缺陷是只支持ADB方法,需要屏幕录制权限,且访问SurfaceFlinger的权限属于系统权限,普通app无法获取。

虚拟屏中转方案

所谓中砖方案是先中转到虚拟屏,然后虚拟屏输出内容到副屏的同时截取画面,从而实现副屏边录边播。当然如果是简单的录制Surface,EGL离屏渲染即可。

Android 11 虚拟屏

Android 11 中的DisplayManager提供了一些方法createVirtualDisplay可以指定displayId,具体暂时没有试过,后续有时间我们补充一下。

Android 11 mirrorSurface

Android 11 提供了另一种方案,但是被@hide注解,暂时没有试过,后续试一下

系统代码:com.android.systemui.accessibility.WindowMagnificationController#createMirror

java 复制代码
private void createMirror() {
    mMirrorSurface = WindowManagerWrapper.getInstance().mirrorDisplay(mDisplayId);
    if (!mMirrorSurface.isValid()) {
        return;
    }
    mTransaction.show(mMirrorSurface)
            .reparent(mMirrorSurface, mMirrorSurfaceView.getSurfaceControl());

    modifyWindowMagnification(mTransaction);
    mTransaction.apply();
}

总结

本篇,我们主要针对Surface截屏方法进行了总结,当然,如果要放到生产环境,如果仅仅是简单的截图就已经够了,但是要是实现视频录制,我们还需要做更多的内存优化,比如前文提到的Bitmap池化(享元模式)。另外我们还可能涉及Bitmap转ByteBuffer(Direct ByteBuffer 不会因为GC而整理内存碎片,引发内存地址变化)的处理,意味着ByteBuffer池化,这部分就不赘述了。

本篇就到这里,希望对你有所帮助。

相关推荐
也无晴也无风雨1 小时前
深入剖析输入URL按下回车,浏览器做了什么
前端·后端·计算机网络
SRC_BLUE_171 小时前
SQLI LABS | Less-39 GET-Stacked Query Injection-Intiger Based
android·网络安全·adb·less
Martin -Tang1 小时前
Vue 3 中,ref 和 reactive的区别
前端·javascript·vue.js
FakeOccupational3 小时前
nodejs 020: React语法规则 props和state
前端·javascript·react.js
放逐者-保持本心,方可放逐3 小时前
react 组件应用
开发语言·前端·javascript·react.js·前端框架
曹天骄4 小时前
next中服务端组件共享接口数据
前端·javascript·react.js
无尽的大道4 小时前
Android打包流程图
android
阮少年、4 小时前
java后台生成模拟聊天截图并返回给前端
java·开发语言·前端
镭封6 小时前
android studio 配置过程
android·ide·android studio
夜雨星辰4876 小时前
Android Studio 学习——整体框架和概念
android·学习·android studio