Android點將臺:絕命暗殺官[-Service-]
至美不過回首,往事重重,顧來時,荊棘漫漫,血浸途中。 今銅衣鐵靴,再行來路,任荊棘漫漫,唯落綠葉殘枝。 ----張風捷特烈 複製程式碼
零、前言
在此之前希望你已經閱讀了: Android點將臺:顏值擔當[-Activity-]
在此之前希望你已經閱讀了: Android點將臺:外交官[-Intent-]
1.本文的知識點
1).Service的簡單 介紹及使用
2).Service的 繫結服務
實現 音樂播放器(條)
3).使用 aidl
實現其他app訪問該Service,播放音樂
2.Service總覽

類名:Service父類:ContextWrapper修飾:public abstract 實現的介面:[ComponentCallbacks2] 包名:android.app依賴類個數:16 內部類/介面個數:0 原始碼行數:790原始碼行數(除註釋):171 屬性個數:3方法個數:21public方法個數:20 複製程式碼

一、Service初步認識
1.簡述
Service和Activity同屬一家,一暗一明,Android作為顏值擔當,Service做後臺工作(如圖)
他不見天日,卻要忠誠地執行任務,Service這個類的本身非常小,裸碼171行
是什麼讓它成為"新手的噩夢",一個單詞: Binder
,曾經讓多少人聞風喪膽的 首席殺手

2.Service的開啟與關閉

2.1:Service測試類
/** * 作者:張風捷特烈<br></br> * 時間:2019/1/17/017:21:30<br></br> * 郵箱:[email protected]<br></br> * 說明:Service測試 */ class MusicService : Service() { /** * 繫結Service * @param intent 意圖 * @return IBinder物件 */ override fun onBind(intent: Intent): IBinder? { Log.e(TAG, "onBind: ") return null } /** * 建立Service */ override fun onCreate() { super.onCreate() Log.e(TAG, "onCreate: ") } /** * 開始執行命令 * @param intent 意圖 * @param flags 啟動命令的額外資料 * @param startId id * @return */ override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { Log.e(TAG, "onStartCommand: ") Toast.makeText(this, "onStartCommand", Toast.LENGTH_SHORT).show() return super.onStartCommand(intent, flags, startId) } /** * 解綁服務 * @param intent 意圖 * @return */ override fun onUnbind(intent: Intent): Boolean { Log.e(TAG, "onUnbind: 成功解綁") return super.onUnbind(intent) } /** * 銷燬服務 */ override fun onDestroy() { super.onDestroy() Log.e(TAG, "onDestroy: 銷燬服務") } companion object { private val TAG = "MusicService" } } 複製程式碼
2.2:ToastSActivity測試類
就兩個按鈕,點一下
//開啟服務 id_btn_start.setOnClickListener { toastIntent = Intent(this, MusicService::class.java) startService(toastIntent) } //銷燬服務 id_btn_kill.setOnClickListener { stopService(toastIntent) } 複製程式碼
2.3:測試類結果
點一下開啟會執行 onCreate
和 onStartCommand
方法

多次點選開啟, onCreate
只會執行一次, onStartCommand
方法每次都會執行

點選開啟與銷燬

3.Activity與Service的資料傳遞
onStartCommand中有Intent,和BroadcastReciver的套路有點像

---->[ToastSActivity#onCreate]---------------------- id_btn_start.setOnClickListener { toastIntent = Intent(this, MusicService::class.java) toastIntent?.putExtra("toast_data", id_et_msg.text.toString()) startService(toastIntent) } ---->[MusicService#onStartCommand]---------------------- override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int Log.e(TAG, "onStartCommand: ") val data = intent.getStringExtra("toast_data") //data?:"NO MSG"表示如果data是空,就取"NO MSG" Toast.makeText(this, data?:"NO MSG", Toast.LENGTH_SHORT).show() return super.onStartCommand(intent, flags, startId) } 複製程式碼
4.在另一個App中使用其他app的Service
建立另一個App,進行測試 Activity
、 BroadcastReciver
、 Service
是四大元件的三棵頂樑柱
Intent可以根據元件包名及類名開啟元件, Activity
、 BroadcastReciver
可以, Service
自然也可以,



侷限性:
1.需要新增android:exported="true",否則會崩 <service android:name=".service.service.ToastService" android:exported="true"/> 2.大概一分鐘後會自動銷燬,自動銷燬後再用就會崩...所以約等於無用 複製程式碼


4.關於隱式呼叫Service
Android5.0+ 明確指出不能隱式呼叫:ContextImpl的 validateServiceIntent
方法中
---->[ContextImpl#validateServiceIntent]--------------------------- private void validateServiceIntent(Intent service) { //包名、類名為空,即隱式呼叫,跑異常 if (service.getComponent() == null && service.getPackage() == null) { //從LOLLIPOP(即5.0開始) if (getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.LOLLIPOP) { IllegalArgumentException ex = new IllegalArgumentException( "Service Intent must be explicit: " + service); throw ex; } else { Log.w(TAG, "Implicit intents with startService are not safe: " + service + " " + Debug.getCallers(2, 3)); } } } 複製程式碼

二、繫結服務
前面的都是元件的日常,接下來才是Service的要點
為了不讓本文看起來太low,寫個佈局吧(效果擺出來了,可以仿著做。不嫌醜的話用button也可以)

1.實現的效果
為了方便管理,這裡寫了一個IPlayer介面規定一下MusicPlayer的幾個主要方法
暫時都是無返回值,無入參的方法,以後有需要再逐步完善


2.播放介面
/** * 作者:張風捷特烈<br></br> * 時間:2018/10/31 0031:23:32<br></br> * 郵箱:[email protected]<br></br> * 說明:播放介面 */ interface IPlayer { fun create()// 誕生 fun start()// 開始 fun resume()// 復甦 fun stop()// 停止 fun pause()// 暫停 fun release()//死亡 } 複製程式碼
3.播放的核心類
/** * 作者:張風捷特烈<br></br> * 時間:2019/1/17/017:21:57<br></br> * 郵箱:[email protected]<br></br> * 說明:播放核心類 */ class MusicPlayer(private val mContext: Context) : Binder(), IPlayer { override fun create() { Toast.makeText(mContext, "誕生", Toast.LENGTH_SHORT).show() } override fun start() { Toast.makeText(mContext, "開始播放", Toast.LENGTH_SHORT).show() } override fun resume() { Toast.makeText(mContext, "恢復播放", Toast.LENGTH_SHORT).show() } override fun stop() { Toast.makeText(mContext, "停止播放", Toast.LENGTH_SHORT).show() } override fun pause() { Toast.makeText(mContext, "暫停播放", Toast.LENGTH_SHORT).show() } override fun release() { Toast.makeText(mContext, "銷燬", Toast.LENGTH_SHORT).show() } } 複製程式碼
4.播放的服務
/** * 作者:張風捷特烈<br></br> * 時間:2019/1/17/017:21:30<br></br> * 郵箱:[email protected]<br></br> * 說明:播放Service測試 */ class MusicService : Service() { override fun onBind(intent: Intent): IBinder? { Log.e(TAG, "onBind: ") Toast.makeText(this, "Bind OK", Toast.LENGTH_SHORT).show() return MusicPlayer(this) } override fun onCreate() { super.onCreate() Log.e(TAG, "onCreate: ") } override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { Log.e(TAG, "onStartCommand: ") return super.onStartCommand(intent, flags, startId) } override fun onUnbind(intent: Intent): Boolean { Toast.makeText(this, "onUnbind: 成功解綁", Toast.LENGTH_SHORT).show() Log.e(TAG, "onUnbind: 成功解綁") return super.onUnbind(intent) } override fun onDestroy() { super.onDestroy() Log.e(TAG, "onDestroy: 銷燬服務") } companion object { private val TAG = "MusicService" } } 複製程式碼
5.Activity中的使用
/** * 繫結服務 */ private fun bindMusicService() { musicIntent = Intent(this, MusicService::class.java) mConn = object : ServiceConnection { // 當連線成功時候呼叫 override fun onServiceConnected(name: ComponentName, service: IBinder) { mMusicPlayer = service as MusicPlayer } // 當連線斷開時候呼叫 override fun onServiceDisconnected(name: ComponentName) { } } //[2]繫結服務啟動 bindService(musicIntent, mConn, BIND_AUTO_CREATE); } 複製程式碼
三、音樂播放條的簡單實現
接下來實現一個播放條,麻雀雖小,五臟俱全,完善了一下UI,如下

1.歌曲準備和修改介面
這裡為了簡潔些,直接用四個路徑,判斷存在什麼的自己完善(非本文重點)
關於 MediaPlayer的相關知識詳見這篇 ,這裡就直接上程式碼了
在create時傳入播放的列表路徑字串

/** * 作者:張風捷特烈<br></br> * 時間:2018/10/31 0031:23:32<br></br> * 郵箱:[email protected]<br></br> * 說明:播放介面 */ interface IPlayer { fun create(musicList: ArrayList<String>)// 誕生 fun start()// 開始 fun stop()// 停止 fun pause()// 暫停 fun release()//死亡 fun next()//下一曲 fun prev()//上一曲 fun isPlaying(): Boolean 是否播放 fun seek(pre_100: Int)//拖動進度 } 複製程式碼
2.create方法和start方法的實現
MusicActivity中通過 ServiceConnection
的 onServiceConnected方法
回撥 IBinder
物件
將 MusicPlayer
物件傳入MusicActivity中,對應的UI點選呼叫對應的方法即可
---->[MusicPlayer]-------------- private lateinit var mPlayer: MediaPlayer private var isInitialized = false//是否已初始化 private var mCurrentPos = 0//當前播放第幾個音樂 private lateinit var mMusicList: ArrayList<String>//當前播放第幾個音樂 ---->[MusicPlayer#create]-------------- override fun create(musicList: ArrayList<String>) { mMusicList = musicList val file = File(musicList[mCurrentPos]) val uri = Uri.fromFile(file) mPlayer = MediaPlayer.create(mContext, uri) isInitialized = true Log.e(TAG, "誕生") } ---->[MusicPlayer#start]-------------- override fun start() { if (!isInitialized && mPlayer.isPlaying) { return } mPlayer.start(); Log.e(TAG, "開始播放") } 複製程式碼
這樣歌曲就能播放了
3.上一曲和下一曲的實現及自動播放下一曲
---->[MusicPlayer]-------------- override fun next() { mCurrentPos++ judgePos()//如果越界則置0 changMusicByPos(mCurrentPos) } override fun prev() { mCurrentPos-- judgePos()//如果越界則置0 changMusicByPos(mCurrentPos) } /** * 越界處理 */ private fun judgePos() { if (mCurrentPos >= mMusicList.size) { mCurrentPos = 0 } if (mCurrentPos < 0) { mCurrentPos = mMusicList.size - 1 } } /** * 根據位置切歌 * @param pos 當前歌曲id */ private fun changMusicByPos(pos: Int) { mPlayer.reset()//重置 mPlayer.setDataSource(mMusicList[pos])//設定當前歌曲 mPlayer.prepare()//準備 start() Log.e(TAG, "當前播放歌曲pos:$pos:,路徑:${mMusicList[pos]}" ) } ---->[MusicPlayer#create]-------------- mPlayer.setOnCompletionListener { next()//播放完成,進入下一曲 } 複製程式碼

4.進度拖拽和監聽處理
這裡每隔一秒更新一下進度,通過Timer實現,當然實現方式有很多

---->[MusicPlayer]-------------- override fun seek(pre_100: Int) { pause() mPlayer.seekTo((pre_100 * mPlayer.duration / 100)) start() } ---->[MusicPlayer#create]-------------- mTimer = Timer()//建立Timer mHandler = Handler()//建立Handler mTimer.schedule(timerTask { if (isPlaying()) { val pos = mPlayer.currentPosition; val duration = mPlayer.duration; mHandler.post { if (mOnSeekListener != null) { mOnSeekListener.onSeek((pos.toFloat() / duration * 100).toInt()); } } } }, 0, 1000) //------------設定進度監聽----------- interface OnSeekListener { fun onSeek(per_100: Int); } private lateinit var mOnSeekListener: OnSeekListener fun setOnSeekListener(onSeekListener: OnSeekListener) { mOnSeekListener = onSeekListener; } 複製程式碼
5.繫結服務的意義何在?
估計很多新手都有一個疑問,我直接在Activity中new 一個MediaPlayer多好
為什麼非要通過Service來繞一圈得到MediaPlayer物件呢?

比如:一臺伺服器S上執行著一個遊戲業務,一個客戶端C連線到伺服器便能夠玩遊戲 沒有人會想把伺服器上的業務移植到客戶端,如果這樣就真的一人一區了 Service相當於提供服務,此時Activity相當於客戶端,通過conn連線服務 MediaPlayer(Binder物件)相當於核心業務,通過繫結獲取服務,是典型的client-server模式 client-server模式的特點是一個Service可以為多個客戶端服務 client可以通過IBinder介面獲取服務業務的例項這裡是MediaPlayer(Binder物件) 從而實現在client端直接呼叫服務業務(MediaPlayer)中的方法以實現靈活互動 但是現在只能在一個app裡玩,如何讓其他app也可以連線服務,這就要說到aidl了 還有很重要的一點:Service存活力強,記得上次在Activity中new MediaPlayer 來播放音樂 切切應用一會就停了。今天在Service裡,玩了半天音樂也沒停 複製程式碼
四、安卓介面定義語言 aidl
在Service中的使用
這個服務端有點弱,現在想辦法讓外部也能用它
不知道下圖你裡看出了什麼,我看的挺興奮,前幾天看framework原始碼,感覺挺相似
你可以看一下 ActivityManagerNative
的原始碼和這裡AS自動生成的,你會有所感觸

1.aidl檔案的書寫
還記得上面的IPlayer的介面吧,aidl內容就是這個介面的方法
只不過書寫的語法稍稍不同,下面是IMusicPlayerService的aidl
寫完後記得點小錘子,他會使用 sdk\build-tools\28.0.3\aidl.exe
生成程式碼

// IMusicPlayerService.aidl package com.toly1994.tolyservice; // Declare any non-default types here with import statements interface IMusicPlayerService { /** * Demonstrates some basic types that you can use as parameters * and return values in AIDL. */ void stop(); void pause(); void start(); void prev(); void next(); void release(); boolean isPlaying(); void seek(int pre_100); //加in void create(in List<String> filePaths); } 複製程式碼
2.自動生成的程式碼使用
IMusicPlayerService
剛才我們是自定義
MusicPlayer
繼承
Binder
並實現
IPlayer
現在有個現成的IMusicPlayerService.Stub,我們繼承它就行了,為避免看起來亂
新建了一個 MusicPlayerService
和 MusicPlayerStub
,可以上面的方式圖對比一下

---->[IMusicPlayerService$Stub]------------ public interface IMusicPlayerService extends android.os.IInterface{ /** Local-side IPC implementation stub class. */ public static abstract class Stub extends android.os.Binder implements com.toly1994.tolyservice.IMusicPlayerService 複製程式碼
3.MusicPlayerStub的實現(Binder物件)
實現上和上面的 MusicPlayer
一模一樣,這裡用java實現
/** * 作者:張風捷特烈<br/> * 時間:2019/1/23/023:17:11<br/> * 郵箱:[email protected]<br/> * 說明:MusicPlayerStub--Binder物件 */ public class MusicPlayerStub extends IMusicPlayerService.Stub { private MediaPlayer mPlayer; private boolean isInitialized = false;//是否已初始化 private int mCurrentPos = 0;//當前播放第幾個音樂 private List<String> mMusicList;//音樂列表 private Context mContext; private Timer mTimer; private Handler mHandler; public MusicPlayerStub(Context mContext) { this.mContext = mContext; } @Override public void create(List<String> filePaths) throws RemoteException { mMusicList = filePaths; File file = new File(mMusicList.get(mCurrentPos)); Uri uri = Uri.fromFile(file); mPlayer = MediaPlayer.create(mContext, uri); isInitialized = true; //建構函式中 mTimer = new Timer();//建立Timer mHandler = new Handler();//建立Handler //開始方法中 mTimer.schedule(new TimerTask() { @Override public void run() { if (mPlayer.isPlaying()) { int pos = mPlayer.getCurrentPosition(); int duration = mPlayer.getDuration(); mHandler.post(() -> { if (mOnSeekListener != null) { mOnSeekListener.onSeek((int) (pos * 1.f / duration * 100)); } }); } } }, 0, 1000); mPlayer.setOnCompletionListener(mp -> { try { next();//播放完成,進入下一曲 } catch (RemoteException e) { e.printStackTrace(); } }); } @Override public void start() throws RemoteException { if (!isInitialized && mPlayer.isPlaying()) { return; } mPlayer.start(); } @Override public void stop() throws RemoteException { } @Override public void pause() throws RemoteException { if (mPlayer.isPlaying()) { mPlayer.pause(); } } @Override public void prev() throws RemoteException { mCurrentPos--; judgePos();//如果越界則置0 changMusicByPos(mCurrentPos); } @Override public void next() throws RemoteException { mCurrentPos++; judgePos();//如果越界則置0 changMusicByPos(mCurrentPos); } @Override public void release() throws RemoteException { } @Override public boolean isPlaying() throws RemoteException { return mPlayer.isPlaying(); } @Override public void seek(int pre_100) throws RemoteException { pause(); mPlayer.seekTo((pre_100 * mPlayer.getDuration() / 100)); start(); } /** * 越界處理 */ private void judgePos() { if (mCurrentPos >= mMusicList.size()) { mCurrentPos = 0; } if (mCurrentPos < 0) { mCurrentPos = mMusicList.size() - 1; } } /** * 根據位置切歌 * * @param pos 當前歌曲id */ private void changMusicByPos(int pos) { mPlayer.reset();//重置 try { mPlayer.setDataSource(mMusicList.get(pos));//設定當前歌曲 mPlayer.prepare();//準備 start(); } catch (IOException | RemoteException e) { e.printStackTrace(); } } //------------設定進度監聽----------- public interface OnSeekListener { void onSeek(int per_100); } private OnSeekListener mOnSeekListener; public void setOnSeekListener(OnSeekListener onSeekListener) { mOnSeekListener = onSeekListener; } } 複製程式碼
4. MusicPlayerService
中返回MusicPlayerStub物件
一般都把MusicPlayerStub作為MusicPlayerService的一個內部類
本質沒有區別,為了和上面對應,看起來舒服些,我把MusicPlayerStub提到了外面
/** * 作者:張風捷特烈<br/> * 時間:2019/1/23/023:16:32<br/> * 郵箱:[email protected]<br/> * 說明:音樂播放服務idal版 */ public class MusicPlayerService extends Service { private MusicPlayerStub musicPlayerStub; @Override public void onCreate() { super.onCreate(); ArrayList<String> musicList = new ArrayList<>(); musicList.add("/sdcard/toly/此生不換_青鳥飛魚.aac"); musicList.add("/sdcard/toly/勇氣-梁靜茹-1772728608-1.mp3"); musicList.add("/sdcard/toly/草戒指_魏新雨.aac"); musicList.add("/sdcard/toly/郭靜 - 下一個天亮 [mqms2].flac"); musicPlayerStub = new MusicPlayerStub(this); try { musicPlayerStub.create(musicList); } catch (RemoteException e) { e.printStackTrace(); } } @Nullable @Override public IBinder onBind(Intent intent) { return musicPlayerStub; } } 複製程式碼
5.在本專案中的使用
如果只在本專案中用,將兩個類換下名字就行了和剛才沒本質區別
/** * 繫結服務 */ private fun bindMusicService() { musicIntent = Intent(this, MusicPlayerService::class.java) mConn = object : ServiceConnection { // 當連線成功時候呼叫 override fun onServiceConnected(name: ComponentName, service: IBinder) { mMusicPlayer = service as MusicPlayerStub mMusicPlayer.setOnSeekListener { per_100 -> id_pv_pre.setProgress(per_100) } } // 當連線斷開時候呼叫 override fun onServiceDisconnected(name: ComponentName) { } } //[2]繫結服務啟動 bindService(musicIntent, mConn, BIND_AUTO_CREATE); } 複製程式碼
話說回來,搞了一大圈,aidl的優勢在哪裡?現在貌似還沒看出來哪裡厲害,接著看
在此之前先配置一下服務 app/src/main/AndroidManifest.xml
<service android:name=".service.service.MusicPlayerService"> <intent-filter> <action android:name="www.toly1994.com.music.player"></action> </intent-filter> </service> 複製程式碼
五、基於 aidl
在另一個專案中使用別的專案Service
這就是aidl的牛掰的地方,跨程序間通訊,以及Android的系統級Service都基於此
下面進入另一個app裡: anotherapp
,核心點就是獲取IMusicPlayerService物件
注意一點: 常識問題,在客戶端連線服務端時,服務端要先開啟...


class ServiceTestActivity : AppCompatActivity() { private var mConn: ServiceConnection? = null private lateinit var mMusicPlayer: IMusicPlayerService override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.ac_br) title="另一個App" bindMusicService() id_btn_send.text="播放音樂" id_btn_send.setOnClickListener { mMusicPlayer.start() } } /** * 繫結服務 */ private fun bindMusicService() { val intent = Intent() //坑點:5.0以後要加 服務包名,不然報錯 intent.setPackage("com.toly1994.tolyservice") intent.action = "www.toly1994.com.music.player" mConn = object : ServiceConnection { // 當連線成功時候呼叫 override fun onServiceConnected(name: ComponentName, service: IBinder) { //核心點獲取IMusicPlayerService物件 mMusicPlayer = IMusicPlayerService.Stub.asInterface(service) } // 當連線斷開時候呼叫 override fun onServiceDisconnected(name: ComponentName) { } } //[2]繫結服務啟動 bindService(intent, mConn, BIND_AUTO_CREATE); } } 複製程式碼
當點選時音樂響起,一切就通了,如果你瞭解client-server模式,你應該明白這有多重要
framework的眾多service就是這個原理,所以不明白aidl,framework的程式碼看起來會很吃力
下一篇將會結合framework,詳細討論aidl以及Binder的機制的第一層。
後記:捷文規範
1.本文成長記錄及勘誤表
專案原始碼 | 日期 | 附錄 |
---|---|---|
V0.1--無 | 2018-1-23 | 無 |
釋出名: Android點將臺:絕命暗殺官[-Service-]
捷文連結: juejin.im/post/5c4a7e…
2.更多關於我
筆名 | 微信 | |
---|---|---|
張風捷特烈 | 1981462002 | zdl1994328 |
我的github: github.com/toly1994328
我的簡書: www.jianshu.com/u/e4e52c116…
我的簡書: www.jianshu.com/u/e4e52c116…
個人網站:www.toly1994.com
3.宣告
1----本文由張風捷特烈原創,轉載請註明
2----歡迎廣大程式設計愛好者共同交流
3----個人能力有限,如有不正之處歡迎大家批評指證,必定虛心改正
4----看到這裡,我在此感謝你的喜歡與支援
