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