Android 車載應(yīng)用開(kāi)發(fā)與分析 (4)- 編寫(xiě)基于AIDL 的 SDK

前言

之前介紹了車載應(yīng)用開(kāi)發(fā)體系中如何使用Jetpack在HMI中構(gòu)建MVVM架構(gòu)Android 車載應(yīng)用開(kāi)發(fā)與分析 (3)- 構(gòu)建 MVVM 架構(gòu)(Java版)拐揭,通過(guò)之前的介紹,也了解到在大多數(shù)車載系統(tǒng)應(yīng)用架構(gòu)中朴爬,一個(gè)完整的應(yīng)用往往會(huì)包含三層荒澡,分別是

  • HMI
    Human Machine Interface,顯示UI信息屉凯,進(jìn)行人機(jī)交互立帖。

  • Service
    在系統(tǒng)后臺(tái)進(jìn)行數(shù)據(jù)處理,監(jiān)控?cái)?shù)據(jù)狀態(tài)悠砚。

  • SDK
    根據(jù)業(yè)務(wù)邏輯晓勇,需要Service對(duì)外暴露的通信接口,其他模塊通過(guò)SDK來(lái)完成與Service通信灌旧,通常是基于AIDL接口绑咱。

本篇主要講解,如何編寫(xiě)基于 AIDL 的 SDK 枢泰。

AIDL 介紹

AIDL描融,Android 接口定義語(yǔ)言,是Android開(kāi)發(fā)中常用的一種進(jìn)程間通信方式衡蚂。關(guān)于如何使用 AIDL 請(qǐng)參考 Android 接口定義語(yǔ)言 (AIDL) | Android 開(kāi)發(fā)者 | Android Developers

這里介紹一些 AIDL 使用過(guò)程中容易混淆的關(guān)鍵字:

  • in
interface HvacInterface {
    void setData(in Hvac hvac);
}

單向數(shù)據(jù)流向窿克。被in修飾的參數(shù),會(huì)順利傳到Server端毛甲,但Servier端對(duì)實(shí)參的任何改變年叮,都不會(huì)回調(diào)給Client端。

  • out
interface HvacInterface {
    void getData(out Hvac hvac);
}

單向數(shù)據(jù)流向玻募。被out修飾的參數(shù)只损,只有默認(rèn)值會(huì)傳到Server端,Servier端對(duì)實(shí)參的改變七咧,在調(diào)用結(jié)束后改执,會(huì)回調(diào)給Client端。

  • inout
interface HvacInterface {
    void getData(inout Hvac hvac);
}

inout 則是上面二者的結(jié)合坑雅,實(shí)參會(huì)順利傳到Server辈挂,且Server對(duì)實(shí)參的修改,在調(diào)用結(jié)束后會(huì)返回Client端裹粤。

  • oneway
    AIDL 定義的接口默認(rèn)是同步調(diào)用终蒂。舉個(gè)例子:Client端調(diào)用setData方法蜂林,setData在Server端執(zhí)行需要耗費(fèi)5秒鐘,那么Client端調(diào)用setData方法的線程就會(huì)被block5秒鐘拇泣。如果在setData方法上加上oneway噪叙,將接口修改為異步調(diào)用就可以避免這個(gè)問(wèn)題。
interface HvacInterface {
    oneway void setData(in Hvac hvac);
}

oneway不僅可以修飾方法霉翔,也可以用來(lái)修飾在interface本身睁蕾,這樣interface內(nèi)所有的方法都隱式地帶上oneway。被oneway修飾了的方法不可以有返回值债朵,也不可以再用out或inout修飾參數(shù)子眶。

AIDL 常規(guī)用法

IRemoteService iRemoteService;

private ServiceConnection mConnection = new ServiceConnection() {
    // Called when the connection with the service is established
    public void onServiceConnected(ComponentName className, IBinder service) {
        // Following the example above for an AIDL interface,
        // this gets an instance of the IRemoteInterface, which we can use to call on the service
        iRemoteService = IRemoteService.Stub.asInterface(service);
    }

    // Called when the connection with the service disconnects unexpectedly
    public void onServiceDisconnected(ComponentName className) {
        Log.e(TAG, "Service has unexpectedly disconnected");
        iRemoteService = null;
    }
};

public void setData(Hvac havc){
    if (iRemoteService!=null){
        iRemoteService.setData(hvac);
    }
}

常規(guī)的用法中,我們需先判斷Client端是否已經(jīng)綁定上Server端序芦,不僅Client端對(duì)Server端的接口調(diào)用臭杰,也要防止綁定失敗導(dǎo)致的空指針。

車載應(yīng)用中上述的常規(guī)用法不僅會(huì)使HMI開(kāi)發(fā)變得繁瑣谚中,還需要處理Service異常狀態(tài)下解除綁定后的狀態(tài)渴杆。下面介紹如何簡(jiǎn)便的封裝SDK

封裝SDK Base類

實(shí)際開(kāi)發(fā)中,我們把Client端對(duì)Service的綁定宪塔、重連磁奖、線程切換等細(xì)節(jié)隱藏到SDK中并封裝成一個(gè)BaseConnectManager,使用時(shí)只需要繼承BaseConnectManager并傳入Service的包名某筐、類名和期望的斷線重連時(shí)間即可比搭。

public abstract class BaseConnectManager<T extends IInterface> {

    private final String TAG = SdkLogUtils.TAG_FWK + getClass().getSimpleName();
    private static final String THREAD_NAME = "bindServiceThread";

    private final Application mApplication;
    private IServiceConnectListener mServiceListener;
    private final Handler mChildThread;
    private final Handler mMainThread;
    private final LinkedBlockingQueue<Runnable> mTaskQueue = new LinkedBlockingQueue<>();
    private final Runnable mBindServiceTask = this::bindService;
    private final ServiceConnection mServiceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            SdkLogUtils.logV(TAG, "[onServiceConnected]");
            mProxy = asInterface(service);
            Remote.tryExec(() -> {
                service.linkToDeath(mDeathRecipient, 0);
            });
            if (mServiceListener != null) {
                mServiceListener.onServiceConnected();
            }
            handleTask();
            mChildThread.removeCallbacks(mBindServiceTask);
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            SdkLogUtils.logV(TAG, "[onServiceDisconnected]");
            mProxy = null;
            if (mServiceListener != null) {
                mServiceListener.onServiceDisconnected();
            }
        }
    };

    private final IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() {
        @Override
        public void binderDied() {
            SdkLogUtils.logV(TAG, "[binderDied]");
            if (mServiceListener != null) {
                mServiceListener.onBinderDied();
            }

            if (mProxy != null) {
                mProxy.asBinder().unlinkToDeath(mDeathRecipient, 0);
                mProxy = null;
            }

            attemptToRebindService();
        }

    };

    private T mProxy;

    public BaseConnectManager() {
        mApplication = SdkAppGlobal.getApplication();
        HandlerThread thread = new HandlerThread(THREAD_NAME, 6);
        thread.start();
        mChildThread = new Handler(thread.getLooper());
        mMainThread = new Handler(Looper.getMainLooper());
        bindService();
    }

    private void bindService() {
        if (mProxy == null) {
            SdkLogUtils.logV(TAG, "[bindService] start");
            ComponentName name = new ComponentName(getServicePkgName(), getServiceClassName());
            Intent intent = new Intent();
            if (getServiceAction() != null) {
                intent.setAction(getServiceAction());
            }
            intent.setComponent(name);
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                mApplication.startForegroundService(intent);
            } else {
                mApplication.startService(intent);
            }
            boolean connected = mApplication.bindService(intent, mServiceConnection,
                    Context.BIND_AUTO_CREATE);
            SdkLogUtils.logV(TAG, "[bindService] result " + connected);
            if (!connected) {
                attemptToRebindService();
            }
        } else {
            SdkLogUtils.logV(TAG, "[bindService] not need");
        }
    }

    protected void attemptToRebindService() {
        SdkLogUtils.logV(TAG, "[attemptToRebindService]");
        mChildThread.postDelayed(mBindServiceTask, getRetryBindTimeMill());
    }

    protected void handleTask() {
        Runnable task;
        while ((task = mTaskQueue.poll()) != null) {
            SdkLogUtils.logV(TAG, "[handleTask] poll task form task queue");
            mChildThread.post(task);
        }
    }

    public void init() {
        bindService();
    }

    public boolean isServiceConnected() {
        return isServiceConnected(false);
    }

    public boolean isServiceConnected(boolean tryConnect) {
        SdkLogUtils.logV(TAG, "[isServiceConnected] tryConnect " + tryConnect + ";isConnected " + (mProxy != null));
        if (mProxy == null && tryConnect) {
            attemptToRebindService();
        }
        return this.mProxy != null;
    }

    public void release() {
        SdkLogUtils.logV(TAG, "[release]");
        if (this.isServiceConnected()) {
            this.mProxy.asBinder().unlinkToDeath(this.mDeathRecipient, 0);
            this.mProxy = null;
            this.mApplication.unbindService(mServiceConnection);
        }
    }

    public void setStateListener(IServiceConnectListener listener) {
        SdkLogUtils.logV(TAG, "[setStateListener]" + listener);
        mServiceListener = listener;
    }

    public void removeStateListener() {
        SdkLogUtils.logV(TAG, "[removeStateListener]");
        mServiceListener = null;
    }

    protected T getProxy() {
        return mProxy;
    }

    protected LinkedBlockingQueue<Runnable> getTaskQueue() {
        return mTaskQueue;
    }

    public Handler getMainHandler() {
        return mMainThread;
    }

    protected abstract String getServicePkgName();

    protected abstract String getServiceClassName();

    protected String getServiceAction() {
        return null;
    }

    protected abstract T asInterface(IBinder service);

    protected abstract long getRetryBindTimeMill();

}

封裝 SDK

開(kāi)發(fā)中多數(shù)時(shí)候我們只有一個(gè)用于操作Service Interface,如下所示:

interface HvacInterface {

    oneway void setTemperature(int temperature);

    oneway void requestTemperature();

    boolean registerCallback(in HvacCallback callback);

    boolean unregisterCallback(in HvacCallback callback);

}

用于回調(diào)Server端處理結(jié)果的Callback

interface HvacCallback {

    oneway void onTemperatureChanged(double temperature);

}

基于BaseConnectManager封裝一個(gè)HvacManager

public class HvacManager extends BaseConnectManager<HvacInterface> {

    private static final String TAG = SdkLogUtils.TAG_FWK + HvacManager.class.getSimpleName();

    private static volatile HvacManager sHvacManager;

    public static final String SERVICE_PACKAGE = "com.fwk.service";
    public static final String SERVICE_CLASSNAME = "com.fwk.service.SimpleService";
    private static final long RETRY_TIME = 5000L;

    private final List<IHvacCallback> mCallbacks = new ArrayList<>();

    private final HvacCallback.Stub mSampleCallback = new HvacCallback.Stub() {
        @Override
        public void onTemperatureChanged(double temperature) throws RemoteException {
            SdkLogUtils.logV(TAG, "[onTemperatureChanged] " + temperature);
            getMainHandler().post(() -> {
                for (IHvacCallback callback : mCallbacks) {
                    callback.onTemperatureChanged(temperature);
                }
            });
        }
    };

    public static HvacManager getInstance() {
        if (sHvacManager == null) {
            synchronized (HvacManager.class) {
                if (sHvacManager == null) {
                    sHvacManager = new HvacManager();
                }
            }
        }
        return sHvacManager;
    }

    @Override
    protected String getServicePkgName() {
        return SERVICE_PACKAGE;
    }

    @Override
    protected String getServiceClassName() {
        return SERVICE_CLASSNAME;
    }

    @Override
    protected HvacInterface asInterface(IBinder service) {
        return HvacInterface.Stub.asInterface(service);
    }

    @Override
    protected long getRetryBindTimeMill() {
        return RETRY_TIME;
    }

    /******************/
  
    public void requestTemperature() {
        Remote.tryExec(() -> {
            if (isServiceConnected(true)) {
                getProxy().requestTemperature();
            } else {
                // 將此方法放入隊(duì)列中来吩,等Service重新連接后敢辩,會(huì)依次調(diào)用
                getTaskQueue().offer(this::requestTemperature);
            }
        });
    }

    public void setTemperature(int temperature) {
        Remote.tryExec(() -> {
            if (isServiceConnected(true)) {
                getProxy().requestTemperature();
            } else {
                getTaskQueue().offer(() -> {
                    setTemperature(temperature);
                });
            }
        });
    }

    public boolean registerCallback(IHvacCallback callback) {
        return Remote.exec(() -> {
            if (isServiceConnected(true)) {
                boolean result = getProxy().registerCallback(mSampleCallback);
                if (result) {
                    mCallbacks.remove(callback);
                    mCallbacks.add(callback);
                }
                return result;
            } else {
                getTaskQueue().offer(() -> {
                    registerCallback(callback);
                });
                return false;
            }
        });
    }

    public boolean unregisterCallback(IHvacCallback callback) {
        return Remote.exec(() -> {
            if (isServiceConnected(true)) {
                boolean result = getProxy().unregisterCallback(mSampleCallback);
                if (result) {
                    mCallbacks.remove(callback);
                }
                return result;
            } else {
                getTaskQueue().offer(() -> {
                    unregisterCallback(callback);
                });
                return false;
            }
        });
    }
}

上述代碼中蔽莱,我們需要注意一點(diǎn)弟疆,每次調(diào)用遠(yuǎn)程方法都需要判斷當(dāng)前service是否處于連接,如果與Service的連接被斷開(kāi)了盗冷,我們要把方法放入一個(gè)隊(duì)列中去怠苔,當(dāng)Service重新被綁定上后,隊(duì)列中的方法仪糖,會(huì)依次被取出執(zhí)行柑司。

最后,我們?cè)赟DK module的 build.gradle中加入可以編譯出jar的腳本

// makeJar
def zipFile = file('build/intermediates/aar_main_jar/release/classes.jar')
task makeJar(type: Jar) {
    from zipTree(zipFile)
    archiveBaseName =  "sdk"
    destinationDirectory = file("build/outputs/")
    manifest {
        attributes(
                'Implementation-Title': "${project.name}",
                'Built-Date': new Date().getDateTimeString(),
                'Built-With':
                        "gradle-${project.getGradle().getGradleVersion()},groovy-${GroovySystem.getVersion()}",
                'Created-By':
                        'Java ' + System.getProperty('java.version') + ' (' + System.getProperty('java.vendor') + ')')
    }
}
makeJar.dependsOn(build)

使用示例

public void requestTemperature() {
    LogUtils.logI(TAG, "[requestTemperature]");
    HvacManager.getInstance().requestTemperature();
}

實(shí)際使用時(shí)锅劝,調(diào)用方既不需要關(guān)心Service的綁定狀態(tài)攒驰,也不需要主動(dòng)進(jìn)行線程切換,極大的簡(jiǎn)便了HMI的開(kāi)發(fā)故爵。
本文源碼地址: https://github.com/linux-link/CarServerArch

參考資料

Android 接口定義語(yǔ)言 (AIDL) | Android 開(kāi)發(fā)者 | Android Developers

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末玻粪,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌劲室,老刑警劉巖伦仍,帶你破解...
    沈念sama閱讀 218,755評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異很洋,居然都是意外死亡充蓝,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)喉磁,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)谓苟,“玉大人,你說(shuō)我怎么就攤上這事线定∧纫辏” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,138評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵斤讥,是天一觀的道長(zhǎng)纱皆。 經(jīng)常有香客問(wèn)我,道長(zhǎng)芭商,這世上最難降的妖魔是什么派草? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,791評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮铛楣,結(jié)果婚禮上近迁,老公的妹妹穿的比我還像新娘。我一直安慰自己簸州,他們只是感情好鉴竭,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著岸浑,像睡著了一般搏存。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上矢洲,一...
    開(kāi)封第一講書(shū)人閱讀 51,631評(píng)論 1 305
  • 那天璧眠,我揣著相機(jī)與錄音,去河邊找鬼读虏。 笑死责静,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的盖桥。 我是一名探鬼主播灾螃,決...
    沈念sama閱讀 40,362評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼揩徊!你這毒婦竟也來(lái)了腰鬼?” 一聲冷哼從身側(cè)響起藐握,我...
    開(kāi)封第一講書(shū)人閱讀 39,264評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎垃喊,沒(méi)想到半個(gè)月后猾普,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,724評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡本谜,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年初家,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片乌助。...
    茶點(diǎn)故事閱讀 40,040評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡溜在,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出他托,到底是詐尸還是另有隱情掖肋,我是刑警寧澤,帶...
    沈念sama閱讀 35,742評(píng)論 5 346
  • 正文 年R本政府宣布赏参,位于F島的核電站志笼,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏把篓。R本人自食惡果不足惜纫溃,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望韧掩。 院中可真熱鬧紊浩,春花似錦、人聲如沸疗锐。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,944評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)滑臊。三九已至口芍,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間简珠,已是汗流浹背阶界。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,060評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工虹钮, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留聋庵,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,247評(píng)論 3 371
  • 正文 我出身青樓芙粱,卻偏偏與公主長(zhǎng)得像祭玉,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子春畔,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容