Android性能優(yōu)化之被忽視的優(yōu)化點
對于性能優(yōu)化這個知識點來說,實在是太廣了,博主本人也一直非常關(guān)注這方面的學習,而對于性能優(yōu)化來說它包括了非常非常非常多方面,比如:I/O的優(yōu)化、網(wǎng)絡操作的優(yōu)化、內(nèi)存的優(yōu)化、數(shù)據(jù)結(jié)構(gòu)的優(yōu)化、代碼層次的優(yōu)化、UI渲染優(yōu)化、CPU資源使用率的優(yōu)化、異常處理的優(yōu)化等等等等。。。
本篇文章就博主本人的理解來講述一些在Android開發(fā)中可以優(yōu)化的地方
ArrayList和Vector
ArrayList和Vector都是內(nèi)部以數(shù)組實現(xiàn)的List,它們兩唯一的區(qū)別就是對多線程的支持,ArrayList是線程不安全的,而Vector內(nèi)部對大多數(shù)方法都做了同步,是線程安全的,既然是線程安全的,所以性能方面肯定不如ArrayList了(當然想法肯定是對的),不過這需要看哪方面了,ArrayList在add、get、remove等操作效率肯定是高于Vector的,而在內(nèi)存方面,Vector卻比ArrayList表現(xiàn)的更好,這歸根都是ArrayList的擴容策略導致的,稍后分析。
實現(xiàn)RandomAccess接口的集合使用fori遍歷
先談談List集合的遍歷方式,有三種:foreach、iterator、fori。
而在開發(fā)中一般需要遍歷時首選肯定是foreach了,因為它效率高,這個觀點沒錯,不過需要分場合了。
下面是我用這三種方式測試遍歷有100w條數(shù)據(jù)的ArrayList集合:
- long start = System.currentTimeMillis();
- for (int i = 0; i < size; i++) {
- data.get(i);
- }
- long end = System.currentTimeMillis();
- Log.v("zxy","fori花費:"+(end-start));
- start = System.currentTimeMillis();
- for (Integer integer : data) {
- }
- end = System.currentTimeMillis();
- Log.v("zxy","foreach花費:"+(end-start));
- Iterator<Integer> iterator = data.iterator();
- start = System.currentTimeMillis();
- while (iterator.hasNext()){
- iterator.next();
- }
- end = System.currentTimeMillis();
- Log.v("zxy","iterator花費:"+(end-start));
- 11-19 09:11:44.276 1418-1418/? V/zxy: fori花費:30
- 11-19 09:11:44.380 1418-1418/? V/zxy: foreach花費:105
- 11-19 09:11:44.476 1418-1418/? V/zxy: iterator花費:95
而通常我們所說的效率高的foreach在遍歷上卻顯得不如意,而fori效率表現(xiàn)的最好,這是因為ArrayList和Vector集合內(nèi)部實現(xiàn)由數(shù)組實現(xiàn),所以隨機訪問的速度是很快的,對于可以進行隨機訪問的List,JDK為它們實現(xiàn)了RandomAccess接口,表示支持快速隨機訪問。
而在遍歷有1w條數(shù)據(jù)的LinkedList集合時:
- 11-19 09:33:23.984 1737-1737/? V/zxy: fori花費:351
- 11-19 09:33:23.988 1737-1737/? V/zxy: foreach花費:2
- 11-19 09:33:23.992 1737-1737/? V/zxy: iterator花費:4
則foreach表現(xiàn)最佳,所以對數(shù)組、或者實現(xiàn)了RandomAccess接口的List,遍歷用fori性能最佳,對LinkedList等以鏈表實現(xiàn)的集合遍歷時使用foreach或者iterator性能最佳,因為foreach的實現(xiàn)就是通過iterator實現(xiàn)的。
我們可以這樣判斷該List遍歷用哪種方式:
- if (list instanceof RandomAccess)
- {
- for (int i = 0; i < list.size(); i++) {}
- } else {
- Iterator<?> iterator = list.iterator();
- while (iterator.hasNext()) {
- iterator.next();
- }
- }
預知容量的情況下構(gòu)造ArrayList時盡量指定初始大小
ArrayList內(nèi)部的擴容策略是當其所存儲的元素數(shù)量超過它已有的大小時,它就會以1.5倍的容量進行擴容,也就是假如當前ArrayList的容量為10000,那么它在需要再存儲一個元素時,即第10001個元素,由于容量不夠而進行一次擴容,而ArrayList擴容后的容量則變?yōu)榱?5000,而多出了一個元素就多了5000個元素的空間,這太浪費內(nèi)存資源了,而且擴容還會導致整個數(shù)組進行一次內(nèi)存復制,而ArrayList集合默認大小為10,因此合理的設置ArrayList的容量可避免集合進行擴容。ArrayList內(nèi)部擴容和數(shù)組復制代碼為:
- Object[] newArray = new Object[s +
- (s < (MIN_CAPACITY_INCREMENT / 2) ?
- MIN_CAPACITY_INCREMENT : s >> 1)];
- System.arraycopy(a, 0, newArray, 0, s);
- array = a = newArray;
而Vector內(nèi)部擴容策略為按需擴容,每次+1:
- if (capacityIncrement <= 0) {
- if ((adding = elementData.length) == 0) {
- adding = 1;
- }
- } else {
- adding = capacityIncrement;
- }
- E[] newData = newElementArray(elementData.length + adding);
同樣,在眾多Map集合中也有各自擴容策略,比如HashMap每次擴容時新容量等于原始的容量*2。在我們常用做字符串拼接的StringBuffer和StringBuilder內(nèi)部,實際上也是有擴容策略,默認為擴容為原始的1.5倍。
所以,在這些需要擴容的api上,如果預先知道了數(shù)據(jù)的大小,則預先設置,這樣不僅可以避免擴容導致的空間浪費,而且還可避免內(nèi)部調(diào)用System.arraycopy()進行大量數(shù)據(jù)復制。
程序如果需要通過索引下標對List做隨機訪問,應優(yōu)先考慮ArrayList和Vector,迫不得已盡量不要使用LinkedList
雖說ArrayList在內(nèi)存上比不上Vector,不過它對數(shù)據(jù)操作的效率高,特別是在Android等移動設備上,采取犧牲一點空間換時間的方式還是可取的,而涉及到線程安全方面,則使用Vector。
如果一個方法不需要使用該對象的成員,那么把該方法設為static
靜態(tài)調(diào)用該方法比對象調(diào)用該方法快15%~20%,因為這樣可以從方法簽名上就可以看出該方法調(diào)用不會影響該對象的狀態(tài)
巧用final關(guān)鍵字
final關(guān)鍵字一般在定義常量和方法用的比較多,而大多數(shù)人對final的理解往往是在不可變性上,而final對性能優(yōu)化也有很大的作用。
比如:static int AGE = 10;當10在后面被引用時,這時會有一個字段查找的過程,對于int類型也就是查找方法區(qū)中的整型常量池,而對于final的常量,則省去了這個過程,比如:static final int AGE = 10;在使用到AGE的地方將直接用10代替。
不過對于上面這種優(yōu)化技巧,僅對基本類型和String類型有效,對于其它的引用類型則無效,但是我們在聲明常量的時候加上 static final 依然是個好習慣
對與final關(guān)鍵字,還有一個強大的作用,就是對那些使用頻繁、已經(jīng)確定為終態(tài)的方法定義final,這樣有什么好處呢?
說這個前先來說說java中方法的執(zhí)行過程吧,當調(diào)用某個方法時,首先這個方法會入棧,執(zhí)行完畢后,這個方法出棧,資源釋放,而這個過程內(nèi)部其實是內(nèi)存地址的轉(zhuǎn)移過程,當執(zhí)行入棧的方法時,其實就是把程序的執(zhí)行地址轉(zhuǎn)移到該方法存放的內(nèi)存地址中,而做此操作前,還有必須進行原先程序執(zhí)行的內(nèi)存地址保存過程,當方法執(zhí)行完出棧后則繼續(xù)按保存的地址繼續(xù)執(zhí)行程序,而這個過程,就是方法的調(diào)用過程。
所以,方法的調(diào)用過程實際上是需要空間和時間的,而對于同一個方法的頻繁調(diào)用的優(yōu)化實際上就是使用內(nèi)聯(lián)的辦法。
又說到內(nèi)聯(lián)函數(shù),內(nèi)聯(lián)函數(shù)實際上是在編譯期做的優(yōu)化,編譯器會將標為為內(nèi)聯(lián)的函數(shù)在其調(diào)用的地方直接用整個函數(shù)體進行替換掉,這就省去了函數(shù)調(diào)用所耗去的時間資源了,而換來的卻是目標代碼量的增加,所以內(nèi)聯(lián)這種優(yōu)化策略實際上是采取了以空間換時間的策略,對于移動端來說,巧用內(nèi)聯(lián)函數(shù)實則非常有益。
而要是一個函數(shù)成為內(nèi)聯(lián)函數(shù),就是將它定義為final,這樣在程序編譯時,編譯器會自動將final函數(shù)進行內(nèi)聯(lián)優(yōu)化,那么在調(diào)用該函數(shù)時則直接展開該函數(shù)體進行使用。
總結(jié),并不是內(nèi)聯(lián)函數(shù)越多越好,一方面它對我們程序的運行效率上確實有提升,而另一方面,對于過多的使用內(nèi)聯(lián)函數(shù),則會弄巧成拙,有可能會把某個方法的方法體越搞越大,而且對于某些方法體比較大的方法,內(nèi)聯(lián)展開的時間有可能超過方法調(diào)用的時間,所以這不僅不會提供性能,反而是降低了本該有的性能。
綜合來看,我們可以對那些使用頻繁、已經(jīng)確定為終態(tài)的方法、方法體不大的方法用final修飾,提供程序的性能。
優(yōu)先考慮系統(tǒng)中提供的代碼而不是自己寫
系統(tǒng)內(nèi)置了許多非常方便的api供我們使用,比如:System、Arrays、Collections、String等內(nèi)置了許多方法api,這比我們自己手寫方便多了,除了這個外,對于Android來說許多api都使用了底層C/C++實現(xiàn),所以效率上也比我們自己寫快,同樣,對于系統(tǒng)api,DVM往往也會使用內(nèi)聯(lián)的方式提高效率
慎用異常
慎用異常并不是不用異常,而是指程序中用拋異常的方式來執(zhí)行某些操作,比如有些人會以強拋異常方式來中斷某些操作等。因為拋異常時都會執(zhí)行fillInStackTrace();方法,該方法作用就是重新調(diào)整堆棧,這使得沒有必要用異常的地方一定要避免使用