高性胜のReact Native ListViewのための文字列の再利甚

スクロヌル時に画面を越えるメモリに以前に保存された行の再利甚は、iOSおよびAndroidで最初に実装されたListViewコンポヌネントの䜿甚を最適化するための広範な技術です。 React NativeのコンポヌネントずしおのListViewの実装は、デフォルトではこの最適化を盎接含んでいたせんが、他にも倚くの利点がありたす。 ただし、これは調査する䟡倀のある玠晎らしい䟋です。 Reactスタディの䞀郚ずしおこの実装を怜蚎するこずも興味深い思考実隓になりたす。



リストはモバむルアプリ開発の重芁な郚分です



リストは、モバむルアプリの栞心です。 倚くのアプリケヌションは、リストを衚瀺したす。これは、Facebookアプリケヌションフィヌドの出版物のリスト、メッセンゞャヌのチャットリスト、Gmailメヌルのリスト、Instagramの写真のリスト、Twitterのツむヌトのリストなどです。



膚倧な数のデヌタ゜ヌス、数千行、倧量のメモリを必芁ずするメディアファむルなど、リストがより耇雑になるず、その開発も難しくなりたす。



䞀方では、アプリケヌションの速床を維持する必芁がありたす。なぜなら、 60 FPSスクロヌルは、ネむティブ゚ンゲヌゞメント゚クスペリ゚ンスUXのゎヌルドスタンダヌドになりたした。 䞀方、モバむルデバむスには過剰なリ゜ヌスがないため、メモリの消費を䜎く抑える必芁がありたす。 これらの䞡方の条件を満たすこずは必ずしも容易ではありたせん。







ListViewの完璧な実装を芋぀ける



゜フトりェア開発の基本的なルヌルは、どのシナリオでも最適化を予枬できないこずです。 別の分野の䟋を芋おみたしょう。デヌタを保存するのに理想的なデヌタベヌスはありたせん。 䞀郚のナヌスケヌスに最適なSQLデヌタベヌス、および他の状況に最適なNoSQLデヌタベヌスに粟通しおいる堎合がありたす。 独自のデヌタベヌスを開発する可胜性は䜎いため、゜フトりェア開発者ずしお、特定の問題を解決するための適切なツヌルを遞択する必芁がありたす。



リストの衚瀺にも同じルヌルが適甚されたす。リストの衚瀺を実装する方法は、どのナヌスケヌスにも適しおいるだけでなく、同時に高いFPS速床ず䜎メモリ芁件を維持する方法を芋぀ける可胜性は䜎いです。



倧たかに蚀えば、モバむルアプリケヌションでリストを䜿甚するためのオプションには2぀のタむプがありたす。



•非垞に倧きなデヌタ゜ヌスを持぀ほが同䞀の行。 連絡先リストの行は同じように芋え、構造も同じです。 ナヌザヌが探しおいるものが芋぀かるたで、文字列をすばやく閲芧できるようにしおください。 䟋アドレス垳。

•非垞に異なる行ず小さなデヌタ゜ヌス。 ここでは、すべおの行が異なり、異なる量のテキストが含たれおいたす。 䞀郚にはメディアが含たれおいたす。 ほずんどの堎合、ナヌザヌはストリヌム党䜓を衚瀺するのではなく、メッセヌゞを順番に読み取りたす。 䟋チャットメッセヌゞ。

異なるナヌスケヌスに分割するこずの利点は、オプションごずに異なる最適化手法を提䟛できるこずです。



既補のReact Nativeリスト



React Nativeには、すぐに䜿えるListView実装が付属しおいたす。 スクロヌル時に画面に衚瀺される行の「遅延読み蟌み」、再描画の回数を最小限に抑える、さたざたなむベントルヌプで線を描画するなど、非垞に合理的な最適化が含たれおいたす。



ListViewの完成した実装のもう1぀の興味深い特性は、React Nativeの䞀郚であるネむティブScrollViewコンポヌネントを介しおJavaScriptで完党に実装されおいるこずです。 iOSたたはAndroidの開発経隓がある堎合、この事実は奇劙に思えるかもしれたせん。 ネむティブ開発キットSDKは、iOS甚のUITableViewずAndroid甚のListViewのリストビュヌの実瞟のある実装に基づいおいたす。 React Nativeチヌムが䜿甚しないず決めたものはないこずは泚目に倀したす。



これにはさたざたな理由があるかもしれたせんが、これは以前に定矩されたナヌスケヌスによるものず思われたす。 iOSのUITableViewずAndroidのListViewは、最初のナヌスケヌスにほが完党に機胜する類䌌の最適化手法を䜿甚したす-ほずんど同䞀の行ず非垞に倧きなデヌタ゜ヌスのリスト。 完成したListView React Nativeは、2番目のオプション甚に最適化されおいたす。



Facebook゚コシステムの䞻なリストは、Facebook投皿フィヌドです。 Facebookアプリケヌションは、React Nativeが登堎するかなり前にiOSずAndroidに実装されたした。 おそらくテヌプの元の実装は、iOSのUITableViewずAndroidのListViewのネむティブ実装に実際に基づいおいたため、想像できるように、期埅どおりに機胜したせんでした。 テヌプは、2番目のナヌスケヌスの兞型的な䟋です。 行は非垞に異なっおいたす、なぜなら すべおの出版物は異なりたす-それらはコンテンツの量が異なり、異なる皮類のメディアファむルを含み、異なる構造を持っおいたす。 ナヌザヌはフィヌド内の出版物を順番に読み、通垞は䞀床に䜕癟行もスクロヌルしたせん。



では、なぜ再利甚を怜蚎しないのですか



2番目のナヌスケヌス非垞に異なる行ず小さなデヌタ゜ヌスを持぀リストがケヌスに適しおいる堎合は、既補のListView実装を遞択するこずを怜蚎する必芁がありたす。 最初のナヌスケヌスでケヌスが説明されおいお、完成した実装の䜜業に満足できない堎合は、代替オプションを詊しおみるこずをお勧めしたす。



最初のナヌスケヌスは、ほずんど同䞀の行ず非垞に倧きなデヌタ゜ヌスを持぀リストであるこずを思い出しおください。 このシナリオでは、有効であるこずが蚌明されおいる䞻な最適化手法は文字列の再利甚です。

デヌタ゜ヌスは非垞に倧きい可胜性があるため、すべおの行を同時にメモリに栌玍できないこずは明らかです。 メモリ消費を最小限に抑えるために、珟圚画面に衚瀺されおいる行のみを保存したす。 スクロヌルの結果ずしお衚瀺されなくなった行は解攟され、衚瀺される新しい行はメモリに配眮されたす。



ただし、スクロヌル䞭に垞にメモリ内の行を解攟しお配眮するには、非垞に集䞭的なプロセッサ䜜業が必芁です。 このネむティブアプロヌチを䜿甚するず、60 FPSの望たしい速床を達成できない堎合がありたす。 幞いなこずに、この䜿甚䟋では、文字列はほずんど同じです。 ぀たり、画面からスクロヌルされた行を解攟する代わりに、衚瀺されおいるデヌタを新しい行のデヌタで眮き換えるだけで新しい行を䜜成し、新しいメモリ䜍眮を回避できたす。



実甚的な郚分に移りたしょう。 このナヌスケヌスを詊すために䟋を甚意したす。 この䟋には、同じ構造の3,000行のデヌタが含たれたす。



import React, { Component } from 'react'; import { Text, View, Dimensions } from 'react-native'; import RecyclingListView from './RecyclingListView'; const ROWS_IN_DATA_SOURCE = 3000; const dataSource = []; for (let i=0; i<ROWS_IN_DATA_SOURCE; i++) dataSource.push(`This is the data for row # ${i+1}`); export default class RecyclingExample extends Component { render() { return ( <View style={{flex: 1, paddingTop: 20,}}> <RecyclingListView renderRow={this.renderRow} numRows={dataSource.length} rowHeight={50} /> </View> ); } renderRow(rowID) { return ( <Text style={{ width: Dimensions.get('window').width, height: 50, backgroundColor: '#ffffff' }}>{dataSource[rowID]}</Text> ); view rawRecyclingExample.js hosted with by GitHub } }
      
      





UITableViewのネむティブ実装を䜿甚する



前述のように、iOSおよびAndroidのネむティブSDKには、文字列を曞き換える堅牢な実装がありたす。 iOSに焊点を合わせ、UITableViewを䜿甚したす。



このテクニックをJavaScriptで完党に実装しようずしない理由を疑問に思うかもしれたせん。 これは興味深い質問であり、いく぀かの個別のブログ゚ントリの詳现な説明に倀したす。 ただし、芁するに、行を適切に䞊曞きするには、珟圚のスクロヌルオフセットを垞に知っおいる必芁がありたす。スクロヌルするずきに行を䞊曞きする必芁があるからです。 スクロヌルむベントはネむティブゟヌンで発生し、 RNブリッゞを通過するトランゞションの数を枛らすために、むベントを远跡するこずは理にかなっおいたす。



 Objective-C: #import "RNTableViewManager.h" #import "RNTableView.h" @implementation RNTableViewManager RCT_EXPORT_MODULE() - (UIView *)view { return [[RNTableView alloc] initWithBridge:self.bridge]; } RCT_EXPORT_VIEW_PROPERTY(rowHeight, float) RCT_EXPORT_VIEW_PROPERTY(numRows, NSInteger) @end
      
      





ラッパヌ自䜓はRNTableView.mで実行され、䞻にプロパティの転送ず適切な堎所でのプロパティの䜿甚を凊理したす。 次の実装の詳现に立ち入る必芁はありたせん。ただ興味深い郚分が欠けおいるからです。



 #import "RNTableView.h" #import "RCTConvert.h" #import "RCTEventDispatcher.h" #import "RCTUtils.h" #import "UIView+React.h" @interface RNTableView()<UITableViewDataSource, UITableViewDelegate> @property (strong, nonatomic) UITableView *tableView; @end @implementation RNTableView RCTBridge *_bridge; RCTEventDispatcher *_eventDispatcher; NSMutableArray *_unusedCells; - (instancetype)initWithBridge:(RCTBridge *)bridge { RCTAssertParam(bridge); if ((self = [super initWithFrame:CGRectZero])) { _eventDispatcher = bridge.eventDispatcher; _bridge = bridge; while ([_bridge respondsToSelector:NSSelectorFromString(@"parentBridge")] && [_bridge valueForKey:@"parentBridge"]) { _bridge = [_bridge valueForKey:@"parentBridge"]; } _unusedCells = [NSMutableArray array]; [self createTableView]; } return self; } RCT_NOT_IMPLEMENTED(-initWithFrame:(CGRect)frame) RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder) - (void)layoutSubviews { [self.tableView setFrame:self.frame]; } - (void)createTableView { _tableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain]; _tableView.dataSource = self; _tableView.delegate = self; _tableView.backgroundColor = [UIColor whiteColor]; [self addSubview:_tableView]; } - (void)setRowHeight:(float)rowHeight { _tableView.estimatedRowHeight = rowHeight; _rowHeight = rowHeight; } - (NSInteger)numberOfSectionsInTableView:(UITableView *)theTableView { return 1; } - (NSInteger)tableView:(UITableView *)theTableView numberOfRowsInSection:(NSInteger)section { return self.numRows; } -(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { return self.rowHeight; } //       @end
      
      





重芁な抂念-ネむティブ環境ずJSの接続



行党䜓がビゞネスロゞックであるため、JavaScriptで定矩されたReactコンポヌネントにする必芁がありたす。 しかし、それらを簡単に構成できるようにしたいのです。 実際の曞き換えロゞックはネむティブ環境で機胜するため、これらのコンポヌネントを䜕らかの方法でJSから「転送」する必芁がありたす。



Reactコンポヌネントをネむティブコンポヌネントに子ずしお枡すこずをお勧めしたす。 JSのネむティブコンポヌネントを䜿甚しお、子コンポヌネントずしおJSXに文字列を远加するず、React Nativeはそれらをネむティブコンポヌネントに衚瀺されるUIViewビュヌに匷制的に倉換したす。



秘Theは、デヌタ゜ヌスのすべおの行からコンポヌネントを䜜成する必芁がないこずです。 私たちの䞻な目暙は文字列を再利甚するこずなので、画面に衚瀺するのに必芁なのはごくわずかです。 画面に20行が同時に衚瀺されるず仮定したす。 この倀は、画面の高さiPhone 6 Plusの堎合は736の論理ポむントを各行の高さこの堎合は50で陀算し、玄15の倀を取埗しおから、いく぀かの行を远加するこずで取埗できたす。



これらの20行が初期化のためにサブビュヌの子ずしおコンポヌネントに枡されるずき、それらはただ衚瀺されおいたせん。 私たちはそれらを「未䜿甚のセル」のバンクに保管したす。



以䞋が最も興味深いです。 UITableViewのネむティブ曞き換えは、「dequeueReusableCell」メ゜ッドを䜿甚しお機胜したす。 セルが画面に衚瀺されおいない行から䞊曞きできる堎合、この方法を䜿甚しお、曞き換えられたセルを返すこずもできたす。 セルを䞊曞きできない堎合、新しいコヌドをメモリに配眮する必芁がありたす。 新しいセルの配眮は、目に芋える線で画面を埋める前の最初にのみ行われたす。 では、新しいセルをメモリに配眮する方法は 銀行の未䜿甚のセルの1぀を取埗したす。



  - (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex { //        subview,        // [super insertSubview:subview atIndex:atIndex]; [_unusedCells addObject:subview]; } - (UIView*) getUnusedCell { UIView* res = [_unusedCells lastObject]; [_unusedCells removeLastObject]; if (res != nil) { res.tag = [_unusedCells count]; } return res; } - (UITableViewCell *)tableView:(UITableView *)theTableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *cellIdentifier = @"CustomCell"; TableViewCell *cell = (TableViewCell *)[theTableView dequeueReusableCellWithIdentifier:cellIdentifier]; if (cell == nil) { cell = [[TableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier]; cell.cellView = [self getUnusedCell]; NSLog(@"Allocated childIndex %d for row %d", (int)cell.cellView.tag, (int)indexPath.row); } else { NSLog(@"Recycled childIndex %d for row %d", (int)cell.cellView.tag, (int)indexPath.row); } //      
 return cell; }
      
      





パズルの最埌の芁玠は、デヌタ゜ヌスからのデヌタで新しい䞊曞き/䜜成されたセルを埋めるこずです。 シリヌズはReactコンポヌネントであるため、このプロセスをReactの甚語に倉換したす。衚瀺するデヌタ゜ヌスの正しい行に基づいお、行のコンポヌネントに新しいプロパティを割り圓おる必芁がありたす。



プロパティの倉曎はJS環境で発生するため、JavaScriptで盎接これを行う必芁がありたす。 これは、いずれかのシリヌズのバむンディングを返す必芁があるこずを意味したす。 これを行うには、むベントをネむティブ環境からJSに枡したす。



これは、関数の完党な実装であり、䞍足しおいる郚分がすべお含たれおいたす。

  - (UITableViewCell *)tableView:(UITableView *)theTableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *cellIdentifier = @"CustomCell"; TableViewCell *cell = (TableViewCell *)[theTableView dequeueReusableCellWithIdentifier:cellIdentifier]; if (cell == nil) { cell = [[TableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier]; cell.cellView = [self getUnusedCell]; NSLog(@"Allocated childIndex %d for row %d", (int)cell.cellView.tag, (int)indexPath.row); } else { NSLog(@"Recycled childIndex %d for row %d", (int)cell.cellView.tag, (int)indexPath.row); } //     JS NSDictionary *event = @{ @"target": cell.cellView.reactTag, @"childIndex": @(cell.cellView.tag), @"rowID": @(indexPath.row), @"sectionID": @(indexPath.section), }; [_eventDispatcher sendInputEventWithName:@"onChange" body:event]; return cell; }
      
      





すべおたずめお



さらに、RecyclingListView.jsの最終的な実装には、JavaScriptのネむティブコンポヌネントのバむンディングが必芁です。



 import React, { Component } from 'react'; import { requireNativeComponent, View } from 'react-native'; import ReboundRenderer from './ReboundRenderer'; const RNTableViewChildren = requireNativeComponent('RNTableViewChildren', null); const ROWS_FOR_RECYCLING = 20; export default class RecyclingListView extends Component { constructor(props) { super(props); const binding = []; for (let i=0; i<ROWS_FOR_RECYCLING; i++) binding.push(-1); this.state = { binding: binding // childIndex -> rowID }; } render() { const bodyComponents = []; for (let i=0; i<ROWS_FOR_RECYCLING; i++) { bodyComponents.push( <ReboundRenderer key={'r_' + i} boundTo={this.state.binding[i]} render={this.props.renderRow} /> ); } return ( <View style={{flex: 1}}> <RNTableView style={{flex: 1}} onChange={this.onBind.bind(this)} rowHeight={this.props.rowHeight} numRows={this.props.numRows} > {bodyComponents} </RNTableView> </View> ); } onBind(event) { const {target, childIndex, rowID, sectionID} = event.nativeEvent; this.state.binding[childIndex] = rowID; this.setState({ binding: this.state.binding }); } }
      
      





远加したい別の最適化は、再描画の回数を最小限にするこずです。 ぀たり 行が䞊曞きされ、バむンディングが倉曎された堎合にのみ、行を再描画する必芁がありたす。



このためには、ReboundRendererが必芁です。 この単玔なJSコンポヌネントのパラメヌタヌずしお、このコンポヌネントが珟圚バむンドされおいるデヌタ゜ヌスの行のむンデックスが取埗されたすパラメヌタヌ "boundTo"。 バむンディングを倉曎するずきにのみ再描画したす暙準の最適化shouldComponentUpdateを䜿甚



 var React = require('React'); var ReboundRenderer = React.createClass({ propTypes: { boundTo: React.PropTypes.number.isRequired, render: React.PropTypes.func.isRequired, }, shouldComponentUpdate: function(nextProps): boolean { return nextProps.boundTo !== this.props.boundTo; }, render: function(): ReactElement<any> { console.log('ReboundRenderer render() boundTo=' + this.props.boundTo); return this.props.render(this.props.boundTo); }, }); module.exports = ReboundRenderer;
      
      





ここにコヌドを含む倧郚分の完党に機胜する䟋は、 このリポゞトリにありたす 。

リポゞトリには、興味のある他のいく぀かの実隓の説明も含たれおいたす。 tableview-children.ios.js実隓もこのケヌスに適甚されたす。



Tal Kolは、iOSおよびAndroid甚のネむティブモバむルアプリの開発を専門ずするフルスタックの開発者です。 React Nativeは圌の新しい趣味です。 Talは2぀のテクノロゞヌ䌁業の共同蚭立者であり、そのうちの1぀は珟圚Wix.com サむトを䜜成するためのプラットフォヌムに属しおいたす 。



元の蚘事 Wix Engineers Blog



All Articles