Android開發筆記——快速入門(入門Service)

語言: CN / TW / HK

本文已參與「新人創作禮」活動.一起開啟掘金創作之路。

軟體環境:

  • Jetbrains Toolbox
  • Android Sudio 2021.1.1 Bumblebee
  • JDK 17.0.2

請先參考前一篇文章複習一下Kotlin的一些語法。

大部分內容參考了郭霖先生的《第一行程式碼》,在書的基礎上針對目前的實際情況進行實踐記錄。

配套程式碼獲取地址:

Gitee直接下載,全部開源

Android的Service

Service是為了解決那些不需要和使用者通過介面互動但還是需要長期執行的程式需求,Service的執行不依賴於任何介面,即使程式切換到後臺,或者使用者打開了另一個應用程式,Service依然能在後臺穩定執行。

不過Service的實現並不是單獨建立了一個程序,而是依賴於建立Service的程序,當某個程式被kiil掉之後,依賴於該程序的Service也會被Kill掉。

更需要注意的是,Service不會自動開啟執行緒去處理內部的操作,實際上是需要你手動開啟執行緒來處理操作,否則就有可能導致主介面內容不重新整理(主執行緒操作被Service佔滿了)。

下面來介紹一下,Android如何開啟執行緒。

Android的多執行緒操作

Android的多執行緒操作基本就是移植Java的操作,在kotlin中使用Java的API我們常常是用lambda表示式來建立一個執行緒,類似於如下所示:

Thread(){    println("YES") }.start()

實際上Kotlin提供了更方便快捷的函式來實現操作:

thread {     println("YES") }

他實現的方法就是對Java介面的再封裝:

public fun thread(    start: Boolean = true,    isDaemon: Boolean = false,    contextClassLoader: ClassLoader? = null,    name: String? = null,    priority: Int = -1,    block: () -> Unit ): Thread {    val thread = object : Thread() {        public override fun run() {            block()       }   }    if (isDaemon)        thread.isDaemon = true    if (priority > 0)        thread.priority = priority    if (name != null)        thread.name = name    if (contextClassLoader != null)        thread.contextClassLoader = contextClassLoader    if (start)        thread.start()    return thread }

Android非同步訊息處理機制

我們知道多執行緒的情況下,有可能存線上程資料不安全的問題,Android提供了一套非同步處理機制,來保證資料的安全,當你需要在一個執行緒中去操作另一個執行緒中的內容的時候,就可以使用這一套機制來實現資料修改。

在Android中所有的UI介面顯示的資料都是執行緒不安全的,也就是說你無法在其他執行緒直接訪問UI的資料並修改其中的內容,因為這樣會導致丟擲執行緒不安全的異常。有些時候,我們必須在子執行緒裡面執行一些耗時的任務,然後根據任務結果來更新UI的內容。

在討論這個部分之前,請先參考部分Kotlin語法的內容。在完全瞭解非同步執行緒機制的之前,我們不妨來先寫一個試試。

下面我們來舉個例子:

我們為了實現音樂播放器在暫停的時候顯示音樂已經暫停的文字,我們在介面之中放置一個TextView來顯示一些資訊,再通過一個執行緒來修改它,具體程式碼如下:

class MainActivity : AppCompatActivity() {    lateinit var binding : ActivityMainBinding    val SongisPause = 1    val handler = object : Handler(Looper.getMainLooper()) {        override fun handleMessage(msg: Message) {            super.handleMessage(msg)            when (msg.what){                SongisPause -> binding.textView.text ="Song is Pause"           }       }   }             override fun onCreate(savedInstanceState: Bundle?) {        super.onCreate(savedInstanceState)        binding= ActivityMainBinding.inflate(layoutInflater)        setContentView(binding.root)                binding.stop.setOnClickListener {            thread {                val msg = Message()                msg.what = SongisPause                handler.sendMessage(msg)           }       }     }     }

這段程式碼看起來很複雜難以理解,我們不妨在再拆開看一看:

在類的開始我們初始化了一些變數和物件:

lateinit var binding : ActivityMainBinding val SongisPause = 1 val handler = object : Handler(Looper.getMainLooper()) {    override fun handleMessage(msg: Message) {        super.handleMessage(msg)        when (msg.what){            SongisPause -> binding.textView.text ="Song is Pause"       }   } }

前兩個就不再多說分別是一個繫結物件和一個一般的變數,需要仔細瞭解的是下面的這一個物件,這個handler使用了物件表示式來構造自己,他繼承了Handler類,看到名字你也應該能猜到他是具體幹什麼的,他就是處理,具體去操作的函式。不過為什麼這裡要宣告一個具體操作的函式,其背後原理的概念我們晚點再說,這裡先理解最後執行緒的操作歸咎於這裡。

binding.stop.setOnClickListener {    thread {        val msg = Message()        msg.what = SongisPause        handler.sendMessage(msg)   } }

我們又在onCreate函式裡面為暫停按鈕綁定了一個回撥函式,在回撥函式裡面我們使用thread函式建立了一個執行緒去處理暫停的操作,實際上我們是建立了一個message來向剛才建立的handler傳送了一條訊息,提示他該暫停了,當handler接收到了訊息以後就會執行剛剛註冊到handler的操作。

最終先效果如下:

And

看完以後你可能一臉懵,什麼鬼?我們再來討論一下,其背後實現的原理。

實際上我們可以注意到,接受收訊息處理的操作還是在主執行緒中完成的,並不是在子執行緒中完成的,子執行緒實際上只是傳送了meaasge,可是,message、handler和隱藏在背後的looper、MessageQueue都是什麼呢?

Message:

是線上程中間傳遞的訊息,它可以攜帶少量的內部資訊,用於在不同執行緒之間傳遞資料,上一節我們使用了Message的what欄位,除此之外他還包含arg1、arg2欄位,用來攜帶一些整型資料,還有obj欄位用來攜帶一個Object物件。

Handler:

它主要用於傳送和處理資訊,傳送訊息一般使用sendMessage和Post方法,而發出的訊息經過內部處理以後,最終會傳遞到Handler的handleMessage方法當中。

MessageQueue:

是隱藏在訊息背後的資料解構,裡面以佇列的形式存放著所有通過Handler傳送的訊息。這部分訊息會一直存放在佇列中,直到被取出後處理。每一個執行緒中最多隻允許存在一個MessageQueue物件。

Looper

Looper是每個執行緒中的管家,呼叫Looper以後會進入到loop方法中,這是一個無限迴圈,每當MessageQueue中存放著一條訊息的時候們就會將它自動取出來,並將訊息內容傳遞到handleMessage()方法中。

我們主要來說一說Looper和MessageQueue是在哪裡建立的,以及他們是如何串起來工作的。

Loop很簡單,在主線建立的時候,就會自動產生一個屬於主執行緒的looper物件,我們一般通過以下方式獲取到looper物件:

  1. 使用Looper靜態方法getMainLooper()
  2. 使用Looper的靜態方法獲取到當前執行緒的Looper物件。

Looper.getMainLooper()

一般情況下我們不會去手動建立一個loop去和某個執行緒產生關聯,大多數直接呼叫擁有Looper的執行緒,比如主執行緒,如果想要建立的話請參考:Looper文件

MessageQueue其中的技術細節我們不探討,我只考慮如何建立的MessageQueue以及MessageQueue是如何前邊幾個物件產生關聯的。

在Hnadler介面的初始化函式裡面我們可以看到:

@UnsupportedAppUsage public Handler(@NonNull Looper looper, @Nullable Callback callback, boolean async) {    mLooper = looper;    mQueue = looper.mQueue;    mCallback = callback;    mAsynchronous = async; }

佇列直接和looper是相互存在的,我們在looper的內部建構函式中可以看到,在Looper建立的時候就會建立配套的MessageQueue。

private Looper(boolean quitAllowed) {    mQueue = new MessageQueue(quitAllowed);    mThread = Thread.currentThread(); }

也就是說在一個執行緒裡面,建立了Looper就相當於建立好了兩個通訊元件,Looper和MessageQueue是相互繫結的。

那麼Looper是如何和Handler來產生關聯的呢?

很簡單就在Handler的建構函式啊,可別忘了我們是傳入了一個Looper進去的。

val handler = object : Handler(Looper.getMainLooper())

那麼在looper裡面是如何呼叫我傳入的處理函式呢?詳細解答請看:stackoverflow上一個大佬的解釋:連線

這裡不再做過多解釋。最後放一張圖來幫助你理解:

Servic-1

當然更具體的詳解可以看:

同步非同步訊息詳解

為什麼looper一直迴圈卻不佔滿cpu?

AsyncTask

AsyncTask是另一種多執行緒工具, 藉助AsyncTask,即使你對非同步訊息處理機制完全不瞭解,也可以十分簡單的從子執行緒切換到主執行緒。當然Async背後也是上述非同步訊息處理機制構成的,只是Android提供了更好的封裝組成了的AsyncTask。

首先我們來看一下用法:

AsyncTask是個抽象類,如果我們想使用它,就必須建立一個子類去繼承他,在繼承的時候,可以為其指定三個泛型引數,這三個引數的用途如下:

Params 在執行AsyncTask時候需要傳入的引數,可用於後臺任務中使用。

Progress 在後臺任務執行的時候,如果需要在介面上顯示當前的進度,則使用這裡指定的泛型作為單位。

Result 當任務執行完畢以後,如果需要對結果進行返回,則這裡指定的泛型作為返回值型別。

inner class Download: AsyncTask<Unit, Int, Boolean>() {}

我們在這裡將Params設定為Unit代表執行緒是不需要引數的設定為Unit。至於這樣設定效果是什麼請看下邊的方法,同時還存在四個需要重寫的方法:

  • onPreExecute()

這個方法會在後臺任務開始之前呼叫,用於進行一些介面上的初始化操作,比如顯示一下進度條對話方塊等。

  • doInBackground(Params )

這個方法中的所有程式碼都會在子執行緒中執行,我們應該在這裡去處理所有耗時的任務。任務一旦完成,就可以通過return語句將任務的執行結果返回,如果AsyncTask的第三個泛型引數指定的是Unit,就可以不返回任務執行結果.注意此方法中是不可以進行UI操作的,因為此時實際上是在另一個執行緒並不是在主執行緒。如果需要反饋任務進度,可以呼叫publishProgress來實現。

  • onProgressUpdate(Progress)

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

  • onPostExecute(Result)

當後臺任務之執行完畢並通過return語句進行返回的時候,這個方法很快就會被呼叫,返回的資料會作為引數傳遞到此方法當中。可以利用返回的資料進行一些UI操作,比如提醒任務執行的結果,以及關閉進度條等。

inner class Download: AsyncTask<Unit, Int, Boolean>() { ​     override fun onPreExecute() {         super.onPreExecute()         binding.progressBar.visibility=View.VISIBLE     } ​     override fun onPostExecute(result: Boolean?) {         super.onPostExecute(result)         if(result == true) {             binding.progressBar.setProgress(100,true)             binding.textView.text="Jszszzy"         }     } ​    override fun doInBackground(vararg params: Unit?): Boolean {        Log.d("doInBackground","This is background")        publishProgress(10)        return true   } ​    override fun onProgressUpdate(vararg values: Int?) {         super.onProgressUpdate(*values)        values[0]?.let { binding.progressBar.setProgress(it,true) }        Toast.makeText(baseContext,"Downloading",Toast.LENGTH_SHORT).show()     } ​ }

我們將第二引數設定為Int,這樣呼叫publishProgress()的時候,切換到主執行緒,並且給主執行緒的方法傳入一個引數為Int,這樣我們就可以獲得具體的引數,並且在主執行緒中更新U。

可惜的是,目前AsyncTask API 在Android10 已經被棄用,目前推薦的是使用協程來實現相同的操作,這個我們在Kotlin專欄裡面再去討論,接下來直接進入Service。

Android 的Service

建立一個Service,我們在專案的頂層包下右鍵,new->Service->Service。

service

建立一個以後我們再來重寫幾個方法:

class PlayerService : Service() { ​    override fun onBind(intent: Intent): IBinder { ​   } ​    override fun onCreate() {        super.onCreate()   } ​    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {        return super.onStartCommand(intent, flags, startId)   } ​    override fun onDestroy() {        super.onDestroy()   } }

其中:

onCreate方法在Service建立的時候呼叫。

onStartCommand方法在每次Service啟動的時候呼叫。

onDestory方法會在Service銷燬的呼叫。

關於具體的生命週期和呼叫我們留到生命週期部分再來詳細說。

實際上在我們通過Service嚮導建立以後,AS在Manifest檔案中幫我們宣告好了對應的Service,所有的Service都需要在Manifest檔案中宣告以後才能使用。實際上Android的四大元件都需要在Manifest檔案中來宣告。

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

與Activity緊密結合

雖然Service是在Activity中啟動的,但是啟動了之後好像與Activty並沒有什麼太大關係了,但是如果我們想讓Service和Act有一點交流呢?比如傳遞個引數什麼的,比如我們有些時候需要在Act中去控制Service的一些方法,讓他去進行一些任務的操作,這就需要我們剛剛專門沒有說的onBind()方法了。

修改Service類,建立一個Binder物件,用來向Act提供內部的方法。

inner class PlayerBinder:Binder() {     fun initPlayer(){         initMediaPlayer()     } }

在類裡面建立成員:

private val mBinder = PlayerBinder()

同時在onBind方法中返回物件,在Act繫結的時候會帶呼叫這個方法,並將繫結物件返回給Act中的繫結方法。

override fun onBind(intent: Intent?): IBinder {    return mBinder }

如何在Act中繫結Service?

首先建立一個與Service連線的類,這裡我們使用物件表示式,連同對應的物件一起建立了:

private  val connection = object : ServiceConnection{        override fun onServiceConnected(name: ComponentName?, service: IBinder) {        playerBinder = service as PlayerService.PlayerBinder        Log.e("jszszzy",playerBinder.toString())        playerBinder.initPlayer()   }        override fun onServiceDisconnected(name: ComponentName?) {   } }

建立ServiceConnection的實體物件需要實現其中的量抽象方法:

onServiceConnected是在Act與Service建立連線完成的時候回撥的,傳入的引數包含binder物件,實際上就是通過剛才建立的binder類構造的,在建立的時候會呼叫Service的onBind方法來返回一個binder物件。

我們當然可以在這個方法中獲取到對應的繫結物件,通過繫結物件就可以在下文程式碼中使用Binder物件中的方法。

比如在這裡我們使用了一個初始化播放器的方法,就是直接呼叫binder物件中的的方法。當然你也可以直接在Act中建立一類成員(lateinit var binding : ActivityMainBinding),讓他直接獲取到binder中的方法,這樣你就可以在Act中的任意位置獲取到binder物件,並呼叫他其中的方法。

class MainActivity : BaseActivity() {    lateinit var binding : ActivityMainBinding    lateinit var playerBinder: PlayerService.PlayerBinder ​    private  val connection = object : ServiceConnection{        override fun onServiceConnected(name: ComponentName?, service: IBinder) {            playerBinder = service as PlayerService.PlayerBinder            Log.e("jszszzy",playerBinder.toString())            playerBinder.initPlayer()       } ​        override fun onServiceDisconnected(name: ComponentName?) {       }   } }

你可能注意到了裡面其實還有一個onServiceDisconnected方法,它只有在Service建立程序崩潰或者被殺掉的時候才會呼叫,這個方法不太常用,所以空著不寫。

當然到這裡還沒有進行繫結呢,繫結的方式也很簡單,我們來舉個例子:

override fun onCreate(savedInstanceState: Bundle?) {     val intent = Intent(this, PlayerService::class.java)     bindService(intent, connection, Context.BIND_AUTO_CREATE) }

我們直接在onCreate回撥中執行繫結的程式碼,先構造要繫結的Service意圖,這和前邊的intent差不多,這裡就不再贅述,再通過context的bindService方法,傳入intent,和剛才構造的ServiceConnection物件,還有一個標誌位Context.BIND_AUTO_CREATE,其代表的意思是,在Act和Service繫結的時候自動建立一個Service物件,其實就是呼叫OnCreate方法,但是這個過程中,onStartCommand()方法並不會執行。

解除繫結的方法也很簡單 ,使用unbindService方法即可解除繫結,同時Service也會執行onDestroy方法來銷燬Service。

我們為介面一個名為stop按鈕新增一個解綁的回撥函式:

binding.stop.setOnClickListener {    playerBinder.stopPlayer()    unbindService(connection) }

注意

Service這個功能是一個極其特殊的功能功能,他在繫結的時候部分程式碼過程並不是在主執行緒完成的,雖然我們知道Service只是一個長久執行的模組,他不建立程序,也不建立執行緒,他需要執行的時候是直接在主執行緒中執行的,但是初始化的時候是通過Android AMS來幫助建立的,這就會導致一個問題,即我在某個階段直接去呼叫它的初始化函式可能導致,變數沒有被正確賦值,因為在其他AMS還沒有執行到對應的賦值程式碼。舉個例子:

如果我們繫結呼叫完就立馬執行binder裡面的方法就會報錯:

我們不再在OnCreate方法裡面繫結我們在一個按鈕的會帶函式裡面繫結並且呼叫繫結的方法:

binding.play.setOnClickListener {    val intent = Intent(this, PlayerService::class.java)    bindService(intent, connection, Context.BIND_AUTO_CREATE)    playerBinder.startPlayer() }

我們點選按鈕就會產生報錯:

E/AndroidRuntime: FATAL EXCEPTION: main    Process: com.example.audiotest, PID: 709    kotlin.UninitializedPropertyAccessException: lateinit property playerBinder has not been initialized        at com.example.audiotest.MainActivity.getPlayerBinder(MainActivity.kt:17)        at com.example.audiotest.MainActivity.onCreate$lambda-0(MainActivity.kt:49)        at com.example.audiotest.MainActivity.$r8$lambda$NLfemTBvJsTkHif14Uhm1FqFVaI(Unknown Source:0)        at com.example.audiotest.MainActivity$$ExternalSyntheticLambda1.onClick(Unknown Source:2)        at android.view.View.performClick(View.java:7603)        at com.google.android.material.button.MaterialButton.performClick(MaterialButton.java:1119)        at android.view.View.performClickInternal(View.java:7577)        at android.view.View.access$3800(View.java:865)        at android.view.View$PerformClick.run(View.java:29375)        at android.os.Handler.handleCallback(Handler.java:955)        at android.os.Handler.dispatchMessage(Handler.java:102)        at android.os.Looper.loopOnce(Looper.java:206)        at android.os.Looper.loop(Looper.java:296)        at android.app.ActivityThread.main(ActivityThread.java:8899)        at java.lang.reflect.Method.invoke(Native Method)        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:569)        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:976)

實際上就是我們的binder物件沒有初始化完成,就在主執行緒裡呼叫了binder的方法。所以繫結和呼叫需要分開在兩個不同的階段,以保證繫結的時候binder物件被正確的賦值。

Service生命週期

我們在每一個回撥函式裡面加入Log來提示對應的生命週期,具體的程式碼如下:

class PlayerService : Service() { ​    override fun onBind(intent: Intent?): IBinder? {        TODO("Not yet implemented")   } ​    override fun onCreate() {        super.onCreate()        Log.d("Service","This is onCreate Service")   } ​    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {        Log.d("Service","This is onStartCommand Service")        return super.onStartCommand(intent, flags, startId) ​   } ​    override fun onDestroy() {        Log.d("Service","This is onDestroy Service")        super.onDestroy()   } }

我們在主函式中向一個button中新增按鈕事件,按下按鈕的時候開啟Service,在另一個按鈕按下的時候關閉Service。

當我們按下開啟的按鈕的時候,對應回撥執行如下:

service-1

當我們按下結束的時候:

service-2

下面來說一下,onCreate,onStartCommand存在什麼區別,onStartCommand在每次Service啟動的時候都會去呼叫這個方法,而onCreate方法只有在第一次Service被呼叫建立的時候才會使用,Service啟動了之後,會一直保持啟動狀態,直到stopService或者stopSelf方法被呼叫的時候Service才會停止。

這都是很常規的情況,如果一個Service被startService()和bindService()同時呼叫,那麼在這種情況下就需要呼叫stopService和unbindService方法,這樣OnDestory才會執行。

```

\