Android 详解 Service

1. 怎样理解 Android 中的异步消息处理机制?

答:Android 中的异步消息处理机制主要由 4 个部分组成:MessageHandlerMessageQueueLooper

  • MessageMessage 是在线程之间传递的消息。它可以在内部携带少量的信息,用于在不同线程之间交换数据。内部有一个 target 属性可以和 Handler 实现绑定(滴滴面试时没答上来这个点)。除了 Messagewhat 字段,还可以使用 arg1arg2 字段来携带一些整型数据,使用 obj 字段携带一个 Object 对象。
  • HandlerHandler 主要用于发送和处理消息。发送消息一般是使用 HandlersendMessage() 方法,发出的消息经过一系列地辗转处理后,最终会传递到 HandlerhandlerMessage() 方法中。
  • MessageQueueMessageQueue 是消息队列的意思。主要用于存放所有通过 Handler 发送的消息。这部分消息会一直存在于消息队列中,等待被处理。每个线程中只会有一个 MessageQueue 对象。
  • LooperLooper 是每个线程中 MessageQueue 的管家。调用 Looperloop() 方法,就会进入到一个无限循环当中,然后每当发现 MessageQueue 中存在一条消息,就会将它取出,并传递到 HandlerhandleMessage() 方法中。每个线程中也只会有一个 Looper 对象。
  • 关于 Handler 的几篇不错的文章

2. 【笔试题】手写一个简单的使用 Handler 的 Demo?

答:

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    public static final int UPDATE_TEXT = 1;
    private TextView text;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        text = (TextView) findViewById(R.id.text);
        Button changeText = (Button) findViewById(R.id.change_text);
        changeText.setOnClickListener(this);
    }

    private Handler handler = new Handler() {
        public void handleMessage() {
            switch (msg.what) {
                case UPDATE_TEXT:
                    // 在这里可以进行 UI 操作
                    text.setText("Nice to meet you");
                    break;
                default:
                    break;
            }
        }
    };

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.change_text:
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        Message message = new Message();
                        message.what = UPDATE_TEXT;
                        handler.sendMessage(message); // 将 Message 对象发送出去
                    }
                }).start();
                break;
            default:
                break;
        }
    }
}

3. 怎样理解 AsyncTask?

答:

  • AsyncTaskAndroid 提供的用于多线程的工具类,背后实现的原理也是基于异步消息处理机制
  • AsyncTask 是一个抽象类,创建子类继承 AsyncTask 时,可以为 AsyncTask 指定 3 个泛型参数:

    • Params:在执行 AsyncTask 时需要传入的参数,可用于在后台任务中使用。
    • Progress:后台任务执行时,如果需要在界面上显示当前的进度,则使用这里指定的泛型作为进度单位。
    • Result:当任务执行完毕后,如果需要对结果进行返回,则使用这里指定的泛型作为返回值类型。
  • AsyncTask 中经常需要重写的方法有 4 个:

    • onPreExecute():这个方法会在后台任务开始之前调用,用于进行一些界面上的初始化操作,比如显示一个进度条对话框等。
    • doInBackground(Params...)这个方法中的所有代码都会在子线程中运行,我们应该在这个方法内部处理所有的耗时任务。任务一旦完成就可以通过 return 语句将任务的执行结果返回,如果 AsyncTask 的第三个泛型参数指定的是 Void,就可以不返回任务执行结果。需要注意的是,这个方法中是不可以进行 UI 操作的,如果需要跟新 UI 元素,比如说反馈当前任务的执行进度,可以调用 publishProgress(Progress...) 方法来完成
    • onProgressUpdate(Progress...):当在后台任务中调用了 publishProgress(Progress...) 方法后,该方法就会很快被调用,方法中携带的参数就是在后台任务中传递过来的。在这个方法中可以对 UI 进行操作,利用参数中的数值就可以对界面元素进行相应的更新。
    • onPostExecute(Result):当后台任务执行完毕并通过 return 语句进行返回时,这个方法就很快会被调用。返回的数据会作为参数传递到该方法中,可以利用返回的数据来进行一些 UI 操作,比如说提醒任务执行的结果,以及关闭掉进度条对话框等。

4. 【笔试题】手写一个简单的使用 AsynTask 的 Demo?

答:

class DownloadTask extends AsyncTask<Void, Integer, Boolean> {
    @Override
    protected void onPreExecute() {
        progressDialog.show(); // 显示进度对话框
    }

    @Override
    protected Boolean doInBackground(Void... params) {
        try {
            while(true) {
                int downloadPercent = doDownload(); // 这是一个虚构的方法
                publishProgress(downloadPercent);
                if(downloadPercent >= 100) {
                    break;
                }
            }
        } catch(Exception e) {
            return false;
        }
        return true;
    }

    @Override
    protected void onProgressUpdate(Integer... values) {
        // 在这里更新下载进度
        progressDialog.setMessage("Download " + values[0] + "%");
    }

    @Override
    protected void onPostExecute(Boolean result) {
        progressDialog.dismiss(); // 关闭进度对话框
        // 在这里提示下载结果
        if(result) {
            Toast.makeText(context, "Download succeeded", Toast.LENGTH_SHORT).show();
        } else {
            Toast.makeText(context, "Download failed", Toast.LENGTH_SHORT).show();
        }
    }
}

// 启动这个任务
new DownloadTask().execute();

5. 怎样理解服务 Service?

答:

  • ServiceAndroid 中实现程序后台运行的解决方案,非常适合执行不需要和用户交互且要求长期运行的服务
  • Service 的运行不依赖于任何用户界面,即使程序被切换到后台,或者用户打开了另外一个应用程序,服务仍然能够保持正常运行。
  • Service 并不是运行在一个独立的进程当中的,而是依赖于创建服务时所在的应用程序进程。当某个应用程序进程被杀掉时,所有依赖于该进程的服务也会停止运行。
  • Service 并不会自动开启线程,所有的代码都是默认运行在主线程当中的。我们通常在服务的内部手动创建子程序,在子线程里执行具体的耗时任务,否则直接在 Service 里执行耗时任务可能出现主线程被阻塞,即 ANRActivity 5 秒;Broadcast 10 秒;Service 20 秒)
  • ServiceThread 的区别?参考 姜维:Android 中的 Service 与 Thread 的区别

6. 怎样理解 Service 的生命周期?

答:

  • 一旦在项目的任何位置调用了 ContextstartService() 方法,方法参数中相应的服务就会启动起来,并回调 onStartCommand() 方法。

    • 如果这个服务之前还没有创建过,ServiceonCreate() 方法会先于 onStartCommand() 方法执行。
    • Service 启动了之后会一直保持运行状态,直到 stopService()stopSelf() 被调用
    • 虽然每调用一次 startService() 方法,onStartCommand() 方法就会执行一次,但实际上每个服务都只会存在一个实例。所以不管调用了多少次 startService() 方法,只需调用一次 stopService()stopSelf(),服务就会停止下来。
  • 还可以调用 ContextbindService() 方法来获取一个服务的持久连接,这时就会回调服务中的 onBind() 方法

    • 类似地,如果这个服务之前还没有创建过,onCreate() 方法会先于 onBind() 方法执行。
    • 之后,调用方可以获取到 onBind() 方法里返回的 IBinder 对象的实例,这样就能自由地和服务进行通信了。
    • 只要调用方和服务之间的连接没有断开,服务就会一直保持运行状态。
  • 当调用了 startService() 方法后,又去调用 stopService() 方法,这时服务中的 onDestroy() 方法就会执行,表示服务以及销毁了。

    • 类似地,当调用了 bindService() 方法后,又去调用 unbindService() 方法,onDestroy() 方法也会执行。
    • 需要注意的是,我们完全有可能对一个服务既调用了 startService() 方法,又调用了 bindService() 方法,这种情况下该怎样销毁服务呢?
    • 根据 Android 系统的机制,一个服务只要被启动或者被绑定之后,就会一直处于运行状态,必须要让以上两种条件同时不满足,服务才能被销毁。所以,这种情况下要同时调用 stopService() 方法和 unbindService() 方法,这样 onDestroy() 方法才会执行。
  • 特殊情况(参考 Android 中 Service 生命周期):

    • 当服务被系统自动或手动(人为地在设置里停止)停止时,Service 仍然会正常走完生命周期,即会回调到 onDestroy() 方法。
    • 当使用类似“腾讯手机管家”一键加速时,直接 kill 掉该进程Service 不会走正常的生命周期,即 onDestroy() 方法不会被调用

7. Service 的生命周期图和 Service 生命周期流程图?

答:

  • Service 生命周期图:

    Service 生命周期图

  • Service 生命周期流程图:

    Service 生命周期流程图

8. 【笔试题】手写一个简单的使用 Service 的 Demo?

答:

  • MainActivity

    public class MainActivity extends AppCompatActivity implements View.OnClickListener {
        private MyService.DownloadBinder downloadBinder;
    
        private ServiceConnection connection = new ServiceConnection() {
            @Override
            public void onServiceDisconnected(ComponentName name) { // Activity 与 Service 解除绑定时调用
            }
    
            @Override
            public void onServiceConnected(ComponentName name, IBinder service) { // Activity 与 Service 成功绑定时调用
                downloadBinder = (MyService.DownloadBinder) service;
                downloadBinder.startDownload();
                downloadBinder.getProgress();
            }
        };
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            Button startService = (Button) findViewById(R.id.start_service);
            Button stopService = (Button) findViewById(R.id.stop_service);
            startService.setOnClickListener(this);
            stopService.setOnClickListener(this);
    
            Button bindService = (Button) findViewById(R.id.bind_service);
            Button unbindService = (Button) findViewById(R.id.unbind_service);
            bindService.setOnClickListener(this);
            unbindService.setOnClickListener(this);
        }
    
        @Override
        public void onClick(View v) {
            switch (v.getId()) {
                case R.id.start_service:
                    Intent startIntent = new Intent(this, MyService.class);
                    startService(startIntent); // 启动服务,这个方法是定义在 Context 中的
                    break;
                case R.id.stop_service:
                    Intent stopIntent = new Intent(this, MyService.class);
                    stopService(stopIntent); // 停止服务,这个方法是定义在 Context 中的。也可以在 MyService 中调用 stopSelf() 方法停止服务
                    break;
                case R.id.bind_service:
                    Intent bindIntent = new Intent(this, MyService.class);
                    // 绑定服务
                    // 第三个参数是一个标志位,这里传入 BIND_AUTO_CREATE,表示在活动和服务进行绑定后自动创建服务。这会使得 MyService 中的 onCreate() 方法得到执行,但 onStartCommand() 方法不会执行
                    bindService(bindIntent, connection, BIND_AUTO_CREATE);
                    break;
                case R.id.unbind_service:
                    // 解绑服务
                    unbindService(connection);
                    break;
                default:
                    break;
            }
        }
    }
    
  • MyService

    // 需要在 AndroidManifest.xml 文件中注册,属性 enabled 和 exported 都设为 true
    // 服务启动之后,还可以在手机的 Settings --> Developer options --> Running services 中找到它
    public class MyService extends Service {
        private DownloadBinder mBinder = new DownloadBinder();
    
        class DownloadBinder extends Binder {
            public void startDownload() { // 模拟方法
                Log.d("MyService", "startDownload executed");
            }
    
            public int getProgress() { // 模拟方法
                Log.d("MyService", "getProgress executed");
                return 0;
            }
        }
    
        // 任何一个服务在整个应用程序范围内都是通用的
        // 即 MyService 不仅可以和 MainActivity 绑定,还可以和任何一个其他的活动进行绑定,而且在绑定完成后它们都可以获取到相同的 DownloadBinder 实例
    
        @Override
        public IBinder onBind(Intent intent) {
            return mBinder;
        }
    
        @Override
        public void onCreate() { // 服务创建的时候调用
            super.onCreate();
            Log.d("MyService", "onCreate executed");
        }
    
        @Override
        public int onStartCommand(Intent intent, int flags, int startId) { // 每次服务启动的时候调用
            Log.d("MyService", "onStartCommand executed");
            return super.onStartCommand(intent, flags, startId);
        }
    
        @Override
        public void onDestroy() { // 服务销毁的时候调用
            super.onDestroy();
            Log.d("MyService", "onDestroy executed");
        }
    }
    

9. 什么是前台服务?前台服务和普通服务的区别是什么?使用前台服务的场景有哪些?

答:

  • 定义:

    • 前台服务是系统优先级比较高的一种服务
  • 区别:

    • 前台服务的系统优先级比普通服务要高,在系统内存不足时被回收的概率比普通服务的要小。
    • 前台服务会有一个正在运行的图标在系统的状态栏显示,下拉状态栏后可以看到更加详细的信息,非常类似于通知的效果
  • 场景:

    • 防止服务被系统回收。
    • 由于特殊的需求会要求必须使用前台服务,比如墨迹天气应用在后台更新数据前台展示、ofo 小黄车应用在用户骑行过程中显示用车详情等。

10. 【笔试题】手写一个简单的使用前台服务的 Demo

答:APINotification/startForeground().

public class MyService extends Service {
    ...
    @Override
    public void onCreate() {
        super.onCreate();
        Log.d("MyService", "onCreate executed");
        Intent intent = new Intent(this, MainActivity.class);
        PendingIntent pi = PendingIntent.getActivity(this, 0, intent, 0);
        Notification notification = new NotificationCompat.Builder(this)
                .setContentTitle("This is content title")
                .setContentText("This is content text")
                .setWhen(System.currentTimeMillis())
                .setSmallIcon(R.mipmap.ic_launcher)
                .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher))
                .setContentIntent(pi)
                .build();

        // 这里没有使用 NotificationManager 显示通知,而是调用了 startForeground() 方法
        // 第一个参数是通知的 id,类似于 notify() 方法的第一个参数,第二个参数就是构建的 Notification 对象
        // 调用 startForeground() 方法就会让 MyService 变成一个前台服务,并在系统状态栏显示
        startForeground(1, notification);
    }
}

11. 怎样理解 IntentService?

答:

  • 程序员的尴尬:

    public class MyService extends Service {
        ...
        @Override
        public int onStartCommand(Intent intent, int flags, int startId) {
            new Thread(new Runnable() { // 尴尬1:程序员总是容易忘记开启子线程去执行耗时逻辑
                @Override
                public void run() {
                    // 处理具体的耗时逻辑
                    stopSelf(); // 尴尬2:程序员总是容易忘记调用 stopService() 或者 stopSelf() 方法停止服务
                }
            }).start();
        }
    }
    
  • IntentServiceAndroid 提供的一个异步的、会自动停止的服务,可以很好地解决程序员的尴尬

12. 【笔试题】手写一个简单的使用 IntentService 的 Demo?

答:APIstartService(intentService).

  • MainActivity

    public class MainActivity extends AppCompatActivity implements View.OnClickListener {
        ...
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            ...
            Button startIntentService = (Button) findViewById(R.id.start_intent_service);
            startIntentService.setOnClickListener(this);
        }
    
        @Override
        public void onClick(View v) {
            switch (v.getId()) {
                ...
                case R.id.start_intent_service:
                    // 打印主线程的 id
                    Log.d("MainActivity", "Thread id is " + Thread.currentThread().getId());
                    Intent intentService = new Intent(this, MyIntentService.class);
                    startService(intentService);
                    break;
                default:
                    break;
            }
        }
    }
    
  • MyIntentService

    // 服务都是需要在 AndroidManifest.xml 文件中注册
    public class MyIntentService extends IntentService {
    
        public MyIntentService() { // 首先提供一个无参的构造方法
            super("MyIntentService"); // 调用父类的有参构造方法
        }
    
        @Override
        protected void onHandleIntent(Intent intent) {
            // 打印当前线程的 id
            Log.d("MyIntentService", "Thread id is " + Thread.currentThread().getId());
        }
    
        @Override
        public void onDestroy() {
            super.onDestroy();
            Log.d("MyIntentService", "onDestroy executed");
        }
    }
    

13. 【笔试题】手写一个在服务中下载文件的 Demo?

答:

  • 添加依赖库:

    • app/build.gradle 文件的 dependencies 闭包中添加依赖:compile 'com.squareup.okhttp3:okhttp:3.4.1'
  • 定义一个回调接口 DownloadListener

    public interface DownloadListener {
    
        void onProgress(int progress);
    
        void onSuccess();
    
        void onFailed();
    
        void onPaused();
    
        void onCanceled();
    }
    
  • 新建一个下载任务 DownloadTask

    // 第一个泛型参数 String 表示在执行 AsyncTask 的时候需要传入一个字符串参数给后台任务
    // 第二个泛型参数 Integer 表示使用整型数据来作为进度显示单位
    // 第三个泛型参数 Integer 表示使用整型数据来反馈执行结果
    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 boolean isCanceled = false;
        private boolean isPaused = false;
    
        private int lastProgress;
        private DownloadListener listener;
    
        public DownloadTask(DownloadListener listener) {
            this.listener = listener;
        }
    
        @Override
        protected Integer doInBackground(String... params) { // 在后台执行具体的下载逻辑
            InputStream is = null;
            RandomAccessFile savedFile = null;
            try {
                long downloadedLength = 0; // 纪录已下载的文件长度
                String downloadUrl = params[0]; // 从参数中获取下载的 URL 地址
                String fileName = downloadUrl.substring(downloadUrl.lastIndexOf("/")); // 根据 URL 地址解析下载的文件名
                // 指定将文件下载到 Environment.DIRECTORY_DOWNLOADS 目录下,即 SD 卡的 Download 目录
                String directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath(); 
                file = new File(directory + fileName);
                if(file.exist()) {
                    downloadedLength = file.length(); // 如果要下载的文件已存在,则读取已下载的字节数,方便后面启用断点续传功能
                }
                long contentLength = getContentLength(downloadUrl); // 获取待下载文件的总长度
                if(contentLength == 0) { // 文件有问题
                    return TYPE_FAILED;
                } else if(contentLength == downloadedLength) { // 已下载字节和文件总字节相等,说明已经下载完成了
                    return TYPE_SUCCESS;
                }
                OkHttpClient client = new OkHttpClient();
                Request request = new Request.Builder()
                    // 断点下载,指定从哪个字节开始下载
                    .addHeader("RANGE", "bytes=" + downloadedLength + "-") // header 用于告诉服务器我们想要从哪个字节开始下载,因为已下载过的部分就不需要重新下载了
                    .url(downloadUrl)
                    .build();
                Response response = client.newCall(request).execute();
                if(response != null) {
                    is = response.body().byteStream();
                    savedFile = new RandomAccessFile(file, "rw");
                    savedFile.seek(downloadedLength); // 跳过已下载的字节
                    byte[] b = new byte[1024];
                    int total = 0;
                    int len;
                    while((len = is.read(b)) != -1) {
                        if(isCanceled) {
                            return TYPE_CANCELED;
                        } else if(isPaused) {
                            return TYPE_PAUSED;
                        } else {
                            total += len;
                            savedFile.write(b, 0, len);
                            // 实时计算已下载的比分比
                            int progress = (int) ((total + downloadedLength) * 100 / contentLength);
                            publishProgress(progress); // 通知最新下载进度
                        }
                    }
                    response.body().close();
                    return TYPE_SUCCESS;
                }
            } catch(Exception e) {
                e.printStackTrace();
            } finally {
                try {
                    if(is != null) {
                        is.close();
                    }
                    if(savedFile != null) {
                        savedFile.close();
                    }
                    if(isCanceled && file != null) {
                        file.delete();
                    }
                } catch(Exception e) {
                    e.printStackTrace();
                }
            }
            return TYPE_FAILED;
        }
    
        @Override
        protected void onProgressUpdate(Integer... values) { // 在界面上更新当前的下载进度
            int progress = values[0];
            if(progress > lastProgress) {
                listener.onProgress(progress);
                lastProgress = progress;
            }
        }
    
        @Override
        protected void onPostExecute(Integer status) { // 通知最终的下载结果
            switch (status) {
                case TYPE_SUCCESS:
                    listener.onSuccess();
                    break;
                case TYPE_FAILED:
                    listener.onFailed();
                    break;
                case TYPE_PAUSED:
                    listener.onPaused();
                    break;
                case TYPE_CANCELED:
                    listener.onCanceled();
                default:
                    break;
            }
        }
    
        public void pauseDownload() {
            isPaused = true;
        }
    
        public void cancelDownload() {
            isCanceled = true;
        }
    
        private long getContentLength(String downloadUrl) throws IOException {
            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;
        }
    }
    
  • 创建下载服务 DownloadService

    public class DownloadService extends Service {
        private DownloadTask downloadTask;
        private String downloadUrl;
    
        private DownloadListener  listener = new DownloadListener() { // 创建匿名类实例
            @Override
            public void onProgress(int progress) {
                getNotificationManager().notify(1, getNotification("Downloading...", progress));
            }
    
            @Override
            public void onSuccess() {
                downloadTask = null;
                // 下载成功时将前台服务通知关闭,并创建一个新的下载成功的通知
                stopForeground(true);
                getNotificationManager().notify(1, getNotification("Download Success", -1));
                Toast.makeText(DownloadService.this, "Download Success", Toast.LENGTH_SHORT).show();
            }
    
            @Override
            public void onFailed() {
                downloadTask = null;
                // 下载失败时将前台服务通知关闭,并创建一个新的下载失败的通知
                stopForeground(true);
                getNotificationManager().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(ture);
                Toast.makeText(DownloadService.this, "Canceled", Toast.LENGTH_SHORT).show();
            }
        };
    
        private DownloadBinder mBinder = new DownloadBinder(); // 让 DownloadService 可以和活动通信
    
        @Override
        public IBinder onBind(Intent intent) {
            return mBinder;
        }
    
        class DownloadBinder extends Binder {
            public void startDownload(String url) {
                if(downloadTask == null) {
                    downloadUrl = url;
                    downloadTask = new DownloadTask(listener);
                    downloadTask.execute(downloadUrl);
                    startForeground(1, getNotification("Downloading...", 0)); // 让这个下载服务成为一个前台服务
                    Toast.makeText(DownloadService.this, "Downloading...", Toast.LENGTH_SHORT).show();
                }
            }
    
            public void pausedDownload() {
                if(downloadTask != null) {
                    downloadTask.pausedDownload();
                }
            }
    
            public void cancelDownload() {
                if(downloadTask != null) {
                    downloadTask.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.exist()) {
                            file.delete();
                        }
                        getNotificationManager().cancel(1);
                        stopForeground(true);
                        Toast.makeText(DownloadService.this, "Canceled", Toast.LENGTH_SHORT).show();
                    }
                }
            }
        }
    
        private NotificationMangaer getNotificationManager() {
            return (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
        }
    
        private Notification getNotification(String title, int progress) {
            Intent intent = new Intent(this, MainActivity.class);
            PendingIntent pi = PendingIntent.getActivity(this, 0, intent, 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) {
                // 当 progress 大于或等于 0 时才需显示下载进度
                builder.setContentText(progress + "%");
                // 第一个参数传入通知的最大进度,第二个参数传入通知单当前进度,第三个参数表示是否使用模糊进度条
                builder.setProgress(100, progress, false);
            }
            return builder.build();
        }
    }
    
  • MainActivity

    // 需要在 AndroidManifest.xml 文件中声明 INTERNET 和 WRITE_EXTERNAL_STORAGE 权限
    // MainActivity 和 DownloadService 也需要声明(四大组件都需要在清单文件中声明)
    public class MainActivity extends AppCompatActivity implements View.OnClickListener {
        private DownloadService.DownloadService.DownloadBinder downloadBinder;
        private ServiceConnection  connection = new ServiceConnection() { // 创建 ServiceConnection 匿名类
            @Override
            public void onServiceDisconnectioned(ComponentName name) {
            }
    
            @Override
            public void onServiceConnected(ComponentName name, IBinder service) {
                downloadBinder = (DownloadService.DownloadBinder) service; // 获取到 DownloadBinder 实例,用于在活动中调用服务的各种方法
            }
        };
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            Button startDownload = (Button) findViewById(R.id.start_download);
            Button pauseDownload = (Button) findViewById(R.id.pause_download);
            Button cancelDownload = (Button) findViewById(R.id.R.id.cancel_download);
    
            startDownload.setOnClickListener(this);
            pauseDownload.setOnClickListener(this);
            cancelDownload.setOnClickListener(this);
    
            Intent intent = new Intent(this, DownloadService.class);
            startService(intent); // 启动服务,保证 DownloadService 一直在后台运行
            bindService(intent, connection, BIND_AUTO_CREATE); // 绑定服务,让 MainActivity 和 DownloadService 进行通信
            // WRITE_EXTERNAL_STORAGE 运行时权限申请,因为下载文件要下载到 SD 卡的 Download 目录下
            if(ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORATE) != PackageManager.PERMISSION_GRANTED) {
                ActivityCompat.requestPermissions(MainActivity.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.start_download:
                    String url = "https://raw.githubusercontent.com/guolindev/eclipse/master/eclipse-inst-win64.exe";
                    downloadBinder.startDownload(url); // 传入一个 Eclipse 地址,以此向这个 Android 平台上曾经最出色的开发工具致敬
                    break;
                case R.id.pause_download:
                    downloadBinder.pauseDownload();
                    break;
                case R.id.cancel_download:
                    downloadBinder.cancelDownload();
                    break;
                default:
                    break;
            }
        }
    
        @Override
        public void onRequestPermissionResult(int requestCode, String[] permissions 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(connection); // 如果活动被销毁了,一定要对服务进行解绑,否则会造成内存泄露
        }
    }
    

14. Android 系统启动流程?

答:

-------------本文结束感谢您的阅读-------------