Android开发 - 线程和服务

服务吧,在程序即便关闭的时候还是可以回后台运行,不搞情怀了。后台功能属于四大组件之一。

服务是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
    捋一遍异步消息处理流程
  1. 主线程创建Handler对象,重写handlerMessage()方法
  2. 子线程需要处理UI的地方创建Message对象,发送给Handler
  3. 消息会被添加到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

image-1
这个问题大概说的是,这个方法已经用不成,过时了。

传统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了,就可以了
image-2

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()方法也执行了,说明他自动停止了。集开启线程和自动停止于一身。
image-3
image-4

小栗子-下载

下载功能在服务中是经常会用到的。
首先需要添加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():后台具体下载逻辑

  1. 从参数获取下载地址,解析出文件名,获取SD卡的Download路径。
  2. 判断一下是否已存在需要下载的文件,如果存在,读取文件字节数,用于断点续传。
  3. 调用getContentLength()方法获取文件总大小,如果为0说明出错,如果等于当前文件大小,说明下载完了。
  4. 如果没有下载完,调用OkHttp的方法发送请求,需要带上一个Header,指示从多少字节开始下载。
  5. 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()方法。

  1. start:创建一个DownloadTask对象,把DownloadListener传入参数,调用execute()开始下载,并且传入参数URL。同时调用startForeground()方法使其称为前台服务,可以在通知栏中持续运行
  2. pause:简单调用DownloadTask中的pauseDownload()方法。
  3. 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

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值