Android App性能優化總結

優化方向

Android系統性能已經非常流暢了。但是,到了各大廠商手裏,改源碼自定系統,使得Android原生系統變得魚龍混雜,然後到了不同層次的開發工程師手裏,因爲技術水平的參差不齊,即使很多手機在跑分軟件性能非常高,打開應用依然存在卡頓現象。另外,隨着產品內容迭代,功能越來越複雜,UI頁面也越來越豐富,也成爲流暢運行的一種阻礙。綜上所述,對APP進行性能優化已成爲開發者該有的一種綜合素質,也是開發者能夠完成高質量應用程序作品的保證。

本着人道主義,一切從用戶體驗的角度去思考,當我們置身處地得把自己當做用戶去玩一款應用時候,那麼都會在意什麼呢?假如正在玩一款手遊,首先一定不希望玩着玩着突然閃退,然後就是遇到畫面內容很豐富的時候不希望卡頓與無響應,其次就是耗電和耗流量不希望太嚴重,最後就是版本更新的時候安裝包希望能小一點。好了,四個方面總結如下:

  • 穩定(降低Crash率,不要在用戶使用過程中崩潰)
  • 流暢(使用時避免出現卡頓和ANR,響應速度快,減少用戶等待的時間)
  • 耗損(節省流量和耗電,減少用戶使用成本,避免使用時導致手機發燙)
  • 安裝包(安裝包小可以降低用戶的安裝成本)

一、穩定

Android 應用的穩定性定義很寬泛,影響穩定性的原因很多,比如內存使用不合理(內存泄漏、內存溢出)、代碼異常場景考慮不周全、代碼邏輯不合理等,都會對應用的穩定性造成影響。其中代碼邏輯導致的Crash情況非常多,無法在這裏展開,最關鍵的還是內存泄漏與內存溢出導致的崩潰,我們着重優化。首先需要了解一下引用的強軟弱虛的概念。

引用的強軟弱虛

早在JDK1.2,Java就把對象的引用分爲四種級別,從而使程序能更加靈活的控制對象的生命週期。這四種級別由高到低依次爲:強引用、軟引用、弱引用和虛引用。

強引用

Java默認就是強引用。當內存不足, jvm開始垃圾回收,對於強引用的對象,就算出現OOM異常也不會對該對象進行回收,Android內存泄露大部分都是強引用導致的。雖然把object1=null,然後object2還是存在。

Object object1 = new Object();
Object object2 = object1;
object1 = null;
System.gc();
System.out.println("object2="+object2);

使用場景
Java虛擬機寧願拋出OutOfMemoryError錯誤,使程序異常終止,也不會靠隨意回收強引用的對象來解決內存不足的場景。

軟引用

軟引用是相對強引用弱點的引用,需要使用SoftReference類來實現,當系統內存充足時,它不會被回收,當系統內存不足時,它會被回收,軟引用通常使用在對內存比較敏感時使用。下面代碼雖然我把object1=null了,但是從軟引用中獲取的對象還是不爲null,這就是證明了在內存足夠時候,不會回收,如果把內存配置成了10m或者更低就會回收。

Object object1 = new Object();
SoftReference<Object> softReference = new SoftReference<>(object1);
object1=null;
System.gc();
System.out.println("object1="+object1);
System.out.println("軟引用-->"+softReference.get());

使用場景
用來處理圖片這種佔用內存大的類。

View view = findViewById(R.id.some_view);
Bitmap bitmap = BitmapFactory.decodeResource(getResources(),R.drawable.ic_launcher);
Drawable drawable = new BitmapDrawable(bitmap);
SoftReference<Drawable> drawableSoftReference = new SoftReference<Drawable>(drawable);
if(drawableSoftReference != null) {
    view.setBackground(drawableSoftReference.get());
}

這樣的好處是:通過軟引用的get()方法,取得drawable對象實例的強引用,發現對象被未回收。在GC在內存充足的情況下,不會回收軟引用對象。此時view的背景顯示。內存吃緊,系統開始會GC。這次GC後,drawables.get()不再返回Drawable對象,而是返回null,這時屏幕上背景圖不顯示,說明在系統內存緊張的情況下,軟引用被回收。使用軟引用以後,在OutOfMemory異常發生之前,這些緩存的圖片資源的內存空間可以被釋放掉的,從而避免內存達到上限,避免Crash發生。

弱引用

弱引用需要用WeakReference來實現,它比軟引用的生存期更短.不管內存是否夠用,只要gc掃描到它都會被回。下面代碼中,在gc之後object1內存會立刻被回收,weakReference.get()變爲null。

Object object1 = new Object();
WeakReference<Object> weakReference = new WeakReference<>(object1);
System.out.println("object1="+object1);
System.out.println("弱引用中的對象="+weakReference.get());
object1 = null;
System.gc();
System.out.println("object1="+object1);
System.out.println("弱引用中的對象="+weakReference.get());

使用場景
靜態內部類Handler聲明非靜態成員變量下的使用防止內存泄露。

public class MainActivity extends AppCompatActivity {
 
    private Handler handler  ;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        handler = new MyHandler( this ) ;
        ...
    }
 
    private static class MyHandler extends Handler {
        WeakReference<MainActivity> weakReference ;
 
        public MyHandler(MainActivity activity ){
            weakReference  = new WeakReference<MainActivity>( activity) ;
        }
 
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            if ( weakReference.get() != null ){
                // update android ui
            }
        }
    }
}

虛引用

虛引用的回收機制跟弱引用差不多,但是它被回收之前,會被放入ReferenceQueue中。注意哦,其它引用是被JVM回收後才被傳入ReferenceQueue中的。由於這個機制,所以虛引用大多被用於引用銷燬前的處理工作。它和沒任何引用一樣,在任何時候都有可能被垃圾回收, 它不能單獨使用也不能通過它訪問對象,虛引用必須配合ReferenceQueue一起使用,虛引用的作用主要是跟蹤對象被垃圾回收的狀態。使用例子:

Object object1 = new Object();
PhantomReference<Object> phantomReference = new PhantomReference<>(object1, new ReferenceQueue<>());

使用場景
對象銷燬前的一些操作,比如說資源釋放等。Object.finalize()雖然也可以做這類動作,但是這個方式即不安全又低效。

內存泄漏

內存泄漏通俗的講是一個本該被回收的對象卻因爲某些原因導致其不能回收 。我們都知道對象是有生命週期的,從生到死,當對象的任務完成之後,由Android系統進行垃圾回收。我們知道Android系統爲某個App分配的內存是有限的(這個可能根據機型的不同而不同),當一個應用中產生的內存泄漏比較多時,就難免會導致應用所需要的內存超過這個系統分配的內存限額,最終導致OOM(OutOfMemory)使程序崩潰。下面列舉常見的內存泄漏:

錯誤使用單例造成的內存泄漏

單例模式或者靜態方法長期持有Context對象,如果持有的Context對象生命週期與單例生命週期更短時,或導致Context無法被釋放回收,則有可能造成內存泄漏。

public class SingleManager {

    private static SingleManager mInstance;
    private Context mContext;

    private SingleManager(Context context) {
    	//持有一般的context
        this.mContext = context;
    }

    public static SingleManager getInstance(Context context) {
        if (mInstance == null) {
            synchronized (SingleManager.class) {
                if (mInstance == null) {
                    mInstance = new SingleManager(context);
                }
            }
        }
        return mInstance;
    }
}

當我們在傳入的Context是Activity時,在這個Activity銷燬之後,這個單例還持有它的引用,造成內存泄漏。解決辦法:引用的Context和AppLication的生命週期一樣或者將Context聲明成弱引用。

private SingleManager(Context context) {
	//用aplication的Context
	this.mContext = context.getApplicationContext();
	//private WeakReference<Context> mContext;
	//this.mContext = new WeakReference<>(context);
}

集合類造成內存泄漏

如果一個對象放入到ArrayList、HashMap等集合中,這個集合就會持有該對象的引用。當我們不再需要這個對象時,也並沒有將它從集合中移除,這樣只要集合還在使用(而此對象已經無用了),這個對象就造成了內存泄露。比如:

Vector v = new Vector(10);
for (int i = 1; i < 100; i++) {
    Object o = new Object();
    v.add(o);
    o = null;
}

在這個例子中,我們循環申請Object對象,並將所申請的對象放入一個 Vector 中,如果我們僅僅釋放引用本身,那麼 Vector 仍然引用該對象,所以這個對象對 GC 來說是不可回收的。因此,如果對象加入到Vector 後,還必須從 Vector 中刪除,最簡單的方法就是將 Vector 對象設置爲 null。

如果集合被靜態引用的話,集合裏面那些沒有用的對象更會造成內存泄露了。所以在使用集合時要及時將不用的對象從集合remove,或者clear集合,以避免內存泄漏。

除此之外,還有一些集合,雖然有添加和刪除的邏輯,但是由於生命週期的衝突導致內存泄漏。


public class myApplication extends Application {
	...
    private List<Activity> list = new ArrayList<>();
    
    public void addActivity(Activity activity) {
    	list.add(activity);
	}

	public void exit() {
	    try {
	        for (Activity activity : list) {
	            if (activity != null)
	                activity.finish();
	        }
	    } catch (Exception e) {
	        e.printStackTrace();
	    }
	}
}

在上面的邏輯中,在我們啓動的Activity中,調用addActivity()方法添加啓動的Activty,退出程序的地方,調用exit()方法,刪除添加的Actiivty。這樣做看似優雅,但是是使用的過程中,調用過myApplication.getInstance().addActivity(this)方法的Activity全都內存泄漏了!
爲什麼呢?這個List是在myApplication中的,這個myApplication的生命週期是整個App的生命週期,因此它自然要比單個Activity的生命週期要長。假如我們從一個Ativity A跳到了另一個Activity B,那麼A就到了後臺,假設這個時候系統內存不足了,想要回收他,卻發現有一個和APP生命週期一樣長的List還持有他的引用,完了,明明沒有用的Activity實例卻回收不了,自然就造成了內存泄漏。
所以這種看似優雅的方式,實際上使用不好就極爲不優雅。其實解決上述問題的方法也很簡單,回收不了是因爲List持有的是Activity的強引用,我們只要想辦法給list改爲成弱引用即可。

private WeakReference<List<Activity>> list = new WeakReference<>(new ArrayList<>());
//list.get().add(activity);
//list.get().get(0).finish();

WebView造成內存泄露

關於WebView的內存泄露,因爲WebView在加載網頁後會長期佔用內存而不能被釋放,因此我們在Activity銷燬後要調用它的destory()方法來銷燬它以釋放內存。
最終的解決方案是:在銷燬WebView之前需要先將WebView從父容器中移除,然後在銷燬WebView。

@Override
protected void onDestroy() {
    super.onDestroy();
    // 先從父控件中移除WebView
    mWebViewContainer.removeView(mWebView);
    mWebView.stopLoading();
    mWebView.getSettings().setJavaScriptEnabled(false);
    mWebView.clearHistory();
    mWebView.removeAllViews();
    mWebView.destroy();
}

屬性動畫造成內存泄露

動畫同樣是一個耗時任務,比如在Activity中啓動了屬性動畫(ObjectAnimator),但是在銷燬的時候,沒有調用cancle方法,雖然我們看不到動畫了,但是這個動畫依然會不斷地播放下去,動畫引用所在的控件,所在的控件引用Activity,這就造成Activity無法正常釋放。因此同樣要在Activity銷燬的時候cancel掉屬性動畫,避免發生內存泄漏。

@Override
protected void onDestroy() {
    super.onDestroy();
    mAnimator.cancel();
}

資源未關閉或者註銷造成的內存泄漏

對於使用了BraodcastReceiver,ContentObserver,File,Cursor,Stream,Sqlite,Bitmap等資源的使用,應該在Activity銷燬時及時關閉或者註銷,否則這些資源將不會被回收,造成內存泄漏。對於 Bitmap 對象在不使用時,我們應該先調用 recycle() 釋放內存,然後才它設置爲 null。
比如下面的獲取媒體庫圖片地址代碼,在查詢結束的時候一定要調用Cursor 的關閉方法防止造成內存泄漏。

String columns[] = new String[]{
	MediaStore.Images.Media.DATA, MediaStore.Images.Media._ID, MediaStore.Images.Media.TITLE, MediaStore.Images.Media.DISPLAY_NAME
};
Cursor cursor = this.getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, columns, null, null, null);
if (cursor != null) {
    int photoIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
    //顯示每張圖片的地址,但是首先要判斷一下,Cursor是否有值
    while (cursor.moveToNext()) {
        String photoPath = cursor.getString(photoIndex); //這裏獲取到的就是圖片存儲的位置信息
    }
    cursor.close();
}

Timer、TimerTask、AsyncTask等異步導致內存泄露

處理一個比較耗時的操作時,可能還沒處理結束Activity就執行了退出操作,但是此時如果這些異步依然持有對Activity的引用就會導致AsynTaskActivity無法釋放回收引發內存泄漏。

private void testAsyn(){
    asyncTask = new AsyncTask<Void, Void, Integer>() {
        @Override
        protected Integer doInBackground(Void... voids) {
            int i=0;
            //在任務沒有結束的時候會一直持有Activity的引用
            while (!isCancelled()){
                i++;
                if(i>10000000000l){
                    break;
                }
            }
            return i;
        }

        @Override
        protected void onPostExecute(Integer integer) {
            super.onPostExecute(integer);
            mTextView.setText(String.valueOf(integer));
        }
    };
    asyncTask.execute();
    //或者無限循環任務,mTimerTask中會一直持有Activity的引用
    mTimer.schedule(mTimerTask, 3000, 3000);
}

由於它們一直持有Activity的引用不能被回收,因此當我們Activity銷燬的時候要立即cancel掉Timer、TimerTask、AsyncTask等異步操作,以避免發生內存泄漏。

Handler造成的內存泄漏

通過內部類的方式創建mHandler對象,此時mHandler會隱式地持有一個外部類對象引用這裏就是HandlerActivity,當執行postDelayed方法時,該方法會將你的Handler裝入一個Message,並把這條Message推到MessageQueue中,MessageQueue是在一個Looper線程中不斷輪詢處理消息,那麼當這個Activity退出時消息隊列中還有未處理的消息或者正在處理消息,而消息隊列中的Message持有mHandler實例的引用,mHandler又持有Activity的引用,所以導致該Activity的內存資源無法及時回收,引發內存泄漏。

public class HandlerActivity extends AppCompatActivity {

    private Handler mHandler = new Handler();
    private TextView mTextView;
    
    private void textHandler() {
        //當退出Activity時Handler任務還沒執行完畢就會造成內存泄漏.
        mHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                mTextView.setText("ceshi");
            }
        },60*1000);
    }
}

要想避免Handler引起內存泄漏問題,需要我們在Activity關閉退出的時候的移除消息隊列中所有消息和所有的Runnable。

@Override
protected void onDestroy() {
    super.onDestroy();
    mHandler.removeCallbacksAndMessages(null);
    mHandler=null;
}

非靜態內部類創建靜態實例造成的內存泄漏

內部類都會持有一個外部類引用,這裏這個外部類就是MainActivity,然而內部類實例又是static靜態變量其生命週期與Application生命週期一樣,所以在MainActivity關閉的時候,內部類靜態實例依然持有對MainActivity的引用,導致MainActivity無法被回收釋放,引發內存泄漏。

public class MainActivity extends AppCompatActivity {

	private static TestResource mResource = null;
	
	@Override    
	protected void onCreate(Bundle savedInstanceState) {
	    super.onCreate(savedInstanceState);
	    setContentView(R.layout.activity_main);
	    if(mManager == null){
	    	mManager = new TestResource();
	    }
	}
	
	class TestResource {
	 	...
	}
} 

對於這種泄漏的解決辦法就是將內部類改成靜態內部類,不再持有MainActivity的引用即可,修改後的代碼如下:

static class TestResource {
	 ...
}

匿名內部類造成的內存泄漏

非靜態匿名內部類會默認持有外部類的引用,因此這個新創建的線程會持有 MainActivity 的引用。而如果要銷燬這個 Activity 之前,線程還在運行的話就會造成該線程持有 MainActivity 的引用,造成 MainActivity 的資源無法回收導致內存泄漏。

public class MainActivity extends Activity {  

    private void exampleOne() {  
    	//匿名內部類,非靜態的匿名類會持有外部類的一個隱式引用
        new Thread() {              		
			@Override  
            public void run() {  
                ...
            }  
        }.start();  
    }  
}

解決方案:將非靜態匿名內部類修改爲靜態匿名內部類

 private static void exampleOne() {  
    	//匿名內部類,非靜態的匿名類會持有外部類的一個隱式引用
        new Thread() {              		
			@Override  
            public void run() {  
                ...
            }  
        }.start();  
    }  

內存溢出

Dalvik 主要管理的內存有 Java heap 和 native heap 兩大塊,而對於一個安卓應用來說,由於手機設備的限制,一般應用使用的RAM不能超過某個設定值,如果你想要分配超過大於該分配值的內存的話,就會報Out Of Memory 錯誤。不同產商默認值不太一樣,一般常見的有16M,24M,32M,48M。也就是說app的 Java heap + native heap < 默認值就不會內存溢出。查看內存限制的方法:

//查詢當前APP的Heap Size閾值,單位是MB
int maxMemoryMB = ActivityManager.getMemoryClass();
//查看每個應用程序最高可用內存是多少,單位是KB
int maxMemoryKB = (int) (Runtime.getRuntime().maxMemory() / 1024);  

內存溢出跟內存泄漏有着密不可分的聯繫,當足夠多的內存泄漏必然會導致內存溢出,這個上面已經討論過,這裏重點討論怎麼優化大內存使用場景防止內存溢出。

圖片的內存消耗

圖片時最常用也是最容易導致OOM的對象,在做優化之前需要科普一下Android中圖片的基礎知識。

常見的顏色模型有RGB、YUV、CMYK等,在大多數圖像API中採用的都是RGB模型,Android也是如此;另外,在Android中還有包含透明度Alpha的顏色模型,即ARGB。在不考慮透明度的情況下,一個像素點的顏色值在計算機中的表示方法有以下3種:

  • 浮點數編碼:比如float: (1.0, 0.5, 0.75),每個顏色分量各佔1個float字段,其中1.0表示該分量的值爲全紅或全綠或全藍;
  • 24位的整數編碼:比如24-bit:(255, 128, 196),每個顏色分量各佔8位,取值範圍0-255,其中255表示該分量的值爲全紅或全綠或全藍;
  • 16位的整數編碼:比如16-bit:(31, 45, 31),第1和第3個顏色分量各佔5位,取值範圍0-31,第2個顏色分量佔6位,取值範圍0-63;

在Java中,float類型的變量佔32位,int類型的變量佔32位,short和char類型的變量都在16位,因此可以看出,用浮點數表示法編碼一個像素的顏色,內存佔用量是96位即12字節;而用24位整數表示法編碼,只要一個int類型變量,佔用4個字節(高8位空着,低24位用於表示顏色);用16位整數表示法編碼,只要一個short類型變量,佔2個字節;因此可以看出採用整數表示法編碼顏色值,可以大大節省內存,當然,顏色質量也會相對低一些。在Android中獲取Bitmap的時候一般也採用整型編碼。

以上2種整型編碼的表示法中,R、G、B各分量的順序可以是RGB或BGR,Android裏採用的是RGB的順序,本文也都是遵循此順序來討論。在24位整型表示法中,由於R、G、B分量各佔8位,有時候業內也以RGB888來指代這一信息;類似的,在16位整型表示法中,R、G、B分量分別佔5、6、5位,就以RGB565來指代這一信息。

現在再考慮有透明度的顏色編碼,其實方式與無透明度的編碼方式一樣:24位整型編碼RGB模型採用int類型變量,其閒置的高8位正好用於放置透明度分量,其中0表示全透明,255表示完全不透明;按照A、R、G、B的順序,就可以以ARGB8888來概括這一情形;而16位整型編碼的RGB模型採用short類型變量,調整各分量所佔爲數分別至4位,那麼正好可以空出4位來編碼透明度值;按照A、R、G、B的順序,就可以以ARGB4444來概括這一情形。回想一下Android的BitmapConfig類中,有ARGB_8888、ARGB_4444、RGB565等常量,現在可以知道它們分別代表了什麼含義。同時也可以計算一張圖片在內存中可能佔用的大小,比如計算一張1920*1200的圖片佔用內存:

//ARGB_8888/ARGB_888
1920*1200*4/1024/1024=8.79MB
//ARGB_4444/RGB565
1920*1200*2/1024/1024=4.395MB

圖片壓縮優化

在展示高分辨率圖片的時候,最好先將圖片進行壓縮。壓縮後的圖片大小應該和用來展示它的控件大小相近,在一個很小的ImageView上顯示一張超大的圖片不會帶來任何視覺上的好處,但卻會佔用我們相當多寶貴的內存,而且在性能上還可能會帶來負面影響。圖片的操作重點講解BitmapFactory.Options這個類,將這個參數的inJustDecodeBounds屬性設置爲true就可以讓解析方法禁止爲bitmap分配內存,返回值也不再是一個Bitmap對象,而是null。雖然Bitmap是null了,但是BitmapFactory.Options的outWidth、outHeight和outMimeType屬性都會被賦值。這個技巧讓我們可以在加載圖片之前就獲取到圖片的長寬值和MIME類型,從而根據情況對圖片進行壓縮。通過設置BitmapFactory.Options中inSampleSize的值就可以實現倍數壓縮:

//reqWidth和reqHeight爲實際要顯示控件的寬高值
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) {  
    // 第一次解析將inJustDecodeBounds設置爲true,來獲取圖片大小  
    final BitmapFactory.Options options = new BitmapFactory.Options();  
    options.inJustDecodeBounds = true;  
    BitmapFactory.decodeResource(res, resId, options);  
    // 調用上面定義的方法計算inSampleSize值  
    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);  
    // 使用獲取到的inSampleSize值再次解析圖片  
    options.inJustDecodeBounds = false;  
    return BitmapFactory.decodeResource(res, resId, options);  
} 

public static int calculateInSampleSize(BitmapFactory.Options options,  int reqWidth, int reqHeight) {  
    // 源圖片的高度和寬度  
    final int height = options.outHeight;  
    final int width = options.outWidth;  
    //String imageType = options.outMimeType; 
    int inSampleSize = 1;  
    if (height > reqHeight || width > reqWidth) {  
        // 計算出實際寬高和目標寬高的比率  
        final int heightRatio = Math.round((float) height / (float) reqHeight);  
        final int widthRatio = Math.round((float) width / (float) reqWidth);  
        // 選擇寬和高中最小的比率作爲inSampleSize的值,這樣可以保證最終圖片的寬和高  
        // 一定都會大於等於目標的寬和高。  
        inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;  
    }  
    return inSampleSize;  
} 

圖片緩存優化

在你應用程序的UI界面加載一張圖片是一件很簡單的事情,但是當你需要在界面上加載一大堆圖片的時候,情況就變得複雜起來。在很多情況下,(比如使用RecyclerView、ListView、GridView 或者 ViewPager 這樣的組件),屏幕上顯示的圖片可以通過滑動屏幕等事件不斷地增加,最終導致OOM。爲了保證內存的使用始終維持在一個合理的範圍,通常會把被移除屏幕的圖片進行回收處理。此時垃圾回收器也會認爲你不再持有這些圖片的引用,從而對這些圖片進行GC操作。用這種思路來解決問題是非常好的,可是爲了能讓程序快速運行,在界面上迅速地加載圖片,你又必須要考慮到某些圖片被回收之後,用戶又將它重新滑入屏幕這種情況。這時重新去加載一遍剛剛加載過的圖片,頻繁申請內存釋放內存,增加了內存的開銷,我們應該想辦法去避免這個情況的發生。

這個時候,使用內存緩存技術可以很好的解決這個問題,它可以讓組件快速地重新加載和處理圖片。市面緩存技術很多,Picasso爲追求小而美,有功能取捨,比如,它無法支持下載動態圖片。還有Google的Glide或Facebook的Fresco,它們各有特點,Glide比較小巧,Fresco性能好。如果自己實現的話,可以使用一種叫作LRU(least recently used,最近最少使用)的存儲策略。基於該種策略,當存儲空間用盡時,緩存會自動清除最近最少使用的對象。

初始化LruCache來緩存Bitmap

LruCache<String, Bitmap> mLruCache;
//獲取手機最大內存,單位kb
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
//一般都將1/8設爲LruCache的最大緩存
int cacheSize = maxMemory / 8;
mLruCache = new LruCache<String, Bitmap>(maxMemory / 8) {
    /**
    * 這個方法從源碼中看出來是設置已用緩存的計算方式的
    * 默認返回的值是 1,也就是每緩存一張圖片就將已用緩存大小加 1
    * 緩存圖片看的是佔用的內存的大小,每張圖片的佔用內存也是不一樣的
    * 因此要重寫這個方法,手動將這裏改爲本次緩存的圖片的大小
    */
    @Override
    protected int sizeOf(String key, Bitmap value) {
        return value.getByteCount() / 1024;
    }
};

使用

//加入緩存
mLruCache.put("key", bitmap);
//從緩存中讀取
Bitmap bitmap = mLruCache.get("key");

臨時Bitmap的及時回收

臨時創建的Bitmap用完之後一定不要忘記主動釋放內存

if(bitmap != null) {
	bitmap.recycle();
}

使用合適的數據結構

Android爲移動操作系統特意編寫了一些更加高效的容器SparseArray和 ArrayMap,在特定情況下用來替換HashMap數據結構。合理運用這些數據結構將爲我們節省內存空間。下面我們進行對比:

HashMap

創建一個 hashMap時, 默認是一個容量爲16的數組,數組中的每一個元素卻又是一個鏈表的頭節點。或者說HashMap內部存儲結構 是使用哈希表的拉鍊結構(數組+鏈表,這種存儲數據的方法 叫做拉鍊法。在這裏插入圖片描述
缺點

  • 就算沒有數據,也需要分配默認16個元素的數組
  • 一旦數據量達到Hashmap限定容量的75%,就將按兩倍擴容,當我們有幾十萬、幾百萬數據時,hashMap 將造成內存空間的消耗和浪費。

優點

  • 數據較大的時候(1000級別),hash查找效率比二分法高

HashMap獲取數據是通過遍歷Entry[]數組來得到對應的元素,在數據量很大時候會比較慢,所以在Android中,HashMap是比較費內存的,我們在一些情況下可以使用SparseArray和ArrayMap來代替HashMap。

SparseArray

SparseArray比HashMap更省內存,默認初始size爲0,每次增加元素,size++。在某些條件下性能更好,主要是因爲它避免了對key的自動裝箱(int轉爲Integer類型)。它內部則是通過兩個數組來進行數據存儲的,一個存儲key,另外一個存儲value,爲了優化性能,它內部對數據還採取了壓縮的方式來表示稀疏數組的數據,從而節約內存空間。而在獲取數據的時候,也是使用二分查找法判斷元素的位置,所以,在獲取數據的時候非常快,但是在數據量大的情況下性能並不明顯,將降低至少50%。刪除元素的時候,咱們先不刪除,通過value賦值的方式,給它貼一個標籤,然後我們再gc的時候再根據這個標誌進行壓縮和空間釋放,在添加元素的時候,我們發現如果對應的元素正好被標記了“刪除”,那麼我們直接覆蓋它即可,在效率上是一個很可觀的提高。

優點

  • 佔用內存小
  • 放棄hash查找,使用二分查找,1000級別以下效率更高。
  • 頻繁的插入刪除操作效率高(延遲刪除機制保證了效率)
  • 避免存取元素時的裝箱和拆箱,性能更好

缺點

  • 二分查找的時間複雜度O(log n),1000級別以上數據量下,效率沒有HashMap高
  • key只能是int 或者long

ArrayMap

ArrayMap是一個<key,value>映射的數據結構,key可以爲任意的類型,它設計上更多的是考慮內存的優化,內部是使用兩個數組進行數據存儲,一個數組記錄key的hash值,另外一個數組記錄Value值,它和SparseArray一樣,也會對key使用二分法進行從小到大排序,在添加、刪除、查找數據的時候都是先使用二分查找法得到相應的index,然後通過index來進行添加、查找、刪除等操作,所以,應用場景和SparseArray的一樣,如果在數據量比較大的情況下,那麼它的性能將退化至少50%。

優點

  • 1000以內數據量,內存利用率高,及時的空間壓縮機制
  • 迭代效率高,可以使用索引來迭代(keyAt()方法以及valueAt() 方法),相比於HashMap迭代使用迭代器模式,效率要高很多
  • key可以爲任意的類型

缺點

  • 二分查找的時間複雜度O(log n),1000級別以上數據量下,效率沒有HashMap高
  • 沒有實現Serializable,不利於在Android中藉助Bundle傳輸。
  • 存取數據複雜度高,花費大

應用場景

  • 首先二者都是適用於數據量小(1000以內)的情況,但是SparseArray以及他的三兄弟們避免了自動裝箱和拆箱問題,也就是說在特定場景下,比如你存儲的value值全部是int類型,並且key也是int類型,那麼就採用SparseArray,其它情況就採用ArrayMap。
  • 數據量多的時候當然還是使用HashMap啦

不要使用Enum枚舉

Android官方的Training課程裏面有下面這樣一句話:

Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.

Android官方強烈建議不要在Android程序裏面使用到enum,因爲會比靜態增加兩倍以上的內存。enum編譯生成dex後的class文件,每個變量都是一個對象,並且還有一個value對象數組,不僅吃內存,還吃字節數,加大apk的大小。可用註解替代枚舉。

private static final int ADD = 0;
private static final int SUB = 1;
private static final int MUL = 2;
private static final int DIV = 3;

@IntDef({ADD,SUB,MUL,DIV})
@Retention(RetentionPolicy.SOURCE)
public @interface Operation{}

public void text(@Operation int operation){
	//...
}
text(ADD);

Try catch某些大內存分配的操作

在某些情況下,我們需要事先評估那些可能發生OOM的代碼,對於這些可能發生OOM的代碼,加入catch機制,可以考慮在catch裏面嘗試一次降級的內存分配操作。例如decode bitmap的時候,catch到OOM,可以嘗試把採樣比例再增加一倍之後,再次嘗試decode。

不要使用String進行字符串拼接

使用String的“+”拼接,每次都會生成一個StringBuilder對象,當大量操作的時候會有大量的內存垃圾產生,會導致OOM以及內存抖動(頻繁創建對象以及頻繁GC)。

頻繁的字符串拼接,使用StringBuffer或者StringBuilder代替String,可以在一定程度上避免OOM和內存抖動。

二、流暢

Android 應用啓動慢,使用時經常卡頓,是非常影響用戶體驗的,應該儘量避免出現。總的來說造成卡頓的原因有如下幾種:

  • UI的繪製上

繪製的層級深、頁面複雜、刷新不合理與過度繪製,由於這些原因導致卡頓的場景更多出現在 UI 和啓動後的初始界面以及跳轉到頁面的繪製上。

  • 數據處理上

導致這種卡頓場景的原因是數據處理量太大,一般分爲三種情況,一是耗時數據在主線程處理,這個是初級工程師會犯的錯誤。二是數據處理佔用 CPU 高,導致主線程拿不到時間片,三是內存增加導致 GC 頻繁,從而引起卡頓。

ANR

一般輕微的卡頓還好,如果一些比較嚴重的卡頓會造成ANR,全稱:Application Not Responding,也就是應用程序無響應。

Android系統中,ActivityManagerService(簡稱AMS)和WindowManagerService(簡稱WMS)會檢測App的響應時間,如果App在特定時間無法相應屏幕觸摸或鍵盤輸入時間,或者特定事件沒有處理完畢,就會出現ANR。產生ANR後,會有Log日誌以及生成“/data/anr/traces.txt”日誌文件。

造成ANR發生的情況

  • InputDispatching Timeout:5秒內無法響應屏幕觸摸事件或鍵盤輸入事件
  • BroadcastQueue Timeout:在執行前臺廣播(BroadcastReceiver)的+ onReceive()函數時10秒沒有處理完成,後臺爲60秒。
  • Service Timeout:前臺服務20秒內,後臺服務在200秒內沒有執行完畢。
  • ContentProvider Timeout:ContentProvider的publish在10s內沒進行完。

ANR重現

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_anr_test);
    // 這是Android提供線程休眠函數,與Thread.sleep()最大的區別是
    // 該使用該函數不會拋出InterruptedException異常。
    SystemClock.sleep(20 * 1000);
}

traces.txt日誌分析

Cmd line: com.yhd.anrtest ----> ANR產生包名
...
at java.lang.Thread.sleep(Native method) ----> 產生ANR的基礎類方法
...
at android.os.SystemClock.sleep(SystemClock.java:122) ----> 產生ANR的方法
at com.yhd.anrtest.ANRTestActivity.onCreate(ANRTestActivity.java:20) ----> 產生ANR的行數
...

通過上方日誌很容易看出原因,但是特別注意的是:產生新的ANR,原來的 traces.txt 文件會被覆蓋,也就是traces.txt只保留最後一次發生ANR時的信息。如果想查看歷史日誌,可以查看DropBox。DropBox保留歷史上發生的所有ANR的log。“/data/system/dropbox”是DB指定的文件存放位置。日誌保存的最長時間, 默認是3天。

shell@yinhaide:/data/system/dropbox # ls
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]

要討論解決卡頓的方案,就要先了解 Android 系統的渲染機制。

Android渲染機制

我們首先需要知道一個大概是生物領域的一個知識點

人眼與大腦之間的協作無法感知超過60fps的畫面更新。12fps大概類似手動快速翻動書籍的幀率,這明顯是可以感知到不夠順滑的。24fps使得人眼感知的是連續線性的運動,這其實是歸功於運動模糊的 效果。24fps是電影膠圈通常使用的幀率,因爲這個幀率已經足夠支撐大部分電影畫面需要表達的內容,同時能夠最大的減少費用支出。但是低於30fps是 無法順暢表現絢麗的畫面內容的,此時就需要用到60fps來達到想要的效果,當然超過60fps是沒有必要的(據說Dart能夠帶來120fps的體驗)。

大多數用戶感知到的卡頓等性能問題的最主要根源都是因爲渲染性能。從設計師的角度,他們希望App能夠有更多的動畫,圖片等時尚元素來實現流暢的用 戶體驗。但是Android系統很有可能無法及時完成那些複雜的界面渲染操作。Android系統每隔16ms發出VSYNC信號,觸發對UI進行渲染, 如果每次渲染都成功,這樣就能夠達到流暢的畫面所需要的60fps,爲了能夠實現60fps,這意味着程序的大多數操作都必須在16ms內完成。
在這裏插入圖片描述
如果你的某個操作花費時間是24ms,系統在得到VSYNC信號的時候就無法進行正常渲染,這樣就發生了丟幀現象。那麼用戶在32ms內看到的會是同一幀畫面。
在這裏插入圖片描述
用戶容易在UI執行動畫或者滑動ListView的時候感知到卡頓不流暢,是因爲這裏的操作相對複雜,容易發生丟幀的現象,從而感覺卡頓。有很多原 因可以導致丟幀,也許是因爲你的layout太過複雜,無法在16ms內完成渲染,有可能是因爲你的UI上有層疊太多的繪製單元,還有可能是因爲動畫執行 的次數過多。這些都會導致CPU或者GPU負載過重。VSync機制就像是一臺轉速固定的發動機(60轉/s)。每一轉會帶動着去做一些UI相關的事情,但不是每一轉都會有工作去做(就像有時在空擋,有時在D檔)。有時候因爲各種阻力某一圈工作量比較重超過了16.6ms,那麼這臺發動機這秒內就不是60轉了,當然也有可能被其他因素影響,比如給油不足(主線程裏乾的活太多)等等,就會出現轉速降低的狀況。我們把這個轉速叫做流暢度。當流暢度越小的時候說明當前程序越卡頓。

我們可以通過一些工具來定位問題,比如可以使用HierarchyViewer來查找Activity中的佈局是否過於複雜,也可以使用手機設置裏 面的開發者選項,打開Show GPU Overdraw等選項進行觀察。你還可以使用TraceView來觀察CPU的執行情況,更加快捷的找到性能瓶頸。

UI的繪製優化減少卡頓

Overdraw的理解與優化建議

Overdraw(過度繪製)描述的是屏幕上的某個像素在同一幀的時間內被繪製了多次。在多層次的UI結構裏面,如果不可見的UI也在做繪製的操作,這就會導致某些像素區域被繪製了多次。這就浪費大量的CPU以及GPU資源。
在這裏插入圖片描述
當設計上追求更華麗的視覺效果的時候,我們就容易陷入採用越來越多的層疊組件來實現這種視覺效果的怪圈。這很容易導致大量的性能問題,爲了獲得最佳的性能,我們必須儘量減少Overdraw的情況發生。幸運的是,我們可以通過手機設置裏面的開發者選項,打開Show GPU Overdraw的選項,可以觀察UI上的Overdraw情況。

設置 -> 開發者選項 -> 調試GPU過度繪製 -> 顯示GPU過度繪製

在這裏插入圖片描述
藍色,淡綠,淡紅,深紅代表了4種不同程度的Overdraw情況,我們的目標就是儘量減少紅色Overdraw,看到更多的藍色區域。Overdraw有時候是因爲你的UI佈局存在大量重疊的部分,還有的時候是因爲非必須的重疊背景。例如某個Activity有一個背景,然後裏面 的Layout又有自己的背景,同時子View又分別有自己的背景。僅僅是通過移除非必須的背景圖片,這就能夠減少大量的紅色Overdraw區域,增加 藍色區域的佔比。這一措施能夠顯著提升程序性能。

Overdraw 的處理方案一:移除不必要的background

在這裏插入圖片描述

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@android:color/white"
    android:orientation="horizontal">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@android:color/white"
        android:orientation="horizontal">
        <ImageView
            android:id="@+id/chat_author_avatar1"
            android:layout_width="@dimen/left_width_height"
            android:layout_height="@dimen/left_width_height"
            android:layout_margin="@dimen/around_margin"
            android:src="@mipmap/ic_launcher"/>
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@android:color/white"
            android:orientation="vertical">
            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="@android:color/white"
                android:text="@string/up_text" />
            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="@android:color/white"
                android:text="@string/down_text"/>
        </LinearLayout>
    </LinearLayout>
</LinearLayout>

我們發現,不居中確實出現了好多次的backgound的配置,所以移除一些不必要的background:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal">
    <ImageView
        android:id="@+id/chat_author_avatar1"
        android:layout_width="@dimen/left_width_height"
        android:layout_height="@dimen/left_width_height"
        android:layout_margin="@dimen/around_margin"
        android:src="@mipmap/ic_launcher"/>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@string/up_text" />
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@string/down_text"/>
    </LinearLayout>
</LinearLayout>

在這裏插入圖片描述
明顯有改善了,所以在我們實際生產中,需要非常有耐心的去不斷診斷,不斷去調試,有些時候我們使用ImageView的時候,可能會給它設置一個background,在代碼中,還有可能給它設了一個imageDrawable,從而發生過度繪製的情況,切記切記。解決方案是把背景圖和真正加載的圖片都通過imageDrawable方法進行設置。

還有一個注意點,我們的這個Activity對應的layout佈局最終會添加在DecorView中,因爲可視部分是Activity,如果這個DecorView會的的背景沒有必要的話,我們可以調用mDecor.setWindowBackground(drawable)去掉無關背景,那麼可以在Activity調用getWindow().setBackgroundDrawable(null)。

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
	setContentView(R.layout.activity_main);
	//清除DecorView的背景色
	getWindow().setBackgroundDrawable(null);
}

Overdraw 的處理方案二:clipRect的妙用

對於那些過於複雜的自定義的View(重寫了onDraw方法),Android系統無法檢測具體在onDraw裏面會執行什麼操作,系統無法監控並自動優化,也就無法避免Overdraw了。但是我們可以通過canvas.clipRect()來 幫助系統識別那些可見的區域。這個方法可以指定一塊矩形區域,只有在這個區域內纔會被繪製,其他的區域會被忽視。這個API可以很好的幫助那些有多組重疊 組件的自定義View來控制顯示的區域。同時clipRect方法還可以幫助節約CPU與GPU資源,在clipRect區域之外的繪製指令都不會被執 行,那些部分內容在矩形區域內的組件,仍然會得到繪製。

在這裏插入圖片描述
上圖書開啓Show Override GPU之後的效果,可以看到,卡片疊加處明顯的過度渲。

public class CardView extends View{

    private Bitmap[] mCards = new Bitmap[3];
    private int[] mImgId = new int[]{R.drawable.alex, R.drawable.chris, R.drawable.claire};

    public CardView(Context context) {
        super(context);
        Bitmap bm = null;
        for (int i = 0; i < mCards.length; i++){
            bm = BitmapFactory.decodeResource(getResources(), mImgId[i]);
            mCards[i] = Bitmap.createScaledBitmap(bm, 400, 600, false);
        }
        setBackgroundColor(0xff00ff00);
    }

    @Override
    protected void onDraw(Canvas canvas){
        super.onDraw(canvas);
        canvas.save();
        canvas.translate(20, 120);
        for (Bitmap bitmap : mCards){
            canvas.translate(120, 0);
            canvas.drawBitmap(bitmap, 0, 0, null);
        }
        canvas.restore();
    }
}

考慮下如何去優化,其實很明顯哈,我們上面已經說了使用cliprect方法,那麼我們目標直指自定義View的onDraw方法。 修改後的代碼:

@Override
protected void onDraw(Canvas canvas){
    super.onDraw(canvas);
    canvas.save();
    canvas.translate(20, 120);
    for (int i = 0; i < mCards.length; i++){
        canvas.translate(120, 0);
        canvas.save();
        if (i < mCards.length - 1){
            canvas.clipRect(0, 0, 120, mCards[i].getHeight());
        }
        canvas.drawBitmap(mCards[i], 0, 0, null);
        canvas.restore();
    }
    canvas.restore();
}

分析得出,除了最後一張需要完整的繪製,其他的都只需要繪製部分;所以我們在循環的時候,給i到n-1都添加了clipRect的代碼。最後的效果圖:

在這裏插入圖片描述
可以看到,所有卡片變爲了淡紫色,對比參照圖,都是1X過度繪製,那麼是因爲我的View添加了一個ff00ff00的背景,可以說明已經是最優了。
如果你按照上面的修改,會發現最終效果圖不是淡紫色,而是青色(2X),那是爲什麼呢?因爲你還忽略了 一個優化的地方,本View已經有了不透明的背景,完全可以移除Window的背景了,即在Activity中,添加getWindow().setBackgroundDrawable(null)。

除了clipRect方法之外,我們還可以使用canvas.quickreject()來判斷是否沒和某個矩形相交,從而跳過那些非矩形區域內的繪製操作,這裏就不多講了。

Overdraw 的處理方案三:巧用Hierarchy Viewe

我們平時在做UI佈局優化的時候,時常提起的一個工具Hierarchy Viewer,它提供了一個很直觀的可視化界面來觀測佈局界面的層級,可以檢查佈局層次結構中每個視圖的屬性和佈局速度。它可以幫助我們查找由視圖層次結構導致的性能瓶頸,從而幫助我們簡化層次結構並減少過度繪製(Overdraw)的問題。
在這裏插入圖片描述
具體怎麼使用這就不多講,讀者自行查找資料~

使用include 和merge標籤減少佈局嵌套

相信大家使用的最多的佈局標籤就是 include了。include的用途就是將佈局中的公共部分提取出來以供其他Layout使用,從而實現佈局的優化。佈局複用的步驟大致爲:

  • 1、創建一個正常的可用佈局layout文件A_layout.xml
<merge xmlns:android="http://schemas.android.com/apk/res/android">
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="張三" />
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="李四" />
</merge>
  • 2、在需要添加複用佈局(A_layout.xml)的當前佈局內B_layout.xml,使用include標籤
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal">
    <include layout="@layout/A_layout"/>
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/hello_world" />
</LinearLayout>

include使用起來很簡單,只需要指定一個layout屬性爲需要包含的佈局文件即可。如果include設置id後,原有的根佈局Id已經被替換爲在 include中指定的id了,所以在 findViewById查找原有id的時候就會報空指針異常。

注意點:

  • 使用include的時候可以不用填寫“android:layout_width”與“android:layout_height”兩個參數。
  • 非layout屬性則無法在include標籤當中進行覆寫。
  • 如果我們想要在include標籤當中覆寫layout屬性,必須要將layout_width和layout_height這兩個屬性也進行覆寫,否則覆寫效果將不會生效。
  • merge標籤是作爲include標籤的一種輔助擴展來使用的,它的主要作用是爲了防止在引用佈局文件時產生多餘的佈局嵌套。
  • merge必須放在佈局文件的根節點上;
  • merge並不是一個ViewGroup,也不是一個View,它相當於聲明瞭一些視圖,等待被添加。
  • merge標籤被添加到A容器下,那麼merge下的所有視圖將被添加到A容器下。
  • 因爲merge標籤並不是View,所以在通過LayoutInflate.inflate方法渲染的時候, 第二個參數必須指定一個父容器,且第三個參數必須爲true,也就是必須爲merge下的視圖指定一個父親節點。
  • 如果Activity的佈局文件根節點是FrameLayout,可以替換爲merge標籤,這樣,執行setContentView之後,會減少一層FrameLayout節點。
  • 自定義View如果繼承LinearLayout,建議讓自定義View的佈局文件根節點設置成merge,這樣能少一層結點。
  • 因爲merge不是View,所以對merge標籤設置的所有屬性都是無效的。

使用ViewStub懶加載減少渲染元素

ViewStub標籤實質上是一個寬高都爲 0 的不可見 View. 通過延遲加載佈局的方式優化佈局提升渲染性能.

這裏的延遲加載是指初始化時, 程序無需顯示該標籤所指向的佈局文件, 只有在特定的條件下, 所指向的佈局文件才需要被渲染, 且此佈局文件直接將當前的 ViewStub替換掉. 但這裏的替換並不是完全意義上的替換, 佈局文件的 layout params 是以 ViewStub 爲優先.

當初次渲染布局文件時, ViewStub 控件雖然也佔據內存, 但是相比於其他控件, 它所佔內存很小. 它主要是作爲一個“佔位符”, 放置於 View Tree中, 且它本身是不可見的.

使用場景

通常用於不常使用的控件. 比如

  • 網絡請求失敗的提示
  • 列表爲空的提示
  • 新內容、新功能的引導, 因爲引導基本上只顯示一次
  • 通用的自定義 View,但其中部分子 View 只在部分情況下才顯示.

下面以在一個佈局main.xml中加入網絡錯誤時的提示頁面network_error.xml爲例。main.mxl代碼如下:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    <ViewStub
        android:id="@+id/network_error_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout="@layout/network_error" />
</RelativeLayout>

其中network_error.xml爲只有在網絡錯誤時才需要顯示的佈局,默認不會被解析,示例代碼如下:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    <Button
        android:id="@+id/network_setting"
        android:layout_width="@dimen/dp_160"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:text="@string/network_setting" />
    <Button
        android:id="@+id/network_refresh"
        android:layout_width="@dimen/dp_160"
        android:layout_height="wrap_content"
        android:layout_below="@+id/network_setting"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="@dimen/dp_10"
        android:text="@string/network_refresh" />
</RelativeLayout>

在代碼中通過(ViewStub)findViewById(id)找到ViewStub,通過stub.inflate()展開ViewStub,然後得到子View,如下:

private View networkErrorView;

private void showNetError() {
  if (networkErrorView != null) {
    networkErrorView.setVisibility(View.VISIBLE);
  }else{
    ViewStub stub = (ViewStub)findViewById(R.id.network_error_layout);
    if(stub !=null){
      networkErrorView = stub.inflate();
      //  下面兩行代碼效果和上面一行是一樣的
      //  ViewStub被展開後的佈局所替換,setVisibility首次執行會觸發stub.inflate
      //  stub.setVisibility(View.VISIBLE);   
      //  networkErrorView =  findViewById(R.id.network_error_layout); // 獲取展開後的佈局
    }
 }
}

private void showNormal() {
  if (networkErrorView != null) {
    networkErrorView.setVisibility(View.GONE);
  }
}

在上面showNetError()中展開了ViewStub,同時我們對networkErrorView進行了保存,這樣下次不用繼續inflate。這裏我對ViewStub的實例進行了一個非空判斷,這是因爲ViewStub在XML中定義的id只在一開始有效,一旦ViewStub中指定的佈局加載之後,這個id也就失敗了,那麼此時findViewById()得到的值也會是空。

要被加載的佈局通過 android:layout 屬性來設置. 然後在程序中調用 inflate() 方法來加載. 還可以設定 Visibility 爲 VISIBLE 或 INVISIBLE, 也會觸發 inflate(). 但只有直接使用 inflate() 方法能返回佈局文件的根 View. 但是這裏只會在首次使用 setVisibility() 會加載要渲染的佈局文件. 再次使用只是單純的設置可見性。對 inflate() 操作也只能進行一次, 因爲 inflate() 的時候是其指向的佈局文件替換掉當前 ViewStub標籤. 之後, 原來的佈局文件中就沒有ViewStub標籤了. 因此, 如果多次 inflate() 操作, 會報錯:

ViewStub must have a non-null ViewGroup viewParent

注意

  • ViewStub所加載的佈局不可以使用merge標籤。

merge是include的輔助類,ViewStub不能使用。因此這有可能導致加載出來的佈局存在着多餘的嵌套結構,具體如何去取捨就要根據各自的實際情況來決定了,對於那些隱藏的佈局文件結構相當複雜的情況,使用ViewStub還是一種相當不錯的選擇的,即使增加了一層無用的佈局結構,仍然還是利大於弊。

ViewStub vs View.GONE

我們經常會遇到這樣的情況,運行時動態根據條件來決定顯示哪個View或佈局。常用的做法是把View都寫在上面,先把它們的可見性都設爲View.GONE,然後在代碼中動態的更改它的可見性。這樣的做法的優點是邏輯簡單而且控制起來比較靈活,不佔位的隱藏,內存不足時,資源會被優先回收掉,再次顯示時會重新繪製。但是它的缺點就是,耗費資源。雖然把View的初始可見View.GONE,但是在Inflate佈局的時候View仍然會被Inflate,也就是說仍然會創建對象,會被實例化,會被設置屬性。也就是說,會耗費內存等資源。結論:

  • View.GONE仍然會被Inflate,會正常佔用內存等資源

ViewStub是一個輕量級的View,它一個看不見的,不佔佈局位置,佔用資源非常小的控件。可以爲ViewStub指定一個佈局,在Inflate佈局的時候,只有ViewStub會被初始化,然後當ViewStub被設置爲可見的時候,或是調用了ViewStub.inflate()的時候,ViewStub所向的佈局就會被Inflate和實例化,然後ViewStub的佈局屬性都會傳給它所指向的佈局。這樣,就可以使用ViewStub來方便的在運行時,要還是不要顯示某個佈局。結論:

  • ViewStub非常輕量級,佔用資源非常小

不能使用ScrollView包裹列表控件

使用ScrollView包裹ListView/GridView/ExpandableListVIew/RecyclerView,會把 ListView 的所有 Item 都加載到內存中,要消耗巨大的內存和 cpu 去繪製圖面。說明:ScrollView 中嵌套 List 或 RecyclerView 的做法官方明確禁止。除了開發過程中遇到的各種視覺和交互問題,這種做法對性能也有較大損耗。ListView 等 UI 組件自身有垂直滾動功能,也沒有必要在嵌套一層 ScrollView。目前爲了較好的 UI 體驗,更貼近 Material Design 的設計,推薦使用 NestedScrollView。

RelativeLayout和LinearLayout性能比較

我們要探討的性能問題,說的簡單明瞭一點就是:當RelativeLayout和LinearLayout分別作爲ViewGroup,表達相同佈局時繪製在屏幕上時誰更快一點。我們分別來追蹤下RelativeLayout和LinearLayout這三大流程的執行耗時。

LinearLayout

  • Measure:0.738ms
  • Layout:0.176ms
  • draw:7.655ms

RelativeLayout

  • Measure:2.280ms
  • Layout:0.153ms
  • draw:7.696ms

從這個數據來看無論使用RelativeLayout還是LinearLayout,layout和draw的過程兩者相差無幾,考慮到誤差的問題,幾乎可以認爲兩者不分伯仲,關鍵是Measure的過程RelativeLayout卻比LinearLayout慢了一大截。我們從源碼分析:

RelativeLayout->onMeasure

@Override    
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
	...
	//橫向測量全部的子View
	View[] views = mSortedHorizontalChildren;  
	int count = views.length;  
	for (int i = 0; i < count; i++) {  
		View child = views[i];  
		if (child.getVisibility() != GONE) {  
		    LayoutParams params = (LayoutParams) child.getLayoutParams();  
		    applyHorizontalSizeRules(params, myWidth);  
		    measureChildHorizontal(child, params, myWidth, myHeight);  
		    if (positionChildHorizontal(child, params, myWidth, isWrapContentWidth)) {  
		          offsetHorizontalAxis = true;  
		    }  
		}  
	}  
	//縱向測量全部的子View
	views = mSortedVerticalChildren;  
	count = views.length;  
	for (int i = 0; i < count; i++) {  
		View child = views[i];  
		if (child.getVisibility() != GONE) {  
		    LayoutParams params = (LayoutParams) child.getLayoutParams();  
		    applyVerticalSizeRules(params, myHeight);  
		    measureChild(child, params, myWidth, myHeight);  
		    ...
		}  
	}  
  ...  
}

根據上述關鍵代碼,RelativeLayout分別對所有子View進行兩次measure,橫向縱向分別進行一次,這是爲什麼呢?首先RelativeLayout中子View的排列方式是基於彼此的依賴關係,而這個依賴關係可能和佈局中View的順序並不相同,在確定每個子View的位置的時候,需要先給所有的子View排序一下。又因爲RelativeLayout允許A,B 2個子View,橫向上B依賴A,縱向上A依賴B。所以需要橫向縱向分別進行一次排序測量。 mSortedHorizontalChildren和mSortedVerticalChildren是分別對水平方向的子控件和垂直方向的子控件進行排序後的View數組。除此之外,RelativeLayout還有另一個性能問題 。先看看View的onMeasure方法都做了啥。

View->onMeasure

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
		//是否強制刷新
        final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
        //View位置是否有變化
        final boolean needsLayout = specChanged && (sAlwaysRemeasureExactly 
        || !isSpecExactly || !matchesSpecSize);
        if (forceLayout || needsLayout) {
			...
            if (cacheIndex < 0 || sIgnoreMeasureCache) {
                // measure ourselves, this should set the measured dimension flag back
                //重新測量
                onMeasure(widthMeasureSpec, heightMeasureSpec);
                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            } else {
                long value = mMeasureCache.valueAt(cacheIndex);
                // Casting a long to int drops the high 32 bits, no mask needed
                setMeasuredDimensionRaw((int) (value >> 32), (int) value);
                mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            }
        }
        ...
    }

View的measure方法裏對繪製過程做了一個優化,如果我們或者我們的子View沒有要求強制刷新,而父View給子View的傳入值也沒有變化(也就是說子View的位置沒變化),就不會做無謂的measure。但是上面已經說了RelativeLayout要做兩次measure,而在做橫向的測量時,縱向的測量結果尚未完成,只好暫時使用myHeight傳入子View系統,假如子View的Height不等於(設置了margin)myHeight的高度,那麼measure中上面代碼所做得優化將不起作用,這一過程將進一步影響RelativeLayout的繪製性能。而LinearLayout則無這方面的擔憂。解決這個問題也很好辦,如果可以,儘量使用padding代替margin,所以一個重要的結論是:

使用RelativeLayout的情況下,請用padding代替margin提高性能。

LinearLayout->onMeasure

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    if (mOrientation == VERTICAL) {
        measureVertical(widthMeasureSpec, heightMeasureSpec);
    } else {
        measureHorizontal(widthMeasureSpec, heightMeasureSpec);
    }
}

//LinearLayout會先做一個簡單橫縱方向判斷,我們選擇縱向這種情況繼續分析  
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
	...
	for (int i = 0; i < count; ++i) {    
	     final View child = getVirtualChildAt(i);    
	     //... child爲空、Gone以及分界線的情況略去  
	     //累計權重  
	     LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();    
	     totalWeight += lp.weight;    
	     //計算
	     if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) {    
	     	//精確模式的情況下,子控件layout_height=0dp且weight大於0無法計算子控件的高度  
	     	//但是可以先把margin值合入到總值中,後面根據剩餘空間及權值再重新計算對應的高度  
	      	final int totalLength = mTotalLength;    
	     	mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);    
	     } else {    
	        if (lp.height == 0 && lp.weight > 0) {    
	        	//如果這個條件成立,就代表 heightMode不是精確測量以及wrap_conent模式  
	         	//也就是說佈局是越小越好,你還想利用權值多分剩餘空間是不可能的,只設爲wrap_content模式  
	          	lp.height = LayoutParams.WRAP_CONTENT;    
	         }    
	         // 子控件測量  
	         measureChildBeforeLayout(child, i, widthMeasureSpec,0, heightMeasureSpec,totalWeight== 0 ? mTotalLength :0);           
	         //獲取該子視圖最終的高度,並將這個高度添加到mTotalLength中  
	         final int childHeight = child.getMeasuredHeight();    
	         final int totalLength = mTotalLength;    
	         mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));   
		}  
	} 
    ...  
}

源碼中已經標註了一些註釋,需要注意的是在每次對child測量完畢後,都會調用child.getMeasuredHeight()獲取該子視圖最終的高度,並將這個高度添加到mTotalLength中。但是getMeasuredHeight暫時避開了lp.weight>0的子View,因爲後面會將把剩餘高度按weight分配給相應的子View。因此可以得出以下結論:

  • 如果我們在LinearLayout中不使用weight屬性,將只進行一次measure的過程。
  • 如果使用了weight屬性,LinearLayout在第一次測量時避開設置過weight屬性的子View,之後再對它們做第二次measure。由此可見,weight屬性對性能是有影響的,而且本身有大坑,請注意避讓。

似乎看起來Linearlayout比relativelayout性能更好。

但是谷歌的官方說明

A RelativeLayout is a very powerful utility for designing a user interface because it can eliminate nested view groups and keep your layout hierarchy flat, which improves performance. If you find yourself using several nested LinearLayout groups, you may be able to replace them with a single RelativeLayout.

Google的意思是“性能至上”, RelativeLayout 在性能上更好,因爲在諸如 ListView/RecyclerView等控件中,使用 LinearLayout 容易產生多層嵌套的佈局結構,這在性能上是不好的。而 RelativeLayout 因其原理上的靈活性,通常層級結構都比較扁平,很多使用LinearLayout 的情況都可以用一個 RelativeLayout 來替代,以降低佈局的嵌套層級,優化性能。所以從這一點來看,Google比較推薦開發者使用RelativeLayout,因此就將其作爲Blank Activity的默認佈局了。

不能單純說Linearlayout和Relativelayout誰性能好就用誰,還要結合佈局層級要,層級優先。

LinearLayout vs FrameLayout

LinearLayout
Measure:2.058ms
Layout:0.296ms
draw:3.857ms

FrameLayout
Measure:1.334ms
Layout:0.213ms
draw:3.680ms

結論

  • 三種常見的ViewGroup的同層級下繪製速度:FrameLayout> LinerLayout> RelativeLayout
  • ConstraintLayout是一個更高性能的消滅佈局層級的神器
  • RecycleView中item 一般用ConstraintLayout或直接使用控件來佈局,以業務需求爲準。
  • 使用佈局優先級:FrameLayout>ConstraintLayout>LinearLayout>RelativeLayout,但要結合效率和需求實現
  • RelativeLayout會讓子View調用2次onMeasure,LinearLayout 在有weight時,也會調用子View 2次onMeasure
  • RelativeLayout的子View如果高度和自己高度不同,則會引發多次測量導致的效率問題,當子View很複雜時,這個問題會更加嚴重。如果可以,儘量使用padding代替margin提高性能,LinearLayout沒有這個問題。
  • 在不影響層級深度的情況下,使用LinearLayout和FrameLayout而不是RelativeLayout。
  • 優先考慮佈局層級,如果LinearLayout無法減少佈局層級,請用RelativeLayout。
  • RelativeLayout將對所有的子View進行兩次measure,而LinearLayout在使用weight屬性進行佈局時也會對子View進行兩次measure,如果他們位於整個View樹的頂端時並可能進行多層的嵌套時,位於底層的View將會進行大量的measure操作,大大降低程序性能。因此,應儘量將RelativeLayout和LinearLayout置於View樹的底層,並減少嵌套。
  • ListView/RecyclerView等控件中,列表項佈局使用 LinearLayout 容易產生多層嵌套的佈局結構,這在性能上是不好的。而 RelativeLayout 因其原理上的靈活性,通常層級結構都比較扁平,很多使用LinearLayout 的情況都可以用一個 RelativeLayout 來替代,以降低佈局的嵌套層級,優化性能。
  • 儘量減少使用wrap_content,推薦使用mathch_parent或固定尺寸配合gravity=“center”,因爲 在測量過程中,match_parent和固定寬高度對應EXACTLY,而wrap_content對應AT_MOST,這兩者對比AT_MOST耗時較多。

數據處理優化減少卡頓

  • 人爲在UI線程中做輕微耗時操作,導致UI線程卡頓

主線程也叫UI線程主要的任務是處理用戶交互、繪製界面、顯示數據、消息處理等工作,如果耗時操作如請求網絡數據、操作數據庫、讀取文件等就不能放在主線程來,可以開啓子線程來操作。使用線程池來代替單獨創建子線程,因爲頻繁的創建和銷燬線程很耗時,創建太多的子線程也會搶佔主線程的CPU使用,從而導致卡頓。

  • 同一時間動畫執行的次數過多,導致CPU或GPU負載過重;

動畫的執行頻率一定不能過快,需要在一個合理的範圍之內,繁殖卡頓

  • 內存抖動導致暫時阻塞渲染操作,造成卡頓

內存泄漏的積累和大量對象的創建,都容易觸發GC過多造成內存抖動。內存抖動會導致暫時阻塞渲染操作,造成卡頓。我們需要在代碼中儘量避免內存泄漏的發生以及大量對象的創建。

  • 工作線程優先級未設置爲Process.THREAD_PRIORITY_BACKGROUND導致後臺線程搶佔UI線程cpu時間片,阻塞渲染操作

異步不總是靈丹妙藥,不正確的異步方式不僅不能較好的完成異步任務,反而會加劇卡頓。UI線程優先級爲THREAD_PRIORITY_DEFAULT = 0。線程優先級有繼承性,如果從UI線程啓動,則該線程優先級默認爲Default,會平等的和UI線程爭奪CPU資源。線程數一旦數量增加,搶佔明顯,造成卡頓。這一點尤其需要注意,在對UI性能要求高的場景下建議將線程優先級設置爲THREAD_PRIORITY_BACKGROUND = 10(值越大優先級越低),以此降低與主線程競爭的能力。

new Thread () {
    @Override
    public void run() {
      super.run();
        android.os.Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
    }
}.start();

三、耗損

電量(重點),電量是我們需要認真考慮的一方面,手機的續航能力是現在用戶關注的一個點,如果手機電量消耗過快,用戶可能會卸載那些消耗電量過大的應用。

根據物理學中的知識,功 = 電壓 * 電流 * 時間,但是一部手機中,電壓值U正常來說是不會變的,所以可以忽略,只通過電流和時間就可以表示電量。模塊電量(mAh) = 模塊電流(mA) * 模塊耗時(h)。模塊耗時比較容易理解,但是模塊電流怎樣獲取呢?以Nexus 6P爲例,使用apktool,對/system/framework/framework-res.apk進行反解析,獲取到手機裏面的power_profile.xml文件,文件中定義了該手機各耗電模塊在不同狀態下的電流值,內容如下所示:

<?xml version="1.0" encoding="utf-8"?>
<device name="Android">
    <item name="none">0</item>
    <item name="screen.on">169.4278765</item>
    <item name="screen.full">79.09344216</item>
    <item name="bluetooth.active">25.2</item>
    <item name="bluetooth.on">1.7</item>
    <item name="wifi.on">21.21733311</item>
    <item name="wifi.active">98.04989804</item>
    <item name="wifi.scan">129.8951166</item>
    <item name="dsp.audio">26.5</item>
    <item name="dsp.video">242.0</item>
    <item name="gps.on">5.661105191</item>
    <item name="radio.active">64.8918361</item>
    <item name="radio.scanning">19.13559783</item>
    <array name="radio.on">
        <value>17.52231575</value>
        <value>5.902211798</value>
        <value>6.454893079</value>
        <value>6.771166916</value>
        <value>6.725541238</value>
    </array>
    <array name="cpu.speeds.cluster0">
        <value>384000</value>
        <value>460800</value>
        <value>600000</value>
        <value>672000</value>
        <value>768000</value>
        <value>864000</value>
        <value>960000</value>
        <value>1248000</value>
        <value>1344000</value>
        <value>1478400</value>
        <value>1555200</value>
    </array>
    <array name="cpu.speeds.cluster1">
        <value>384000</value>
        <value>480000</value>
        <value>633600</value>
        <value>768000</value>
        <value>864000</value>
        <value>960000</value>
        <value>1248000</value>
        <value>1344000</value>
        <value>1440000</value>
        <value>1536000</value>
        <value>1632000</value>
        <value>1728000</value>
        <value>1824000</value>
        <value>1958400</value>
    </array>
    <item name="cpu.idle">0.144925583</item>
    <item name="cpu.awake">9.488210416</item>
    <array name="cpu.active.cluster0">
        <value>202.17</value>
        <value>211.34</value>
        <value>224.22</value>
        <value>238.72</value>
        <value>251.89</value>
        <value>263.07</value>
        <value>276.33</value>
        <value>314.40</value>
        <value>328.12</value>
        <value>369.63</value>
        <value>391.05</value>
    </array>
    <array name="cpu.active.cluster1">
        <value>354.95</value>
        <value>387.15</value>
        <value>442.86</value>
        <value>510.20</value>
        <value>582.65</value>
        <value>631.99</value>
        <value>812.02</value>
        <value>858.84</value>
        <value>943.23</value>
        <value>992.45</value>
        <value>1086.32</value>
        <value>1151.96</value>
        <value>1253.80</value>
        <value>1397.67</value>
    </array>
    <array name="cpu.clusters.cores">
        <value>4</value>
        <value>4</value>
    </array>
    <item name="battery.capacity">3450</item>
    <array name="wifi.batchedscan">
        <value>.0003</value>
        <value>.003</value>
        <value>.03</value>
        <value>.3</value>
        <value>3</value>
    </array>
</device>

從文件內容中可以看到,power_profile.xml文件中,定義了消耗電量的各模塊。如下圖所示:
在這裏插入圖片描述
看一看出,App電量 = ∑App模塊電量。可以清楚的知道這個手機上,哪些模塊會耗電,以及哪些模塊在什麼狀態下耗電量最高。那麼測試的時候,應該重點關注調用了這些模塊的地方。比如App在哪些地方使用WiFi、藍牙、GPS等等。例如最近對比測試其他App發現,在一些特定的場景下,該App置於前臺20min內,掃描了WiFi 50次,這種異常會導致App耗電量大大增加。並且反過來,當有case報App耗電量異常時,也可以從這些點去考慮,幫助定位問題。

網絡優化(Radio模塊)

對電池來說,網絡連接是最耗電的工作。手機裏面有一個芯片,Ham radio,他的功能就是連接當地的電話信號塔並和它們進行大量的數據傳輸。但是這個芯片不是一直活躍的,一旦你發送了數據,無線芯片會在一定時間內保持開啓狀態再接收返回的數據。但是如果沒有活動,這個硬件就會休眠以節省電量。電量優化包含着網絡優化。

避免DNS解析

DNS域名的系統,主要的功能根據應用請求所用的域名URL去網絡上面映射表中查相對應的IP地址,這個過程有可能會消耗上百毫秒,而且可能存在着DNS劫持的危險,可以替換爲Ip直接連接的方式來代替域名訪問的方法,從而達到更快的網絡請求,但是使用Ip地址不夠靈活,當後臺變換了Ip地址的話,會出現訪問不了,前段的App需要發包,解決方法是增加Ip地址動態更新的能力,或者是在IP地址訪問失敗了,切換到域名的訪問。

合併網絡請求

一次完整的Http請求,首先進行的是DNS查找,通過TCP三次握手,從而建立連接,過多的請求次數耗電耗時耗流量。如果是https請求的話,還要經過TLS握手成功後纔可以進行連接,對於網絡請求,減少接口,能夠合併的網絡請求就儘量合併。

壓縮數據的大小

  • 使用Gzip來壓縮request和response, 減少傳輸數據量, 從而減少流量消耗。
  • 使用webP格式代替圖片格式(webP是google新出的一種圖片格式,旨在縮小圖片體積的情況下,儘量好的顯示圖片。加快圖片加載速度,提升用戶體驗。據說現在某寶和某東都再用webP格式的圖片了)。
  • 如果我們的接口每次傳輸的數據量很大的話,從網絡流量優化的角度可以考慮下protobuf, 會比JSON數據量小很多。當然相比來說,JSON也有其優勢, 可讀性更高。
  • 在請求圖片的url中添加諸如質量, 格式, width, height等path來獲取合適的圖片資源。

使用緩存策略

對於圖片或者文件,內存緩存+磁盤緩存+網絡緩存三級緩存策略,一般我們本地需要做的是二級緩存,當緩存中存在圖片或者是文件,直接從緩存中讀取,不會走網絡,下載圖片,在Android中使用LruCache實現內存緩存,DiskLruCache實現本地緩存。http協議自帶的緩存策略,當資源沒有修改時,http status 爲304。適當的緩存, 既可以讓我們的應用看起來更快, 也能避免一些不必要的流量消耗。

使用JobScheduler

Android 5.0 發佈的JobScheduler和Android 6.0出現的Doze都一樣,總結來說就是限制應用頻繁喚醒硬件,將任務集中處理,從而達到省電的效果。 當你需要在Android設備滿足某種場合才需要去執行處理數據,例如:

  • 應用具有您可以推遲的非面向用戶的工作(定期數據庫數據更新)
  • 應用具有當插入設備時您希望優先執行的工作(充電時才希望執行的工作備份數據)
  • 需要訪問網絡或 Wi-Fi 連接的任務(如向服務器拉取內置數據)
  • 希望作爲一個批次定期運行的許多任務

結合JobScheduler來根據實際情況做網絡請求. 比方說Splash閃屏廣告圖片, 我們可以在連接到Wifi時下載緩存到本地; 新聞類的App可以在充電、Wifi狀態下做離線緩存。這樣做有兩個好處:

  • 避免頻繁的喚醒硬件模塊,造成不必要的電量消耗。
  • 避免在不合適的時間(例如低電量情況下、弱網絡或者移動網絡情況下的)執行過多的任務消耗電量;

不同的網絡狀況,做不同的事

在WiFi,4G,3G等不同的網絡下設計不同大小的預取數據量策略,我們還需要把當前的網絡環境情況添加到設計預取數據量的策略當中去。

  • 我們可以把網絡請求延遲劃分爲三檔:例如把網絡延遲小於60ms的劃分爲GOOD,大於220ms的劃分爲BAD,介於兩者之間的劃分爲OK(這裏的60ms,220ms會需要根據不同的場景提前進行預算推測)。如果網絡延遲屬於GOOD的範疇,我們就可以做更多比較激進的預取數據的操作,如果網絡延遲屬於BAD的範疇,我們就應該考慮把當下的網絡請求操作Hold住等待網絡狀況恢復到GOOD的狀態再進行處理。
  • 我們可以根據WiFi,4G,3G等不同網絡狀況動態調整網絡超時的時間。

屏幕顯示優化(Screen模塊)

屏幕顯示是耗電大戶,在一段時間後,android設備的屏幕會變暗,直至關閉,然後會停止cpu運行,減少設備的功耗。但某些場景我們需要來保持屏幕常量,比如電子書閱讀器,視頻軟件,視頻聊天軟件等等,我們會用到一些策略,讓屏幕保持常量。

PowerManager 用來控制設備的電源狀態. 而PowerManager.WakeLock也稱作喚醒鎖, 是一種保持 CPU 運轉防止設備休眠的方式。例如播放音樂,即使在屏幕關閉時也需要程序在後臺運行。但是我們要謹慎使用WakeLock,WakeLock獲取釋放成對出現以及
使用超時WakeLock, 以防出異常導致沒有釋放。這裏要儘量使用 acquire(long timeout) 設置超時, (也被稱作超時鎖). 例如網絡請求的數據返回時間不確定, 導致本來只需要10s的事情一直等待了1個小時, 這樣會使得電量白白浪費了。 設置超時之後, 會自動釋放節省電量。

PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE);
// 創建喚醒鎖
WakeLock wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MyWakelockTag");
// 獲得喚醒鎖,需要加上超時機制
wakeLock.acquire(timeout);
// 釋放喚醒鎖, 如果沒有其它喚醒鎖存在, 設備會很快進入休眠狀態
wakelock.release();

當然還有更好的喚醒策略,只有在特定的界面纔會保持屏幕,不像喚醒鎖(wake locks),需要申請android.permission.WAKE_LOCK權限。並且不用擔心界面切換以及資源釋放問題。

//Activity中使用FLAG_KEEP_SCREEN_ON 的Flag。
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);

還有一種佈局屬性:keepScreenOn

<LinearLayout
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:keepScreenOn="true">
</LinearLayout>

定位優化(GPS模塊)

不同的定位策略耗電不一樣,我們應該根據應用的實際情況使用合適的定位策略,達到省電的目的。Android系統支持多個Location Provider:

  • GPS_PROVIDER
    GPS定位,利用GPS芯片通過衛星獲得自己的位置信息。定位精準度高,一般在10米左右,耗電量大;但是在室內,GPS定位基本沒用。
  • NETWORK_PROVIDER
    網絡定位,利用手機基站和WIFI節點的地址來大致定位位置,這種定位方式取決於服務器,即取決於將基站或WIF節點信息翻譯成位置信息的服務器的能力。
  • PASSIVE_PROVIDER
    被動定位,就是用現成的,當其他應用使用定位更新了定位信息,系統會保存下來,該應用接收到消息後直接讀取就可以了。

注意:位置更新監聽頻率的設定,minTime用來指定間更新通知的最小的時間間隔,單位是毫秒,minDistance用來指定位置更新通知的最小的距離,單位是米,根據業務需求設置一個合理的更新頻率值。定位中使用GPS, 請記得及時關閉。

 // Remove the listener you previously added
locationManager.removeUpdates(locationListener)

其他模塊

其他模塊的省電優化這裏就不多講,我們可以再實際開發中藉助一些工具,比如Batterystats & bugreport或者Battery Historian逐步分析,找出耗電大戶,對症下藥優化。

四、安裝包

用戶常常避免下載太大的APP,尤其是使用移動流量的情況下,而且太大的APP也會佔用更多的內存並消耗更多的資源,導致安裝速度和加載速度變慢,特別是在低配手機上,這些情況尤爲嚴重。

APK的組成結構

在這裏插入圖片描述
Raw File Size表示原文件大小,Download Size表示經過Google play處理壓縮後的apk大小。可以看到佔空間最多的主要是四個部分:lib、res、classes.dex、resources.arsc。也是重要的優化對象。

  • lib:so引用庫的文件夾
  • res:包含了資源文件,比如圖片、佈局文件等等
  • classes.dex:包含有 Java 代碼的字節碼文件
  • resources.arsc:包含所有的值資源文件,如 strings, dimensions, styles, integers 等等。

1、整體優化

插件化

從應用功能擴張的角度看,APK包體積的增大是必然的,然而插件化技術的出現很好的解決了這個問題。通過分離應用中比較獨立的模塊,然後以插件的形式進行加載。比如愛奇藝Android客戶端有很多相對獨立的功能,遊戲、漫畫、文學、電影票、應用商店等,都是通過插件的方式,從服務器下載,然後以插件的額方式加載到我們的主工程。

移除用不到的語言資源文件

官方的 support library,默認是支持國際化的,也就是包含了很多不同語言的資源文件,我們就可以通過這樣設置來移除用不到的語言資源文件。如果你的應用不需要支持國際化,那麼可以設置 resConfigs 爲 “zh”,“en”,即只支持中英文:

defaultConfig {
    resConfigs "zh","en"
}

2、lib庫優化

在使用一些三方庫的時候,會集成大量的so文件到項目中,這些so文件都對應着不同的CPU架構。Android系統目前支持以下七種不同的CPU架構:ARMv5、ARMv7、x86、MIPS、ARMv8、MIPS64、x86_64,每一個CPU架構對應一個ABI:armeabi,armeabi-v7a,x86,mips,arm64-v8a,mips64,x86_64。目前市面上絕大部分的CPU架構都是 ARMv7/ARMv8,所以可以在gradle中加入配置,只保留v7和v8。同時,Google市場要求必須提供相應的支持64位so,所以可以配置如下:

defaultConfig {
    ...
    ndk {
        abiFilters 'armeabi-v7a', 'arm64-v8a' 
    }
}

3、res資源優化

採用一套圖片資源

Android在適配圖片資源的時候,如果只有一套資源,低密度的手機會縮放圖片,高密度的手機會拉伸圖片。我們利用這個特性,存放一套資源圖就可以供所有密度的手機使用。綜合考慮圖片清晰度,靜態大小和內存佔用情況,建議取1080p的資源,放到xxhdpi目錄。

合併重複資源

很多時候,隨着工程的增大,以及開發人員的變動,有些資源文件名字不同,但是內容卻完全不同。我們可以同過掃描文件的MD5值,找出名字不同,內容相同的圖片並刪除,做到圖片不重複。

移除無用的資源

  • 用Android Lint工具進行無用資源、無效索引刪除,在Analyze中Run Inspection By Name檢索unused resources,進行清理。
    在這裏插入圖片描述
  • 可以通過設置shrinkResources=true讓Gradle移走無用的資源,否則默認混淆情況下,Gradle編譯只會移除無用代碼,而不會關心無用資源。需要特別注意的是shrinkResources依賴於minifyEnabled,必須和minifyEnabled一起用,即打開shrinkResources也必須打開minifyEnabled。
buildTypes {
	release {
		minifyEnabled true
		shrinkResources true
	}
}

整體移除res下某個文件夾

如果想整體移除res下某個文件夾可以添加如下aaptOptions配置,而不用打包時手工刪除,多個文件夾用:隔開

android {
    aaptOptions {
        ignoreAssetsPattern 'drawable-hdpi;drawable-mhdpi'
    }
}

png圖片壓縮

可以通過使用圖片壓縮工具對png圖片進行壓縮,壓縮效果比較好的工具有:pngcrush,pngquant,zopflipng等,可以在保持圖片質量的前提下,縮減圖片的大小。還可以通過網站對圖片進行壓縮,如比較有名的www.tinypng.com,tinypng 是一個支持壓縮png和jpg圖片格式的網站,通過其獨特的算法(通過一種叫“量化”的技術,把原本png文件的24位真彩色壓縮爲8位的索引演示,是一 種矢量壓縮方法,把顏色值用數值123等代替。)可以實現在無損壓縮的情況下圖片文件大小縮小到原來的30%-50%,但是隻支持500張免費圖片,更多圖片處理是要收費的。

採用WebP格式

目前WEBP與JPG相比較,編碼速度慢10倍,解碼速度慢1.5倍,雖然會增加額外的解碼時間,但是由於減少了文件體積,縮短了加載的時間,實際上文件的渲染速度反而變快了。如果你的 Android Studio 爲 2.3,並且項目的 minimum version 爲 18 或以上,應該使用 webp 而不是 png 圖片。webp 圖片有更小的體積,圖片質量還沒有什麼損失。我們可以選中 drawable 和 mipmap 文件夾,右鍵後選擇 convert to webp,將圖片轉爲 webp 格式。
在這裏插入圖片描述

svg矢量代替圖片

svg矢量圖。其實是圖片的描述文件,犧牲CPU的計算能力的,節省空間。適用於簡單的圖標。

用shape代替圖片

能用shape就絕不用圖片。對於純色或漸變的圖片,能用shape渲染的就優先使用shape。不僅文件體積小,還渲染速度快,也不用考慮適配問題。

4、arsc文件優化

只保留需要的語言

android {
    defaultConfig {
            resConfigs "zh", "zh_CN", "zh_HK", "zh_MO", "zh_TW", "en"
    }
}

縮小訪問路徑

resource.arsc文件中保存着資源文件夾中各個資源的路徑。微信開源的AndResGuard資源混淆工具只針對資源,他會將原本冗長的資源路徑變短,例如將res/drawable/wechat變爲r/d/a。,再生成新的resource.arsc文件,替換源文件打包簽名即可。

發佈了35 篇原創文章 · 獲贊 7 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章