主頁 > 移動端開發 > 《第一行代碼:Android篇》學習筆記(十)

《第一行代碼:Android篇》學習筆記(十)

2022-05-12 08:31:50 移動端開發

本文和接下來的幾篇文章為閱讀郭霖先生所著《第一行代碼:Android(篇第2版)》的學習筆記,按照書中的內容順序進行記錄,書中的Demo本人全部都做過了,
每一章節本人都做了詳細的記錄,以下是我學習記錄(包含大量書中內容的整理和自己在學習中遇到的各種bug及解決方案),方便以后閱讀和查閱,最后,非常感激郭霖先生提供這么好的書籍,

第10章 后臺默默的勞動者——探究服務

以前,全球的手機市場是由諾基亞統治著的,諾基亞的Symbian作業系統做得特別出色,因為比起一般的手機,它可以支持后臺功能,

而如今,Symbian早已風光不再,Android和iOS幾乎占據了智能手機全部的市場份額,在這兩大移動作業系統中,iOS一開始是不支持后臺的,后來逐漸意識到這個功能的重要性,才加入了后臺功能,而Android則是沿用了Symbian的老習慣,從一開始就支持后臺功能,這使得應用程式即使在關閉的情況下仍然可以在后臺繼續運行,

10.1 服務是什么

服務(Service)是Android中實作程式后臺運行的解決方案,它非常適合去執行那些不需要和用戶互動而且還要求長期運行的任務,服務的運行不依賴于任何用戶界面,即使程式被切換到后臺,或者用戶打開了另外一個應用程式,服務仍然能夠保持正常運行,

需要注意的是,服務并不是運行在一個獨立的行程當中的,而是依賴于創建服務時所在的應用程式行程,當某個應用程式行程被殺掉時,所有依賴于該行程的服務也會停止運行,

另外,也不要被服務的后臺概念所迷惑,實際上服務并不會自動開啟執行緒,所有的代碼都是默認運行在主執行緒當中的,也就是說,我們需要在服務的內部手動創建子執行緒,并在這里執行具體的任務,否則就有可能出現主執行緒被阻塞住的情況,

10.2 Android多執行緒編程

當我們需要執行一些耗時操作,比如說發起一條網路請求時,考慮到網速等其他原因,服務器未必會立刻回應我們的請求,如果不將這類操作放在子執行緒里去運行,就會導致主執行緒被阻塞住,從而影響用戶對軟體的正常使用,

10.2.1 執行緒的基本用法

Android多執行緒編程其實并不比Java多執行緒編程特殊,基本都是使用相同的語法,比如說,定義一個執行緒只需要新建一個類繼承自Thread,然后重寫父類的run()方法,并在里面撰寫耗時邏輯即可,如下所示:

class MyThread extends Thread {
    @Override
    public void run() {
        //處理具體的邏輯
    }
} 

啟動執行緒

new MyThread().start();

當然,使用繼承的方式耦合性有點高,更多的時候我們都會選擇使用實作Runnable介面的方式來定義一個執行緒,如下所示:

class MyThread implements Runnable {
    @Override
    public void run() {
        //處理具體邏輯
    }
}

如果使用了這種寫法,啟動執行緒的方法也需要進行相應的改變,如下所示:

MyThread myThread = new MyThread();
new Thread(myThread).start();

當然,如果你不想專門再定義一個類去實作Runnable介面,也可以使用匿名類的方式,這種寫法更為常見,如下所示:

new Thread(new Runnable() {
    @Override
    public void run() {
        //處理具體的邏輯
    }
}).start();

下面我們來看一下Android多執行緒編程與Java多執行緒編程不同的地方,

10.2.2 在子執行緒中更新UI

和許多其他的GUI庫一樣,Android的UI也是執行緒不安全的,也就是說,如果想要更新應用程式里的UI元素,則必須在主執行緒中進行,否則就會出現例外,新建一個AndroidThreadTest專案,然后修改activity_main.xml中的代碼,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_
    android:layout_height="match_parent">
    <Button
        android:layout_
        android:layout_height="wrap_content"
        android:id="@+id/change_text"
        android:text="Change Text"/>
    <TextView
        android:layout_
        android:layout_height="wrap_content"
        android:id="@+id/text"
        android:layout_centerInParent="true"
        android:text="Hello World"
        android:textSize="20sp"/>
</RelativeLayout>

布局檔案中定義了兩個控制元件,TextView用于在螢屏的正中央顯示一個Helloworld字串,Button用于改變TextView中顯示的內容,我們希望在點擊Button后可以把TextView中顯示的字串改成Nice to meet you,接下來修改MainActivity中的代碼,如下所示:

package com.zhouzhou.androidthreadtest;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    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);
    }

    @Override
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.change_text:
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        text.setText("Nice to meet you!");
                    }
                }).start();
                break;
            default:
                break;
        }
    }
}

可以看到,在Change Text按鈕的點擊事件里面開啟了一個子執行緒,然后在子執行緒中呼叫TextView的setText()方法將顯示的字串改成Nice to meetyou,

代碼的邏輯非常簡單,只不過我們是在子執行緒中更新UI的,現在運行一下程式,并點擊Change Text按鈕,你會發現程式果然崩潰了,閃退,

然后觀察logcat中的錯誤日志,可以看出是由于在子執行緒中更新UI所導致的,如圖:

android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

image

由此證實了Android確實是不允許在子執行緒中進行UI操作的,但是有些時候,我們必須在子執行緒里去執行一些耗時任務,然后根據任務的執行結果來更新相應的UI控制元件,這該如何是好呢?

對于這種情況,Android提供了一套異步訊息處理機制,完美地解決了在子執行緒中進行UI操作的問題,本小節中我們先來學習一下異步訊息處理的使用方法,下一小節中再去分析它的原理,修改MainActivity中的代碼,如下所示:

package com.zhouzhou.androidthreadtest;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    public static final int UPDATE_TEXT =1;
    private TextView text;
    private Handler handler = new Handler() {
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case UPDATE_TEXT:
                    //這里可以進行UI操作
                    text.setText("Nice to meet you!");
                    break;
                default:
                    break;
            }
        }
    };
    @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);
    }

    @Override
    public void onClick(View view) {
        switch (view.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;
        }
    }
}

我們先是定義了一個整型常量UPDATE_TEXT,用于表示更新TextView這個動作,然后新增一個Handler物件,并重寫父類的handleMessage()方法,在這里對具體的Message進行處理,如果發現Message的what欄位的值等于UPDATE_TEXT,就將TextView顯示的內容改成Nice to meet you,

下面再來看一下Change Text按鈕的點擊事件中的代碼,可以看到,這次我們并沒有在子執行緒里直接進行UI操作,而是創建了一個Message(android.os.Message)物件,并將它的what欄位的值指定為UPDATE_TEXT,然后呼叫Handler的sendMessage()方法將這條Message發送出去,

Handler就會收到這條Message,并在handleMessage()方法中對它進行處理,注意此時handleMessage()方法中的代碼就是在主執行緒當中運行的了,所以我們可以放心地在這里進行UI操作,接下來對Message攜帶的what欄位的值進行判斷,如果等于UPDATE_TEXT,就將TextView顯示的內容改成Nice to meet you,

現在重新運行程式,可以看到螢屏的正中央顯示著Hello world,然后點擊一下Change Text按鈕,顯示的內容就被替換成Nice to meet you,如圖:

image

10.2.3 決議異步訊息處理機制

Android中的異步訊息處理主要由4個部分組成:Message、Handler、MessageQueue和Looper,其中Message和Handler在上一小節中我們已經接觸過了,而MessageQueue和Looper對于你來說還是全新的概念,下面我就對這4個部分進行一下簡要的介紹,

  1. Message

Message是在執行緒之間傳遞的訊息,它可以在內部攜帶少量的資訊,用于在不同執行緒之間交換資料,上一小節中我們使用到了Message的what欄位,除此之外還可以使用arg1和arg2欄位來攜帶一些整型資料,使用obj欄位攜帶一個Object物件,

  1. Handler

Handler顧名思義也就是處理者的意思,它主要是用于發送和處理訊息的,發送訊息一般是使用Handler的sendMessage()方法,而發出的訊息經過一系列地輾轉處理后,最侄訓傳遞到Handler的handleMessage()方法中,

  1. MessageQueue

MessageQueue是訊息佇列的意思,它主要用于存放所有通過Handler發送的訊息,這部分訊息會一直存在于訊息佇列中,等待被處理,每個執行緒中只會有一個MessageQueue物件,

  1. Looper

Looper是每個執行緒中的MessageQueue的管家,呼叫Looper的loop()方法后,就會進入到一個無限回圈當中,然后每當發現MessageQueue中存在一條訊息,就會將它取出,并傳遞到Handler的handleMessage()方法中,每個執行緒中也只會有一個Looper物件,

了解了Message、Handler、MessageQueue以及Looper的基本概念后,我們再來把異步訊息處理的整個流程梳理一遍,

  • 首先,需要在主執行緒當中創建一個Handler物件,并重寫handleMessage()方法,
  • 然后,當子執行緒中需要進行UI操作時,就創建一個Message物件,并通過Handler將這條訊息發送出去,
  • 之后,這條訊息會被添加到MessageQueue的佇列中等待被處理,而Looper則會一直嘗試從MessageQueue中取出待處理訊息,
  • 最后分發回Handler的handleMessage()方法中,

由于Handler是在主執行緒中創建的,所以此時handleMessage()方法中的代碼也會在主執行緒中運行,于是我們在這里就可以安心地進行UI操作了,整個異步訊息處理機制的流程示意圖如圖:

image

一條Message經過這樣一個流程的輾轉呼叫后,也就從子執行緒進入到了主執行緒,從不能更新UI變成了可以更新UI,整個異步訊息處理的核心思想也就是如此,

而我們在9.2.1小節中使用到的runOnUiThread()方法:

... 
private void showResponse(final String response) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                //在這里進行Ui操作,將結果顯示到界面
                responseText.setText(response);
            }
        });
    }

其實就是一個異步訊息處理機制的介面封裝,它雖然表面上看起來用法更為簡單,但其實背后的實作原理和上圖的描述是一模一樣的,

10.2.4 使用AsyncTask

為了更加方便我們在子執行緒中對UI進行操作,Android還提供了另外一些好用的工具,比如AsyncTask

借助AsyncTask,即使你對異步訊息處理機制完全不了解,也可以十分簡單地從子執行緒切換到主執行緒,當然,AsyncTask背后的實作原理也是基于異步訊息處理機制的,只是Android幫我們做了很好的封裝而已,

首先來看一下AsyncTask的基本用法,由于AsyncTask是一個抽象類,所以如果我們想使用它,就必須要創建一個子類去繼承它,在繼承時我們可以為AsyncTask類指定3個泛型引數,這3個引數的用途如下:

? Params,在執行AsyncTask時需要傳入的引數,可用于在后臺任務中使用,

? Progress,后臺任務執行時,如果需要在界面上顯示當前的進度,則使用這里指定的泛型作為進度單位,

? Result,當任務執行完畢后,如果需要對結果進行回傳,則使用這里指定的泛型作為回傳值型別,

因此,一個最簡單的自定義AsyncTask就可以寫成如下方式:

class DownloadTask extends AsyncTask<Void,Integer,Boolean> {
    ...
}

這里我們把AsyncTask的第一個泛型引數指定為Void,表示在執行AsyncTask的時候不需要傳入引數給后臺任務,第二個泛型引數指定為Integer,表示使用整型資料來作為進度顯示單位,第三個泛型引數指定為Boolean,則表示使用布爾型資料來反饋執行結果,

當然,目前我們自定義的DownloadTask還是一個空任務,并不能進行任何實際的操作,我們還需要去重寫AsyncTask中的幾個方法才能完成對任務的定制,經常需要去重寫的方法有以下4個,

  1. onPreExecute()

這個方法會在后臺任務開始執行之前呼叫,用于進行一些界面上的初始化操作,比如顯示一個進度條對話框等,

  1. doInBackground(Params...)

這個方法中的所有代碼都會在子執行緒中運行,我們應該在這里去處理所有的耗時任務,任務一旦完成就可以通過return陳述句來將任務的執行結果回傳,如果AsyncTask的第三個泛型引數指定的是Void,就可以不回傳任務執行結果,

注意,在這個方法中是不可以進行UI操作的,如果需要更新UI元素,比如說反饋當前任務的執行進度,可以呼叫publishProgress (Progress...)方法來完成,

  1. onProgressUpdate(Progress...)

當在后臺任務中呼叫了publishProgress(Progress...)方法后,onProgressUpdate (Progress...)方法就會很快被呼叫,該方法中攜帶的引數就是在后臺任務中傳遞過來的,在這個方法中可以對UI進行操作,利用引數中的數值就可以對界面元素進行相應的更新,

  1. onPostExecute(Result)

當后臺任務執行完畢并通過return陳述句進行回傳時,這個方法就很快會被呼叫,回傳的資料會作為引數傳遞到此方法中,可以利用回傳的資料來進行一些UI操作,比如說提醒任務執行的結果,以及關閉掉進度條對話框等,

因此,一個比較完整的自定義AsyncTask就可以寫成如下方式:

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("Downloaded " + values[0] + "%");
    }
    @Override
    protected void onPostExecute(Boolean result) {
        progressDialog.dismiss();//關閉進度對話框
        //這里顯示下載結果
        if (result) {
            Toast.makeText(context,"Download succeded",Toast.LENGTH_SHORT).show();
        } else {
            Toast.makeText(context,"Download failed",Toast.LENGTH_SHORT).show();
        }
    }
}

在這個DownloadTask中,我們在doInBackground()方法里去執行具體的下載任務,這個方法里的代碼都是在子執行緒中運行的,因而不會影響到主執行緒的運行,

注意,這里虛構了一個doDownload()方法,這個方法用于計算當前的下載進度并回傳,我們假設這個方法已經存在了,在得到了當前的下載進度后,下面就該考慮如何把它顯示到界面上了,由于doInBackground()方法是在子執行緒中運行的,在這里肯定不能進行UI操作,所以我們可以呼叫publishProgress()方法并將當前的下載進度傳進來,這樣onProgressUpdate()方法就會很快被呼叫,在這里就可以進行UI操作了,

當下載完成后,doInBackground()方法會回傳一個布爾型變數,這樣onPostExecute()方法就會很快被呼叫,這個方法也是在主執行緒中運行的,然后在這里我們會根據下載的結果來彈出相應的Toast提示,從而完成整個DownloadTask任務,

簡單來說,使用AsyncTask的訣竅就是,在doInBackground()方法中執行具體的耗時任務,在onProgressUpdate()方法中進行UI操作,在onPostExecute()方法中執行一些任務的收尾作業,

如果想要啟動這個任務,只需撰寫以下代碼即可:

new DownloadTask().execute();

以上就是AsyncTask的基本用法,我們并不需要去考慮什么異步訊息處理機制,也不需要專門使用一個Handler來發送和接收訊息,只需要呼叫一下publishProgress()方法,就可以輕松地從子執行緒切換到UI執行緒了,在本章的最佳實踐環節,我們會對下載這個功能進行完整的實作,

10.3 服務的基本用法

作為Android四大組件之一,服務也少不了有很多非常重要的知識點,先從最基本的用法開始學習,

10.3.1 定義一個服務

首先看一下如何在專案中定義一個服務,新建一個ServiceTest專案,然后右擊com.zhouzhou. servicetest→New→Service→Service→MyService,會彈出如圖所示的視窗:

image

將服務命名為MyService, Exported屬性表示是否允許除了當前程式之外的其他程式訪問這個服務,Enabled屬性表示是否啟用這個服務,將兩個屬性都勾中,點擊Finish完成創建,現在觀察MyService中的代碼,如下所示:

package com.zhouzhou.servicetest;

import android.app.Service;
import android.content.Intent;
import android.os.IBinder;

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");
    }
}

MyService是繼承自Service類的,說明這是一個服務,onBind()方法是Service中唯一的一個抽象方法,所以必須要在子類里實作,

會在后面的小節中使用到onBind()方法,目前可以暫時將它忽略掉,既然是定義一個服務,自然應該在服務中去處理一些事情了,那處理事情的邏輯應該寫在哪里呢?這時就可以重寫Service中的另外一些方法了,如下所示:

package com.zhouzhou.servicetest;

import android.app.Service;
import android.content.Intent;
import android.os.IBinder;

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();
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        return super.onStartCommand(intent, flags, startId);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
    }
}

可以看到,這里我們又重寫了onCreate()、onStartCommand()和onDestroy()這3個方法,它們是每個服務中最常用到的3個方法了,

  • onCreate()方法會在服務創建的時候呼叫,
  • onStartCommand()方法會在每次服務啟動的時候呼叫,
  • onDestroy()方法會在服務銷毀的時候呼叫,

通常情況下,如果希望服務一旦啟動就立刻去執行某個動作,就可以將邏輯寫在onStartCommand()方法里,而當服務銷毀時,又應該在onDestroy()方法中去回收那些不再使用的資源,另外需要注意,每一個服務都需要在AndroidManifest.xml檔案中進行注冊才能生效,這是Android四大組件共有的特點,智能的Android Studio早已自動幫我們將這一步完成了,打開AndroidManifest.xml檔案瞧一瞧,代碼如下所示:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.zhouzhou.servicetest">

    <application
        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/Theme.ServiceTest">

        <service
            android:name=".MyService"
            android:enabled="true"
            android:exported="true" />

        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

這樣的話,就已經將一個服務完全定義好了,

10.3.2 啟動和停止服務

定義好了服務之后,接下來就應該考慮如何去啟動以及停止這個服務,主要是借助Intent來實作的,下面就讓我們在ServiceTest專案中嘗試去啟動以及停止MyService這個服務,首先修改activity_main.xml中的代碼,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_
    android:layout_height="match_parent">

    <Button
        android:layout_
        android:layout_height="wrap_content"
        android:id="@+id/start_service"
        android:text="Start Service"/>
    <Button
        android:layout_
        android:layout_height="wrap_content"
        android:id="@+id/stop_service"
        android:text="Stop Service"/>

</LinearLayout>

加入了兩個按鈕,分別是用于啟動服務和停止服務的,然后修改MainActivity中的代碼,如下所示:

package com.zhouzhou.servicetest;

import androidx.appcompat.app.AppCompatActivity;

import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    @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);
    }

    @Override
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.start_service:
                Intent startIntent = new Intent(this,MyService.class);
                startService(startIntent);//啟動服務
                break;
            case R.id.stop_service:
                Intent stopIntent = new Intent(this,MyService.class);
                stopService(stopIntent);
                break;
            default:
                break;
        }
    }
}

可以看到,這里在onCreate()方法中分別獲取到了Start Service按鈕和StopService按鈕的實體,并給它們注冊了點擊事件,然后在Start Service按鈕的點擊事件里,我們構建出了一個Intent物件,并呼叫startService()方法來啟動MyService這個服務,在Stop Serivce按鈕的點擊事件里,我們同樣構建出了一個Intent物件,并呼叫stopService()方法來停止MyService這個服務,

startService()和stopService()方法都是定義在Context類中的,所以在活動里可以直接呼叫這兩個方法,注意,這里完全是由活動來決定服務何時停止的,如果沒有點擊Stop Service按鈕,服務就會一直處于運行狀態,那服務有沒有什么辦法讓自已停止下來呢?當然可以,只需要在MyService的任何一個位置呼叫stopSelf()方法就能讓這個服務停止下來了,

那么接下來又有一個問題需要思考了,我們如何才能證實服務已經成功啟動或者停止了呢?最簡單的方法就是在MyService的幾個方法中加入列印日志,如下所示:

package com.zhouzhou.servicetest;

import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.util.Log;

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","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() {
        Log.d("MyService","on Destroy executed");
        super.onDestroy();
    }
}

現在可以運行一下程式來進行測驗了,點擊一下Start Service按鈕,觀察logcat中的列印日志,如圖:

image

MyService中的onCreate()和onStartCommand()方法都執行了,說明這個服務確實已經啟動成功了,并且還可以在(我用的是手機來測驗的)Settings→Developeroptions(開發人員選項)→Running services(正在運行的服務)中找到它,如圖(手機截屏):

image

然后,再點擊一下Stop Service按鈕,觀察logcat中的列印日志:

image

再次查看,Settings→Developeroptions(開發人員選項)→Running services(正在運行的服務),發現MyService確實已經成功停止下來了,(圖略)

onCreate()方法和onStartCommand()方法到底有什么區別呢?

因為剛剛點擊Start Service按鈕后兩個方法都執行了,其實onCreate()方法是在服務第一次創建的時候呼叫的,而onStartCommand()方法則在每次啟動服務的時候都會呼叫,由于剛才我們是第一次點擊Start Service按鈕,服務此時還未創建過,所以兩個方法都會執行,之后如果你再連續多點擊幾次Start Service按鈕,就會發現只有onStartCommand()方法可以得到執行了,

image

10.3.3 活動和服務進行通信

上一小節中,我們在活動里呼叫了startService()方法來啟動MyService這個服務,然后MyService的onCreate()和onStartCommand()方法就會得到執行,之后服務會一直處于運行狀態,但具體運行的是什么邏輯,活動就控制不了了,這就類似于活動通知了服務一下:“你可以啟動了!”然后服務就去忙自己的事情了,但活動并不知道服務到底去做了什么事情,以及完成得如何,

那么有沒有什么辦法能讓活動和服務的關系更緊密一些呢?例如在活動中指揮服務去干什么,服務就去干什么,當然可以,這就需要借助我們剛剛忽略的onBind()方法了,

比如說,目前我們希望在MyService里提供一個下載功能,然后在活動中可以決定何時開始下載,以及隨時查看下載進度,實作這個功能的思路是創建一個專門的Binder物件來對下載功能進行管理,修改MyService中的代碼,如下所示:

package com.zhouzhou.servicetest;

import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.util.Log;

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;
        }
    }

    public MyService() {
    }

    @Override
    public IBinder onBind(Intent intent) {
        // TODO: Return the communication channel to the service.
        //throw new UnsupportedOperationException("Not yet implemented");
        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() {
        Log.d("MyService","on Destroy executed");
        super.onDestroy();
    }

}

可以看到,這里新建了一個DownloadBinder類,并讓它繼承自Binder,然后在它的內部提供了開始下載以及查看下載進度的方法,當然這只是兩個模擬方法,并沒有實作真正的功能,我們在這兩個方法中分別列印了一行日志.

接著,在MyService中創建了DownloadBinder的實體,然后在onBind()方法里回傳了這個實體,這樣MyService中的作業就全部完成了,下面就要看一看,在活動中如何去呼叫服務里的這些方法了,首先需要在布局檔案里新增兩個按鈕,修改activity_main.xml中的代碼,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_
    android:layout_height="match_parent">
...
    <Button
        android:layout_
        android:layout_height="wrap_content"
        android:id="@+id/bind_service"
        android:text="Bind Service"/>
    <Button
        android:layout_
        android:layout_height="wrap_content"
        android:id="@+id/unbind_service"
        android:text="Unbind Service"/>
</LinearLayout>

這兩個按鈕分別是用于系結服務和取消系結服務的,那到底誰需要去和服務系結呢?當然就是活動了,當一個活動和服務系結了之后,就可以呼叫該服務里的Binder提供的方法了,修改MainActivity中的代碼,如下所示:

package com.zhouzhou.servicetest;

import androidx.appcompat.app.AppCompatActivity;

import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.view.View;
import android.widget.Button;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    private MyService.DownloadBinder downloadBinder;
    private ServiceConnection connection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
            downloadBinder = (MyService.DownloadBinder) iBinder;
            downloadBinder.startDownload();
            downloadBinder.getProgress();
        }

        @Override
        public void onServiceDisconnected(ComponentName componentName) {

        }
    };
    @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 view) {
        switch (view.getId()) {
            case R.id.start_service:
                Intent startIntent = new Intent(this,MyService.class);
                startService(startIntent);//啟動服務
                break;
            case R.id.stop_service:
                Intent stopIntent = new Intent(this,MyService.class);
                stopService(stopIntent);
                break;
            case R.id.bind_service:
                Intent bindIntent = new Intent(this,MyService.class);
                bindService(bindIntent,connection,BIND_AUTO_CREATE);//系結服務
                break;
            case R.id.unbind_service:
                unbindService(connection);//解綁服務
                break;
            default:
                break;
        }
    }
}

這里首先創建了一個ServiceConnection的匿名類,在里面重寫了onServiceConnected()方法和onServiceDisconnected()方法,這兩個方法分別會在活動與服務成功系結以及活動與服務的連接斷開的時候呼叫,在onServiceConnected()方法中,又通過向下轉型得到了DownloadBinder的實體,有了這個實體,活動和服務之間的關系就變得非常緊密了,

現在可以在活動中根據具體的場景來呼叫DownloadBinder中的任何public方法,即實作了指揮服務干什么服務就去干什么的功能,這里仍然只是做了個簡單的測驗,在onServiceConnected()方法中呼叫了DownloadBinder的startDownload()和getProgress()方法,

當然,現在活動和服務其實還沒進行系結呢,這個功能是在Bind Service按鈕的點擊事件里完成的,可以看到,這里我們仍然是構建出了一個Intent物件,然后呼叫bindService()方法將MainActivity和MyService進行系結,

bindService()方法接收3個引數:

  • 第一個引數就是剛剛構建出的Intent物件;
  • 第二個引數是前面創建出的ServiceConnection的實體;
  • 第三個引數則是一個標志位;

(這里傳入BIND_AUTO_CREATE表示在活動和服務進行系結后自動創建服務,這會使得MyService中的onCreate()方法得到執行,但onStartCommand()方法不會執行,)

然后,如果我們想解除活動和服務之間的系結該怎么辦呢?呼叫一下unbindService()方法就可以了,這也是Unbind Service按鈕的點擊事件里實作的功能,現在讓我們重新運行一下程式,點擊一下Bind Service按鈕,然后觀察logcat中的列印日志,如圖:

image

可以看到,首先是MyService的onCreate()方法得到了執行,然后startDownload()和getProgress()方法都得到了執行,說明我們確實已經在活動里成功呼叫了服務里提供的方法了,

另外需要注意,任何一個服務在整個應用程式范圍內都是通用的,即MyService不僅可以和MainActivity系結,還可以和任何一個其他的活動進行系結,而且在系結完成后它們都可以獲取到相同的DownloadBinder實體,

10.4 服務的生命周期

服務也有自己的生命周期,前面我們使用到的onCreate()、onStartCommand()、onBind()和onDestroy()等方法都是在服務的生命周期內可能回呼的方法,

一旦在專案的任何位置呼叫了Context的startService()方法,相應的服務就會啟動起來,并回呼onStartCommand()方法,如果這個服務之前還沒有創建過,onCreate()方法會先于onStartCommand()方法執行,服務啟動了之后會一直保持運行狀態,直到stopService()或stopSelf()方法被呼叫,

注意,雖然每呼叫一次startService()方法,onStartCommand()就會執行一次,但實際上每個服務都只會存在一個實體,所以不管你呼叫了多少次startService()方法,只需呼叫一次stopService()或stopSelf()方法,服務就會停止下來了,

另外,還可以呼叫Context的bindService()來獲取一個服務的持久連接,這時就會回呼服務中的onBind()方法,類似地,如果這個服務之前還沒有創建過,onCreate()方法會先于onBind()方法執行,之后,呼叫方可以獲取到onBind()方法里回傳的IBinder物件的實體,這樣就能自由地和服務進行通信了,只要呼叫方和服務之間的連接沒有斷開,服務就會一直保持運行狀態,

當呼叫了startService()方法后,又去呼叫stopService()方法,這時服務中的onDestroy()方法就會執行,表示服務已經銷毀了,類似地,當呼叫了bindService()方法后,又去呼叫unbindService()方法,onDestroy()方法也會執行,這兩種情況都很好理解,

需要注意,我們是完全有可能對一個服務既呼叫了startService()方法,又呼叫了bindService()方法的,這種情況下該如何才能讓服務銷毀掉呢?根據Android系統的機制,一個服務只要被啟動或者被系結了之后,就會一直處于運行狀態,必須要讓以上兩種條件同時不滿足,服務才能被銷毀,所以,這種情況下要同時呼叫stopService()和unbindService()方法,onDestroy()方法才會執行,

10.5 服務的更多技巧

關于服務的更多高級使用技巧,

10.5.1 使用前臺服務

服務幾乎都是在后臺運行的,一直以來它都是默默地做著辛苦的作業,但是,服務的系統優先級還是比較低的,當系統出現記憶體不足的情況時,就有可能會回收掉正在后臺運行的服務,如果你希望服務可以一直保持運行狀態,而不會由于系統記憶體不足的原因導致被回收,就可以考慮使用前臺服務,

前臺服務和普通服務最大的區別就在于,它會一直有一個正在運行的圖示在系統的狀態欄顯示,下拉狀態欄后可以看到更加詳細的資訊,非常類似于通知的效果,當然有時候你也可能不僅僅是為了防止服務被回收掉才使用前臺服務的,有些專案由于特殊的需求會要求必須使用前臺服務,比如說彩云天氣這款天氣預報應用,它的服務在后臺更新天氣資料的同時,還會在系統狀態欄一直顯示當前的天氣資訊,如圖:

image

那么我們就來看一下如何才能創建一個前臺服務吧,修改MyService中的代碼,如下所示:

package com.zhouzhou.servicetest;

import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.graphics.BitmapFactory;
import android.os.Binder;
import android.os.Build;
import android.os.IBinder;
import android.util.Log;

import androidx.core.app.NotificationCompat;

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;
        }
    }

    public MyService() {
    }

    @Override
    public IBinder onBind(Intent intent) {
        // TODO: Return the communication channel to the service.
        //throw new UnsupportedOperationException("Not yet implemented");
        return mBinder;
    }

    @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);
        NotificationManager manager = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            String channelId = "default";
            String channelName = "默認通知";
            //new NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_HIGH)
            manager.createNotificationChannel(new NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_HIGH));
        }
        Notification notification = new NotificationCompat.Builder(MyService.this,"default")
                .setContentTitle("This is content title")
                .setContentText("This is content text")
                .setWhen(System.currentTimeMillis())
                .setSmallIcon(R.drawable.small_icon)
                .setLargeIcon(BitmapFactory.decodeResource(getResources(),
                        R.drawable.large_icon)).build();
        manager.notify(1,notification);
    }

    @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() {
        Log.d("MyService","on Destroy executed");
        super.onDestroy();
    }

}

可以看到,這里只是修改了onCreate()方法中的代碼,這是在第8章中學習的創建通知的方法,

現在重新運行一下程式,并點擊Start Service或Bind Service按鈕,MyService就會以前臺服務的模式啟動了,并且在系統狀態欄會顯示一個通知圖示,下拉狀態欄后可以看到該通知的詳細內容,如圖:

image

10.5.2 使用IntentService

在本章一開始的時候我們就已經知道,服務中的代碼都是默認運行在主執行緒當中的,如果直接在服務里去處理一些耗時的邏輯,就很容易出現ANR(Application Not Responding)的情況,

所以,這個時候就需要用到Android多執行緒編程的技術,我們應該在服務的每個具體的方法里開啟一個子執行緒,然后在這里去處理那些耗時的邏輯,因此,一個比較標準的服務就可以寫成如下形式:

public class MyService extends Service {
    ...
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        
        new Thread(new Runnable() {
            @Override
            public void run() {
                //處理具體邏輯
            }
        }).start();
        return super.onStartCommand(intent, flags, startId);
    }
...

}

但是,這種服務一旦啟動之后,就會一直處于運行狀態,必須呼叫stopService()或者stopSelf()方法才能讓服務停止下來,所以,如果想要實作讓一個服務在執行完畢后自動停止的功能,就可以這樣寫:

public class MyService extends Service {
    ...
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        
        new Thread(new Runnable() {
            @Override
            public void run() {
                //處理具體邏輯
                stopSelf();
            }
        }).start();
        return super.onStartCommand(intent, flags, startId);
    }
...

}

雖說這種寫法并不復雜,但是總會有一些程式員忘記開啟執行緒,或者忘記呼叫stopSelf()方法,為了可以簡單地創建一個異步的、會自動停止的服務,Android專門提供了一個IntentService類,這個類就很好地解決了前面所提到的兩種尷尬,

下面我們就來看一下它的用法,新建一個MyIntentService類繼承自IntentService,代碼如下所示:

package com.zhouzhou.servicetest;

import android.app.IntentService;
import android.content.Intent;
import android.util.Log;

import androidx.annotation.Nullable;

public class MyIntentService extends IntentService {
    //下面是無參構造哦~,是super()中再呼叫父類的有參建構式,不然在AndroidManifest.xml里面注冊的時候會爆紅,并且在運行后點擊測驗按鈕,程式會閃退,報錯:“has no zero argument constructor”
    public MyIntentService() {
        super("MyIntentService");//呼叫父類的有參建構式
    }

    @Override
    protected void onHandleIntent(@Nullable Intent intent) {
        //列印當前執行緒的id
        Log.d("MyIntentService","This id is " + Thread.currentThread().getId());
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.d("MyIntentService","onDestroy executed");
    }
}

這里首先要提供一個無參的建構式,并且必須在其內部呼叫父類的有參建構式,然后要在子類中去實作onHandleIntent()這個抽象方法,在這個方法中可以去處理一些具體的邏輯,而且不用擔心ANR的問題,因為這個方法已經是在子執行緒中運行的了,這里為了證實一下,我們在onHandleIntent()方法中列印了當前執行緒的id,另外根據IntentService的特性,這個服務在運行結束后應該是會自動停止的,所以我們又重寫了onDestroy()方法,在這里也列印了一行日志,以證實服務是不是停止掉了,

接下來修改activity_main.xml中的代碼,加入一個用于啟動MyIntentService這個服務的按鈕,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_
    android:layout_height="match_parent">
    ...
    <Button
        android:layout_
        android:layout_height="wrap_content"
        android:id="@+id/start_intent_service"
        android:text="Start IntentService"/>
</LinearLayout>

然后修改MainActivity中的代碼,如下所示:

package com.zhouzhou.servicetest;

import androidx.appcompat.app.AppCompatActivity;

import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.util.Log;
import android.view.View;
import android.widget.Button;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    private MyService.DownloadBinder downloadBinder;
    private ServiceConnection connection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
            downloadBinder = (MyService.DownloadBinder) iBinder;
            downloadBinder.startDownload();
            downloadBinder.getProgress();
        }

        @Override
        public void onServiceDisconnected(ComponentName componentName) {

        }
    };
    @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);

        Button startIntentService = (Button) findViewById(R.id.start_intent_service);
        startIntentService.setOnClickListener(this);
    }

    @Override
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.start_service:
                Intent startIntent = new Intent(this,MyService.class);
                startService(startIntent);//啟動服務
                break;
            case R.id.stop_service:
                Intent stopIntent = new Intent(this,MyService.class);
                stopService(stopIntent);
                break;
            case R.id.bind_service:
                Intent bindIntent = new Intent(this,MyService.class);
                bindService(bindIntent,connection,BIND_AUTO_CREATE);//系結服務
                break;
            case R.id.unbind_service:
                unbindService(connection);//解綁服務
                break;
            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;
        }
    }
}

可以看到,我們在Start IntentService按鈕的點擊事件里面去啟動MyIntentService這個服務,并在這里列印了一下主執行緒的id,稍后用于和IntentService進行比對,你會發現,其實IntentService的用法和普通的服務沒什么兩樣,最后不要忘記,服務都是需要在AndroidManifest.xml里注冊的,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.zhouzhou.servicetest">

    <application
        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/Theme.ServiceTest">
        
        <service android:name=".MyIntentService"/>
        ...
</manifest>

當然,也可以使用Android Studio提供的快捷方式來創建IntentService,不過這樣會自動生成一些我們用不到的代碼,現在重新運行一下程式,點擊Start IntentService按鈕后,觀察logcat中的列印日志,如圖:

image

可以看到,不僅MyIntentService和MainActivity所在的執行緒id不一樣,而且onDestroy()方法也得到了執行,說明MyIntentService在運行完畢后確實自動停止了,集開啟執行緒和自動停止于一身,IntentService還是博得了不少程式員的喜愛,

10.6 服務的最佳實踐——完整版的下載示例

下面我們就來綜合運用一下,嘗試實作一個在服務中經常會使用到的功能——下載,

本節中我們將要撰寫一個完整版的下載示例,其中會涉及第7章、第8章、第9章和第10章的部分內容,

創建一個ServiceBestPractice專案,然后開始本節內容,首先我們需要將專案中會使用到的依賴庫添加好,編輯app/build.gradle檔案,在dependencies閉包中添加如下內容:

dependencies {
	implementation("com.squareup.okhttp3:okhttp:4.9.3")
}

只需添加一個OkHttp的依賴就行了,待會兒在撰寫網路相關的功能時,我們將使用OkHttp來進行實作,

接下來需要定義一個回呼介面,用于對下載程序中的各種狀態進行監聽和回呼,新建一個DownloadListener介面,代碼如下所示:

package com.zhouzhou.servicebestpractice;

public interface DownloadListener {
    //用于通知當前的下載進度
    void onProgress(int progress);
    //用于通知下載成功事件
    void onSuccess();
    //用于通知下載失敗事件
    void onFailed();
    //用于通知下載暫停事件
    void onPaused();
    //用于通知下載取消事件
    void onCanceled();
}

一共定義了5個回呼方法,回呼介面定義好了之后,下面可以開始撰寫下載功能了,這里使用本章中剛學的AsyncTask來進行實作,新建一個DownloadTask繼承自AsyncTask,代碼如下所示:

package com.zhouzhou.servicebestpractice;

import android.os.AsyncTask;
import android.os.Environment;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;

import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

/**
 * 在AsyncTask中的3個泛型引數:
 * 第一個泛型引數指定為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 DownloadListener listener;
    private boolean isCanceled = false;
    private boolean isPaused = false;
    private int lastProgress;

    // 在DownloadTask的建構式中要求傳入一個剛剛定義的DownloadListener引數,待會就會將下載的狀態通過這個引數進行回呼,
    public DownloadTask(DownloadListener listener) {
        this.listener = listener;
    }

    /**
     * doInBackground()方法:
     * 首先從引數中獲取到了下載的URL地址,并根據URL地址決議出了下載的檔案名;
     * 然后指定將檔案下載到Environment.DIRECTORY_DOWNLOADS目錄下,也就是SD卡的Download目錄;
     */
    @Override
    protected Integer doInBackground(String... params) {
        InputStream is = null;
        RandomAccessFile savedFile = null;
        File file = null;
        try {
            long downloadedLength = 0;// 記錄已下載的檔案長度
            String downloadUrl = params[0];
            String fileName = downloadUrl.substring(downloadUrl.lastIndexOf("/"));
            String directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath();
            file = new File(directory + fileName);
            if (file.exists()) {
                //還要判斷一下Download目錄中是不是已經存在要下載的檔案了,
                // 如果已經存在的話則讀取已下載的位元組數,這樣就可以在后面啟用斷點續傳的功能,
                downloadedLength = file.length();
            }
            //先是呼叫了getContentLength()方法來獲取待下載檔案的總長度,
            // 如果檔案長度等于0則說明檔案有問題,直接回傳TYPE_FAILED,
            // 如果檔案長度等于已下載檔案長度,那么就說明檔案已經下載完了,直接回傳TYPE_SUCCESS即可
            long contentLength = getContentLength(downloadUrl);
            if (contentLength == 0) {
                return TYPE_FAILED;
            } else if (contentLength == downloadedLength) {
                //已下載位元組和檔案總位元組數相等,說明已經下載完成了
                return TYPE_SUCCESS;
            }
            //使用OkHttp來發送一條網路請求,需要注意的是,這里在請求中添加了一個header,
            // 用于告訴服務器我們想要從哪個位元組開始下載,因為已下載過的部分就不需要再重新下載了,
            OkHttpClient client = new OkHttpClient();
            Request request = new Request.Builder()
                    // 斷點下載,指定從哪個位元組開始下載
                    .addHeader("RANGE","bytes=" + downloadedLength + "-")
                    .url(downloadUrl)
                    .build();
            Response response = client.newCall(request).execute();
            //使用Java的檔案流方式,不斷從網路上讀取資料,不斷寫入到本地,一直到檔案全部下載完成為止,
            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;
                //在這個程序中,我們還要判斷用戶有沒有觸發暫停或者取消的操作,
                // 如果有的話則回傳TYPE_PAUSED或TYPE_CANCELED來中斷下載,
                // 如果沒有的話則實時計算當前的下載進度,然后呼叫publishProgress()方法進行通知,
                // 暫停和取消操作都是使用一個布爾型的變數來進行控制的,呼叫pauseDownload()或cancelDownload()方法即可更改變數的值,
                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;
    }

    /**
     * onProgressUpdate()方法:
     * 它首先從引數中獲取到當前的下載進度,然后和上一次的下載進度進行對比,
     * 如果有變化的話則呼叫DownloadListener的onProgress()方法來通知下載進度更新,
     */
    @Override
    protected void onProgressUpdate(Integer... values) {
        super.onProgressUpdate(values);
        int progress = values[0];
        if (progress > lastProgress) {
            listener.onProgress(progress);
            lastProgress = progress;
        }
    }

    /**
     * onPostExecute()方法:是根據引數中傳入的下載狀態來進行回呼,
     * 下載成功就呼叫DownloadListener的onSuccess()方法,
     * 下載失敗就呼叫onFailed()方法,暫停下載就呼叫onPaused()方法,
     * 取消下載就呼叫onCanceled()方法,
     */
    @Override
    protected void onPostExecute(Integer status) {
        super.onPostExecute(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();
                break;
            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.body().close();
            return contentLength;
        }
        return 0;
    }
}

這樣就把具體的下載功能完成了,下面為了保證DownloadTask可以一直在后臺運行,還需要創建一個下載的服務,右擊com.zhouzhou.servicebestpractice→New→Service→Service,新建DownloadService,然后修改其中的代碼,如下所示:

package com.zhouzhou.servicebestpractice;

import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.graphics.BitmapFactory;
import android.os.Binder;
import android.os.Environment;
import android.os.IBinder;
import android.widget.Toast;

import androidx.core.app.NotificationCompat;

import java.io.File;

public class DownloadService extends Service {
    private DownloadTask downloadTask;
    private String downloadUrl;
    // 首先這里創建了一個DownloadListener的匿名類實體,
    // 并在匿名類中實作了onProgress()、onSuccess()、onFailed()、onPaused()和onCanceled()這5個方法,
    private DownloadListener listener = new DownloadListener() {
        @Override
        public void onProgress(int progress) {
            //在onProgress()方法中,呼叫getNotification()方法構建了一個用于顯示下載進度的通知,
            //然后呼叫NotificationManager的notify()方法去觸發這個通知,這樣就可以在下拉狀態欄中實時看到當前下載的進度了,
            getNotificationManager().notify(1,getNotification("Downloading...",progress));
        }
        @Override
        public void onSuccess() {
            //在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(true);
            Toast.makeText(DownloadService.this,"Canceled",Toast.LENGTH_SHORT).show();
        }
    };
    //為了要讓DownloadService可以和活動進行通信,又創建了一個DownloadBinder,
    private DownloadBinder mBinder = new DownloadBinder();
    @Override
    public IBinder onBind(Intent intent) {
        // TODO: Return the communication channel to the service.
        //throw new UnsupportedOperationException("Not yet implemented");
        return mBinder;
    }

    // DownloadBinder中提供了startDownload()、pauseDownload()和cancelDownload()這3個方法,
    class DownloadBinder extends Binder {
        public void startDownload(String url) {
            if (downloadTask == null) {
                downloadUrl = url;
                //創建了一個DownloadTask的實體,把剛才的DownloadListener作為引數傳入,
                //然后呼叫execute()方法開啟下載,并將下載檔案的URL地址傳入到execute()方法中,
                downloadTask = new DownloadTask(listener);
                downloadTask.execute(downloadUrl);
                //為了讓這個下載服務成為一個前臺服務,還呼叫了startForeground()方法,這樣就會在系統狀態欄中創建一個持續運行的通知了,
                startForeground(1,getNotification("Downloading...",0));
                Toast.makeText(DownloadService.this,"Downloading...",Toast.LENGTH_SHORT).show();
            }
        }
        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();
                }
                getNotificationManager().cancel(1);
                stopForeground(true);
                Toast.makeText(DownloadService.this,"Canceled",Toast.LENGTH_SHORT).show();
            }
        }
    }

    private NotificationManager getNotificationManager() {
        return (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
    }
    private Notification getNotification(String title,int progress) {
        Intent intent = new Intent(this,MainActivity.class);
        PendingIntent pendingIntent = 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(pendingIntent);
        builder.setContentTitle(title);
        if (progress >=0 ) {
            //當progress 大于或等于0時才需要顯示下載進度
            builder.setContentText(progress + "%");
            //setProgress()方法接收3個引數,第一個引數傳入通知的最大進度,第二個引數傳入通知的當前進度,第三個引數表示是否使用模糊進度條,這里傳入false,
            //設定完setProgress()方法,通知上就會有進度條顯示出來了,
            builder.setProgress(100,progress,false);
        }
        return builder.build();
    }
}

現在下載的服務也已經成功實作,后端的作業基本都完成了,那么接下來我們開始撰寫前端的部分,修改activity_main.xml中的代碼,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_
    android:layout_height="match_parent">

    <Button
        android:layout_
        android:layout_height="wrap_content"
        android:id="@+id/start_download"
        android:text="Start Download"/>
    <Button
        android:layout_
        android:layout_height="wrap_content"
        android:id="@+id/pause_download"
        android:text="Pause Download"/>
    <Button
        android:layout_
        android:layout_height="wrap_content"
        android:id="@+id/cancel_download"
        android:text="Cancel Download"/>
</LinearLayout>

在LinearLayout中放置了3個按鈕,分別用于開始下載、暫停下載和取消下載,然后修改MainActivity中的代碼,如下所示:

package com.zhouzhou.servicebestpractice;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;

import android.Manifest;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.IBinder;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    private DownloadService.DownloadBinder downloadBinder;
    //首先創建了一個ServiceConnection的匿名類,然后在onServiceConnected()方法中獲取到DownloadBinder的實體,
    //有了這個實體,就可以在活動中呼叫服務提供的各種方法了,
    private ServiceConnection connection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
            downloadBinder = (DownloadService.DownloadBinder) iBinder;
        }
        @Override
        public void onServiceDisconnected(ComponentName componentName) {
        }
    };
    @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.cancel_download);
        startDownload.setOnClickListener(this);
        pauseDownload.setOnClickListener(this);
        cancelDownload.setOnClickListener(this);
        Intent intent = new Intent(this,DownloadService.class);
        //分別呼叫了startService()和bindService()方法來啟動和系結服務,
        //這一點至關重要,因為啟動服務可以保證DownloadService一直在后臺運行,
        //系結服務則可以讓MainActivity和DownloadService進行通信,因此兩個方法呼叫都必不可少,
        startService(intent);//啟動服務
        bindService(intent,connection,BIND_AUTO_CREATE);//系結服務
        //進行了WRITE_EXTERNAL_STORAGE的運行時權限申請,
        //因為下載檔案是要下載到SD卡的Download目錄下的,如果沒有這個權限的話,整個程式都無法正常作業,
        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
    public void onClick(View view) {
        if (downloadBinder == null) {
            return;
        }
        switch (view.getId()) {
            case R.id.start_download:
                //別整太小的檔案,不然進度條0%,刷的一下就下載完了,看不到進入條慢慢增長的狀態,哈哈
                String url = "https://raw.githubusercontent.com/guolindev/eclipse/master/eclipse-inst-win64.exe";
                downloadBinder.startDownload(url);
                break;
            case R.id.pause_download:
                downloadBinder.pauseDownload();
                break;
            case R.id.cancel_download:
                downloadBinder.cancelDownload();
                break;
            default:
                break;
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        switch (requestCode) {
            case 1:
                if (grantResults.length > 0 && grantResults[0] != PackageManager.PERMISSION_GRANTED) {
                    Toast.makeText(this,"決絕權限將無法使用程式",Toast.LENGTH_SHORT).show();
                    finish();
                }
                break;
            default:
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        //需要注意,如果活動被銷毀了,那么一定要記得對服務進行解綁,不然就有可能會造成記憶體泄漏,
        unbindService(connection);
    }
}

只差最后一步了,還需要在AndroidManifest.xml檔案中宣告使用到的權限,當然除了權限之外,MainActivity和DownloadService也是需要宣告的,不過Android Studio應該早就幫我們將這兩個組件宣告好了,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.zhouzhou.servicebestpractice">
    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <application
        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/Theme.ServiceBestPractice">
        <service
            android:name=".DownloadService"
            android:enabled="true"
            android:exported="true"></service>

        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

其中,由于我們的程式使用到了網路和訪問SD卡的功能,因此需要宣告INTERNET和WRITE_EXTERNAL_STORAGE這兩個權限,這樣所有代碼就都撰寫完了,現在終于可以運行一下程式了,如圖:

image

程式一啟動立刻就會申請訪問SD卡的權限,這里點擊ALLOW,然后點擊Start Download按鈕,出現閃退,并且報錯:java.lang.SecurityException: Permission Denial: startForeground from pid=4270, uid=10125 requires android.permission.FOREGROUND_SERVICE

image

問題原因是,這個是android9.0運行前臺的權限,解決辦法,在AndroidManifest.xml中添加如下代碼:

<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>

繼續運行,程式依然閃退,報錯資訊:android.app.RemoteServiceException: Bad notification for startForeground

image

問題原因,是安卓版本問題,使用Notificatio通知再Android8.0以上的的通知要設定渠道,否則就無法顯示,

解決辦法:修改DownloadService類中的getNotification()方法,代碼如下:

package com.zhouzhou.servicebestpractice;

import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.graphics.BitmapFactory;
import android.os.Binder;
import android.os.Build;
import android.os.Environment;
import android.os.IBinder;
import android.widget.Toast;

import androidx.core.app.NotificationCompat;

import java.io.File;

public class DownloadService extends Service {
    private DownloadTask downloadTask;
    private String downloadUrl;
    // 首先這里創建了一個DownloadListener的匿名類實體,
    // 并在匿名類中實作了onProgress()、onSuccess()、onFailed()、onPaused()和onCanceled()這5個方法,
    private DownloadListener listener = new DownloadListener() {
        @Override
        public void onProgress(int progress) {
            //在onProgress()方法中,呼叫getNotification()方法構建了一個用于顯示下載進度的通知,
            //然后呼叫NotificationManager的notify()方法去觸發這個通知,這樣就可以在下拉狀態欄中實時看到當前下載的進度了,
            getNotificationManager().notify(1,getNotification("Downloading...",progress));
        }
        @Override
        public void onSuccess() {
            //在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(true);
            Toast.makeText(DownloadService.this,"Canceled",Toast.LENGTH_SHORT).show();
        }
    };
    //為了要讓DownloadService可以和活動進行通信,又創建了一個DownloadBinder,
    private DownloadBinder mBinder = new DownloadBinder();
    @Override
    public IBinder onBind(Intent intent) {
        // TODO: Return the communication channel to the service.
        //throw new UnsupportedOperationException("Not yet implemented");
        return mBinder;
    }

    // DownloadBinder中提供了startDownload()、pauseDownload()和cancelDownload()這3個方法,
    class DownloadBinder extends Binder {
        public void startDownload(String url) {
            if (downloadTask == null) {
                downloadUrl = url;
                //創建了一個DownloadTask的實體,把剛才的DownloadListener作為引數傳入,
                //然后呼叫execute()方法開啟下載,并將下載檔案的URL地址傳入到execute()方法中,
                downloadTask = new DownloadTask(listener);
                downloadTask.execute(downloadUrl);
                //為了讓這個下載服務成為一個前臺服務,還呼叫了startForeground()方法,這樣就會在系統狀態欄中創建一個持續運行的通知了,
                startForeground(1,getNotification("Downloading...",0));
                Toast.makeText(DownloadService.this,"Downloading...",Toast.LENGTH_SHORT).show();
            }
        }
        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();
                }
                getNotificationManager().cancel(1);
                stopForeground(true);
                Toast.makeText(DownloadService.this,"Canceled",Toast.LENGTH_SHORT).show();
            }
        }
    }

    private NotificationManager getNotificationManager() {
        return (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
    }
    private Notification getNotification(String title,int progress) {
        Intent intent = new Intent(this,MainActivity.class);
        PendingIntent pendingIntent = PendingIntent.getActivity(this,0,intent,0);
        NotificationManager manager = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
        NotificationCompat.Builder notification;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            String channelId = "default";
            String channelName = "默認通知";
            //new NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_HIGH)
            manager.createNotificationChannel(new NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_HIGH));
        }
        notification = new NotificationCompat.Builder(DownloadService.this,"default")
                .setSmallIcon(R.mipmap.ic_launcher)
                .setLargeIcon(BitmapFactory.decodeResource(getResources(),R.mipmap.ic_launcher))
                .setContentIntent(pendingIntent)
                .setContentTitle(title);
        if (progress >=0 ) {
            //當progress 大于或等于0時才需要顯示下載進度
            notification.setContentText(progress + "%");
            //setProgress()方法接收3個引數,第一個引數傳入通知的最大進度,第二個引數傳入通知的當前進度,第三個引數表示是否使用模糊進度條,這里傳入false,
            //設定完setProgress()方法,通知上就會有進度條顯示出來了,
            notification.setProgress(100,progress,false);
        }
        return notification.build();
    }
}

image

繼續運行程式,點擊Start Download按鈕,就可以開始下載了,下載程序中可以下拉系統狀態欄查看實時的下載進度,如圖:

image

同時,還可以點擊Pause Download或Cancel Download,甚至于斷網操作來測驗這個下載程式的健壯性,最終下載完成后會彈出一個DownloadSuccess的通知:

image

然后,可以通過任意一個檔案瀏覽器來查看一下SD卡的Download目錄,如圖:

image

10.7 小結與點評

學習了很多與服務相關的重要知識點,包括Android多執行緒編程、服務的基本用法、服務的生命周期、前臺服務和IntentService等,

個人學習筆記,針對本人在自學中遇到的問題,

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/472410.html

標籤:Android

上一篇:《第一行代碼:Android篇》學習筆記(九)

下一篇:《第一行代碼:Android篇》學習筆記(十一)

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【從零開始擼一個App】Dagger2

    Dagger2是一個IOC框架,一般用于Android平臺,第一次接觸的朋友,一定會被搞得暈頭轉向。它延續了Java平臺Spring框架代碼碎片化,注解滿天飛的傳統。嘗試將各處代碼片段串聯起來,理清思緒,真不是件容易的事。更不用說還有各版本細微的差別。 與Spring不同的是,Spring是通過反射 ......

    uj5u.com 2020-09-10 06:57:59 more
  • Flutter Weekly Issue 66

    新聞 Flutter 季度調研結果分享 教程 Flutter+FaaS一體化任務編排的思考與設計 詳解Dart中如何通過注解生成代碼 GitHub 用對了嗎?Flutter 團隊分享如何管理大型開源專案 插件 flutter-bubble-tab-indicator A Flutter librar ......

    uj5u.com 2020-09-10 06:58:52 more
  • Proguard 常用規則

    介紹 Proguard 入口,如何查看輸出,如何使用 keep 設定入口以及使用實體,如何配置壓縮,混淆,校驗等規則。

    ......

    uj5u.com 2020-09-10 06:59:00 more
  • Android 開發技術周報 Issue#292

    新聞 Android即將獲得類AirDrop功能:可向附近設備快速分享檔案 谷歌為安卓檔案管理應用引入可安全隱藏資料的Safe Folder功能 Android TV新主界面將顯示電影、電視節目和應用推薦內容 泄露的Android檔案暗示了傳說中的谷歌Pixel 5a與折疊屏新機 谷歌發布Andro ......

    uj5u.com 2020-09-10 07:00:37 more
  • AutoFitTextureView Error inflating class

    報錯: Binary XML file line #0: Binary XML file line #0: Error inflating class xxx.AutoFitTextureView 解決: <com.example.testy2.AutoFitTextureView android: ......

    uj5u.com 2020-09-10 07:00:41 more
  • 根據Uri,Cursor沒有獲取到對應的屬性

    Android: 背景:呼叫攝像頭,拍攝視頻,指定保存的地址,但是回傳的Cursor檔案,只有名稱和大小的屬性,沒有其他諸如時長,連ID屬性都沒有 使用 cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATIO ......

    uj5u.com 2020-09-10 07:00:44 more
  • Android連載29-持久化技術

    一、持久化技術 我們平時所使用的APP產生的資料,在記憶體中都是瞬時的,會隨著斷電、關機等丟失資料,因此android系統采用了持久化技術,用于存盤這些“瞬時”資料 持久化技術包括:檔案存盤、SharedPreference存盤以及資料庫存盤,還有更復雜的SD卡記憶體儲。 二、檔案存盤 最基本存盤方式, ......

    uj5u.com 2020-09-10 07:00:47 more
  • Android Camera2Video整合到自己專案里

    背景: Android專案里呼叫攝像頭拍攝視頻,原本使用的 MediaStore.ACTION_VIDEO_CAPTURE, 后來因專案需要,改成了camera2 1.Camera2Video 官方demo有點問題,下載后,不能直接整合到專案 問題1.多次拍攝視頻崩潰 問題2.雙擊record按鈕, ......

    uj5u.com 2020-09-10 07:00:50 more
  • Android 開發技術周報 Issue#293

    新聞 谷歌為Android TV開發者提供多種新功能 Android 11將自動填表功能整合到鍵盤輸入建議中 谷歌宣布Android Auto即將支持更多的導航和數字停車應用 谷歌Pixel 5只有XL版本 搭載驍龍765G且將比Pixel 4更便宜 [圖]Wear OS將迎來重磅更新:應用啟動時間 ......

    uj5u.com 2020-09-10 07:01:38 more
  • 海豚星空掃碼投屏 Android 接收端 SDK 集成 六步驟

    掃碼投屏,開放網路,獨占設備,不需要額外下載軟體,微信掃碼,發現設備。支持標準DLNA協議,支持倍速播放。視頻,音頻,圖片投屏。好點意思。還支持自定義基于 DLNA 擴展的操作動作。好像要收費,沒體驗。 這里簡單記錄一下集成程序。 一 跟目錄的build.gradle添加私有mevan倉庫 mave ......

    uj5u.com 2020-09-10 07:01:43 more
最新发布
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:40:31 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:40:11 more
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:39:36 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:39:13 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:16:23 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:16:15 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:15:46 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:14:53 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:14:08 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:08:34 more