Android開發高級進階——多線程(實現簡單下載器)

每個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提示下載完成。

效果圖如下:

下載Demo.gif

AsyncTask

AsyncTask適用于簡單的異步處理,不需要借助線程和Handler即可實現。

AsyncTask<Params, Progress, Result>是一個抽象類,通常用于被繼承,繼承AsyncTask時需要指定三個泛型參數。

  • Params:啟動任務執行的輸入參數的類型。
  • Progress:后臺任務完成的進度值的類型。
  • Result:后臺執行任務完成后返回結果的類型。

使用AsyncTask的步驟:

  1. 創建AsyncTask的子類,并為三個泛型參數指定類型。如果某個泛型參數不需要指定類型,則可將它指定為void。
  2. 根據需要實現以下方法:
  • doInBackground(Params...):后臺線程將要完成的任務。該方法可以調用publishProgress(Progress... values)方法更新任務的執行進度。
  • onProgressUpdate(Progress... values):在doInBackground()方法中調用publishProgress()方法更新任務的執行進度后,將會觸發該方法。
  • onPreExecute():該方法將在執行后臺耗時操作前被調用。通常用于完成一些初始化準備工作。
  • onPostExecute(Result result):當doInBackground()完成后,系統會自動調用onPostExecute()方法,并將doInBackground()方法的返回值傳給該方法。
  1. 調用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()開始執行任務。

效果圖如下:

下載Demo

五.new Thread() VS ThreadPoolExecutor


new Thread

弊端:

  1. 每次都需要new Thread,新建對象性能差。
  2. 線程缺乏統一管理,可能無限制新建線程,相互之間競爭,極可能占用過多系統資源導致死機或OOM。
  3. 缺乏更多功能,如定時執行、定期執行、線程中斷。

ThreadPoolExecutor——線程池(多線程的管理者)

引入的好處:

  1. 提升性能,創建和消耗對象費時費CPU資源。
  2. 防止內存過度消耗,控制活動線程的數量,防止并發線程過多。

線程池的分類:

  1. new CachedThreadPool:創建一個可緩存線程池,如果線程池長度超過處理需要,可靈活回收空閑線程,若無可回收,則新建線程。
  2. new FixedThreadPool:創建一個定長線程池,可控制線程最大并發數,超出的線程會在隊列中等待。
  3. new ScheduledThreadPool:創建一個定長線程池,支持定時及周期性任務執行。
  4. new SingleThreadPool:創建一個單線程化線程池,它只會用唯一的工作線程來執行任務,保證所有的任務按照指定順序(FIFO、LIFO、優先級)執行。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,321評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,559評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,442評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,835評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,581評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,922評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,931評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,096評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,639評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,374評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,591評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,104評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,789評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,196評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,524評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,322評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,554評論 2 379

推薦閱讀更多精彩內容