每個Android應用在被啟動時都會創建一個線程,這個線程稱為主線程或UI線程,Android應用的所有操作都會運行在這個線程中。但是為了保證UI的流暢性,通常會將耗時操作放到子線程中,例如IO操作、網絡請求等。而幾乎每個Android應用都會涉及到網絡請求等耗時操作,所以多線程對于Android來說變得至關重要。
一.什么是多線程?
線程:是進程中單一的連續控制流程/執行路徑。
多線程:多個線程并行執行。
二.為什么要使用多線程?
使用多線程可以提高效率,并且不會使程序出現卡頓現象(比如ANR)。
三.什么時候使用多線程?
Android3.0以及以后的版本中,禁止在主線程執行網絡請求,否則會拋出異常,可見在UI線程中執行耗時操作是不推薦的行為。所以,在進行與耗時操作同步進行的操作時(即并行)使用多線程。
四.如何使用多線程?
我們經常說Android中的主線程是線程不安全的,所以只能在主線程中更新UI。那么如何更新主線程且保證線程是安全的呢?
Android中提供了保證線程安全的幾種解決方案:
- 使用Handler實現線程之間的通信。
- Activity.runOnUiThread(Runnable):一般在Activity的Thread中運用。
- View.post(Runnable)
- View.postDelayed(Runnable, long)
Android中的線程分為主線程(UI線程)和工作線程。
- 主線程(UI線程):程序運行時被創建的線程。
- 工作線程:自己創建的線程。
以上兩個線程之間的通信最基本的有兩種:
Thread和Runnable
Thread和Runnable的使用需要用到Handler,Handler的用法可以參考之前的文章:Android應用界面開發——Handler(實現倒計時)
這里通過實現一個簡單的下載器來學習Thread和Runnable。
這個下載器就一個界面,包含一個輸入框,一個進度條,用來顯示下載進度,用來輸入下載地址,一個按鈕,用來開始下載。
界面代碼如下:activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="16dp"
android:layout_marginTop="16dp"
tools:context="com.trampcr.downloaddemo.MainActivity">
<EditText
android:id="@+id/et_url"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:hint="請輸入下載地址" />
<ProgressBar
android:id="@+id/pb_down_load"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/et_url"
android:layout_marginTop="30dp"
android:max="100" />
<TextView
android:id="@+id/tv_progress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/pb_down_load"
android:layout_marginTop="20dp"
android:text="下載進度"
android:textColor="#000000" />
<Button
android:id="@+id/btn_start_download"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/tv_progress"
android:layout_centerHorizontal="true"
android:layout_marginTop="50dp"
android:background="@drawable/btn_style"
android:text="開始下載"
android:textColor="#000000" />
</RelativeLayout>
細心的人可能會注意到這里的按鈕用了一個背景@drawable/btn_style,這里是自定義按鈕的形狀。代碼如下:btn_style.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:topLeftRadius="10dp"
android:radius="8dp"
android:topRightRadius="10dp"
android:bottomLeftRadius="10dp"
android:bottomRightRadius="10dp" />
<stroke android:color="#000000"
android:width="0.7dp"/>
</shape>
接下來就是下載操作了,代碼如下:MainActivity.java
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
//public static final String DOWNLOAD_URL = "http://psoft.33lc.com:801/small/rootexplorer_33lc.apk";
private Button mBtnStartDownload;
private EditText mEtUrl;
private String mUrl;
private ProgressBar mPbDownload;
private TextView mTvProgress;
private Handler mHandler = new DownloadHandler(this);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mEtUrl = (EditText) findViewById(R.id.et_url);
mBtnStartDownload = (Button) findViewById(R.id.btn_start_download);
mPbDownload = (ProgressBar) findViewById(R.id.pb_down_load);
mTvProgress = (TextView) findViewById(R.id.tv_progress);
mBtnStartDownload.setOnClickListener(this);
}
@Override
public void onClick(View v) {
mUrl = mEtUrl.getText().toString().trim();
new Thread(new Runnable() {
@Override
public void run() {
download(mUrl);
}
}).start();
}
private void download(String mUrl) {
try {
URL url = new URL(mUrl);
URLConnection urlConnection = url.openConnection();
int contentLength = urlConnection.getContentLength(); //下載文件大小
InputStream inputStream= urlConnection.getInputStream();
String downloadFolderName = Environment.getExternalStorageDirectory() + File.separator + "trampcr" + File.separator;
File file = new File(downloadFolderName);
if (!file.exists()){
file.mkdir();
}
String fileName = downloadFolderName + "zxm.apk";
File apkFile = new File(fileName);
if (apkFile.exists()) {
apkFile.delete();
}
int downloadSize = 0;
byte[] buff = new byte[1024];
int length = 0;
OutputStream outputStream = new FileOutputStream(fileName);
while ((length = inputStream.read(buff)) != -1) {
outputStream.write(buff, 0, length);
downloadSize += length;
int progress = downloadSize * 100 / contentLength;
Message msg = mHandler.obtainMessage();
msg.what = 0;
msg.obj = progress;
mHandler.sendMessage(msg);
}
outputStream.close();
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
public static class DownloadHandler extends Handler{
public final WeakReference<MainActivity> weakRefActivity;
public DownloadHandler(MainActivity mainActivity) {
weakRefActivity = new WeakReference<MainActivity>(mainActivity);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
MainActivity activity = weakRefActivity.get();
switch (msg.what){
case 0:
int progress = (int) msg.obj;
activity.mPbDownload.setProgress(progress);
activity.mTvProgress.setText("下載進度:" + progress + "%");
if (progress == 100){
Toast.makeText(activity, "下載完成", Toast.LENGTH_LONG).show();
}
break;
}
}
}
}
通過Handler把子線程中的message發送到主線程,并在handleMessage中更新進度條。當Progress=100時,彈出Toast提示下載完成。
效果圖如下:
AsyncTask
AsyncTask適用于簡單的異步處理,不需要借助線程和Handler即可實現。
AsyncTask<Params, Progress, Result>是一個抽象類,通常用于被繼承,繼承AsyncTask時需要指定三個泛型參數。
- Params:啟動任務執行的輸入參數的類型。
- Progress:后臺任務完成的進度值的類型。
- Result:后臺執行任務完成后返回結果的類型。
使用AsyncTask的步驟:
- 創建AsyncTask的子類,并為三個泛型參數指定類型。如果某個泛型參數不需要指定類型,則可將它指定為void。
- 根據需要實現以下方法:
- doInBackground(Params...):后臺線程將要完成的任務。該方法可以調用publishProgress(Progress... values)方法更新任務的執行進度。
- onProgressUpdate(Progress... values):在doInBackground()方法中調用publishProgress()方法更新任務的執行進度后,將會觸發該方法。
- onPreExecute():該方法將在執行后臺耗時操作前被調用。通常用于完成一些初始化準備工作。
- onPostExecute(Result result):當doInBackground()完成后,系統會自動調用onPostExecute()方法,并將doInBackground()方法的返回值傳給該方法。
- 調用AsyncTask子類的實例的execute(Params... params)開始執行耗時任務。
這里通過實現一個簡單的下載器來學習AsyncTask。
這個下載器就一個界面,包含一個輸入框,用來輸入下載地址,一個按鈕,用來開始下載。
界面代碼如下:activity_download.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="16dp"
android:layout_marginTop="16dp"
tools:context="com.trampcr.downloaddemo.MainActivity">
<EditText
android:id="@+id/et_url"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:hint="請輸入下載地址" />
<Button
android:id="@+id/btn_start_download"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/et_url"
android:layout_centerHorizontal="true"
android:layout_marginTop="50dp"
android:background="@drawable/button_style"
android:text="開始下載"
android:textColor="#000000" />
</RelativeLayout>
細心的人可能會注意到這里的按鈕用了一個背景@drawable/button_style,這里是自定義按鈕的形狀。代碼如下:button_style.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:topLeftRadius="10dp"
android:radius="8dp"
android:topRightRadius="10dp"
android:bottomLeftRadius="10dp"
android:bottomRightRadius="10dp" />
<stroke android:color="#000000"
android:width="0.7dp"/>
</shape>
界面寫完了,實現下載代碼,根據上面的步驟,第一步是實現AsyncTask的子類,代碼如下:DownloadAsyncTask.java
public class DownloadAsyncTask extends AsyncTask<URL, Integer, String> {
private ProgressDialog progressDialog;
private int hasRead = 0;
private Context context;
public DownloadAsyncTask(Context context) {
this.context = context;
}
@Override
protected String doInBackground(URL... params) {
try {
URLConnection urlConnection = params[0].openConnection();
InputStream inputStream= urlConnection.getInputStream();
String downloadFolderName = Environment.getExternalStorageDirectory() + File.separator + "trampcr" + File.separator;
File file = new File(downloadFolderName);
if (!file.exists()){
file.mkdir();
}
String fileName = downloadFolderName + "zxm.apk";
File apkFile = new File(fileName);
if (apkFile.exists()) {
apkFile.delete();
}
byte[] buff = new byte[1024];
int length = 0;
OutputStream outputStream = new FileOutputStream(fileName);
while ((length = inputStream.read(buff)) != -1) {
outputStream.write(buff, 0, length);
hasRead++;
publishProgress(hasRead);
}
outputStream.close();
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
@Override
protected void onPostExecute(String s) {
super.onPostExecute(s);
progressDialog.dismiss();
Toast.makeText(context, "下載完成", Toast.LENGTH_LONG).show();
}
@Override
protected void onPreExecute() {
super.onPreExecute();
progressDialog = new ProgressDialog(context);
//設置對話框標題
progressDialog.setTitle("任務正在進行中");
//設置對話框顯示的內容
progressDialog.setMessage("正在下載,請稍等...");
//設置對話框的取消按鈕
progressDialog.setCancelable(true);
//設置進度條的最大值
progressDialog.setMax(2000);
//設置進度條風格
progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
//設置對話框的進度條是否顯示進度
progressDialog.setIndeterminate(false);
progressDialog.show();
}
@Override
protected void onProgressUpdate(Integer... values) {
super.onProgressUpdate(values);
//更新進度
progressDialog.setProgress(values[0]);
}
}
這里在onPreExecute()方法中實現了初始化并顯示進度對話框,在doBackground()方法通過讀文件、寫文件完成下載任務,并調用publishProgress()方法發出更新進度,在onProgressUpdate()方法中執行更新進度,在onPostExecute()方法中銷毀進度條對話框,并彈出Toast提示下載完成。
DownloadActivity.java
public class DownloadActivity extends AppCompatActivity implements View.OnClickListener{
// public static final String DOWNLOAD_URL = "http://psoft.33lc.com:801/small/rootexplorer_33lc.apk";
private Button mBtnStartDownload;
private EditText mEtUrl;
private String mUrl;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_download);
mEtUrl = (EditText) findViewById(R.id.et_url);
mBtnStartDownload = (Button) findViewById(R.id.btn_start_download);
mBtnStartDownload.setOnClickListener(this);
}
@Override
public void onClick(View v) {
mUrl = mEtUrl.getText().toString().trim();
DownloadAsyncTask downloadAsyncTask = new DownloadAsyncTask(DownloadActivity.this);
try {
// downloadAsyncTask.execute(new URL(DOWNLOAD_URL));
downloadAsyncTask.execute(new URL(mUrl));
} catch (MalformedURLException e) {
e.printStackTrace();
}
}
}
上一步實現了AsyncTask的子類,這一步就需要創建該子類的實例,并執行execute()開始執行任務。
效果圖如下:
五.new Thread() VS ThreadPoolExecutor
new Thread
弊端:
- 每次都需要new Thread,新建對象性能差。
- 線程缺乏統一管理,可能無限制新建線程,相互之間競爭,極可能占用過多系統資源導致死機或OOM。
- 缺乏更多功能,如定時執行、定期執行、線程中斷。
ThreadPoolExecutor——線程池(多線程的管理者)
引入的好處:
- 提升性能,創建和消耗對象費時費CPU資源。
- 防止內存過度消耗,控制活動線程的數量,防止并發線程過多。
線程池的分類:
- new CachedThreadPool:創建一個可緩存線程池,如果線程池長度超過處理需要,可靈活回收空閑線程,若無可回收,則新建線程。
- new FixedThreadPool:創建一個定長線程池,可控制線程最大并發數,超出的線程會在隊列中等待。
- new ScheduledThreadPool:創建一個定長線程池,支持定時及周期性任務執行。
- new SingleThreadPool:創建一個單線程化線程池,它只會用唯一的工作線程來執行任務,保證所有的任務按照指定順序(FIFO、LIFO、優先級)執行。