Android 高斯模糊(2)BackgroundBlurDrawable使用及相关Bug

前文介绍了窗口高斯模糊的两种实现和基本原理

Android 高斯模糊(1) 窗口模糊及java侧基本流程简述

本文将介绍一下BackgroundBlurDrawable的使用,原生bug,以及解决方案

1.任意View使用BackgroundBlurDrawable

BackgroundBlurDrawable在原生中是给DecorView作为背景使用的,假如我们其他的View也想使用,可以参考原生代码,自行创建一个BackgroundBlurDrawable,设置给View作为背景即可。

kotlin 复制代码
// 注意需要View在attachWindow之后才能获取到ViewRootImpl
hostView.background = hostView.getViewRootImpl().createBackgroundBlurDrawable().apply {
setColor(Color.TRANSPARENT)
    setBlurRadius(70)
    setCornerRadius(32f)
} 

2.存在的bug

2.1. 软件绘制时会发生Crash

这是因为软件绘制时不支持drawRenderNode

修复方案:软件绘制时不执行drawRenderNode(需修改源码)

2.2 高斯模糊效果可以渲染到窗口范围之外

向窗口范围只有一部分,未铺满全屏,比如一个小弹框,其内部view通过scroll或transition偏移时,其高斯模糊效果会跟随便宜甚至会超出当前窗口范围。

解决方案:限定坐标范围,使其范围不能超过最大值和最小值。

(这里的数字只是举例,正常通过添加方法支持外部参数设置)(修改源码)

2.3. App端修改方案

以上两个问题修改方案都是在framework中修改,但是我们在app开发过程中,无法直接修改framework;此外BackgroundBlurDrawable是final类型,在app端也无法直接继承修改。

好在存在DrawableWrapper,我们可以创建一个DrawableWrapper包装原生的BackgroundBlurDrawable,

  1. 解决Crash问题

仅在硬件加速时,BackgroundBlurDrawable才绘制

  1. 解决高斯模糊效果渲染到窗口范围之外的问题

反射获取到rendernode,使用自己的监听器,替换掉旧的监听器

示例方案代码如下

kotlin 复制代码
package com.lws.app.graphics.drawable

import android.annotation.SuppressLint
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Path
import android.graphics.Rect
import android.graphics.RenderNode
import android.graphics.drawable.DrawableWrapper
import android.util.Log
import android.view.View
import androidx.annotation.ColorInt
import com.android.internal.graphics.drawable.BackgroundBlurDrawable

class BackgroundBlurDrawableV2 private constructor(private val backgroundBlurDrawable: BackgroundBlurDrawable) :
    DrawableWrapper(backgroundBlurDrawable) {

    private val mPaint: Paint = fieldPaint.get(backgroundBlurDrawable) as Paint
    private val mRectPath: Path = fieldRectPath.get(backgroundBlurDrawable) as Path

    private val mAggregator = fieldAggregator.get(backgroundBlurDrawable)
    private var mRect: Rect = fieldRect.get(backgroundBlurDrawable) as Rect

    var minTop = 130
    var maxBottom = 1800

    val positionUpdateListener: RenderNode.PositionUpdateListener =
        object : RenderNode.PositionUpdateListener {
            public override fun positionChanged(
                frameNumber: Long, left: Int, top: Int, right: Int,
                bottom: Int
            ) {
                onRenderNodePositionChanged.invoke(mAggregator, frameNumber, Runnable {
mRect.set(left, top, right, bottom)
                    if (mRect.top < minTop) {
                        mRect.top = minTop
                    }
                    if (mRect.bottom < minTop) {
                        mRect.bottom = minTop
                    }
                    if (mRect.top > maxBottom) {
                        mRect.top = maxBottom
                    }
                    if (mRect.bottom > maxBottom) {
                        mRect.bottom = maxBottom
                    }
                    Log.e("ssssssxxxx", "xxxx $mRect")
                } )
            }

            public override fun positionLost(frameNumber: Long) {
                onRenderNodePositionChanged.invoke(mAggregator, frameNumber, Runnable {
mRect.setEmpty()
                } )
            }
        }

    init {
        val mRenderNode = fieldRenderNode.get(backgroundBlurDrawable) as RenderNode
        mRenderNode.addPositionUpdateListener(positionUpdateListener)
        mRenderNode.removePositionUpdateListener(backgroundBlurDrawable.mPositionUpdateListener)
    }


    override fun draw(canvas: Canvas) {
        if (canvas.isHardwareAccelerated) {
            super.draw(canvas)
        } else {
            canvas.drawPath(mRectPath, mPaint)
        }
    }


    fun setColor(@ColorInt color: Int) {
        backgroundBlurDrawable.setColor(color)
    }

    fun setBlurRadius(blurRadius: Int) {
        backgroundBlurDrawable.setBlurRadius(blurRadius)
    }

    fun setCornerRadius(cornerRadius: Float) {
        backgroundBlurDrawable.setCornerRadius(cornerRadius)
    }


    companion object {
        @SuppressLint("PrivateApi")
        private val classBackgroundBlurDrawable =
            Class.forName("com.android.internal.graphics.drawable.BackgroundBlurDrawable")

        private val fieldAggregator = classBackgroundBlurDrawable.getDeclaredField("mAggregator")
            .apply { isAccessible = true }
private val fieldRect =
            classBackgroundBlurDrawable.getDeclaredField("mRect").apply { isAccessible = true }

private val fieldRectPath =
            classBackgroundBlurDrawable.getDeclaredField("mRectPath").apply { isAccessible = true }

private val fieldPaint =
            classBackgroundBlurDrawable.getDeclaredField("mPaint").apply { isAccessible = true }

private val fieldRenderNode = classBackgroundBlurDrawable.getDeclaredField("mRenderNode")
            .apply { isAccessible = true }


@SuppressLint("PrivateApi")
        private val onRenderNodePositionChanged =
            Class.forName("com.android.internal.graphics.drawable.BackgroundBlurDrawable$Aggregator")
                .getDeclaredMethod(
                    "onRenderNodePositionChanged",
                    Long::class.java, Runnable::class.java
).apply { isAccessible = true }


 /**
* 特别注意: 需要view在attachToWindow之后
*/
@JvmStatic
        fun createFromView(hostView: View): BackgroundBlurDrawableV2 {
            val backgroundBlurDrawable = hostView.getViewRootImpl().createBackgroundBlurDrawable()
            return BackgroundBlurDrawableV2(backgroundBlurDrawable)
        }
    }
}

3. 原生内存泄漏问题

全部指向 CrossWindowBlurListeners, 而且根据这个引用关系,可以发现是DecorView中引入的

查看DecorView源码,确实有添加这样一个监听器

在onDetachedFromWindow时是有移除该监听的逻辑的。

那么可以推断:造成该泄漏的原因是没有走到onDetachedFromWindow这里

正常窗口关闭肯定是会走到 onDetachedFromWindow 的,如果没有走到,那可能之前根本就没有AttachedToWindow。

搜索setBackgroundBlurRadius , 可以发现是从PhoneWindow调用的

再继续查看PhoneWindow源码,发现在generateLayout 时就会setBackgroundBlurRadius了

也就是说,即使这个窗口没有显示(没有AttachedToWindow),只要调用 generateLayout 或者 setBackgroundBlurRadius

就会添加这样一个监听器,后续如果没有AttachedToWindowonDetachedFromWindow 就会出现内存泄漏问题。

可能存在的场景:

构造了一个Dialog,这个Dialog主题中声明使用 windowBackgroundBlurRadius, 即使用BackgroundBlurDrawable,

调用了setContentView/或者getWindow(都会触发generateLayout 导致设置监听blur的监听器)

后续没有show,就会出现内存泄漏!!!

这其实是AOSP中一个非常低级的代码编写问题:

注册监听,移除监听没有对称执行。

原则上来说,generateLayout/setBackgroundBlurRadius时不应该直接注册监听,

应该在onAttachedToWindow时注册监听,在onDetachedFromWindow时移除监听,这样才是符合对称的设计。

对比高版本代码,直至Android16,AOSP中仍没有修复该问题。

App端规避方案

为了规避该问题,在窗口主题中不建议 声明使用windowBackgroundBlurRadius, 这样可以避免Framework流程中提前注册blur监听器。

在窗口显示/消失时,手动setBackgroundBlurRadius,保证blur监听器的注册/移除可控。

kotlin 复制代码
class TestDialog(context: Context) : Dialog(context) {

    override fun onStart() {
        super.onStart()
        // 在窗口显示时设置自己期望的模糊值
        window!!.setBackgroundBlurRadius(70)
    }

    override fun onStop() {
        super.onStop()
        // 模糊值清零,触发移除监听
        // (其实这里不清零也可以,因为根据上述分析流程,DecorView会在onDetachedFromWindow时移除监听)
        window!!.setBackgroundBlurRadius(0)
    }
}
相关推荐
儿歌八万首21 分钟前
硬核春节:用 Compose 打造“赛博鞭炮”
android·kotlin·compose·春节
消失的旧时光-19433 小时前
从 Kotlin 到 Dart:为什么 sealed 是处理「多种返回结果」的最佳方式?
android·开发语言·flutter·架构·kotlin·sealed
Jinkxs3 小时前
Gradle - 与Groovy/Kotlin DSL对比 构建脚本语言选择指南
android·开发语言·kotlin
&有梦想的咸鱼&3 小时前
Kotlin委托机制的底层实现深度解析(74)
android·开发语言·kotlin
LDORntKQH3 小时前
基于深度强化学习的混合动力汽车能量管理策略 1.利用DQN算法控制电池和发动机发电机组的功率分配 2
android
冬奇Lab3 小时前
Android 15 ServiceManager与Binder服务注册深度解析
android·源码·源码阅读
2501_916008895 小时前
深入解析iOS机审4.3原理与混淆实战方法
android·java·开发语言·ios·小程序·uni-app·iphone
独行soc7 小时前
2026年渗透测试面试题总结-20(题目+回答)
android·网络·安全·web安全·渗透测试·安全狮
常利兵7 小时前
2026年,Android开发已死?不,它正迎来黄金时代!
android
Risehuxyc7 小时前
备份三个PHP程序
android·开发语言·php