AudioManager#abandonAudioFocus之后仍然泄漏?

一、问题背景&等效还原

    最近在做视频播放时,发现使用AudioManager#requestAudioFocus注册焦点变化listener之后,再使用AudioManager#abandonAudioFocus反注册listener,反注册虽然有执行过,但是在使用Android Studio的Profiler验证内存泄漏时,仍然发现存在这个点的泄漏。
    编写的代码大概如下:

/**
 * @author TechMix
 * @date 2023/9/5 20:07
 * @description 测试验证AudioManager会导致内存泄漏的做法。
 * @email [email protected]
 */
private const val TAG = "MyCustomVideoView"

class MyCustomVideoView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr), AudioManager.OnAudioFocusChangeListener {
    private val audioManager: AudioManager by lazy {
        context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
    }

    @RequiresApi(Build.VERSION_CODES.O)
    override fun onAttachedToWindow() {
        super.onAttachedToWindow()
        audioManager.requestAudioFocus(
            AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
                .setOnAudioFocusChangeListener(this)
                .setAudioAttributes(
                    AudioAttributes.Builder()
                        .setLegacyStreamType(AudioManager.STREAM_MUSIC).build()
                )
                .build()
        )
    }

    @RequiresApi(Build.VERSION_CODES.O)
    override fun onDescendantInvalidated(child: View, target: View) {
        super.onDescendantInvalidated(child, target)
        audioManager.abandonAudioFocusRequest(
            AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
                .setOnAudioFocusChangeListener(this)
                .setAudioAttributes(
                    AudioAttributes.Builder()
                        .setLegacyStreamType(AudioManager.STREAM_MUSIC).build()
                )
                .build()
        )
    }

    override fun onAudioFocusChange(focusChange: Int) {
        Log.d(TAG, "onAudioFocusChange: focusChanged = $focusChange")
    }
}

    如上面的代码所示,在自定义View的onAttchedToWindow()中调用requestAudioFocus(),在onDetachedFromWindow()中调用abandonAudioFocus(),传入的listener的实现对象是自定义View的this对象。

二、问题原因分析

    分析AudioManager#requestAudioFocus方法listener集合的结构,发现是一个ConcurrentHashMap,key的取值规则是:
取AudioManager对象的toString(),拼接listener对象的toString()值。

  • android.media.AudioManager#getIdForAudioFocusListener
private String getIdForAudioFocusListener(OnAudioFocusChangeListener l) {
        if (l == null) {	
            return new String(this.toString());
        } else {
            return new String(this.toString() + l.toString());
        }
    }

对于AudioManager对象而言,其未重写toString()方法,那就是取Object#toString()的结果,是getClass().getName() + “@” + Integer.toHexString(hashCode());,因为AudioManager对象是单例的形式存在的,具体实现是转调AudioService做一些和系统服务通信的事情。所以问题就基本确定在listener.toString()在反注册时,已经和注册时是不同的值,导致反注册无法从listener的ConcurrentHashMap中移除,导致内存泄漏。

    public String toString() {
        return getClass().getName() + "@" + Integer.toHexString(hashCode());
    }

我们看看View#toString()的实现:

public String toString() {
        StringBuilder out = new StringBuilder(128);
        out.append(getClass().getName());
        out.append('{');
        out.append(Integer.toHexString(System.identityHashCode(this)));
        out.append(' ');
        switch (mViewFlags&VISIBILITY_MASK) {
            case VISIBLE: out.append('V'); break;
            case INVISIBLE: out.append('I'); break;
            case GONE: out.append('G'); break;
            default: out.append('.'); break;
        }
        out.append((mViewFlags & FOCUSABLE) == FOCUSABLE ? 'F' : '.');
        out.append((mViewFlags&ENABLED_MASK) == ENABLED ? 'E' : '.');
        out.append((mViewFlags&DRAW_MASK) == WILL_NOT_DRAW ? '.' : 'D');
        out.append((mViewFlags&SCROLLBARS_HORIZONTAL) != 0 ? 'H' : '.');
        out.append((mViewFlags&SCROLLBARS_VERTICAL) != 0 ? 'V' : '.');
        out.append((mViewFlags&CLICKABLE) != 0 ? 'C' : '.');
        out.append((mViewFlags&LONG_CLICKABLE) != 0 ? 'L' : '.');
        out.append((mViewFlags&CONTEXT_CLICKABLE) != 0 ? 'X' : '.');
        out.append(' ');
        out.append((mPrivateFlags&PFLAG_IS_ROOT_NAMESPACE) != 0 ? 'R' : '.');
        out.append((mPrivateFlags&PFLAG_FOCUSED) != 0 ? 'F' : '.');
        out.append((mPrivateFlags&PFLAG_SELECTED) != 0 ? 'S' : '.');
        if ((mPrivateFlags&PFLAG_PREPRESSED) != 0) {
            out.append('p');
        } else {
            out.append((mPrivateFlags&PFLAG_PRESSED) != 0 ? 'P' : '.');
        }
        out.append((mPrivateFlags&PFLAG_HOVERED) != 0 ? 'H' : '.');
        out.append((mPrivateFlags&PFLAG_ACTIVATED) != 0 ? 'A' : '.');
        out.append((mPrivateFlags&PFLAG_INVALIDATED) != 0 ? 'I' : '.');
        out.append((mPrivateFlags&PFLAG_DIRTY_MASK) != 0 ? 'D' : '.');
        out.append(' ');
        out.append(mLeft);
        out.append(',');
        out.append(mTop);
        out.append('-');
        out.append(mRight);
        out.append(',');
        out.append(mBottom);
        final int id = getId();
        if (id != NO_ID) {
            out.append(" #");
            out.append(Integer.toHexString(id));
            final Resources r = mResources;
            if (id > 0 && Resources.resourceHasPackage(id) && r != null) {
                try {
                    String pkgname;
                    switch (id&0xff000000) {
                        case 0x7f000000:
                            pkgname="app";
                            break;
                        case 0x01000000:
                            pkgname="android";
                            break;
                        default:
                            pkgname = r.getResourcePackageName(id);
                            break;
                    }
                    String typename = r.getResourceTypeName(id);
                    String entryname = r.getResourceEntryName(id);
                    out.append(" ");
                    out.append(pkgname);
                    out.append(":");
                    out.append(typename);
                    out.append("/");
                    out.append(entryname);
                } catch (Resources.NotFoundException e) {
                }
            }
        }
        out.append("}");
        return out.toString();
    }

可从上述代码中看到,View#toString()方法实现,是会根据View的可见性、是否可上焦、是否按下状态、是否可点击等一系列的int类型的flag值决定的,所以先使用View.this作为listener注册之后,再反注册View.this对象,只有在View的状态完全相同时才能反注册,比如:注册时View是可见的,反注册时是不可见的,那这样反注册是无效的。

解决方案

原因确定就很好解决这个问题,新建一个实现listener接口的类,保持注册和反注册时都是同一个对象即可,默认的toString()方法就是Object#toString()实现,只要是同一个对象就好。

public String toString() {
        return getClass().getName() + "@" + Integer.toHexString(hashCode());
    }

三、反思:Google为什么要这样设计?

你可能感兴趣的:(开发经验总结,Android基础,Android源码分析,音视频,android)