完整示例代码:https://gitee.com/daizhufei/remote-service.git
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="cn.android666.remoteservice">
<application
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.RemoteService">
</application>
</manifest>
package cn.android666.remoteservice;
interface IDzEngine {
String hello(String name);
}
执行菜单:Build > Recomplile ‘IDzEngine.aidl’,它会自动生成IDzEngine.java(注:现在是2023年1月11日,我发现最新的Android Studio按这个操作不会生成对应的java文件,我们可以在项目中使用一下这个类,比如在MainActivity中写上:val engine :cn.android666.remoteservice.IDzEngine? = null,此时代码会报错,不用管它,点击运行按钮,把app运行到手机上,此时就会自动生成对应的java文件了),生成的java文件如下:
class RemoteService : Service() {
override fun onBind(intent: Intent): IBinder {
return DzEngine()
}
class DzEngine : IDzEngine.Stub() {
override fun hello(name: String?): String {
return "$name,你好"
}
}
}
清单文件声明如下:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="cn.android666.remoteservice">
<application
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.RemoteService">
<service
android:name=".RemoteService"
android:enabled="true"
android:exported="true"
android:permission="android.permission.ACCESS_LOCATION_EXTRA_COMMANDS">
<intent-filter>
<action android:name="cn.android666.remoteservice.hahaha" />
</intent-filter>
</service>
</application>
</manifest>
这里我们给服务指定一个权限,和一个action,则别人在绑定我们这个服务的时候就需要声明相同的权限和指定相同的action,这里的权限推荐使用一个系统权限,不推荐使用自定义权限,如果使用自定义权限的话,先装客户端,后装服务端就会行不通,具体见:https://www.cdsy.xyz/computer/programme/android/230205/cd40177.htmlOK,到此,服务端代码我们就写好了。
class MainActivity : AppCompatActivity(), ServiceConnection {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
bindRemoteService()
}
private fun bindRemoteService() {
val intent = Intent("cn.android666.remoteservice.hahaha") // action为远程服务的action
intent.setPackage("cn.android666.remoteservice") // 包名为远程服务的的包名
val result = bindService(intent, this, Context.BIND_AUTO_CREATE)
Log.i("ABCD", "绑定函数返回结果:$result")
}
/** 在与 Service 的连接建立时调用 */
override fun onServiceConnected(name: ComponentName, service: IBinder) {
val dzEngine = IDzEngine.Stub.asInterface(service)
val hello = dzEngine.hello("张三")
Log.i("ABCD","远程服务绑定成功: $hello")
}
/** 当与服务的连接被意外中断时执行,这通常发生在服务的进程崩溃或被终止时。 */
override fun onServiceDisconnected(name: ComponentName?) {
Log.i("ABCD","远程服务的连接被意外中断")
}
}
OK,到这里客户端的代码就完成了。如上图,默认为“Default Activity",我们要改为Nothing,然后点击“OK”,然后点击运行按钮,把app运行到手机上,然后再把client也运行到手机上,这里我运行到Android 7.1的手机上,client输出日志如下:
绑定函数返回结果:true
远程服务绑定成功: 张三,你好
接下来,我们找一部Android 11的手机,先运行app,再运行client。在安装app的时候,如果是oppo手机会提示此应用无桌面图标,可以点击“无视风险安装”即可,如下:
安装后,app在手机桌面上是没有图标的,在设置的应用管理中可以找到此应用。
运行client,输出日志如下:
绑定函数返回结果:false
这说明绑定失败了,这是因为在Android 11版本的时候,添加了应用可见性的限制,默认所有第三方应用对于我们是不可见的,我们无法与不可见的应用进行交互,比如绑定第三方应用的服务,要想第三方应用对于我们可见,可在清单文件中添加第三方应用的包名,如下:
通过指定包名的方式,这个包名的应用对于我们就是可见的了,所以可以与之进行交互了,再次运行client就可以绑定成功了,我在Android 11的模拟器上试是可以的。如果是运行到oppo手机的话,我们发现结果还是绑定不上,这是因为服务端默认并没有运行,我们绑定的时候它才会运行,但是oppo添加了一个限制,默认不允一个应用启动另一个应用,我们可以在服务端的应用设置界面中解除此设置,如下:
设置了“允许应用关联启动”后,我们再次运行client,然后就可以绑定成功了!
如果运行到小米的Android 11,没有“允许应用关联启动”的选项,而且也绑定不上,我们只需要打开“允许应用自启动”即可绑定成功!而oppo开启“允许应用自启动”是绑定不了的,必须要打开“允许应用关联启动”才可以绑定成功! 其他品牌手机如果绑定不上,也可参考这些设置。在小米手机上,绑定成功后,我们把“允许应用自启动”关闭,会发现onServiceDisconnected就被执行了,当然,我们直接杀死服务端进程也会触发这个函数的执行。
关于第三方应用可见性,我们之前是通过配置包,也可通过指定Action的方式,即所有能处理此Action的应用对于我们都会变成可见的,如下:
通过这样设置,也可以使服务绑定成功。
可以监听系统启动,然后自动启动服务,但是从Android8.0开始,如果在后台启动服务,则此服务必须是foregroundService类型的才能启动:https://developer.android.google.cn/guide/components/foreground-services。
解决方案可以把目标版本设置为Android 7.0。
官方描述后台应用的限制:https://developer.android.google.cn/about/versions/oreo/background.html
从网上抄的一段:
Android 8.0 的应用尝试在不允许其创建后台服务的情况下使用 startService() 函数,则该函数将引发一个 IllegalStateException。 新的 Context.startForegroundService() 函数将启动一个前台服务。现在,即使应用在后台运行, 系统也允许其调用 Context.startForegroundService()。不过,应用必须在创建服务后的五秒内调用该服务的 startForeground() 函数。
解决方法:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent);
} else {
context.startService(intent);
}
2. 并且在service里再调用startForeground方法,不然就会出现ANR
context.startForeground(SERVICE_ID, builder.getNotification());
如果远程服务有启动Activity,可以在启动Activity中启动服务,这样的话就不需要设置“关联启动”或“自启动”权限,客户端可以直接绑定成功。所以这里有个想法,如果服务端真的不需要界面的话,也可以设置一个空Activity一启动后就立马启动Service,然后finishActivity。这样的话,我们的客户端可以调用启动服务端的Activity,然后再去绑定服务,这样似乎就不需要“关联启动”或“自启动”权限,且也不需要手动去启动服务端。