刚学习安卓编程,在学习《第一行代码》第二版中的服务时,我学习了多线程编程和服务的相关知识,也请教了很多大佬。但是遇到最后的服务下载部分时碰到了难题,当我照着书创建任务类继承AsyncTask时,发现AsyncTask因为内存泄露等问题,已经过时了。
因此我希望找到一个能够替代AsyncTask的异步线程处理工具,由于我并没有学习过kotlin的相关语法,也没有看过郭神的第三版书中的解决方式,于是我选择了大佬推荐的书写优雅、上手容易、线程调节灵活的Rxjava。
花了差不多一天时间,把Rxjava的代码思想和书写方式大致学习了一下,我尝试着改造原来利用AsyncTask的代码,用Rxjava替代实现下载的功能。
这里是介绍Rxjava很不错的参考文章:
https://www.jianshu.com/p/b00b2657d073
由于我用Rxjava替代,因此我主要修改了原来继承AsyncTask的自定义的类,其他部分的代码基本不变,花了一个下午加一个晚上的时间,终于搞定了,在安卓8.1.0版本的手机上完美测试运行。
下面贴一下修改后的代码:
public class DownloadTask {
//修改了定义的变量,避免和0-99的porgress冲突
public static final int TYPE_SUCCESS = 100;
public static final int TYPE_FAILED = 101;
public static final int TYPE_PAUSED = 102;
public static final int TYPE_CANCELED = 103;
private DownloadListener downloadListener;
private boolean isCanceled = false;
private boolean isPaused = false;
private boolean isReading = false;
private static final String TAG = "DownloadTask";
public DownloadTask(DownloadListener listener) {
downloadListener = listener;
}
private int lastProgress=0;
public void execute(String downloadUrl) {
Log.e(TAG, "execute: " + downloadUrl);
//创建被观察者
Observable.create(new ObservableOnSubscribe<Integer>() {
@Override
public void subscribe(@NonNull ObservableEmitter<Integer> emitter) throws Throwable {
InputStream inputStream = null;
RandomAccessFile randomAccessFile = null;
File file = null;
try {
long downloadedLength = 0;
String fileName = downloadUrl.substring(downloadUrl.lastIndexOf("/"));
String directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath();
file = new File(directory + fileName);
if (file.exists()) {
//已经下载的字节数
downloadedLength = file.length();
Log.e(TAG, "subscribe:已下载 " + downloadedLength);
}
long contentLength = getContentLength(downloadUrl);
Log.e(TAG, "subscribe:总大小 " + contentLength);
if (contentLength == 0) {
//用onNext()方法即可发送事件
emitter.onNext(TYPE_FAILED);
} else if (contentLength == downloadedLength) {
emitter.onNext(TYPE_SUCCESS);
} else {
OkHttpClient okHttpClient = new OkHttpClient();
Request request = new Request.Builder()
.url(downloadUrl)
.addHeader("RANGE", "bytes=" + downloadedLength + "-")
.build();
Response response = okHttpClient.newCall(request).execute();
if (response != null) {
inputStream = response.body().byteStream();
randomAccessFile = new RandomAccessFile(file, "rw");
randomAccessFile.seek(downloadedLength);//跳过已下载的字节
byte[] bytes = new byte[1024];
int total = 0;
int len = 0;
while ((len = inputStream.read(bytes)) != -1) {
isReading = true;
if (isCanceled) {
emitter.onNext(TYPE_CANCELED);
break;
} else if (isPaused) {
//由于不像AsyncTask一样可以返回,因此只能用break退出循环并中断下载
emitter.onNext(TYPE_PAUSED);
break;
} else {
total += len;
randomAccessFile.write(bytes, 0, len);
//计算百分比
int progress = (int) ((total + downloadedLength) * 100 / contentLength);
emitter.onNext(progress);
}
}
isReading = false;
response.body().close();
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (inputStream != null) {
inputStream.close();
}
if (randomAccessFile != null) {
randomAccessFile.close();
}
if (isCanceled && file != null) {
//这是主动停止删除
file.delete();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}).subscribeOn(Schedulers.io())//耗时操作在IO线程
.observeOn(AndroidSchedulers.mainThread())//在主线程中更新UI
.subscribe(new Observer<Integer>() {
@Override
public void onSubscribe(@NonNull Disposable d) {
}
@Override
public void onNext(@NonNull Integer integer) {
Log.e(TAG, "onNext" + integer);
//接收到integer类型的事件,进行处理
switch (integer) {
case TYPE_SUCCESS:
downloadListener.onSuccess();
lastProgress=integer;
break;
case TYPE_FAILED:
downloadListener.onFailed();
break;
case TYPE_PAUSED:
downloadListener.onPaused();
break;
case TYPE_CANCELED:
downloadListener.onCanceled();
break;
default:
//如果是0-99,则更新progress
if (integer > lastProgress) {
downloadListener.onProgress(integer);
lastProgress = integer;
}
break;
}
}
@Override
public void onError(@NonNull Throwable e) {
}
@Override
public void onComplete() {
}
});
}
public void pauseDownload() {
if (isReading) {
isPaused = true;
} else {
downloadListener.onPaused();
}
}
public void cancelDownload() {
if (isReading) {
isCanceled = true;
} else {
downloadListener.onCanceled();
}
}
//请求网络并获取文件长度
private long getContentLength(String downloadUrl) throws IOException {
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(5, TimeUnit.SECONDS)
.build();
Request request = new Request.Builder()
.url(downloadUrl)
.build();
try {
//由于可能会给出连接错误的异常,捕捉异常才能返回0
Response response = client.newCall(request).execute();
if (response != null && response.isSuccessful()) {
long contentLength = response.body().contentLength();
//释放资源
response.body().close();
return contentLength;
}
} catch (ConnectException e) {
e.printStackTrace();
}
return 0;
}
}
可以看到,我自定义了DownloadTask的类,在类中定义了execute方法,这样就可以和AsyncTask的execute方法一样,用downloadTask.execute()启动下载。
主要修改的部分有:
- return TYPE_…改成了emitter.onNext(TYPE_…),因为Rxjava没法返回,于是我就选择直接传递,反正还是要回到主线程在观察者中进行处理。
- 原来用publishProgress方法,来进行progress的实时更新。现在选择emitter.onNext(progress)来直接传递,直接在主线程中的观察者中进行更新。
- 原来下载状态发生改变之后,调用downloadListener的方法发布通知。现在只需在观察者的onNext方法中进行处理。
可以看到,用Rxjava替代之后,原来需要重写三个函数,现在只需简化成构建一对观察和被观察的对象,被观察者发送事件,观察者在主线程中处理事件,三合一更加简洁、直观。
此外,我还修改了最后面的请求代码。我发现如果连接不上,程序提出警告之后就不会继续运行。我搞了老长时间,怎么也得不到response返回的数据,后来才发现是我连接错误了,没有返回值。因此,可以用一个try\catch的结构捕捉异常,返回字节长度0。
还有初始定义的变量值我改为了从100开始,是为了防止和0-99的progress冲突,当然当progress为100时,自然下载状态就是TYPE_SUCCESS.
通过学习,我基本体会到了Rxjava的强大功能和链式的处理方式,整个下载服务就是在主函数中绑定服务,利用downloadBinder对象进行活动和服务之间的控制,在downloadBinder对象的方法中开启downloadTask,当下载状态改变时,调用监听downloadListener发布通知和更改状态,形成前台后台活动整体的控制。
在下载的时候,我把郭神提供的安装包下载下来,放到本地服务器的文件夹下,Url就是本地服务器的地址。当我试了试之前做其他项目时成功访问的IPV4地址时,发现怎么也连接不上,搞了好久才发现,
本地的IPV4地址是变化的!!!
修改为正确的IPV4地址后,程序成功运行。
大致的修改就是这样,以防万一,我把活动和服务的代码全部贴出来,下面是活动代码:
public class SecondActivity extends AppCompatActivity {
@BindView(R.id.button2)
Button button2;
@BindView(R.id.start_download)
Button startDownload;
@BindView(R.id.pause_download)
Button pauseDownload;
@BindView(R.id.cancel_download)
Button cancelDownload;
private DownloadService.DownloadBinder downloadBinder;
private ServiceConnection serviceConnection=new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
Log.e("main", "onServiceConnected executed ");
downloadBinder= (DownloadService.DownloadBinder) service;
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR1)
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_second);
ButterKnife.bind(this);
startDownload.setOnClickListener(v -> {
if(downloadBinder!=null) {
Log.e("main", "onCreate success" );
String Url = "http://10.128.73.133/eclipse-inst-win64.exe";
downloadBinder.startDownload(Url);
}else{
Log.e("main", "onCreate false");
}
});
pauseDownload.setOnClickListener(v -> {
if(downloadBinder!=null) {
downloadBinder.pauseDownload();
}
});
cancelDownload.setOnClickListener(v -> {
if(downloadBinder!=null) {
downloadBinder.cancelDownload();
}
});
Intent intent=new Intent(this,DownloadService.class);
startService(intent);//启动服务
bindService(intent,serviceConnection,BIND_AUTO_CREATE);//绑定服务
if(ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)!= PackageManager.PERMISSION_GRANTED){
ActivityCompat.requestPermissions(this,new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},1);
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
switch (requestCode){
case 1:
if(grantResults.length>0 && grantResults[0]!=PackageManager.PERMISSION_GRANTED) {
Toast.makeText(this, "权限获取失败", Toast.LENGTH_SHORT).show();
finish();
}
break;
default:
break;
}
}
@Override
protected void onDestroy() {
super.onDestroy();
unbindService(serviceConnection);
}
}
下面是服务代码:
public class DownloadService extends Service {
private DownloadTask downloadTask;
private NotificationManager notificationManager;
private String downloadUrl;
private static final String TAG = "DownloadService";
//获取通知对象
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR1)
private Notification getNotification(String title, int progress){
NotificationManagerCompat notificationManagerCompat=NotificationManagerCompat.from(this);
if(!notificationManagerCompat.areNotificationsEnabled()) {
Intent intent=new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
intent.setData(Uri.fromParts("package", getPackageName(), null));
startActivity(intent);
}
Notification.Builder builder = new Notification.Builder(this)
.setContentTitle(title)
.setWhen(System.currentTimeMillis())
.setSmallIcon(R.mipmap.ic_launcher);
if(progress>=0){
builder.setContentText(progress+"%");
builder.setProgress(100,progress,false);
}
if (Build.VERSION.SDK_INT > 26) {
NotificationChannel notificationChannel = new NotificationChannel("1", "download", NotificationManager.IMPORTANCE_DEFAULT);
notificationManager.createNotificationChannel(notificationChannel);
builder.setChannelId("1");
}
return builder.build();
}
public DownloadService() {
}
@Override
public void onCreate() {
notificationManager= (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
Log.e(TAG, "onCreate executed");
}
//定义监听器
private DownloadListener downloadListener=new DownloadListener() {
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR1)
@Override
public void onProgress(int progress) {
notificationManager.notify(1,getNotification("Downloading...",progress));
}
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR1)
@Override
public void onSuccess() {
downloadTask=null;
//前台通知关闭,创建一个下载成功的通知
stopForeground(true);
notificationManager.notify(1,getNotification("Download success",-1));
Toast.makeText(DownloadService.this,"Download Success",Toast.LENGTH_SHORT).show();
}
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR1)
@Override
public void onFailed() {
downloadTask=null;
stopForeground(true);
notificationManager.notify(1,getNotification("Download Failed",-1));
Toast.makeText(DownloadService.this,"Download Failed",Toast.LENGTH_SHORT).show();
}
@Override
public void onPaused() {
downloadTask=null;
Toast.makeText(DownloadService.this,"Paused",Toast.LENGTH_SHORT).show();
}
@Override
public void onCanceled() {
downloadTask=null;
stopForeground(true);
Toast.makeText(DownloadService.this,"Canceled",Toast.LENGTH_SHORT).show();
}
};
private DownloadBinder downloadBinder=new DownloadBinder();
class DownloadBinder extends Binder {
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR1)
public void startDownload(String Url){
if(downloadTask==null){
downloadUrl=Url;
downloadTask=new DownloadTask(downloadListener);
downloadTask.execute(downloadUrl);
Log.e(TAG, "startDownload executed");
//开启前台通知服务
startForeground(1,getNotification("Downloading...",0));
Toast.makeText(DownloadService.this,"Downloading...",Toast.LENGTH_SHORT).show();
}else{
Log.e(TAG, "startDownload not null");
}
}
public void pauseDownload(){
if(downloadTask!=null){
downloadTask.pauseDownload();
}
}
public void cancelDownload(){
if(downloadTask!=null){
downloadTask.cancelDownload();
}
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();
}
notificationManager.cancel(1);
stopForeground(true);
Toast.makeText(DownloadService.this,"Canceled",Toast.LENGTH_SHORT).show();
}
}
}
@Override
public IBinder onBind(Intent intent) {
// TODO: Return the communication channel to the service.
return downloadBinder;
}
}
最终结果:
我对于Rxjava的理解还十分浅薄,如果有任何错误欢迎指正。如果有任何的问题,也欢迎和我交流,希望大家共同进步!!