服务吧,在程序即便关闭的时候还是可以回后台运行,不搞情怀了。后台功能属于四大组件之一。
服务是Android中实现程序后台运行的解决方案,很适合执行不需要与用户交互而且长时间运行的任务。不依赖于任何UI,即便用户被切换到后台的时候,或者打开另一个程序的时候,服务仍然可以运行。
但是服务不是单独的进程,依赖于创建服务时所处的应用程序进程,当这个程序呗kill的时候,服务也就停了。服务本身并不会自动开启线程,所有代码都默认运行在主线程上,因此需要给服务手动创建子线程,否则可能会阻塞主线程。
多线程
基本用法
定义一个线程只需要新建一个类继承自Thread重写run方法,编写逻辑就行了
class MyThread extends Thread{
@Override
public void run(){
//逻辑
}
}
使用的话,new一个实例以后,调用start()启动即可。
另一种方法时继承Runnable接口
class MyThread implement Runnable{
public void run(){
//逻辑
}
}
这时候调用就需要这样
MtThread myThread=new MyThread();
new Thread(myThread).start();
最常用的一种就是匿名类方法
new Thread (new Runnable(){
public void run(){
//逻辑
}
}).start();
子线程中更新UI
和其他UI一样,Android的UI也是线程不安全,之前的WPF也是,得用委托才能在线程里更新UI。
一个栗子
修改activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.example.k.androidpractice.MainActivity">
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/ChangeText"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="helloworld"
android:id="@+id/ShowText"/>
</LinearLayout>
再修改一下MainActivity.java的内容,就是获取一下控件,然后设置监听,点击按钮后修改TextView按钮的文字内容。
public class MainActivity extends AppCompatActivity {
Button ChangeTextButton;
TextView ShowTextView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ChangeTextButton=findViewById(R.id.ChangeText);
ShowTextView=findViewById(R.id.ShowText);
ChangeTextButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new Thread(new Runnable() {
@Override
public void run() {
ShowTextView.setText("balabala");
}
}).start();
}
});
}
}
运行程序试一下。
效果是,TextView的内容改变了,但是随后程序就退出了。
报错
<font color="red">
E/AndroidRuntime: FATAL EXCEPTION: Thread-2
Process: com.example.k.androidpractice, PID: 9389
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:7753)
at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1225)
at android.view.View.requestLayout(View.java:23093)
at android.view.View.requestLayout(View.java:23093)
at android.view.View.requestLayout(View.java:23093)
at android.view.View.requestLayout(View.java:23093)
at android.view.View.requestLayout(View.java:23093)
at android.view.View.requestLayout(View.java:23093)
at android.view.View.requestLayout(View.java:23093)
at android.widget.TextView.checkForRelayout(TextView.java:8908)
at android.widget.TextView.setText(TextView.java:5730)
at android.widget.TextView.setText(TextView.java:5571)
at android.widget.TextView.setText(TextView.java:5528)
at com.example.k.androidpractice.MainActivity$1$1.run(MainActivity.java:72)
at java.lang.Thread.run(Thread.java:764)
</font>
说明的确不允许在子线程中修改UI,但是当必须用的时候怎么办呢,比如处理耗时的事件然后更新UI,Android提供了一套异步消息处理机制。
修改一下MainActivity代码,主要是创建了一个Handler对象,重写一下handleMessage()方法,通过msg.what判断传入的什么消息,进行相应逻辑处理。
在按钮点击事件中的子线程里,创建一个Message对象,将标识作为参数传入,标识是一个int型好像,再把Message发送给Handler,Handler收到消息后就会在handleMessage()中处理,而此时这个HandleMessage()运行在主线程。
public class MainActivity extends AppCompatActivity {
public static final int UPDATE_TEXT=1;
Button ChangeTextButton;
TextView ShowTextView;
private Handler handler=new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what){
case UPDATE_TEXT:ShowTextView.setText("aaaaa");break;
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ChangeTextButton=findViewById(R.id.ChangeText);
ShowTextView=findViewById(R.id.ShowText);
ChangeTextButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new Thread(new Runnable() {
@Override
public void run() {
Message message=new Message();
message.what=UPDATE_TEXT;
//发送
handler.sendMessage(message);
}
}).start();
}
});
}
}
然后运行程序,其实这次avd的软件又闪退了,重启AS就解决了。现在点击按钮可以发现TextView的内容改变了,而且软件不会报错。
异步消息处理机制
主要由四部分组成,Message、Handler、MesageQueue和Looper。前两者刚才用到了。
- Message:线程之间传递消息,可以在内部携带少量信息。刚才用了what字段存储数据,还可以用arg1和arg2字段携带一些整形数据,使用obj字段携带Object对象
- Handler:用于发送和处理消息。发送使用sendMessage(),最终会转到handleMessage()方法
- MessageQueue:消息队列用于存放所有通过Handler发送的消息,会一直存储在消息队列中,等待被处理,每个线程只有一个MessageQueue
- Looper:是MessageQueue管家,调用Loop的loop()后,陷入无限循环,每当MessageQueue中存有一条消息就取出来发送给handleMessage()中,每个线程也只有一个Looper
捋一遍异步消息处理流程
- 主线程创建Handler对象,重写handlerMessage()方法
- 子线程需要处理UI的地方创建Message对象,发送给Handler
- 消息会被添加到MessageQueue中等待处理,此时Looper会一直试图从队列中取出待处理消息,发送给handleMessage()方法
而runOnUiThread()其实就是一个异步消息处理机制的接口封装,内部流程和上面一样。
AsyncTask
另一个比较好的在子线程中更新UI的工具 。
其背后原理也是基于异步消息处理,同样Android做好了封装。
AsyncTask是一个抽象类,必须有一个类继承它,其中有三个泛型参数
- Params:执行AsyncTask需要传入的参数,用于后台任务中
- Progress:后台任务执行中,如果需要在界面显示进度之类的,使用这里指定的泛型作为进度单位
- Result:任务执行完毕后,如果需要对结果返回,在这里指定泛型作为返回值类型
class DownloadTask extends AsyncTask<Void,Integer,BBoolean>{
...
}
第一个参数Void标识不需要传入参数给后台任务,第二个参数指定为整型表示用整型数据作为进度显示单位,第三个表示用布尔型数据作为返回值结果。
然后还需要再重写几个方法,主要是这四个
- onPreExecute():在后台任务开始之前调用,用于界面初始化,比如显示一个进度条对话框
- doInBackground(Params…):这里面的所有代码都在子线程中运行,处理耗时任务,任务完成后可以通过return将任务结果 返回,如果AsyncTask第三个参数是Void则不返回结果,且在这里面不能进行UI的操作,如果需要反馈当前任务执行进度需要调用publishProgress()方法
- onProgressUpdate(Progress…):后台调用publishProgress(Progress…)方法以后,这个方法就会被调用,参数是在后台任务中传递过来的,在这里面可以进行对UI的操作,更新
- onPostExecute(Result):后台任务执行完毕后并通过return语句返回的时候这个方法会调用,返回的数据作为参数传递进来,可以利用返回数据进行UI操作,比如提醒任务执行的结果,关闭对话框之类的
在后面给个小栗子吧
服务
定义
首先先定义一个服务,在项目名右键 -> New -> Service -> Service,命名为MyService,代码如下所示
public class MyService extends Service {
public MyService() {
}
@Override
public IBinder onBind(Intent intent) {
// TODO: Return the communication channel to the service.
throw new UnsupportedOperationException("Not yet implemented");
}
}
其中只有一个方法onBind(),这也是唯一一个抽象方法,必须要在子类中实现。
还要重写onCreate()、onStartCommand()和onDestroy()三个方法,分别是在服务创建时调用、在每次启动服务的时候调用和在服务销毁的时候调用。
通常希望服务一旦启动就立刻执行某个动作的话,将代码写到onStartCommand()中,服务销毁的时候需要在onDestroy()中回收不使用的资源。
其实每一个服务也都需要在AndroidManifest.xml中注册才行,但是因为是直接New的,所以已经自动完成了。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.k.androidpractice">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:name="org.litepal.LitePalApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<font color="red">
<service
android:name=".MyService"
android:enabled="true"
android:exported="true"></service>
</font>
</application>
</manifest>
启动和停止
启动停止只要是通过Intent实现的。
小栗子
修改一下activity_main.xml内容,添加两个Button,一个开始一个停止
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.example.k.androidpractice.MainActivity">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Start"
android:id="@+id/Start"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Stop"
android:id="@+id/Stop"/>
</LinearLayout>
然后修改MainActivity.java内容,可以看到Intent的创建直接就是new一个Intent,参数直接指定Service的类名,调用startActivity()或者stopActivity()方法启动或者停止服务。
public class MainActivity extends AppCompatActivity {
Button StartButton,StopButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
StartButton=findViewById(R.id.Start);
StopButton.findViewById(R.id.Start);
StartButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent StartIntent=new Intent(MainActivity.this,MyService.class);
startService(StartIntent);
}
});
StopButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent StopIntent=new Intent(MainActivity.this,MyService.class);
stopService(StopIntent);
}
});
}
}
这里是通过按钮让服务停止的,如果想让服务自己停止,在MyService中的额任何一个位置调用stopSelf()方法就可以停下来了。
判断服务是否启动或者停止,最简单就是输出一点日志看看,修改MyService.java,就加了几个日志输出。
public class MyService extends Service {
public MyService() {
}
@Override
public IBinder onBind(Intent intent) {
// TODO: Return the communication channel to the service.
throw new UnsupportedOperationException("Not yet implemented");
}
@Override
public void onCreate() {
super.onCreate();
Log.d("MyService","启动了服务");
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.d("MyService","OnStartCommand");
return super.onStartCommand(intent, flags, startId);
}
@Override
public void onDestroy() {
super.onDestroy();
Log.d("MyService","服务停止了");
}
}
运行程序,点击按钮,可以在日志中看到有输出内容,第一次点击Start会触发onStart和onStartCommand,之后点击就只有后者了,点击Stop会停止服务。
启动服务后应该可以在运行程序进程里看到,但是我找不到= =
活动和服务进行通信
服务启动以后,就不受活动控制了,自己运行自己的逻辑代码,如果想让二者关系更为紧密,就需要用到onBind()方法,比如下载任务。
修改一下MyService.java,新建了一个DownloadBinder类继承字Binder,有两个方法一个是开始下载另一个是获取进度,这里只用日志输出作为示例,在onBind()方法中返回对象。
public class MyService extends Service {
private DownloadBinder mBinder=new DownloadBinder();
class DownloadBinder extends Binder{
public void startDownload(){
Log.d("MyService","开始下载");
}
public int getProgress(){
Log.d("MyService","获取进度");
return 0;
}
}
public MyService() {
}
@Override
public IBinder onBind(Intent intent) {
// TODO: Return the communication channel to the service.
return mBinder;
}
@Override
public void onCreate() {
super.onCreate();
Log.d("MyService","启动了服务");
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.d("MyService","OnStartCommand");
return super.onStartCommand(intent, flags, startId);
}
@Override
public void onDestroy() {
super.onDestroy();
Log.d("MyService","服务停止了");
}
}
在activity_main.xml中添加两个按钮,一个是绑定一个是解除绑定
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.example.k.androidpractice.MainActivity">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Start"
android:id="@+id/Start"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Stop"
android:id="@+id/Stop"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Bind Service"
android:id="@+id/Bind"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="UnBind Service"
android:id="@+id/UnBind"/>
</LinearLayout>
修改一下MainActivity.java,加的东西有点多。首先是创建了一个MyService.DownloadBinder对象,创建了一个ServiceConnection匿名类,重写了onServiceConnected()和onServiceDisconnected()方法。
在onServiceConnected()方法中,将service转为我们使用的DownloadBinder类,这样就可以调用DownloadBinder中的各种方法了。
又添加了两个按钮,一个是绑定按钮,一个是解绑按钮,绑定按钮监听事件中创建一个Intent,还是MyService类,调用bindService()方法绑定服务。
在解绑按钮监听事件中,调用unbindService()方法解绑服务。
public class MainActivity extends AppCompatActivity {
Button StartButton,StopButton,UnBindButton,BindButon;
private MyService.DownloadBinder downloadBinder;
private ServiceConnection connection=new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
downloadBinder=(MyService.DownloadBinder)service;
downloadBinder.startDownload();
downloadBinder.getProgress();
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
StartButton=findViewById(R.id.Start);
StopButton=findViewById(R.id.Stop);
BindButon=findViewById(R.id.Bind);
UnBindButton=findViewById(R.id.UnBind);
StartButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent StartIntent=new Intent(MainActivity.this,MyService.class);
startService(StartIntent);
}
});
StopButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent StopIntent=new Intent(MainActivity.this,MyService.class);
stopService(StopIntent);
}
});
BindButon.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent BindIntent=new Intent(MainActivity.this,MyService.class);
bindService(BindIntent,connection,BIND_AUTO_CREATE);
}
});
UnBindButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
unbindService(connection);
}
});
}
}
然后运行程序,点击Start就会开启服务,Stop停止服务,Bind会绑定,Unbind会解绑,但是,绑定以后必须要解绑才能停止服务好像,不能用stop停止。
而且如果直接点击Bind开始绑定,不先启动服务,会自动启动服务,然后用UnBind停止服务,Stop没用。
服务生命周期
服务也有自己的生命周期,和活动碎片一样。
当程序中调用Context的startService()方法,服务就会启动,同时回调onStartComand()方法,如果服务没有创建过,会先调用onCreate()方法然后调用onStartCommand()方法。启动之后会一直保持运行状态,直到调用了stopService()或者stopSelf()方法,不管调用了多少次startService(),一次就能停。
可以通过Context的bindService()获取一个持久性连接,会回调服务中的onBind()方法,如果服务没有创建,会先调用onCreate()方法然后调用onBind()方法。
当调用了startService()后,调用stopService()方法,则会执行服务的onDestroy()方法,即销毁服务。
调用bindService()后调用unbindService()方法,也会执行onDestroy()方法。
但是当同时执行了startService()和bindService()后,必须要同时不满足启动服务和绑定才能让服务停止,即必须同时调用stopService()和unbindService()方法才会执行onDestroy()销毁服务。
Other
服务几乎都是后台运行,优先级比较低,内存不够的时候可能会回收掉后台的一些服务,如果希望服务一直运行不被回收可以考虑前台服务,前台服务的最大区别是会一直在通知栏拥有一个提醒,不仅仅因为怕被回收用这个前台服务,天气软件之类的也可以使用,在通知栏显示信息。
修改一下MyService,只修改onCreate()就好了。
public void onCreate() {
super.onCreate();
Log.d("MyService","启动了服务");
Intent intent=new Intent(this,MainActivity.class);
PendingIntent pendingIntent=PendingIntent.getActivity(this,0,intent,0);
Notification notification=new NotificationCompat.Builder(this)
.setContentTitle("this is a title")
.setContentText("this is a text")
.setWhen(System.currentTimeMillis())
.setSmallIcon(R.mipmap.ic_launcher)
.setLargeIcon(BitmapFactory.decodeResource(getResources(),R.mipmap.ic_launcher))
.setContentIntent(pendingIntent).build();
startForeground(1,notification);
}
然后就报错了
03-02 08:46:34.815 17933-17933/com.example.k.androidpractice D/skia: --- Failed to create image decoder with message 'unimplemented'
03-02 08:46:34.816 17933-17933/com.example.k.androidpractice W/Notification: Use of stream types is deprecated for operations other than volume control
03-02 08:46:34.816 17933-17933/com.example.k.androidpractice W/Notification: See the documentation of setSound() for what to use instead with android.media.AudioAttributes to qualify your playback use case
这个问题大概说的是,这个方法已经用不成,过时了。
传统Notification
API 26以前,主要使用NotificationManager和Notification,通过api设置一系列属性
NotificationManager mNotificationManager =(NotificationManager) getSystemService(NOTIFICATION_SERVICE);//得到一个manager对象
Notification notification = new NotificationCompat.Builder(this)
.setSmallIcon(R.mipmap.ic_launcher) .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher))
.setContentTitle("您有一条新通知")
.setContentText("这是一条逗你玩的消息")
.build();//以上采用Builder模式,获得notification
mNotificationManager.notify(1, notification);//将其显示出来,这个1主要是指notification的ID
然后通知也应该是可以点击的,点击之后通知消失,启动另一个Activity。就需要一个PendingIntent,表示一个延时的意思。这两个0其实是优先级,先不管,第一个就是Context,第三个是跳转到哪个Intent。
Intent intent=new Intent(this,MainActivity.class);
PendingIntent pendingIntent=PendingIntent.getActivity(this,0,intent,0);
可以添加这个属性
.setAutoCancel(true)//控制点击消失
.setContentIntent(pintent)//设置跳转Activity
然后还可以设置呼吸灯,震动之类的
.setVibrate(new long[]{100, 200, 300, 400, 500, 400, 300, 200, 400})//设置震动
.setLights(Color.RED,1000,1000)//设置LED灯光,参数分别为:颜色、亮时间、暗时间
传统通知是这样。
新
我也搞不清有什么变化,反正就是API 26以上不能这么写。
解决方法呢,我把avd换成API 25了,就可以了
IntentService
服务中的代码都是默认运行在主线程里的,如果处理一些比较耗时的逻辑会ANR现象(Application Not Responding)。
就需要多线程了。在服务中的每个具体方法中开启一个子线程,然后处理耗时逻辑。大致结构如下
public class MyService extends Service{
...
public int onStartVommand(Intent intent,int flags,int startId){
new Thread(new Runnable(){
public void run(){
//逻辑代码
stopSelf();
}
}).start();
return ...
}
}
但是可能会出现忘记开启线程或者忘记调用stopSelf()方法,可以简单创建一个异步、会自动停止的服务,Android专门提供了一个InterService类解决这个问题。
新建一个类继承自IntentService,一个无参构造函数,调用一下父类构造函数,重写onHandleIntent()方法,可以处理一些逻辑而且不用担心ANR问题,因为这里面已经新建了一个线程。
public class MyIntentService extends IntentService {
public MyIntentService() {
super("MyIntentService");
}
@Override
public void onDestroy() {
super.onDestroy();
Log.d("MyIntentService","onDestroy executed");
}
@Override
protected void onHandleIntent(@Nullable Intent intent) {
Log.d("MyIntentService","Thread id is "+Thread.currentThread().getId());
}
}
修改一下布局文件,添加一个启动IntentService的按钮,添加监听。
StartIntentServiceButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.d("MainActivity","Thread id is "+Thread.currentThread().getId());
Intent intentService=new Intent(MainActivity.this,MyIntentService.class);
startService(intentService);
}
});
最后在AndroidManifest.xml中注册一下服务
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.k.androidpractice">
<uses-permission android:name="android.permission.INTERNET" />
<application
...
<service android:name=".MyIntentService"></Service>
</application>
</manifest>
运行程序,点击最后一个按钮,可以看到日志输出。不仅id不一样,onDestroy()方法也执行了,说明他自动停止了。集开启线程和自动停止于一身。
小栗子-下载
下载功能在服务中是经常会用到的。
首先需要添加okhttp的依赖
compile 'com.squareup.okhttp3:okhttp:3.4.1'
创建一个interface叫DownloadListener用来监听下载时候的状况,实现的五个方法分别是通知当前下载进度、成功事件、失败事件、暂停事件和取消事件。
public interface DownloadListener {
void onProgress(int progress);
void onSuccess();
void onFailed();
void onPaused();
void onCanceled();
使用AsyncTask实现,创建一个类DownloadTask继承自AsyncTask类,需要重写三个方法,doInBackground()、onProgressUpdate()和onPostEecute()。
首先需要给AsyncTask传入三个泛型参数,第一个String传入给后台任务,第二个Integer表示使用整型作为进度显示单位,第三个表示用整型数据作为返回结果。
定义四个TYPE用于指示下载的四个状态,创建一个DownloadListener对象,之后可以用这个进行回调。
doInBackground():后台具体下载逻辑
- 从参数获取下载地址,解析出文件名,获取SD卡的Download路径。
- 判断一下是否已存在需要下载的文件,如果存在,读取文件字节数,用于断点续传。
- 调用getContentLength()方法获取文件总大小,如果为0说明出错,如果等于当前文件大小,说明下载完了。
- 如果没有下载完,调用OkHttp的方法发送请求,需要带上一个Header,指示从多少字节开始下载。
- while读取接收到的数据,写入文件中,同时每次需要判断是否取消或者暂停,没有的话计算下载进度,调用publishProgress()发出更新通知。
onProgressUpdate():在UI上显示下载进度
将当前下载进度和上次下载进度进行比较,如果有变化 调用DownloadListener的onnProgress()方法更新进度
onPostExecute():显示最终下载结果
根据返回的参数调用相应的回调方法。
public class DownloadTask extends AsyncTask<String,Integer,Integer> {
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 DListener;
private boolean IsCanceled=false;
private boolean IsPaused=false;
private int LastProgress;
public DownloadTask(DownloadListener listener){
this.DListener=listener;
}
@Override
protected Integer doInBackground(String... params) {
InputStream Is=null;
RandomAccessFile SavedFile=null;
File DownloadFile=null;
try{
long DownloadLength=0; //下载文件长度
String DownloadURL=params[0];
String FileName=DownloadURL.substring(DownloadURL.lastIndexOf("/"));
String Directory= Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath();
DownloadFile=new File(Directory+FileName);
if (DownloadFile.exists()){
DownloadLength=DownloadFile.length();
}
long ContentLength=getContentLenngth(DownloadURL);
if (ContentLength==0){ //长度为0失败了
return TYPE_FAILED;
}else if (ContentLength==DownloadLength){ //下载成功
return TYPE_SUCCESS;
}
OkHttpClient Client=new OkHttpClient();
Request HttpRequest=new Request.Builder() //断点续传?
.addHeader("RANGE","bytes="+DownloadLength+"-")
.url(DownloadURL)
.build();
Response HttpResponse=Client.newCall(HttpRequest).execute();
if (HttpResponse!=null){
Is=HttpResponse.body().byteStream();
SavedFile=new RandomAccessFile(FileName,"rw");
SavedFile.seek(DownloadLength);
byte[] Byte=new byte[1024];
int Total=0;
int Len;
while ((Len=Is.read(Byte))!=-1){
if (IsCanceled){
return TYPE_CANCELED;
}else if (IsPaused){
return TYPE_PAUSED;
}else{
Total+=Len;
SavedFile.write(Byte,0,Len);
int Progress=(int)((Total+DownloadLength)*100/ContentLength);
publishProgress(Progress);
}
}
HttpResponse.body().close();
return TYPE_SUCCESS;
}
}catch (Exception e){
e.printStackTrace();
}finally {
try{
if (Is!=null)
Is.close();
if (SavedFile!=null)
SavedFile.close();
if (IsCanceled && DownloadFile!=null)
DownloadFile.delete();
}catch(Exception e){
e.printStackTrace();
}
}
return TYPE_FAILED;
}
@Override
protected void onProgressUpdate(Integer... values) {
int Progress=values[0];
if (Progress>LastProgress){
DListener.onProgress(Progress);
LastProgress=Progress;
}
}
@Override
protected void onPostExecute(Integer integer) {
switch (integer){
case TYPE_SUCCESS:DListener.onSuccess();break;
case TYPE_CANCELED:DListener.onCanceled();break;
case TYPE_FAILED:DListener.onFailed();break;
case TYPE_PAUSED:DListener.onPaused();break;
}
}
public void pauseDownload(){
IsPaused=true;
}
public void cancelDownload(){
IsCanceled=true;
}
private long getContentLenngth(String DownloadURL)throws IOException {
OkHttpClient Client=new OkHttpClient();
Request HttpRequests=new Request.Builder()
.url(DownloadURL)
.build();
Response HttpResponse=Client.newCall(HttpRequests).execute();
if (HttpResponse!=null && HttpResponse.isSuccessful()){
long ContentLength=HttpResponse.body().contentLength();
HttpResponse.body().close();
return ContentLength;
}
return 0;
}
}
之后需要编写代码实现后台运行的功能,创建一个下载服务DownloadService
右键com.example.k.androidpractice -> new -> Service ->Service
。
修改代码,真的好长啊!!!!!!!!!!!!!!!!!!!!!!!!
首先创建一个匿名类实例,实现了接口中的方法,调用getNotification()方法构建了用于显示下载进度的通知,调用NotificationManager中的notify()方法触发通知,可以在下拉状态栏中看到了。
onSuccess()等方法:关闭正在下载的前台通知,创建新通知告诉用户下载好了。
创建了一个DownloadBinder类,提供了startDownload()方法、pauseDownload()和cancelDownload()方法。
- start:创建一个DownloadTask对象,把DownloadListener传入参数,调用execute()开始下载,并且传入参数URL。同时调用startForeground()方法使其称为前台服务,可以在通知栏中持续运行
- pause:简单调用DownloadTask中的pauseDownload()方法。
- cancel:和暂停差不多,但是取消的时候需要将下载的文件删除。
所有的通知都是通过getNotification()构建的,这个之前应该有出现过,startProgress()没出现过,有三个参数,第一个是进度条最大值,第二个当前进度,第三个是否使用模糊进度条,设置完后状态栏就有进度条了。
public class DownloadService extends Service {
DownloadTask DTask;
String DownloadURL;
DownloadListener DListener=new DownloadListener() {
@Override
public void onProgress(int progress) {
getNotificationManager().notify(1,getNotification("Download...",progress));
}
@Override
public void onSuccess() {
DListener=null;
//下载成功,关闭服务通知,并且创建下载成功的通知
stopForeground(true);
getNotificationManager().notify(1,getNotification("Download success",-1));
Toast.makeText(DownloadService.this,"下载好了'", Toast.LENGTH_SHORT).show();
}
@Override
public void onFailed() {
DTask=null;
//下载失败,关闭服务通知,并且创建下载失败的通知
stopForeground(true);
getNotificationManager().notify(1,getNotification("Download failed",-1));
Toast.makeText(DownloadService.this,"下载失败bbbbbbbbbbbbbb'", Toast.LENGTH_SHORT).show();
}
@Override
public void onPaused() {
DTask=null;
Toast.makeText(DownloadService.this,"下载暂停zzzzzzzzzzz", Toast.LENGTH_SHORT).show();
}
@Override
public void onCanceled() {
DTask=null;
stopForeground(true);
Toast.makeText(DownloadService.this,"下载取消了'", Toast.LENGTH_SHORT).show();
}
};
public DownloadService() {
}
DownloadBinder mBinder=new DownloadBinder();
@Override
public IBinder onBind(Intent intent) {
// TODO: Return the communication channel to the service.
return mBinder;
}
//自定义binder
class DownloadBinder extends Binder{
public void startDownload(String DURL){
if (DTask==null){
DownloadURL=DURL;
DTask=new DownloadTask(DListener);
DTask.execute(DownloadURL);
startForeground(1,getNotification("Downloading...",0));
Toast.makeText(DownloadService.this,"Downloading...",Toast.LENGTH_SHORT).show();
}
}
}
public void pauseDownload(){
if (DTask!=null){
DTask.pauseDownload();
}
}
public void cancelDownload(){
if (DTask!=null){
DTask.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,"取消了",Toast.LENGTH_SHORT).show();
}
}
}
public NotificationManager getNotificationManager(){
return (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
}
public Notification getNotification(String Title,int Progress){
Intent intent=new Intent(this,MainActivity.class);
PendingIntent PI=PendingIntent.getActivity(this,0,intent,0);
NotificationCompat.Builder NBuilder=new NotificationCompat.Builder(this);
NBuilder.setSmallIcon(R.mipmap.ic_launcher);
NBuilder.setLargeIcon(BitmapFactory.decodeResource(getResources(),R.mipmap.ic_launcher));
NBuilder.setContentIntent(PI);
NBuilder.setContentTitle(Title);
if (Progress>0){
//进度大于0时显示进度
NBuilder.setContentText(Progress+"%");
NBuilder.setProgress(100,Progress,false);
}
return NBuilder.build();
}
}
至此后端基本上结束了,开始前端UI。
在MainActivity.xml中添加三个按钮
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.example.k.androidpractice.MainActivity">
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="start"
android:id="@+id/Start"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="pause"
android:id="@+id/Pause"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="cancel"
android:id="@+id/Cancel"/>
</LinearLayout>
修改MainActivity.java代码。
首先创建了一个匿名类ServiceConnection,在onServiceConnected()方法中获取DownloadBinder实例,之后就可以调用服务提供的方法了。
onCreate():初始化并设置了点击事件监听,调用startService()方法和bindService()方法,启动和绑定服务,服务绑定后可以让MainActivity和DownloadService通信。最后申请一些权限。
然后是一个简单的onClick()重写。
活动销毁时调用onDestroy()方法,需要在这里解除服务的绑定,否则可能会内存泄漏。
public class MainActivity extends AppCompatActivity implements View.OnClickListener{
DownloadService.DownloadBinder DownloadBinder;
private ServiceConnection connection=new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
DownloadBinder=(DownloadService.DownloadBinder)service;
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button StartButton=findViewById(R.id.Start);
Button PauseButton=findViewById(R.id.Pause);
Button CancelButton=findViewById(R.id.Cancel);
StartButton.setOnClickListener(this);
PauseButton.setOnClickListener(this);
CancelButton.setOnClickListener(this);
//服务
Intent intent=new Intent(this,DownloadService.class);
startService(intent);
bindService(intent,connection,BIND_AUTO_CREATE); //绑定服务
if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED){
ActivityCompat.requestPermissions(MainActivity.this,new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},1);
}
}
@Override
protected void onDestroy() {
super.onDestroy();
unbindService(connection);
}
@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;
}
}
@Override
public void onClick(View v) {
if (DownloadBinder==null)
return;
switch(v.getId()){
case R.id.Start:
String url = "https://raw.githubusercontent.com/guolindev/eclipse/master/eclipse-inst-win64.exe";
DownloadBinder.startDownload(url);
break;
case R.id.Pause:
DownloadBinder.pauseDownload();
break;
case R.id.Cancel:
DownloadBinder.cancelDownload();
break;
}
}
}
最后在Manifest.xml文件中添加权限就好了,应该还需要声明服务的,但是new服务的时候已经自动完成了。
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
然后运行。差不多就这样。但是不知道是有bug还是网不好,下载失败,不管了先。
ssssssssssss