title: React Native 學(xué)習(xí)筆記--進階(五)--性能央渣、升級版本芽丹、特定平臺代碼
tags: React Native
categories: React Native
description:
React Native 進階(五)--性能枉氮、升級版本聊替、特定平臺代碼
性能
使用React Native替代基于WebView的框架來開發(fā)App的一個強有力的理由培廓,就是為了使App可以達到每秒60幀(足夠流暢)肩钠,并且能有類似原生App的外觀和手感价匠。但是,還是有一些地方有所欠缺坡氯,以及在某些場合React Native還不能夠替你決定如何進行優(yōu)化洋腮,因此人工的干預(yù)依然是必要的啥供。
關(guān)于“幀”你所需要知道的
視頻中逼真的動態(tài)效果其實是一種幻覺伙狐,這種幻覺是由一組靜態(tài)的圖片以一個穩(wěn)定的速度快速變化所產(chǎn)生的。我們把這組圖片中的每一張圖片叫做一幀窒百,而每秒鐘顯示的幀數(shù)直接的影響了視頻(或者說用戶界面)的流暢度和真實感篙梢。iOS設(shè)備提供了每秒60的幀率美旧,這就留給了開發(fā)者和UI系統(tǒng)大約16.67ms來完成生成一張靜態(tài)圖片(幀)所需要的所有工作。如果在這分派的16.67ms之內(nèi)沒有能夠完成這些工作陶舞,就會引發(fā)‘丟幀’的后果绪励,使界面表現(xiàn)的不夠流暢疏魏。
調(diào)出你應(yīng)用的開發(fā)菜單,打開Show FPS Monitor. 你會注意到有兩個不同的幀率(JS和UI):
JavaScript 幀率
對大多數(shù)React Native應(yīng)用來說蛉腌,業(yè)務(wù)邏輯是運行在JavaScript線程上的烙丛。這是React應(yīng)用所在的線程羔味,也是發(fā)生API調(diào)用赋元,以及處理觸摸事件等操作的線程们陆。更新數(shù)據(jù)到原生支持的視圖是批量進行的,并且在事件循環(huán)每進行一次的時候被發(fā)送到原生端杂腰,這一步通常會在一幀時間結(jié)束之前處理完(一切順利的話)喂很。如果JavaScript線程有一幀沒有及時響應(yīng)皆刺,就被認為發(fā)生了一次丟幀羡蛾。 例如:你在一個復(fù)雜應(yīng)用的根組件上調(diào)用了this.setState,從而導(dǎo)致一次開銷很大的子組件樹的重繪器予,可想而知捐迫,這可能會花費200ms也就是整整12幀的丟失施戴。此時,任何由JavaScript控制的動畫都會卡住雷则。只要卡頓超過100ms巧婶,用戶就會明顯的感覺到。
這種情況經(jīng)常發(fā)生在Navigator的切換過程中:當你push一個新的路由時英岭,JavaScript需要繪制新場景所需的所有組件诅妹,以發(fā)送正確的命令給原生端去創(chuàng)建視圖吭狡。由于切換是由JavaScript線程所控制划煮,因此經(jīng)常會占用若干幀的時間,引起一些卡頓器躏。有的時候蟹略,組件會在componentDidMount函數(shù)中做一些額外的事情挖炬,這甚至可能會導(dǎo)致頁面切換過程中多達一秒的卡頓。
另一個例子是觸摸事件的響應(yīng):如果你正在JavaScript線程處理一個跨越多個幀的工作太抓,你可能會注意到TouchableOpacity的響應(yīng)被延遲了令杈。這是因為JavaScript線程太忙了逗噩,不能夠處理主線程發(fā)送過來的原始觸摸事件异雁。結(jié)果TouchableOpacity就不能及時響應(yīng)這些事件并命令主線程的頁面去調(diào)整透明度了。
主線程 (也即UI線程) 幀率
很多人會注意到纲刀,NavigatorIOS的性能要比Navigator好的多项炼。原因就是它的切換動畫是完全在主線程上執(zhí)行的,因此不會被JavaScript線程上的掉幀所影響示绊。
同樣锭部,當JavaScript線程卡住的時候,你仍然可以歡快的上下滾動ScrollView面褐,因為ScrollView運行在主線程之上(盡管滾動事件會被分發(fā)到JS線程拌禾,但是接收這些事件對于滾動這個動作來說并不必要)。
性能問題的常見原因
console.log語句
在運行打好了離線包的應(yīng)用時展哭,控制臺打印語句可能會極大地拖累JavaScript線程。注意有些第三方調(diào)試庫也可能包含控制臺打印語句匪傍,比如redux-logger您市,所以在發(fā)布應(yīng)用前請務(wù)必仔細檢查,確保全部移除役衡。
有個babel插件可以幫你移除所有的console.*調(diào)用茵休。首先需要使用npm install babel-plugin-transform-remove-console --save來安裝,然后在項目根目錄下編輯(或者是新建)一個名為·.babelrc`的文件映挂,在其中加入:
{
"env": {
"production": {
"plugins": ["transform-remove-console"]
}
}
}
這樣在打包發(fā)布時泽篮,所有的控制臺語句就會被自動移除,而在調(diào)試時它們?nèi)匀粫徽U{(diào)用柑船。
開發(fā)模式 (dev=true)
JavaScript線程的性能在開發(fā)模式下是很糟糕的帽撑。這是不可避免的,因為有許多工作需要在運行的時候去做鞍时,譬如使你獲得良好的警告和錯誤信息亏拉,又比如驗證屬性類型(propTypes)以及產(chǎn)生各種其他的警告扣蜻。
緩慢的導(dǎo)航器(Navigator)切換
Navigator的動畫是由JavaScript線程所控制的。想象一下“從右邊推入”這個場景的切換:每一幀中及塘,新的場景從右向左移動莽使,從屏幕右邊緣開始,最終移動到x軸偏移為0的屏幕位置笙僚。切換過程中的每一幀芳肌,JavaScript線程都需要發(fā)送一個新的x軸偏移量給主線程。如果JavaScript線程卡住了肋层,它就無法處理這項事情亿笤,因而這一幀就無法更新,動畫就被卡住了栋猖。
長遠的解決方法净薛,其中一部分是要允許基于JavaScript的動畫從主線程分離。同樣是上面的例子蒲拉,我們可以在切換動畫開始的時候計算出一個列表肃拜,其中包含所有的新的場景需要的x軸偏移量,然后一次發(fā)送到主線程以某種優(yōu)化的方式執(zhí)行雌团。由于JavaScript線程已經(jīng)從更新x軸偏移量給主線程這個職責(zé)中解脫了出來燃领,因此JavaScript線程中的掉幀就不是什么大問題了 —— 用戶將基本上不會意識到這個問題,因為用戶的注意力會被流暢的切換動作所吸引辱姨。
不幸的是柿菩,這個方案還沒有被實現(xiàn)戚嗅。所以當前的解決方案是雨涛,在動畫的進行過程中,利用InteractionManager來選擇性的渲染新場景所需的最小限度的內(nèi)容懦胞。
InteractionManager.runAfterInteractions的參數(shù)中包含一個回調(diào)替久,這個回調(diào)會在navigator切換動畫結(jié)束的時候被觸發(fā)(每個來自于Animated接口的動畫都會通知InteractionManager)。
你的場景組件看上去應(yīng)該是這樣的:
class ExpensiveScene extends React.Component {
constructor(props, context) {
super(props, context);
this.state = {renderPlaceholderOnly: true};
}
componentDidMount() {
InteractionManager.runAfterInteractions(() => {
this.setState({renderPlaceholderOnly: false});
});
}
render() {
if (this.state.renderPlaceholderOnly) {
return this._renderPlaceholderView();
}
return (
<View>
<Text>Your full view goes here</Text>
</View>
);
}
_renderPlaceholderView() {
return (
<View>
<Text>Loading...</Text>
</View>
);
}
};
你不必被限制在僅僅是做一些loading指示的渲染躏尉,你也可以繪制部分的頁面內(nèi)容 —— 例如:當你加載Facebook應(yīng)用的時候蚯根,你會看見一個灰色方形的消息流的占位符,是將來用來顯示文字的地方胀糜。如果你正在場景中繪制地圖颅拦,那么最好在場景切換完成之前,顯示一個灰色的占位頁面或者是一個轉(zhuǎn)動的動畫教藻,因為切換過程的確會導(dǎo)致主線程的掉幀距帅。
ListView初始化渲染太慢以及列表過長時滾動性能太差
這是一個頻繁出現(xiàn)的問題。因為iOS配備了UITableView括堤,通過重用底層的UIViews實現(xiàn)了非常高性能的體驗碌秸。用React Native實現(xiàn)相同效果的工作仍正在進行中绍移,但是在此之前,我們有一些可用的方法來稍加改進性能以滿足我們的需求讥电。
initialListSize
這個屬性定義了在首次渲染中繪制的行數(shù)蹂窖。如果我們關(guān)注于快速的顯示出頁面,可以設(shè)置initialListSize為1恩敌,然后我們會發(fā)現(xiàn)其他行在接下來的幀中被快速繪制到屏幕上瞬测。而每幀所顯示的行數(shù)由pageSize所決定。
pageSize
在初始渲染也就是initialListSize被使用之后纠炮,ListView將利用pageSize來決定每一幀所渲染的行數(shù)涣楷。默認值為1 —— 但是如果你的頁面很小,而且渲染的開銷不大的話抗碰,你會希望這個值更大一些狮斗。稍加調(diào)整,你會發(fā)現(xiàn)它所起到的作用弧蝇。
scrollRenderAheadDistance
“在將要進入屏幕某些區(qū)域中先渲染行碳褒,距離按像素計算”
如果我們有一個2000個元素的列表,并且立刻全部渲染出來的話看疗,無論是內(nèi)存還是計算資源都會顯得很匱乏沙峻。還很可能導(dǎo)致非常可怕的阻塞两芳。因此scrollRenderAheadDistance允許我們來指定一個超過視野范圍之外所需要渲染的行數(shù)摔寨。
removeClippedSubviews
“當這一選項設(shè)置為true的時候,超出屏幕的子視圖(同時overflow值為hidden)會從它們原生的父視圖中移除怖辆。這個屬性可以在列表很長的時候提高滾動的性能是复。默認為true。(0.14版本前默認為false)”
這是一個應(yīng)用在長列表上極其重要的優(yōu)化竖螃。Android上淑廊,overflow值總是hidden的,所以你不必擔(dān)心沒有設(shè)置它特咆。而在iOS上季惩,你需要確保在行容器上設(shè)置了overflow: hidden。
我的組件渲染太慢腻格,我不需要立即顯示全部
這在初次瀏覽ListView時很常見画拾,適當?shù)氖褂盟谦@得穩(wěn)定性能的關(guān)鍵。就像之前所提到的菜职,它可以提供一些手段在不同幀中來分開渲染頁面青抛,稍加改進就可以滿足你的需求。此外要記住的是些楣,ListView也可以橫向滾動脂凶。
在重繪一個幾乎沒有什么變化的頁面時宪睹,JS幀率嚴重降低
如果你正在使用一個ListView,你必須提供一個rowHasChanged函數(shù)蚕钦,它通過快速的算出某一行是否需要重繪亭病,來減少很多不必要的工作。如果你使用了不可變的數(shù)據(jù)結(jié)構(gòu)嘶居,這項工作就只需檢查其引用是否相等罪帖。
同樣的,你可以實現(xiàn)shouldComponentUpdate函數(shù)來指明在什么樣的確切條件下邮屁,你希望這個組件得到重繪整袁。如果你編寫的是純粹的組件(返回值完全由props和state所決定),你可以利用PureRenderMixin來為你做這個工作佑吝。再強調(diào)一次坐昙,不可變的數(shù)據(jù)結(jié)構(gòu)在提速方面非常有用 —— 當你不得不對一個長列表對象做一個深度的比較,它會使重繪你的整個組件更加快速芋忿,而且代碼量更少炸客。
由于在JavaScript線程中同時做很多事情,導(dǎo)致JS線程掉幀
“導(dǎo)航切換極慢”是該問題的常見表現(xiàn)戈钢。在其他情形下痹仙,這種問題也可能會出現(xiàn)。使用InteractionManager是一個好的方法殉了,但是如果在動畫中开仰,為了用戶體驗的開銷而延遲其他工作并不太能接受,那么你可以考慮一下使用LayoutAnimation薪铜。
Animated的接口一般會在JavaScript線程中計算出所需要的每一個關(guān)鍵幀众弓,而LayoutAnimation則利用了Core Animation,使動畫不會被JS線程和主線程的掉幀所影響痕囱。
注意:LayoutAnimation只工作在“一次性”的動畫上("靜態(tài)"動畫) -- 如果動畫可能會被中途取消田轧,你還是需要使用Animated暴匠。
在屏幕上移動視圖(滾動鞍恢,切換,旋轉(zhuǎn))時每窖,UI線程掉幀
當具有透明背景的文本位于一張圖片上時帮掉,或者在每幀重繪視圖時需要用到透明合成的任何其他情況下,這種現(xiàn)象尤為明顯窒典。設(shè)置shouldRasterizeIOS或者renderToHardwareTextureAndroid屬性可以顯著改善這一現(xiàn)象蟆炊。 注意不要過度使用該特性,否則你的內(nèi)存使用量將會飛漲瀑志。在使用時涩搓,要評估你的性能和內(nèi)存使用情況污秆。如果你沒有需要移動這個視圖的需求,請關(guān)閉這一屬性昧甘。
使用動畫改變圖片的尺寸時良拼,UI線程掉幀
在iOS上,每次調(diào)整Image組件的寬度或者高度充边,都需要重新裁剪和縮放原始圖片庸推。這個操作開銷會非常大,尤其是大的圖片浇冰。比起直接修改尺寸贬媒,更好的方案是使用transform: [{scale}]的樣式屬性來改變尺寸。比如當你點擊一個圖片肘习,要將它放大到全屏的時候际乘,就可以使用這個屬性。
Touchable系列組件不能很好的響應(yīng)
有些時候漂佩,如果我們有一項操作與點擊事件所帶來的透明度改變或者高亮效果發(fā)生在同一幀中蚓庭,那么有可能在onPress函數(shù)結(jié)束之前我們都看不到這些效果。比如在onPress執(zhí)行了一個setState的操作仅仆,這個操作需要大量計算工作并且導(dǎo)致了掉幀器赞。對此的一個解決方案是將onPress處理函數(shù)中的操作封裝到requestAnimationFrame中:
handleOnPress() {
// 謹記在使用requestAnimationFrame、setTimeout以及setInterval時
// 要使用TimerMixin(其作用是在組件unmount時墓拜,清除所有定時器)
this.requestAnimationFrame(() => {
this.doExpensiveAction();
});
}
分析
你可以利用內(nèi)置的分析器來同時獲取JavaScript線程和主線程中代碼執(zhí)行情況的詳細信息港柜。
升級
時刻將React Native更新到最新的版本,可以獲得更多API咳榜、視圖夏醉、開發(fā)者工具以及其他一些好東西(官方開發(fā)任務(wù)繁重,人手緊缺涌韩,幾乎不會對舊版本提供維護支持畔柔,所以即便更新可能帶來一些兼容上的變更,但建議開發(fā)者還是盡一切可能第一時間更新)臣樱。由于一個完整的React Native項目是由Android項目靶擦、iOS項目和JavaScript項目組成的,且都打包在一個npm包中雇毫,所以升級可能會有一些麻煩玄捕。以下是目前所需的升級步驟:
更新react-native的node依賴包
打開項目目錄下的package.json文件,然后在dependencies模塊下找到react-native棚放,將當前版本號改到最新(或指定)版本號枚粘,如:
{
"name": "reactnativedemo",
"version": "1.0.0",
"description": "",
"main": "index.android.js",
"scripts": {
"start": "node node_modules/react-native/local-cli/cli.js start"
},
"author": "",
"license": "ISC",
"dependencies": {
"react": "^15.4.1",
"react-native": "^0.38.0"
}
}
react-native的npm包的最新版本可以去這里查看,或使用npm info react-native命令查看飘蚯。
項目的根目錄執(zhí)行:
npm install
安裝最新的React Native版本,成功后可能會出現(xiàn)如下類似警告:
npm WARN react-native@0.38.0 requires a peer of react@15.4.1 but none was installed.
根據(jù)警告執(zhí)行:
npm install –save react@15.4.1
更新最新的React且項目下package.json 的 dependencies下的react版本會被修改為 15.4.1
升級項目模板文件
新版本的npm包通常還會包含一些動態(tài)生成的文件馍迄,這些文件是在運行react-native init創(chuàng)建新項目時生成的福也,比如iOS和Android的項目文件。為了使老項目的項目文件也能得到更新(不重新init)攀圈,你需要在命令行中運行:
react-native upgrade
這一命令會檢查最新的項目模板拟杉,然后進行如下操作:
- 如果是新添加的文件,則直接創(chuàng)建量承。
- 如果文件和當前版本的文件相同搬设,則跳過。
- 如果文件和當前版本的文件不同撕捍,則會提示你一些選項:查看兩者的不同拿穴,選擇保留你的版本或是用新的模板覆蓋。你可以按下h鍵來查看所有可以使用的命令忧风。
注意:如果你有修改原生代碼默色,那么在使用upgrade升級前,先備份狮腿,再覆蓋腿宰。覆蓋完成后,使用比對工具找出差異缘厢,將你之前修改的代碼逐步搬運到新文件中吃度。
手動升級
有時候React Native的項目結(jié)構(gòu)改動較大,此時還需要手動做一些修改贴硫,例如從0.13到0.14版本椿每,或是0.28到0.29版本。所以在升級時請先閱讀一下更新日志英遭,以確定是否需要做一些額外的手動修改间护。
查看版本是否升級成功
執(zhí)行:
react-native -v
通過如上命令來看最新的版本,檢測是否升級成功挖诸!
特定平臺代碼
在制作跨平臺的App時汁尺,多半會碰到針對不同平臺編寫不同代碼的需求。最直接的方案就是把組件放置到不同的文件夾下:
/common/components/
/android/components/
/ios/components/
另一個選擇是根據(jù)平臺不同在組件的文件命名上加以區(qū)分多律,如下:
BigButtonIOS.js
BigButtonAndroid.js
但除此以外React Native還提供了另外兩種簡單區(qū)分平臺的方案:
特定平臺擴展名
React Native會檢測某個文件是否具有.ios.或是.android.的擴展名痴突,然后根據(jù)當前運行的平臺加載正確對應(yīng)的文件。
假設(shè)你的項目中有如下兩個文件:
BigButton.ios.js
BigButton.android.js
這樣命名組件后你就可以在其他組件中直接引用菱涤,而無需關(guān)心當前運行的平臺是哪個苞也。
import BigButton from './components/BigButton';
React Native會根據(jù)運行平臺的不同引入正確對應(yīng)的組件。
平臺模塊
React Native提供了一個檢測當前運行平臺的模塊粘秆。如果組件只有一小部分代碼需要依據(jù)平臺定制,那么這個模塊就可以派上用場收毫。
import { Platform, StyleSheet } from 'react-native';
var styles = StyleSheet.create({
height: (Platform.OS === 'ios') ? 200 : 100,
});
Platform.OS在iOS上會返回ios攻走,而在Android設(shè)備或模擬器上則會返回android殷勘。
還有個實用的方法是Platform.select(),它可以以Platform.OS為key昔搂,從傳入的對象中返回對應(yīng)平臺的值玲销,見下面的示例:
import { Platform, StyleSheet } from 'react-native';
const styles = StyleSheet.create({
container: {
flex: 1,
...Platform.select({
ios: {
backgroundColor: 'red',
},
android: {
backgroundColor: 'blue',
},
}),
},
});
上面的代碼會根據(jù)平臺的不同返回不同的container樣式——iOS上背景色為紅色,而android為藍色摘符。
這一方法可以接受任何合法類型的參數(shù)贤斜,因此你也可以直接用它針對不同平臺返回不同的組件,像下面這樣:
const Component = Platform.select({
ios: () => require('ComponentIOS'),
android: () => require('ComponentAndroid'),
})();
<Component />;
檢測Android版本
在Android上逛裤,平臺模塊還可以用來檢測當前所運行的Android平臺的版本:
import { Platform } from 'react-native';
if(Platform.Version === 21){
console.log('Running on Lollipop!');
}
React Native學(xué)習(xí)筆記--進階(一)--嵌入到Android原生應(yīng)用中瘩绒、組件的生命周期、顏色带族、圖片锁荔、觸摸事件
React Native學(xué)習(xí)筆記--進階(二)--動畫
React Native學(xué)習(xí)筆記--進階(三)--定時器、直接操作(setNativeProps)蝙砌、調(diào)試
React Native學(xué)習(xí)筆記--進階(四)--導(dǎo)航器
React Native學(xué)習(xí)筆記--進階(五)--性能阳堕、升級、特定平臺代碼