title: React Native 學(xué)習(xí)筆記--進(jìn)階(五)--性能、升級版本、特定平臺(tái)代碼
tags: React Native
categories: React Native
description:
React Native 進(jìn)階(五)--性能、升級版本、特定平臺(tái)代碼
性能
使用React Native替代基于WebView的框架來開發(fā)App的一個(gè)強(qiáng)有力的理由,就是為了使App可以達(dá)到每秒60幀(足夠流暢),并且能有類似原生App的外觀和手感。但是,還是有一些地方有所欠缺,以及在某些場合React Native還不能夠替你決定如何進(jìn)行優(yōu)化,因此人工的干預(yù)依然是必要的。
關(guān)于“幀”你所需要知道的
視頻中逼真的動(dòng)態(tài)效果其實(shí)是一種幻覺,這種幻覺是由一組靜態(tài)的圖片以一個(gè)穩(wěn)定的速度快速變化所產(chǎn)生的。我們把這組圖片中的每一張圖片叫做一幀,而每秒鐘顯示的幀數(shù)直接的影響了視頻(或者說用戶界面)的流暢度和真實(shí)感。iOS設(shè)備提供了每秒60的幀率,這就留給了開發(fā)者和UI系統(tǒng)大約16.67ms來完成生成一張靜態(tài)圖片(幀)所需要的所有工作。如果在這分派的16.67ms之內(nèi)沒有能夠完成這些工作,就會(huì)引發(fā)‘丟幀’的后果,使界面表現(xiàn)的不夠流暢。
調(diào)出你應(yīng)用的開發(fā)菜單,打開Show FPS Monitor. 你會(huì)注意到有兩個(gè)不同的幀率(JS和UI):
JavaScript 幀率
對大多數(shù)React Native應(yīng)用來說,業(yè)務(wù)邏輯是運(yùn)行在JavaScript線程上的。這是React應(yīng)用所在的線程,也是發(fā)生API調(diào)用,以及處理觸摸事件等操作的線程。更新數(shù)據(jù)到原生支持的視圖是批量進(jìn)行的,并且在事件循環(huán)每進(jìn)行一次的時(shí)候被發(fā)送到原生端,這一步通常會(huì)在一幀時(shí)間結(jié)束之前處理完(一切順利的話)。如果JavaScript線程有一幀沒有及時(shí)響應(yīng),就被認(rèn)為發(fā)生了一次丟幀。 例如:你在一個(gè)復(fù)雜應(yīng)用的根組件上調(diào)用了this.setState,從而導(dǎo)致一次開銷很大的子組件樹的重繪,可想而知,這可能會(huì)花費(fèi)200ms也就是整整12幀的丟失。此時(shí),任何由JavaScript控制的動(dòng)畫都會(huì)卡住。只要卡頓超過100ms,用戶就會(huì)明顯的感覺到。
這種情況經(jīng)常發(fā)生在Navigator的切換過程中:當(dāng)你push一個(gè)新的路由時(shí),JavaScript需要繪制新場景所需的所有組件,以發(fā)送正確的命令給原生端去創(chuàng)建視圖。由于切換是由JavaScript線程所控制,因此經(jīng)常會(huì)占用若干幀的時(shí)間,引起一些卡頓。有的時(shí)候,組件會(huì)在componentDidMount函數(shù)中做一些額外的事情,這甚至可能會(huì)導(dǎo)致頁面切換過程中多達(dá)一秒的卡頓。
另一個(gè)例子是觸摸事件的響應(yīng):如果你正在JavaScript線程處理一個(gè)跨越多個(gè)幀的工作,你可能會(huì)注意到TouchableOpacity的響應(yīng)被延遲了。這是因?yàn)镴avaScript線程太忙了,不能夠處理主線程發(fā)送過來的原始觸摸事件。結(jié)果TouchableOpacity就不能及時(shí)響應(yīng)這些事件并命令主線程的頁面去調(diào)整透明度了。
主線程 (也即UI線程) 幀率
很多人會(huì)注意到,NavigatorIOS的性能要比Navigator好的多。原因就是它的切換動(dòng)畫是完全在主線程上執(zhí)行的,因此不會(huì)被JavaScript線程上的掉幀所影響。
同樣,當(dāng)JavaScript線程卡住的時(shí)候,你仍然可以歡快的上下滾動(dòng)ScrollView,因?yàn)镾crollView運(yùn)行在主線程之上(盡管滾動(dòng)事件會(huì)被分發(fā)到JS線程,但是接收這些事件對于滾動(dòng)這個(gè)動(dòng)作來說并不必要)。
性能問題的常見原因
console.log語句
在運(yùn)行打好了離線包的應(yīng)用時(shí),控制臺(tái)打印語句可能會(huì)極大地拖累JavaScript線程。注意有些第三方調(diào)試庫也可能包含控制臺(tái)打印語句,比如redux-logger,所以在發(fā)布應(yīng)用前請務(wù)必仔細(xì)檢查,確保全部移除。
有個(gè)babel插件可以幫你移除所有的console.*調(diào)用。首先需要使用npm install babel-plugin-transform-remove-console --save來安裝,然后在項(xiàng)目根目錄下編輯(或者是新建)一個(gè)名為·.babelrc`的文件,在其中加入:
{
"env": {
"production": {
"plugins": ["transform-remove-console"]
}
}
}
這樣在打包發(fā)布時(shí),所有的控制臺(tái)語句就會(huì)被自動(dòng)移除,而在調(diào)試時(shí)它們?nèi)匀粫?huì)被正常調(diào)用。
開發(fā)模式 (dev=true)
JavaScript線程的性能在開發(fā)模式下是很糟糕的。這是不可避免的,因?yàn)橛性S多工作需要在運(yùn)行的時(shí)候去做,譬如使你獲得良好的警告和錯(cuò)誤信息,又比如驗(yàn)證屬性類型(propTypes)以及產(chǎn)生各種其他的警告。
緩慢的導(dǎo)航器(Navigator)切換
Navigator的動(dòng)畫是由JavaScript線程所控制的。想象一下“從右邊推入”這個(gè)場景的切換:每一幀中,新的場景從右向左移動(dòng),從屏幕右邊緣開始,最終移動(dòng)到x軸偏移為0的屏幕位置。切換過程中的每一幀,JavaScript線程都需要發(fā)送一個(gè)新的x軸偏移量給主線程。如果JavaScript線程卡住了,它就無法處理這項(xiàng)事情,因而這一幀就無法更新,動(dòng)畫就被卡住了。
長遠(yuǎn)的解決方法,其中一部分是要允許基于JavaScript的動(dòng)畫從主線程分離。同樣是上面的例子,我們可以在切換動(dòng)畫開始的時(shí)候計(jì)算出一個(gè)列表,其中包含所有的新的場景需要的x軸偏移量,然后一次發(fā)送到主線程以某種優(yōu)化的方式執(zhí)行。由于JavaScript線程已經(jīng)從更新x軸偏移量給主線程這個(gè)職責(zé)中解脫了出來,因此JavaScript線程中的掉幀就不是什么大問題了 —— 用戶將基本上不會(huì)意識到這個(gè)問題,因?yàn)橛脩舻淖⒁饬?huì)被流暢的切換動(dòng)作所吸引。
不幸的是,這個(gè)方案還沒有被實(shí)現(xiàn)。所以當(dāng)前的解決方案是,在動(dòng)畫的進(jìn)行過程中,利用InteractionManager來選擇性的渲染新場景所需的最小限度的內(nèi)容。
InteractionManager.runAfterInteractions的參數(shù)中包含一個(gè)回調(diào),這個(gè)回調(diào)會(huì)在navigator切換動(dòng)畫結(jié)束的時(shí)候被觸發(fā)(每個(gè)來自于Animated接口的動(dòng)畫都會(huì)通知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)容 —— 例如:當(dāng)你加載Facebook應(yīng)用的時(shí)候,你會(huì)看見一個(gè)灰色方形的消息流的占位符,是將來用來顯示文字的地方。如果你正在場景中繪制地圖,那么最好在場景切換完成之前,顯示一個(gè)灰色的占位頁面或者是一個(gè)轉(zhuǎn)動(dòng)的動(dòng)畫,因?yàn)榍袚Q過程的確會(huì)導(dǎo)致主線程的掉幀。
ListView初始化渲染太慢以及列表過長時(shí)滾動(dòng)性能太差
這是一個(gè)頻繁出現(xiàn)的問題。因?yàn)閕OS配備了UITableView,通過重用底層的UIViews實(shí)現(xiàn)了非常高性能的體驗(yàn)。用React Native實(shí)現(xiàn)相同效果的工作仍正在進(jìn)行中,但是在此之前,我們有一些可用的方法來稍加改進(jìn)性能以滿足我們的需求。
initialListSize
這個(gè)屬性定義了在首次渲染中繪制的行數(shù)。如果我們關(guān)注于快速的顯示出頁面,可以設(shè)置initialListSize為1,然后我們會(huì)發(fā)現(xiàn)其他行在接下來的幀中被快速繪制到屏幕上。而每幀所顯示的行數(shù)由pageSize所決定。
pageSize
在初始渲染也就是initialListSize被使用之后,ListView將利用pageSize來決定每一幀所渲染的行數(shù)。默認(rèn)值為1 —— 但是如果你的頁面很小,而且渲染的開銷不大的話,你會(huì)希望這個(gè)值更大一些。稍加調(diào)整,你會(huì)發(fā)現(xiàn)它所起到的作用。
scrollRenderAheadDistance
“在將要進(jìn)入屏幕某些區(qū)域中先渲染行,距離按像素計(jì)算”
如果我們有一個(gè)2000個(gè)元素的列表,并且立刻全部渲染出來的話,無論是內(nèi)存還是計(jì)算資源都會(huì)顯得很匱乏。還很可能導(dǎo)致非常可怕的阻塞。因此scrollRenderAheadDistance允許我們來指定一個(gè)超過視野范圍之外所需要渲染的行數(shù)。
removeClippedSubviews
“當(dāng)這一選項(xiàng)設(shè)置為true的時(shí)候,超出屏幕的子視圖(同時(shí)overflow值為hidden)會(huì)從它們原生的父視圖中移除。這個(gè)屬性可以在列表很長的時(shí)候提高滾動(dòng)的性能。默認(rèn)為true。(0.14版本前默認(rèn)為false)”
這是一個(gè)應(yīng)用在長列表上極其重要的優(yōu)化。Android上,overflow值總是hidden的,所以你不必?fù)?dān)心沒有設(shè)置它。而在iOS上,你需要確保在行容器上設(shè)置了overflow: hidden。
我的組件渲染太慢,我不需要立即顯示全部
這在初次瀏覽ListView時(shí)很常見,適當(dāng)?shù)氖褂盟谦@得穩(wěn)定性能的關(guān)鍵。就像之前所提到的,它可以提供一些手段在不同幀中來分開渲染頁面,稍加改進(jìn)就可以滿足你的需求。此外要記住的是,ListView也可以橫向滾動(dòng)。
在重繪一個(gè)幾乎沒有什么變化的頁面時(shí),JS幀率嚴(yán)重降低
如果你正在使用一個(gè)ListView,你必須提供一個(gè)rowHasChanged函數(shù),它通過快速的算出某一行是否需要重繪,來減少很多不必要的工作。如果你使用了不可變的數(shù)據(jù)結(jié)構(gòu),這項(xiàng)工作就只需檢查其引用是否相等。
同樣的,你可以實(shí)現(xiàn)shouldComponentUpdate函數(shù)來指明在什么樣的確切條件下,你希望這個(gè)組件得到重繪。如果你編寫的是純粹的組件(返回值完全由props和state所決定),你可以利用PureRenderMixin來為你做這個(gè)工作。再強(qiáng)調(diào)一次,不可變的數(shù)據(jù)結(jié)構(gòu)在提速方面非常有用 —— 當(dāng)你不得不對一個(gè)長列表對象做一個(gè)深度的比較,它會(huì)使重繪你的整個(gè)組件更加快速,而且代碼量更少。
由于在JavaScript線程中同時(shí)做很多事情,導(dǎo)致JS線程掉幀
“導(dǎo)航切換極慢”是該問題的常見表現(xiàn)。在其他情形下,這種問題也可能會(huì)出現(xiàn)。使用InteractionManager是一個(gè)好的方法,但是如果在動(dòng)畫中,為了用戶體驗(yàn)的開銷而延遲其他工作并不太能接受,那么你可以考慮一下使用LayoutAnimation。
Animated的接口一般會(huì)在JavaScript線程中計(jì)算出所需要的每一個(gè)關(guān)鍵幀,而LayoutAnimation則利用了Core Animation,使動(dòng)畫不會(huì)被JS線程和主線程的掉幀所影響。
注意:LayoutAnimation只工作在“一次性”的動(dòng)畫上("靜態(tài)"動(dòng)畫) -- 如果動(dòng)畫可能會(huì)被中途取消,你還是需要使用Animated。
在屏幕上移動(dòng)視圖(滾動(dòng),切換,旋轉(zhuǎn))時(shí),UI線程掉幀
當(dāng)具有透明背景的文本位于一張圖片上時(shí),或者在每幀重繪視圖時(shí)需要用到透明合成的任何其他情況下,這種現(xiàn)象尤為明顯。設(shè)置shouldRasterizeIOS或者renderToHardwareTextureAndroid屬性可以顯著改善這一現(xiàn)象。 注意不要過度使用該特性,否則你的內(nèi)存使用量將會(huì)飛漲。在使用時(shí),要評估你的性能和內(nèi)存使用情況。如果你沒有需要移動(dòng)這個(gè)視圖的需求,請關(guān)閉這一屬性。
使用動(dòng)畫改變圖片的尺寸時(shí),UI線程掉幀
在iOS上,每次調(diào)整Image組件的寬度或者高度,都需要重新裁剪和縮放原始圖片。這個(gè)操作開銷會(huì)非常大,尤其是大的圖片。比起直接修改尺寸,更好的方案是使用transform: [{scale}]的樣式屬性來改變尺寸。比如當(dāng)你點(diǎn)擊一個(gè)圖片,要將它放大到全屏的時(shí)候,就可以使用這個(gè)屬性。
Touchable系列組件不能很好的響應(yīng)
有些時(shí)候,如果我們有一項(xiàng)操作與點(diǎn)擊事件所帶來的透明度改變或者高亮效果發(fā)生在同一幀中,那么有可能在onPress函數(shù)結(jié)束之前我們都看不到這些效果。比如在onPress執(zhí)行了一個(gè)setState的操作,這個(gè)操作需要大量計(jì)算工作并且導(dǎo)致了掉幀。對此的一個(gè)解決方案是將onPress處理函數(shù)中的操作封裝到requestAnimationFrame中:
handleOnPress() {
// 謹(jǐn)記在使用requestAnimationFrame、setTimeout以及setInterval時(shí)
// 要使用TimerMixin(其作用是在組件unmount時(shí),清除所有定時(shí)器)
this.requestAnimationFrame(() => {
this.doExpensiveAction();
});
}
分析
你可以利用內(nèi)置的分析器來同時(shí)獲取JavaScript線程和主線程中代碼執(zhí)行情況的詳細(xì)信息。
升級
時(shí)刻將React Native更新到最新的版本,可以獲得更多API、視圖、開發(fā)者工具以及其他一些好東西(官方開發(fā)任務(wù)繁重,人手緊缺,幾乎不會(huì)對舊版本提供維護(hù)支持,所以即便更新可能帶來一些兼容上的變更,但建議開發(fā)者還是盡一切可能第一時(shí)間更新)。由于一個(gè)完整的React Native項(xiàng)目是由Android項(xiàng)目、iOS項(xiàng)目和JavaScript項(xiàng)目組成的,且都打包在一個(gè)npm包中,所以升級可能會(huì)有一些麻煩。以下是目前所需的升級步驟:
更新react-native的node依賴包
打開項(xiàng)目目錄下的package.json文件,然后在dependencies模塊下找到react-native,將當(dāng)前版本號改到最新(或指定)版本號,如:
{
"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命令查看。
項(xiàng)目的根目錄執(zhí)行:
npm install
安裝最新的React Native版本,成功后可能會(huì)出現(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且項(xiàng)目下package.json 的 dependencies下的react版本會(huì)被修改為 15.4.1
升級項(xiàng)目模板文件
新版本的npm包通常還會(huì)包含一些動(dòng)態(tài)生成的文件,這些文件是在運(yùn)行react-native init創(chuàng)建新項(xiàng)目時(shí)生成的,比如iOS和Android的項(xiàng)目文件。為了使老項(xiàng)目的項(xiàng)目文件也能得到更新(不重新init),你需要在命令行中運(yùn)行:
react-native upgrade
這一命令會(huì)檢查最新的項(xiàng)目模板,然后進(jìn)行如下操作:
- 如果是新添加的文件,則直接創(chuàng)建。
- 如果文件和當(dāng)前版本的文件相同,則跳過。
- 如果文件和當(dāng)前版本的文件不同,則會(huì)提示你一些選項(xiàng):查看兩者的不同,選擇保留你的版本或是用新的模板覆蓋。你可以按下h鍵來查看所有可以使用的命令。
注意:如果你有修改原生代碼,那么在使用upgrade升級前,先備份,再覆蓋。覆蓋完成后,使用比對工具找出差異,將你之前修改的代碼逐步搬運(yùn)到新文件中。
手動(dòng)升級
有時(shí)候React Native的項(xiàng)目結(jié)構(gòu)改動(dòng)較大,此時(shí)還需要手動(dòng)做一些修改,例如從0.13到0.14版本,或是0.28到0.29版本。所以在升級時(shí)請先閱讀一下更新日志,以確定是否需要做一些額外的手動(dòng)修改。
查看版本是否升級成功
執(zhí)行:
react-native -v
通過如上命令來看最新的版本,檢測是否升級成功!
特定平臺(tái)代碼
在制作跨平臺(tái)的App時(shí),多半會(huì)碰到針對不同平臺(tái)編寫不同代碼的需求。最直接的方案就是把組件放置到不同的文件夾下:
/common/components/
/android/components/
/ios/components/
另一個(gè)選擇是根據(jù)平臺(tái)不同在組件的文件命名上加以區(qū)分,如下:
BigButtonIOS.js
BigButtonAndroid.js
但除此以外React Native還提供了另外兩種簡單區(qū)分平臺(tái)的方案:
特定平臺(tái)擴(kuò)展名
React Native會(huì)檢測某個(gè)文件是否具有.ios.或是.android.的擴(kuò)展名,然后根據(jù)當(dāng)前運(yùn)行的平臺(tái)加載正確對應(yīng)的文件。
假設(shè)你的項(xiàng)目中有如下兩個(gè)文件:
BigButton.ios.js
BigButton.android.js
這樣命名組件后你就可以在其他組件中直接引用,而無需關(guān)心當(dāng)前運(yùn)行的平臺(tái)是哪個(gè)。
import BigButton from './components/BigButton';
React Native會(huì)根據(jù)運(yùn)行平臺(tái)的不同引入正確對應(yīng)的組件。
平臺(tái)模塊
React Native提供了一個(gè)檢測當(dāng)前運(yùn)行平臺(tái)的模塊。如果組件只有一小部分代碼需要依據(jù)平臺(tái)定制,那么這個(gè)模塊就可以派上用場。
import { Platform, StyleSheet } from 'react-native';
var styles = StyleSheet.create({
height: (Platform.OS === 'ios') ? 200 : 100,
});
Platform.OS在iOS上會(huì)返回ios,而在Android設(shè)備或模擬器上則會(huì)返回android。
還有個(gè)實(shí)用的方法是Platform.select(),它可以以Platform.OS為key,從傳入的對象中返回對應(yīng)平臺(tái)的值,見下面的示例:
import { Platform, StyleSheet } from 'react-native';
const styles = StyleSheet.create({
container: {
flex: 1,
...Platform.select({
ios: {
backgroundColor: 'red',
},
android: {
backgroundColor: 'blue',
},
}),
},
});
上面的代碼會(huì)根據(jù)平臺(tái)的不同返回不同的container樣式——iOS上背景色為紅色,而android為藍(lán)色。
這一方法可以接受任何合法類型的參數(shù),因此你也可以直接用它針對不同平臺(tái)返回不同的組件,像下面這樣:
const Component = Platform.select({
ios: () => require('ComponentIOS'),
android: () => require('ComponentAndroid'),
})();
<Component />;
檢測Android版本
在Android上,平臺(tái)模塊還可以用來檢測當(dāng)前所運(yùn)行的Android平臺(tái)的版本:
import { Platform } from 'react-native';
if(Platform.Version === 21){
console.log('Running on Lollipop!');
}
React Native學(xué)習(xí)筆記--進(jìn)階(一)--嵌入到Android原生應(yīng)用中、組件的生命周期、顏色、圖片、觸摸事件
React Native學(xué)習(xí)筆記--進(jìn)階(二)--動(dòng)畫
React Native學(xué)習(xí)筆記--進(jìn)階(三)--定時(shí)器、直接操作(setNativeProps)、調(diào)試
React Native學(xué)習(xí)筆記--進(jìn)階(四)--導(dǎo)航器
React Native學(xué)習(xí)筆記--進(jìn)階(五)--性能、升級、特定平臺(tái)代碼