pjsip在Android 10+系统兼容性问题一文说清:从崩溃到稳定的实战指南
你有没有遇到过这种情况?
一个原本在Android 9上跑得稳如老狗的pjsip VoIP应用,升级到Android 10或更高版本后突然“失联”——注册频繁掉线、后台收不到来电、一通话就静音……用户投诉不断,技术支持焦头烂额。
这不是个别现象。自Android 10(API 29)起,Google对权限模型、后台执行和隐私保护进行了结构性调整。这些改动本意是提升安全与续航,但对于依赖长期连接和音频采集的通信类应用来说,几乎是一次“降维打击”。
而作为开源SIP栈中的佼佼者,pjsip虽然功能强大、跨平台支持好,却也因底层C/C++实现与Java层交互复杂,在新系统中暴露出了大量兼容性“坑点”。
本文不讲空话,也不堆砌文档术语,而是以一名实际踩过所有坑的开发者视角,带你彻底搞懂:
为什么你的pjsip应用在Android 10+会失效?又该如何真正解决?
我们将从权限、服务、网络、音频四大维度切入,结合真实代码与调试经验,还原问题本质,并给出可直接落地的解决方案。
一、先搞清楚:pjsip到底是个啥?它靠什么活着?
要解决问题,得先知道你在跟谁打交道。
pjsip不是一个简单的SDK,它是用C/C++写的完整多媒体通信引擎,包含:
- SIP信令协议栈
- RTP/RTCP媒体传输
- SDP协商
- 音频编解码(G.711, Opus等)
- 回声消除(AEC)、噪声抑制(ANS)
- NAT穿透(STUN/TURN/ICE)
在Android上,它通常被封装成.so动态库,通过JNI由Java/Kotlin调用。你可以把它理解为一个“躲在本地的通信小黑盒”,但它有几个关键生存条件:
- 必须能持续访问麦克风
- 需要一直联网并发送心跳
- 得有个常驻线程处理事件循环(
pjsua_handle_events()) - 不能轻易被系统杀死
一旦某个环节被Android新机制卡住——比如后台禁止录音、服务被杀、Socket被关——整个通话链路就会断裂。
所以,我们接下来要解决的问题,本质上就是:如何让这个“小黑盒”在越来越严苛的Android环境下合法地活下去?
二、第一个致命坑:麦克风权限在后台被静默禁用
问题现象
App退到后台后,来电可以弹出通知,但一接听就发现对方听不到声音,或者自己完全听不见对方。日志显示音频设备打开失败。
根本原因
尽管RECORD_AUDIO权限早在Android 6就开始要求动态申请,但从Android 10 开始,系统明确禁止后台应用访问麦克风,即使你之前已经授权!
这意味着:
- 用户切到微信回个消息 → 你的App进入后台 → 系统立即暂停麦克风访问;
- 此时若触发来电,pjsip尝试启动RTP流 → 调用pjmedia_aud_stream_create()失败 → 音频通路中断。
更糟的是,这种失败往往是静默的——没有异常抛出,只有返回码告诉你“操作未完成”。
解决方案:前台服务 + 持续通知
最根本的办法只有一个:让你的服务始终处于“前台状态”。
只要你是前台服务(Foreground Service),系统就认为你正在为用户提供可见服务,允许继续使用麦克风。
✅ 正确做法示例
public class SipService extends Service { private static final int NOTIFICATION_ID = 1001; private static final String CHANNEL_ID = "sip_foreground"; @Override public int onStartCommand(Intent intent, int flags, int startId) { // 必须先创建通知渠道(Android 8+) createNotificationChannel(); // 构建不可清除的持续通知 Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID) .setContentTitle("电话服务正在运行") .setContentText("保持在线以便接收来电") .setSmallIcon(R.drawable.ic_call_white_24dp) .setOngoing(true) // 关键:设为持续通知 .setPriority(NotificationCompat.PRIORITY_LOW) .build(); // 在5秒内调用,否则崩溃! startForeground(NOTIFICATION_ID, notification); // 启动pjsip栈 initializePjSip(); return START_STICKY; } @TargetApi(Build.VERSION_CODES.O) private void createNotificationChannel() { NotificationChannel channel = new NotificationChannel( CHANNEL_ID, "VoIP 通话服务", NotificationManager.IMPORTANCE_LOW ); channel.setDescription("用于维持SIP注册状态"); getSystemService(NotificationManager.class).createNotificationChannel(channel); } }别忘了在AndroidManifest.xml中声明权限和服务:
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <service android:name=".SipService" android:foregroundServiceType="microphone" />⚠️ 注意:从 Android 9 开始,前台服务还必须指定类型(如
microphone或phoneCall),否则可能无法获得音频权限。
三、第二个大坑:后台服务被系统回收,导致无法接收来电
问题现象
App切换到后台几分钟后,SIP注册状态变为“未注册”,再也收不到任何来电。
根本原因
Android 8.0 起引入了后台服务限制:
- 普通startService()在后台会被系统阻止;
- 已运行的服务会在短时间内被终止;
- 即使设置了START_STICKY,也不能保证重启成功。
而 pjsip 的核心逻辑依赖于一个长期运行的事件轮询线程(pjsua_handle_events()),一旦宿主服务被杀,这个线程也就没了,自然无法处理 INVITE 请求。
解决方案:前台服务是唯一出路
再次强调:只有前台服务才能长期存活。
很多人误以为加个WorkManager或AlarmManager定时唤醒就能保活,其实这是治标不治本。现代Android系统对这类行为管控极严,尤其在厂商定制ROM中(华为、小米、OPPO等),定时任务经常被冻结。
真正的稳定方案是:
1. 所有pjsip相关初始化都在SipService中进行;
2. 服务启动即调用startForeground();
3. 通话期间保持通知存在;
4. 用户手动退出时才停止服务。
这样系统才会认为你在提供“正在进行的服务”,从而保留资源。
四、第三个隐患:明文网络被禁,TLS配置不当导致连接失败
问题现象
在某些设备上,SIP注册总是超时,日志显示TCP连接失败,但Wi-Fi下正常,4G下不行。
根本原因
Android 9 开始默认关闭明文流量(HTTP/非TLS TCP):
<!-- 默认值 --> <application android:usesCleartextTraffic="false">如果你的SIP服务器使用的是普通TCP而非TLS加密传输(即不是sips:协议),那么连接请求将被系统拦截。
此外,Doze模式下系统还会主动关闭空闲Socket,导致UDP端口映射失效,NAT穿透失败。
解决方案:双管齐下
方法一:启用TLS加密(推荐)
使用PJSIP_TRANSPORT_TLS替代 TCP:
pjsip_transport_config cfg; pjsip_transport_config_default(&cfg); cfg.port = 5061; // TLS常用端口 pj_status_t status = pjsip_tls_transport_start(&cfg, NULL, NULL);同时确保证书验证正确处理(避免自签名证书报错)。
方法二:允许特定域名走明文(临时方案)
如果暂时无法部署TLS,可在res/xml/network_security_config.xml中放行:
<network-security-config> <domain-config cleartextTrafficPermitted="true"> <domain includeSubdomains="true">your-sip-server.com</domain> </domain-config> </network-security-config>并在AndroidManifest.xml引用:
<application android:networkSecurityConfig="@xml/network_security_config" ... > </application>📌 建议尽快迁移到TLS,明文传输在未来版本中可能彻底废弃。
方法三:开启Keep-Alive心跳
设置SIP OPTIONS心跳包,防止NAT超时:
pjsua_config ua_cfg; pjsua_config_default(&ua_cfg); ua_cfg.ka_interval = 30; // 每30秒发一次OPTIONS这能有效维持路由器上的公网端口映射。
五、第四个常见问题:音频通道混乱,回声大、声音小、听不见
问题现象
通话时对方听到回声,或只能单向通话,甚至完全无声。
根本原因
Android系统的音频路由策略越来越智能,但前提是你要“告诉它你想干什么”。
很多开发者忽略了两个关键点:
1.没有正确申请音频焦点
2.没有设置合适的 AudioAttributes
结果系统把你当成“普通媒体播放器”,而不是“通话应用”,于是走错了音频通路(例如扬声器外放而非听筒),甚至被音乐App抢占资源。
解决方案:规范使用Audio Focus + 正确标识用途
1. 申请音频焦点
AudioManager am = (AudioManager) getSystemService(AUDIO_SERVICE); AudioFocusRequest focusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN) .setAudioAttributes(new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION) // 关键! .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) .build()) .setAcceptsDelayedFocusGain(true) .setOnAudioFocusChangeListener(focusChangeListener) .build(); int result = am.requestAudioFocus(focusRequest); if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { startCall(); }2. 在pjsip中统一音频参数
确保采样率、声道数一致,避免AEC失效:
pjmedia_audio_dev_param param; pjmedia_audio_dev_default_get_param(PJMEDIA_AUD_DEV_DEFAULT_CAPTURE_ID, ¶m); param.clock_rate = 16000; // 推荐16kHz,兼顾质量与性能 param.channel_count = 1; // 单声道 param.samples_per_frame = 160; // 对应10ms帧 param.bits_per_sample = 16; pjmedia_aud_stream_create(¶m, &aud_cb, user_data, &aud_strm);3. 启用内置AEC(回声消除)
pjmedia_echo_capture *ec; pjmedia_echo_create(pool, 16000, 1, 160, &ec); // 创建回声处理器 pjmedia_aud_stream_set_echo(aud_strm, ec); // 绑定到音频流🔔 提示:若使用蓝牙耳机,需监听
ACTION_HEADSET_PLUG和BluetoothHeadset广播,动态切换设备。
六、终极建议:构建高可用VoIP架构的五大原则
光修修补补还不够。要想让你的pjsip应用在各种Android设备上都稳定运行,必须建立一套完整的健壮性设计体系。
原则一:永远假设服务会被杀死
- 记录当前登录状态(SharedPreference / Room)
- 监听
BOOT_COMPLETED和PACKAGE_REPLACED,开机自动重连 - 使用
JobScheduler或AlarmManager实现断线重试(非实时场景)
原则二:网络变化必须重建栈
监听网络切换:
IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION); registerReceiver(networkChangeReceiver, filter);一旦检测到网络变更(Wi-Fi ↔ 移动数据),立即销毁并重新初始化pjsip栈,避免旧Socket残留。
原则三:ProGuard别乱混淆
JNI函数名不能被混淆,务必保留:
-keep class org.pjsip.pjsua2.** { *; } -dontwarn org.pjsip.pjsua2.* -keepclasseswithmembernames class * { native <methods>; }原则四:ABI覆盖要全
提供以下架构的.so文件:
- armeabi-v7a(32位ARM)
- arm64-v8a(64位ARM,主流)
- x86(模拟器)
- x86_64(部分平板)
可通过Application.mk控制编译目标:
APP_ABI := armeabi-v7a arm64-v8a x86 x86_64原则五:日志要够细,便于远程诊断
开启pjsip详细日志:
pj_log_set_level(5); // 最详细级别 pjsua_logging_config log_cfg; log_cfg.msg_logging = PJ_TRUE;并将关键事件上报至服务器(脱敏处理),方便分析线上问题。
写在最后:适配的本质是理解系统的演进逻辑
pjsip本身没有错,Android的新规则也没有错。冲突的根源在于:旧的开发思维撞上了新的系统治理理念。
过去我们可以“偷偷开个后台服务 + 持续录音”,现在不行了。Google要的是透明、可控、节能的应用生态。
因此,真正的解决方案不是绕过限制,而是顺应规则,合理表达需求:
- 你需要长时间运行?→ 用前台服务告诉用户“我在工作”;
- 你要用麦克风?→ 明确说明用途,获取信任;
- 你要保持连接?→ 使用标准加密 + 合理心跳;
- 你要高质量通话?→ 正确标识音频属性,走专业通路。
当你学会用系统的方式做事,你会发现,不仅兼容性问题迎刃而解,用户体验也会大幅提升。
未来,随着 Android 对隐私和性能的要求进一步提高(如 Scoped Storage、Privacy Dashboard、Direct Boot 支持等),通信类应用还需持续进化。
但只要你掌握了这套“与系统共舞”的方法论,无论版本如何迭代,都能从容应对。
如果你正在开发基于pjsip的VoIP应用,欢迎收藏本文,也欢迎在评论区分享你遇到的奇葩问题和解决方案。我们一起把这条路走得更稳、更远。