Android学习记录,第八周

多任务多线程下载

多线程多任务下载,本例子使用AsyncTask进行多线程下载。AsyncTask线程池支持两种模式。

1、第一种SERIAL_EXECUTOR用于保证线程的顺序执行。此线程池内只有唯一线程,若有多个线程需要运行,则后续线程需要等待,类似一个队列的效果。若AsyncTask直接调用execute()方法,则默认使用这种模式。即executeOnExecutor(AsyncTask.SERIAL_EXECUTOR)和execute()效果一样。

2、第二种是THREAD_POOL_EXECUTOR。THREAD_POOL_EXECUTOR内最多可以有5个线程并发执行,超过5个则后续线程需要等待。调用方法为executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR) 。

3、AsyncTask也可以使用自定义线程池.

Android中的线程池和JAVA中一模一样,可以分为4种:
1、newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
2、newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
3、newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
4、newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

以下例子用ListView显示每个任务,每个任务使用多线程下载。

首先在主界面定义这个ListView

public class MainActivity extends AppCompatActivity {

    private ListView mListView;
    private EditText mEditText;
    private Button mDownloadButton;
    private int mTotalMission;


    @Override
    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        findViews();


        final DownloadAdapter downloadAdapter = new DownloadAdapter(this);

        mListView.setAdapter(downloadAdapter);
        mDownloadButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (!TextUtils.equals(mEditText.getText(), "")) {
                    String url = mEditText.getText().toString();
                    mTotalMission++;
                    ConnectivityManager connectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
                    NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
                    if(networkInfo !=null){
                        downloadAdapter.setTotalCount(mTotalMission);
                        downloadAdapter.setUrl(url);
                        downloadAdapter.notifyDataSetChanged();
                    }else {
                        Toast.makeText(MainActivity.this,"请检查网络连接",Toast.LENGTH_SHORT).show();
                    }
                }
            }
        });

    }

    private void findViews() {
        mListView = (ListView) findViewById(R.id.main_list_view);
        mEditText = (EditText) findViewById(R.id.download_address_edit_text);
        mDownloadButton = (Button) findViewById(R.id.button);


    }
}

在这里,每当一个下载任务被创建时,则新增一个ListView的子项目,并用notifyDataSetChanged()通知适配器刷新数据。
在Adapter代码如下:

public class DownloadAdapter extends BaseAdapter {

    private int totalCount;
    private Context mContext;
    static TempView mTempView;
    String mUrl;
    final int threadCount = 3;
    public static int mStartIndex;
    public static int mEndIndex;
    private RandomAccessFile mRandomAccessFile;
    private HttpURLConnection mHttpURLConnection;
    public static int mLength;

    public DownloadAdapter(Context context) {
        mContext = context;
        mTempView = new TempView();
    }

    public void setUrl(String url) {
        mUrl = url;
    }

    public void setTotalCount(int totalCount) {
        this.totalCount = totalCount;
    }



    @Override
    public int getCount() {
        return totalCount;
    }

    @Override
    public Object getItem(int position) {
        return null;
    }

    @Override
    public long getItemId(int position) {
        return position;
    }


    @Override
    public View getView(final int position, View convertView, ViewGroup parent) {

        if (convertView == null){
            convertView = LayoutInflater.from(mContext).inflate(R.layout.list_item,null);
            mTempView.mButton= (Button) convertView.findViewById(R.id.pause_button);
            mTempView.mTextView = (TextView) convertView.findViewById(R.id.precent_text_view);
            mTempView.mProgressBar = (ProgressBar) convertView.findViewById(R.id.download_progressbar);
            convertView.setTag(mTempView);
            mTempView.mTextView.setText(getFileName(mUrl));

            try {
                String path = Environment.getExternalStorageDirectory().getPath() + "/" + getFileName(mUrl);
                Log.d("main",getFileName(mUrl));
                mRandomAccessFile = new RandomAccessFile(path,"rwd");
                URL url = new URL(mUrl);
                mHttpURLConnection = (HttpURLConnection) url.openConnection();
                mHttpURLConnection.setRequestMethod("GET");
                mHttpURLConnection.setConnectTimeout(5000);
                mHttpURLConnection.setReadTimeout(5000);
                final MyHandler handler = new MyHandler();

                final View finalConvertView = convertView;
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        //获取整个文件的大小
                        mLength = mHttpURLConnection.getContentLength();
                        Message message = handler.obtainMessage();
                        message.arg1 = mLength;
                        message.obj = finalConvertView;
                        handler.sendMessage(message);
                    }
                }).start();
                //设置一个和服务器文件大小一模一样大小的randomAccessFile文件

            } catch (MalformedURLException e) {
                e.printStackTrace();
                Toast.makeText(mContext,"请输入正确的URL",Toast.LENGTH_SHORT).show();
            } catch (IOException e) {
                e.printStackTrace();
                Log.d("main", "IO异常");
            }


        }else {
            mTempView = (TempView) convertView.getTag();
        }

        mTempView.mButton.setOnClickListener(new PauseOrStartListener());

        return convertView;
    }

    class TempView{
        Button mButton;
        TextView mTextView;
        ProgressBar mProgressBar;
    }

    public String getFileName(String url){
        int index = url.lastIndexOf("/");
        return url.substring(index +1);
    }

    class MyHandler extends Handler{
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            int totalLength = msg.arg1;
            View convertView = (View) msg.obj;
            try {
                mRandomAccessFile.setLength(totalLength);
                int blockSize = totalLength / threadCount;
                for (int i = 0; i < threadCount; i++) {
                    mStartIndex = i * blockSize;
                    mEndIndex = (i+1) * blockSize - 1;
                    if(i == threadCount -1){
                        mEndIndex = totalLength - 1;
                    }
                    URL url = new URL(mUrl);
                    HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection();
                    httpURLConnection.setRequestMethod("GET");
                    httpURLConnection.setConnectTimeout(5000);
                    httpURLConnection.setReadTimeout(5000);
                    httpURLConnection.setRequestProperty("Range", "bytes=" + mStartIndex + "-" + mEndIndex);
                    Log.d("main", "startIndex:" + mStartIndex + ",endIndex:" + mEndIndex);
                    Log.d("main", "mStartIndex在Download中:" + mStartIndex + ",mEndIndex:" + mEndIndex);
                    MyAsyTask myAsyTask = new MyAsyTask(mContext,convertView,mUrl,mStartIndex,mEndIndex,i);
                    myAsyTask.executeOnExecutor(Executors.newCachedThreadPool(), httpURLConnection);

                }

            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

在此任务中,首先需要创建一个线程,去请求下载任务的总大小。任务的总大小可以用getContentLength()去获取下载的长度。并在本地创建一个长度和下载内容长度一模一样的RandomAccessFile文件用于保存数据。
多线程下载,其实主要原理在于每个线程向服务器请求部分数据,当这些线程任务都下载完成后, 再把这些数据拼接到本地,就实现了多线程下载。假设我们开3个线程向服务器请求数据,即下载情况如图所示:

线程一和线程二下载4个字节内容,而线程三则下载剩余所有内容。

下载的情况如图所示,从服务器端请求的第一部分数据(即红色部分),写入本地RandomAccessFile文件的第一部分,第二部分数据(即蓝色部分)写入第二部分,以此类推。为了得到写入位置,我们需要获得总长度,即这里的mLength,一共三个线程,即每个线程下载的大小blockSize = mLength / 3。
线程的开始地址为i * blockSize;
线程的结束地址为(i + 1) *blockSize -1,最后一个线程的结束位置为mLength -1。
多线程下载需要设置HttpURLConnection的setRequestProperty内的”Range”属性。把这个属性设置为startIndex-endIndex既可。
然后再在AsyncTask执行下载即可,代码如下:

public class MyAsyTask extends AsyncTask<HttpURLConnection, Integer, Integer> {

    private View mView;
    private ProgressBar mProgressBar;
    private Button mButton;
    private String mUrl;
    private TextView mTextView;
    private Context mContext;
    private int mStartIndex;
    private int mEndIndex;
    private int totalSize = 0;
    private int mThreadNum;

    public MyAsyTask(Context context, View view, String url, int startIndex, int endIndex,int threadNum) {
        mContext = context;
        mView = view;
        mUrl = url;
        mStartIndex = startIndex;
        mEndIndex = endIndex;
        mThreadNum = threadNum;
    }

    @Override
    protected void onPreExecute() {
        mButton = (Button) mView.findViewById(R.id.pause_button);
        mProgressBar = (ProgressBar) mView.findViewById(R.id.download_progressbar);
        mTextView = (TextView) mView.findViewById(R.id.precent_text_view);

    }


    @Override
    protected Integer doInBackground(HttpURLConnection... params) {


        if (params[0] != null) {
            try {
                if (params[0].getResponseCode() == 206) {
                    InputStream inputStream = params[0].getInputStream();
                    totalSize = params[0].getContentLength();

                    String path = Environment.getExternalStorageDirectory().getPath() + "/" + getFileName(mUrl);
                    RandomAccessFile raf = new RandomAccessFile(path, "rwd");
                    //设置RandomAccessFile写入的开始位置,否则直接调用write(byte[])方法会直接从头开始写
                    raf.seek(mStartIndex);
                    byte[] b = new byte[1024];
                    int length = 0;
                    int writeLength = 0;
                    while ((length = inputStream.read(b)) != -1) {
                        writeLength += length;
                        raf.write(b,0,length);
                        publishProgress(writeLength);

                    }
                    raf.close();

                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        } else {
            Log.d("MyAsyTask", "参数空");
        }

        return params.length;
    }

    @Override
    protected void onProgressUpdate(Integer... values) {
        //这个对象用于同步锁,任意对象都可以
        MainActivity mainActivity = new MainActivity();
        synchronized (mainActivity){
            //线程进度刷新交于线程1刷新
            if (mThreadNum == 1){
                Log.d(MyAsyTask.class.getSimpleName(), Thread.currentThread().getName() + "总量为:" + values[0] * 100/totalSize);
                mProgressBar.setProgress(values[0] * 100 / totalSize);
            }
        }

        //DownloadAdapter.getNowSize(values[0]);
    }

    @Override
    protected void onPostExecute(Integer result) {
        Toast.makeText(mContext, "下载完成", Toast.LENGTH_SHORT).show();
        Log.d("MyAsyTask", "长度为:" + result);

    }

    public String getFileName(String url) {
        if (url.contains("/")){
            int index = mUrl.lastIndexOf("/");
            return url.substring(index + 1);
        }else {
            return url;
        }

    }
}

特别说明,因为是分段下载,则下载成功的responseCode为206,不是200。
通过以上的方法,既可实现多任务多线程下载。
以下为xml文件代码:
activity_main:

<?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:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity">

<EditText  android:id="@+id/download_address_edit_text" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/hint" />
    <ListView  android:id="@+id/main_list_view" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_below="@id/download_address_edit_text" />

    <Button  android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/download" android:id="@+id/button" android:layout_alignBottom="@+id/main_list_view" android:layout_alignParentRight="true" android:layout_alignParentEnd="true"/>


</RelativeLayout>

item_list_activity:

<?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:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity">

    <LinearLayout  android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal">
        <ProgressBar  android:id="@+id/download_progressbar" android:layout_width="0dp" android:layout_weight="5" android:layout_height="wrap_content" android:layout_gravity="center_vertical" style="?android:progressBarStyleHorizontal" android:max="100" />
        <Button  android:id="@+id/pause_button" android:layout_width="0dp" android:layout_weight="1" android:layout_height="wrap_content" android:text="@string/pause" />

    </LinearLayout>
    <TextView  android:id="@+id/precent_text_view" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="percent" />

</RelativeLayout>

你可能感兴趣的:(Android学习记录,第八周)