云音乐大前端专栏

如何把应用搬上车

2021-07-30

本文作者:陆康、陈驰枻、聂帅

当前造车新势力越来越火,汽车智能化成为风口,很多手机应用希望拓展车机场景,云音乐及旗下 Look 直播也在车机端场景进行了一些探索,下面分享过程中的一些总结和心得体会

目前车载开发的类型和特点

当前车载接入方式主要有三种,第一种是以华为 Hicar 为代表的手机app扩展接入,第二种是提供对外的 OpenApi,车企自行研发应用进行接入,最后一种是最为普遍的车机独立 app 接入。

1. 手机 app 扩展接入,以华为 HiCar 为例

这种方式并不要求给车机提供独立的车载版 apk,而是由手机端的应用接入 Hicar sdk,直接在原有的工程上开发。

目前多家手机厂商采用的车联方案都是基于 Android 系统自带的 MediaSession 框架进行模板化开发,手机端的应用只需要根据厂商提供的模板准备数据,具体的UI展示由车机设备完成,开发者无需关心屏幕适配及UI风格统一的问题,具体的播控指令同步也是通过 MediaSession 框架完成的。

该接入方式需要自己制定 Media Data Tree 的结构。因为 ViewTree 的展示是交给外部进行渲染的,我们往往只能通过 onPlayFromMediaId 回调里的 mediaId 和 extras 来获取车机上点击播放的媒体信息,mediaId可以构造成例如 tab -> page -> listId -> songId 的层级关系,我们就可以知道播放的是具体来自哪个页面中哪个歌单中的哪首歌了,这也是 Android 官方的 Universal Android Music Player Sample 中采用的实现方式。

这种车载接入方式有如下特点

  • 接入方便,直接在原有工程基础上开发,基础能力是现成的,交付形式为手机apk,与原来保持一致
  • 适配方便,例如 Hicar 针对不同类型的应用,直接提供了模板化开发的能力,音频应用只需专注于音频数据的准备和播放服务的实现即可,其它繁琐的工作,例如绘制车机界面并保证各分辨率兼容性、管理音频桌面卡片和实现音频任务接续等都由 HiCar 完成
  • 更新方便,只需要手机上的应用更新了即可更新车机展示逻辑,相比更新车机应用,引导用户的成本低不少
  • 适用范围的局限性,即只与特定平台绑定,比如 Hicar 只支持华为手机,并且要求车机接入了华为 Hicar 系统,目前来说,国产的几家主流手机厂商都在尝试推类似的生态,汽车厂商在互联网造车的势头中,也加快了这些系统的引入,但从总量来说,仍然属于车机中的少部分

2. OpenAPI 接入

这种实现方式是服务端根据我们的服务内容提供对应的 OpenAPI 接口。厂商可以自行设计需求方案和视觉方案,根据不同的需求范围去调用不同的接口来获取数据并展示,但是最后一般需要通过我们的审核才能发布。这种接入方式中的开发资源也是由厂商自己提供的,承载的平台包括 Linux、Android 等多个系统环境。由于这种方式不是本文的重点,就不在此赘诉了。

这种接入方式有以下特点

  • 我方投入的人力成本小,主要开发成本集中在厂商那一边
  • 可以适配各种环境,并不局限于某一种车机系统
  • 可控性较小,数据的获取有一部分依赖于厂商提供,我方只能拿到接口调用次数,在涉及到结算的问题上容易产生分歧
  • 迭代困难,依赖于厂商自身的开发资源

3. 独立 app 接入

可以看出上述 OpenAPI 的实现方式还是存在一些比较关键的问题,所以一般来说我们会优先采用独立 app 接入的方式,这是目前更为普遍的方式,也是本文主要描述的接入方式。这种方式与手机应用开发其实类似,但也有一些特点

  • 车机系统的碎片化相比如今比较成熟的手机生态(绝大多数份额在头部厂商)更加严重,很多厂商基于 Android 研发自己的车机系统,针对方控、桌面 widget、仪表显示等设备依赖能力,厂商往往都会提供自己的一套接入 SDK,所以渠道分包势在必行
  • 车机应用的交互要求简洁,突出重点,应用支持语音操作对于用户来说会是很大的吸引点
  • 测试车机设备比较缺乏
  • 系统版本跨度较大,目前接触到的设备从 Android 4.3 可以一直覆盖到 Android 10
  • 性能一般较为羸弱,在开发时要格外注意性能的瓶颈

方案设计

针对上文中提到的车载独立 app 开发的一些特点,我们在渠道分包、解耦车机依赖、语音操作接入、分辨率适配等方面进行了一系列探索,下面介绍几个相关的方案设计

1. 多渠道接入能力抽象

上面提到车机系统比较碎片化,要实现车机的方控、桌面 widget、仪表显示等控制,一般有两种情况

  1. 厂商的相关操控实现了 Android 原生的 MediaSession 规范,这种情况下我们要响应相关的 KeyEvent,并在各种播放相关时机调用 MediaSession api 更新状态
  2. 厂商为相关操控提供了 sdk 接入,这种情况下我们要按照厂商自定义的规范来

考虑到上层业务代码最好能不感知平台差异,决定对渠道接入能力做一层封装隔离 如上图所示,将渠道依赖的能力抽象为 EnvironmentDependency 接口,不同渠道依赖各自的车机 sdk 实现该接口,Mediasession 规范单独实现一个通用类。业务层看到的是渠道无关的DependcyWrapper 代理实例,只需在各业务处理时机调用代理的对应方法即可,规避了业务层写渠道相关的代码。方控响应能力抽象为 EventCallback 接口,业务实现后注入对应 dependcy 实例,由其适时触发。 针对分渠道打包问题,采用 AGP 自带的 productFlavors 方案,不同的渠道包含不同的源文件夹,隔离 sdk 依赖。

flavorDimensions "channel"
    productFlavors {
        //小鹏
        xp {
            dimension "channel"
            buildConfigField("String", "channel", "\"xp\"")
        }
        //比亚迪
        byd {
            dimension "channel"
            buildConfigField("String", "channel", "\"byd\"")
        }
       ......
    }

2. 语音控制的设计实现

要做语音控制,首先需要思考如下问题

  1. 是应用自己实现还是使用车机能力? 从对接经验来看,目前提供车机语音开放能力的厂商并不普遍,个别厂商即使提供,其接入和自定义流程也比较复杂,需要相当长的周期,所以应用自己集成三方sdk来实现是更合理的选择,但是针对于一些需要支持车机自带语音助手的厂商我们也要提供出对应的方案
  2. 语音控制如何唤起?(除了页面点击外,能否提供其他快捷入口) 如果要实现特定短句唤起语音助手,就要求语音识别 sdk 在应用生命周期内长期收音,一直抢占着 mic 焦点,导致车机系统自带的语音助手无法工作(有个别车机实现了多麦克风阵列,即系统收音使用单独 mic 通道,但这种车机是极少数),因此,短句唤起方案是行不通的。那么,能否借助方控呢?方控普遍能提供确认键的响应,如果应用业务本身不需要确认键(如应用为直播业务,不需要暂停、恢复)则可直接使用确认键唤起语音助手,如果需要,也可以设计某种点按方式唤起(比如长按或者双击,这可以通过在业务层判断按键事件的时间间隔做到),当然,对应的引导也需要跟上,比如在用户首次进入时展示浮层加语音的引导
  3. 如何从语音识别出的文字映射到对应操作?最方便的做法肯定是客户端直接判断文字匹配性,比如识别到“下一首”就切换到下一个直播,但是这种做法容错性较低,用户稍微调整下说法就会失效,更加合理的做法是在语音转文字环节后再加上语义识别环节,流程如下

解决了这些基本问题后,再来考虑下一个比较完善的语音助手的完整交互流程,助手唤起后,会首先进入询问态并提示语音支持的操作类型,接着用户输入,如果输入超时会提示助手即将关闭,正常输入后进行请求解析,获取结果后某些操作执行会直接关闭面板,而某些操作将直接在面板展示结果并回到询问态,若无法解析则直接提示并回到询问态,由此可见客户端上整个流程比较适合抽象为一个状态机

  1. 如果需要对接不同的车机自带的语音助手,涉及到控制相关的指令和播放信息的回调需要抽离出更为普遍的接口去实现,对于常见的指令,比如播放、暂停、上一首、下一首、收藏、搜索点播等需要封装成独立的方法,不同的车机的 app 注册不同的 server,客户端的实现则由同一个 client 处理,同时能将客户端处理后的结果返回给 server 端进行展示,这样做的好处是与车机对接的部分完全交给 server 进行处理,client 只需要根据下发的指令进行对应的操作即可,前半部分是解耦的,后半部分是复用的

3. 多分辨率适配

前置的视觉交互设计中,考虑到驾驶时的场景,常用的操作区域要尽量放在靠近驾驶侧的一边,同时交互流程要尽可能简单,页面跳转层级不宜过多。除去主流的横屏布局之外,比亚迪、小鹏等车机屏幕也会存在竖屏的情况。 常见的屏幕适配方案包括 smallestWidth 适配、头条的修改 DisplayMetrics#density 方案、使用百分比布局等。结合项目的实际情况,我们建议大部分的布局都采用流式布局,只需要在布局中改变 recyclerView 的方向就可以适配横竖屏的切换,同时卡片布局尽量扁平化,ConstraintLayout 中的 Guideline、layoutconstraintHeightpercent 等属性都能帮助我们很方便的实现百分比布局,如果遇到比例特别奇怪的屏幕,页面又不能使用流式布局时,可以考虑结合 sw 限定符的方案,让视觉同学给出布局调整策略,单独针对少量特殊的屏幕进行适配。 在进行视觉适配开发时,我们的第一反应当然是让厂商提供所有可能涉及的车机设备,然而这是不现实的,从我们的对接经验来看,测试车机是相当紧缺的,部分厂商甚至连车机都暂时无法提供,只提供文档,让我们自行适配后再内部测试。在这种情况下,我们只能模拟不同的分辨率设备。adb shell wm size 命令就是解决方法,其接受 总长度像素值x总宽度像素值 格式的参数,运行后即可调整成对应的长宽比,测试过程只需要在同一设备上运行不同参数的命令即可实现不同分辨率的模拟。

性能优化

上面提到车机相比于手机,总体性能上要落后很多。在一开始,一方面由于历史包袱、组件复用等因素,另一方面编写代码时也往往忽略了性能相关问题,使得 app 运行在车机上的体验相当糟糕,安装慢、启动速度慢、卡顿丢帧等性能问题很明显的就暴露了出来,于是我们做了一系列针对性的优化

1. 减小包体积

减小包体积包括代码和资源两方面,通常的做法如下:

  • 图片压缩
  • 资源混淆
  • 减少 Dex 数量

2. 减少进程数

多进程运行需要占用更多的系统资源,在性能较弱的设备上,单app多进程的运行方式会给设备 CPU、内存等带来更多压力

3. 减少线程数

和进程相似,线程过多在启动中频繁切换带来了很大的开销成本,主线程得到执行的时间也会减少

4. IO优化

启动过程中文件 IO 过多也会拖慢启动速度,尽量减少不必要的文件读写

5. 减少Activity的跳转次数

为了更快地展示界面或者执行某项具体功能,最好减少启动流程中 Activity 的跳转层级,每多一个 Activity 就会增加几百毫秒的耗时;在请求一些接口时,也要考虑到请求时机,是否可以前置并行请求,或者合并请求,减少接口的 RT

6. 优化布局层次,减少过度绘制

下面分享一个性能优化的实例,在与某家车厂的合作过程中,厂商反馈语音唤起阶段从冷启动到开始播放速度特别慢,将近 8s 之久。我们在手机上测试是完全没有问题的,但是受限于车机的性能,在前后反复数轮的沟通联调下,我们主要做了以下优化

  • 大幅减少包体积,删除大量无用业务代码,包体积减少约 80%,因包体积大幅减小,启动过程需要解压的dex数量也相应减少,加载的类变少,速度有数秒提升
  • 将播放进程合入主进程,多进程改为单进程,并去除 aidl,去除 aidl 通信前后的几次文件读写,减少约2s左右耗时
  • 将 LoadingActivity 和首页 Activity 合并为一个,减少启动链路过程 activity 的数量,减少约数百毫秒耗时
  • 将多个接口合并成一个减少网络请求,减少约 200 毫秒耗时

最终将时间压缩到 3s 内,我们的优化过程从前期对耗时明显部分着重优化,效果明显,到后期分析启动日志,一点点抠细节,最终通过厂商方面的验收。在开始着手优化前,需要量化好具体指标,明确好目标再着手进行,用数据来衡量优化效果能让优化过程更加顺畅

踩坑指南

车载开发过程中,还遇到了一些之前手机应用开发不常见的问题,印象深刻,也在这里分享下

1. 上了预装,RN页面咋都不行了

车载场景,用户主动下载及更新 app 的频率相对手机来说要低很多,所以预装是很重要的铺量手段,但当我们好不容易与某渠道谈成预装后,却发现一个奇怪的问题,所有用 RN 实现的页面进入进入或者预加载就会引起应用的 crash,崩溃堆栈提示的直接原因是 libjsexcutor.so 这个 RN 依赖的 js 解析库加载失败了,于是初步看了下 RN 崩溃位置的源码,发现 RN 的 so 库都是通过 SoLoader 这个 facebook 的工具加载的(官方文档说主要用来兼容 4.3 以下版本的 so 加载依赖问题),而应用中其他业务 so 的都是正常工作的,所以就猜测 SoLoader 在应用预装场景会存在问题,于是复现并重点查看 Soloader 相关的日志 上图为问题渠道上的 RN 加载日志,而下图为正常场景下的 RN 加载日志 可以看到两者的区别就在于问题渠道上,标红处的 so 查找路径没有被添加(该路径实际就是应用安装后的 so 路径的软链接),而正常渠道上是在该路径上找到了 RN 相关的 so 并进行了加载,顺着该思路查看了下 SoLoader 的源码,发现有如下逻辑 即判断当前应用是系统应用后,就不将 app 默认 so 路径加入查找路径,导致 RN 相关用 Soloader 加载的库都会失败,定位到原因后,再仔细过了下 SoLoader 加载 so 相关源码,发现其提供了 setSystemLoadLibraryWrapper 的设置接口,可以由上层来定义针对系统应用场景如何加载依赖的 so,所以我们只要设置该场景用应用原本的 so 加载方式即可解决问题,如下代码所示

SoLoader.setSystemLoadLibraryWrapper {
    ReLinker.loadLibrary(context, it)
}

2. 车机测试设备上的奇怪问题

  1. 某个渠道的测试车机连上公司 wifi 后,始终无法访问网络,与厂商沟通,他们告知也是首次提供测试车机给外部,内部使用是没问题的,于是只能自己定位。考虑到大概率与网络环境有关,遂用 iptables 工具查看车机网络规则( iptables 是运行在用户空间的应用软件,通过控制 Linux 内核 netfilter 模块,来管理网络数据包的处理和转发,数据包的详细流转流程如下图所示,可以在各个环节增加规则来拦截) 查看后果然发现部分规则比较特殊,猜测是测试车机本来是只给厂商内部使用的,为了防止流出后产生问题,对网络环境做了识别,一旦发现非厂商公司内网就丢弃数据包,于是用如下命令清理规则,问题解决

    iptables -F
    iptables -X
    iptables -P INPUT ACCEPT
    iptables -P OUTPUT ACCEPT
    iptables -P FORWARD ACCEPT
  2. 某个渠道的车机,开发过程发现部分接口报错。仔细看了下,发现报错的接口都是 https 协议(开发阶段还在测试环境,大部分接口是 http 协议),adb 日志里看到的报错内容大致如下
javax.net.ssl.SSLHandshakeException: com.android.org.bouncycastle.jce.exception.ExtCertPathValidatorException: 
Could not validate certificate: Certificate not valid until Wed Dec 16 
09:00:05 GMT+08:00 2015 (compared to Sun Oct 12 16:20:03 GMT+08:00 1980)

看起来是时间和证书有效期对不上,查看系统时间发现确实不对,原来该车机每次启动后都会重置系统时间,而 SSL 客户端的校验过程是包含证书有效期校验的,调整系统时间后即可解决问题 上述可见,测试车机会因为一些特殊设定而带来一些奇怪的开发问题,不过比较好的一点是这些测试车机往往是已经 root 过的,所以命令权限足够大,可以进行深入地分析。

技术之外的体会

参与车载应用从启动到正式上架的全过程,技术之外,还有一些其他的体会

  • 车厂项目管理和互联网产品有较大区别,其作风比较严谨细致,求稳不求快,没有互联网快速迭代的理念,往往不太能接受部分问题先带上线后续迭代 fix 的做法,所以其测试周期通常比较长,问题反馈轮次较多,反馈问题的角度也比较多样(产品设计、内容运营、技术点),应用方需要有心理准备,耐心处理。
  • 在与车厂初步沟通时,就要对齐好交付标准,比如适配的需求范围、应用的性能指标等等,避免因为交付标准的不统一造成来回的沟通和返工,根据我们自身的项目情况也要制定好自己的标准基线,平时通过 Monkey 和性能自动化测试保证 app 的稳定性
  • 目前各车厂接入 app 的整体流程还不能说很完善,存在文档欠缺、测试车机欠缺、模拟器不稳定、反馈问题响应较慢等问题,这就要求应用方早做功课,对依赖项要尽早梳理,和厂商及时沟通,预知风险,后面随着应用接入越来越普遍,厂商这块的建设应该会有改进。

小结

本文介绍了目前车载开发的一些现状,分享了一些开发过程的设计思路和遇到的典型问题,希望能对大家的应用上车有所帮助!

本文发布自 网易云音乐大前端团队,文章未经授权禁止任何形式的转载。我们常年招收前端、iOS、Android,如果你准备换工作,又恰好喜欢云音乐,那就加入我们 grp.music-fe(at)corp.netease.com!