摘要:系統創建好進程后,實際上就啟動執行了該進程的主執行線程。也就是說,對線程來說,進程相當于一個容器,可以有許多線程同時在一個進程里執行。默認情況下,同一應用程序下的所有組件都運行在相同的進程和線程一般稱為程序的主線程中。
一 前言
異步編程是android初學者的一個難點,卻也是始終不能繞過的一個坎。可以說幾乎每個app都逃不了網絡編程,而網絡編程又往往建立在異步的機制之上(你不應該也無法在UI線程里執行網絡請求,你也不應該在UI線程中頻繁的進行IO操作)。等等,你不知道什么是線程?那就對了,我們一起來回憶一下大學課本的知識,一切從進程講起。
二 進程和線程我曾經在知乎上聽一個朋友說一個優秀的程序員一定會有著極強的對抽象的理解能力,我很贊同這句話,我心里一直鼓勵自己:當你對抽象不再懼怕的時候,可能你正在成為一名真正的coder。
1.進程(process)
A process is the operating system’s abstraction for a running program
這是csapp中的原話,我覺得兩個詞特別重要,一個是abstraction,說明進程是一種抽象,是人為的一種定義,另一個是running,說明進程是正在執行的程序,而不是保存在磁盤上的一個程序文件。不管你現在怎么理解進程,你都得看下面一段代碼:
#includeint main() { printf("hello, world "); return 0; }
這可能是我們人生寫得第一行代碼,讓我們在終端里gcc得到可執行文件a.out,然后執行它,好,在你按下return鍵的那一瞬間到終端里打印出hello,world(好吧我承認我詞窮了,其實就是a.out被執行時),進程動態產生,動態消亡。怎么直觀的感受它呢,來改一下代碼:
#include#include int main(){ printf("Hello World from process ID %ld ",(long)getpid()); return 0; }
編譯,運行得到:
Hello World from process ID 20289
在這里我們得到了這個進程的ID(UNIX系統確保每個進程都有一個唯一的數字標識符,稱為進程ID,進程ID總是一個非負整數。),這也算進程存在的一點痕跡吧。我們再改動一下代碼:
#include#include int doSomething(); int main(){ printf("Hello World from process ID %ld ",(long)getpid()); doSomething(); return 0; } int doSomething(){ printf("let us doing something from ID %ld ",(long)getpid()); return 0;
編譯,執行:
Hello World from process ID 20777 let us doing something from ID 20777
可以看到這兩個函數的進程ID是一樣的,其實你進一步調用getppid()函數得到父進程的函數其實也是一樣的。細心的朋友就會發現,上一次執行后得到的ID是20289,這次執行得到的ID卻是20777,同樣的文件為什么每次執行得到的ID卻是不同的呢?這就需要我們好好體會進程是動態產生動態消亡的了,抽象嗎?
2.線程(Thread)
可以這么說,一切的抽象都是為了解放生產力。系統為什么要抽象出進程的概念?一個直觀的解釋就是它可以讓每個進程獨立的擁有虛擬地址空間、代碼、數據和其它各種系統資源,它還可以讓多個進程同時執行,讓你在寫代碼的同時還能掛著微信,放著音樂。可是這還不夠,因為一個進程在某一時刻只能做一件事情,為了進一步提高效率,又抽象出進程的概念,來看下面這段話:
線程是進程內部的一個執行單元。系統創建好進程后,實際上就啟動執行了該進程的主執行線程。主執行線程終止了,進程也就隨之終止。
也就是說,對線程來說,進程相當于一個容器,可以有許多線程同時在一個進程里執行。
3.安卓中的進程與線程
這里引用官方文檔的解釋,也不知是誰翻譯的,總之獻上膝蓋看原網頁點這里為了閱讀方便把原文貼出來了并改正了一些錯別字
當一個Android應用程序組件啟動時候,如果此時這個程序的其他組件沒有正在運行,那么系統會為這個程序以單一線程的形式啟動一個新的Linux 進程。 默認情況下,同一應用程序下的所有組件都運行在相同的進程和線程(一般稱為程序的“主”線程)中。如果一個應用組件啟動但這個應用的進程已經存在了(因為這個應用的其他組件已經在之前啟動了),那么這個組件將會在這個進程中啟動,同時在這個應用的主線程里面執行。然而,你也可以讓你的應用里面的組件運行在 不同的進程里面,也可以為任何進程添加額外的線程。
這片文章討論了Android程序里面的進程和線程如何運作的。
進程
默認情況下,同一程序的所有組件都運行在相同的進程里面,大多數的應用都是這樣的。然而,如果你發現你需要讓你的程序里面的某個組件運行在特定的進程里面,你可以在manifest 文件里面設置。
manifest 文件里面為每一個組件元素—
Android 可能在系統剩余內存較少,而其他直接服務用戶的進程又要申請內存的時候shut down 一個進程, 這時這個進程里面的組件也會依次被kill掉。當這些組件有新的任務到達時,他們對應的進程又會被啟動。
在決定哪些進程需要被kill的時候,Android系統會權衡這些進程跟用戶相關的重要性。比如,相對于那些承載這可見的activities的 進程,系統會更容易的kill掉那些承載不再可見activities的進程。決定是否終結一個進程取決于這個進程里面的組件運行的狀態。下面我們會討論 kill進程時所用到的一些規則。
進程的生命周期
作為一個多任務的系統,Android 當然系統能夠盡可能長的保留一個應用進程。但是由于新的或者更重要的進程需要更多的內存,系統不得不逐漸終結老的進程來獲取內存。為了聲明哪些進程需要保 留,哪些需要kill,系統根據這些進程里面的組件以及這些組件的狀態為每個進程生成了一個“重要性層級” 。處于最低重要性層級的進程將會第一時間被清除,接著是重要性高一點,然后依此類推,根據系統需要來終結進程。
在這個重要性層級里面有5個等級。下面的列表按照重要性排序展示了不同類型的進程(第一種進程是最重要的,因此將會在最后被kill):
Foreground進程 一個正在和用戶進行交互的進程,如果一個進程處于下面的狀態之一,那么我們可以把這個進程稱為 foreground 進程:
進程包含了一個與用戶交互的 Activity (這個 Activity的 onResume() 方法被調用)。
進程包含了一個綁定了與用戶交互的activity的 Service 。
進程包含了一個運行在”in the foreground”狀態的 Service —這個 service 調用了 startForeground()方法。
進程包含了一個正在運行的它的生命周期回調函數 (onCreate(), onStart(), oronDestroy())的 Service 。
進程包含了一個正在運行 onReceive() 方法的 BroadcastReceiver 。
一般說來,任何時候,系統中只存在少數的 foreground 進程。 只有在系統內存特別緊張以至于都無法繼續運行下去的時候,系統才會通過kill這些進程來緩解內存壓力。在這樣的時候系統必須kill一些 (Generally, at that point, the device has reached a memory paging state,這句如何翻譯較好呢)foreground 進程來保證 用戶的交互有響應。
Visible進程 一個進程沒有任何 foreground 組件, 但是它還能影響屏幕上的顯示。 如果一個進程處于下面的狀態之一,那么我們可以把這個進程稱為 visible 進程:
進程包含了一個沒有在foreground 狀態的 Activity ,但是它仍然被用戶可見 (它的 onPause() 方法已經被調用)。這種情況是有可能出現的,比如,一個 foreground activity 啟動了一個 dialog,這樣就會讓之前的 activity 在dialog的后面部分可見。
進程包含了一個綁定在一個visible(或者foreground)activity的 Service 。
一個 visible 進程在系統中是相當重要的,只有在為了讓所有的foreground 進程正常運行時才會考慮去kill visible 進程。
Service進程 一個包含著已經以 startService() 方法啟動的 Service 的 進程,同時還沒有進入上面兩種更高級別的種類。盡管 service 進程沒有與任何用戶所看到的直接關聯,但是它們經常被用來做用戶在意的事情(比如在后臺播放音樂或者下載網絡數據),所以系統也只會在為了保證所有的 foreground and visible 進程正常運行時kill掉 service 進程。
Background進程 一個包含了已不可見的activity的 進程 (這個 activity 的 onStop() 已 經被調用)。這樣的進程不會直接影響用戶的體驗,系統也可以為了foreground 、visible 或者 service 進程隨時kill掉它們。一般說來,系統中有許多的 background 進程在運行,所以將它們保持在一個LRU (least recently used)列表中可以確保用戶最近看到的activity 所屬的進程將會在最后被kill。如果一個 activity 正確的實現了它的生命周期回調函數,保存了自己的當前狀態,那么kill這個activity所在的進程是不會對用戶在視覺上的體驗有影響的,因為當用戶 回退到這個 activity時,它的所有的可視狀態將會被恢復。查看 Activities 可以獲取更多如果保存和恢復狀態的文檔。
Empty 進程 一個不包含任何活動的應用組件的進程。 這種進程存在的唯一理由就是緩存。為了提高一個組件的啟動的時間需要讓組件在這種進程里運行。為了平衡進程緩存和相關內核緩存的系統資源,系統需要kill這些進程。
Android是根據進程中組件的重要性盡可能高的來評級的。比如,如果一個進程包含來一個 service 和一個可見 activity,那么這個進程將會被評為 visible 進程,而不是 service 進程。
另外,一個進程的評級可能會因為其他依附在它上面的進程而被提升—一個服務其他進程的進程永遠不會比它正在服務的進程評級低的。比如,如果進程A中 的一個 content provider 正在為進程B中的客戶端服務,或者如果進程A中的一個 service 綁定到進程B中的一個組件,進程A的評級會被系統認為至少比進程B要高。
因為進程里面運行著一個 service 的評級要比一個包含background activities的進程要高,所以當一個 activity 啟動長時操作時,最好啟動一個 service 來 做這個操作,而不是簡單的創建一個worker線程—特別是當這個長時操作可能會拖垮這個activity。比如,一個需要上傳圖片到一個網站的 activity 應當開啟一個來執行這個上傳操作。這樣的話,即使用戶離開來這個activity也能保證上傳動作在后臺繼續。使用 service 可以保證操作至少處于”service process” 這個優先級,無論這個activity發生了什么。這也是為什么 broadcast receivers 應該使用 services 而不是簡單的將耗時的操作放到線程里面。
線程
當一個應用啟動的時候,系統會為它創建一個線程,稱為“主線程”。這個線程很重要因為它負責處理調度事件到相關的 user interface widgets,包括繪制事件。你的應用也是在這個線程里面與來自Android UI toolkit (包括來自 android.widget 和 android.view 包的組件)的組件進行交互。因此,這個主線程有時候也被稱為 UI 線程。
系統沒有為每個組件創建一個多帶帶的線程。同一進程里面的所有組件都是在UI 線程里面被實例化的,系統對每個組件的調用都是用過這個線程進行調度的。所以,響應系統調用的方法(比如 onKeyDown() 方法是用來捕捉用戶動作或者一個生命周期回調函數)都運行在進程的UI 線程里面。
比如,當用戶點擊屏幕上的按鈕,你的應用的UI 線程會將這個點擊事件傳給 widget,接著這個widget設置它的按壓狀態,然后發送一個失效的請求到事件隊列。這個UI 線程對請求進行出隊操作,然后處理(通知這個widget重新繪制自己)。
當你的應用與用戶交互對響應速度的要求比較高時,這個單線程模型可能會產生糟糕的效果(除非你很好的實現了你的應用)。特別是,當應用中所有的事情 都發生在UI 線程里面,那些訪問網絡數據和數據庫查詢等長時操作都會阻塞整個UI線程。當整個線程被阻塞時,所有事件都不能被傳遞,包括繪制事件。這在用戶看來,這個 應用假死了。甚至更糟糕的是,如果UI 線程被阻塞幾秒(當前是5秒)以上,系統將會彈出臭名昭著的 “application not responding” (ANR) 對話框。這時用戶可能選擇退出你的應用甚至卸載。
另外,Android的UI 線程不是線程安全的。所以你不能在一個worker 線程操作你的UI—你必須在UI線程上對你的UI進行操作。這有兩條簡單的關于Android單線程模型的規則:
不要阻塞 UI 線程
不要在非UI線程里訪問 Android UI toolkit
Worker 線程
由于上面對單一線程模型的描述,保證應用界面的及時響應同時UI線程不被阻塞變得很重要。如果你不能讓應用里面的操作短時被執行玩,那么你應該確保把這些操作放到獨立的線程里(“background” or “worker” 線程)。
比如,下面這段代碼在一個額外的線程里面下載圖片并在一個 ImageView顯示:
new Thread(new Runnable(){ public void run(){ Bitmap b = loadImageFromNetwork("http://example.com/image.png"); mImageView.setImageBitmap(b); } }).start();}
起先這段代碼看起來不錯,因為它創建一個新的線程來處理網絡操作。然而,它違反來單一線程模型的第二條規則: 不在非UI線程里訪問 Android UI toolkit—這個例子在一個worker線程修改了 ImageView 。這會導致不可預期的結果,而且還難以調試。
為了修復這個問題,Android提供了幾個方法從非UI線程訪問Android UI toolkit 。詳見下面的這個列表:
Activity.runOnUiThread(Runnable)
View.post(Runnable)
View.postDelayed(Runnable, long)
那么,你可以使用 View.post(Runnable) 方法來修改之前的代碼:
public void onClick(View v){ new Thread(new Runnable(){ public void run(){ final Bitmap bitmap = loadImageFromNetwork("http://example.com/image.png"); mImageView.post(new Runnable(){ public void run(){ mImageView.setImageBitmap(bitmap); } }); } }).start();}
現在這個方案的線程安全的:這個網絡操作在獨立線程中完成后,UI線程便會對ImageView 進行操作。
然而,隨著操作復雜性的增長,代碼會變得越來越復雜,越來越難維護。為了用worker 線程處理更加復雜的交互,你可以考慮在worker線程中使用Handler ,用它來處理UI線程中的消息。也許最好的方案就是繼承 AsyncTask 類,這個類簡化了需要同UI進行交互的worker線程任務的執行。
使用 AsyncTask
AsyncTask 能讓你在UI上進行異步操作。它在一個worker線程里進行一些阻塞操作然后把結果交給UI主線程,在這個過程中不需要你對線程或者handler進行處理。
使用它,你必須繼承 AsyncTask 并實現 doInBackground() 回調方法,這個方法運行在一個后臺線程池里面。如果你需要更新UI,那么你應該實現onPostExecute(),這個方法從 doInBackground() 取出結果,然后在 UI 線程里面運行,所以你可以安全的更新你的UI。你可以通過在UI線程調用 execute()方法來運行這個任務。
比如,你可以通過使用 AsyncTask來實現之前的例子:
public void onClick(View v){ new DownloadImageTask().execute("http://example.com/image.png"); } private class DownloadImageTask extends AsyncTask{ protected Bitmap doInBackground(String... urls){ return loadImageFromNetwork(urls[0]); } protected void onPostExecute(Bitmap result){ mImageView.setImageBitmap(result); }}
現在UI是安全的了,代碼也更加簡單了,因為AsyncTask把worker線程里做的事和UI線程里要做的事分開了。
你應該閱讀一下 AsyncTask 的參考文檔以便更好的使用它。下面就是一個對 AsyncTask 如何作用的快速的總覽:
你可以具體設置參數的類型,進度值,任務的終值,使用的范型
doInBackground() 方法自動在 worker 線程執行
onPreExecute(), onPostExecute(), 和 onProgressUpdate() 方法都是在UI線程被調用
doInBackground() 的返回值會被送往 onPostExecute()方法
你可以隨時在 doInBackground()方法里面調用 publishProgress() 方法來執行UI 線程里面的onProgressUpdate() 方法
你可以從任何線程取消這個任務
注意: 你在使用worker線程的時候可能會碰到的另一個問題就是因為runtime configuration change (比如用戶改變了屏幕的方向)導致你的activity不可預期的重啟,這可能會kill掉你的worker線程。為了解決這個問題你可以參考 Shelves 這個項目。
線程安全的方法
在某些情況下,你實現的方法可能會被多個線程所調用,因此你必須把它寫出線程安全的。
大家先不要困在上面這篇文章中的具體代碼實現上,把關注點放在Android中進程,線程和android基本組件之間的關系上。我們看完了如何在java中進行線程操作之后再去學習Android相關機制就會相對容易一些。
三 java并發編程java并發編程是一個很龐大的話題,我不會也沒有能力講得過于深入,我沒辦法告訴你淘寶網是怎么處理每秒成千上萬次的點擊而屹立不倒,我只會講一下為什么我們可以利用java并發編程讓應用在下載文件的同時UI不會卡頓。java并發操作可以讓我們把一個程序分成幾部分,各自獨立的去完成任務。好首先我們來定義一下這里的任務(tasks)。
1.定義tasks
一個線程承載著一個任務,如何描述它呢?java中提供Runnable這個接口,來,上代碼:
public class ExampleTask implements Runnable { private static int taskCount = 0; private final int id = taskCount++; protected int count = 10; private String status(){ return "#"+id+": "+"count is "+count; } @Override public void run() { while (count -- > 0){ System.out.println(status()); Thread.yield();//the part of the Java threading mechanism that moves the CPU from one thread to the next } } }
注意靜態變量taskCount和final變量int,是為了該類每次被實例化時能有一個獨一無二的id。
在覆寫的run方法中我們通常放入一個循環,先不用理會yield方法。
然后我們在一個線程中將它實例化并調用run方法:
public class MainThread { public static void main(String args[]){ ExampleTask exampleTask = new ExampleTask(); exampleTask.run(); } }
結果如下:
#0: count is 9 #0: count is 8 #0: count is 7 #0: count is 6 #0: count is 5 #0: count is 4 #0: count is 3 #0: count is 2 #0: count is 1 #0: count is 0
這里并沒有什么特別之處,只是被main方法調用而已(也就是存在于系統分配給main的線程中)。
2 Thread類
Thread類被實例化時,即在當前進程中創建一個新的線程,來看代碼:
public class BasicThread { public static void main(String[] args){ Thread t = new Thread(new ExampleTask()); t.start(); System.out.println("ExampleTask任務即將開始"); } }
可以看出我們需要將ExampleTask傳給Thread的構造方法,上面說過任務是對線程的描述,這里也就不難理解了。我們先看一下執行結果:
結果一
#0: count is 9 ExampleTask任務即將開始 #0: count is 8 #0: count is 7 #0: count is 6 #0: count is 5 #0: count is 4 #0: count is 3 #0: count is 2 #0: count is 1 #0: count is 0
結果二
ExampleTask任務即將開始 #0: count is 9 #0: count is 8 #0: count is 7 #0: count is 6 #0: count is 5 #0: count is 4 #0: count is 3 #0: count is 2 #0: count is 1 #0: count is 0
不用奇怪我為什么給出這兩種結果(尤其是第一種),因為在多次運行試驗中確確實實出現了這兩種結果。我們來分析一下,當我們實例化Thread并將Task傳遞給它時,當前進程將在main()線程之外重新創建一個t線程,然后我們執行t.start(),這個方法會做一些必要的線程初始化的工作然后就通知t線程里的ExampleTask任務需要執行run方法了,然后start會迅速return到main()線程,所以我們不必等到ExampleTask里的run方法里面的循環執行完就可以看見
ExampleTask任務即將開始
至于為什么會發現第一種情況,我猜測是由于start返回的不夠快,讓t線程搶先了(對,就這么生動的理解線程你就不會怕了,雖然解釋的很糟糕)
再看看下面這代碼:
public class MoreBasicThread { public static void main(String args[]){ for (int i=0;i<5;i++){ Thread t = new Thread(new ExampleTask()); t.start(); } System.out.println("前方高能!多個線程即將開始打架!"); } }
現在你可以回過頭去看一下這段代碼了:
public void onClick(View v){ new Thread(new Runnable(){ public void run(){ final Bitmap bitmap = loadImageFromNetwork("http://example.com/image.png"); mImageView.post(new Runnable(){ public void run(){ mImageView.setImageBitmap(bitmap); } }); } }).start();}四 AsyncTask
有了上面這些知識的鋪墊,我們回到Android中。我們設想一個場景,當用戶點擊某個Button時,我們想從網絡上加載一些文本到當前UI,前面說了我們沒辦法在UI線程中直接進行網絡請求(因為可能會有阻塞UI線程的風險),現在我們很容易想到在當前進程中再創建一個線程,讓其執行網絡請求,請求完成后再來更新UI,比如上面的方案,我們還可以用安卓給我們提供的AsyncTask,使用起來更加方便,也更容易維護,操作起來:
1.準備工作
public class Loader { public byte[] getUrlBytes(String urlSpecfic)throws IOException{ URL url = new URL(urlSpecfic); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); try { if (connection.getResponseCode() != HttpURLConnection.HTTP_OK){ throw new IOException(connection.getResponseMessage()+"with"+urlSpecfic); } ByteArrayOutputStream out = new ByteArrayOutputStream(); InputStream in = connection.getInputStream(); byte[] buffer = new byte[1024]; int byteRead = 0; while ((byteRead = in.read(buffer))>0){ out.write(buffer,0,byteRead); } out.close(); return out.toByteArray(); }finally { connection.disconnect(); } } public String getUrlString(String urlSpecific) throws IOException{ return new String(getUrlBytes(urlSpecific)); } }
這個類的主要作用是請求特定url的網絡資源,不理解的話要么跳過,要么去找一本java書回顧一下java網絡編程。
接下來是布局文件:很簡單,一個TextView,一個Button
2.使用AsyncTask
public class MainActivity extends AppCompatActivity { private TextView urlText; private Button urlButton; private Loader loader = new Loader(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); urlText = (TextView) findViewById(R.id.url_text); urlButton = (Button) findViewById(R.id.url_button); urlButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { new DownLoader().execute("https://segmentfault.com/"); } }); } private class DownLoader extends AsyncTask{ @Override protected String doInBackground(String... params) { try { return loader.getUrlString(params[0]); } catch (IOException e) { e.printStackTrace(); } return null; } @Override protected void onPostExecute(String s) { urlText.setText(s); } } }
這里覆寫了兩個方法,doInBackground會在一個新的線程里執行,參數類型由AsyncTask的第一個泛型參數決定,返回參數由AsyncTask的第三個泛型參數決定,其返回值會傳遞給onPostExecute方法。而onPostExecute方法是可以操作UI線程的,故用其為urlText賦值。好,編譯,運行,點擊按鈕,幾秒鐘后urlText里的內容便被請求回來的segmentfault的首頁html所替換。
五 后記設想如果我們需要請求的內容遠不止一個html文件,可能是一個非常龐大的json數據或者是無窮無盡的圖片資源,如果還用上面的方法,恐怕用戶會在urlText前等到終老,別擔心,安卓提供了非常令人頭痛但是也同樣非常高效的異步機制HandlerThread,Looper,Handler以及Message。別怕,別虛。下次我們一起征服。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/65602.html
摘要:當然,如果你的核心數夠多,到個線程的并行度不滿足的話,也可以自定義一個線程池來執行,不過這樣的話,要注意自己維護這個線程池的初始化,釋放等等操作了。 事情起源于一個bug排查,一個AsyncTask的子類,執行的時候發現onPreExecute方法執行了,doInBackground卻遲遲沒有被調用。懂AsyncTask一些表面原理的都知道,onPreExecute方法是在主線程執行,...
閱讀 2568·2023-04-25 17:33
閱讀 648·2021-11-23 09:51
閱讀 2951·2021-07-30 15:32
閱讀 1395·2019-08-29 18:40
閱讀 1940·2019-08-28 18:19
閱讀 1465·2019-08-26 13:48
閱讀 2237·2019-08-23 16:48
閱讀 2275·2019-08-23 15:56