问题现象
相机应用进行模式切换的压力测试中发现,低概率出现 UI 无响应,界面卡死的现象
问题分析
通过问题现场的机器执行
adb shell dumpsys activity com.mediatek.camera
Dump Message in Queue (1): { when=-1m20s353ms barrier=161563 } // barrier同步锁没有移除
Dump Message in Queue (2): { when=-1m20s353ms what=30 target=android.view.ViewRootImpl$ViewRootHandler }
Dump Message in Queue (3): { when=-1m20s353ms what=30 target=android.view.ViewRootImpl$ViewRootHandler }
Dump Message in Queue (4): { when=-1m20s353ms what=30 target=android.view.ViewRootImpl$ViewRootHandler }
Dump Message in Queue (5): { when=-1m20s353ms callback=com.mediatek.camera.feature.setting.noisereduction.NoiseReduction$4 target=android.os.Handler }
Dump Message in Queue (6): { when=-1m20s352ms callback=com.mediatek.camera.feature.setting.exposure.ExposureViewController$3 target=android.os.Handler }
Dump Message in Queue (7): { when=-1m20s350ms callback=com.mediatek.camera.common.mode.CameraModeBase$2 target=android.os.Handler }
Dump Message in Queue (8): { when=-1m20s349ms what=5 target=com.mediatek.camera.feature.setting.zoom.ZoomViewCtrl$MainHandler }
Dump Message in Queue (9): { when=-1m20s349ms what=1 target=com.mediatek.camera.feature.setting.dng.DngViewCtrl$MainHandler }
Dump Message in Queue (10): { when=-1m20s349ms what=2 target=com.mediatek.camera.feature.setting.dng.DngViewCtrl$MainHandler }
Dump Message in Queue (11): { when=-1m20s349ms what=1 target=com.agold.feature.superlight.SuperLightViewController$MainHandler }
Dump Message in Queue (12): { when=-1m20s349ms what=3 obj=true target=com.agold.feature.superlight.SuperLightViewController$MainHan
可以看到上面类似的信息,MessageQueue 中第一个是 barrier 信息,表示此时消息队列处于同步锁状态,
这种情况下只有异步信息才可以执行,其他所有同步信息都处于等待状态,导致无法执行
异常信息需要通过 setAsynchronous 等方式显式指定,一般情况下都是同步信息。
界面刷新时或者 VSYNC 信号刷新 UI 时,会设置同步屏障,结束后移除屏障
alps/frameworks/base/core/java/android/view/ViewRootImpl.java
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier(); // 添加同步屏障
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); // vsync 刷新
}
}
void unscheduleTraversals() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier); // 移除
}
}
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier); // 移除
}
}
这里可以看到,scheduleTraversals 并不是线程安全,只保证统一线程中,同步屏障功能是线性的。
此问题现象中,同步屏障没有移除,一开始怀疑是否是应用中 MainHandler 中有耗时操作导致主线程卡死
通过在应用中添加 log:
Looper.getMainLooper().setMessageLogging(new Printer() {
@Override
public void println(String x) {
android.util.Log.i("CameraLooper",x);
}
});
通过监听 Looper 消息分发,判断是否有消息没有完成,或者超时
超时时间通可以通过 Looper 中的下列方法添加,如果有超过执行时间或者分发时间,会打印 log
alps/frameworks/base/core/java/android/os/Looper.java
public void setSlowLogThresholdMs(long slowDispatchThresholdMs, long slowDeliveryThresholdMs) {
mSlowDispatchThresholdMs = slowDispatchThresholdMs;
mSlowDeliveryThresholdMs = slowDeliveryThresholdMs;
}
通过以上方案,添加 log,复现问题之后,发现没有出现超时等异常现象。
此时,怀疑是否是应用 UI 刷新等存在异常?比如多线程问题,但是一般来说子线程不能刷新 UI 的,会提示线程错误。
查询发现在 ViewRootImpl 中存在一个方法:
public void onDescendantInvalidated(@NonNull View child, @NonNull View descendant) {
// TODO: Re-enable after camera is fixed or consider targetSdk checking this
// checkThread(); //google默认注释掉
if ((descendant.mPrivateFlags & PFLAG_DRAW_ANIMATION) != 0) {
mIsAnimating = true;
}
invalidate();
}
void invalidate() {
if (!mWillDrawSoon) {
scheduleTraversals(); // 刷新ui,下同步屏障
}
}
alps/frameworks/base/core/java/android/view/ViewGroup.java
public void onDescendantInvalidated(@NonNull View child, @NonNull View target) {
/*
* HW-only, Rect-ignoring damage codepath * * We don't deal with rectangles here, since RenderThread native code computes damage for * everything drawn by HWUI (and SW layer / drawing cache doesn't keep track of damage area) */
// if set, combine the animation flag into the parent mPrivateFlags |= (target.mPrivateFlags & PFLAG_DRAW_ANIMATION);
if (mParent != null) {
mParent.onDescendantInvalidated(this, target);
}
}
public final void invalidateChild(View child, final Rect dirty) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null && attachInfo.mHardwareAccelerated) { // 应用打开硬件加速
// HW accelerated fast path
onDescendantInvalidated(child, child);
return;
}
}
这个方法会在应用打开了硬件加速的情况下走到,==且没有检查线程==,checkThread 方法默认是注释掉的
所以这里存在一个漏洞,如果应用打开了硬件加速,那么可能会出现子线程刷新 UI,但是不会报错的情况!
从代码中可以看到 ViewRootImp#invalidate 方法也会刷新 UI,那么这里就会出现线程同步的问题,非 UI 线程也会添加同步屏障,可能会出现同步屏障非线性情况,导致没有正确移除的问题
==而相机应用是打开了硬件加速的。因此怀疑是否真的是线程问题==
这里直接将 checkThread 取消注释(添加包名判断,避免其他应用出现问题),理论上如果存在非 UI 线程刷新 UI, 那么将会报错,或者卡死
编译相机应用测试,在切换到专业模式的,百分百出现了界面卡死现象,高度怀疑出现了界面线程刷新问题。
11-20 08:05:57.625 19348 20472 D CamAp_SettingManager-0: [setCameraCharacteristics]+, mInitialized:tru
20 08:05:57.631 19348 20472 D CamAp_SettingManager-0: [setCameraCharacteristics]requestConfigure :com.mediatek.camera.feature.setting.ais.AISCaptureRequestConfig@d855cad
11-20 08:05:57.633 19348 20472 D CamAp_SettingManager-0: [setCameraCharacteristics]requestConfigure :com.mediatek.camera.feature.setting.iso.ISOCaptureRequestConfig@ed35be2
...
11-20 08:05:57.668 19348 20472 D CamAp_SettingManager-0: [setCameraCharacteristics]requestConfigure :com.agold.feature.focusdistance.FocusDistanceCaptureRequestConfigure@f240d5
11-20 08:05:57.668 19348 20472 D CamAp_SettingManager-0: [setCameraCharacteristics]requestConfigure :com.agold.feature.gradienter.GradienterCaptureRequestConfig@a4b0e // 卡在这个函数中
通过代码流程添加 log 发现,卡在了 GradienterCaptureRequestConfig#setCameraCharacteristics 方法中
src/com/../feature/gradienter/GradienterCaptureRequestConfig.java
@Override
public void setCameraCharacteristics(CameraCharacteristics characteristics) {
List<String> platformSupportedValues = new ArrayList<>();
platformSupportedValues.add(Gradienter.VALUE_OFF);
platformSupportedValues.add(Gradienter.VALUE_ON);
mGradienter.initializeValue(platformSupportedValues, Gradienter.VALUE_OFF);
}
src/com/../feature/gradienter/Gradienter.java
public void initializeValue(List<String> platformSupportedValues, String defaultValue) {
...
if (mOnGradienterStatusListener != null) {
mOnGradienterStatusListener.onGradienterStatusChanged(VALUE_ON.equals(value));
}
}
src/com/../feature/ui/preview/gradienter/GradienterViewManager.java
public GradienterViewManager(IApp app) {
mApp.getAppUi().setOnGradienterStatusListener(this::setOpened);
}
public void setOpened(boolean isOpened) {
mGradienterView.setOpened(isOpened);
}
src/com/../feature/ui/preview/gradienter/GradienterView.java
public void setOpened(boolean isOpened) {
mIsOpened = isOpened;
invalidate(); // 这里请求了UI刷新!
}
setCameraCharacteristics 方式是在相机打开之后的回调方法中执行的,属于 Camera 线程,非 UI 线程
==因此可以发现 GradienterView#setOpened 正好调用了 UI 刷新的功能,且整条逻辑没有在主线程执行!==
将 invalidate 方法改为 postInvalidate 方式,ui 刷新请求通过 handler 提交到主线程执行。
编译之后再次测试,切换到专业模式中,功能正常,没有出现卡死问题,因此确定为子线程刷新 UI 导致的问题
解决方案
UI 刷新要在主线程执行,不要在子线程中请求。
备注:
本次修改包括两个问题:
src/com/../feature/ui/preview/gradienter/GradienterView.java
src/com/../feature/ui/preview/gridlines/GridlinesView.java
两个文件中都存在子线程刷新 UI 的问题