Android點將臺:絕命暗殺官[-Service-]

張風捷特烈發表於1970-01-01

個人所有文章整理在此篇,將陸續更新收錄:知無涯,行者之路莫言終(我的程式設計之路)


零、前言

1.本文的知識點
1).Service的簡單`介紹及使用`   
2).Service的`繫結服務`實現`音樂播放器(條)`   
3).使用`aidl`實現其他app訪問該Service,播放音樂
複製程式碼

2.Service總覽

Service.png

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

Service繼承關係.png


一、Service初步認識

1.簡述

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

as.png


2.Service的開啟與關閉

最簡單的Service使用.png

2.1:Service測試類
/**
 * 作者:張風捷特烈<br></br>
 * 時間:2019/1/17/017:21:30<br></br>
 * 郵箱:1981462002@qq.com<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:測試類結果

點一下開啟會執行onCreateonStartCommand方法

點一下開啟.png

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

多次點選開啟.png

點選開啟與銷燬

點選開啟與銷燬.png


3.Activity與Service的資料傳遞

onStartCommand中有Intent,和BroadcastReciver的套路有點像

Service的資料傳遞.png

---->[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,進行測試 ActivityBroadcastReciverService是四大元件的三棵頂樑柱
Intent可以根據元件包名及類名開啟元件,ActivityBroadcastReciver可以,Service自然也可以,

建立另一個App.png

兩個app.png

Service在其他應用中訪問.png


侷限性:

1.需要新增android:exported="true",否則會崩
<service android:name=".service.service.ToastService" android:exported="true"/> 

2.大概一分鐘後會自動銷燬,自動銷燬後再用就會崩...所以約等於無用
複製程式碼

異常.png

自動銷燬.png


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));
        }
    }
}
複製程式碼

隱式問題.png


二、繫結服務

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

T-M-001.png


1.實現的效果

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

繫結服務.png

繫結服務.gif


2.播放介面
/**
 * 作者:張風捷特烈<br></br>
 * 時間:2018/10/31 0031:23:32<br></br>
 * 郵箱:1981462002@qq.com<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>
 * 郵箱:1981462002@qq.com<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>
 * 郵箱:1981462002@qq.com<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,如下

新增進度條.png


1.歌曲準備和修改介面

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

準備四首音樂.png

/**
 * 作者:張風捷特烈<br></br>
 * 時間:2018/10/31 0031:23:32<br></br>
 * 郵箱:1981462002@qq.com<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中通過ServiceConnectiononServiceConnected方法回撥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()//播放完成,進入下一曲
}
複製程式碼

播放的日誌.png


4.進度拖拽和監聽處理

這裡每隔一秒更新一下進度,通過Timer實現,當然實現方式有很多

Android點將臺:絕命暗殺官[-Service-]

---->[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物件呢?

cs.png

比如:一臺伺服器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自動生成的,你會有所感觸

aidl生成檔案.png


1.aidl檔案的書寫

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

自動生成.png

// 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如何使用,下一篇將詳細分析它 可以看出IMusicPlayerService中有一個內部類Stub繼承自Binder還實現了IMusicPlayerService
剛才我們是自定義MusicPlayer繼承Binder並實現IPlayer
現在有個現成的IMusicPlayerService.Stub,我們繼承它就行了,為避免看起來亂
新建了一個MusicPlayerServiceMusicPlayerStub,可以上面的方式圖對比一下

aidl.png

---->[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/>
 * 郵箱:1981462002@qq.com<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/>
 * 郵箱:1981462002@qq.com<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物件
注意一點:常識問題,在客戶端連線服務端時,服務端要先開啟...

aidl在另一個應用中使用.png

aidl繫結服務.png

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.本文的知識點
1).Service的簡單`介紹及使用`   
2).Service的`繫結服務`實現`音樂播放器(條)`   
3).使用`aidl`實現其他app訪問該Service,播放音樂
複製程式碼

2.Service總覽

Service.png

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

Service繼承關係.png


一、Service初步認識

1.簡述

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

as.png


2.Service的開啟與關閉

最簡單的Service使用.png

2.1:Service測試類
/**
 * 作者:張風捷特烈<br></br>
 * 時間:2019/1/17/017:21:30<br></br>
 * 郵箱:1981462002@qq.com<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:測試類結果

點一下開啟會執行onCreateonStartCommand方法

點一下開啟.png

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

多次點選開啟.png

點選開啟與銷燬

點選開啟與銷燬.png


3.Activity與Service的資料傳遞

onStartCommand中有Intent,和BroadcastReciver的套路有點像

Service的資料傳遞.png

---->[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,進行測試 ActivityBroadcastReciverService是四大元件的三棵頂樑柱
Intent可以根據元件包名及類名開啟元件,ActivityBroadcastReciver可以,Service自然也可以,

建立另一個App.png

兩個app.png

Service在其他應用中訪問.png


侷限性:

1.需要新增android:exported="true",否則會崩
<service android:name=".service.service.ToastService" android:exported="true"/> 

2.大概一分鐘後會自動銷燬,自動銷燬後再用就會崩...所以約等於無用
複製程式碼

異常.png

自動銷燬.png


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));
        }
    }
}
複製程式碼

隱式問題.png


二、繫結服務

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

T-M-001.png


1.實現的效果

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

繫結服務.png

繫結服務.gif


2.播放介面
/**
 * 作者:張風捷特烈<br></br>
 * 時間:2018/10/31 0031:23:32<br></br>
 * 郵箱:1981462002@qq.com<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>
 * 郵箱:1981462002@qq.com<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>
 * 郵箱:1981462002@qq.com<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,如下

新增進度條.png


1.歌曲準備和修改介面

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

準備四首音樂.png

/**
 * 作者:張風捷特烈<br></br>
 * 時間:2018/10/31 0031:23:32<br></br>
 * 郵箱:1981462002@qq.com<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中通過ServiceConnectiononServiceConnected方法回撥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()//播放完成,進入下一曲
}
複製程式碼

播放的日誌.png


4.進度拖拽和監聽處理

這裡每隔一秒更新一下進度,通過Timer實現,當然實現方式有很多

Android點將臺:絕命暗殺官[-Service-]

---->[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物件呢?

cs.png

比如:一臺伺服器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自動生成的,你會有所感觸

aidl生成檔案.png


1.aidl檔案的書寫

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

自動生成.png

// 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如何使用,下一篇將詳細分析它 可以看出IMusicPlayerService中有一個內部類Stub繼承自Binder還實現了IMusicPlayerService
剛才我們是自定義MusicPlayer繼承Binder並實現IPlayer
現在有個現成的IMusicPlayerService.Stub,我們繼承它就行了,為避免看起來亂
新建了一個MusicPlayerServiceMusicPlayerStub,可以上面的方式圖對比一下

aidl.png

---->[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/>
 * 郵箱:1981462002@qq.com<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/>
 * 郵箱:1981462002@qq.com<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物件
注意一點:常識問題,在客戶端連線服務端時,服務端要先開啟...

aidl在另一個應用中使用.png

aidl繫結服務.png

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的機制的第一層。

相關文章