第十五章-Android性能優化

推薦個音樂下載app【佳語音樂下載】
https://gitlab.com/gaopinqiang/checkversion/raw/master/Music_Download.apk

第十四章-JNI和NDK編程
這個我在之前的博客中有寫過,書中也只是簡單的介紹了下開發的流程。所以可以參考之前的博客就行。
https://blog.csdn.net/gaopinqiang/article/details/76652575

本章是最後一章,介紹的主題是Android的性能優化方法和程序設計的一些思想。
一、Android的性能優化方法

本節主要介紹一些有效的性能優化方法,主要內容包括佈局優化、繪製優化、內存泄漏優化、響應速度優化、ListView優化、Bitmap優化、線程優化以及一些性能優化建議。

1、佈局優化

佈局優化的思路很簡單,就是儘量減少佈局文件的層級,佈局層級少了,這就意味着Android繪製時的工作量少了,那麼程序的性能自然就高了。

如何進行佈局優化呢?首先刪除佈局中的無用控件和層級,其次有選擇地使用性能較低的ViewGroup,比如RelativeLayout,如果佈局中既可以使用LinearLayout,也可以使用RelativeLayout,那就採用LinearLayout,這是因爲RelativeLayout的功能比較複雜,它的佈局需要花費更多的CPU時間。FrameLayout和LinearLayout一樣都是一種簡單高效的ViewGroup,因此可以考慮使用它們。

佈局優化還有另外一種手段是採用標籤、標籤和ViewStub。標籤主要用於佈局重用,標籤一般和配合使用,它可以降低減少佈局的層級,而ViewStub則提供了按需加載的功能,當需要時纔會將ViewStub中的佈局加載到內存,這提高了程序的初始化效率。

< include>標籤

<include>標籤可以將一個指定的佈局文件加載到當前的佈局文件中,如下所示。
	<?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="match_parent"
		android:orientation="vertical">

		<include layout="@layout/titlebar"/>
		
	</LinearLayout>

上面的代碼中,@layout/titlebar指定了另外一個佈局文件,通過這種方式就不用把titlebar這個佈局的內容再重複寫一遍了,這就是的好處。標籤只支持android:layout開頭的屬性,比如android:layout_width、android:layout_height,其他的屬性是不支持的,比如android:backgroup。當然android:id這個屬性是個特例,如果指定了這個id屬性,同時被包含的佈局文件的根元素也指定了id屬性,那麼以指定的id屬性爲準。需要注意的是,如果標籤指定了android:layout_*這種屬性,那麼android:layout_width和android:layout_height是必須存在的。

< merge>標籤
< merge>標籤一般和一起使用從而減少佈局的層級。比如上面的佈局是一個豎直方向的LinearLayout,這個時候如果被包含的佈局文件中也採用了豎直方向的LinearLayout,那麼顯然被包含的佈局文件中的LinearLayout是多餘的,通過標籤就可以去掉多餘的那一層LinearLayout。

	<?xml version="1.0" encoding="utf-8"?>
	<merge xmlns:android="http://schemas.android.com/apk/res/android"
		android:layout_width="match_parent"
		android:layout_height="match_parent"
		android:orientation="vertical">

		<TextView
			android:layout_width="wrap_content"
			android:layout_height="wrap_content"
			android:text="test" />

	</merge>

ViewStub
ViewStub繼承View,它非常輕量級且高寬都是0,因此它本身不參與任何的佈局和繪製過程。ViewStub的意義在於按需加載所需的佈局文件,在實際的開發中,很多佈局文件在正常的情況下不會顯示,比如網絡異常時的界面,這個時候就沒有必要在整個界面初始化的時候將其加載進來,通過ViewStub就可以做到在使用的時候再加載,提高了程序初始化時的性能。

    <ViewStub
		android:id="@+id/stub_import"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout="@layout/include_main_test"
        android:inflatedId="@id/ll_include"
        />

其中stub_import是ViewStub的id。如何做到按需加載呢?可以按照下面的兩種方式:

	((ViewStub)findViewById(R.id.stub_import).setVisibility(View.VISIBLE);
或者	
	((ViewStub)findViewById(R.id.stub_import)).inflate()

當ViewStub通過setVisibility或者inflate方法加載後,ViewStub就會被它內部的佈局替換掉,這個時候ViewStub就不再是整個佈局結構的一部分了。目前ViewStub還不支持< merge >標籤。

2、繪製優化
繪製優化是指View的onDraw方法要避免執行大量的操作,這主要體現在兩個方面。

首先,onDraw中不要創建新的局部對象,這是因爲onDraw方法可能會被頻繁調用,這樣就會瞬間產生大量的臨時對象,這不僅佔用了過多的內存而且還會導致系統更加頻繁gc,降低了程序的執行效率。

另外一方面,onDraw方法中不要做耗時的任務,也不能執行成千上萬次的循環操作,儘管循環是很輕量級,但是大量的循環仍然十分搶佔CPU的時間片,這會造成View的繪製過程不流暢。按照Google官方給出的性能優化典範中的標準,View的繪製幀率保證60fps是最佳的,這就要求繪製時間不能超過16ms(16ms = 1000/60)。

3、內存泄漏優化

內存泄漏在開發過程中是一個需要重視的問題,但是由於內存泄漏問題對於開發者的經驗和開發意識有較高的要求,因此這也是開發人員最容易犯的錯誤之一。內存泄漏的優化分兩個方面,一方面是在開發過程中避免寫出內存泄露的代碼,另一方面是通過一些分析工具比如MAT來找出潛在的內存泄漏繼而解決。下面介紹幾個常見的內存泄漏的場景。

場景1:靜態變量導致的內存泄漏
下面這種情形是最簡單的內存泄漏,相信大家都不會這麼幹,下面代碼將導致Activity無法正常銷燬,因爲靜態變量sContext引用了它。

	public class MainActivity extends AppCompatActivity {
		private static final String TAG = "MainActivity";
		
		private static Context sContext;

		@Override
		protected void onCreate(Bundle savedInstanceState) {
			super.onCreate(savedInstanceState);
			setContentView(R.layout.activity_main);
			sContext = this;//將activity對象賦值給sContext這個靜態變量,導致內存泄漏
		}
	}

上面的代碼也可以改造一下。sView是一個靜態變量,它內部持有了當前的Activity,所以Activity仍然無法釋放

	public class MainActivity extends AppCompatActivity {

		private static final String TAG = "MainActivity";
		//內存泄漏
		private static View sView;
		
		@Override
		protected void onCreate(Bundle savedInstanceState) {
			super.onCreate(savedInstanceState);
			setContentView(R.layout.activity_main);
			sView = new View(this);
		}
	}

場景2:單例模式導致的內存泄漏
靜態變量導致的內存泄漏都太過於明顯,相信大家都不會犯這種錯誤,而單例模式帶來的內存泄漏是我們很容易忽視的,如下所示。首先提供一個單例模式的TestManager,TestManager可以接收外部的註冊並將外部的監聽器存儲起來。

	public class TestManager {
		private List<OnDataArrivedListener> mOnDataArrivedListener = new ArrayList<OnDataArrivedListener>();

		private static class SingletonHolder {
			public static final TestManager INSTANCE = new TestManager();
		}

		private TestManager() {
		}

		public static TestManager getInstance() {
			return SingletonHolder.INSTANCE;
		}

		public synchronized void registerListener(OnDataArrivedListener listener) {
			if (mOnDataArrivedListener.contains(listener)) {
				mOnDataArrivedListener.add(listener);
			}
		}

		public synchronized void unregisterListener(OnDataArrivedListener listener) {
			mOnDataArrivedListener.remove(listener);
		}

		public interface OnDataArrivedListener {
			public void onDataArrived(Object data);
		}
	}

接着再讓Activity實現OnDataArrivedListener接口並向TestManager註冊監聽,如下所示。下面的代碼由於缺少解註冊的操作會引起內存泄漏,泄漏的原因是Activity的對象被單例模式的TestManager所持有,而單例模式的特點是生命週期和Application保持一致,所以Activity無法被及時釋放。

	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		TestManager.getInstance().registerListener(this);
	}

場景3:屬性動畫所導致的內存泄漏
從Android3.0開始,Google提供了屬性動畫,屬性動畫中有一類無限循環的動畫,如果在Activity中播放此類動畫且沒有在onDestroy中去停止動畫,那麼動畫會一直播放下去,儘管已經無法在界面上看到動畫效果了,並且這個時候Activity的View會被動畫持有,而View又持有了Activity,最終Activity無法釋放。下面的動動畫是無限動畫,會泄露當前Activity,解決方法是在 Activity的 onDestroy中調用 animator.cancel()來停止動畫。

	public class MainActivity extends AppCompatActivity  {
		@Override
		protected void onCreate(Bundle savedInstanceState) {
			super.onCreate(savedInstanceState);
			setContentView(R.layout.activity_main);

			mButton = (Button) findViewById(R.id.mButton);
			ObjectAnimator animator = ObjectAnimator.ofFloat(mButton,"rotation",0,360).setDuration(2000);
			animator.setRepeatCount(ValueAnimator.INFINITE);
			animator.start();
			//animator.cancel();
		}
	}

4、響應速度優化和ANR日誌分析
響應速度優化的核心思想是避免在主線程中做耗時操作,但是有時候的確有很多耗時操作,怎麼辦呢?可以將這些耗時的操作放在線程中去執行,即採用異步的方式執行耗時操作。
響應速度過慢更多地體現在Activity的啓動速度上面,如果在主線程中做太多事情會導致Activity啓動時出現黑屏現象,甚至出現ANR。Android規定,Activity如果5秒鐘之內無法響應屏幕觸摸事件或者鍵盤輸入事件就會出現ANR。BroadcastReceiver是10秒。系統會在 data/anr目錄下創建一個文件 traces.txt,通過分析這個文件就能定位出ANR的原因。下面模擬一個ANR的場景。下面的代碼在 Activity的 oncreate中休眠30s,程序運行後持續點擊屏幕,應用一定會出現ANR:

	public class MainActivity extends AppCompatActivity  {

		@Override
		protected void onCreate(Bundle savedInstanceState) {
			super.onCreate(savedInstanceState);
			setContentView(R.layout.activity_main);

			SystemClock.sleep(30 * 1000);
		}
	}

trace.txt文件在/data/anr/traces.txt中。

如果在Android10上面沒有權限導出traces.txt可以使用bugreport命令(會成成比較完整的日誌,本地測試有14M左右)
【adb bugreport anrlog.zip】

具體的traces文件的內容分析也比較簡單,會有明確的提示。例如:

	"main" prio=5 tid=1 Blocked
	  | group="main" sCount=1 dsCount=0 flags=1 obj=0x74e80d60 self=0xef579000
	  | sysTid=27326 nice=-4 cgrp=default sched=0/0 handle=0xf708e494
	  | state=S schedstat=( 3483871367 187630217 2232 ) utm=313 stm=34 core=5 HZ=100
	  | stack=0xff2e1000-0xff2e3000 stackSize=8MB
	  | held mutexes=
	  at android.app.Activity.finish(Activity.java:5873)
	  - waiting to lock <0x0b4b107d> (a com.example.basetest.MainActivity) held by thread 14
	  at android.app.Activity.finish(Activity.java:5907)
	  at com.example.basetest.MainActivity.endDisturbAnalysisTest(MainActivity.java:2552)
	  at com.example.basetest.MainActivity.onBackPressed(MainActivity.java:2499)
	  at android.app.Activity.onKeyUp(Activity.java:3229)
	  at android.view.KeyEvent.dispatch(KeyEvent.java:2792)
	  at android.app.Activity.dispatchKeyEvent(Activity.java:3537)
	  at com.android.internal.policy.DecorView.dispatchKeyEvent(DecorView.java:403)
	  at android.view.ViewRootImpl$ViewPostImeInputStage.processKeyEvent(ViewRootImpl.java:5661)

很明顯看到在等待0x0b4b107d這個鎖,這個鎖被thread 14持有,我們看下這個線程的狀態就明白了。

5、ListView和Bitmap優化
ListView在12章就已經做了介紹,這裏簡單回顧一下。主要分爲三個方面:首先採用ViewHolder並避免在getView中執行耗時操作;其次是根據列表的滑動狀態來控制任務的執行頻率,比如列表快速滑動的時候顯然不太適合開啓大量的異步任務的;最後可以嘗試開啓硬件加速來使Listview滑動更加流暢。

Bitmap優化同樣在12章已經做了詳細的介紹,主要通過BitmapFactory.Options來根據需要對圖片進行採樣,採樣過程主要用到了BitmapFactory.Options的inSampleSize參數。

6、線程優化
線程優化的思想是採用線程池,避免程序中存在大量的Thread。線程池可以重用內部的線程,從而避免了線程的創建和銷燬所帶來的性能開銷。

7、一些性能建議

  • 避免創建過多的對象;
  • 不要過多使用枚舉,枚舉佔用的內存空間要比整型大;
  • 常量請使用static final來修飾;
  • 使用一些Android特有的數據結構,比如SparseArray和Pair等,它們都具有更好的性能;
  • 適當使用軟引用和弱引用;
  • 採用內存緩存和磁盤緩存;
  • 儘量採用靜態內部類,這樣可以避免潛在的由於內部類而導致的內存泄漏。

二、內存泄漏分析之MAT工具
MAT的全稱Eclipse Memory Analyzer,它是一款強大的內存泄漏分析工具,MAT不需要安裝,下載解壓即可使用。

關於MAT的使用之前有寫過博客;
https://blog.csdn.net/gaopinqiang/article/details/78640454

三、提高程序的可維護性
本節所講述的內容是Android的程序設計思想,主旨是如何提高代碼的可維護性和可擴展性,而程序的可維護性本質上也包含可擴展性。本節的切入點爲:代碼風格、代碼的層次性和單一職責原則、面向擴展編程程以及設計模式,下面圍繞着它們分別展開。

可讀性是代碼可維護性的前提,一段別人很難讀懂的代碼的可維護性顯然是極差的。而良好的代碼風格在一定程度上可以提高程序的可讀性。代碼風格包含很多方面,比如命名規範、代碼的排版以及是否寫註釋等。到底什麼樣的代碼風格是良好的?這是個仁者見仁的問題,下面是筆者的一些看法。

  • (1)命名要規範,要能正確地傳達出變量或者方法的含義,少用縮寫,關於變量的前綴可以參考Android源碼的命名名方式,比如私有成員以m開頭,靜態成員以s開頭,常量則全部用大寫字母表示,等等。
  • (2)代碼的排版上需要留出合理的空白來區分不同的代碼塊,其中同類變量的聲明要放在一組,兩類變量之間要留出一行空白作爲區分。
  • (3)僅爲非常關鍵的代碼添加註釋,其它地方不寫註釋,這就對變量和方法的命名風格提出了很高的要求,一個合理的命名風格可以讓讀者閱讀源碼就像在閱讀註釋一樣,因此根本不需要爲代碼額外寫註釋。

代碼的層次性是指代碼要有分層的概念,對於一段業務邏輯,不要試圖在一個方法或者一個類中去全部實現,而要將它分成幾個子邏輯,然後每個子邏輯做自己的事情,這樣既顯得代碼層次分明,又可以分解任務從而實現簡化邏輯的效果。

單一職責是和層次性相關聯的,代碼分層以後,每一層僅僅關注少量的邏輯,這樣就做到了單一職責。

程序的擴展性標誌着開發人員是否有足夠的經驗,很多時候在開發過程中我們無法保證已經做好的需求不在後面的版本發生變更,因此在寫程序的過程中要時刻考慮到擴展,考慮着如果這個邏輯後面發生了改變那麼需要做哪些修改,以及怎麼樣才能降低修改的工作量,面向擴展編程會使程序具有很好的擴展性。

恰當地使用設計模式可以提高代碼的可維護性和可擴展性,但是Android程序容易有性能瓶頸,因此要控制設計的度,設計不能太牽強,否則就是過度設計了。常見的設計模式有很多,比如單例模式、工廠模式以及觀察者模式等,由於本書不是專門介紹設計模式的書,因此這裏就不對設計模式進行詳細的介紹了,讀者可以參看《大話設計模式》和《Android源碼設計模式解析與實戰》這兩本書,另外設計模式需要理解後靈活運用才能發揮更好的效果。

到這裏已經對全書有了詳細閱讀和記錄了。確實寫的不錯。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章