跳转到主要内容

子线程刷新UI导致handler同步屏障锁死不移除引起无响应

问题现象

相机应用进行模式切换的压力测试中发现,低概率出现 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 的问题