Service 基础总结

1.StartService可以跨进程吗通信?如果可以的话,它是如何做到的?

首先来看下这段代码, 进程A中点击某个按钮后,启动进程B中的服务:

Intent intent = new Intent();
ComponentName componentName = new ComponentName("com.example.进程B","com.example.进程B.MyService");
intent.setComponent(componentName);
startService(intent);

要启动 Service ,我们会调用 startService 方法,它在 ContextImpl.java 中实现,代码如下:

ContextImpl.java 客户端

@Override
public ComponentName startService(Intent service) {
    warnIfCallingFromSystemProcess();
    return startServiceCommon(service, false, mUser);
}

private ComponentName startServiceCommon(Intent service, boolean requireForeground,
        UserHandle user) {
    try {
        // 通过 IActivityManager 的接口调用 AMS
        ComponentName cn = ActivityManager.getService().startService(
            mMainThread.getApplicationThread(), service,
            service.resolveTypeIfNeeded(getContentResolver()), requireForeground,
            getOpPackageName(), user.getUserId());
        if (cn == null) {
            return null;
        }
        return cn;
    } catch (RemoteException e) {
        throw e.rethrowFromSystemServer();
    }
}

  • ActivityManager.getService() 获取一个 IActivityManager 接口的实例,该接口是与 AMS 通信的桥梁。
  • mMainThread.getApplicationThread() 获取客户端应用程序的 IApplicationThread 对象,用于与 AMS 进行回调。
  • ActivityManager.getService().startService(…) 这是一个跨进程调用,它将请求传递给 AMS。

** ActivityManagerService.java (服务端)😗*

@Override
public ComponentName startService(IApplicationThread caller, Intent service,
        String resolvedType, boolean requireForeground, String callingPackage, int userId) {
    // ... 权限检查 ...

    synchronized(this) {
        // ... 创建 ServiceRecord ...

        // 如果服务不在同一个进程中,则启动新的进程
        if (app == null) {
            // 启动新的进程
            startProcessLocked(appInfo.processName, appInfo, false, 0,
                    "service", component.flattenToShortString(), false, isolated, userId,
                    callerPackage, false, fgServiceTypes);

        }

        // ... 将启动服务的请求放入队列 ...
        bringUpServiceLocked(r, service.getFlags(), false, false, false);
    }

    return r.name;
}

private void bringUpServiceLocked(ServiceRecord r, int intentFlags, boolean execInFg,
        boolean oomAdjusted, boolean isRestart) throws TransactionTooLargeException {
    // ...
    realStartServiceLocked(r, app, execInFg);
}

private void realStartServiceLocked(ServiceRecord r, ProcessRecord app, boolean execInFg)
        throws RemoteException {
    // ...
    app.thread.scheduleCreateService(r, r.serviceInfo,
                    mAm.getCompatInfoForPackageLocked(r.serviceInfo.packageName,
                            r.userId), app.repProcState);
    // ...
}
  • startService 方法接收来自客户端的请求。
  • startProcessLocked 方法用于启动新的进程(如果需要)。
  • app.thread.scheduleCreateService 方法是一个跨进程调用,它将创建服务的请求发送到目标进程。 app.thread 是一个 IApplicationThread 对象,用于与客户端进程进行通信。

ActivityThread.java (服务端):

public final class ActivityThread {
    // ...

    private class ApplicationThread extends IApplicationThread.Stub {
        // ...
        @Override
        public final void scheduleCreateService(ServiceRecord token,
                ServiceInfo info, CompatibilityInfo compatInfo, int processState) {
            // ...
            sendMessage(H.CREATE_SERVICE, data);
        }
    }
}
  • ApplicationThread 是运行在应用程序进程中的一个类,它实现了 IApplicationThread 接口。
  • scheduleCreateService 方法接收来自 AMS 的创建服务的请求,并将请求放入消息队列中。

工作流程

  1. 客户端(Client):

客户端应用程序通过 Context.startService(Intent) 发起启动服务的请求。

客户端提供的 Intent 包含了目标服务的组件名(ComponentName),或者可以解析为目标服务的组件名。

  1. 系统服务(ActivityManagerService, AMS):

客户端的 startService 调用最终会传递给 ActivityManagerService (AMS),这是一个运行在 system_server 进程中的系统服务,负责管理应用程序的生命周期。

AMS 接收到请求后,会验证 Intent 和客户端的权限。

AMS 查找与 Intent 匹配的服务。 如果服务尚未运行,AMS 会创建一个新的服务进程(如果需要),并在该进程中启动服务。

如果服务已经运行在另一个进程中,AMS 会向该进程发送一个命令,告知它需要启动服务。

  1. 服务端(Service):

服务端进程(包含目标服务的进程)接收到 AMS 发送的命令。

Android 运行时环境会创建一个 Service 实例,并调用它的 onCreate() 和 onStartCommand() 方法。

  1. 跨进程调用(IPC):

即使 startService 方法返回了,客户端和服务端之间的通信并没有结束。 如果客户端需要与运行在服务端进程中的 Service 进行进一步的交互(例如,调用服务的方法),它通常会使用 bindService 方法绑定到服务。 绑定服务会创建一个 ServiceConnection 对象,该对象允许客户端接收来自服务端的 Binder 对象。

Binder 对象是一个实现了 IBinder 接口的类,它充当了客户端和服务端之间的桥梁。 通过 Binder 对象,客户端可以调用服务端的方法,并且可以将数据传递给服务端。

因此,startService可以跨进程,也就是说我们可以在进程B启动进程A中的Service。启动请求会传递给AMS,AMS查找与 Intent 匹配的服务。 如果服务尚未运行,AMS 会创建一个新的服务进程(如果需要),并在该进程中启动服务。如果服务已经运行在另一个进程中,AMS 会向该进程发送一个命令,告知它需要启动服务。

startService跨进程的缺点

1)没有好的机制立即返回执行结果,往往Service完成任务后,还需要其他方式向Client端反馈。

2)Service端无法识别Client端是谁,只知道有启动命令,但无法知道是谁下的命令。

3)在已经创建好Service情况下,每次调用startService,就会执行onStartCommand()生命周期方法,相比于bindService,效率低下。

4)如果Client端忘记调用stopService()了,那么该Service会一直运行下去,这也是一个隐患。

所以,针对上述缺点,往往建议startService()方式用于同一个App内

2.在Service所在进程未启动的情况下, bindService可以成功绑定吗?

首先来看下这段代码, 进程A中点击某个按钮后,绑定进程B中的服务:

// 进程 A
Intent intent = new Intent();
intent.setComponent(new ComponentName("com.example.appB", "com.example.appB.MyService"));
boolean success = bindService(intent, mConnection, Context.BIND_AUTO_CREATE);

要绑定 Service ,我们会调用 bindService 方法,它在 ContextImpl.java 中实现,代码如下:

ContextImpl.java 客户端

@Override
public boolean bindService(Intent service, ServiceConnection conn, int flags) {
    return bindServiceCommon(service, conn, flags, mUser);
}

private boolean bindServiceCommon(Intent service, ServiceConnection conn, int flags,
        UserHandle user) {
    try {
        // 通过 IActivityManager 接口调用 AMS
        int res = ActivityManager.getService().bindService(
            mMainThread.getApplicationThread(), getActivityToken(), service,
            service.resolveTypeIfNeeded(getContentResolver()),
            conn, flags, getOpPackageName(), user.getUserId());
        if (res < 0) {
            return false;
        }
        return true;
    } catch (RemoteException e) {
        throw e.rethrowFromSystemServer();
    }
}

与 startService 类似,bindService 也通过 ActivityManager.getService() 获取 IActivityManager 接口的实例,用于与 AMS 通信。

ActivityManagerService.java (服务端):

@Override
public int bindService(IApplicationThread caller, IBinder token, Intent service,
        String resolvedType, IServiceConnection connection, int flags, String callingPackage,
        int userId) {
    synchronized(this) {
        // ...
        ServiceRecord s = getServiceRecordLocked(service, callingPackage, userId, false, true);
        if (s == null) {
            // 找不到 ServiceRecord,可能是 Service 尚未启动

            if ((flags&Context.BIND_AUTO_CREATE) != 0) {
                // BIND_AUTO_CREATE 标志被设置,需要启动 Service
                 bringUpServiceLocked(s, service.getFlags(), false, false, false); //启动service
            } else {
                // 没有 BIND_AUTO_CREATE 标志,无法绑定
                return 0; //或其它错误码
            }
        }
         //关联service和client,并调用client端的onServiceConnected
         ...
    }
}

private void bringUpServiceLocked(ServiceRecord r, int intentFlags, boolean execInFg,
        boolean oomAdjusted, boolean isRestart) throws TransactionTooLargeException {
    // ...
    realStartServiceLocked(r, app, execInFg);
}

private void realStartServiceLocked(ServiceRecord r, ProcessRecord app, boolean execInFg)
        throws RemoteException {

    if (!app.hasStarted) { //启动进程A
         //startProcessLocked(appInfo.processName, appInfo, false, 0,
         //           "service", component.flattenToShortString(), false, isolated, userId,
         //           callerPackage, false, fgServiceTypes);
    }
    app.thread.scheduleCreateService(r, r.serviceInfo,
                    mAm.getCompatInfoForPackageLocked(r.serviceInfo.packageName,
                            r.userId), app.repProcState); //创建service
     ...
}

bindService 方法接收绑定请求。

关键在于 (flags&Context.BIND_AUTO_CREATE) != 0 的判断。 如果设置了 BIND_AUTO_CREATE 标志,并且 Service 尚未启动,则 AMS 会负责启动 Service。

如果 Service 之前没有运行,startProcessLocked()会尝试启动进程A,然后app.thread.scheduleCreateService会被调用,创建一个Service实例,并调用onCreate和onBind。

工作流程和依赖的条件:

  1. 进程 A 未启动: 进程 A 没有正在运行的任何组件(Activity、Service、BroadcastReceiver 等)。

  2. 进程 B 尝试绑定: 进程 B 调用 bindService(Intent, ServiceConnection, int),其中 Intent 指向进程 A 中的 Service。

  3. AMS 的处理:

    • ActivityManagerService (AMS) 接收到绑定请求。

    • 由于进程 A 尚未运行,AMS 会启动进程 A。

    • 在进程 A 中,Android 运行时环境会创建 Application 实例(如果尚未创建),并初始化该进程。

    • 然后,AMS 会尝试创建 Service 的实例,并调用它的 onCreate() 方法。

    关键点: 如果绑定标志包含 BIND_AUTO_CREATE (或者使用默认值),则在进程 A 中的 Service 未启动时,系统会先创建 Service 的实例。如果没有BIND_AUTO_CREATE, 且Service未启动,那么绑定会失败。

  4. Service 的 onBind 方法:

    • 在 Service 的 onCreate() 方法之后,系统会调用 Service 的 onBind(Intent) 方法。 onBind() 方法返回一个 IBinder 对象,该对象是进程 A 和进程 B 之间进行通信的桥梁。

    • 如果 onBind() 返回 null,则绑定将失败。

  5. ServiceConnection 的回调:

    • 进程 B 中的 ServiceConnection 对象会收到回调:

      • 如果绑定成功,则会调用 onServiceConnected(ComponentName, IBinder) 方法,并将 onBind() 方法返回的 IBinder 对象传递给进程 B。

      • 如果绑定失败,则会调用 onServiceDisconnected(ComponentName) 方法。

  6. 后续通信: 进程 B 可以使用 IBinder 对象来调用进程 A 中的 Service 的方法。

3.bindService跨进程通信所遇到的一些问题

02-08 18:24:10.867 W/OplusAppStartupManager( 1745): isAllowStartFromStartService: prevent start com.seres.smartscene5 , intent = Intent { act=android.intent.action.STANDARD_SERVICE cmp=com.seres.smartscene5/cn.seres.alps.service.BluetoothService } by cn.seres.starryskypanel
02-08 18:24:10.869 W/ALPS.SERVICE(10222): startConnect: [bindService] result false

OplusAppStartupManager 阻止了应用 cn.seres.starryskypanel 启动 com.seres.smartscene5 应用中的 BluetoothService。 这很可能是因为 Oppo 手机上限制了后台服务的启动,以节省电量或提高性能。

解决方案

进入 “设置” → “应用管理” → “权限管理” → “自启动管理”
允许 com.seres.smartscene5 和 cn.seres.starryskypanel 自启动
允许 “后台运行权限”

Done dumping
02-08 19:31:46.948 E/ActivityManager( 1745): ANR in com.seres.smartscene5
02-08 19:31:46.948 E/ActivityManager( 1745): PID: 1067
02-08 19:31:46.948 E/ActivityManager( 1745): Reason: Context.startForegroundService() did not then call Service.startForeground(): ServiceRecord{bae1474 u0 com.seres.smartscene5/cn.seres.alps.service.BluetoothService}
02-08 19:31:46.948 E/ActivityManager( 1745): Frozen: false

从 Android 8.0 (API level 26) 开始,对后台服务启动进行了更严格的限制。 如果一个服务需要在后台长时间运行,并且需要优先级别,那么它必须声明为前台服务 (Foreground Service)。

解决方案:

public class BluetoothService extends Service {

    private static final String CHANNEL_ID = "BluetoothServiceChannel";
    private static final int NOTIFICATION_ID = 1;

    @Override
    public void onCreate() {
        super.onCreate();

        // 创建通知渠道 (Android 8.0 及以上)
        createNotificationChannel();

        // 创建通知
        Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID)
                .setContentTitle("蓝牙服务")
                .setContentText("正在后台运行...")
                .setSmallIcon(android.R.drawable.ic_menu_manage) // 替换为你的应用图标
                .build();

        // 启动前台服务
        startForeground(NOTIFICATION_ID, notification);
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        // 这里执行你的蓝牙服务逻辑
        return START_STICKY; // 根据你的需求选择合适的返回值
    }

    @Override
    public IBinder onBind(Intent intent) {
        return null; // 如果不需要绑定,返回 null
    }

    private void createNotificationChannel() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            NotificationChannel serviceChannel = new NotificationChannel(
                    CHANNEL_ID,
                    "Bluetooth Service Channel",
                    NotificationManager.IMPORTANCE_DEFAULT
            );

            NotificationManager manager = getSystemService(NotificationManager.class);
            manager.createNotificationChannel(serviceChannel);
        }
    }
}

AndroidManifes.xml:

Service所在的进程A中:

 <uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <!-- 添加此权限 -->
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
    <uses-permission android:name="android.permission.BLUETOOTH"
        android:maxSdkVersion="30" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"
        android:maxSdkVersion="30" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
    <uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    
 <service
            android:name="cn.seres.alps.service.BluetoothService"
            android:enabled="true"
            android:exported="true"
            android:foregroundServiceType="connectedDevice|location">
            <intent-filter>
                <action android:name="android.intent.action.STANDARD_SERVICE" />
            </intent-filter><!-- 允许其他应用绑定 -->
        </service>

android:foregroundServiceType="connectedDevice|location|bluetooth": 重要! 从 Android 10 (API level 29) 开始,如果你的前台服务用于特定目的,你必须在 android:foregroundServiceType 属性中声明这些目的。 可用的类型包括:

  • connectedDevice: 与蓝牙、USB 设备连接或通信。
  • location: 访问设备位置信息。
  • microphone: 访问设备麦克风。
  • camera: 访问设备摄像头。
  • health: 执行与健康相关的任务。
  • remoteMessaging: 处理远程消息传递。
  • bluetooth: 执行常规的蓝牙操作。
  • dataSync: 同步数据。
  • mediaPlayback: 播放媒体。
  • mediaProjection: 屏幕投影。

添加了connectedDevice和location,需要添加权限:
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />

进程B中:

<queries>
        <package android:name="com.seres.smartscene5"/>
        <intent>
            <action android:name="android.intent.action.STANDARD_SERVICE" />
        </intent><!-- 允许其他应用绑定 -->
    </queries>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值