天天看點

再談鴻蒙Service

再談鴻蒙Service
鴻蒙Service相比Android的Service來講,重要性和使用頻率要高很多,因為其分布式的特點,Service被重新定義,做了很大的擴充,不僅僅隻做一些背景任務,還可以進行遠端控制、資料通信、資源配置設定等。鴻蒙應用開發中的Service是Ability的一種,并非和Android那樣有明顯的區分,使用方法也和Ability類似,也分本地和遠端,單向和雙向。
之前淺談過鴻蒙Service,主要是對Service做了簡單概述,對其生命周期進行了簡單分析。本文主要對Service的具體使用做簡單說明。

建立Service

建立Service比較簡單,基本不用寫代碼,一路點下去就行,如下圖所示:

再談鴻蒙Service

由于Service區分為本地和遠端的,是以這裡建立兩個Service,以備使用,分别為:LocalServiceAbility和RemoteServiceAbility:

public class LocalServiceAbility extends Ability {
    private static final HiLogLabel LABEL_LOG = new HiLogLabel(3, 0xD001100, "LocalServiceAbility ");

    @Override
    public void onStart(Intent intent) {
        HiLog.error(LABEL_LOG, "LocalServiceAbility::onStart");
        super.onStart(intent);
    }

    @Override
    public void onBackground() {
        super.onBackground();
        HiLog.info(LABEL_LOG, "LocalServiceAbility::onBackground");
    }

    @Override
    public void onStop() {
        super.onStop();
        HiLog.info(LABEL_LOG, "LocalServiceAbility::onStop");
    }

    @Override
    public void onCommand(Intent intent, boolean restart, int startId) {
    }

    @Override
    public IRemoteObject onConnect(Intent intent) {
        return null;
    }

    @Override
    public void onDisconnect(Intent intent) {
    }
}
           

當點選建立Serivice的Finish按鈕後,上述代碼就會自動生成,開發者可根據自己的習慣配置一下日志輸出格式。RemoteServiceAbility和LocalServiceAbility除了類名不一樣外,其他都一樣。由于Service是Ability的一種,是以Service都是繼承Ability的,使用是比較友善,但是Service不同于Page,放在一起使用容易混亂,是以使用Service的時候建議封裝個基類比較好容易區分,便于管理。當Service建立完後,DevEco Studio 自動在config.json中注冊,無需手動注冊。

本地服務

啟動與關閉

啟動服務

Intent intent1 = new Intent();
            Operation operation = new Intent.OperationBuilder()
                    .withDeviceId("")
                    .withBundleName("com.harmonyos.service")
                    .withAbilityName("com.harmonyos.service.LocalServiceAbility")
                    .build();
            intent1.setOperation(operation);
            startAbility(intent1);
           

停止服務

Intent intent1 = new Intent();
            Operation operation = new Intent.OperationBuilder()
                    .withDeviceId("")
                    .withBundleName("com.harmonyos.service")
                    .withAbilityName("com.harmonyos.service.LocalServiceAbility")
                    .build();
            intent1.setOperation(operation);
            stopAbility(intent1);
           

下一頁

Intent intent1 = new Intent();
    present(new SecondAbilitySlice(), intent1);
           

簡單設定了三個按鈕:開啟服務、停止服務、下一頁。仔細觀察一下上述代碼,可以發現服務的開啟與停止和Ability的使用是一樣的,如果有什麼不清楚的可以看看HarmonyOS-page之間的跳轉。

日志輸出

使用的LocalServiceAbility的代碼和文章開頭建立的一樣,在生命周期中的每個方法進行日志輸出,在onCommand()中将所有的參數進行了輸出與彈出:

@Override
    public void onCommand(Intent intent, boolean restart, int startId) {
        System.out.println("LocalServiceAbility::onCommand restart:" + restart + ",startId:" + startId);
        ToastDialog toastDialog = new ToastDialog(getContext());
        toastDialog.setText("::onCommand restart:" + restart + ",startId:" + startId);
        toastDialog.show();
    }
           
效果
再談鴻蒙Service

不管是在手機上還是電視上,效果都一樣,光看頁面看不出什麼,主要是分析日志。

  • 第一次開啟服務時,LocalServiceAbility會依次執行onStart()->onCommand(),同時onCommand()中restart值為false,startId值為1。
  • 在不停止服務的時候再次開啟服務會發現直接執行了onCommand(),且onCommand()中restart值為false,startId值為2。
  • 同樣不關閉該服務,跳轉至下一頁然後再回來再次開啟服務,會發現直接執行了onCommand(),且onCommand()中restart值為false,startId值為3。
  • 當切回到home頁,再切會到該頁面,點選開啟服務,會發現直接執行了onCommand(),且onCommand()中restart值為false,startId值為4 。
  • 當點選停止服務時,此地點選開啟服務此時Service會從頭開始執行onStart()->onCommand(),且onCommand()中restart值為false,startId值為1。點選停止服務後跳轉至下一頁後傳回開啟服務,執行和點選停止服務後再開啟服務後一樣。

不管執行到哪一步,直接輸出Service的執行個體,發現Service執行個體對應的位址是一樣的。

上述方法的執行順序,側面應證了Service的一些特點:

  • 相同的Service是單例的,不會被多次建立。
  • Service自己的生命周期并不和Ability的生命周期綁定在一起,即不會随着頁面的銷毀而銷毀。Service一旦被建立,不會自動停止,除非調用stopAbility或在Service内部相關操作執行完後調用terminateAbility後該服務會停止。
  • onStart隻會在Service第一次開啟的時候被調用,若該服務沒有被停止,再次啟用該服務,該方法不會被調用,即onStart在Service整個生命周期中隻會執行一次。
  • onCommand在Service生命周期中允許被多次調用。其參數restart并非是指再次啟用服務時,restart為true,檢視日志會發現不管調用多少次結果均為false,是以restart代表的是服務異常關閉時再次啟用會被置為true,比如崩潰。startId其實相當于計數器,在一次完整的生命周期中,每次調用該服務,startId均會+1,檢視這個可以看看該服務被掉用了多少次。當服務被停止後startId會從頭開始。
  • 當停止服務時,不會最先調用onStop(),而是先onBackground(),後onStop()。
連接配接與斷開

上文中一個簡單的服務從開啟到停止一個完整的流程已經走完,但是onConnect()和onDisconnect()沒有被召喚,這兩個方法肯定不會是多餘的,onConnect()和onDisconnect()是Service使用的另一種方式。之是以配置兩種使用方式,是适用于不同的場景。

開啟和停止服務屬于最基本操作,該使用方式屬于開關式,隻管開始和結束,無法控制過程,即如果使用該方式開啟了音樂播放,隻能開啟播放音樂,但是是否開啟成功,播放哪一首,到什麼時間了,是否被人為關掉了,該方式均無法把控。是以開啟和停止服務屬于單項信号式,而連接配接與斷開是雙向通信的方式,不僅可以開關服務,還可以擷取服務狀态。

建立Service執行個體

private IAbilityConnection connection = new IAbilityConnection() {
        @Override
        public void onAbilityConnectDone(ElementName elementName, IRemoteObject iRemoteObject, int resultCode) {
        }

        @Override
        public void onAbilityDisconnectDone(ElementName elementName, int resultCode) {
          
        }
    };
           
再談鴻蒙Service

onAbilityConnectDone()是用來處理連接配接Service成功的回調。elementName為連接配接裝置的相關資訊,啟動Ability中有使用intent.setElementName(String deviceId, String bundleName, String abilityName)來啟用,不清楚的可以看看HarmonyOS-page之間的跳轉末尾的評論。IRemoteObject 相當于Service連接配接通道中的資料包,resultCode是傳回結果碼,一般0代表連接配接成功,否則連接配接失敗。

再談鴻蒙Service

onAbilityDisconnectDone()參數和onAbilityConnectDone方法中一樣。注意上圖中的crashes,unexpectedly,說明onAbilityDisconnectDone是用來處理Service異常死亡的回調,是以在正常的斷開連接配接時是看不到此方法的調用。

連接配接

Intent intent = new Intent();
Operation operation = new Intent.OperationBuilder()
        .withDeviceId("deviceId")
        .withBundleName("com.harmonyos.service")
        .withAbilityName("com.harmonyos.service.LocalServiceAbility")
        .build();
intent.setOperation(operation);
connectAbility(intent, connection);
           

連接配接時需要調用connectAbility方法,而且需要将建立好的Service執行個體connection帶進入即可。

斷開

斷開調用disconnectAbility,把Service執行個體connection傳進入即可。

建立IRemoteObject實作類

建立傳回資料就是Service側也需要在onConnect()時傳回IRemoteObject,進而定義與Service進行通信的接口。系統預設提供IRemoteObject的實作LocalRemoteObject ,也可以直接繼承RemoteObject

//    private class CurrentRemoteObject extends ohos.aafwk.ability.LocalRemoteObject {
//
//        public CurrentRemoteObject() {
//        }
//    }

    private class CurrentRemoteObject extends RemoteObject {
        private CurrentRemoteObject() {
            super("CurrentRemoteObject");
        }

        @Override
        public boolean onRemoteRequest(int code, MessageParcel data, MessageParcel reply, MessageOption option) {
            return true;
        }
    }
    
           

LocalRemoteObject 也是繼承了RemoteObject ,是以兩種方式其實是一樣的。建立好了之後在LocalServiceAbility中将其傳回

@Override
    public IRemoteObject onConnect(Intent intent) {
        System.out.println("LocalServiceAbility::onConnect");
        return new CurrentRemoteObject();
    }
           

效果與分析

頁面還是上文中的頁面,但是日志變化不小。

  1. 連接配接服務時會調用onStart()–>ononConnect(),若有傳回的RemoteObject 實作類,之後會調用onAbilityConnectDone(),此時宣告Service連接配接成功,并可以接收連接配接成功後的資料傳回IRemoteObject,通過resultCode可以知道是哪個Service傳回的,友善處理。在沒有斷開服務的情況下,多次連接配接上述方法并不會執行,因為一旦連接配接成功,便維持,是以多次連接配接已連接配接的服務無意義。
  2. 斷開服務時onDisconnect()–>onBackground()–>onStop(),多次斷開已斷開的Service也是沒有意義的。
  3. onAbilityDisconnectDone()隻有在Service異常死亡的時候才會被調用。
  4. CurrentRemoteObject()是将Service自身的執行個體傳回給調用者。
  5. 若有多個Ability連接配接該服務,第一個Ability連接配接服務時,ononConnect()會被調用,生成IRemoteObject 對象,并将此對象傳回到所有連接配接到該服務的Ability。此時Service像是一個真實的伺服器,将資料發給所有需要的使用者,是以在文章開頭講鴻蒙Service有資料通信、資源配置設定功能。特别是遠端連接配接服務的時候,使用者完全可以将任意一台裝置作為Service,其他裝置作為Client,典型的C-S模式的任意切換。

遠端服務

遠端服務的開啟與停止,連接配接與斷開與本地服務的操作基本一緻,但是deviceId要手動擷取,并添加flag:

...
   Operation operation = new Intent.OperationBuilder()
                    .withDeviceId(deviceId)
                    .withBundleName("com.harmonyos.service")
                    .withAbilityName("com.harmonyos.service.RemoteServiceAbility")
                    .withFlags(Intent.FLAG_ABILITYSLICE_MULTI_DEVICE)
                    .build();
   ...
           

deviceId怎麼擷取,可使用如下方法擷取:

private String getRemoteDeviceId() {
        List<DeviceInfo> infoList = DeviceManager.getDeviceList(DeviceInfo.FLAG_GET_ALL_DEVICE);
        if ((infoList == null) || (infoList.size() == 0)) {
            return "";
        }
        int random = new SecureRandom().nextInt(infoList.size());
        return infoList.get(random).getDeviceId();
    }
           

withFlags(Intent.FLAG_ABILITYSLICE_MULTI_DEVICE)就是添加個标簽,遠端服務就是跨裝置、多裝置,是以添加個FLAG_ABILITYSLICE_MULTI_DEVICE理所應當的,也可以在intent.setFlags中添加,但是該方法已被廢棄,過時廢棄的方法能不用就不用。

遠端服務和本地服務不同之處在于遠端服務中你不知道誰是服務端,誰是用戶端,是以需要在代碼中兩端的操作都要做好。

用戶端工作

這裡簡單模拟的是用戶端傳遞一個int類型的資料給遠端服務端,然後服務端将資料*1024再傳回。首先建立用戶端代理類并實作IRemoteBroker,IRemoteBroker也就是遠端代理,就像兩個經紀人之間的交流,并非本主。

public class ClientRemoteProxy implements IRemoteBroker {
    private static final int RESULT_SUCCESS = 0;
    private static final int RESULT_TODO = 1;
    private final IRemoteObject remoteObject;
    
    public ClientRemoteProxy(IRemoteObject remoteObject) {
        this.remoteObject = remoteObject;
    }

    public int todoServiceJob(int command) {
        MessageParcel message = MessageParcel.obtain();
        message.writeInt(command);

        MessageParcel reply = MessageParcel.obtain();
        MessageOption option = new MessageOption(MessageOption.TF_SYNC);
        int result = 0;
        try {
            remoteObject.sendRequest(RESULT_TODO, message, reply, option);
            int resultCode = reply.readInt();
            if (resultCode != RESULT_SUCCESS) {
                throw new RemoteException();
            }
            result = reply.readInt();
        } catch (RemoteException e) {
            e.printStackTrace();
        }
        return result;
    }
    @Override
    public IRemoteObject asObject() {
        return remoteObject;
    }
}
           

最重要的是todoServiceJob方法,這裡是消息發送最關鍵的地方,remoteObject.sendRequest()就是向遠端釋出消息,告訴遠端裝置我要幹嘛。

第一個參數相當于請求碼,兩端約定好,用戶端發送這個碼,遠端裝置接口後識别這個碼就知道要幹嘛了。MessageParcel 使用起來像隊列,實際功能卻和Map很像,通過MessageParcel.obtain()擷取,然後writeInt和readInt寫入和寫出資料,資料類型包含基本類型和自己建立的對象。

再談鴻蒙Service

MessageParcel讀取或寫入資料時,寫一次指針往後移動一位,寫一個資料就占一格,再寫就往後再占一格,依次按順序往下,使用的時候按順序來就行。第二個參數MessageParcel var2就是用戶端向遠端裝置傳遞的裝置,第三個參數MessageParcel var3是遠端裝置向用戶端回複的消息,發送的時候就已經把需要接收的消息的位置給留好了,而不是遠端裝置接收資料後将資料清空然後再将結果寫入。兩個籃子,一個裝請求,一個裝結果,互不幹擾。第四個參數MessageOption var4是本次通信是同步的還是異步的,同步的如下:

異步的就是将MessageOption.TF_SYNC換成MessageOption.TF_ASYNC。傳回的資料中一般第一位為狀态碼,resultCode = reply.readInt(),如果resultCode等于成功的狀态碼就取第二位,第二位才是真正的結果,然後将其傳給需要的地方。資料存放的格式不固定,兩端約定好就行,不一定要把狀态碼放在第一位,可以不傳,可以放在前三位,均可。

ClientRemoteProxy 的使用如下:

public class MainAbilitySlice extends AbilitySlice {
    private ClientRemoteProxy clientRemoteProxy = null;
    private IAbilityConnection connection = new IAbilityConnection() {
        @Override
        public void onAbilityConnectDone(ElementName elementName, IRemoteObject iRemoteObject, int i) {
            clientRemoteProxy = new ClientRemoteProxy(iRemoteObject);
            System.out.println("onAbilityConnectDone");

        }
        @Override
        public void onAbilityDisconnectDone(ElementName elementName, int i) {
            System.out.println("onAbilityDisconnectDone");

        }
    };
    
        @Override
    public void onStart(Intent intent) {
        super.onStart(intent);
        super.setUIContent(ResourceTable.Layout_ability_main);
        findComponentById(ResourceTable.Id_btn_next_page).setClickedListener(component -> {
            if(clientRemoteProxy != null){
                int result = clientRemoteProxy.todoServiceJob(512);
                System.out.println("result:" + result);
            }
        });
	...
    }
           

當服務連接配接成功後調用onAbilityConnectDone方法并傳回IRemoteObject ,此時ClientRemoteProxy拿到IRemoteObject建立執行個體,然後在相應的位置發送消息clientRemoteProxy.todoServiceJob(512),就是将512發送給遠端裝置,等待遠端裝置傳回結果。

遠端裝置工作

遠端裝置就是将傳遞過來的資料處理後再傳回去。

public class ServiceRemoteProxy extends RemoteObject implements IRemoteBroker {
    private static final int RESULT_SUCCESS = 0;
    private static final int RESULT_FAILED = -1;
    private static final int RESULT_TODO = 1;

    public ServiceRemoteProxy(String descriptor) {
        super(descriptor);
    }

    @Override
    public IRemoteObject asObject() {
        return this;
    }
    @Override
    public boolean onRemoteRequest(int code, MessageParcel data, MessageParcel reply, MessageOption option) throws RemoteException {
        if(code != RESULT_TODO){
            reply.writeInt(RESULT_FAILED);
            return  false;
        }

        int initData = data.readInt();
        int resultData = initData * 1024;
        reply.writeInt(RESULT_SUCCESS);
        reply.writeInt(resultData);
        
        return true;
    }
}
           

最重要的是onRemoteRequest,裡面的參數解釋和ClientRemoteProxy 中的sendRequest一樣,若code不等于約定值,直接傳回錯誤碼拒絕服務。代碼中将傳進來的資料*1024後傳回去,至此遠端裝置的服務工作已結束但是遠端裝置還沒有被使用,使用如下:

public class RemoteServiceAbility extends Ability {
 	...
    private ServiceRemoteProxy serviceRemoteProxy = new ServiceRemoteProxy("");
    ...
    @Override
    public IRemoteObject onConnect(Intent intent) {
        return serviceRemoteProxy;
    }
   ...
}
           

在連接配接遠端服務調用onConnect,直接将遠端服務的代理類ServiceRemoteProxy的執行個體傳回即可。至此所有的遠端服務工作結束。

遠端服務流程
  1. 建立發送端的代理類實作IRemoteBroker,用于發送資料。
  2. 建立遠端端的代理類繼承RemoteObject并實作IRemoteBroker,用于接收、處理、傳回資料。
  3. 建立連接配接遠端服務需要的的IAbilityConnection,并在onAbilityConnectDone中擷取傳回IRemoteObject建立發送端代理類的執行個體,并在适合的位置用建立的執行個體發送消息。
  4. 連接配接起遠端服務後會調用onConnect,在此方法中傳回遠端代理類的執行個體。

順序不一定是上面的順序,隻要該有的都有了,調用遠端服務就沒有問題。

總結

  • 相同的Service是單例的。
  • Service不主動銷毀,使用完畢後建議及時斷開或銷毀。
  • 多個Ability可共用一個Service,其中一個Ability調用Service,Service會将結果發送給所有Ability。
  • 當多個Ability共用一個Service時,所有Ability退出後,Service才能退出。
  • Service執行在主線程中,若有耗時操作,請另開線程,否則ANR。
  • 鴻蒙Service分布式可助任意連接配接裝置變成Service裝置。