在Android开发中,截屏时出现的黑屏卡顿问题是一个常见但棘手的问题。这个问题通常发生在应用在处理截屏请求时,由于某些操作耗时过长或资源分配不当,导致应用界面渲染出现问题。以下是一些可能导致黑屏卡顿的原因以及相应的优化策略:
### 可能的原因
1. "截屏处理耗时过长":应用在处理截屏请求时执行了大量的计算或I/O操作,导致主线程阻塞。
2. "资源分配不当":在截屏过程中,应用分配了过多的内存或CPU资源,导致系统资源紧张,影响界面渲染。
3. "渲染线程阻塞":截屏操作涉及到界面渲染,如果渲染线程被阻塞,会导致黑屏卡顿。
### 优化策略
1. "异步处理截屏请求":将截屏处理操作放在异步线程中进行,避免阻塞主线程。可以使用`HandlerThread`或`AsyncTask`来实现。
```java
HandlerThread handlerThread = new HandlerThread("ScreenshotHandlerThread");
handlerThread.start();
Handler handler = new Handler(handlerThread.getLooper());
handler.post(() -> {
// 截屏处理逻辑
Bitmap bitmap = takeScreenshot();
// 处理截图
processScreenshot(bitmap);
});
```
2. "优化截屏处理逻辑":确保截屏处理逻辑尽可能高效,避免进行大量的计算或I/O操作。可以将一些复杂的操作
相关内容:
Android性能优化之截屏时黑屏卡顿问题
1.前言:
之前在做云游戏项目时遇到了一个黑屏的问题,接触过云游戏的小伙伴肯定都知道,他的实现方式是以推拉流+websocket 展现给用户的,视频推拉流很多种协议 RTSP、RTMP 等,然后根据设备 id、游玩时长、游戏名称、游戏 id 等拉起一个小游戏,然后以视频流的方式呈现给用户,这其他云游戏有几种方式可以进行操控,鼠标、遥控器、手柄、手机扫码等,当然手柄还分单手柄和双手柄,可以在手机、TV、盒子、投影、平板等各种设备上进行畅玩,今天要说的是场景是推拉流的同时手机进行扫码,h5通过 websocket 截屏的方式进行消息和数据发送传递,然而在4.4-6.0的 TV 和盒子上面长时间进行扫码操作的时候会导致用户游戏界面黑屏然后直接卡死,这是一个非常严重的问题.2.截屏实现:
因为之前项目的版本很低所以录频权限不一样,这 #技术分享里就讲解看项目的适配方式了,现在的项目都需要适配 Android15录屏权限2.1 请求截取手机屏幕权限
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private void createScreenCaptureIntent() {
MediaProjectionManager mediaProjectionManager = (MediaProjectionManager)
getSystemService(Context.MEDIA_PROJECTION_SERVICE);
startActivityForResult(
mediaProjectionManager.createScreenCaptureIntent(),
1001);
}
2.2 创建截屏画面
- 这里由于是云游戏和在TV大屏上面运行,所以画面必现保持高清
- 图片的格式必现是PixelFormat.RGBA_8888,565会导致画面模糊
- 项目要求高清画质,游戏体验也得提升
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private void createVirtualDisplay(Intent data) {
mImageReader = ImageReader.newInstance(mSWDisplay.getWidth(), mSWDisplay.getHeight(), PixelFormat.RGBA_8888, 2);
MediaProjection mMediaProjection = ((MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE)).getMediaProjection(Activity.RESULT_OK, data);
mMediaProjection.createVirtualDisplay("screen",
mSWDisplay.getWidth(), mSWDisplay.getHeight(), 2, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
mImageReader.getSurface(), null, null);
}
3.截取游戏画面和图片:
private void acquireScreenImage() {
Image image = mImageReader.acquireLatestImage()
if (image == null) {
return
}
int width = image.getWidth()
int height = image.getHeight()
final Image.Plane planes = image.getPlanes()
final ByteBuffer buffer = planes.getBuffer()
int pixelStride = planes.getPixelStride()
int rowStride = planes.getRowStride()
int rowPadding = rowStride -
Bitmap bitmap = Bitmap.createBitmap(width +
bitmap.copyPixelsFromBuffer(buffer) image.close() }
4.初始化扫码:
private void initQRCode() {
LogUtils.e("Account" + SPManager.getInstance().getAccount())
mQRCodeUrl = "http://www.example.com/control.html" + "?userId=" + SPManager.getInstance().getAccount() + "&apkId=" + mAppId
mQRImageView.setImageBitmap(ZXingUtils.createQRImage(mQRCodeUrl, ConvertUtils.dp2px(100), ConvertUtils.dp2px(100)))
mQRCodeLayout.setVisibility(_handlerGame.hasGameController() || mSupportRemoteControl || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP ? View.GONE : View.VISIBLE)
}
C 5.websocket初始化:
protected String getWSUrl() {
String suffix = "/" + SPManager.getInstance().getAccount() + "/" + mAppId + "/" + Constants.channelId;
return "ws://"+"xxxx:8180"+"/paas/android/webSocket/socketStatus" +
}
private void connectTimeWebSocket() { OkHttpClient client = new OkHttpClient.Builder().build(); Request mRequest = new Request.Builder().url(getWSUrl()).build(); LogUtils.d("--timeWebSocket--", getWSUrl()); mWebSocketTime = client.newWebSocket(mRequest, new WebSocketListener() { @Override public void onClosed(WebSocket webSocket, int code, String reason) { super.onClosed(webSocket, code, reason); LogUtils.e("WebSocket..", "onClosed " + reason); } @Override public void onClosing(WebSocket webSocket, int code, String reason) { super.onClosing(webSocket, code, reason); LogUtils.e("WebSocket..", "onClosing " + code + reason); } @Override public void onFailure(WebSocket webSocket, Throwable t, okhttp3.Response response) { super.onFailure(webSocket, t, response); LogUtils.e("WebSocket..", "onFailure" + t); LogUtils.e("WebSocket..", "onFailure" + response); } @Override public void onMessage(WebSocket webSocket, String text) { super.onMessage(webSocket, text); LogUtils.e("WebSocket..", "onMessage: " + text); Gson gson = new Gson(); WSInfo wsInfo = gson.fromJson(text, WSInfo.class); if (wsInfo == null || isDestroyed()) { return; } if (WSInfo.TYPE_CONNECT.equals(wsInfo.getType())) { if (WSInfo.RESULT_TRUE.equals(wsInfo.getResult())) { runOnUiThread(new Runnable() { @Override public void run() { connectTimeWebSocketSuccess(); } }); } else { ToastUtils.showLong(Constants.ERROR_MSG_10009); finish(); } } if (WSInfo.TYPE_PING.equals(wsInfo.getType())) { wsInfo.setType(WSInfo.TYPE_PONG); webSocket.send(gson.toJson(wsInfo)); } runOnUiThread(new Runnable() { @Override public void run() { onWSMessage(webSocket, wsInfo, gson); } }); } @Override public void onMessage(WebSocket webSocket, ByteString bytes) { super.onMessage(webSocket, bytes); LogUtils.e("WebSocket..", "onMessage bytes: " + bytes); } @Override public void onOpen(WebSocket webSocket, okhttp3.Response response) { super.onOpen(webSocket, response); LogUtils.e("WebSocket..", "onOpen"); } }); }
6.游戏sdk初始化:
private void startPlay(Bundle bundle) {
if (bundle != null) {
int groupId = Integer.valueOf(SPManager.getInstance().getBaseGroupId())
LogUtils.e("groupId: " + SPManager.getInstance().getBaseGroupId())
appBackground = bundle.getString("appBackground", "")
mAppId = bundle.getInt("appId", 1)
LogUtils.d("appId: " + mAppId)
long onlineTime = bundle.getInt("onlineTime", 1)
final String padCode = bundle.getString("padCode", null)
mManual = bundle.getString(Constants.MANUAL, "")
//操控方式 0手柄 1遥控器
mOperationMode = bundle.getStringArrayList(Constants.OPERATION_MODE)
LogUtils.d("OperationMode", mOperationMode)
//是否显示虚拟手柄二维码,若操控方式不为空且操控方式为遥控器时不显示,否则显示
mSupportRemoteControl = mOperationMode != null && mOperationMode.contains(Constants.OPERATION_MODE_REMOTE_CONTROL)
final int apiLevel = 2
final int useSSL = 0
String ip = NetWorkUtils.getIPAddress(this)
LogUtils.d("--ipAddress--",ip)
PlaySdkManager.connectDevice(DataConstants.getSWSign(), padCode, onlineTime, groupId, packageName,
null, 0, 15000, new PlaySdkManager.OnResponseListener() {
public void onResponse(int result, String content) {
LogUtils.d("connectRequest, result:" + result + ", content:" + content)
do {
if (result == 0) {
mPlaySdkManager = new PlaySdkManager(VideoPlayActivity.this, mIsUseSfDecode)
mPlaySdkManager.setSdkCallback(VideoPlayActivity.this)
if (_handlerGame != null) {
_handlerGame.setPlaySdkManager(mPlaySdkManager)
}
if (mIsUseSfDecode) {
if (mPlaySdkManager.setParams(content, packageName, apiLevel, useSSL,
mSWViewDisplay, VideoPlayActivity.this) != 0) {
break
}
} else {
//5、set game parameters
if (mPlaySdkManager.setParams(content, packageName, apiLevel, useSSL,
mSWDisplay, VideoPlayActivity.this) != 0) {
break
}
}
mPadCode = mPlaySdkManager.getPadCode()
mPadCodeView.setText("设备编号:" + mPadCode)
if (mPlaySdkManager.start() != 0) {
break
}
return
}
}
while (false)
}
})
}
}
7.优化后的游戏截图:
- 使用bitmapsetScale对图片进行缩放
- 使用Luban压缩进行按质量和比例压缩
- 发送websocket图片数据包给h5
- 使用bitmap.recycle()回收图片资源
7.1 对图片进行缩放处理:
private void acquireScreenImage() {
Image image = mImageReader.acquireLatestImage()
if (image == null) {
return
}
int width = image.getWidth()
int height = image.getHeight()
final Image.Plane planes = image.getPlanes()
final ByteBuffer buffer = planes.getBuffer()
int pixelStride = planes.getPixelStride()
int rowStride = planes.getRowStride()
int rowPadding = rowStride -
Bitmap bitmap = Bitmap.createBitmap(width +
bitmap.copyPixelsFromBuffer(buffer) if (Constants.PACKAGE_DY.equals(packageName)) { bitmap = Bitmap.createBitmap(bitmap, 0, 0, width, height) } else { Matrix matrix = new Matrix() matrix.setScale(0.35f, 0.35f) bitmap = Bitmap.createBitmap(bitmap, 0, 0, width, height, matrix, true) } if (bitmap != null) { compressionImage((BitmapUtils.bitmapToString(bitmap)), bitmap) } image.close() }
7.2 使用luban压缩图片:
可以根据项目自行设置压缩的比例和质量,private void compressionImage(String imageList, Bitmap bitmap) {
Luban.with(this)
.load(imageList)
.ignoreBy(100)
.setCompressListener(new OnCompressListener() {
@Override
public void onStart() {
LogUtils.d("-----------开始压缩 == ");
}
@Override
public void onSuccess(File file) {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos);
byte data = baos.toByteArray();
mWebSocket.send(ByteString.of(data));
baos.flush();
baos.close();
if (bitmap != null && !bitmap.isRecycled()) {
bitmap.recycle();
}
LogUtils.d("-----------压缩成功 file.length()== " +
} catch (IOException e) { e.printStackTrace(); } } @Override public void onError(Throwable e) { LogUtils.d("-----------压缩失败 == "); } }).launch(); }
8.在onDestroy()回收资源:
- 游戏sdk断开连接,停止拉流
- 释放手柄
- handler回收
- 广播取消注册
- 计时器回收
- websocket断开链接
@Override
protected void onDestroy() {
mSendScreenImage = false;
if (mPlaySdkManager != null) {
mPlaySdkManager.stop();
mPlaySdkManager.release();
PlaySdkManager.disconnectDevice(DataConstants.getSWSign(), mPadCode, mAppId, null);
}
if (_handlerGame != null) {
_handlerGame.onRelease();
}
mHandler.removeMessages(mTipLoopWhat);
unregisterReceiver(receiver);
if (null != timer) {
timer.cancel();
}
super.onDestroy();
if (mWebSocket != null) { mWebSocket.cancel(); } if (mWebSocketTime != null) { mWebSocketTime.cancel(); } closeTipDialog(); }
9.总结:
通过以上的方式解决游戏黑屏卡顿问题,当然现在不能复现,也展示不了效果,因为项目接口都连接不上了,只能从以前的代码进行分析排查原因,然后去解决卡顿黑屏问题,实际项目比这里更复杂,由于不能抓取到 trace 信息,这里就不展示分析的过程了.本文做为性能优化的开篇,后面如果有项目可以给出分析 trace 我会把整个完整的分析过程展示出来,要不然只讲问题和解决方式很枯燥,只有结合实际情况,然后分析原因,讲解过程,最后才是给出方案和验证.- 保证质量的情况下处理黑屏问题
- 图片的格式不能使用RGB_565
- 产生问题的原因是由于长时间对TV盒子进行截屏、还有websocket心跳包、推拉流、扫码操控TV盒子的游戏界面,TV设备的内存和性能较低,各种线程比较耗资源,当然这种长时间操作不回收资源也是最主要的问题
- 对图片进行缩放处理
- 对图片进行压缩图片(按照比例和质量)
- 回收图片资源
- 退出游戏时也要注意回收资源