VirtualApp之Service

1 系统AIDL服务与ServiceManger

1.1 启动

  • ServiceManager是Binder IPC通信过程中的守护进程,本身也是一个Binder服务,用来管理系统Service组件。主要用来查询和注册服务。并且向Client组件提供获取Service代理对象的服务。
  • ServiceManager运行在一个独立的进程中,Service组件与Client组件需要Binder进程间通信机制对其访问。
  • ServiceManger由init进程负责启动。

1.2 注册与查询

服务注册查询.png
  • 由于SM的句柄值为0,获取SM代理对象省去了与Binder驱动的交互,进程可直接根据句柄创建SM代理对象。
  • SystemServer进程启动AMS时,调用addService将AMS注册到ServiceManger。
  • App进程想要访问AMS,需要调用getService向ServiceManger获取AMS的代理对象。
  • 最后App进程通过AMS的代理对象访问AMS。

2 VA中的VAMS、VPMS等AIDL服务

VA实现对内部AIDL Service的管理,其设计思想与系统基本一致。

VA系统服务.png
  • ServiceFetcher是一个Binder对象,VA中的作用类似系统的ServiceManger,用来缓存VAMS、VPMS等AIDL Service的Binder本地对象。
  • ServiceFetcher与VActivityMangerService的Binder本地对象都存活在VA:X进程,addService过程不再跨进程,直接缓存。
  • Android系统获取SM代理对象的过程,改为了使用ContentProvider.call(),App进程可以从返回值Bundle中获取ServiceFetcher代理对象。
  • App进程调用ServiceFetcher.getService()获取VAMS的代理对象,才能跨进程访问VAMS。

2.1 启动、注册服务

public final class BinderProvider extends ContentProvider {

    private final ServiceFetcher mServiceFetcher = new ServiceFetcher();
    ...
    private boolean init() {
        ...
        addService(ServiceManagerNative.ACTIVITY, VActivityManagerService.get());
        addService(ServiceManagerNative.USER, VUserManagerService.get());
        ...
        return true;
    }

    private void addService(String name, IBinder service) {
        ServiceCache.addService(name, service);
    }

    @Override
    public Bundle call(String method, String arg, Bundle extras) {
        if (!sInitialized) {
            init();
        }
        if ("@".equals(method)) {
            Bundle bundle = new Bundle();
            BundleCompat.putBinder(bundle, "_VA_|_binder_", mServiceFetcher);
            return bundle;
        }
        return null;
    }

      private class ServiceFetcher extends IServiceFetcher.Stub {
        @Override
        public IBinder getService(String name) throws RemoteException {
            if (name != null) {
                return ServiceCache.getService(name);
            }
            return null;
        }

        @Override
        public void addService(String name, IBinder service) throws RemoteException {
            if (name != null && service != null) {
                ServiceCache.addService(name, service);
            }
        }

        @Override
        public void removeService(String name) throws RemoteException {
            if (name != null) {
                ServiceCache.removeService(name);
            }
        }
    }
}

2.2 获取VAMS代理对象

流程图.png
public class VActivityManager {

    private static final VActivityManager sAM = new VActivityManager();
    private IActivityManager mService;

    public IActivityManager getService() {
        if (!IInterfaceUtils.isAlive(mService)) {
            synchronized (VActivityManager.class) {
                final Object remote = getRemoteInterface();
                mService = LocalProxyUtils.genProxy(IActivityManager.class, remote);
            }
        }
        return mService;
    }

    private Object getRemoteInterface() {
        return IActivityManager.Stub
                .asInterface(ServiceManagerNative.getService(ServiceManagerNative.ACTIVITY));
    }
}


public class ServiceManagerNative {

    public static final String SERVICE_DEF_AUTH = "virtual.service.BinderProvider";
    private static final String TAG = ServiceManagerNative.class.getSimpleName();
    public static String SERVICE_CP_AUTH = "virtual.service.BinderProvider";

    private static IServiceFetcher sFetcher;

    private static String getAuthority() {
        return VirtualCore.getConfig().getBinderProviderAuthority();
    }

    private static IServiceFetcher getServiceFetcher() {
        if (sFetcher == null || !sFetcher.asBinder().isBinderAlive()) {
            synchronized (ServiceManagerNative.class) {
                Context context = VirtualCore.get().getContext();
                Bundle response = new ProviderCall.Builder(context, getAuthority()).methodName("@").callSafely();
                if (response != null) {
                    IBinder binder = BundleCompat.getBinder(response, "_VA_|_binder_");
                    linkBinderDied(binder);
                    sFetcher = IServiceFetcher.Stub.asInterface(binder);
                }
            }
        }
        return sFetcher;
    }

    public static IBinder getService(String name) {
        if (VirtualCore.get().isServerProcess()) {
            return ServiceCache.getService(name);
        }
        IServiceFetcher fetcher = getServiceFetcher();
        if (fetcher != null) {
            try {
                return fetcher.getService(name);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
        VLog.e(TAG, "GetService(%s) return null.", name);
        return null;
    }
}

3 Service的生命周期

生命周期图.png
  • onCreate()首次创建服务时,系统将调用此方法。如果服务已在运行,则不会调用此方法,该方法只调用一次。
  • onStartCommand()当另一个组件通过调用startService()请求启动服务时,系统将调用此方法。
  • onDestroy()当服务不再使用且将被销毁时,系统将调用此方法。
  • onBind()当另一个组件通过调用bindService()与服务绑定时,系统将调用此方法。
  • onUnbind()当另一个组件通过调用unbindService()与服务解绑时,系统将调用此方法。
  • onRebind()当旧的组件与服务解绑后,另一个新的组件与服务绑定,onUnbind()返回true时,系统将调用此方法。
生命周期流程图.png

A:onCreate()
B:onStartCommand()
C:onDestroy()
D:onBind()
E:onUnbind()
F:onRebind()

顺序 结果
startService->stopService A-B-C
bindService->unbindService A-D-E-C
startService->startService A-B-B
startService->bindService->stopService A-B-D
startService->bindService->unbindService A-B-D-E
bindService->startService->unbindService A-D-B-E
startService->bindService->unbindService->bindService A-B-D-E(true)-F

4 VA拦截startService

4.1 startService的流程

Service的生命周期比较简单,没有界面和交互,跟其他Framework Service没有任何关系,所以不需要告知AMS,生命周期部分绕过AMS完全由VA控制,最后直接调用ApplicationThread(VClient)处理,这跟Activity占坑的实现方式不同。VA尝试使用过Service占坑的方法,由AMS启动占坑的Service,再由占坑Service去控制真正Service的生命周期,这种方法会经过系统AMS控制,生命周期如果没有正常返回会出现ANR,例如VA中运行微信7.0版本偶现ANR。

startService流程.png

4.1.1 Service的ANR

  • 简单来说service调用onCreate或者onStartCommand之前会延迟发送一个timeout消息,延时为20s或者200s,然后执行完了之后会移除掉该timeout消息,如果不能及时移除,则处理timeout消息,导致service anr。
  • 所以Service的生命周期中不能做耗时操作,生命周期必须正常返回,才能在serviceDoneExecutingLocked中移除timeout消息。
  • 参考https://blog.csdn.net/sinat_20059415/article/details/80997425

4.2 startService相关的代码

4.2.1 MethodProxies$StartService

  • VA:X进程也Hook了AM,该进程调用startService,直接跳过走系统。
  • 如果VA内查不到该要启动服务,判断isVisiblePackage,会去VA外应用白名单中查找是否有该服务。
static class StartService extends MethodProxy {

        @Override
        public Object call(Object who, Method method, Object... args) throws Throwable {
            ...
            if (service.getComponent() != null
                    && getHostPkg().equals(service.getComponent().getPackageName())) {
                return method.invoke(who, args);
            }
            ...
            ServiceInfo serviceInfo = VirtualCore.get().resolveServiceInfo(service, VUserHandle.myUserId());
            if (serviceInfo != null) {
                return VActivityManager.get().startService(service, resolvedType, userId);
            }
            ResolveInfo resolveInfo = VirtualCore.get().getUnHookPackageManager().resolveService(service, 0);
            if (resolveInfo == null || !isVisiblePackage(resolveInfo.serviceInfo.applicationInfo)) {
                return null;
            }
            return method.invoke(who, args);
        }

4.2.2 VAMS.startServiceCommonLocked

  • r.startId用来记录调用startService的个数,如果unbindService时判断startId=0,才会stopService。
  • scheduleCreateService只被调用一次,scheduleServiceArgs每次startService都被调用。
  • 当启动Service时,调用startShadowService用来提高进程优先级,这点单独介绍。
private ComponentName startServiceCommonLocked(Intent service,
                                                   boolean scheduleServiceArgs, int userId) {
        ServiceInfo serviceInfo = VirtualCore.get().resolveServiceInfo(service, userId);
        if (serviceInfo == null) {
            return null;
        }
        ProcessRecord targetApp = startProcessIfNeedLocked(ComponentUtils.getProcessName(serviceInfo),
                userId,
                serviceInfo.packageName, -1, VBinder.getCallingUid());

        if (targetApp == null) {
            VLog.e(TAG, "Unable to start new process (" + ComponentUtils.toComponentName(serviceInfo) + ").");
            return null;
        }
        ServiceRecord r;
        synchronized (mHistory) {
            r = findRecordLocked(userId, serviceInfo);
        }
        if (r == null) {
            r = new ServiceRecord();
            r.startId = 0;
            r.activeSince = SystemClock.elapsedRealtime();
            r.process = targetApp;
            r.serviceInfo = serviceInfo;
            try {
                targetApp.client.scheduleCreateService(r, r.serviceInfo);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
            startShadowService(targetApp);
            addRecord(r);
        }
        r.lastActivityTime = SystemClock.uptimeMillis();
        if (scheduleServiceArgs) {
            r.startId++;
            try {
                targetApp.client.scheduleServiceArgs(r, r.startId, service);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }

        return ComponentUtils.toComponentName(serviceInfo);
    }

4.2.3 VClient.handleCreateService

  • 如果是startService启动的进程,该进程还没有跟app绑定,调用bindApplication完成绑定
  • 从LoadedApk中拿到应用的ClassLoader来创建Service的实例
  • 创建一个ContextImpl对象,这个context是VA的context,调用service.attach将ContextImpl设置到ContextWrapper的mBaseContext变量中,这样service就有能力调用context的接口。
  • VA的context.mPackageManager是没有被Hook的,调用fixContext先将ContextImpl.mPackageManager置空,然后调用getPackageManager重新设置被Hook的PM。
private void handleCreateService(CreateServiceData data) {
        ServiceInfo info = data.info;
        if (!isAppRunning()) {
            bindApplication(info.packageName, info.processName);
        }
        ClassLoader classLoader = LoadedApk.getClassLoader.call(mBoundApplication.info);
        Service service;
        try {
            service = (Service) classLoader.loadClass(info.name).newInstance();
        } catch (Exception e) {
            throw new RuntimeException(
                    "Unable to instantiate service " + info.name
                            + ": " + e.toString(), e);
        }
        try {
            Context context = VirtualCore.get().getContext().createPackageContext(
                    data.info.packageName,
                    Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY
            );
            ContextImpl.setOuterContext.call(context, service);
            mirror.android.app.Service.attach.call(
                    service,
                    context,
                    VirtualCore.mainThread(),
                    info.name,
                    clientConfig.token,
                    mInitialApplication,
                    ActivityManagerNative.getDefault.call()
            );
            ContextFixer.fixContext(service);
            service.onCreate();
            mServices.put(data.token, service);
            VActivityManager.get().serviceDoneExecuting(data.token, SERVICE_DONE_EXECUTING_ANON, 0, 0);
        } catch (Exception e) {
            throw new RuntimeException(
                    "Unable to create service " + data.info.name
                            + ": " + e.toString(), e);
        }
    }

4.2.4 VA使用ShadowService提高进程优先级

  • VA的AndroidManifest.xml中定义了100个ShadowService,每个进程对应一个ShadowService。
  • 如果一个进程以startService的方法启动,那么这个进程的优先级属于服务进程,进程的优先级:前台进程>可见进程>服务进程>后台进程
  • Service的优先级被默认后台任务,如果bindService设置了BIND_AUTO_CREATE则Service的优先级将等同于宿主进程,也就是调用bindService的进程。
  • 也就是说VA:X进程去调用bindService,绑定了启动进程的ShadowService,进程优先级提高到了与VA:X进程(前台进程)相同。
  • 例如在VA中打开wps应用,wps调用startService启动了cn.wps.moffice_eng:pushservice和cn.wps.moffice_eng:scan进程,如果不调用startShadowService,使用cat /proc/pid/oom_adj看到这两个进程的优先级都是11,如果调用startShadowService,这两个进程的优先级都是0,跟VA:X进程一致。
private void startShadowService(ProcessRecord app) {
        String serviceName = StubManifest.getStubServiceName(app.vpid);
        Intent intent = new Intent();
        intent.setClassName(StubManifest.getStubPackageName(app.is64bit), serviceName);
        try {
            VirtualCore.get().getContext().bindService(intent, app.conn, Context.BIND_AUTO_CREATE);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

5 VA拦截bindService

5.1 举例说明bindService

  • bindService()目的是回调onBind方法,作用是在Service和调用者之间建立一个桥梁,
  • ServiceConnection是一个interface,会将ServiceConnection封装成一个实现了IServiceConnection接口的Binder本地对象InnerConnection,调用bindService将InnerConnection的代理对象传给AMS,最后AMS会用InnerConnection给App进程传递LocalService的代理对象。


    bindService.png
public class BindingActivity extends Activity {
      LocalService mService;
 
      protected void onCreate(Bundle savedInstanceState) {
        Intent intent = new Intent(this, LocalService.class);
        bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
    }   
      private ServiceConnection mConnection = new ServiceConnection() {   
        public void onServiceConnected(ComponentName className, IBinder service) {
            LocalBinder binder = (LocalBinder) service;
            mService = binder.getService();
         }      
   };
}

public class LocalService extends Service {
    private final IBinder mBinder = new LocalBinder();
    private final Random mGenerator = new Random(); 
     
    public class LocalBinder extends Binder {
        LocalService getService() {
            return LocalService.this;
        }
    }   
    public IBinder onBind(Intent intent) {
        return mBinder;
    }   
}

5.2 VA中bindService的流程

  • VA中bindService的流程与系统相似,只不过给ServiceConnect多封装了一层代理ServiceConnectDelegate,给onBind返回的Service的代理对象也封装了一层代理BinderDelegateService。
bindService流程.png

5.2.1 ServiceConnectionDelegate

  • 每个进程可能会有多个组件想要绑定服务,DELEGATE_MAP用来缓存多个IServiceConnection本地对象与ServiceDispatcher(LoadedApk.getServiceDispatcher的返回值)的对应关系
  • 每一个绑定过Service组件的Activity组件在LoadedApk中都有一个对应的ServiceDispatcher对象,它负责将这个被绑定的service组件与绑定它的Actvity组件关联起来,
public class ServiceConnectionDelegate extends IServiceConnection.Stub {
    private final static ArrayMap DELEGATE_MAP = new ArrayMap<>();
    private IServiceConnection mConn;

    private ServiceConnectionDelegate(IServiceConnection mConn) {
        this.mConn = mConn;
    }

    public static IServiceConnection getDelegate(Context context, ServiceConnection connection, int flags) {
        IServiceConnection sd = null;
        if (connection == null) {
            throw new IllegalArgumentException("connection is null");
        }
        try {
            Object activityThread = ActivityThread.currentActivityThread.call();
            Object loadApk = ContextImpl.mPackageInfo.get(VirtualCore.get().getContext());
            Handler handler = ActivityThread.getHandler.call(activityThread);
            sd = LoadedApk.getServiceDispatcher.call(loadApk, connection, context, handler, flags);
        } catch (Exception e) {
            Log.e("ConnectionDelegate", "getServiceDispatcher", e);
        }
        if (sd == null) {
            throw new RuntimeException("Not supported in system context");
        }
        return getDelegate(sd);
    }

   
    public static ServiceConnectionDelegate getDelegate(IServiceConnection conn) {
        if (conn instanceof ServiceConnectionDelegate) {
            return (ServiceConnectionDelegate) conn;
        }
        IBinder binder = conn.asBinder();
        ServiceConnectionDelegate delegate = DELEGATE_MAP.get(binder);
        if (delegate == null) {
            delegate = new ServiceConnectionDelegate(conn);
            DELEGATE_MAP.put(binder, delegate);
        }
        return delegate;
    }
 
    @Override
    public void connected(ComponentName name, IBinder service) throws RemoteException {
        connected(name, service, false);
    }

    public void connected(ComponentName name, IBinder service, boolean dead) throws RemoteException {
        IBinderDelegateService delegateService = IBinderDelegateService.Stub.asInterface(service);
        if (delegateService != null) {
            name = delegateService.getComponent();
            service = delegateService.getService();
            IBinder proxy = ProxyServiceFactory.getProxyService(VClient.get().getCurrentApplication(), name, service);
            if (proxy != null) {
                service = proxy;
            }
        }

        if (BuildCompat.isOreo()) {
            IServiceConnectionO.connected.call(mConn, name, service, dead);
        } else {
            mConn.connected(name, service);
        }
    }
}

5.2.2 ServiceRecord和IntentBindRecord

  • ServiceRecord.IntentBindRecord用来描述跟这个Service绑定的应用进程,而且以Filter为关键字,保存在ServiceRecord.bindings中。
  • 由于一个Service组件可以被同个应用进程的多个Activity组件绑定,这些IServiceConnection都被放到IntentBindRecord.connections列表中。
  • doRebind,Service的onUnbind()的返回值为true时,将通过AMS的unbindFinished()传给AMS,记录在doRebind标志位中。下一次再有client绑定服务时,将根据doRebind的值来决定是调用onBind(),还是调用onRebind()。
public class ServiceRecord extends Binder {
    final List bindings = new ArrayList<>();
    long activeSince;
    long lastActivityTime;
    ServiceInfo serviceInfo;
    int startId;  //记录了调用startService的次数
    ProcessRecord process;
    int foregroundId;
    Notification foregroundNoti;

    public boolean containConnection(IServiceConnection connection) {
        synchronized (bindings) {
            for (IntentBindRecord record : bindings) {
                if (record.containConnectionLocked(connection)) {
                    return true;
                }
            }
        }
        return false;
    }

    IntentBindRecord peekBindingLocked(Intent service) {
        for (IntentBindRecord bindRecord : bindings) {
            if (bindRecord.intent.filterEquals(service)) {
                return bindRecord;
            }
        }
        return null;
    }

    void addToBoundIntentLocked(Intent intent, IServiceConnection connection) {
        IntentBindRecord record = peekBindingLocked(intent);
        if (record == null) {
            record = new IntentBindRecord();
            record.intent = intent;
            bindings.add(record);
        }
        record.addConnectionLocked(connection);
    }
    }

    static class IntentBindRecord {
        final List connections = new ArrayList<>();
        IBinder binder;
        Intent intent;  //service intent
        boolean doRebind = false;
       ...
    }
}

5.2.3 VAMS.bindService

  • 首先向VPMS中差找到ServiceInfo,在查找ServiceRecord,如果不存在,调用startServiceCommonLocked先启动Service,并且生命周期只调用onCreate,不调用onStartCommand。
  • ServiceRecord中记录有ProcessRecord,调用scheduleBindService到应用进程。
  • VA拦截了UnbindFinished(MethodProxies),boundRecord.doRebind的值在这里被赋值。
  • 如果boundRecord非空,说明Service已经被绑定过,不再执行onBind()生命周期,直接调用connectServiceLocked去连接。
  • addToBoundIntentLocked保存IntentBindRecord和IServiceConnection。
    public int bindService(IBinder caller, IBinder token, Intent service, String resolvedType,
                           IServiceConnection connection, int flags, int userId) {
        synchronized (this) {
            ServiceInfo serviceInfo = VirtualCore.get().resolveServiceInfo(service, userId);
            if (serviceInfo == null) {
                return 0;
            }
            ServiceRecord r;
            synchronized (mHistory) {
                r = findRecordLocked(userId, serviceInfo);
            }
            boolean firstLaunch = r == null;
            if (firstLaunch) {
                if ((flags & Context.BIND_AUTO_CREATE) != 0) {
                    startServiceCommonLocked(service, false, userId);
                    synchronized (mHistory) {
                        r = findRecordLocked(userId, serviceInfo);
                    }
                }
            }
            if (r == null) {
                return 0;
            }
            synchronized (r.bindings) {
                ServiceRecord.IntentBindRecord boundRecord = r.peekBindingLocked(service);
                if (boundRecord != null && boundRecord.binder != null && boundRecord.binder.isBinderAlive()) {
                    if (boundRecord.doRebind) {
                        try {
                            r.process.client.scheduleBindService(r, service, true);
                        } catch (RemoteException e) {
                            e.printStackTrace();
                        }
                    }
                    ComponentName componentName = new ComponentName(r.serviceInfo.packageName, r.serviceInfo.name);
                    connectServiceLocked(connection, componentName, boundRecord, false);
                } else {
                    try {
                        r.process.client.scheduleBindService(r, service, false);
                    } catch (RemoteException e) {
                        e.printStackTrace();
                    }
                }
                r.lastActivityTime = SystemClock.uptimeMillis();
                r.addToBoundIntentLocked(service, connection);
            }

            return 1;
        }
    }

5.2.4 VClient.handleBindService

  • 根据rebind的值判断调用onBind或者onRebind
  • serviceDoneExecuting更新VAMS中缓存的ServiceRecord
 private void handleBindService(BindServiceData data) {
        Service s = mServices.get(data.token);
        if (s != null) {
            try {
                data.intent.setExtrasClassLoader(s.getClassLoader());
                if (!data.rebind) {
                    IBinder binder = s.onBind(data.intent);
                    VActivityManager.get().publishService(data.token, data.intent, binder);
                } else {
                    s.onRebind(data.intent);
                    VActivityManager.get().serviceDoneExecuting(
                            data.token, SERVICE_DONE_EXECUTING_ANON, 0, 0);
                }
            } catch (Exception e) {
                throw new RuntimeException(
                        "Unable to bind to service " + s
                                + " with " + data.intent + ": " + e.toString(), e);
            }
        }
    }

6 VA:X进程的保活

  • VA在启动BinderProvider时,启动的KeepAliveService服务,虽然VA:X进程Hook了AM,但startService过滤掉了对VA服务的处理,所以走正常系统流程
  • KeepAliveService启动一个HiddenForeNotification,两个服务同时startForeground,且绑定同样的 ID。Stop 掉HiddenForeNotification ,这样保证了startForeground设置前台服务,而且通知栏图标也被移除。
  • 参考方案二:https://blog.csdn.net/qq_37199105/article/details/81224842
public class KeepAliveService extends Service {

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        if(!VirtualCore.getConfig().isHideForegroundNotification()) {
            HiddenForeNotification.bindForeground(this);
        }
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        return START_STICKY;
    }
}

public class HiddenForeNotification extends Service {
    private static final int ID = 2781;

    public static void bindForeground(Service service) {
        Builder builder = NotificationChannelCompat.createBuilder(service.getApplicationContext(),
                NotificationChannelCompat.DAEMON_ID);
        builder.setSmallIcon(android.R.drawable.ic_dialog_dialer);
        if (VERSION.SDK_INT > 24) {
            builder.setContentTitle(service.getString(R.string.keep_service_damon_noti_title_v24));
            builder.setContentText(service.getString(R.string.keep_service_damon_noti_text_v24));
        } else {
            builder.setContentTitle(service.getString(R.string.keep_service_damon_noti_title));
            builder.setContentText(service.getString(R.string.keep_service_damon_noti_text));
            builder.setContentIntent(PendingIntent.getService(service, 0, new Intent(service, HiddenForeNotification.class), 0));
        }
        builder.setSound(null);
        service.startForeground(ID, builder.getNotification());
        if (VERSION.SDK_INT <= 24) {
            service.startService(new Intent(service, HiddenForeNotification.class));
        }
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        try {
            Builder builder = NotificationChannelCompat.createBuilder(getBaseContext(),
                    NotificationChannelCompat.DAEMON_ID);
            builder.setSmallIcon(android.R.drawable.ic_dialog_dialer);
            builder.setContentTitle(getString(R.string.keep_service_noti_title));
            builder.setContentText(getString(R.string.keep_service_noti_text));
            builder.setSound(null);
            startForeground(ID, builder.getNotification());
            stopForeground(true);
            stopSelf();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return START_NOT_STICKY;
    }

    public IBinder onBind(Intent intent) {
        return null;
    }
}

参考

https://www.jianshu.com/p/ee224f18a4bd
https://blog.csdn.net/qq_37199105/article/details/81224842
https://blog.csdn.net/sinat_20059415/article/details/80997425

你可能感兴趣的:(VirtualApp之Service)