每个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、优先级)执行。