AudioService之音频输出通道切换

2023-05-16

前言:音频输出的方式有很多种,外放即扬声器(Speaker)、听筒(Telephone Receiver)、有线耳机(WiredHeadset)、蓝牙音箱(Bluetooth A2DP)等,电话免提、插拔耳机、连接断开蓝牙设备等操作系统都会自动切换Audio音频到相应的输出设备上。

我们知道,音频输出通道切换有些是系统自动切换的,但是有些自动切换并不是我们想要的,如:音乐App在听歌时,需要对听歌时拔出耳机的操作进行阻止(暂停播放)而不是突然切换到外放,又如通话时插入耳机并按下免提,希望声音既可以从耳机输出,又可以从扬声器输出;

我们可以不用系统的自动切换,而自己设置音频输出通道的切换吗?

答案是可以的,即可以在应用层修改,也可以在Framework修改,今天讲的就是Framework层的修改,也就是修改AudioService;

AudioSystem.java
public static final int FORCE_NONE = 0;//默认通道
public static final int FORCE_SPEAKER = 1;//扬声器通道
public static final int FORCE_HEADPHONES = 2;//耳机通道
//下面两个是蓝牙耳机通道
public static final int FORCE_BT_SCO = 3;//是一种双向的音频数据的传输链路,只能用于普通语音的传输,不能用于播放音乐
public static final int FORCE_BT_A2DP = 4;//是一种单向的高品质音频数据传输链路,通常用于播放立体声音乐
public static final int FORCE_WIRED_ACCESSORY = 5;//有线设备通道,如有线耳机
public static final int FORCE_BT_CAR_DOCK = 6;
public static final int FORCE_BT_DESK_DOCK = 7;
public static final int FORCE_ANALOG_DOCK = 8;
public static final int FORCE_DIGITAL_DOCK = 9;
public static final int FORCE_NO_BT_A2DP = 10;
public static final int FORCE_SYSTEM_ENFORCED = 11;
public static final int FORCE_HDMI_SYSTEM_AUDIO_ENFORCED = 12;
public static final int FORCE_ENCODED_SURROUND_NEVER = 13;
public static final int FORCE_ENCODED_SURROUND_ALWAYS = 14;
public static final int NUM_FORCE_CONFIG = 15;
public static final int FORCE_DEFAULT = FORCE_NONE;

常用的也就是:扬声器,有线耳机,听筒,蓝牙耳机等;

2.播放模式

在使用音频输出通道时,需要指定播放模式:

AudioSystem.java
/* modes for setPhoneState, must match AudioSystem.h audio_mode */
public static final int MODE_INVALID            = -2;
public static final int MODE_CURRENT            = -1;
public static final int MODE_NORMAL             = 0;//待机模式,既不是铃声模式也不是通话模式,如music
public static final int MODE_RINGTONE           = 1;//铃声模式
public static final int MODE_IN_CALL            = 2;//音频通话模式
public static final int MODE_IN_COMMUNICATION   = 3;//通信模式,包括音/视频,VoIP通话.(3.0加入的,与通话模式类似)
public static final int NUM_MODES               = 4;

我们指定音频播放模式时,会通知HAL,我们当前音频所处于的状态,以便可以适当地传送音频。

3.流类型

设置播放模式的时候,需要考虑流类型,常用的流类型有:

/** Used to identify the default audio stream volume */
public static final int STREAM_DEFAULT = -1;
/** Used to identify the volume of audio streams for phone calls */
public static final int STREAM_VOICE_CALL = 0;
/** Used to identify the volume of audio streams for system sounds */
public static final int STREAM_SYSTEM = 1;
/** Used to identify the volume of audio streams for the phone ring and message alerts */
public static final int STREAM_RING = 2;
/** Used to identify the volume of audio streams for music playback */
public static final int STREAM_MUSIC = 3;
/** Used to identify the volume of audio streams for alarms */
public static final int STREAM_ALARM = 4;
/** Used to identify the volume of audio streams for notifications */
public static final int STREAM_NOTIFICATION = 5;
/** Used to identify the volume of audio streams for phone calls when connected on bluetooth */
public static final int STREAM_BLUETOOTH_SCO = 6;
/** Used to identify the volume of audio streams for enforced system sounds in certain
* countries (e.g camera in Japan) */
public static final int STREAM_SYSTEM_ENFORCED = 7;
/** Used to identify the volume of audio streams for DTMF tones */
public static final int STREAM_DTMF = 8;
/** Used to identify the volume of audio streams exclusively transmitted through the
*  speaker (TTS) of the device */
public static final int STREAM_TTS = 9;
/** Used to identify the volume of audio streams for accessibility prompts */
public static final int STREAM_ACCESSIBILITY = 10;
/**

4.音频输出通道,播放模式和流类型的关系

音频通道是与播放模式一起用的,而播放模式与音频流类型有关系;
(1)音频通道是指声音从哪里出来,这个容易理解;
(2)播放模式,也叫音频状态,手机有4种音频状态:待机状态,音视频通话状态,视频/VoIP通话状态与响铃状态,这4种状态对底层的音频输出设备的选择影响很大,相应的情景下就得使用相应的模式,如视频情景的播放模式就是MODE_IN_COMMUNICATION,或者,播放音乐情景的播放模式就是MODE_NORMAL,什么样的情形就得用什么样的播放模式,不能搞混,比如MODE_IN_CALL,就只能由通话时才能使用;
(3)音频流类型,我们操作手机的音频时需要指定操作的是哪一个流,虽然手机的中音频流类型有很多,但是一旦进入到属性里,android就会将其整理成几种类型,这才是实际的类型,与上面的播放模式对应;

5.源码分析

上面介绍了音频输出通道,播放模式和流类型,接下来就从源码framework层的角度来分析了;
 

AudioManager.java
public void setMode(int mode) {
	final IAudioService service = getService();
	try {
	    //调用AudioService的setMode()方法
	    service.setMode(mode, mICallBack, mApplicationContext.getOpPackageName());
	} catch (RemoteException e) {
	    throw e.rethrowFromSystemServer();
	}
}
AudioService.java
public void (int mode, IBinder cb, String callingPackage) {
    //应用设置音频模式,需要android.Manifest.permission.MODIFY_AUDIO_SETTINGS权限
    if (!checkAudioSettingsPermission("setMode()")) {
        return;
    }
 
    //如果要使用的音频模式是MODE_IN_CALL,那得有android.Manifest.permission.MODIFY_PHONE_STATE权限;
    //所以,除非是要通话,一般都很少用MODE_IN_CALL,语音视频等用的是MODE_IN_COMMUNICATION
    if ( (mode == AudioSystem.MODE_IN_CALL) &&
            (mContext.checkCallingOrSelfPermission(
                    android.Manifest.permission.MODIFY_PHONE_STATE)
                        != PackageManager.PERMISSION_GRANTED)) {
        return;
    }
 
    if (mode < AudioSystem.MODE_CURRENT || mode >= AudioSystem.NUM_MODES) {
        return;
    }
 
    int newModeOwnerPid = 0;
    synchronized(mSetModeDeathHandlers) {
        if (mode == AudioSystem.MODE_CURRENT) {
            mode = mMode;
        }
        //如果此次设置的音频播放模式和上一次的不同,那就返回这次使用新音频播放模式的进程的pid
        newModeOwnerPid = mode, cb, Binder.getCallingPid(), callingPackage);
    }
 
    //如果进入了RINGTONE, IN_CALL 或者IN_COMMUNICATION模式,清除掉当前更改音频模式的应用进程的蓝牙SCO连接
    if (newModeOwnerPid != 0) {
         disconnectBluetoothSco(newModeOwnerPid);
    }
}

setMode()中的重点是setModeInt()方法,真正的逻辑都在这里:

AudioService.java
private int setModeInt(int mode, IBinder cb, int pid, String caller) {
    int newModeOwnerPid = 0;
    if (cb == null) {
        return newModeOwnerPid;
    }
 
    SetModeDeathHandler hdlr = null;
    Iterator iter = mSetModeDeathHandlers.iterator();
    //循环遍历mSetModeDeathHandlers,找到与传递进来的相同pid的SetModeDeathHandler,并赋值给hdlr,
    //相同的pid也就是相同的应用,即找到相同的应用
    while (iter.hasNext()) {
        SetModeDeathHandler h = (SetModeDeathHandler)iter.next();
        if (h.getPid() == pid) {
            hdlr = h;
            // Remove from client list so that it is re-inserted at top of list
            iter.remove();
            hdlr.getBinder().unlinkToDeath(hdlr, 0);
            break;
        }
    }
    
    //下面就是设置新的音频播放模式
 
    int status = AudioSystem.AUDIO_STATUS_OK;
    //实际的播放模式
    int actualMode;
    do {
        //初始化实际的播放模式,与传递进来的mode相同
        actualMode = mode;
        //如果设置的模式是正常的播放模式,那就从mSetModeDeathHandlers列表的顶端获取一个模式给actualMode,
        //最近一次设置非正常音频模式的应用都会被放在mSetModeDeathHandlers的顶端
        if (mode == AudioSystem.MODE_NORMAL) {
            // get new mode from client at top the list if any
            if (!mSetModeDeathHandlers.isEmpty()) {
                hdlr = mSetModeDeathHandlers.get(0);
                cb = hdlr.getBinder();
                actualMode = hdlr.getMode();
            }
        } else {
            //新建hdlr
            if (hdlr == null) {
                hdlr = new SetModeDeathHandler(cb, pid);
            }
            // Register for client death notification
            try {
                cb.linkToDeath(hdlr, 0);
            } catch (RemoteException e) {
                // Client has died!
            }
 
            //将hdlr加到mSetModeDeathHandlers中,并放到首位,也就是最后一个调用setMode()的进程位于列表的顶部
            mSetModeDeathHandlers.add(0, hdlr);
            //设置当前进程的音频播放模式,hdlr.setMode()会将mode设置给mMode,这个要注意,要不然很容易跟下面的"actualMode != mMode"混淆
            hdlr.setMode(mode);
        }
 
        if (actualMode != mMode) {
            //通过AudioSystem将当前的音频模式设置到底层去,status返回设置的结果
            status = AudioSystem.setPhoneState(actualMode);
            if (status == AudioSystem.AUDIO_STATUS_OK) {
                if (DEBUG_MODE) { Log.v(TAG, " mode successfully set to " + actualMode); }
                //如果设置成功,保存当前的音频播放模式
                mMode = actualMode;
            } else {
                //如果设置不成功,从mSetModeDeathHandlers中删除该应用
                if (hdlr != null) {
                    mSetModeDeathHandlers.remove(hdlr);
                    cb.unlinkToDeath(hdlr, 0);
                }
                mode = AudioSystem.MODE_NORMAL;
            }
        } else {
            status = AudioSystem.AUDIO_STATUS_OK;
        }
    } while (status != AudioSystem.AUDIO_STATUS_OK && !mSetModeDeathHandlers.isEmpty());
    
    if (status == AudioSystem.AUDIO_STATUS_OK) {
        if (actualMode != AudioSystem.MODE_NORMAL) {
            if (mSetModeDeathHandlers.isEmpty()) {
                Log.e(TAG, "setMode() different from MODE_NORMAL with empty mode client stack");
            } else {
                //如果这个进程设置的音频模式为非正常模式,那就返回这个进程的pid
                newModeOwnerPid = mSetModeDeathHandlers.get(0).getPid();
            }
        }
        
        //下面的代码用来设置当前音频流类型的音量
        int streamType = getActiveStreamType(AudioManager.USE_DEFAULT_STREAM_TYPE);
        int device = getDeviceForStream(streamType);
        int index = mStreamStates[mStreamVolumeAlias[streamType]].getIndex(device);
        setStreamVolumeInt(mStreamVolumeAlias[streamType], index, device, true, caller);
 
        updateStreamVolumeAlias(true /*updateVolumes*/, caller);
    }
    return newModeOwnerPid;
}

AudioService用mMode来保存当前的音频播放模式;

(2)设置音频音频输出管道

设置音频输出管道的方法有两个分别是setSpeakerphoneOn()和setBluetoothScoOn(),我们来就看下最常用的设置扬声器播放;

AudioManager.java
public void setSpeakerphoneOn(boolean on){
    final IAudioService service = getService();
    try {
        //调用AudioService的setSpeakerphoneOn()
        service.setSpeakerphoneOn(on);
    } catch (RemoteException e) {
        throw e.rethrowFromSystemServer();
    }

进入到AudioService:

AudioService.java
public void setSpeakerphoneOn(boolean on){
    //需要权限android.Manifest.permission.MODIFY_AUDIO_SETTINGS
    if (!checkAudioSettingsPermission("setSpeakerphoneOn()")) {
        return;
    }
    // for logging only
    final String eventSource = new StringBuilder("setSpeakerphoneOn(").append(on)
            .append(") from u/pid:").append(Binder.getCallingUid()).append("/")
            .append(Binder.getCallingPid()).toString();
 
    if (on) {
        //开启扬声器
        if (mForcedUseForComm == AudioSystem.FORCE_BT_SCO) {
                sendMsg(mAudioHandler, MSG_SET_FORCE_USE, SENDMSG_QUEUE,
                        AudioSystem.FOR_RECORD, AudioSystem.FORCE_NONE,
                        eventSource, 0);
        }
        //进入扬声器播放的标志AudioSystem.FORCE_SPEAKER
        mForcedUseForComm = AudioSystem.FORCE_SPEAKER;
    } else if (mForcedUseForComm == AudioSystem.FORCE_SPEAKER){
        //取消扬声器
        mForcedUseForComm = AudioSystem.FORCE_NONE;
    }
 
    mForcedUseForCommExt = mForcedUseForComm;
 
    //此时是语音模式AudioSystem.FOR_COMMUNICATION,mForcedUseForComm表示当前是哪种音频通道
    sendMsg(mAudioHandler, MSG_SET_FORCE_USE, SENDMSG_QUEUE,
            AudioSystem.FOR_COMMUNICATION, mForcedUseForComm, eventSource, 0);
}
 
 
private static void sendMsg(Handler handler, int msg,
            int existingMsgPolicy, int arg1, int arg2, Object obj, int delay) {
    //existingMsgPolicy是SENDMSG_QUEUE
    if (existingMsgPolicy == SENDMSG_REPLACE) {
        handler.removeMessages(msg);
    } else if (existingMsgPolicy == SENDMSG_NOOP && handler.hasMessages(msg)) {
        return;
    }
    synchronized (mLastDeviceConnectMsgTime) {
        long time = SystemClock.uptimeMillis() + delay;
        //传递进来的参数封装,交给mAudioHandler处理;
        //arg1是AudioSystem.FOR_COMMUNICATION, arg2是mForcedUseForComm, 
        //obj是eventSource,eventSource就是调用扬声器的进程的信息
        handler.sendMessageAtTime(handler.obtainMessage(msg, arg1, arg2, obj), time);
        if (msg == MSG_SET_WIRED_DEVICE_CONNECTION_STATE ||
                msg == MSG_SET_A2DP_SRC_CONNECTION_STATE ||
                msg == MSG_SET_A2DP_SINK_CONNECTION_STATE) {
            mLastDeviceConnectMsgTime = time;
        }
    }
}

进入到mAudioHandler,调用setForceUse(msg.arg1, msg.arg2, (String) msg.obj);

private void setForceUse(int usage, int config, String eventSource) {
    synchronized (mConnectedDevices) {
        setForceUseInt_SyncDevices(usage, config, eventSource);
    }
}
 
private void setForceUseInt_SyncDevices(int usage, int config, String eventSource) {
    if (usage == AudioSystem.FOR_MEDIA) {
        sendMsg(mAudioHandler, MSG_REPORT_NEW_ROUTES,
                SENDMSG_NOOP, 0, 0, null, 0);
    }
    mForceUseLogger.log(new ForceUseEvent(usage, config, eventSource));
    //将语音模式AudioSystem.FOR_COMMUNICATION,音频通道mForcedUseForComm交给AudioSystem,
    //AudioSystem会将其设置到HAL底层
    AudioSystem.setForceUse(usage, config);
}

可以看到,AudioService用mForcedUseForComm和mForcedUseForCommExt保存了当前的音频通道,AudioService在调用AudioSystem.setForceUse(usage, config)方法时,会将相应的音频播放模式和音频通道设置到底层,从这里可以就看出,为什么在调用setSpeakerphoneOn()时要结合setMode()一起使用了;

6.疑惑:

前面介绍中就说了android手机中有很多的音频输出通道,为啥AudioService只提供了setSpeakerphoneOn()和setBluetoothScoOn这两个手动切换音频输出通道的方法呢?
之所以AudioService只提供这两个方法,是因为有些切换是系统自动完成的,比如有线耳机,蓝牙耳机的插入和拔出等,这些音频外设的切换在应用层是无法处理的;
有线耳机和蓝牙耳机均属于音频外设,关于音频外设的内容,可以看下一个章节;

7.听筒,扬声器,有线耳机这三个输出设备的切换

(1)听筒通道

听筒模式一般只会在通话或者语音过程中才会用到,所以,要使用听筒模式,必须得指定播放模式为MODE_IN_CALL或者是MODE_IN_COMMUNICATION;

(2)扬声器通道

在不插入音频外设如耳机的情况下,手机中的输出设备只有听筒和扬声器,要想在听筒和扬声器中切换是比较容易的,无非就是setSpeakerphoneOn()方法调用以及指定播放模式为MODE_IN_CALL或者是MODE_IN_COMMUNICATION;

(3)有线耳机

耳机是音频外设,此时手机中的音频输出设备有3个,除了耳机还有听筒和扬声器;

那底层是怎样选择一个设备进行音频输出的呢?

这就和音频系统中的音频路由策略有关,底层在播放音频时会选择一个设备,这个逻辑跟设备的优先级有关,代码在AudoPolicyServcie中,有时间在剖析这个具体原理;

所以,当手机中的音频输出设备有耳机,听筒和扬声器时,会根据设备的优先级来进行选择;

从测试的结果来看,3个当中,耳机的优先级最高,其次是听筒;

8.总结:

手机的输出设备大体可以分为3类:听筒,扬声器和外设,AudioServcie只提供了手动切换扬声器和蓝牙sro的接口,其余的则由系统根据设备的优先级进行自动切换,听筒设备只能在通话模式或者通信模式下才能使用;

手机音频的播放模式也可以分为4类:待机状态,音视频通话状态,视频/VoIP通话状态与响铃状态,这几个状态会影响系统对音频输出设备的选择;

在AudioService中,无论是手动切换输出设备,还是设置音频的播放模式,都是通过AudioSystem将值设置到AudioFlinger和AudioPolicyManager中,再由AudioFlinger和AudioPolicyManager将其设置到底层中;

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

AudioService之音频输出通道切换 的相关文章

  • Camera2 三预览

    1 获取预览尺寸 CameraCharacteristics 是一个只读的相机信息提供者 xff0c 其内部携带大量的相机信息 xff0c 包括代表相机朝向的 LENS FACING xff1b 判断闪光灯是否可用的 FLASH INFO
  • Camera2 四拍照

    1 理解 Capture 工作流程 在正式介绍如何拍照之前 xff0c 我们有必要深入理解几种不同模式的 Capture 的工作流程 xff0c 只要理解它们的工作流程就很容易掌握各种拍照模式的实现原理 xff0c 在第一章 Camera2
  • 解决从PDF复制文字后乱码问题

    背景 需要从PDF复制文字出来做笔记 xff0c 可是谁知道PDF通过adobe打开后复制出来后是乱码 xff0c 如下图所示 xff1a 解决 尝试过安装字体 xff0c 可惜没卵用 方法1 CAJViewer打开 用该软件打开后复制 x
  • Android Studio不能启动模拟器原因探秘 The emulator process for AVD xxx has terminated

    文章背景 在Android Studio中创建模拟器后 xff0c 启动模拟器时弹出提示 The emulator process for AVD Pixel 2 API 31 has terminated xff0c 但是并没有显示具体错
  • AndroidStudio-快捷键-格式化代码

    Windows Ctrl 43 Alt 43 L Ctrl 43 Shift 43 F 无效 亲测 和qq热键冲突 我的解决方式是把qq除捕获屏幕外的热键全部设置为无 Mac OPTION 43 CMD 43 L
  • 安装APK时报错:Failure [INSTALL_FAILED_TEST_ONLY: installPackageLI]

    使用AS自动运行时会在app build outputs apk debug文件夹下自动生成测试APK xff1a app debug apk xff0c 用命令adb install app debug apk时报错 xff1a Fail
  • 计算机网络-划分子网 四大类必会题型

    必记知识点 A类 xff1a 0 126 xff0c 默认子网掩码 xff1a 255 0 0 0 B类 xff1a 128 191 xff0c 默认子网掩码 xff1a 255 255 0 0 C类 xff1a 192 223 xff0c
  • C语言-解释复杂声明

    基本术语 xff1a 声明符 xff1a int a 就是一个声明符 标识符 定义的变量名字 xff0c 如 xff1a int a xff0c 那么a就是一个标识符 1 两个原则 xff1a 始终从内往外读声明符 xff0c 括号优先级高
  • Android EditText 不自动获取焦点

    在Activity上面显示一个EditText xff0c 进入该页面时想阻止这个EditText自动获取焦点而自动调起键盘 思路如下 xff1a 可以采取让父级控件来获取焦点就可以了 例如说在这个EditText外面包一个LinearLa
  • Wireshark中无法显示网卡列表的解决方法

    1 问题描述 打开Wireshark时 xff0c 都会有一个网卡列表 xff0c 在该列表中显示了电脑的所有网卡 但是 xff0c 有时打开Wireshark时 xff0c 该网卡列表不显示 xff0c 如图1所示 图1 不显示网卡列表
  • Android事件分发与事件处理源码分析

    一 前言 Android中事件分发与事件处理是一个老生常谈的问题了 xff0c 自己在网上也看过很多文章 xff0c 但是大部分人都只是抛出一些结论或是一些流程图或者干脆就是一些运行demo的截图等 xff0c 对于这些结论和流程图是怎么来
  • 解决小米pad USB安装apk时AS报错:INSTALL_FAILED_USER_RESTRICTED

    设置 更多设置 开发者选项 gt 取消启用MIUI优化 用USB接口 xff0c 选择传输文件 xff08 MTP xff09
  • git 通过 comment 关键字查找 commit

    git log grep 61 word 比如 xff1a git log grep 61 同步
  • git合并多个 Commit

    在使用 Git 作为版本控制的时候 xff0c 我们可能会由于各种各样的原因提交了许多临时的 commit xff0c 而这些 commit 拼接起来才是完整的任务 那么我们为了避免太多的 commit 而造成版本控制的混乱 xff0c 通
  • git查看某次提交的文件列表

    Git操作常用 xff1a 一 查看某次提交的文件列表 首先使用git log查看历史提交记录 xff1a 复制你想要查看记录的某个提交代号9ddc9dca00b 使用命令git show 9ddc9dca00b stat查看详细文件列表
  • Android SoundPool插入耳机后依然有外放声音

    使用soundPool播放声音 xff0c 当手机已经接通耳机时 xff0c 还会有外放声音 xff0c 是因为在初始化soundpool是用的流类型 xff08 streamType xff09 导致的 xff0c 有些流类型系统是一定会
  • 一文搞懂Android JetPack组件原理之Lifecycle、LiveData、ViewModel与源码分析技巧

    Lifecycle LiveData和ViewModel作为AAC架构的核心 xff0c 常常被用在Android业务架构中 在京东商城Android应用中 xff0c 为了事件传递等个性化需求 xff0c 比如ViewModel间通信 V
  • Linux下安装npm

    1 root 登录linux 2 没有目录就自己创建一个 cd usr local node 3 下载安装包 wget https npm taobao org mirrors node v4 4 7 node v4 4 7 linux x

随机推荐

  • AudioService之音频输出通道切换

    前言 xff1a 音频输出的方式有很多种 xff0c 外放即扬声器 xff08 Speaker xff09 听筒 xff08 Telephone Receiver xff09 有线耳机 xff08 WiredHeadset xff09 蓝牙
  • Android音频——音量调节

    一 音量相关概念 1 相关术语解释 track volume 单个App设置音量时设置的是这个 xff0c 它只影响本App的音量 stream volume xff1a 设置某一stream的音量 xff0c Android系统中支持10
  • 电话状态权限及IMEI获取流程源码分析

    IMEI是设备唯一性的一个重要指标 xff0c 这篇文章对IMEI获取做一些分析 xff0c 以达到以下两个目的 xff1a 1 梳理Android源码中获取IMEI流程 2 理解获取IMEI时 xff0c 源码中权限调用流程 备注 xff
  • Android Handler深入学习(源码分析)

    目录 xff1a 1 背景 在分析源码之前 xff0c 先来了解一下Message MessageQueue Looper这几个对象 1 1 Message 消息 定义 xff1a 是线程间通讯的数据单元 xff0c 包含着描述信息及任意数
  • git 合并两个不同仓库

    在日常开发过程中 xff0c 可能会遇到需要将两个不同的仓库合并成到一个仓库的场景 这里介绍一下怎么将两个不同的仓库合并到一个仓库中 合并两个不同仓库 思路 xff1a 添加两个远程仓库 xff0c 将两个代码作为两个分支 xff0c 然后
  • Android-Handler源码解析-Message

    成员变量 标识Message public int what 存储简单数据 xff0c 如果存储复杂的数据使用setData 方法 public int arg1 public int arg2 发送给接收者的任意对象 public Obj
  • Git中submodule的使用

    背景 面对比较复杂的项目 xff0c 我们有可能会将代码根据功能拆解成不同的子模块 主项目对子模块有依赖关系 xff0c 却又并不关心子模块的内部开发流程细节 这种情况下 xff0c 通常不会把所有源码都放在同一个 Git 仓库中 有一种比
  • Android-Handler源码解析-Looper

    成员变量 Log的TAG private static final String TAG 61 34 Looper 34 线程本地变量 xff0c 保证了每个线程仅有唯一的Looper对象 64 UnsupportedAppUsage st
  • 5步删除 git submodule

    1 删除submodule缓存 需要先暂存 gitmodules 文件 否则会报错 fatal please stage your changes to gitmodules or stash them to proceed 1 2 git
  • java.lang.NoSuchMethodError: java.lang.reflect.Field.trySetAccessible()Z

    把jdk 设置成11 就可以了
  • ES6 模块

    概述 在 ES6 前 xff0c 实现模块化使用的是 RequireJS 或者 seaJS xff08 分别是基于 AMD 规范的模块化库 xff0c 和基于 CMD 规范的模块化库 xff09 ES6 引入了模块化 xff0c 其设计思想
  • jdk 8 、9 10 11 12 13 14和 jdk 1.8 什么关系?

    jdk 8 就是 jdk 1 8 jdk9 就是 jdk 1 9
  • android studio设置jdk版本项目设置和全局设置

    android studio设置jdk版本项目设置和全局设置 方法1 xff1a 修改项目的gradle构建jdk xff08 建议在使用别人的单个项目时使用 xff09 打开项目设置 打开jdk设置 选择jdk11 注意要apply保存然
  • Gradle:执行命令时指定 JDK 版本

    应用场景 在命令行执行 Gradle 时使用的 Gradle 版本为系统环境变量中指定的 Gradle 版本 xff0c 使用的 JDK 为系统环境变量 JAVA HOME 指定的 JDK 来自 Gradle 官网的说明 xff1a JAV
  • Java基础-方法区以及static的内存分配图

    什么是方法区 xff1a 方法区是系统分配的一个内存逻辑区域 xff0c 是JVM在装载类文件时 xff0c 用于存储类型信息的 类的描述信息 方法区存放的信息包括 xff1a 类的基本信息 xff1a 1 每个类的全限定名 2 每个类的直
  • AudioManager 蓝牙sco连接相关接口

    蓝牙耳机连接之后 xff0c 发现音频发声的还是终端 xff0c 并没有转换到蓝牙耳机发声 网上搜索相关资料 xff0c 发现是蓝牙耳机需要建立链路来播放音频 简单介绍下蓝牙耳机的两种链路 xff1a A2DP xff08 异步链路 xff
  • Android 音频源码分析——AudioTrack设备选择

    基于Andorid9 0源码 以AudioTrack为例 xff0c 梳理下输出设备选择流程 音频设备选择的影响因素 xff1a AudioAttributes 声音流类型 setForceUse 设置 setPreferredDevice
  • 音频输出设备的选择

    场景 xff1a 使用AudioTrack播放一段音频 xff0c streamtype是AUDIO STREAM MUSIC xff1b 跟踪音频输出设备选择的流程 xff0c 代码会走到这里 xff1a 1 frameworks av
  • Engine::getDeviceForStrategyInt()解析

    audio devices t是int类型 audio devices t Engine getDeviceForStrategyInt routing strategy strategy DeviceVector availableOut
  • AudioService之音频输出通道切换

    前言 xff1a 音频输出的方式有很多种 xff0c 外放即扬声器 xff08 Speaker xff09 听筒 xff08 Telephone Receiver xff09 有线耳机 xff08 WiredHeadset xff09 蓝牙