概述
本文參考業(yè)界標(biāo)準(zhǔn)力麸,并結(jié)合應(yīng)用TS&JS部分的性能優(yōu)化實(shí)踐經(jīng)驗(yàn),從應(yīng)用編程指南拣挪、高性能編程實(shí)踐、性能優(yōu)化調(diào)試工具等維度俱诸,為應(yīng)用開發(fā)者提供參考指導(dǎo)菠劝,助力開發(fā)者開發(fā)出高性能的應(yīng)用。
應(yīng)用TS&JS高性能編程實(shí)踐
高性能編程實(shí)踐乙埃,是在開發(fā)過程中逐步總結(jié)出來的一些高性能的寫法和建議闸英,在業(yè)務(wù)功能實(shí)現(xiàn)過程中,我們要同步思考并理解高性能寫法的原理介袜,運(yùn)用到代碼邏輯實(shí)現(xiàn)中甫何。
本文中的實(shí)踐示例代碼,會(huì)統(tǒng)一標(biāo)注正例或者反例遇伞,正例為推薦寫法辙喂,反例為不推薦寫法。
屬性訪問與屬性增刪
熱點(diǎn)循環(huán)中常量提取鸠珠,減少屬性訪問次數(shù)
在實(shí)際的應(yīng)用場(chǎng)景中抽離出來如下用例巍耗,其在循環(huán)中會(huì)大量進(jìn)行一些常量的訪問操作,該常量在循環(huán)中不會(huì)改變渐排,可以提取到循環(huán)外部炬太,減少屬性訪問的次數(shù)。
【反例】
// 優(yōu)化前代碼
private getDay(year: number): number {
/* Year has (12 * 29 =) 348 days at least */
let totalDays: number = 348;
for (let index: number = 0x8000; index > 0x8; index >>= 1) {
// 此處會(huì)多次對(duì)Time的INFO及START進(jìn)行查找驯耻,并且每次查找出來的值是相同的
totalDays += ((Time.INFO[year- Time.START] & index) !== 0) ? 1 : 0;
}
return totalDays + this.getDays(year);
}
可以將Time.INFO[year - Time.START]
進(jìn)行熱點(diǎn)函數(shù)常量提取操作亲族,這樣可以大幅減少屬性的訪問次數(shù),性能收益明顯可缚。
【正例】
// 優(yōu)化后代碼
private getDay(year: number): number {
/* Year has (12 * 29 =) 348 days at least */
let totalDays: number = 348;
const info = Time.INFO[year - Time.START]; // 1. 從循環(huán)中提取不變量
for (let index: number = 0x8000; index > 0x8; index >>= 1) {
if ((info & index) !== 0) {
totalDays++;
}
}
return totalDays + this.getDays(year);
}
避免頻繁使用delete
delete對(duì)象的某一個(gè)屬性會(huì)改變其布局霎迫,影響運(yùn)行時(shí)優(yōu)化效果,導(dǎo)致執(zhí)行性能下降帘靡。
說明:
不建議直接使用delete刪除對(duì)象的任何屬性知给,如果有需要,建議使用map和set或者引擎實(shí)現(xiàn)的高性能容器類。
【反例】
class O1 {
x: string | undefined = "";
y: string | undefined = "";
}
let obj: O1 = {x: "", y: ""};
obj.x = "xxx";
obj.y = "yyy";
delete obj.x;
建議使用如下兩種寫法之一實(shí)現(xiàn)屬性的增刪涩赢。
【正例】
// 例1:將Object中不再使用的屬性設(shè)置為null
class O1 {
x: string | null = "";
y: string | null = "";
}
let obj: O1 = {x: "", y: ""};
obj.x = "xxx";
obj.y = "yyy";
obj.x = null;
// 例2:使用高性能容器類操作屬性
import HashMap from '@ohos.util.HashMap';
let myMap= new HashMap();
myMap.set("x", "xxx");
myMap.set("y", "yyy");
myMap.remove("x");
數(shù)值計(jì)算
數(shù)值計(jì)算避免溢出
常見的可能導(dǎo)致溢出的數(shù)值計(jì)算包括如下場(chǎng)景戈次,溢出之后,會(huì)導(dǎo)致引擎走入慢速的溢出邏輯分支處理筒扒,影響后續(xù)的性能朝扼。
針對(duì)加法、減法霎肯、乘法、指數(shù)運(yùn)算等運(yùn)算操作榛斯,應(yīng)避免數(shù)值大于INT32_MAX或小于INT32_MIN观游,否則會(huì)導(dǎo)致int溢出。
針對(duì)&(and)驮俗、>>>(無符號(hào)右移)等運(yùn)算操作懂缕,應(yīng)避免數(shù)值大于INT32_MAX,否則會(huì)導(dǎo)致int溢出王凑。
數(shù)據(jù)結(jié)構(gòu)
使用合適的數(shù)據(jù)結(jié)構(gòu)
在實(shí)際的應(yīng)用場(chǎng)景中抽離出來如下用例搪柑,該接口中使用JS Object來作為容器去處理Map的邏輯,建議使用HashMap來進(jìn)行處理索烹。
【反例】
getInfo(t1, t2) {
if (!this.check(t1, t2)) {
return "";
}
// 此處使用JS Object作為容器
let info= {};
this.setInfo(info);
let t1= info[t2];
return (t1!= null) ? t1: "";
}
setInfo(info) {
// 接口內(nèi)部實(shí)際上進(jìn)行的是map的操作
info[T1] = '七六';
info[T2] = '九一';
... ...
info[T3] = '十二';
}
代碼可以進(jìn)行如下修改工碾,除了使用引擎中提供的標(biāo)準(zhǔn)內(nèi)置map之外,還可以使用ArkTS提供的高性能容器類百姓。
【正例】
import HashMap from '@ohos.util.HashMap';
getInfo(t1, t2) {
if (!this.check(t1, t2)) {
return "";
}
// 此處替換為HashMap作為容器
let info= new HashMap();
this.setInfo(info);
let t1= info.get(t2);
return (t1!= null) ? t1: "";
}
setInfo(info) {
// 接口內(nèi)部實(shí)際上進(jìn)行的是map的操作
info.set(T1, '七六');
info.set(T2, '九一');
... ...
info.set(T3, '十二');
}
數(shù)值數(shù)組推薦使用TypedArray
如果是涉及純數(shù)值計(jì)算的場(chǎng)合渊额,推薦使用TypedArray數(shù)據(jù)結(jié)構(gòu)。
常見的TypedArray包括:Int8Array垒拢、Uint8Array旬迹、Uint8ClampedArray、Int16Array求类、Uint16Array奔垦、Int32Array、Uint32Array尸疆、Float32Array椿猎、Float64Array、BigInt64Array仓技、BigUint64Array鸵贬。
【正例】
const typedArray1 = new Int8Array([1, 2, 3]); // 針對(duì)這一場(chǎng)景,建議不要使用new Array([1, 2, 3])
const typedArray2 = new Int8Array([4, 5, 6]); // 針對(duì)這一場(chǎng)景脖捻,建議不要使用new Array([4, 5, 6])
let res = new Int8Array(3);
for (let i = 0; i < 3; i++) {
res[i] = typedArray1[i] + typedArray2[i];
}
避免使用稀疏數(shù)組
分配數(shù)組時(shí)阔逼,應(yīng)避免其大小超過1024或形成稀疏數(shù)組。
虛擬機(jī)在分配超過1024大小的數(shù)組或者針對(duì)稀疏數(shù)組地沮,均采用hash表來存儲(chǔ)元素嗜浮,相對(duì)使用偏移來訪問數(shù)組元素速度較慢羡亩。
在開發(fā)時(shí),盡量避免數(shù)組變成稀疏數(shù)組危融。
【反例】
// 如下幾種情形會(huì)變成稀疏數(shù)組
// 1. 直接分配100000大小的數(shù)組畏铆,虛擬機(jī)會(huì)處理成用hash表來存儲(chǔ)元素
let count = 100000;
let result: number[] = new Array(count);
// 2. 分配數(shù)組之后直接,在9999處初始化吉殃,會(huì)變成稀疏數(shù)組
let result: number[] = new Array();
result[9999] = 0;
// 3. 刪除數(shù)組的element屬性辞居,虛擬機(jī)也會(huì)處理成用hash表來存儲(chǔ)元素
let result = [0, 1, 2, 3, 4];
delete result[0];
對(duì)象初始化
使用字面量進(jìn)行對(duì)象創(chuàng)建
通常在代碼中,進(jìn)行一些對(duì)象創(chuàng)建的時(shí)候蛋勺,大家會(huì)采用動(dòng)態(tài)添加屬性方式瓦灶,這種方式,在前端解析時(shí)抱完,不能獲取到更多的信息贼陶,因此不能為運(yùn)行時(shí)提供優(yōu)化信息。
【反例】
let arr = new Array(); // 創(chuàng)建一個(gè)array
let obj = new Object(); // 創(chuàng)建一個(gè)普通對(duì)象
let oFruit = new Object();
oFruit.color = "red";
oFruit.name = "apple"; // 創(chuàng)建一個(gè)對(duì)象巧娱,并設(shè)置屬性
在要求性能的場(chǎng)合碉怔,可以使用字面量進(jìn)行對(duì)象創(chuàng)建,這樣在運(yùn)行時(shí)可以獲得指令級(jí)別的優(yōu)化禁添。
【正例】
let arr = []; // 創(chuàng)建一個(gè)array
let obj = {}; // 創(chuàng)建一個(gè)普通對(duì)象
class O1 {
color: string = "";
name: string = "";
}
let oFruit: O1 = {color: "red", name: "apple"}; // 創(chuàng)建一個(gè)對(duì)象撮胧,并設(shè)置屬性
對(duì)象構(gòu)造初始化
對(duì)象構(gòu)造的時(shí)候,要提供默認(rèn)值初始化老翘,不要訪問未初始化的屬性趴樱。
【反例】
// 不要訪問未初始化的屬性
class A {
x: number;
}
// 構(gòu)造函數(shù)中要對(duì)屬性進(jìn)行初始化
class A {
x: number;
constructor() {
}
}
let a = new A();
// x使用時(shí)還未賦值,這種情況會(huì)訪問整個(gè)原型鏈
print(a.x);
【正例】
// 推薦一:聲明初始化
class A {
x: number = 0;
}
// 推薦二:構(gòu)造函數(shù)直接賦初值
class A {
constructor() {
this.x = 0;
}
}
let a = new A();
print(a.x);
number正確初始化
針對(duì)number類型酪捡,編譯器在優(yōu)化時(shí)會(huì)區(qū)分整型和浮點(diǎn)類型叁征。開發(fā)者在初始化時(shí)如果預(yù)期是整型就初始化成0,如果預(yù)期是浮點(diǎn)型就初始化為0.0逛薇,不要把一個(gè)number類型初始化成undefined或者null捺疼。
【正例】
function foo(d: number) : number {
// 變量i預(yù)期是整型,不要聲明成undefined/null或0.0永罚,直接初始化為0
let i: number = 0;
i += d;
return i;
}
避免動(dòng)態(tài)添加屬性
對(duì)象在創(chuàng)建的時(shí)候啤呼,如果開發(fā)者明確后續(xù)還需要添加屬性,可以提前置為undefined呢袱。動(dòng)態(tài)添加屬性會(huì)導(dǎo)致對(duì)象布局變化官扣,影響編譯器和運(yùn)行時(shí)優(yōu)化效果。
【反例】
// 后續(xù)obj需要再添加z屬性
class O1 {
x: string = "";
y: string = "";
}
let obj: O1 = {"x": xxx, "y": "yyy"};
...
// 這種動(dòng)態(tài)添加方式是不推薦的
obj.z = "zzz";
【正例】
class O1 {
x: string = "";
y: string = "";
z: string = "";
}
let obj: O1 = {"x": "xxx", "y": "yyy", "z": ""};
...
obj.z = "zzz";
調(diào)用構(gòu)造函數(shù)的入?yún)⒁c標(biāo)注類型匹配
由于TS語言類型系統(tǒng)是一種標(biāo)注類型羞福,不是編譯期強(qiáng)制約束惕蹄,如果入?yún)⒌膶?shí)際類型與標(biāo)注類型不匹配,會(huì)影響引擎內(nèi)部的優(yōu)化效果。
【反例】
class A {
private a: number | undefined;
private b: number | undefined;
private c: number | undefined;
constructor(a?: number, b?: number, c?: number) {
this.a = a;
this.b = b;
this.c = c;
}
}
// new的過程中沒有傳入?yún)?shù)卖陵,a遭顶,b,c會(huì)獲取一個(gè)undefined的初值泪蔫,和標(biāo)注類型不符
let a = new A();
針對(duì)上文的示例場(chǎng)景棒旗,開發(fā)者大概率預(yù)期該入?yún)㈩愋褪莕umber類型,需要顯式寫出來撩荣。
參照正例進(jìn)行如下修改铣揉,不然會(huì)造成標(biāo)注的入?yún)⑹莕umber,實(shí)際傳入的是undefined餐曹。
【正例】
class A {
private a: number | undefined;
private b: number | undefined;
private c: number | undefined;
constructor(a?: number, b?: number, c?: number) {
this.a = a;
this.b = b;
this.c = c;
}
}
// 初始化直接傳入默認(rèn)值0
let a = new A(0, 0, 0);
不變的變量聲明為const
不變的變量推薦使用const進(jìn)行初始化老速。
【反例】
// 該變量在后續(xù)過程中并未發(fā)生更改,建議聲明為常量
let N = 10000;
function getN() {
return N;
}
【正例】
const N = 10000;
function getN() {
return N;
}
接口及繼承
避免使用type類型標(biāo)注
如果傳入的參數(shù)類型是type類型凸主,實(shí)際入?yún)⒖赡苁且粋€(gè)object literal,也可能是一個(gè)class额湘,編譯器及虛擬機(jī)因?yàn)轭愋筒还潭ㄇ渫拢瑹o法做編譯期假設(shè)進(jìn)而進(jìn)行相應(yīng)的優(yōu)化。
【反例】
// type類型無法在編譯期確認(rèn), 可能是一個(gè)object literal锋华,也可能是另一個(gè)class Person
type Person = {
name: string;
age: number;
};
function greet(person: Person) {
return "Hello " + person.name;
}
// type方式是不推薦的嗡官,因?yàn)槠溆腥缦聝煞N使用方式,type類型無法在編譯期確認(rèn)
// 調(diào)用方式一
class O1 {
name: string = "";
age: number = 0;
}
let objectliteral: O1 = {name : "zhangsan", age: 20 };
greet(objectliteral);
// 調(diào)用方式二
class Person {
name: string = "zhangsan";
age: number = 20;
}
let person = new Person();
greet(person);
【正例】
interface Person {
name: string ;
age: number;
}
function greet(person: Person) {
return "Hello " + person.name;
}
class Person {
name: string = "zhangsan";
age: number = 20;
}
let person = new Person();
greet(person);
函數(shù)調(diào)用
聲明參數(shù)要和實(shí)際的參數(shù)一致
聲明的參數(shù)要和實(shí)際的傳入?yún)?shù)個(gè)數(shù)及類型一致毯焕,如果不傳入?yún)?shù)衍腥,則會(huì)作為undefined處理,可能造成與實(shí)際入?yún)㈩愋筒黄ヅ涞那闆r纳猫,從而導(dǎo)致運(yùn)行時(shí)走入慢速路徑婆咸,影響性能。
【反例】
function add(a: number, b: number) {
return a + b;
}
// 參數(shù)個(gè)數(shù)是2芜辕,不能給3個(gè)
add(1, 2, 3);
// 參數(shù)個(gè)數(shù)是2尚骄,不能給1個(gè)
add(1);
// 參數(shù)類型是number,不能給string
add("hello", "world");
【正例】
function add(a: number, b: number) {
return a + b;
}
// 按照函數(shù)參數(shù)個(gè)數(shù)及類型要求傳入?yún)?shù)
add(1, 2);
函數(shù)內(nèi)部變量盡量使用參數(shù)傳遞
能傳遞參數(shù)的盡量傳遞參數(shù)侵续,不要使用閉包倔丈。閉包作為參數(shù)會(huì)多一次閉包創(chuàng)建和訪問。
【反例】
let arr = [0, 1, 2];
function foo() {
// arr 盡量通過參數(shù)傳遞
return arr[0] + arr[1];
}
foo();
【正例】
let arr = [0, 1, 2];
function foo(array: Array) : number {
// arr 盡量通過參數(shù)傳遞
return array[0] + array[1];
}
foo(arr);
函數(shù)與類聲明
避免動(dòng)態(tài)聲明function與class
不建議動(dòng)態(tài)聲明function和class状蜗。
以如下用例為例需五,動(dòng)態(tài)聲明了class Add和class Sub,每次調(diào)用foo
都會(huì)重新創(chuàng)建class Add和class Sub轧坎,對(duì)內(nèi)存和性能都會(huì)有影響宏邮。
【反例】
function foo(f: boolean) {
if (f) {
return class Add{};
} else {
return class Sub{};
}
}
【正例】
class Add{};
class Sub{};
function foo(f: boolean) {
if (f) {
return Add;
} else {
return Sub;
}
}
TS&JS性能優(yōu)化工具使用
通過如下工具和使用方法,能夠幫助開發(fā)者查看待分析場(chǎng)景下各階段的耗時(shí)分布情況,并進(jìn)一步針對(duì)耗時(shí)情況使用對(duì)應(yīng)的工具做細(xì)化分析蜀铲。
工具使用介紹:
- 針對(duì)應(yīng)用開發(fā)者边琉,推薦使用自帶的Smartperf工具來進(jìn)行輔助分析,可以從宏觀角度查看應(yīng)用各個(gè)階段耗時(shí)分布情況记劝,快速找到待分析優(yōu)化模塊变姨。
- 針對(duì)第一步分析得到的待優(yōu)化模塊,需要進(jìn)行進(jìn)一步分析確認(rèn)耗時(shí)點(diǎn)是在TS&JS部分還是C++部分厌丑。C++部分耗時(shí)模塊細(xì)化分析建議使用hiperf工具定欧;針對(duì)TS&JS部分耗時(shí),可以使用CPU Profiler工具怒竿。
- 針對(duì)虛擬機(jī)開發(fā)者砍鸠,如果需要進(jìn)一步拆分細(xì)化,推薦使用虛擬機(jī)提供的RUNTIME_STAT工具耕驰。
Smartperf工具使用指導(dǎo)
以如下某個(gè)應(yīng)用場(chǎng)景使用過程的trace為例爷辱,可以通過Smartperf工具抓取到應(yīng)用使用階段的耗時(shí)信息,其中大部分為GC(Garbage Collection朦肘,垃圾回收)等操作饭弓。如果此接口大部分是應(yīng)用開發(fā)者通過TS&JS實(shí)現(xiàn),并且在trace中體現(xiàn)此階段比較耗時(shí)媒抠,則可以繼續(xù)使用CPU Profiler工具來進(jìn)一步分析TS&JS部分耗時(shí)情況弟断。
除了可以查看系統(tǒng)的trace之外,還可以在應(yīng)用的源碼的關(guān)鍵流程中加入一些trace點(diǎn)趴生,用于做性能分析阀趴。startTrace用于記錄trace起點(diǎn),finishTrace用于記錄trace終點(diǎn)苍匆,在應(yīng)用中增加trace點(diǎn)的方式如下:
import hiTraceMeter from '@ohos.hiTraceMeter';
... ...
hiTraceMeter.startTrace("fillText1", 100);
... ...
hiTraceMeter.finishTrace("fillText1", 100);
在應(yīng)用層或Native層增加trace點(diǎn)刘急,具體可見 性能打點(diǎn)跟蹤開發(fā)指導(dǎo)。
hiperf工具使用指導(dǎo)
集成在Smartperf的hiperf工具使用指導(dǎo)浸踩,具體可見 HiPerf的抓取和展示說明排霉。
hiperf工具的單獨(dú)使用指導(dǎo),具體可見 hiperf應(yīng)用性能優(yōu)化工具民轴。
TS&JS及NAPI層面耗時(shí)分析工具
TS&JS層面耗時(shí)主要分為如下幾種情況:
Ability的生命周期回調(diào)的耗時(shí)攻柠。
組件的TS&JS業(yè)務(wù)代碼的回調(diào)的耗時(shí)。
應(yīng)用TS&JS邏輯代碼耗時(shí)后裸。
NAPI層面的耗時(shí)主要分為如下幾種情況:
TS&JS業(yè)務(wù)代碼通過調(diào)用JS API產(chǎn)生的耗時(shí)瑰钮。
TS&JS業(yè)務(wù)代碼調(diào)用開發(fā)者通過NAPI封裝的C/C++實(shí)現(xiàn)時(shí)產(chǎn)生的耗時(shí)。
針對(duì)應(yīng)用中的TS&JS及NAPI兩種業(yè)務(wù)場(chǎng)景的耗時(shí)分析微驶,我們提供了 CPU Profiler工具 浪谴,用來識(shí)別熱點(diǎn)函數(shù)及耗時(shí)代碼开睡。
其支持的采集方式如下:
DevEco Studio連接設(shè)備實(shí)時(shí)采集;
hdc shell連接設(shè)備進(jìn)行命令行采集苟耻。
可以通過CPU Profiler工具篇恒,對(duì)TS&JS中執(zhí)行的熱點(diǎn)函數(shù)進(jìn)行抓取。以應(yīng)用實(shí)際使用場(chǎng)景為例凶杖,在此場(chǎng)景中胁艰,可以抓到應(yīng)用中的某一熱點(diǎn)函數(shù),在此基礎(chǔ)上智蝠,針對(duì)該接口做進(jìn)一步分析腾么。
寫在最后
如果你覺得這篇內(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