前言:最近重读了下“第一行代码”,看到“第一行代码”的一个小项目,特写这篇博客梳理下流程。这个项目实现了使用OKHttp 断点下载大文件,通过服务在下载的过程中暂停和取消并更新通知消息,下面看下效果图:
首先总结一句话,在Android的多线程处理中,尽量做到在子线程中进行耗时操作,在主线程中更新界面UI。好了,下面开始写这个项目。
一。首先创建一个回调接口,用于对下载过程中的各种状态进行监听和回调,代码如下:
public interface DownloadListener {
// 通知下载进度
void onProgress(int progress);
// 通知下载成功
void onSuccess();
// 通知下载失败
void onFailed();
// 通知下载暂停
void onPaused();
//通知下载失败
void onCanceled();
}
好吧,这里插一点题外话,回调函数。主要说一下回调函数的机制
(1)。设置接口,定义回调函数
public interface MyListener {
void onClick();
}
(2)。提供函数实现的一方在初始化的时候,将回调函数的函数指针注册给调用者。
public class Realize {
private MyListener myListener;
public void setMyListener(MyListener myListener) {
this.myListener = myListener;
}
public void doSth() {
myListener.onClick();
}
}
(3)。当特定的时间或条件发生的时候,调用者使用函数指针调用回调函数对事件进行处理。
public class Test {
public static void main(String[] args) {
Realize realize = new Realize();
realize.setMyListener(new MyListener() {
@Override
public void onClick() {
System.out.println("特定事件或条件发生了");
}
});
realize.doSth();
}
}
上面就是一个回调函数的流程,仔细理解下,就能理解回调的实现了,下面回到正题。
二。使用AsyncTask实现下载功能的处理。
emmm,这里还要插入一下的AsyncTask的解释.AsyncTask的英文一种轻量级的异步任务类,这个类可以执行后台操作,并在用户界面上发布结果,而不必处理线程和处理程序.OK,先举个例子:
public class DownloadTask extends AsyncTask {
/**
* 刚开始执行的时候调用,可以用于进行一些界面上的初始化操作,比如说显示一个进度条对话框
*/
@Override
protected void onPreExecute() {
super.onPreExecute();
}
/**
* 这里的代码会在子线程中执行,可以执行耗时操作
*
* @param voids
* @return
*/
@Override
protected Boolean doInBackground(Void... voids) {
// 反馈当前任务的进度,执行完这个方法会调用onProgressUpdate 方法
publishProgress(50);
return null;
}
/**
* 根据返回的数据更新UI
*
* @param values
*/
@Override
protected void onProgressUpdate(Integer... values) {
super.onProgressUpdate(values);
}
/**
* 后台任务执行完毕通过return语句进行返回时 也可以根据返回的数据更新UI
*
* @param aBoolean
*/
@Override
protected void onPostExecute(Boolean aBoolean) {
super.onPostExecute(aBoolean);
}
}
简单的来说,使用AsyncTask的诀窍就是,在doInBackground()方法中执行具体的耗时任务,在onProgressUpdate()方法中进行UI操作,在onPostExecute()方法中执行一些任务的收尾工作。如果想要启动这个任务,只需编写以下代码即可:
new DownloadTask()。execute();
继续回到正题(这样讲着讲着会不会歪楼呀)先在app / build.gradle文件中的依赖中添加OKHttp依赖
编译'com.squareup.okhttp3:okhttp:3.4.1'
下面编写下载功能,新建一个DownloadFileTask继承AsyncTask。
// 第一个参数 传给后台参数 第二个 使用整型数据作为进度显示单位 第三个 使用整型数据反馈执行结果
public class DownloadFileTask extends AsyncTask {
// 下载成功
public static final int TYPE_SUCCESS = 0;
// 下载失败
public static final int TYPE_FAILED = 1;
// 下载暂停
public static final int TYPE_PAUSED = 2;
// 下载取消
public static final int TYPE_CANCELED = 3;
// 下载状态监听回调
private DownloadListener listener;
// 是否取消
private boolean isCancelled = false;
// 是否暂停
private boolean isPaused = false;
// 当前进度
private int lastProgress;
/**
* 带监听的构造函数
*
* @param listener
*/
public DownloadFileTask(DownloadListener listener) {
this.listener = listener;
}
/**
* 在后台执行具体的下载逻辑 是在子线程里面 可以执行耗时操作
*/
@Override
protected Integer doInBackground(String... strings) {
// 文件输入流
InputStream is = null;
RandomAccessFile accessFile = null;
File file = null;
// 记录已下载的文件长度
long downloadedLength = 0;
// 获取下载的URL地址
String downloadUrl = strings[0];
// 从URL下载地址中截取下载的文件名
String fileName = downloadUrl.substring(downloadUrl.lastIndexOf("/"));
// 获取SD卡的Download 目录
String directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath();
// 得到要保存的文件
file = new File(directory + fileName);
// 如果文件已经存在 获取文件的长度
if (file.exists()) {
downloadedLength = file.length();
}
try {
// 获取待下载文件的字节长度
long contentLength = getContentLength(downloadUrl);
// 如果待下载文件的字节长度为0 说明待下载文件有问题
if (contentLength == 0) {
return TYPE_FAILED;
} else if (contentLength == downloadedLength) {
// 已下载字节和文件总字节相等 说明已经下载完了
return TYPE_SUCCESS;
}
// 获取OkHttpClient 对象
OkHttpClient client = new OkHttpClient();
// 创建请求
Request request = new Request.Builder()
// 断点下载,指定从哪个字节开始下载
.addHeader("RANGE", "bytes=" + downloadedLength + "-")
// 设置下载地址
.url(downloadUrl)
.build();
// 获取响应
Response response = client.newCall(request).execute();
if (response != null) {
// 读取服务器响应的数据
is = response.body().byteStream();
// 获取随机读取文件类 可以随机读取一个文件中指定位置的数据
accessFile = new RandomAccessFile(file, "rw");
// 跳过已下载的字节
accessFile.seek(downloadedLength);
//指定每次读取文件缓存区的大小为1KB
byte[] b = new byte[1024];
int total = 0;
int len;
// 每次读取的字节长度
while ((len = is.read(b)) != -1) {
if (isCancelled) {
return TYPE_CANCELED;
} else if (isPaused) {
return TYPE_PAUSED;
} else {
// 读取的全部字节的长度
total += len;
// 写入每次读取的字节长度
accessFile.write(b, 0, len);
// 计算已下载的百分比
int progress = (int) ((total + downloadedLength) * 100 / contentLength);
// 更新进度条
publishProgress(progress);
}
}
// 关闭连接 返回成功
response.body().close();
return TYPE_SUCCESS;
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
// 关闭输入流
if (is != null) {
is.close();
}
// 关闭文件
if (accessFile != null) {
accessFile.close();
}
Log.d("TAG", "这里永远都会执行 ");
// 如果是取消的 就删除掉文件
if (isCancelled && file != null) {
file.delete();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
/**
* 获取下载文件的长度
*
* @param downloadUrl
* @return
* @throws IOException
*/
private long getContentLength(String downloadUrl) throws IOException {
// 获取OkHttpClient
OkHttpClient client = new OkHttpClient();
// 创建请求
Request request = new Request.Builder()
.url(downloadUrl)
.build();
// 获取响应
Response response = client.newCall(request).execute();
// 如果响应是成功的话
if (response != null && response.isSuccessful()) {
// 获取文件的长度 清除响应
long contentLength = response.body().contentLength();
response.close();
return contentLength;
}
return 0;
}
/**
* 在界面上更新当前的下载进度
*
* @param values
*/
@Override
protected void onProgressUpdate(Integer... values) {
super.onProgressUpdate(values);
int progress = values[0];
if (progress > lastProgress) {
listener.onProgress(progress);
lastProgress = progress;
}
}
/**
* 用于通知最后的下载结果
*
* @param integer
*/
@Override
protected void onPostExecute(Integer integer) {
super.onPostExecute(integer);
switch (integer) {
case TYPE_SUCCESS:
listener.onSuccess();
break;
case TYPE_FAILED:
listener.onFailed();
break;
case TYPE_PAUSED:
listener.onPaused();
break;
case TYPE_CANCELED:
listener.onCanceled();
break;
}
}
/**
* 暂停下载
*/
public void pauseDownload() {
isPaused = true;
}
/**
* 取消下载
*/
public void cancelDownload() {
isCancelled = true;
}
}
三。创建一个下载的服务
没错,还得简单的介绍下服务(这么墨迹的吗)。一般我们都调用startService()方法来启动服务,并调用stopService()方法来停止这个服务,但这样启动服务后,活动无法干预到服务到底执行了怎样的逻辑。这时候就要用onBind()方法了,对服务进行绑定之后,就可以调用服务里的Binder提供的方法了.OK,下面举个个子子。首先创建一个MyService,代码如下:
public class MyService extends Service {
private static final String TAG = "MyService";
private DownloadBinder mBinder = new DownloadBinder();
// 创建 DownloadBinder 实例 随便定义了两个方法
class DownloadBinder extends Binder {
public void startDownload() {
Log.d(TAG, "startDownload: executed");
}
public int getProgress() {
Log.d(TAG, "getProgress: executed");
return 0;
}
}
/**
* 返回这个DownloadBinder 实例
*
* @param intent
* @return
*/
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
}
然后在活动中绑定服务,实现活动去指挥服务去干什么。
public class FirstActivity extends AppCompatActivity {
private Button btnBind;
private Button btnUnBind;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_first);
btnBind = (Button) findViewById(R.id.btnBind);
btnUnBind = (Button) findViewById(R.id.btnUnBind);
btnBind.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 绑定服务
Intent intent = new Intent(FirstActivity.this, MyService.class);
bindService(intent, connection, BIND_AUTO_CREATE);
}
});
btnUnBind.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 解绑服务
unbindService(connection);
}
});
}
private MyService.DownloadBinder downloadBinder;
ServiceConnection connection = new ServiceConnection() {
/**
* 活动与服务绑定成功后
* @param name
* @param service
*/
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
// 向下转型获得DownloadBinder 实例 就能调用DownloadBinder的方法 进而控制服务的逻辑
downloadBinder = (MyService.DownloadBinder) service;
downloadBinder.startDownload();
downloadBinder.getProgress();
}
/**
* 活动与服务解绑后
* @param name
*/
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
}
好的,了解到活动如何去控制服务以后,下面正式写下载的服务,代码如下:
public class DownloadService extends Service {
// 下载的异步操作类
private DownloadFileTask downloadFileTask;
// 下载地址
private String downloadUrl;
private static final String TAG = "DownloadService";
// 下载状态的回调
private DownloadListener listener = new DownloadListener() {
/**
* 更新下载进度状态
* @param progress
*/
@Override
public void onProgress(int progress) {
Log.d(TAG, "onProgress: -------" + progress);
getNotificationManager().notify(1, getNotification("Downloading", progress));
}
/**
* 下载成功
*/
@Override
public void onSuccess() {
Log.d(TAG, "onSuccess: -------------");
downloadFileTask = null;
//下载成功时将前台服务通知关闭,并创建一个下载成功的通知
stopForeground(true);
getNotificationManager().notify(1, getNotification("Download Success", -1));
Toast.makeText(DownloadService.this, "Download Success", Toast.LENGTH_SHORT).show();
}
/**
* 下载失败
*/
@Override
public void onFailed() {
Log.d(TAG, "onFailed: -------------");
downloadFileTask = null;
// 下载失败时将前台服务通知关闭,并创建一个下载失败的通知
stopForeground(true);
getNotificationManager().notify(1, getNotification("Download Failed", -1));
Toast.makeText(DownloadService.this, "Download Failed", Toast.LENGTH_SHORT).show();
}
/**
* 下载暂停
*/
@Override
public void onPaused() {
Log.d(TAG, "onPaused: -------------");
downloadFileTask = null;
Toast.makeText(DownloadService.this, "Download Paused", Toast.LENGTH_SHORT).show();
}
/**
* 下载取消
*/
@Override
public void onCanceled() {
Log.d(TAG, "onCanceled: -------------");
downloadFileTask = null;
stopForeground(true);
Toast.makeText(DownloadService.this, "Download Canceled", Toast.LENGTH_SHORT).show();
}
};
DownloadBinder mBinder = new DownloadBinder();
/**
* 返回这个DownloadBinder 实例
*
* @param intent
* @return
*/
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
// 创建 DownloadBinder 实例
class DownloadBinder extends Binder {
// 开始下载
public void startDownload(String url) {
Log.d(TAG, "startDownload--------: 开始下载");
if (downloadFileTask == null) {
downloadUrl = url;
downloadFileTask = new DownloadFileTask(listener);
downloadFileTask.execute(downloadUrl);
startForeground(1, getNotification("DownLoading", 0));
Toast.makeText(DownloadService.this, "Downloading", Toast.LENGTH_SHORT).show();
}
}
// 暂停下载
public void pauseDownload() {
Log.d(TAG, "pauseDownload--------: 暂停下载");
if (downloadFileTask != null) {
downloadFileTask.pauseDownload();
}
}
// 取消下载
public void cancelDownload() {
Log.d(TAG, "cancelDownload--------: 取消下载---" + downloadFileTask);
if (downloadFileTask != null) {
downloadFileTask.cancelDownload();
} else {
if (downloadUrl != null) {
// 先暂停后取消 取消下载时需将文件删除,并通知关闭
String fileName = downloadUrl.substring(downloadUrl.lastIndexOf("/"));
String directory = Environment.getExternalStoragePublicDirectory
(Environment.DIRECTORY_DOWNLOADS).getPath();
File file = new File(directory + fileName);
if (file.exists()) {
file.delete();
}
getNotificationManager().cancel(1);
stopForeground(true);
Toast.makeText(DownloadService.this, "Canceled", Toast.LENGTH_SHORT).show();
}
}
}
}
/**
* 获取通知栏管理器
*
* @return
*/
public NotificationManager getNotificationManager() {
return (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
}
/**
* 设置通知栏的样式 并获取通知栏的实例
*
* @param title
* @param progress
* @return
*/
private Notification getNotification(String title, int progress) {
Intent[] intents = new Intent[]{(new Intent(this, DownloadActivity.class))};
PendingIntent pi = PendingIntent.getActivities(this, 0, intents, 0);
NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
builder.setSmallIcon(R.mipmap.ic_launcher);
builder.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher));
builder.setContentIntent(pi);
builder.setContentTitle(title);
if (progress > 0) {
builder.setContentText(progress + "%");
builder.setProgress(100, progress, false);
}
return builder.build();
}
}
四,接下来在活动中绑定这个服务就可以了。
1.先实现下界面,修改xml中的代码,创建三个按钮。启动暂停取消
2.在活动中绑定服务,让活动与服务进行通信,因为要对文件下载,还要动态的申请写文件的权限,代码如下:
public class DownloadActivity extends AppCompatActivity implements View.OnClickListener {
// 开始下载按钮
private Button btnStart;
// 暂停下载按钮
private Button btnPause;
// 取消下载按钮
private Button btnCancel;
// 服务
private Intent intent;
// 下载操作的实例
private DownloadService.DownloadBinder downloadBinder;
private ServiceConnection serviceConnection = new ServiceConnection() {
/**
* 活动与服务绑定成功后
* @param name
* @param service
*/
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
downloadBinder = (DownloadService.DownloadBinder) service;
}
/**
* 活动与服务解绑后
* @param name
*/
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_download);
btnStart = (Button) findViewById(R.id.btnStart);
btnPause = (Button) findViewById(R.id.btnPause);
btnCancel = (Button) findViewById(R.id.btnCancel);
btnStart.setOnClickListener(this);
btnPause.setOnClickListener(this);
btnCancel.setOnClickListener(this);
// 启动服务并绑定
intent = new Intent(this, DownloadService.class);
startService(intent);
bindService(intent, serviceConnection, BIND_AUTO_CREATE);
// 获取写的权限
if (ContextCompat.checkSelfPermission(DownloadActivity.this,
Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(DownloadActivity.this,
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1);
}
}
@Override
public void onClick(View v) {
if (downloadBinder == null) {
return;
}
switch (v.getId()) {
case R.id.btnStart:
String url = "https://raw.githubusercontent.com/guolindev/eclipse/master/eclipse-inst-win64.exe";
downloadBinder.startDownload(url);
break;
case R.id.btnPause:
downloadBinder.pauseDownload();
break;
case R.id.btnCancel:
downloadBinder.cancelDownload();
break;
}
}
/**
* 申请权限的返回结果
*
* @param requestCode
* @param permissions
* @param grantResults
*/
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
switch (requestCode) {
case 1:
if (grantResults.length > 0 && grantResults[0] != PackageManager.PERMISSION_GRANTED) {
Toast.makeText(this, "拒绝权限将无法使用程序", Toast.LENGTH_SHORT).show();
finish();
}
break;
}
}
/**
* 结束活动的时候关闭服务
*/
@Override
protected void onDestroy() {
super.onDestroy();
unbindService(serviceConnection);
stopService(intent);
}
}
最后在AndroidManifest.xml中文件中声明使用的权限,还有别忘了服务也是需要声明的。
这个项目的总结就到这里就结束了......,最后附上源代码 源码下载地址