編輯:關於Android編程
在Android中,UI主線程並非線程安全的,所有UI相關的操作均需在UI主線程中完成。在默認情況下,開發者創建的Service、Activity、Broadcast均運行在UI主線程中,但將一些耗時操作,如網絡下載、大文件讀寫、加解密計算、數據庫操作等,也放在UI線程中執行,往往會阻塞UI線程,造成ANR異常,因此,在Android應用開發中,應特別注意線程的使用。
在Android中,實現多線程的方式有多種,開發者可以通過原生Java線程或Android對Java線程的封裝即AsyncTask來實現多線程的效果。
為了開發出性能更好的應用,開發者必須對Android的多線程實現由清楚的了解,了解每種實現方式的優缺點和線程安全方面的問題,這樣才能最大程度地發揮出Android的潛力。
1.Java線程實現
Android線程的實現本質上仍是Java線程,只是為了方便開發者進行實現,針對特定場景做了封裝。本節將著重介紹基於Thread、Runnable的java線程的實現,並介紹Android的線程優先級。
(1)基於Thread的實現
基於Thread實現新線程和在Runnable構造器中實現新線程是傳統Java實現線程的兩種方式,基於Thread的實現非常簡單。示例如下:
static class AThread extends Thread{
ActivityManagerService mService;
boolean mRead=false;
public AThread(){
super("ActivityManager");
}
public void run(){
Looper.prepare(); //初始化事件循環之後調用loop()
android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_FOREGROUND):
android.os.Precess.setCanSelfBackground(false);
ActivityManagerService m=mew ActivityManagerService();
synchronized(this){
mSerice=m;
notifyAll();
}
synchronized(this){
while(!mReady){
try{
wait();
catch(InterruptedException e){
}
}
}
Looper.loop();
}
}
在Thread聲明周期中,存在NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED等多個狀態。線程的生命周期和狀態遷移過程如下圖:
下面是Thread中常用方法的說明,在實現Java線程時,務必掌握這些方法的用途。
getState() //獲取線程的當前狀態 interrupt() //中斷當前線程
interrupted() //判斷當前線程是否已中斷 isAlive //判斷線程是否仍在運行
isInterrupted() //判斷是否處於“中斷”狀態 sleep() //線程睡眠指定的時間
join() //線程同步處理 run() //線程暫停一段時間
start() //開啟線程 yield() //暫停當前線程,同優先級的其它線程獲得時間片
(2)基於Runable的實現
對於APP開發來說,由於應用層本身的特點,Runnable使用更加廣泛,下面是一個Runnable實現的示例:
private Runnable mForceCheckBoxRunnable =new Runnable(){
public void run(){
if(mCheckBoxPreference!=null){
mCheckBoxPreference.setChecked(!mCheckBoxPreference.isChecked());
}
mHandle.postDelayed(this, 1000);
}
};
另外,通過Handle的removeCallbacks()方法可以移除待運行的線程,通過postAtTime方法可以在特定時間將線程放入消息隊列。
(3)線程優先級
Android線程優先級是以及Linux設置的,其優先級是用數字表示的,范圍是-20~19,其中-20為最高優先級,19為最低優先級。優先級的設置通常用於處理並發線程產生的阻塞,防止重要性較低的線程占用大量的CPU資源。設置優先級的方法如下:
Process.setThreadPriority(priority);
根據適用場景的不同,Android提供了以下幾種常用場景的線程優先級:
THREAD_PRIORITY_AUDIO //值為-16,用於音樂播放場景
THREAD_PRIORITY_BACKGROUND //值為10,用於普通後台程序
THREAD_PRIORITY_DEFAULT //值為0,默認優先級
THREAD_PRIORITY_DISPLAY //值為-4,用於普通顯示線程
THREAD_PRIORITY_FOREGROUND //值為-2,用於普通前台程序
THREAD_PRIORITY_LESS_FAVORABLE //值為1,低於默認優先級
THREAD_PRIORITY_LOWEST //值為19,最低優先級
THREAD_PRIORITY_MORE_FAVORABLE //值為-1,最高默認優先級
THREAD_PRIORITY_URGENT_AUDIO //值為-19,重要的音頻線程
THREAD_PRIORITY_UNGENT_DISPLAY //值為-8,重要的顯示線程
2.Android線程封裝
在Android中,為了簡化開發者的工作,對Java線程進行了一定程度的封裝,最主要的工作包括AsyncTask封裝和接入UI線程。
(1)AsyncTask封裝
AsyncTask通常用於後台線程和UI線程的交互。雖然AsyncTask本質上還是Thread加Handler的一個封裝,但是由於采用了Java 1.5中的並發庫FutureTask,故其在處理異步事務時表現卓越。使用AsyncTask的示例如下:
private class ReceicePushTask extends AsyncTask
protected Void doInBackground(Intent... instentd){
Intent intent-intents[0];
}
public void onReceive(Context context, Intent intent){}
}
AsyncTask中最基本的兩個方法為doInBackground和onReceive,前者執行真正的後台計算,後者向UI主線程反饋計算結果。為了使用AsyncTask,開發者必須對AsyncTask進行繼承。
AsyncTask的其他重要方法如下:
public final AsyncTask
public void onPostExecute(Result result) //在doInBackGround方法執行後執行,該方法在UI線程中執行
protected void onPreExecute() //在執行doInBackground方法前執行
protected final void publishProgress(Progress... values) //向UI線程反饋計算進度
public final boolean cancel(boolean mayInterruptIfRunning) //中斷線程執行
public final boolean isCancelled() //判斷AsyncTask是否已經被中斷
在Android3.0中,Google又引入了execute(Runnable runnable)等多個方法。AsyncTask可以支持多個輸入參數,甚至可變參數,其參數在execute方法中傳入。
在中斷開始執行時,系統將會調用AsyncTask的onCancelled方法,開發者可以在該方法中進行一些收尾工作,但是要注意,必須顯式判斷線程是否中斷。假設在某個目錄下尋找和關鍵字匹配的文件和文件夾,後台計算的工作被放置在AsyncTask中,顯式判斷線程是否中斷的工作在dosth中進行,示例如下:
SearchTask mSearchTask=new SearchTask();
mSearchTask.execute(path) //執行線程
public void dosth(String value){
for(...){
if(mSearchTask.isCancelled()){
return;
}
}
}
下面是SearchTask的實現,其中包含了後台計算的入口、正常結束的入口、線程中斷的入口等。
class SearchTask extends AsyncTask
public void doInBackground(String... value){
dosth(value[0]);
}
pretected void onCanceled(){
//執行中斷的善後工作
}
protected void onPostExecute(Result result){ //該方法在UI線程執行
//執行正常的結束工作
}
}
(2)接入UI線程
在多線程編程中,執行後台計算的線程通常需要和UI線程進行交互,將計算的結果反饋給UI線程,呈現給用戶,而傳統的方法一般需要借組Handle。在Android中,充分考慮了開發者者方面的需要,從Activity、View和UI主線程3個層次提供了4種方式來接入UI線程。
1)Activity的runOnUiThread(Runnable)方法
Activity提供的接入UI線程的方法實際上是通過runOnUiThread(Runnable)方法進行的,從本質上講,runOnUiThread方法時基於Handler的post(Runnable r)方法實現的。runOnUiThread(Runnable)的示例如下:
mActivity.runOnUiThread(new Runnable(){
public void run(){
mToast.setGravity(Gravity.BOTTOM, 0, 0);
mToast.show();
}
});
2)View的post(Runnable)方法
View的post(Runnable)方法從控件層面對多線程提供了支持,是開發更加靈活,其實現原理與基於Activity的runOnUiThread(Runnable)方法類似,均是借助Handle進行的。關於View的post(Runnable)方法的示例如下:
public void onClick(View v){
new Thread(new Runnable(){
public void run(){
finnal Bitmap bitmap=loadImageFromNetwork(http://example.com/image.png);
mImageView.post(new Runnable(){
public void run(){
mImageView.setImageBitmap(bitmap);
}
});
}
}).start
}
(3)View的postDelayed(Runnable, long)方法
View的postDelayed(Runnable, long)方法提供了延時處理的能力,示例如下:
final static int PAUSE_DELAY=100;
Runnable mRunnable=new Runnable(){
public void run(){
mView.postDelayed(mRunnable, PAUSE_DELAY);
}
};
(4)Handler方法
利用Message消息向UI主線程發送後台計算的消息,及時將計算結果反饋給實際用戶,這一切均需要借助Handle進行消息處理。當UI線程接收到Message消息後,會調用其Handler進行消息處理。Handler處理消息的過程如下:
Message message=mHandle.obtainMessage(ORIENTATION_CHANGED);
mHandler.sendMessage(message);
Handler mHandler=new Handler(){
public void handleMessage(Message msg){
switch(msg.what){
case ORIENTATION_CHANGED;
break;
}
}
};
3.線程間的消息通信
相比Thread加Handler的封裝,Async更為可靠,更易於維護。AsyncTask的缺點是,一旦線程開啟即dobackground方法執行後無法向線程發送消息,僅能通過預先設置好的標記來控制,當然可以通過掛起線程並等待標志位的改變來進行通信。
對於某些應用,原生的Thread加Handler以及Looper可能更靈活。下圖揭示了三者之間的關系。
(1)消息隊列
消息隊列的維護是在線程中進行的,但默認除UI主線程外,其他線程並不擁有消息隊列。為了維護一個消息隊列,需要在線程中調用Looper的prepare方法進行初始化。為了使線程可以接收發送過來的消息,通常需要借組Handler來接收消息。一個維護消息隊列的示例如下:
private class Queryrunner extends Thread{
public Handler mHandle
public void run(){
Looper.prepare(); //loop()方法應隨後調用
mHandler=new Handler(){
public void handleMessage(Message msg){//處理收到的消息}
};
Looper.loop();
};
}
通過Looper還可以判斷當前線程是否為UI主線程,方法如下:
private void assertUiThread(){
if(!Looper.getMainLooper().equals(Looper.myLooper())){
throw new RuntimeException("Not on the UI thread!");
}
}
通過查看Message的實現即可發現,Message繼承於Parcelable,在消息的傳遞過程中,實際上傳遞的是序列化數據。Message的實現如下:
public final class message implements Parcelable{
public int what; //消息表示符
public int arg1; //傳遞參數
public int arg2; //傳遞對象
public Object obj; //傳遞的對象
public Messager replyTo;
long when;
Bundle data;
Handle target; //目標Handler
Runnable callback;
Message next;
private static final int MAX_POOL_SIZE=10;
...
}
通過分析Message的源代碼實現可以發現,一個線程會自動維護一個消息池,該消息池的大小為10.在需要生成消息時,首先從消息池中獲取消息,只有當消息池中的消息均被使用時,才會重新創建新消息,當消息使用結束時,消息池會回收消息。
從obtain方法的實現中可以看出,在發送消息時,通過obtain方法獲得Message比創建Message的效率更高。發送消息的示例如下:
public void progress(boolean progress){
android.os.Message msg=android.os.Message.obtain();
msg.what=MSG_PROGRESS;
msg.arg1=progress ? 1: 0;
sendMessage(msg);
}
(2)消息分發
消息的分發和接收均與Handler密切相關,對於消息的分發,Android目前提供了兩種消息類型,一種是post消息,一種是send消息。其中post消息會在為了某一時間加入消息隊列,而send消息則會立即加入到消息隊列。
1)send消息
分發一個簡單的send消息的方法如下:
Messagemsg=mHandler.obtainMessage(KILL_APPLICATION_MSG);
msg.arg1=uid;
msg.arg2=0;
msg.obj=pkg;
mHandler.sendMessage(msg);
如果沒有參數需要傳遞,那麼可以發送僅攜帶消息表示符的空消息,方法如下:
mHandler.sendEnptyMessage(CANCEL_HEAVY_NOTIFICATION_MSG);
2)post消息
通過post消息可以實現類似循環計算的功能,方法如下:
mTicker = new Runnable(){
public void run(){
if(mTickerStopped) return;
mCalender.setTimeInMillis(System.currentTimeMillis());
setText(DateFormat(mFormat, mCalendar));
invalidate();
long now=SystemClock.uptimerMillis();
long next=now + (1000-now%1000);
mHandler.postAtTime(mTicker, next);
}
};
mTicker.run();
(3)消息接收
消息的接收相對簡單,但是如果存在重入問題,應注意數據的同步。關於數據安全的內容。在Handler中消息接收的過程如下:
public void dispatchMessage(Message msg){
if(msg.callback !=null){
handleCallback(msg); //通過Message自身回調進行處理
}else{
if(mCallback.handleMessage(msg)){
return;
}
}
handleMessage(msg); //handle自己處理
}
對於消息隊列而言,如何監聽消息的來臨呢,筆者的經驗是,在Looper的loop方法中執行對消息的監聽。loop方法的實現如下:
public static final void loop(){
Looper me=myLooper();
MessageQueue queue=me.mQueue;
Binder.clearCallingIdentity();
finnal long ident=Binder.clearCallingIdentity();
if(msg!=null){
if(msg.target==null){
return;
}
msg.target.dispatchMessage(msg); //調用Handler進行消息處理
msg.recycle(); //消息回收
}
}
4.線程安全處理
線程安全多是由多線程對共享資源的訪問引起的,在線程安全層面,Android更多的是采用Java的實現,除了Java的join()、wait()、sleep()、notify()等安全方法和synchronized關鍵字外,還有Java的並發庫。
除了synchronized關鍵字外,基於Java的多線程安全均比較復雜,對於不是十分復雜的場景,優先進行多線程處理。
(1)synchronized同步
在Android應用層,線程的並發很多是靠synchronized關鍵字來實現的,這樣的實現非常簡單。通過synchronized關鍵字,可以實現方法和語句塊的同步。同步的本質是對特定的對象加鎖,他們可以是來自調用方法的對象,也可以是開發者設計的對象。
另外,synchronized關鍵字並不能繼承,對於父類的同步方法,在子類中必須再次顯式聲明才能成為同步方法。
synchronized關鍵字的局限在於在試圖獲得鎖時無法設定超時和中斷,每個鎖只有一個條件,在某些場景下可能不夠用。
另外,同步會帶來較大的系統開銷,甚至造成死鎖,因此在進行開發時應避免無謂的同步。synchronize關鍵字可以實現方法同步和語句塊同步。
1)方法同步
方法同步分一般方法同步和靜態方法同步兩種。一般方法同步的本質在於將synchronized關鍵字作用於對象引用(object referece),作用域僅限類的單個對象,下面是AbsSeekBar中的一個實現:
public synchronized void setMax(int max){
super.setMax(max);
}
以上實現等同於如下實現:
public void setMax(int max){
synchronized(this){
super.setMax(max);
}
}
靜態方法同步的本質是將類本身作為所,其作用域是該類的所有對象。下面是BitmapManaget中利用synchronized實現單子模式的過程。
private static BitmapManager sManager=null;
public static synchronized BitmapManaget instance(){
if(sManager==null){
sManager=new BitmapManager();
}
}
2)語句塊的同步
方法同步的作用域較大,但事實上需要同步的范圍可能並沒那麼大。當需要同步的范圍不是很大時,可采用語句塊同步。下面是AccelerometerListener中的一個示例:
private void setOrientation(int orientation){
synchronized(this){
}
}
將對象引用本身作為鎖,顯然影響並發效率,更靈巧的設計是自定義鎖。自定義鎖可以是任何類型的對象,但通常將其類型設計為Object。AbstractPreferences中的一個示例如下:
public abstract class AbstractPreferences extends Preferences
{
protected final Object lock;
protected AbstractPreferences getChild(String name) throws
BackingStoreException{
synchronized(lock){
}
}
}
采用自定義鎖可以帶來更高的並發效率,當然具體采用什麼樣的鎖,需要根據實際場景決定。
(2)RPC通信
在基於AIDL的通信中,由於允許多個客戶端的存在,其實現必須是線程安全的,開發者要注意,對於IPC調用方法,如果是一種耗時的操作,應避免在UI主線程中調用,以免阻塞UI主線程。
(3)SQLite調用
對於SQLite的調用,可能會存在多處執行讀寫操作的場景,這種場景也需要考慮線程的安全性。為了方便開發者操作,Android提供了類似於AsyncTask的AsyncQueryHandler方法來解決這一問題,將耗時的查詢等操作放置在非UI主線程中,操作結束後,通過Handler調用相應的UI主線程的方法處理操作執行的結果,示例如下:
AsyncQueryHandler mAsyncQueryHandler=new AsyncQueryHandler(getContectResolver()){
protected void onQueryComplete(int token, Object cookie, Cursor cursor){
if(cursor!=null && cursor.moveToFirst()){
}
}
}
mAsyncQueryHandler.startQuery(0, null, mUri, new String[] {
MediaStore.Audio.Media.TITLE,MediaStore.Audio.Media.ARTIST
},MediaStore.Audio.Media,DATA+”=?“, new string[]{path}, null());
可能有些同學不明白,為啥要圖片反轉(不是旋轉哦),我們在游戲開發中,為了節省圖片資源(空間) 有可能會使用到圖片反轉,例如,一個人物圖片,面向左,或右,如果不能實現圖片反
您應該始終外部化應用資源,例如圖像和代碼中的字符串,這樣有利於您單獨維護這些資源。 此外,您還應該為特定設備配置提供備用資源,方法是將它們分組到專門命名的資源目錄中。 在
引言前面一篇文章介紹了二級Preference的使用和特點,接下來進入系統給我提供的底級Preference的使用CheckBox選擇項CheckBoxPreferenc
一. 首先在xml添加ViewPager控件 我們希望每個viewpager顯示一張圖片 代碼如下 二.分析