編輯:關於Android編程
在Android開發的時候,當我們需要完成一個耗時操作的時候,通常會新建一個子線程出來,例如如下代碼
new Thread(new Runnable() { @Override public void run() { //耗時代碼 } }).start();這種方式的線程隨處可見,但是這種方式的寫法是存在一定問題的,我們知道,在操作系統中,線程是操作系統調度的最小單元,同時線程又不能無限制的產生,並且線程的創建和銷毀都會有資源的開銷,同時當線程頻繁的創建或者銷毀的時候,還會讓GC頻繁的運行,造成程序的卡頓,例如當我們需要網絡請求的時候,一定是講網絡請求的代碼放到子線程中去運行的,同時如果是ListView中圖片的畫,采用傳統的new Thread的形式,會在ListView滑動的時候,一下開數十個子線程,程序就會卡頓起來;或者當我們進行下載的時候,通常會指定下載的優先級,優先級高的優先下載,優先級低的會暫停排隊,這種需求傳統的Thread也是做不到的。那麼這就需要用到線程池了。
最後總結一下這種直接使用Thread的缺點
線程池從名字就可以看出,它是用來管理線程的,在線程池中,我們的線程不會被隨意的創建出來,它可以緩存一定數量的線程,減少了資源的消耗,同時還可以指定線程的優先級,或者同時需要大量在耗時任務的時候,這些耗時操作是使用FIFO還是LIFO的策略。
Android中的線程池來源於Java,它們主要是通過Executor來派生指定的線程池,使用起來比較方便
總結來說,線程池的有點可以概括為以下三點:
ThreadPoolExecutor是線程池的真正實現,它的構造方法提供了一系列參數來配置線程池,下面是線程池的一個構造方法:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue我們來看看這幾個參數的意義:(以下的所說的任務可以理解為實現Runnable接口的對象)workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
threadPoolExecutor.allowCoreThreadTimeOut(true);
值得注意的是,並不是說只有核心線程才能去執行任務,而是核心線程是最穩定的線程,在默認狀態下,它們不會銷毀,這樣在新的任務需要執行的時候,就會很節省時間,所以核心線程數只需要保證大於0就可以了。
線程池所能容納的最大線程數,當線程池中的線程達到這個數值,後續的新任務就會被阻塞,無法再添加到線程池中,會被線程池所拒絕,注意最大線程數一定要比核心線程數大的。
而TimeUnit是時間的單位,它是一個枚舉類型,從天到納秒之間的單位都有的,常用的有MILLISECONDS(毫秒)、SECONDS(秒)和MINUTES(分鐘)。值得注意的是,如果配置了線程池的allowCoreThreadTimeOut屬性為true的話,那麼這兩個參數同樣會作用於核心線程的。
線程池中的任務隊列,通過線程池的execute方法來提交的Runnable對象都會存儲在這個任務隊列中的。
當向線程池中提交任務的時候,會滿足以下規則:
如果線程池中的線程數量沒有達到核心線程的數量,那麼會直接啟動一個核心線程來執行該任務。如果線程池中的線程數量已經達到核心線程數,那麼任務就會被插入到任務隊列中排隊等待執行,當核心線程空閒的時候,就會從任務隊列中按照某種規則取出一個任務來執行如果任務隊列滿了,或者由於其他原因,向線程池提交的任務不能插入到任務隊列中的時候,這個時候就會去看線程池中的線程數是否達到線程池的上限,如果沒有,就立即開啟一個線程並執行。如果線程池中正在工作的線程數已經達到了線程池設置的上限,此時再向線程池中提交任務,線程池就會拒絕執行此任務
關於workQueue我們在接下來會再詳細說明的
線程工廠,為線程池創建提供創建新線程的功能。ThreadFactory是一個接口,它只有一個方法
Thread newThread(Runnable r);
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { if (!e.isShutdown()) { r.run(); } }使用執行線程池的線程本身來運行該任務,此策略提供簡單的反饋控制機制,能減緩新任務的提交速度。
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { throw new RejectedExecutionException(); }這種策略直接拋出異常,丟棄任務
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { }這種策略也是丟棄任務,不同的是,它並不會拋出異常
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { if (!e.isShutdown()) { e.getQueue().poll(); e.execute(r); } }
以上這些拒絕策略中AbortPolicy是線程池默認的拒絕策略
我們已經對線程池的構造方法中各個參數進行了詳細的介紹,我們在來介紹一下Android中最常見的四類具有不同功能的線程池,它們都直接或間接地通過配置ThreadPoolExecutor來實現自己的功能特性,這四類線程池分別是FixedThreadPool、CachedThreadPool、ScheduledThreadPool和SingleThreadExecutor
看看它的構造方法
public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue()); }
通過它的構造方法可以看出,它是一種線程數量固定的線程池,它的核心線程和最大線程是相等的,即該線程池中的所有線程都是核心線程,所以它也並沒有超時機制,而他的任務隊列是無邊界的任務隊列,也就是可以添加無上限的任務,但是都會排隊執行
示例:
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3); for (int i = 1; i <= 10; i++) { final int index = i; fixedThreadPool.execute(new Runnable() { @Override public void run() { String threadName = Thread.currentThread().getName(); System.out.println("線程:" + threadName + ",任務:" + index); try { //模擬耗時 Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } }); }我們創建了一個最大線程和核心線程都是3的一個線程池,然後向其中循環添加10個任務,每一個人只打印當前線程的名字,來看一下效果
可以看到一開始就會執行3個任務,而後面的7個任務都會進入等待狀態當核心線程執行完一個之後,就會從隊列中按照FIFO的策略取出一個線程進行執行,所以除了前三個任務,剩下的任務是按照順序執行的
它是通過Executors的newCachedThreadPool方法來創建,實例化方法如下
public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue通過它的實例方法可以看出它的核心線程數是0也就是說該線程池並沒有核心線程,而它的最大線程數是int類型的的上限,那麼我們可以理解為該線程池的最大線程數是沒有上限的,也就是說可以無限的創建線程。那麼當新任務向線程池中提交的時候,如果有空閒線程,就會把任務放到空閒線程中去,如果沒有空閒線程,就會開啟一個新的線程來執行此任務,而它的隊列SynchronousQueue是一個特殊的隊列,在多數情況下,我們可以把它簡單的理解為一個無法插入的隊列,我們會在之後詳細說明的。()); }
代碼示例:
ExecutorService cachedThreadPool = Executors.newCachedThreadPool(); for (int i = 1; i <= 10; i++) { final int index = i; try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } cachedThreadPool.execute(new Runnable() { @Override public void run() { String threadName = Thread.currentThread().getName(); System.out.println("線程:" + threadName + ",任務:" + index); try { long time = index * 500; Thread.sleep(time); } catch (InterruptedException e) { e.printStackTrace(); } } }); }我們這次是在每一次添加的時候都會停止1s的時間,來看看CachedThreadPool的運行情況,並且,每一個任務所執行的時間也不一樣,效果如下
可以看出當有存活的空閒線程的時候,任務會放到該空閒線程中去執行例如任務2和任務1就是在同一個線程中去執行的,而如果沒有空閒線程的話,新提交的任務就會開啟一個新的線程來執行該任務,那麼這類線程池比較適合執行大量的耗時少的任務,當線程池處於閒置狀態的時候,線程池中的線程都會被銷毀,這個時候該線程池幾乎是不占用任何系統資源的
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) { return new ScheduledThreadPoolExecutor(corePoolSize); }
public ScheduledThreadPoolExecutor(int corePoolSize) { super(corePoolSize, Integer.MAX_VALUE, DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS, new DelayedWorkQueue()); }可以看出它的核心線程數是固定的,而最大線程數也是int類型的上限,理解為沒有限制,它與之前的線程池相區別的就是它的任務隊列,DelayedWorkQueue能讓任務周期性的執行,也就是說該線程池可以周期性的執行任務。
代碼示例:
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3); //延遲2秒後執行該任務 System.out.println("任務開始"); scheduledThreadPool.schedule(new Runnable() { @Override public void run() { String threadName = Thread.currentThread().getName(); System.out.println("任務:" + threadName ); } }, 3, TimeUnit.SECONDS);我們首先模擬延遲執行任務的代碼,在我們提交任務之前先打印一次任務開始,在提交任務的時候,去設置延遲時間為3s,運行一下看看效果
可以看到,我們的程序在執行完開始任務後,經過了3s的時間,才去執行提交的任務,這就是ScheduledThreadPool的延遲功能,而ScheduledThreadPool還可以設置重復不斷的執行任務,代碼如下
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3); //延遲2秒後執行該任務 System.out.println("任務開始"); //延遲4秒後,每隔1秒執行一次該任務 scheduledThreadPool.scheduleAtFixedRate(new Runnable() { @Override public void run() { System.out.println("執行任務"); } }, 4, 1, TimeUnit.SECONDS);我們設置了在提交任務時,需要延遲4s才會第一次執行,同時在任務執行完畢後每隔1s又會重復的執行一次該任務,看一下效果
可以看到,我們的線程池實現了該功能,在提交任務的時候,會等待4秒的時間然後開始循環執行任務,每兩次執行任務的間隔是1s的時間
最後一種線程池,它是只有一個線程的線程池,所有的任務是按照提交順序排列的,但是它幾乎就是湊數的,為什麼這麼說呢,我們來看一下它的實例化方法:
public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue通過代碼可以看出,這種線程池,就是FixedThreadPool但是實例化方法的參數是1的嘛。())); }
官方實際上不建議我們使用自己配置的線程池,建議我們使用為我們提供的幾種線程池,通常來說,這幾種線程池已經可以滿足大多數的需求了,使用起來也比較簡單
通過觀察官方為我們提供的幾種線程池,我們發現,對於不同類型的線程池來說,決定他們有各種功能的最主要因素就是這個任務隊列,而這個任務隊列實際上是一個實現了叫BlockingQueue的對象,在這個接口裡規定了加入或取出等方法,一共有11個方法,要復寫起來非常麻煩,所幸,Java也為我們封裝了一些常用的實現類來方便我們的使用,常用的有以下幾種
LinkedBlockingQueue:無界的隊列
SynchronousQueue:直接提交的隊列
DelayedWorkQueue:等待隊列
PriorityBlockingQueue:優先級隊列
ArrayBlockingQueue:有界的隊列
這兩個隊列很像LinkedList和ArrayList就是一個是用數組實現的,一個使用鏈表實現的,它們都是FIFO的,而區別是LinkedBlockingQueue可以是沒有數量上限的,而根據之間說的任務向現場池中添加的順序我們知道,如果隊列是無上限的話,線程池就不需要非核心線程了,可以看到Java封裝好的線程池只要使用這個隊列的,它的核心線程數和最大線程數都是一樣的。
這個隊列會把任務直接提交給線程而不保持它們。在此,如果不存在可用於立即運行任務的線程,則試圖把任務加入隊列將失敗,因此會構造一個新的線程。此策略可以避免在處理可能具有內部依賴性的請求集時出現鎖。直接提交通常要求無界 maximumPoolSizes 以避免拒絕新提交的任務。當命令以超過隊列所能處理的平均數連續到達時,簡單說來,這種隊列就是沒什麼用,走個過場而已,所以使用這個隊列的時候,線程池的最大線程數一般是無上限的,也就是int類型的最大值
優先級隊列,這種隊列在向線程池中提交任務的時候會檢測每一個任務的優先級,會先把優先級高的任務扔到線程池中,前幾種隊列我們都用過了,我們來寫一個利用優先級隊列的線程池。
首先我們知道我們的任務都是Runnable,我們要想讓我們Runnable能夠滿足優先級隊列,那就得讓他具有可比性,得讓優先級隊列知道到底誰的優先級比較高,所以我們自己寫一個抽象類來:
public abstract class PriorityRunnable implements Runnable, Comparable我們的抽象類最基本的需要實現Runnable接口才能被提交到線程池中,同時我們需要再實現Compareable接口,來告訴隊列到底誰大誰小,這裡我們自己寫了一個int類型的變量代表每一個任務的優先級,然後復寫compareTo方法來寫我們的比較條件,這裡要注意的是,當我們自己去寫的時候,不一定非要指定一個int類型的變量,也可以是其他的例如String等的,只要實現了Comparable接口就可以了。接下來我們就可以使用這個任務了,代碼如下{ private int priority; public PriorityRunnable(int priority) { if (priority < 0) { throw new IllegalArgumentException(); } this.priority = priority; } @Override public int compareTo(PriorityRunnable another) { return another.getPriority() - priority; } public int getPriority() { return priority; } }
ExecutorService priorityThreadPool = new ThreadPoolExecutor(3, 3, 0L, TimeUnit.MILLISECONDS, new PriorityBlockingQueue我們創建了一個核心線程為3的使用優先級隊列的線程池,向線程池中添加10個優先級不同的任務,來看看效果()); for (int i = 1; i <= 10; i++) { final int priority = i; priorityThreadPool.execute(new PriorityRunnable(priority) { @Override public void run() { String threadName = Thread.currentThread().getName(); System.out.println("線程:" + threadName + ",正在執行優先級為:" + priority + "的任務"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } }); }
當我們運行的時候,前三個任務是沒有進入隊列的,至接進入到線程池的核心線程開始干活了,之後的7個任務都會進入到優先級隊列,通過比較,再進入線程池工作的時候,就會讓線程按照我們設定好的優先級順序執行,優先級高的任務會先執行任務,如果我們讓每一個提交的任務都加入一個當前的時間,就可以完成類似LIFO的功能啦~
除了以上功能外,線程池還給我們提供了3個方法,分別是:
beforeExecute() : 任務執行前執行的方法
afterExecute() :任務執行結束後執行的方法
terminated() :線程池關閉後執行的方法
而這三個方法要想使用的話,需要我們自定義一個類來繼承自ThreadPoolExecutor,然後復寫這三個方法
public class MyThreadPool extends ThreadPoolExecutor { public MyThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue類似這樣,就可以在線程池執行任務之前和之後,都很方便的加上我們的功能workQueue) { super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue); } @Override protected void beforeExecute(Thread t, Runnable r) { super.beforeExecute(t, r); //在執行任務之前 } @Override protected void afterExecute(Runnable r, Throwable t) { super.afterExecute(r, t); //在執行任務之後 } @Override protected void terminated() { super.terminated(); //線程池關閉 } }
那麼什麼是信號量呢?想像商場的停車場,假設有3個車位,但是一共有6台車想要停在停車場,那麼在停車場的門口就會有一個電子顯示屏告訴新來的車輛,還有多少的空的車位,而如果停車場已經滿了,新來的車要想停在這個商場的停車場就需要在門口等待,知道顯示屏顯示又有車位的,而信號量就相當於停車場的電子顯示屏,它可以確定有多少個位置,如果沒有位置了,線程會卡在那一直的等待。
下面用代碼實驗一下:
//只有一個位置的信號量,相當於停車場只有一個位置 final Semaphore semp = new Semaphore(2); for(int i = 0;i<4;i++){ new Thread(new Runnable() { @Override public void run() { String threadName = Thread.currentThread().getName(); try { //申請信號量 semp.acquire(); System.out.println(threadName+"申請到了信號量"); Thread.sleep(2000); //釋放信號量 semp.release(); System.out.println(threadName+"釋放了了信號量"); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); }首先我們定義了一個只有2個位置的信號量,然後循環開啟了4個線程,而這4個線程可以理解為幾乎是在同一時間開啟的,當線程開始的時候會嘗試申請信號量,如果申請成功就開始模擬一個耗時操作,在操作完成後,再釋放一次信號量,我們來看一看這幾個線程的運行情況
可以看到前兩個線程成功申請到了信號量,開始執行耗時操作,後兩個線程的代碼就會停在申請信號量的這一步,直到有新的信號量的放出,代碼會繼續執行,也就是說必須等到前兩個線程完成了任務之後,後面的線程才會繼續運行,是不是和2個核心線程的線程池很像啊~
掌握了信號量的基本使用,我們就可以嘗試自定義一個可以動態切換FIFO和LIFO的線程池了,首先看一下效果
可以看到 我們點擊FIFO的時候,會從0一直到99,而點擊LIFO的時候,線程瞬間就從後向前執行了,非常方便,我們來看看代碼,首先是布局文件
<linearlayout android:layout_height="match_parent" android:layout_width="match_parent" android:orientation="vertical" android:paddingbottom="@dimen/activity_vertical_margin" android:paddingleft="@dimen/activity_horizontal_margin" android:paddingright="@dimen/activity_horizontal_margin" android:paddingtop="@dimen/activity_vertical_margin" tools:context="com.lanou.chenfengyao.myapplication.MainActivity" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <linearlayout android:layout_height="wrap_content" android:layout_width="match_parent" android:orientation="horizontal"><button android:id="@+id/btn_FIFO" android:layout_height="wrap_content" android:layout_width="wrap_content" android:text="FIFO"></button><button android:id="@+id/btn_LIFO" android:layout_height="wrap_content" android:layout_width="wrap_content" android:text="LIFO"> <textview android:id="@+id/main_text" android:layout_height="wrap_content" android:layout_width="wrap_content" android:text="Hello World!"> </textview></button></linearlayout></linearlayout>這個不用多說,就是2個Button來控制線程池的,一個TextView用來顯示當前的線程的
接下來是MainActivity:
public class MainActivity extends AppCompatActivity implements View.OnClickListener { private Button FIFOBtn, LIFOBtn; private TextView mainTv; private MyThreadPool myThreadPool; private Handler handler; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); FIFOBtn = (Button) findViewById(R.id.btn_FIFO); LIFOBtn = (Button) findViewById(R.id.btn_LIFO); mainTv = (TextView) findViewById(R.id.main_text); FIFOBtn.setOnClickListener(this); LIFOBtn.setOnClickListener(this); handler = new Handler(new Handler.Callback() { @Override public boolean handleMessage(Message msg) { //設置給TextView; mainTv.setText("任務:"+msg.what); return false; } }); myThreadPool = new MyThreadPool(1); for (int i = 0; i < 100; i++) { final int index = i; myThreadPool.execute(new Runnable() { @Override public void run() { handler.sendEmptyMessage(index); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); } } @Override public void onClick(View v) { switch (v.getId()) { case R.id.btn_FIFO: myThreadPool.setWay(MyThreadPool.OutWay.FIFO); break; case R.id.btn_LIFO: myThreadPool.setWay(MyThreadPool.OutWay.LIFO); break; } } }我直接在onCreate方法裡創建了我自定義的一個線程池,然後向裡面添加了100個任務,而這個線程池的核心線程和最大線程我都設置成了1個,每一個任務也就是把自己任務號通過handler發送給主線程,然後顯示到TextView上,在按鈕的監聽裡,調用自定義線程池的setWay方法,把FIFO還是LIFO的信息設置上,那麼關鍵的代碼就在我們自定義的線程池裡了,我們來看一下:
package com.lanou.chenfengyao.myapplication; import java.util.ArrayList; import java.util.List; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.Semaphore; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; /** * Created by ChenFengYao on 16/5/17. */ public class MyThreadPool extends ThreadPoolExecutor { private volatile Semaphore semaphore; private List可以看到,這個自定義的線程池的核心線程和最大線程都是一樣的,通過構造方法傳進來的,這裡是1,同時,我們將我們信號量的數值也設置為核心線程數,並且我們在內部有一個List用來存放我們提交的任務,我將線程池父類的execute方法復寫了,這裡不再向線程池內提交,而是存放到我們自己的RunnableList裡。runnableList; private LoopThread loopThread; private boolean flag; //兩種策略,先進先出和先進後出 enum OutWay { FIFO, LIFO } private OutWay outWay; public MyThreadPool(int corePoolSize) { super(corePoolSize, corePoolSize, 0l, TimeUnit.MILLISECONDS, new LinkedBlockingQueue ()); semaphore = new Semaphore(corePoolSize); runnableList = new LinkedList<>(); flag = true; outWay = OutWay.FIFO;//默認是先進先出 loopThread = new LoopThread(); loopThread.start(); } //提交任務的方法 @Override public synchronized void execute(Runnable command) { //所有來的任務是提交到我們自己的任務隊列中 runnableList.add(command); if (runnableList.size() < 2) { //如果這是隊列中的第一個任務,那麼就去喚醒輪詢線程 synchronized (loopThread) { loopThread.notify(); } } } //設置是FIFO/LIFO public void setWay(OutWay outWay) { this.outWay = outWay; } @Override protected void afterExecute(Runnable r, Throwable t) { super.afterExecute(r, t); //任務完成釋放信號量 semaphore.release(); } @Override protected void terminated() { super.terminated(); flag = false;//輪詢線程關閉 } class LoopThread extends Thread { @Override public void run() { super.run(); while (flag) { if (runnableList.size() == 0) { try { //如果沒有任務,輪詢線程就等待 synchronized (this) { wait(); } } catch (InterruptedException e) { e.printStackTrace(); } } else { try { //請求信號量 semaphore.acquire(); int index = runnableList.size(); switch (outWay) { case FIFO: //先進先出 index = 0; break; case LIFO: //先進後出 index = runnableList.size() - 1; break; } //調用父類的添加方法,將任務添加到線程池中 MyThreadPool.super.execute(runnableList.get(index)); runnableList.remove(index); } catch (InterruptedException e) { e.printStackTrace(); } } } } } }
同時我們開啟了一個輪詢線程,這個線程的作用就是把RunnableList裡的Runnable對象提交到線程池內,首先如果RunnableList裡沒有任務的話,該線程就wait,一直等到提交任務了再將其喚醒,如果有任務的話,首先嘗試拿到一個信號量,也就是看看線程池內是否有空閒的線程可以工作,如果有的話,就根據是FIFO還是LIFO從任務隊列中拿一個任務提交給線程池,注意這裡需要調用父類execute方法哦,一直重復這樣的循環。
那麼什麼時候釋放信號量呢?自然是任務執行完畢的時候啦,我們復寫了線程池的afterExecute方法,當線程池內有一個任務完成後,就會回調該方法,在這個方法裡,我們釋放一次信號量,那麼輪詢線程就會繼續向線程池內提交一個任務
這樣,能控制任務的提交方式的線程池就寫好啦~
最後線程池在使用過後,通常是項目整個退出之後,是需要關閉的,關閉線程池有兩個方法shutDown和shutDownNow,它們的區別是shutDown會讓線程池內的任務完成後關閉,而shutDownNow會放棄掉還沒完成的任務立即關閉。
在使用線程池的時候,我們確定線程池核心線程數的時候通常會根據CPU的核心數來確定的,通常會使用CPU核心數+1來定為當前線程池的核心線程數,在Android中的CPU核心數可以通過
Runtime.getRuntime().availableProcessors();來獲得
現在大家很少用短信和飛信發信息了,自從微信出現後,微信可以說已經慢慢替代了短信,飛信,QQ等通信方式,很多朋友都選擇使用微信和朋友們溝通和交流,在節假日的時
大家先看下效果圖:Android為我們提供了四種應組件,分別為Activity、Service、Broadcast receivers和Content providers
最近工作中,遇到了幾個內存優化的問題,1.應用退出後,此應用進程保持了不少內存得不到釋放,用工具強制gc也無法釋放。2.應用進入某些頁面瞬間請求分配內存過大。此兩個問題對
1、DataProvider package com.njupt.ndk_passdata; public class DataProvider { public