您当前的位置:首页 > 计算机 > 编程开发 > 安卓(android)开发

android 绑定远程服务

时间:02-07来源:作者:点击数:

创建远程服务(即被绑定的服务)

完整示例代码:https://gitee.com/daizhufei/remote-service.git

  1. 创建一个新的项目,然后把清单文件中的Activity注释掉(保留Activity也可以,但是我这里想演示一下不要Activity的服务以及相关的问题),如下:
    <?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>
    
  2. 右击模块名称,创建aidl目录:New > Folder > AIDL Folder,如下:
    在这里插入图片描述
  3. 在aidl目录中创建包名,以及创建对应的aidl类,如下:
    在这里插入图片描述
    在这里插入图片描述
    如上图,通过模板生成了一个名为IDzEngine的aidl类,类名一般以大写字母I开关,表示这是一个接口,类中自动生成了一个basicType函数,这是一个模板函数,此函数用于演示AIDL参数可以使用的基本类型,以及AIDL函数可以返回的基本类型,我们可以删除此函数,创建自己的函数,如下:
    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文件如下:
    在这里插入图片描述
    可以看到,IDzEngine中有一个抽象的子类:IDzEngine.Stub,且里面有一个IDzEngine.Stub.asInterface(android.os.IBinder obj)的静态方法,后面会用到。
  4. 创建服务类,如下:
    在这里插入图片描述
    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.html

OK,到此,服务端代码我们就写好了。

创建客户端

  1. 创建一个client应用,然后把服务端的aidl整个文件夹复制过来,然后再执行菜单:Build > Recomplile ‘IDzEngine.aidl’,让其自动生成IDzEngine.java,如下:
    在这里插入图片描述
  2. 在清单文件中声明权限:“android.permission.ACCESS_LOCATION_EXTRA_COMMANDS”
  3. 在MainActivity中进行服务绑定操作,如下:
        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,到这里客户端的代码就完成了。

测试

  1. 先安装服务端,我们的服务端写在了app模块中,本来是有一个Activity的,被我们注释掉了,则运行的时候,我们需要配置不运行任何的Activity,在工具栏中点开“Edit Configurations…”,如下:
在这里插入图片描述
在这里插入图片描述

如上图,默认为“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() 函数。

解决方法:

  1. 修改启动方式

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());

https://stackoverflow.com/questions/46445265/android-8-0-java-lang-illegalstateexception-not-allowed-to-start-service-inten

注意事项

如果远程服务有启动Activity,可以在启动Activity中启动服务,这样的话就不需要设置“关联启动”或“自启动”权限,客户端可以直接绑定成功。所以这里有个想法,如果服务端真的不需要界面的话,也可以设置一个空Activity一启动后就立马启动Service,然后finishActivity。这样的话,我们的客户端可以调用启动服务端的Activity,然后再去绑定服务,这样似乎就不需要“关联启动”或“自启动”权限,且也不需要手动去启动服务端。

方便获取更多学习、工作、生活信息请关注本站微信公众号城东书院 微信服务号城东书院 微信订阅号
推荐内容
相关内容
栏目更新
栏目热门