零、前言
1.本文的知识点
1).Service的简单`介绍及使用` | |
2).Service的`绑定服务`实现`音乐播放器(条)` | |
3).使用`aidl`实现其他app访问该Service,播放音乐 |
2.Service总览
类名:Service 父类:ContextWrapper 修饰:public abstract | |
实现的接口:[ComponentCallbacks2] | |
包名:android.app 依赖类个数:16 | |
内部类/接口个数:0 | |
源码行数:790 源码行数(除注释):171 | |
属性个数:3 方法个数:21 public方法个数: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> | |
* 邮箱: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:测试类结果
点一下开启会执行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 | |
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> | |
* 邮箱: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,如下
1.歌曲准备和修改接口
这里为了简洁些,直接用四个路径,判断存在什么的自己完善(非本文重点) 关于MediaPlayer的相关知识详见这篇,这里就直接上代码了 在create时传入播放的列表路径字符串
/** | |
* 作者:张风捷特烈<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中通过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如何使用,下一篇将详细分析它 可以看出IMusicPlayerService中有一个内部类Stub继承自Binder还实现了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/> | |
* 邮箱: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; | |
} | |
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() { | |
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(); | |
} | |
}); | |
} | |
public void start() throws RemoteException { | |
if (!isInitialized && mPlayer.isPlaying()) { | |
return; | |
} | |
mPlayer.start(); | |
} | |
public void stop() throws RemoteException { | |
} | |
public void pause() throws RemoteException { | |
if (mPlayer.isPlaying()) { | |
mPlayer.pause(); | |
} | |
} | |
public void prev() throws RemoteException { | |
mCurrentPos--; | |
judgePos();//如果越界则置0 | |
changMusicByPos(mCurrentPos); | |
} | |
public void next() throws RemoteException { | |
mCurrentPos++; | |
judgePos();//如果越界则置0 | |
changMusicByPos(mCurrentPos); | |
} | |
public void release() throws RemoteException { | |
} | |
public boolean isPlaying() throws RemoteException { | |
return mPlayer.isPlaying(); | |
} | |
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; | |
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(); | |
} | |
} | |
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.本文的知识点
1).Service的简单`介绍及使用` | |
2).Service的`绑定服务`实现`音乐播放器(条)` | |
3).使用`aidl`实现其他app访问该Service,播放音乐 |
2.Service总览
类名:Service 父类:ContextWrapper 修饰:public abstract | |
实现的接口:[ComponentCallbacks2] | |
包名:android.app 依赖类个数:16 | |
内部类/接口个数:0 | |
源码行数:790 源码行数(除注释):171 | |
属性个数:3 方法个数:21 public方法个数: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> | |
* 邮箱: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:测试类结果
点一下开启会执行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 | |
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> | |
* 邮箱: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,如下
1.歌曲准备和修改接口
这里为了简洁些,直接用四个路径,判断存在什么的自己完善(非本文重点) 关于MediaPlayer的相关知识详见这篇,这里就直接上代码了 在create时传入播放的列表路径字符串
/** | |
* 作者:张风捷特烈<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中通过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如何使用,下一篇将详细分析它 可以看出IMusicPlayerService中有一个内部类Stub继承自Binder还实现了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/> | |
* 邮箱: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; | |
} | |
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() { | |
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(); | |
} | |
}); | |
} | |
public void start() throws RemoteException { | |
if (!isInitialized && mPlayer.isPlaying()) { | |
return; | |
} | |
mPlayer.start(); | |
} | |
public void stop() throws RemoteException { | |
} | |
public void pause() throws RemoteException { | |
if (mPlayer.isPlaying()) { | |
mPlayer.pause(); | |
} | |
} | |
public void prev() throws RemoteException { | |
mCurrentPos--; | |
judgePos();//如果越界则置0 | |
changMusicByPos(mCurrentPos); | |
} | |
public void next() throws RemoteException { | |
mCurrentPos++; | |
judgePos();//如果越界则置0 | |
changMusicByPos(mCurrentPos); | |
} | |
public void release() throws RemoteException { | |
} | |
public boolean isPlaying() throws RemoteException { | |
return mPlayer.isPlaying(); | |
} | |
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; | |
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(); | |
} | |
} | |
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的机制的第一层。