06、Android--Service服务 Service IntentService 前台服务 远程服务

Service声明

Service的启动方式上,可以将Service分为Started Service和Bind Service。无论哪种具体的Service启动类型,都是通过继承Service基类自定义而来。在使用Service时,要想系统

能够找到此自定义Service,无论哪种类型,都需要在AndroidManifest.xml中声明,语法格式如下:

<service android:enabled=["true" | "false"]
     android:exported=["true" | "false"]
     android:icon="drawable resource"
     android:isolatedProcess=["true" | "false"]
     android:label="string resource"
     android:name="string"
     android:permission="string"
     android:process="string" >
</service>

上述使用到的属性说明在下面的表格:

模式 描述
enable Service是否能被系统初始化,默认为true。
exported 其他应用能否访问该服务,如果不能,则只有本应用或有相同用户ID的应用能访问。
icon 类似 label ,是图标,尽量用 drawable 资源的引用定义。
isolatedProcess 设置 true 意味着,服务会在一个特殊的进程下运行,这个进程与系统其他进程分开且没有自己的权限。
lable 可以显示给用户的服务名称。如果没设置,就用 <application>lable
name 你所编写的服务类的类名,可填写完整名称,包名+类名
permission 标识此Service的调用权限,如果没有设置,则默认使用的权限设置。
process 服务运行所在的进程名。通常为默认为应用程序所在的进程,与包名同名。

在清单文件进行注册之后,创建一个类继承Service并实现onBind()方法:

public class CustomService extends Service {
    @Override
    public IBinder onBind(Intent intent) {
        throw new UnsupportedOperationException("Not yet implemented");
    }
}

Service基础

开启Service有两种方式:start Service(标准启动) 和 bound Serive(绑定启动)。

Start Service

标准开启的Service会长期在后台运行,但是此时Activity是不能调用服务中的方法。

Intent service = new Intent(MainActivity.this, CustomService.class);
context.startService(service);

如果想停止Service则调用stopService(intent)即可,当然Service也可以调用stopSelf()来停止自身。

Bind Service

绑定开启Service时,Activity可以调用服务中的方法,但是此时服务是不能长期在后台运行。

Intent service = new Intent(MainActivity.this, CustomService.class);
/**
 * service:         intent意图
 * ServiceConnection:   绑定开启的代理对象
 * BIND_AUTO_CREATE:    服务如果在绑定的时候不存在,会自动创建
 */
bindService(service, new MyServiceConnection(), Context.BIND_AUTO_CREATE);

绑定开启服务需要注意在Service中的onBind()方法必须有Bind的实现类的当做返回值,否则会报错。

Mix Service

mix Service(混合启动)来启动。它具备标准启动和绑定启动的共同优点,此时服务即可长期在后台运行,Activity也可以调用服务中的方法。

1、首先通过标准模式启动服务,这样服务就长期在后台运行。
2、如果需要调用服务中的方法,则再使用绑定模式绑定服务。
3、如果需要解绑服务则调用unbindService()解绑服务。
4、如果需要停止服务,则调用stopService()停止服务。

注:有时候我们解绑服务后,发现还是可以调用服务中的方法,是因为垃圾回收器还没有回收调用该方法的对象。

生命周期

Service的生命周期根据不同的启动方式而不同,具体参看下图所示:

![img](file:///C:/Users/Legend/Documents/My Knowledge/temp/f7e21929-10e7-4a9f-a7c8-d88d9613bae6/128/index_files/641556de-37ab-4be9-8a6a-44074e20b0e7.jpg)

Start Service的生命周期

标准开启Service时,会执行onCreate(0 -> onStartCommand()方法,如果多次开启服务只会执行onStartCommand()方法。

如果停止Service的话,会执行stopService() -> onDestory()方法。

startService() -> onCreate() -> onStartCommand() -> running -> stopService()/stopSelf() -> onDestroy() -> stopped

BInd Service的生命周期

绑定开启Service时,会执行onCreate() -> onBind()方法,如果多次开启不会调用任何方法。如果停止Service的话会执行onUnbind() -> onDestory()方法。

bindService() -> onCreate() -> onBind() -> running-> onUnbind() -> onDestroy() -> stopped

绑定开启服务还有如下一些地方需要注意:

1、如果oBind()方法返回值是null,onServerConnected方法不会被调用。
2、绑定的服务在系统设置界面,正在运行条目是看不到的。
3、绑定的服务和Activity不求同时生,但求同时死。
4、解除绑定服务后,服务会立即停止,且服务只可以被解除绑定一次,多次解除绑定代码会抛出异常。

Service的销毁

Service进程在处理完任务后,要使用安卓Service的stopself方法或者onDestroy方法结束Service,所在进程的回收交给安卓的垃圾回收机制来做。

其中,onStartCommand返回值有下面几种情况:建议不要修改onStartCommand方法的返回值,都交由系统处理比较好。

START_STICKY:Service被异常kill掉,会被系统拉起,但Intent内容丢失。
START_NOT_STICKY:Service被异常kill掉时,不会被系统拉起。
START_REDELIVER_INTENT:Service被异常kill掉时,会被系统拉起,并且Intent内容保留。
START_STICKY_COMPATIBILITY:START_STICKY的兼容版本,但不保证服务被终止后一定能重启。

Service通信

如果Activity需要和Service进行通信,我们必须用Bind Service的方法来开启服务。绑定方式开启的Service必须实现onBind()方法,该方法返回同一个定义

了服务通信接口的IBinder对象,Activity或其他程序组件可以调用bindService()方法获取接口并且调用服务上的方法。

创建绑定的服务,首先要定义客户端和服务通信方式的接口,这个接口必须是IBinder的实现类,并且必须通过onBind()方法返回,一旦客户端接收到IBinder,

就可以通过这个接口进行交互。除此之外,多个客户端可以绑定一个服务,还可以通过unBindService()方法解除绑定,当没有组件绑定在该服务时,服务会自动销毁。

// Activity
public class MainActivity extends AppCompatActivity {
    private class MyServiceConnection implements ServiceConnection {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            // 调用Service中的方法
            MyBinder binder = (MyBinder) service;
            String message = binder.getMessage();
            System.out.println(message);
        }
        @Override
        public void onServiceDisconnected(ComponentName name) {
        }
    }
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        findViewById(R.id.btn_nomal).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent service = new Intent(MainActivity.this, CustomService.class);
                bindService(service, new MyServiceConnection(), Context.BIND_AUTO_CREATE);
            }
        });
    }
}
// Service
public class CustomService extends Service {
    @Override
    public IBinder onBind(Intent intent) {
        return new MyBinder();
    }
    public class MyBinder extends Binder {
        /**
         * 该方法提供给Activity或其他组件进行调用
         */
        public String getMessage() {
            return "I am in Service";
        }
    }
}

IntentService

IntentService是继承于Service并处理异步请求的一个类,在IntentService内有一个工作线程来处理耗时操作,启动IntentService的方式和启动传统Service一样,

同时,当任务执行完后,IntentService会自动停止,另外,IntentService可以被启动多次,而每一个耗时操作会以工作队列的方式在IntentService的onHandleIntent

回调方法中执行,所有请求都在一个单线程中,不会阻塞应用程序的主线程(UI Thread),同一时间只处理一个请求。

Service是运行在主线程的,所以它不能做耗时操作,如果要做耗时操作则可以开启一个线程,而IntentService就是解决该问题的,它的处理流程如下:

创建worker线程传递给onStartCommand()的所有intent,不占用UI线程。
创建一个工作队列,传递一个intent到你实现的onHandleIntent()方法,避免多线程。
在所有启动请求被处理后,会自动关闭服务,不需要调用stopSelf()方法。
默认提供onBind()的实现,并返回null。
默认提供onStartCommand()的实现,实现发送intent到工作队列,再到onHandleIntent()方法实现。

以上都是IntentService都已经实现的,我们需要做的就是实现构造方法和 onHandleIntent():

public class MyIntentService extends IntentService {
    public MyIntentService() {
        super("MyIntentService");
    }
    @Override
    protected void onHandleIntent(Intent intent) {
        try {
            for (int i = 0; i < 20; i++) {
                Log.w("MyIntentService:", String.valueOf(i));
                Thread.sleep(1 * 1000);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    @Override
    public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
        return super.onStartCommand(intent, flags, startId);
    }
}

注意:如果需要重写其他回调方法一定要调用 super() 方法,保证 IntentService 正确处理 worker 线程,只有 onHandleIntent() 和 onBind() 不需要如此。

前台服务

Service按照运行来划分有两种:前台服务和后台服务。

  • 前台服务:前台服务可以一直保持运行状态,且不会在内存不足的情况下被回收。
  • 后台服务:后台服务也就是我们平时使用的普通服务,它的优先级比较低,会在内存不够的情况下可能被回收。
public class ForeGroundService extends Service {
    @Override
    public void onCreate() {
        super.onCreate();
        // 构建通知栏
        Notification.Builder builder = new Notification.Builder(getApplicationContext());
        Intent intent = new Intent(this, MainActivity.class);
        builder.setContentIntent(PendingIntent.getActivity(this, 0, intent, 0))
        .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher))
        .setContentTitle("title")
        .setContentText("text")
        .setWhen(System.currentTimeMillis());
        Notification notification = builder.build();
        notification.defaults = Notification.DEFAULT_SOUND;
        // 设置服务为前台服务,参数一:唯一的通知标识;参数二:通知消息。
        startForeground(110, notification);
    }
    @Override
    public IBinder onBind(Intent intent) {
        // TODO: Return the communication channel to the service.
        throw new UnsupportedOperationException("Not yet implemented");
    }
    @Override
    public void onDestroy() {
        // 停止前台服务--参数:表示是否移除之前的通知
        stopForeground(true);
    }
}

远程服务

aidl(Android Interface definition language),它是一种android内部进程通信接口的描述语言,通过它我们可以定义进程间的通信接口IPC(interprocess communication)

内部进程通信,满足两个进程之间接口数据的交换。

使用远程服务有如下的优点和缺点:

优点
远程服务有自己的独立进程,不会受到其它进程的影响;
可以被其它进程复用,提供公共服务;
具有很高的灵活性。
缺点
相对普通服务,占用系统资源较多,使用AIDL进行IPC也相对麻烦。

关于远程服务的通信示意图如下:
06、Android--Service服务
Service
IntentService
前台服务
远程服务

远程服务创建

定义AIDL接口 通过AIDL文件定义服务(Service)向客户端(Client)提供的接口,我们需要在对应的目录下添加一个后缀为.aidl的文件,IMyAidlInterface.aidl文件内容如下:

interface IMyAidlInterface {
  String getMessage();
}

注:如果服务端与客户端不在同一App上,需要在客户端、服务端两侧都建立该aidl文件。

创建远程Service

在远程服务中,通过Service的onBind(),在客户端与服务端建立连接时,用来传递Stub(存根)对象。

// 远程服务示例
public class RemoteService extends Service {
  
  public RemoteService() {
  }
  
  @Override
  public IBinder onBind(Intent intent) {
    return stub;// 在客户端连接服务端时,Stub通过ServiceConnection传递到客户端
  }
  
  // 实现接口中暴露给客户端的Stub--Stub继承自Binder,它实现了IBinder接口
  private IMyAidlInterface.Stub stub = new IMyAidlInterface.Stub(){
  
    // 实现了AIDL文件中定义的方法
    @Override
    public String getMessage() throws RemoteException {
      // 在这里我们只是用来模拟调用效果,因此随便反馈值给客户端
      return "Remote Service方法调用成功";        
    }    
  };
}

同时,在AndroidManifest.xml中对Remote Service进行如下配置:

<service
  android:name=".RemoteService"
  android:process="com.test.remote.msg">
  <intent-filter>
    <action android:name="com.legend.remoteservice.RemoteService"/>
  </intent-filter>
</service>

如果客户端与服务端在同个App中,AndroidManifest.xml中设置Remote Service的andorid:process属性时,有两种情况需要注意:

设置的进程名以(:)开头,则新进程是私有的,每次被执行或者被需要的时候会在新进程中创建。
设置的进程以小写字符开头,则服务运行在以这个名字命名的全局进程中,允许不同组件共享进程,从而减少资源浪费(需要相应权限)。

客户端调用远程服务接口

在客户端中建立与Remote Service的连接,获取Stub,然后调用Remote Service提供的方法来获取对应数据。

public class MainActivity extends AppCompatActivity {
  
  private IMyAidlInterface iMyAidlInterface;// 定义接口变量
  private ServiceConnection connection;
  
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState); 
    setContentView(R.layout.activity_main);
    bindRemoteService();
  }
  
  private void bindRemoteService() {
    Intent intentService = new Intent();
    intentService.setClassName(this,"com.zihao.remoteservice.RemoteService");
  
    connection = new ServiceConnection() {
      @Override
      public void onServiceConnected(ComponentName componentName,IBinder iBinder) {
        // 从连接中获取Stub对象
        iMyAidlInterface = IMyAidlInterface.Stub.asInterface(iBinder);
        // 调用Remote Service提供的方法
        try {
          Log.d("MainActivity", "获取到消息:" + iMyAidlInterface.getMessage()); 
        } catch (RemoteException e) {
          e.printStackTrace();
        } 
      } 
  
      @Override
      public void onServiceDisconnected(ComponentName componentName) {
        // 断开连接
        iMyAidlInterface = null;
      }
    }; 
  
    bindService(intentService, connection, Context.BIND_AUTO_CREATE);
  } 
  
  @Override
  protected void onDestroy() {
    super.onDestroy(); 
    if (connection != null)
      unbindService(connection);// 解除绑定
  }
}

远程服务实例

反射挂断电话

1、找到上下文的mBase引用的类ContextImpl,通过查看getSystemService源码可以知道,所有的系统服务都在一个map集合中。

public Object getSystemService(String name){
    ServiceFetcher fetcher = SYSTEM_SERVICE_MAP.get(name);
    return fetcher == null ? null : fetcher.getService(this);
}

2、接下来去查找map集合SYSTEM_SERVERCE_MAP,发现它其实是一个hashMap,这里需要详细解说:

registerService(POWER_SERVICE, new ServiceFetcher(){
    public Object createService(ContextImpl ctx){
        IBinder b = ServiceManager.getService(POWER_SERVICE);
        IPowerManager service = IPowerManager.Stub.asInterface(b);
        return new PowerManager(service, ctx.mMainThread.getHandler());
    }}
);

由于某些服务被认为不安全或侵犯用户隐私,所以谷歌在包装系统服务的时候,将某些服务进行了隐藏(@hide),比如挂断电话。我们需要先拿到ServiceManager对象, 但是谷歌不希望我们使用该对象,所以将该对象进行隐藏,所以参考下面的反射。

IBinder iBinder = ServiceManager.getService(TELEPHONY_SERVICE);

3、通过当前的service类的字节码来获取ServiceManager的字节码文件

// IBinder iBinder = ServiceManager.getService(TELEPHONY_SERVICE);
try{
    Class clazz = CallSmsSafeService.class.getClassLoader().loadClass("android.os.ServiceManager");
    Method method = clazz.getDeclaredMethod("getService", String.class);
    IBinder iBinder = (IBinder) method.invoke(null, TELEPHONY_SERVICE);
}catch (Exception e){
    e.printStackTrace();
}

4、下一步则是将iBinder转成接口类型,需要两个aidl文件,其中一个是依赖另外一个存在的,注意保证包名一致
1、android.telephony下的NeighboringCellInfo.aidl

/* //device/java/android/android/content/Intent.aidl
**
** Copyright 2007, The Android Open Source Project
**
** Licensed under the Apache License, Version 2.0 (the "License");
** you may not use this file except in compliance with the License.
** You may obtain a copy of the License at
**
**     http://www.apache.org/licenses/LICENSE-2.0
**
** Unless required by applicable law or agreed to in writing, software
** distributed under the License is distributed on an "AS IS" BASIS,
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
** See the License for the specific language governing permissions and
** limitations under the License.
*/

package android.telephony;

parcelable NeighboringCellInfo;

2、com.android.internal.telephony下的ITelephony.aidl

/*
 * Copyright (C) 2007 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.internal.telephony;

import android.os.Bundle;
import java.util.List;
import android.telephony.NeighboringCellInfo;

/**
 * Interface used to interact with the phone.  Mostly this is used by the
 * TelephonyManager class.  A few places are still using this directly.
 * Please clean them up if possible and use TelephonyManager insteadl.
 *
 * {@hide}
 */
interface ITelephony {

    /**
     * Dial a number. This doesn't place the call. It displays
     * the Dialer screen.
     * @param number the number to be dialed. If null, this
     * would display the Dialer screen with no number pre-filled.
     */
    void dial(String number);

    /**
     * Place a call to the specified number.
     * @param number the number to be called.
     */
    void call(String number);

    /**
     * If there is currently a call in progress, show the call screen.
     * The DTMF dialpad may or may not be visible initially, depending on
     * whether it was up when the user last exited the InCallScreen.
     *
     * @return true if the call screen was shown.
     */
    boolean showCallScreen();

    /**
     * Variation of showCallScreen() that also specifies whether the
     * DTMF dialpad should be initially visible when the InCallScreen
     * comes up.
     *
     * @param showDialpad if true, make the dialpad visible initially,
     *                    otherwise hide the dialpad initially.
     * @return true if the call screen was shown.
     *
     * @see showCallScreen
     */
    boolean showCallScreenWithDialpad(boolean showDialpad);

    /**
     * End call or go to the Home screen
     *
     * @return whether it hung up
     */
    boolean endCall();

    /**
     * Answer the currently-ringing call.
     *
     * If there's already a current active call, that call will be
     * automatically put on hold.  If both lines are currently in use, the
     * current active call will be ended.
     *
     * TODO: provide a flag to let the caller specify what policy to use
     * if both lines are in use.  (The current behavior is hardwired to
     * "answer incoming, end ongoing", which is how the CALL button
     * is specced to behave.)
     *
     * TODO: this should be a oneway call (especially since it's called
     * directly from the key queue thread).
     */
    void answerRingingCall();

    /**
     * Silence the ringer if an incoming call is currently ringing.
     * (If vibrating, stop the vibrator also.)
     *
     * It's safe to call this if the ringer has already been silenced, or
     * even if there's no incoming call.  (If so, this method will do nothing.)
     *
     * TODO: this should be a oneway call too (see above).
     *       (Actually *all* the methods here that return void can
     *       probably be oneway.)
     */
    void silenceRinger();

    /**
     * Check if we are in either an active or holding call
     * @return true if the phone state is OFFHOOK.
     */
    boolean isOffhook();

    /**
     * Check if an incoming phone call is ringing or call waiting.
     * @return true if the phone state is RINGING.
     */
    boolean isRinging();

    /**
     * Check if the phone is idle.
     * @return true if the phone state is IDLE.
     */
    boolean isIdle();

    /**
     * Check to see if the radio is on or not.
     * @return returns true if the radio is on.
     */
    boolean isRadioOn();

    /**
     * Check if the SIM pin lock is enabled.
     * @return true if the SIM pin lock is enabled.
     */
    boolean isSimPinEnabled();

    /**
     * Cancels the missed calls notification.
     */
    void cancelMissedCallsNotification();

    /**
     * Supply a pin to unlock the SIM.  Blocks until a result is determined.
     * @param pin The pin to check.
     * @return whether the operation was a success.
     */
    boolean supplyPin(String pin);

    /**
     * Handles PIN MMI commands (PIN/PIN2/PUK/PUK2), which are initiated
     * without SEND (so <code>dial</code> is not appropriate).
     *
     * @param dialString the MMI command to be executed.
     * @return true if MMI command is executed.
     */
    boolean handlePinMmi(String dialString);

    /**
     * Toggles the radio on or off.
     */
    void toggleRadioOnOff();

    /**
     * Set the radio to on or off
     */
    boolean setRadio(boolean turnOn);

    /**
     * Request to update location information in service state
     */
    void updateServiceLocation();

    /**
     * Enable location update notifications.
     */
    void enableLocationUpdates();

    /**
     * Disable location update notifications.
     */
    void disableLocationUpdates();

    /**
     * Enable a specific APN type.
     */
    int enableApnType(String type);

    /**
     * Disable a specific APN type.
     */
    int disableApnType(String type);

    /**
     * Allow mobile data connections.
     */
    boolean enableDataConnectivity();

    /**
     * Disallow mobile data connections.
     */
    boolean disableDataConnectivity();

    /**
     * Report whether data connectivity is possible.
     */
    boolean isDataConnectivityPossible();

    Bundle getCellLocation();

    /**
     * Returns the neighboring cell information of the device.
     */
    List<NeighboringCellInfo> getNeighboringCellInfo();

     int getCallState();
     int getDataActivity();
     int getDataState();

    /**
     * Returns the current active phone type as integer.
     * Returns TelephonyManager.PHONE_TYPE_CDMA if RILConstants.CDMA_PHONE
     * and TelephonyManager.PHONE_TYPE_GSM if RILConstants.GSM_PHONE
     */
    int getActivePhoneType();

    /**
     * Returns the CDMA ERI icon index to display
     */
    int getCdmaEriIconIndex();

    /**
     * Returns the CDMA ERI icon mode,
     * 0 - ON
     * 1 - FLASHING
     */
    int getCdmaEriIconMode();

    /**
     * Returns the CDMA ERI text,
     */
    String getCdmaEriText();

    /**
     * Returns true if CDMA provisioning needs to run.
     */
    boolean getCdmaNeedsProvisioning();

    /**
      * Returns the unread count of voicemails
      */
    int getVoiceMessageCount();

    /**
      * Returns the network type
      */
    int getNetworkType();
    
    /**
     * Return true if an ICC card is present
     */
    boolean hasIccCard();
}

这时系统会在gen目录的com.android.internal.telephony包下自动生成一个ITelephony.java的接口文件

5、继续代码实现反射挂断电话的操作,这时会出现很多高级的api可以供我们使用了

// IBinder iBinder = ServiceManager.getService(TELEPHONY_SERVICE)
try {
    Class clazz = CallSmsSafeService.class.getClassLoader().loadClass("android.os.ServiceManager");
    Method method = clazz.getDeclaredMethod("getService", String.class);
    IBinder iBinder = (IBinder) method.invoke(null, TELEPHONY_SERVICE);
    ITelephony iTelephony = ITelephony.Stub.asInterface(iBinder);
    iTelephony.endCall();
} catch (Exception e) {
    e.printStackTrace();
}   

要挂断电话还需要添加拨打电话的权限

<uses-permission android:name="android.permission.CALL_PHONE"/>  

此时如果是在API28以下的设备中可以正常拦截 但API28以上的设备会报如下错误:

JAVA.LANG.NOSUCHMETHODERROR: NO INTERFACE METHOD ENDCALL()Z IN CLASS LCOM/ANDROID/INTERNAL/TELEPHONY/ITELEPHONY;

出现这个异常的原因就是因为在API28以上的设备时,不再支持反射。而是同过TelecomManager 调用endCall()方法。