Android代碼優化小技巧

来源:http://www.cnblogs.com/8hao/archive/2016/03/02/5234824.html
-Advertisement-
Play Games

這篇文章主要介紹一些小細節的優化技巧,當這些小技巧綜合使用起來的時候,對於整個App的性能提升還是有作用的,只是不能較大幅度的提升性能而已。選擇合適的演算法與數據結構才應該是你首要考慮的因素,在這篇文章中不會涉及這方面。你應該使用這篇文章中的小技巧作為平時寫代碼的習慣,這樣能夠提升代碼的效率。 通常來


這篇文章主要介紹一些小細節的優化技巧,當這些小技巧綜合使用起來的時候,對於整個App的性能提升還是有作用的,只是不能較大幅度的提升性能而已。選擇合適的演算法與數據結構才應該是你首要考慮的因素,在這篇文章中不會涉及這方面。你應該使用這篇文章中的小技巧作為平時寫代碼的習慣,這樣能夠提升代碼的效率。

通常來說,高效的代碼需要滿足下麵兩個規則:

  • 不要做冗餘的工作
  • 如果能避免,儘量不要分配記憶體

在優化App時最難解決的問題之一就是讓App能在各種類型的設備上運行。不同版本的虛擬機在不同的處理器上會有不同的運行速度。你甚至不能簡單的認為“設備X的速度是設備Y的F倍”,然後還用這種倍數關係去推測其他設備。特別的是,在模擬器上的運行速度和在實際設備上的速度沒有半點關係。同樣,設備有沒有JIT(即時編譯,譯者註)也對運行速度有重大影響:在有JIT情況下的最優化代碼不一定在沒有JIT的情況下也最優化。

為了確保App在各設備上都能良好運行,就要確保你的代碼在不同檔次的設備上都儘可能的優化。

避免創建不必要的對象

創建對象從來不是無代價的。線上程分配池裡的逐代垃圾回收器可以使臨時對象的分配變得廉價一些,但是分配記憶體總是比不分配更昂貴。

隨著你在App中分配更多的對象,你可能需要強制GC(垃圾回收,譯者註),為用戶體驗做一個小小的減壓。Android 2.3 中引入的併發GC會幫助你做這件事情,但畢竟不必要的工作應該儘量避免

因此請儘量避免創建不必要的對象,有下麵一些例子來說明這個問題:

  • 如果你需要返回一個String對象,並且你知道它最終會需要連接到一個StringBuffer,請修改你的函數簽名和實現方式,避免直接進行連接操作,應該採用創建一個臨時對象來做這個操作.
  • 當從輸入的數據集中抽取出String的時候,嘗試返回原數據的substring對象,而不是創建一個重覆的對象。你將會 new 一個 String 對象,但是它應該和原數據共用內部的char[](代價是如果你只是用原數據中的一小部分,你只需要保存這一小部分的對象在記憶體中)

一個稍微激進點的做法是把所有多維的數據分解成一維的數組:

  • 一組int數據要比一組Integer對象要好很多。可以得知,兩組一維數組要比一個二維數組更加的有效率。同樣的,這個道理可以推廣至其他原始數據類型。
  • 如果你需要實現一個數組用來存放(Foo,Bar)的對象,記住使用Foo[]與Bar[]要比(Foo,Bar)好很多。(例外的是,為了某些好的API的設計,可以適當做一些妥協。但是在自己的代碼內部,你應該多多使用分解後的容易)。

通常來說,需要避免創建更多的臨時對象。更少的對象意味者更少的GC動作,GC會對用戶體驗有比較直接的影響。

選擇Static而不是Virtual

如果你不需要訪問一個對象的值域,請保證這個方法是static類型的,這樣方法調用將快15%-20%。這是一個好的習慣,因為你可以從方法聲明中得知調用無法改變這個對象的狀態。

常量聲明為Static Final

考慮下麵這種聲明的方式

static int intVal = 42; static String strVal = "Hello, world!"; 

編譯器會使用一個初始化類的函數,然後當類第一次被使用的時候執行。這個函數將42存入intVal,還從class文件的常量表中提取了strVal的引用。當之後使用intValstrVal的時候,他們會直接被查詢到。

我們可以用final關鍵字來優化:

static final int intVal = 42; static final String strVal = "Hello, world!"; 

這時再也不需要上面的方法了,因為final聲明的常量進入了靜態dex文件的域初始化部分。調用intVal的代碼會直接使用42,調用strVal的代碼也會使用一個相對廉價的“字元串常量”指令,而不是查表。

註意:這個優化方法只對原始類型和String類型有效,而不是任意引用類型。不過,在必要時使用static final是個很好的習慣

避免內部的Getters/Setters

像C++等native language,通常使用getters(i = getCount())而不是直接訪問變數(i = mCount)。這是編寫C++的一種優秀習慣,而且通常也被其他面向對象的語言所採用,例如C#與Java,因為編譯器通常會做inline訪問,而且你需要限制或者調試變數,你可以在任何時候在getter/setter裡面添加代碼。

然而,在Android上,這是一個糟糕的寫法。虛函數的調用比起直接訪問變數要耗費更多。在面向對象編程中,將getter和setting暴露給公用介面是合理的,但在類內部應該僅僅使用域直接訪問。

在沒有JIT(Just In Time Compiler)時,直接訪問變數的速度是調用getter的3倍。有JIT時,直接訪問變數的速度是通過getter訪問的7倍。

請註意,如果你使用ProGuard, 你可以獲得同樣的效果,因為ProGuard可以為你inline accessors.

使用增強的For迴圈

增強的For迴圈(也被稱為 for-each 迴圈)可以被用在實現了 Iterable 介面的 collections 以及數組上。使用collection的時候,Iterator (迭代器,譯者註) 會被分配,用於for-each調用hasNext()next()方法。使用ArrayList時,手寫的計數式for迴圈會快3倍(不管有沒有JIT),但是對於其他collection,增強的for-each迴圈寫法會和迭代器寫法的效率一樣。

請比較下麵三種迴圈的方法:

static class Foo {     int mSplat; }  Foo[] mArray = ...  public void zero() {     int sum = 0;     for (int i = 0; i < mArray.length; ++i) {         sum += mArray[i].mSplat;     } }  public void one() {     int sum = 0;     Foo[] localArray = mArray;     int len = localArray.length;      for (int i = 0; i < len; ++i) {         sum += localArray[i].mSplat;     } }  public void two() {     int sum = 0;     for (Foo a : mArray) {         sum += a.mSplat;     } } 
  • zero()是最慢的,因為JIT沒有辦法對它進行優化。
  • one()稍微快些。
  • two() 在沒有做JIT時是最快的,可是如果經過JIT之後,與方法one()是差不多一樣快的。它使用了增強的迴圈方法for-each。

所以請儘量使用for-each的方法,但是對於ArrayList,請使用方法one()。

你還可以參考 Josh Bloch 的 《Effective Java》這本書的第46條

使用包級訪問而不是內部類的私有訪問

參考下麵一段代碼

public class Foo {     private class Inner {         void stuff() {             Foo.this.doStuff(Foo.this.mValue);         }     }      private int mValue;      public void run() {         Inner in = new Inner();         mValue = 27;         in.stuff();     }      private void doStuff(int value) {         System.out.println("Value is " + value);     } } 

這裡重要的是,我們定義了一個私有的內部類(Foo$Inner),它直接訪問了外部類中的私有方法以及私有成員對象。這是合法的,這段代碼也會如同預期一樣列印出"Value is 27"。

問題是,VM因為FooFoo$Inner是不同的類,會認為在Foo$Inner中直接訪問Foo類的私有成員是不合法的。即使Java語言允許內部類訪問外部類的私有成員。為了去除這種差異,編譯器會產生一些仿造函數:

/*package*/ static int Foo.access$100(Foo foo) {     return foo.mValue; } /*package*/ static void Foo.access$200(Foo foo, int value) {     foo.doStuff(value); } 

每當內部類需要訪問外部類中的mValue成員或需要調用doStuff()函數時,它都會調用這些靜態方法。這意味著,上面的代碼可以歸結為,通過accessor函數來訪問成員變數。早些時候我們說過,通過accessor會比直接訪問域要慢。所以,這是一個特定語言用法造成性能降低的例子。

如果你正在性能熱區(hotspot:高頻率、重覆執行的代碼段)使用像這樣的代碼,你可以把內部類需要訪問的域和方法聲明為包級訪問,而不是私有訪問許可權。不幸的是,這意味著在相同包中的其他類也可以直接訪問這些域,所以在公開的API中你不能這樣做。

避免使用float類型

Android系統中float類型的數據存取速度是int類型的一半,儘量優先採用int類型。

就速度而言,現代硬體上,float 和 double 的速度是一樣的。空間而言,double 是兩倍float的大小。在桌面機上,空間不是問題的情況下,你應該使用 double 。

同樣,對於整型,有些處理器實現了硬體幾倍的乘法,但是沒有除法。這時,整型的除法和取餘是在軟體內部實現的,這在你使用哈希表或大量輸血操作時要考慮到。

使用庫函數

除了那些常見的讓你多使用自帶庫函數的理由以外,記得系統函數有時可以替代第三方庫,並且還有彙編級別的優化,他們通常比帶有JIT的Java編譯出來的代碼更高效。典型的例子是:Android API 中的String.indexOf(),Dalvik出於內聯性能考慮將其替換。同樣System.arraycopy()函數也被替換,這樣的性能在Nexus One測試,比手寫的for迴圈並使用JIT還快9倍。

參見 Josh Bloch 的 《Effective Java》這本書的第47條

謹慎使用native函數

結合Android NDK使用native代碼開發,並不總是比Java直接開發的效率更好的。Java轉native代碼是有代價的,而且JIT不能在這種情況下做優化。如果你在native代碼中分配資源(比如native堆上的記憶體,文件描述符等等),這會對收集這些資源造成巨大的困難。你同時也需要為各種架構重新編譯代碼(而不是依賴JIT)。你甚至對已同樣架構的設備都需要編譯多個版本:為G1的ARM架構編譯的版本不能完全使用Nexus One上ARM架構的優勢,反之亦然。

Native 代碼是在你已經有本地代碼,想把它移植到Android平臺時有優勢,而不是為了優化已有的Android Java代碼使用。

如果你要使用JNI,請學習JNI Tips

參見 Josh Bloch 的 《Effective Java》這本書的第54條

關於性能的誤區

在沒有JIT的設備上,使用一種確切的數據類型確實要比抽象的數據類型速度要更有效率(例如,調用HashMap map要比調用Map map效率更高)。有誤傳效率要高一倍,實際上只是6%左右。而且,在JIT之後,他們直接並沒有大多差異。

在沒有JIT的設備上,讀取緩存域比直接讀取實際數據大概快20%。有JIT時,域讀取和本地讀取基本無差。所以優化並不值得除非你覺得能讓你的代碼更易讀(這對 final, static, static final 域同樣適用)。

關於測量

在優化之前,你應該決定你遇到了性能問題。你應該確保你能夠準確測量出現在的性能,否則你也不會知道優化是否真的有效。

本章節中所有的技巧都需要Benchmark(基準測試)的支持。Benchmark可以在code.google.com "dalvik" project中找到

 

Benchmark是基於Java版本的Calipermicrobenchmarking(基準微測,譯者註)框架開發的。Microbenchmarking很難做準確,所以Caliper幫你完成這部分工作,甚至還幫你測了你沒想到需要測量的部分(因為,VM幫你管理了代碼優化,你很難知道這部分優化有多大效果)。我們強烈推薦使用Caliper來做你的基準微測工作。

我們也可以用Traceview來測量,但是測量的數據是沒有經過JIT優化的,所以實際的效果應該是要比測量的數據稍微好些。

關於如何測量與調試,還可以參考下麵兩篇文章:


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 使用實例詳細闡述 日曆簽到功能 的實現方案,及 項目文件結構的設置方法
  • 前言 本篇文章精講iOS開發中使用Block時一定要註意記憶體管理問題,很容易造成迴圈引用。本篇文章的目標是幫助大家快速掌握使用block的技巧。 我相信大家都覺得使用block給開髮帶來了多大的便利,但是有很多開發者對block記憶體管理掌握得不夠好,導致經常出現迴圈引用的問題。對於新手來說,出現迴圈
  • 目錄 Android多解析度適配實踐【0】基礎適配篇(撰寫中)Android多解析度適配實踐【1】使用字體圖標,精準控制不同解析度的圖標樣式(內含兩枚神器)Android多解析度適配實踐【2】Iconify中文使用說明Android多解析度適配實踐【3】Iconify擴展,自製或導入SVG字體圖標庫
  • 序言 網路連接狀態檢測對於我們的iOS app開發來說是一個非常通用的需求。為了更好的用戶體驗,我們會在無網路時展現本地或者緩存的內容,並對用戶進行合適的提示。對絕大部分iOS開發者來說,從蘋果示例代碼改變而來的各種Reachablity框架是實現這個需求的普遍選擇,比如這個庫。但事實上,基於此方案
  • +(UIColor *)colorWithHexString:(NSString *)coloStr{ // 檢索 去下空格和換行 轉成大寫 NSString *cString = [[coloStr stringByTrimmingCharactersInSet:[NSCharacterSet w
  • 提高OC代碼質量的小心機 一、OC特性 OC 為 C 語言添加了面向對象特性,是其超集; OC 使用動態綁定的消息結構,也就是,在運行時才會檢查對象類型; 接收一條消息後,究竟應執行何種代碼,由運行期環境來決定,而非編譯器;ps:理解C語言的核心概念有助於寫好OC程式,尤其要掌握記憶體模型與指針。 二
  • 本文由CocoaChina--BYB_1132(論壇ID)翻譯 原文:Thoughts On AlamoFire--Swift’s AFNetworking Implementation HTTP協議就是現代開發的同義詞,對於有經驗的iOS開發者來說, 熟悉並儘可能使用這些流行的協議是日常工作的基礎
  • 這是一張QQ空間說說詳情的截圖。 分析: 1、點擊右上角三個點的圖標,在界面底部彈出一個區域,這個區域有一些按鈕提供給我們操作 2、當該區域出現的時候,詳情界面便灰了,也說成透明度變化了 3、當任意選了一個按鈕或者點擊了該區域以外的部分,該區域消失,灰色界面變回亮白色,並執行點擊的按鈕對應的操作 顯
一周排行
    -Advertisement-
    Play Games
  • Timer是什麼 Timer 是一種用於創建定期粒度行為的機制。 與標準的 .NET System.Threading.Timer 類相似,Orleans 的 Timer 允許在一段時間後執行特定的操作,或者在特定的時間間隔內重覆執行操作。 它在分散式系統中具有重要作用,特別是在處理需要周期性執行的 ...
  • 前言 相信很多做WPF開發的小伙伴都遇到過表格類的需求,雖然現有的Grid控制項也能實現,但是使用起來的體驗感並不好,比如要實現一個Excel中的表格效果,估計你能想到的第一個方法就是套Border控制項,用這種方法你需要控制每個Border的邊框,並且在一堆Bordr中找到Grid.Row,Grid. ...
  • .NET C#程式啟動閃退,目錄導致的問題 這是第2次踩這個坑了,很小的編程細節,容易忽略,所以寫個博客,分享給大家。 1.第一次坑:是windows 系統把程式運行成服務,找不到配置文件,原因是以服務運行它的工作目錄是在C:\Windows\System32 2.本次坑:WPF桌面程式通過註冊表設 ...
  • 在分散式系統中,數據的持久化是至關重要的一環。 Orleans 7 引入了強大的持久化功能,使得在分散式環境下管理數據變得更加輕鬆和可靠。 本文將介紹什麼是 Orleans 7 的持久化,如何設置它以及相應的代碼示例。 什麼是 Orleans 7 的持久化? Orleans 7 的持久化是指將 Or ...
  • 前言 .NET Feature Management 是一個用於管理應用程式功能的庫,它可以幫助開發人員在應用程式中輕鬆地添加、移除和管理功能。使用 Feature Management,開發人員可以根據不同用戶、環境或其他條件來動態地控制應用程式中的功能。這使得開發人員可以更靈活地管理應用程式的功 ...
  • 在 WPF 應用程式中,拖放操作是實現用戶交互的重要組成部分。通過拖放操作,用戶可以輕鬆地將數據從一個位置移動到另一個位置,或者將控制項從一個容器移動到另一個容器。然而,WPF 中預設的拖放操作可能並不是那麼好用。為瞭解決這個問題,我們可以自定義一個 Panel 來實現更簡單的拖拽操作。 自定義 Pa ...
  • 在實際使用中,由於涉及到不同編程語言之間互相調用,導致C++ 中的OpenCV與C#中的OpenCvSharp 圖像數據在不同編程語言之間難以有效傳遞。在本文中我們將結合OpenCvSharp源碼實現原理,探究兩種數據之間的通信方式。 ...
  • 一、前言 這是一篇搭建許可權管理系統的系列文章。 隨著網路的發展,信息安全對應任何企業來說都越發的重要,而本系列文章將和大家一起一步一步搭建一個全新的許可權管理系統。 說明:由於搭建一個全新的項目過於繁瑣,所有作者將挑選核心代碼和核心思路進行分享。 二、技術選擇 三、開始設計 1、自主搭建vue前端和. ...
  • Csharper中的表達式樹 這節課來瞭解一下表示式樹是什麼? 在C#中,表達式樹是一種數據結構,它可以表示一些代碼塊,如Lambda表達式或查詢表達式。表達式樹使你能夠查看和操作數據,就像你可以查看和操作代碼一樣。它們通常用於創建動態查詢和解析表達式。 一、認識表達式樹 為什麼要這樣說?它和委托有 ...
  • 在使用Django等框架來操作MySQL時,實際上底層還是通過Python來操作的,首先需要安裝一個驅動程式,在Python3中,驅動程式有多種選擇,比如有pymysql以及mysqlclient等。使用pip命令安裝mysqlclient失敗應如何解決? 安裝的python版本說明 機器同時安裝了 ...