概述
在開(kāi)發(fā)應(yīng)用時(shí)率触,要實(shí)現(xiàn)高效的客戶端跟服務(wù)器之間數(shù)據(jù)交換终议,文件傳輸?shù)男阅苁侵陵P(guān)重要的。一個(gè)數(shù)據(jù)交換性能較低的應(yīng)用會(huì)導(dǎo)致其在加載過(guò)程中耗費(fèi)較長(zhǎng)時(shí)間葱蝗,在很多的場(chǎng)景造成頁(yè)面卡頓穴张,極大的影響了用戶體驗(yàn)。相反两曼,一個(gè)數(shù)據(jù)交換高效的應(yīng)用皂甘,則會(huì)讓應(yīng)用變得更加流暢。
本文將介紹兩種常見(jiàn)的上傳下載傳輸和網(wǎng)絡(luò)請(qǐng)求的關(guān)鍵技術(shù):數(shù)據(jù)壓縮和斷點(diǎn)續(xù)傳悼凑,可提升上傳下載的性能偿枕、減少寬帶占用,從而提高數(shù)據(jù)傳輸效率户辫。
上傳下載接口
目前系統(tǒng)內(nèi)提供給文件上傳下載可用的模塊有@ohos.net.http模塊和@ohos.request模塊渐夸。@ohos.net.http模塊提供基礎(chǔ)的HTTP數(shù)據(jù)請(qǐng)求能力,功能較為基礎(chǔ)渔欢,本文不做介紹墓塌。@ohos.request模塊主要給應(yīng)用提供上傳下載文件、后臺(tái)傳輸代理的基礎(chǔ)能力。它具備任務(wù)管理系統(tǒng)的默認(rèn)并發(fā)功能苫幢,簡(jiǎn)化下載功能的實(shí)現(xiàn)和管理访诱,提升數(shù)據(jù)傳輸?shù)陌踩贤ㄖ獧C(jī)制韩肝,新增任務(wù)狀態(tài)與進(jìn)度查詢功能盐数,具有靈活性、高效性伞梯、可擴(kuò)展性、可靠性帚屉、一致性和安全性的優(yōu)勢(shì)谜诫。
具體來(lái)說(shuō),@ohos.request模塊包括以下功能:
任務(wù)管理:任務(wù)管理操作包括創(chuàng)建任務(wù)攻旦、暫停任務(wù)喻旷、恢復(fù)任務(wù)、刪除任務(wù)牢屋、文件上傳且预、文件下載、系統(tǒng)通知等烙无。創(chuàng)建的任務(wù)分為前端任務(wù)和后臺(tái)任務(wù)锋谐。前端任務(wù)是立即的、模態(tài)界面的截酷、同步的涮拗,跟隨應(yīng)用的生命周期,通常數(shù)據(jù)量較小迂苛、耗時(shí)短三热,例如發(fā)布微信朋友圈、微博三幻,通常優(yōu)先級(jí)高且傾斜帶寬資源就漾。后臺(tái)任務(wù)為可等待的、任意界面的念搬、異步的抑堡,通常數(shù)據(jù)量較大、耗時(shí)長(zhǎng)锁蠕,例如緩存一部電影夷野、同步數(shù)百兆字節(jié)乃至若干吉字節(jié)的數(shù)據(jù),優(yōu)先級(jí)相較于前端任務(wù)低且與應(yīng)用生命周期無(wú)關(guān)荣倾。
任務(wù)查詢管理:系統(tǒng)查詢所有任務(wù)悯搔、過(guò)濾上傳任務(wù)、過(guò)濾下載任務(wù)、過(guò)濾時(shí)間段內(nèi)任務(wù)妒貌、過(guò)濾前端任務(wù)通危、過(guò)濾后臺(tái)任務(wù)、用戶查詢指定任務(wù)信息灌曙、用戶查詢指定隱藏任務(wù)信息菊碟、系統(tǒng)查詢指定任務(wù)信息、系統(tǒng)清理指定任務(wù)等在刺。
任務(wù)自動(dòng)恢復(fù):網(wǎng)絡(luò)條件不滿足時(shí)任務(wù)不啟動(dòng)或者暫停逆害,滿足后自動(dòng)啟動(dòng)或者恢復(fù)(需要HTTP服務(wù)器支持?jǐn)帱c(diǎn)續(xù)傳)。
安全隱私保護(hù):包括網(wǎng)絡(luò)權(quán)限檢查蚣驼、普通接口僅操作自己創(chuàng)建的任務(wù)魄幕、任務(wù)信息加密存儲(chǔ)、系統(tǒng)接口檢查颖杏、系統(tǒng)接口查詢隱匿任務(wù)敏感字段纯陨、普通接口查詢隱匿任務(wù)敏感字段、遍歷攻擊留储、DOS翼抠、僵尸任務(wù)、惡意的靜默后臺(tái)任務(wù)获讳、系統(tǒng)管理接口權(quán)限等阴颖。
日志:包括調(diào)試模式和發(fā)布模式。調(diào)試模式可打印所有內(nèi)存修改丐膝、磁盤膘盖、網(wǎng)絡(luò)讀寫、邏輯分支等日志尤误。發(fā)布模式下除了導(dǎo)致任務(wù)失敗侠畔、服務(wù)異常的日志,其余日志都會(huì)關(guān)閉损晤。
任務(wù)失敗重試:對(duì)于不可恢復(fù)的原因软棺,直接失敗尤勋;對(duì)于可恢復(fù)的原因喘落,網(wǎng)絡(luò)斷開(kāi)、網(wǎng)絡(luò)類型不匹配等最冰,不現(xiàn)場(chǎng)重試瘦棋,任務(wù)到等待網(wǎng)絡(luò)恢復(fù)隊(duì)列;網(wǎng)絡(luò)超時(shí)則就地重試1次暖哨,仍網(wǎng)絡(luò)超時(shí)赌朋,則立即失敗。
服務(wù)按需啟停:上傳下載服務(wù)不隨系統(tǒng)自啟。應(yīng)用主動(dòng)調(diào)用任意接口沛慢,上傳下載服務(wù)自動(dòng)啟動(dòng)赡若。網(wǎng)絡(luò)連接事件會(huì)觸發(fā)上傳下載服務(wù)啟動(dòng)。在任務(wù)隊(duì)列中团甲,沒(méi)有正在處理的任務(wù)逾冬,或者等待網(wǎng)絡(luò)恢復(fù)的任務(wù),延遲10秒鐘躺苦,再check一次身腻,仍舊沒(méi)有的,則通知系統(tǒng)服務(wù)框架(SAMGR)可以停止并卸載上傳下載服務(wù)匹厘。在服務(wù)退出過(guò)程中霸株,新的接口請(qǐng)求可能失敗,在客戶端檢查服務(wù)狀態(tài)集乔、通過(guò)重試按需啟動(dòng)。
通知:任務(wù)從第一次開(kāi)始到最終結(jié)束都應(yīng)該有進(jìn)度通知坡椒。目前采用固定時(shí)間間隔觸發(fā)進(jìn)度通知扰路,前臺(tái)任務(wù)1秒,后臺(tái)任務(wù)3秒倔叼。任務(wù)狀態(tài)的每次變化也要觸發(fā)進(jìn)度通知汗唱。當(dāng)任務(wù)完成和失敗,則觸發(fā)其專用的進(jìn)度通知丈攒。提供了抑制開(kāi)關(guān)哩罪,可以在創(chuàng)建任務(wù)時(shí)打開(kāi),以避免頻繁通知巡验。
下載任務(wù)的狀態(tài)遷移流程
使用@ohos.request模塊執(zhí)行下載的任務(wù)际插,具有四種運(yùn)行狀態(tài):初始任務(wù)、就緒任務(wù)显设、掛起任務(wù)框弛、待網(wǎng)任務(wù)、成功任務(wù)捕捂、失敗任務(wù)瑟枫。可以通過(guò)create創(chuàng)建任務(wù)指攒,start開(kāi)始任務(wù)慷妙,pause掛起任務(wù),resume恢復(fù)任務(wù)允悦,remove移除任務(wù)膝擂,stop停止任務(wù),任務(wù)結(jié)果有final-failed任務(wù)失敗,final-completed下載完成猿挚,recoverable-failed重試失敗咐旧,并支持查詢?nèi)蝿?wù)狀態(tài),具體流程如圖一所示:
圖一 模塊流程圖
常見(jiàn)場(chǎng)景與方案
場(chǎng)景1:低帶寬網(wǎng)絡(luò)上傳瑣碎文件場(chǎng)景
在網(wǎng)絡(luò)連接較差绩蜻,低帶寬的網(wǎng)絡(luò)環(huán)境中铣墨,HTTP連接的建立耗時(shí)可能會(huì)大幅提升。這時(shí)候進(jìn)行數(shù)據(jù)壓縮可以加快頁(yè)面加載速度办绝,并減少HTTP請(qǐng)求數(shù)量和移動(dòng)數(shù)據(jù)流量伊约。
場(chǎng)景2:處理大量資源的場(chǎng)景
如應(yīng)用商店、網(wǎng)盤應(yīng)用等孕蝉,這類應(yīng)用通常擁有大體積的文件資源屡律。當(dāng)用戶從暫停或者斷網(wǎng)中重新恢復(fù)時(shí)降淮,如果從頭開(kāi)始上傳下載則會(huì)額外耗費(fèi)大量的時(shí)間超埋。此時(shí)可以采用斷點(diǎn)續(xù)傳方法進(jìn)行上傳下載。
數(shù)據(jù)壓縮
數(shù)據(jù)壓縮是指在應(yīng)用中對(duì)數(shù)據(jù)進(jìn)行壓縮佳鳖,以減少存儲(chǔ)空間和數(shù)據(jù)傳輸量霍殴、節(jié)省帶寬,提高加載速度系吩。數(shù)據(jù)壓縮通常在網(wǎng)絡(luò)傳輸和存儲(chǔ)方面發(fā)揮著重要作用来庭,特別是在處理大量數(shù)據(jù)或需要頻繁傳輸數(shù)據(jù)的場(chǎng)景下。
在應(yīng)用開(kāi)發(fā)中穿挨,常見(jiàn)的數(shù)據(jù)壓縮技術(shù)分類如下:
- 有損壓縮:僅限圖片視頻音頻等文件適用月弛。通過(guò)減少圖片視頻文件的分辨率,降低音頻的音質(zhì)等手段科盛,以減少文件的大小帽衙,來(lái)實(shí)現(xiàn)減少加載時(shí)間和帶寬消耗。
- 無(wú)損壓縮:對(duì)一些零碎文件可以使用 @ohos.zlib(Zip模塊)來(lái)進(jìn)行打包壓縮贞绵,減少上傳請(qǐng)求次數(shù)佛寿;對(duì)一些大文件可以利用緩存技術(shù),服務(wù)器將曾經(jīng)上傳過(guò)的大文件MD5碼緩存起來(lái)但壮,本地在上傳前預(yù)生成MD5碼并傳輸?shù)椒?wù)器進(jìn)行比對(duì)冀泻,如果相同則說(shuō)明服務(wù)器存在該文件,可以跳過(guò)該文件上傳蜡饵,從而省略重復(fù)傳輸時(shí)間弹渔。
以從相冊(cè)批量上傳圖片為例,介紹大量文件打包無(wú)損壓縮上傳相關(guān)技術(shù)溯祸,下圖為相關(guān)示例的界面截圖:
圖二 相冊(cè)批量上傳圖片示例圖
以批量上傳照片(分辨率為480*640肢专,24位舞肆,平均大小50~120KB)為例,在RK設(shè)備上測(cè)試的結(jié)果如下表所示:
上傳照片數(shù)量 | 優(yōu)化前耗時(shí)(ms) | 優(yōu)化后耗時(shí)(ms) |
---|---|---|
10 | 470 | 526 |
20 | 1124 | 1091 |
... | ... | ... |
50 | 2379 | 2138 |
80 | 3950 | 3258 |
... | ... | ... |
100 | 5276 | 3909 |
圖三 上傳數(shù)量和耗時(shí)對(duì)比圖表
由于上傳耗時(shí)收到網(wǎng)絡(luò)狀態(tài)影響偏差較大博杖,結(jié)果取的幾次測(cè)量結(jié)果的最小值椿胯。但是仍然可以從數(shù)據(jù)中看出,優(yōu)化前的耗時(shí)基本為線性增長(zhǎng)剃根,壓縮優(yōu)化后的耗時(shí)在上傳文件數(shù)量較低時(shí)并不明顯哩盲,還會(huì)因?yàn)槎嘤嗟膲嚎s處理影響耗時(shí)。不過(guò)隨著上傳的照片數(shù)量增多狈醉,優(yōu)化后的耗時(shí)和優(yōu)化之前的耗時(shí)差距越來(lái)越明顯廉油,優(yōu)化效果越好。
數(shù)據(jù)壓縮的相關(guān)示例代碼如下:
- 導(dǎo)入相關(guān)模塊:
import common from '@ohos.app.ability.common';
import fs from '@ohos.file.fs';
import zlib from '@ohos.zlib';
- 創(chuàng)建壓縮上傳相關(guān)類:
class ZipUpload {
// 創(chuàng)建任務(wù)前存放的uri
private waitList: Array<string> = [];
// 需要上傳的文件uri
private fileUris: Array<string> = [];
...
}
- 建立用于接收?qǐng)D庫(kù)圖片的臨時(shí)文件夾苗傅,并將整個(gè)臨時(shí)文件夾打包添加到待上傳list內(nèi):
// 文件壓縮處理
async zipUploadFiles(fileUris: Array<string>): Promise<void> {
this.context = getContext(this) as common.UIAbilityContext;
let cacheDir = this.context.cacheDir;
let tempDir = fs.mkdtempSync(`${cacheDir}/XXXXXX`);
// 將圖庫(kù)圖片獲取的uri放入fileUris中抒线,遍歷復(fù)制到臨時(shí)文件夾
for (let i = 0; i < fileUris.length; i++) {
let fileName = fileUris[i].split('/').pop();
let resourceFile: fs.File = fs.openSync(fileUris[i], fs.OpenMode.READ_ONLY);
fs.copyFileSync(resourceFile.fd, `${tempDir}/${fileName}`, 0);
fs.closeSync(resourceFile);
}
// 文件壓縮,將之前生成的臨時(shí)文件夾內(nèi)打包到test.zip內(nèi)
let options: zlib.Options = {
level: zlib.CompressLevel.COMPRESS_LEVEL_DEFAULT_COMPRESSION,
memLevel: zlib.MemLevel.MEM_LEVEL_DEFAULT,
strategy: zlib.CompressStrategy.COMPRESS_STRATEGY_DEFAULT_STRATEGY
};
let data = await zlib.compressFile(tempDir, `${cacheDir}/test.zip`, options);
// 刪除臨時(shí)文件夾
fs.rmdirSync(tempDir);
// 將生成的zip包放到傳輸隊(duì)列
this.waitList.push(`${cacheDir}/test.zip`);
}
斷點(diǎn)續(xù)傳
斷點(diǎn)續(xù)傳功能的實(shí)現(xiàn)渣慕,不管是應(yīng)用端還是服務(wù)器端都需要用到合理的技術(shù)來(lái)互相協(xié)同嘶炭。在實(shí)際開(kāi)發(fā)中,開(kāi)發(fā)者無(wú)需親自實(shí)現(xiàn)斷點(diǎn)續(xù)傳功能逊桦,只需對(duì)SDK進(jìn)行合理配置眨猎。
在應(yīng)用端需要用到的技術(shù)和API:
- @ohos.file.fs(文件管理):用于處理文件上傳操作,提供了讀取文件內(nèi)容卫袒,文件分片和組合的功能。
- @ohos.file.hash(文件哈希處理):用于實(shí)現(xiàn)文件MD5的計(jì)算单匣,將計(jì)算的MD5值預(yù)先傳到服務(wù)器端進(jìn)行預(yù)處理夕凝,實(shí)現(xiàn)文件秒傳,同時(shí)確保傳輸?shù)臏?zhǔn)確性和可靠性户秤。
- @ohos.request(上傳下載):用于實(shí)現(xiàn)文件上傳操作码秉,并支持在上傳過(guò)程中的斷點(diǎn)續(xù)傳功能。
在服務(wù)器端需要用到的技術(shù):
- 協(xié)議需要支持Range:用于在服務(wù)器端支持范圍請(qǐng)求鸡号,方便處理文件上傳下載斷點(diǎn)續(xù)傳功能转砖。
- 文件校驗(yàn)相關(guān)邏輯:需要實(shí)現(xiàn)校驗(yàn)文件是否有錯(cuò),確保在傳輸中斷后能夠準(zhǔn)確恢復(fù)并繼續(xù)傳輸鲸伴。
通過(guò)結(jié)合應(yīng)用端和服務(wù)器端的相關(guān)技術(shù)府蔗,可以共同實(shí)現(xiàn)高效且可靠的文件斷點(diǎn)續(xù)傳功能,提供更好的用戶體驗(yàn)并確保數(shù)據(jù)傳輸?shù)姆€(wěn)定性汞窗。
文件上傳
對(duì)于大文件斷點(diǎn)續(xù)傳上傳姓赤,本文采用@ohos.request(上傳下載)模塊中的request.agent任務(wù)托管接口,可以自動(dòng)實(shí)現(xiàn)暫停繼續(xù)重試等操作仲吏,無(wú)需手動(dòng)將文件分片和記錄上傳分片信息不铆。流程圖如圖四所示:
圖四 斷點(diǎn)續(xù)傳上傳流程圖
斷點(diǎn)續(xù)傳上傳示例代碼如下:
- 導(dǎo)入相關(guān)模塊:
import common from '@ohos.app.ability.common';
import request from '@ohos.request';
- 創(chuàng)建相關(guān)上傳類:
class Upload {
// 后臺(tái)任務(wù)
private backgroundTask: request.agent.Task | undefined = undefined;
// 創(chuàng)建任務(wù)前存放的uri
private waitList: Array<string> = [];
...
}
- 生成MD5碼蝌焚,上傳到服務(wù)器進(jìn)行校驗(yàn):
async checkFileExist(fileUri: string): Promise<boolean> {
let httpRequest = http.createHttp();
// 生成md5碼
let md5 = await hash.hash(fileUri, 'md5');
let requestOption: http.HttpRequestOptions = {
method: http.RequestMethod.POST,
extraData: {
'MD5': md5
}
}
let response = await httpRequest.request('http://XXX.XXX.XXX.XXX/XXXX', requestOption);
let result = response.result;
let flag = false;
... // 根據(jù)服務(wù)器返回對(duì)應(yīng)數(shù)據(jù)判斷是否存在
if (flag) {
return true;
} else {
return false;
}
}
- 配置Config,創(chuàng)建后臺(tái)上傳任務(wù):
private config: request.agent.Config = {
action: request.agent.Action.UPLOAD,
headers: HEADER,
url: '',
mode: request.agent.Mode.BACKGROUND,
method: 'POST',
title: 'upload',
network: request.agent.Network.ANY,
data: [],
token: 'UPLOAD_TOKEN'
}
...
// 轉(zhuǎn)換uri
private async getFilesAndData(cacheDir: string, fileUris: Array<string>): Promise<Array<request.agent.FormItem>> {
...
}
// 創(chuàng)建文件上傳后臺(tái)任務(wù)
async createBackgroundTask(fileUris: Array<string>) {
// 獲取上傳url
this.config.url = 'http://XXX.XXX.XXX.XXX';
this.config.mode = request.agent.Mode.BACKGROUND;
let tempData = await this.getFilesAndData(this.context.cacheDir, fileUris);
// 判斷每個(gè)文件是否為空
for (let i = 0; i < tempData.length; i++) {
let flag = await this.checkFileExist(`${this.context.cacheDir}/${tempData[i].name}`);
if (!flag) {
this.config.data.push(tempData[i])
}
}
let isFileExist = await this.checkFileExist(`${this.context.cacheDir}/${this.config.data[0].name}`);
if (this.config.data.length === 0) {
return;
}
this.backgroundTask = await request.agent.create(this.context, this.config);
}
- 任務(wù)開(kāi)始:
await this.backgroundTask.start();
- 任務(wù)暫停:
async pause() {
if (this.backgroundTask === undefined) {
return;
}
await this.backgroundTask.pause();
}
- 任務(wù)繼續(xù):
async resume() {
if (this.backgroundTask === undefined) {
return;
}
await this.backgroundTask.resume();
}
文件下載
對(duì)于大文件斷點(diǎn)續(xù)傳下載誓斥,也可以直接調(diào)用request.agent接口只洒,該接口的斷點(diǎn)續(xù)傳是基于HTTP協(xié)議Header里的Range字段實(shí)現(xiàn)的,在任務(wù)暫停重啟的時(shí)候劳坑,會(huì)自動(dòng)設(shè)置Header中的Range字段毕谴,無(wú)需進(jìn)行額外的配置。
Range簡(jiǎn)介
HTTP協(xié)議里面的Range字段泡垃,官方名稱為范圍請(qǐng)求析珊,它允許服務(wù)器只發(fā)送 HTTP
消息的一部分到客戶端,可以用來(lái)請(qǐng)求部分?jǐn)?shù)據(jù)而不是整個(gè)資源蔑穴。Range的格式通常是Range:
<unit>=<start>-<end>
忠寻,其中<unit>
表示范圍所采用的單位,通常是字節(jié)(bytes)存和,<start>
和<end>
表示請(qǐng)求的起始字節(jié)和結(jié)束字節(jié)的位置奕剃。Range語(yǔ)法如下:
// 表示從range-start到文件末尾 Range: <unit>=<range-start>- // 表示從range-start到range-end Range: <unit>=<range-start>-<range-end> // 可以同時(shí)選擇多段,用逗號(hào)分隔 Range: <unit>=<range-start>-<range-end>, <range-start>-<range-end> // 示例:表示返回1024 bytes之后的文件 Range: bytes=1024-
服務(wù)器收到請(qǐng)求后捐腿,正確處理請(qǐng)求會(huì)回復(fù)206 Partial
Content纵朋,未正常處理則會(huì)回復(fù)其他響應(yīng)碼。下表是服務(wù)器回復(fù)的常見(jiàn)響應(yīng)碼:
服務(wù)器響應(yīng)碼 常見(jiàn)的原因 206 Partial Content 服務(wù)器收到正常Range請(qǐng)求的響應(yīng)碼茄袖,返回部分內(nèi)容的響應(yīng)操软。 416 Range Not Satisfiable 所請(qǐng)求的范圍不合法,表示服務(wù)器錯(cuò)誤宪祥。 200 OK 服務(wù)器忽略了 Range 首部聂薪,返回整個(gè)文件。
斷點(diǎn)續(xù)傳下載示例代碼如下:
- 導(dǎo)入模塊:
import common from '@ohos.app.ability.common';
import request from '@ohos.request';
- 創(chuàng)建下載類:
class Download {
// 任務(wù)存放前的uri
private waitList: Array<string[]> = [];
// 下載任務(wù)
private downloadTask: request.agent.Task | undefined = undefined;
// 后臺(tái)任務(wù)下載列表
private backgroundDownloadTaskList: Array<request.agent.Task> = [];
...
}
- 配置Config蝗羊,創(chuàng)建后臺(tái)下載任務(wù):
async createBackgroundTask(downloadList: Array<string[]>) {
let splitUrl = url.split('//')[1].split('/');
let context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext;
let downloadConfig: request.agent.Config = {
action: request.agent.Action.DOWNLOAD,
url: url,
method: 'POST',
title: 'download',
mode: request.agent.Mode.FOREGROUND, // 必須是后臺(tái)任務(wù)才能續(xù)傳
network: request.agent.Network.ANY,
saveas: `./${folder}/${splitUrl[splitUrl.length-1]}`,
overwrite: true
}
this.downloadTask = await request.agent.create(context, downloadConfig);
if (this.backgroundDownloadTaskList.findIndex(task => task.config.url === downTask.config.url) === -1) {
this.backgroundDownloadTaskList.push(downTask);
}
}
- 任務(wù)開(kāi)始:
...
await downTask.start();
...
- 任務(wù)暫停:
async pause() {
if (this.backgroundDownloadTaskList.length === 0) {
return;
}
this.backgroundDownloadTaskList.forEach(async task => {
await task.pause();
})
}
- 任務(wù)繼續(xù):
async resume() {
if (this.backgroundDownloadTaskList.length === 0) {
return;
}
this.backgroundDownloadTaskList.forEach(async task => {
await task.resume();
})
}
- 任務(wù)停止:
async deleteAllBackTasks() {
if (this.backgroundDownloadTaskList.length > 0) {
this.backgroundDownloadTaskList.forEach(async task => {
await request.agent.remove(task.tid);
})
this.backgroundDownloadTaskList = [];
}
}
寫在最后
如果你覺(jué)得這篇內(nèi)容對(duì)你還蠻有幫助藏澳,我想邀請(qǐng)你幫我三個(gè)小忙:
- 點(diǎn)贊,轉(zhuǎn)發(fā)耀找,有你們的 『點(diǎn)贊和評(píng)論』翔悠,才是我創(chuàng)造的動(dòng)力。
- 關(guān)注小編野芒,同時(shí)可以期待后續(xù)文章ing??蓄愁,不定期分享原創(chuàng)知識(shí)。
- 想要獲取更多完整鴻蒙最新學(xué)習(xí)知識(shí)點(diǎn)狞悲,請(qǐng)移步前往小編:
https://gitee.com/MNxiaona/733GH/blob/master/jianshu