音频播放开发概述
如何选择音频播放开发方式
系统提供了多样化的API,来帮助开发者完成音频播放的开发,不同的API适用于不同音频数据格式、音频资源来源、音频利用场景,甚至是不同开发语言。因此,选择符合的音频播放API,有助于降低开发工作量,实现更佳的音频播放效果。
- AudioRenderer:用于音频输出的ArkTS/JS API,仅支持PCM格式,需要应用持续写入音频数据进行工作。应用可以在输入前添加数据预处置处罚,如设定音频文件的采样率、位宽等,要求开发者具备音频处置处罚的基础知识,适用于更专业、更多样化的媒体播放应用开发。
- AudioHaptic:用于音振协同播放的ArkTS/JS API,适用于需要在播放音频时同步发起振动的场景,如来电铃声随振、键盘按键反馈、消息通知反馈等。
- OpenSL ES:一套跨平台尺度化的音频Native API,同样提供音频输出本领,仅支持PCM格式,适用于从其他嵌入式平台移植,或依赖在Native层实现音频输出功能的播放应用利用。
- OHAudio:用于音频输出的Native API,此API在设计上实现归一,同时支持普通音频通路和低时延通路。仅支持PCM格式,适用于依赖Native层实现音频输出功能的场景。
除上述方式外,也可以通过Media Kit实现音频播放。
- AVPlayer:用于音频播放的ArkTS/JS API,集成了流媒体和本地资源剖析、媒体资源解封装、音频解码和音频输出功能。可用于直接播放mp3、m4a等格式的音频文件,不支持直接播放PCM格式文件。
- SoundPool:低时延的短音播放ArkTS/JS API,适用于播放急促简短的音效,如相机快门音效、按键音效、游戏射击音效等。
开发音频播放应用须知
应用如果要实现后台播放或熄屏播放,需要同时满足:
- 利用媒领会话功能注册到系统内同一管理,否则在应用进入后台时,播放将被强制停止。具体参考AVSession Kit开发指导。
- 申请长时任务制止进入挂起(Suspend)状态。具体参考长时任务开发指导。
当应用进入后台,播放被停止,如果被媒领会话管控,将打印日记“pause id”;如果没有该日记,则阐明被长时任务管控。
利用AudioRenderer开发音频播放功能
AudioRenderer是音频渲染器,用于播放PCM(Pulse Code Modulation)音频数据,相比AVPlayer而言,可以在输入前添加数据预处置处罚,更适合有音频开发履历的开发者,以实现更灵活的播放功能。
开发指导
利用AudioRenderer播放音频涉及到AudioRenderer实例的创建、音频渲染参数的配置、渲染的开始与停止、资源的释放等。本开发指导将以一次渲染音频数据的过程为例,向开发者讲解如何利用AudioRenderer进行音频渲染,发起搭配AudioRenderer的API阐明阅读。
下图展示了AudioRenderer的状态变化,在创建实例后,调用对应的方法可以进入指定的状态实现对应的举动。需要注意的是在确定的状态实行不符合的方法可能导致AudioRenderer发生错误,发起开发者在调用状态转换的方法前进行状态检查,制止程序运行产生预期以外的效果。
为包管UI线程不被阻塞,大部分AudioRenderer调用都是异步的。对于每个API均提供了callback函数和Promise函数,以下示例均采用callback函数。
图1 AudioRenderer状态变化表示图
在进行应用开发的过程中,发起开发者通过on(‘stateChange’)方法订阅AudioRenderer的状态变更。因为针对AudioRenderer的某些操纵,仅在音频播放器在固定状态时才气实行。如果应用在音频播放器处于错误状态时实行操纵,系统可能会抛出异常或生成其他未定义的举动。
- prepared状态: 通过调用createAudioRenderer()方法进入到该状态。
- running状态: 正在进行音频数据播放,可以在prepared状态通过调用start()方法进入此状态,也可以在paused状态和stopped状态通过调用start()方法进入此状态。
- paused状态: 在running状态可以通过调用pause()方法停息音频数据的播放并进入paused状态,停息播放之后可以通过调用start()方法继续音频数据播放。
- stopped状态: 在paused/running状态可以通过stop()方法停止音频数据的播放。
- released状态: 在prepared、paused、stopped等状态,用户均可通过release()方法释放掉全部占用的硬件和软件资源,并且不会再进入到其他的任何一种状态了。
开发步骤及注意事项
- 配置音频渲染参数并创建AudioRenderer实例,音频渲染参数的具体信息可以检察AudioRendererOptions。
- import { audio } from '@kit.AudioKit';
- let audioStreamInfo: audio.AudioStreamInfo = {
- samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_48000, // 采样率
- channels: audio.AudioChannel.CHANNEL_2, // 通道
- sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE, // 采样格式
- encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW // 编码格式
- };
- let audioRendererInfo: audio.AudioRendererInfo = {
- usage: audio.StreamUsage.STREAM_USAGE_VOICE_COMMUNICATION,
- rendererFlags: 0
- };
- let audioRendererOptions: audio.AudioRendererOptions = {
- streamInfo: audioStreamInfo,
- rendererInfo: audioRendererInfo
- };
- audio.createAudioRenderer(audioRendererOptions, (err, data) => {
- if (err) {
- console.error(`Invoke createAudioRenderer failed, code is ${err.code}, message is ${err.message}`);
- return;
- } else {
- console.info('Invoke createAudioRenderer succeeded.');
- let audioRenderer = data;
- }
- });
复制代码
- 调用on(‘writeData’)方法,订阅监听音频数据写入回调。
- import { BusinessError } from '@kit.BasicServicesKit';
- import { fileIo } from '@kit.CoreFileKit';
- let bufferSize: number = 0;
- class Options {
- offset?: number;
- length?: number;
- }
- let path = getContext().cacheDir;
- //确保该路径下存在该资源
- let filePath = path + '/StarWars10s-2C-48000-4SW.wav';
- let file: fileIo.File = fileIo.openSync(filePath, fileIo.OpenMode.READ_ONLY);
- let writeDataCallback = (buffer: ArrayBuffer) => {
-
- let options: Options = {
- offset: bufferSize,
- length: buffer.byteLength
- }
- fileIo.readSync(file.fd, buffer, options);
- bufferSize += buffer.byteLength;
- }
- audioRenderer.on('writeData', writeDataCallback);
复制代码
- 调用start()方法进入running状态,开始渲染音频。
- import { BusinessError } from '@kit.BasicServicesKit';
- audioRenderer.start((err: BusinessError) => {
- if (err) {
- console.error(`Renderer start failed, code is ${err.code}, message is ${err.message}`);
- } else {
- console.info('Renderer start success.');
- }
- });
复制代码- import { BusinessError } from '@kit.BasicServicesKit';
- audioRenderer.stop((err: BusinessError) => {
- if (err) {
- console.error(`Renderer stop failed, code is ${err.code}, message is ${err.message}`);
- } else {
- console.info('Renderer stopped.');
- }
- });
复制代码- import { BusinessError } from '@kit.BasicServicesKit';
- audioRenderer.release((err: BusinessError) => {
- if (err) {
- console.error(`Renderer release failed, code is ${err.code}, message is ${err.message}`);
- } else {
- console.info('Renderer released.');
- }
- });
复制代码 选择精确的StreamUsage
创建播放器时候,开发者需要根据应用场景指定播放器的StreamUsage,选择精确的StreamUsage可以制止用户碰到不符合预期的举动。
在音频API文档StreamUsage介绍中,列举了每一种类型推荐的应用场景。比方音乐场景推荐利用STREAM_USAGE_MUSIC,电影或者视频场景推荐利用STREAM_USAGE_MOVIE,游戏场景推荐利用STREAM_USAGE_GAME,等等。
如果开发者配置了不精确的StreamUsage,可能带来一些不符合预期的举动。比方以下场景。
- 游戏场景错误利用STREAM_USAGE_MUSIC类型,游戏应用将无法和其他音乐应用并发播放,而游戏场景通常可以与其他音乐应用并发播放。
- 导航场景错误利用STREAM_USAGE_MUSIC类型,导航应用播报时候会导致正在播放的音乐停止播放,而导航场景我们通常期望正在播放的音乐仅仅降低音量播放。
完整示例
下面展示了利用AudioRenderer渲染音频文件的示例代码。
- import { audio } from '@kit.AudioKit';
- import { BusinessError } from '@kit.BasicServicesKit';
- import { fileIo } from '@kit.CoreFileKit';
- const TAG = 'AudioRendererDemo';
- class Options {
- offset?: number;
- length?: number;
- }
- let context = getContext(this);
- let bufferSize: number = 0;
- let renderModel: audio.AudioRenderer | undefined = undefined;
- let audioStreamInfo: audio.AudioStreamInfo = {
- samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_48000, // 采样率
- channels: audio.AudioChannel.CHANNEL_2, // 通道
- sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE, // 采样格式
- encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW // 编码格式
- }
- let audioRendererInfo: audio.AudioRendererInfo = {
- usage: audio.StreamUsage.STREAM_USAGE_MUSIC, // 音频流使用类型
- rendererFlags: 0 // 音频渲染器标志
- }
- let audioRendererOptions: audio.AudioRendererOptions = {
- streamInfo: audioStreamInfo,
- rendererInfo: audioRendererInfo
- }
- let path = getContext().cacheDir;
- //确保该路径下存在该资源
- let filePath = path + '/StarWars10s-2C-48000-4SW.wav';
- let file: fileIo.File = fileIo.openSync(filePath, fileIo.OpenMode.READ_ONLY);
- let writeDataCallback = (buffer: ArrayBuffer) => {
- let options: Options = {
- offset: bufferSize,
- length: buffer.byteLength
- }
- fileIo.readSync(file.fd, buffer, options);
- bufferSize += buffer.byteLength;
- }
- // 初始化,创建实例,设置监听事件
- function init() {
- audio.createAudioRenderer(audioRendererOptions, (err, renderer) => { // 创建AudioRenderer实例
- if (!err) {
- console.info(`${TAG}: creating AudioRenderer success`);
- renderModel = renderer;
- if (renderModel !== undefined) {
- (renderModel as audio.AudioRenderer).on('writeData', writeDataCallback);
- }
- } else {
- console.info(`${TAG}: creating AudioRenderer failed, error: ${err.message}`);
- }
- });
- }
- // 开始一次音频渲染
- function start() {
- if (renderModel !== undefined) {
- let stateGroup = [audio.AudioState.STATE_PREPARED, audio.AudioState.STATE_PAUSED, audio.AudioState.STATE_STOPPED];
- if (stateGroup.indexOf((renderModel as audio.AudioRenderer).state.valueOf()) === -1) { // 当且仅当状态为prepared、paused和stopped之一时才能启动渲染
- console.error(TAG + 'start failed');
- return;
- }
- // 启动渲染
- (renderModel as audio.AudioRenderer).start((err: BusinessError) => {
- if (err) {
- console.error('Renderer start failed.');
- } else {
- console.info('Renderer start success.');
- }
- });
- }
- }
- // 暂停渲染
- function pause() {
- if (renderModel !== undefined) {
- // 只有渲染器状态为running的时候才能暂停
- if ((renderModel as audio.AudioRenderer).state.valueOf() !== audio.AudioState.STATE_RUNNING) {
- console.info('Renderer is not running');
- return;
- }
- // 暂停渲染
- (renderModel as audio.AudioRenderer).pause((err: BusinessError) => {
- if (err) {
- console.error('Renderer pause failed.');
- } else {
- console.info('Renderer pause success.');
- }
- });
- }
- }
- // 停止渲染
- async function stop() {
- if (renderModel !== undefined) {
- // 只有渲染器状态为running或paused的时候才可以停止
- if ((renderModel as audio.AudioRenderer).state.valueOf() !== audio.AudioState.STATE_RUNNING && (renderModel as audio.AudioRenderer).state.valueOf() !== audio.AudioState.STATE_PAUSED) {
- console.info('Renderer is not running or paused.');
- return;
- }
- // 停止渲染
- (renderModel as audio.AudioRenderer).stop((err: BusinessError) => {
- if (err) {
- console.error('Renderer stop failed.');
- } else {
- fileIo.close(file);
- console.info('Renderer stop success.');
- }
- });
- }
- }
- // 销毁实例,释放资源
- async function release() {
- if (renderModel !== undefined) {
- // 渲染器状态不是released状态,才能release
- if (renderModel.state.valueOf() === audio.AudioState.STATE_RELEASED) {
- console.info('Renderer already released');
- return;
- }
- // 释放资源
- (renderModel as audio.AudioRenderer).release((err: BusinessError) => {
- if (err) {
- console.error('Renderer release failed.');
- } else {
- console.info('Renderer release success.');
- }
- });
- }
- }
复制代码 当同优先级或高优先级音频流要利用输出设备时,当前音频流会被停止,应用可以自行相应停止事件并做出处置处罚。具体的音频并发处置处罚方式可参考处置处罚音频焦点事件
利用OHAudio开发音频播放功能(C/C++)
OHAudio是系统在API version 10中引入的一套C API,此API在设计上实现归一,同时支持普通音频通路和低时延通路。仅支持PCM格式,适用于依赖Native层实现音频输出功能的场景。
利用入门
开发者要利用OHAudio提供的播放或者录制本领,需要添加对应的头文件。
在 CMake 脚本中链接动态库
- target_link_libraries(sample PUBLIC libohaudio.so)
复制代码 添加头文件
开发者通过引入<native_audiostreambuilder.h>头文件,利用音频播放相干API。
- #include <ohaudio/native_audiorenderer.h>
- #include <ohaudio/native_audiostreambuilder.h>
复制代码 音频流构造器
OHAudio提供OH_AudioStreamBuilder接口,遵循构造器设计模式,用于构建音频流。开发者需要根据业务场景,指定对应的OH_AudioStream_Type。
OH_AudioStream_Type包含两种类型:
- AUDIOSTREAM_TYPE_RENDERER
- AUDIOSTREAM_TYPE_CAPTURER
利用OH_AudioStreamBuilder_Create创建构造器示例:
- OH_AudioStreamBuilder* builder;
- OH_AudioStreamBuilder_Create(&builder, streamType);
复制代码 在音频业务结束之后,开发者应该实行OH_AudioStreamBuilder_Destroy接口来烧毁构造器。
- OH_AudioStreamBuilder_Destroy(builder);
复制代码 开发步骤及注意事项
具体的API阐明请参考OHAudio API参考。
开发者可以通过以下几个步骤来实现一个简单的播放功能。
- OH_AudioStreamBuilder* builder;
- OH_AudioStreamBuilder_Create(&builder, AUDIOSTREAM_TYPE_RENDERER);
复制代码
- 配置音频流参数
创建音频播放构造器后,可以设置音频流所需要的参数,可以参考下面的案例。
- // 设置音频采样率
- OH_AudioStreamBuilder_SetSamplingRate(builder, 48000);
- // 设置音频声道
- OH_AudioStreamBuilder_SetChannelCount(builder, 2);
- // 设置音频采样格式
- OH_AudioStreamBuilder_SetSampleFormat(builder, AUDIOSTREAM_SAMPLE_S16LE);
- // 设置音频流的编码类型
- OH_AudioStreamBuilder_SetEncodingType(builder, AUDIOSTREAM_ENCODING_TYPE_RAW);
- // 设置输出音频流的工作场景
- OH_AudioStreamBuilder_SetRendererInfo(builder, AUDIOSTREAM_USAGE_MUSIC);
复制代码- 注意,播放的音频数据要通过回调接口写入,开发者要实现回调接口,使用OH_AudioStreamBuilder_SetRendererCallback设置回调函数。回调函数的声明请查看[OH_AudioRenderer_Callbacks](https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/_o_h_audio-V5#oh_audiorenderer_callbacks) 。
复制代码
- 设置音频回调函数
多音频并发处置处罚可参考文档处置处罚音频焦点事件,仅接口语言差异。
- // 自定义写入数据函数
- int32_t MyOnWriteData(
- OH_AudioRenderer* renderer,
- void* userData,
- void* buffer,
- int32_t length)
- {
- // 将待播放的数据,按length长度写入buffer
- return 0;
- }
- // 自定义音频流事件函数
- int32_t MyOnStreamEvent(
- OH_AudioRenderer* renderer,
- void* userData,
- OH_AudioStream_Event event)
- {
- // 根据event表示的音频流事件信息,更新播放器状态和界面
- return 0;
- }
- // 自定义音频中断事件函数
- int32_t MyOnInterruptEvent(
- OH_AudioRenderer* renderer,
- void* userData,
- OH_AudioInterrupt_ForceType type,
- OH_AudioInterrupt_Hint hint)
- {
- // 根据type和hint表示的音频中断信息,更新播放器状态和界面
- return 0;
- }
- // 自定义异常回调函数
- int32_t MyOnError(
- OH_AudioRenderer* renderer,
- void* userData,
- OH_AudioStream_Result error)
- {
- // 根据error表示的音频异常信息,做出相应的处理
- return 0;
- }
- OH_AudioRenderer_Callbacks callbacks;
- // 配置回调函数
- callbacks.OH_AudioRenderer_OnWriteData = MyOnWriteData;
- callbacks.OH_AudioRenderer_OnStreamEvent = MyOnStreamEvent;
- callbacks.OH_AudioRenderer_OnInterruptEvent = MyOnInterruptEvent;
- callbacks.OH_AudioRenderer_OnError = MyOnError;
- //设置输出音频流的回调
- OH_AudioStreamBuilder_SetRendererCallback(builder, callbacks, nullptr);
复制代码- 为了避免不可预期的行为,在设置音频回调函数时,请确保OH_AudioRenderer_Callbacks的每一个回调都被**自定义的回调方法**或**空指针**初始化。
复制代码- // 自定义写入数据函数
- int32_t MyOnWriteData(
- OH_AudioRenderer* renderer,
- void* userData,
- void* buffer,
- int32_t length)
- {
- // 将待播放的数据,按length长度写入buffer
- return 0;
- }
- // 自定义音频中断事件函数
- int32_t MyOnInterruptEvent(
- OH_AudioRenderer* renderer,
- void* userData,
- OH_AudioInterrupt_ForceType type,
- OH_AudioInterrupt_Hint hint)
- {
- // 根据type和hint表示的音频中断信息,更新播放器状态和界面
- return 0;
- }
- OH_AudioRenderer_Callbacks callbacks;
- // 配置回调函数,如果需要监听,则赋值
- callbacks.OH_AudioRenderer_OnWriteData = MyOnWriteData;
- callbacks.OH_AudioRenderer_OnInterruptEvent = MyOnInterruptEvent;
- // (必选)如果不需要监听,使用空指针初始化
- callbacks.OH_AudioRenderer_OnStreamEvent = nullptr;
- callbacks.OH_AudioRenderer_OnError = nullptr;
复制代码- OH_AudioRenderer* audioRenderer;
- OH_AudioStreamBuilder_GenerateRenderer(builder, &audioRenderer);
复制代码
- 利用音频流
音频流包含下面接口,用来实现对音频流的控制。
接口阐明OH_AudioStream_Result OH_AudioRenderer_Start(OH_AudioRenderer* renderer)开始播放OH_AudioStream_Result OH_AudioRenderer_Pause(OH_AudioRenderer* renderer)停息播放OH_AudioStream_Result OH_AudioRenderer_Stop(OH_AudioRenderer* renderer)停止播放OH_AudioStream_Result OH_AudioRenderer_Flush(OH_AudioRenderer* renderer)释放缓存数据OH_AudioStream_Result OH_AudioRenderer_Release(OH_AudioRenderer* renderer)释放播放实例
- 释放构造器
构造器不再利用时,需要释放相干资源。
- OH_AudioStreamBuilder_Destroy(builder);
复制代码 设置低时延模式
当设备支持低时延通路时,开发者可以利用低时延模式创建播放器,获得更高质量的音频体验。
开发流程与普通播放场景同等,仅需要在创建音频流构造器时,调用OH_AudioStreamBuilder_SetLatencyMode()设置低时延模式。
开发示例
- OH_AudioStreamBuilder_SetChannelLayout(builder, CH_LAYOUT_STEREO);
复制代码 设置音频声道布局
播放音频文件时,可以通过设置音频的声道布局信息,指定渲染或播放时的扬声器摆位,使得渲染和播放效果更佳,获得更高质量的音频体验。
开发流程与普通播放场景同等,仅需要在创建音频流构造器时,调用OH_AudioStreamBuilder_SetChannelLayout()设置声道布局信息。
当声道布局与声道数不匹配时,创建音频流会失败。发起在设置声道布局时,确认下发的声道布局信息是精确的。
如果不知道准确的声道布局信息,或者开发者需要利用默认声道布局,可以不调用设置声道布局接口,或者下发CH_LAYOUT_UNKNOWN,以利用基于声道数的默认声道布局。
对于HOA格式的音频,想要获得精确的渲染和播放效果,必须指定声道布局信息。
开发示例
- OH_AudioStreamBuilder_SetChannelLayout(builder, CH_LAYOUT_STEREO);
复制代码 播放AudioVivid格式音源
播放AudioVivid格式音频文件时,需要利用与普通播放不同的数据写入回调函数,该回调可以同时写入PCM数据与元数据。
开发流程与普通播放场景同等,仅需要在创建音频流构造器时,调用OH_AudioStreamBuilder_SetWriteDataWithMetadataCallback()设置PCM数据与元数据同时写入的回调函数,同时调用OH_AudioStreamBuilder_SetEncodingType()设置编码类型为AUDIOSTREAM_ENCODING_TYPE_AUDIOVIVID。
在播放AudioVivid时,帧长是固定的,不可通过OH_AudioStreamBuilder_SetFrameSizeInCallback()设置回调帧长。同时,在设置播放声道数和声道布局时,需要将写入音源的声床数和对象数相加后进行设置。
开发示例
- // 自定义同时写入PCM数据和元数据函数
- int32_t MyOnWriteDataWithMetadata(
- OH_AudioRenderer* renderer,
- void* userData,
- void* audioData,
- int32_t audioDataSize,
- void* metadata,
- int32_t metadataSize)
- {
- // 将待播放的PCM数据和元数据,分别按audioDataSize和metadataSize写入buffer
- return 0;
- }
- // 设置编码类型
- OH_AudioStreamBuilder_SetEncodingType(builder, AUDIOSTREAM_ENCODING_TYPE_AUDIOVIVID);
- // 配置回调函数
- OH_AudioRenderer_WriteDataWithMetadataCallback metadataCallback = MyOnWriteDataWithMetadata;
- // 设置同时写入PCM数据和元数据的回调
- OH_AudioStreamBuilder_SetWriteDataWithMetadataCallback(builder, metadataCallback, nullptr);
复制代码 写在末了
有很多小同伴不知道学习哪些鸿蒙开发技能?不知道需要重点掌握哪些鸿蒙应用开发知识点?而且学习时频仍踩坑,终极浪费大量时间。所以有一份实用的鸿蒙(HarmonyOS NEXT)文档用来跟着学习黑白常有须要的。
盼望这一份鸿蒙学习文档能够给大家带来帮助,有需要的小同伴自行领取,限时开源,先到先得~无套路领取!!
如果你是一名有履历的资深Android移动开发、Java开发、前端开发、对鸿蒙感兴趣以及转行职员,可以直接领取这份资料
请点击→纯血版全套鸿蒙HarmonyOS学习文档
鸿蒙(HarmonyOS NEXT)5.0最新学习路线
路线图适合人群:
IT开发职员:想要拓展职业边界
零基础小白:鸿蒙爱好者,盼望从0到1学习,增长一项技能。
技能提拔/进阶跳槽:发展瓶颈期,提拔职场竞争力,快速掌握鸿蒙技能
获取以上完整版高清学习路线,请点击→纯血版全套鸿蒙HarmonyOS学习文档
《鸿蒙 (HarmonyOS)开发入门教学视频》
《鸿蒙生态应用开发V3.0白皮书》
《鸿蒙 (OpenHarmony)开发基础到实战手册》
OpenHarmony北向、南向开发环境搭建
《鸿蒙开发基础》
●ArkTS语言
●安装DevEco Studio
●运用你的第一个ArkTS应用
●ArkUI声明式UI开发
.……
《鸿蒙开发进阶》
●Stage模子入门
●网络管理
●数据管理
●电话服务
●分布式应用开发
●通知与窗口管理
●多媒体技能
●安全技能
●任务管理
●WebGL
●国际化开发
●应用测试
●DFX面向未来设计
●鸿蒙系统移植和裁剪定制
……
《鸿蒙进阶实战》
●ArkTS实践
●UIAbility应用
●网络案例
……
获取以上完整鸿蒙HarmonyOS学习文档,请点击→纯血版全套鸿蒙HarmonyOS学习文档
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |