由客戶端內部通訊引發的插件化開發的隨想和實踐

来源:http://www.cnblogs.com/youngytj/archive/2017/08/30/7455829.html
-Advertisement-
Play Games

背景 最近在寫一個基於Android的IPC實現的一個小工具,主要實現的就是能夠在手機查看被監視程式的值的變化和日誌等。因為用了入侵的方式,所以需要被監視APK集成一個SDK。程式界面一覽: <! 工程結構以及SDK簡單示例: ! 大概還是一個半成品的樣子,後續會寫一些Root以後才有的功能。 遇到 ...


背景

最近在寫一個基於Android的IPC實現的一個小工具,主要實現的就是能夠在手機查看被監視程式的值的變化和日誌等。因為用了入侵的方式,所以需要被監視APK集成一個SDK。程式界面一覽:






大概還是一個半成品的樣子,後續會寫一些Root以後才有的功能。

遇到的問題

在實際的開發中,因為主程式中會包含各個集成SDK的Client端的數據,所以主程式的數據接受到最後UI的呈現,就面臨了一個傳輸的選擇。在客戶端的開發中,我們解決內部的通信問題一般有三種方式:

  • 基於事件匯流排(EventBus, PubSubEvent等);
  • 協議化(內建Server,接收方和發送方約定好數據格式);
  • 介面。

第一種方式極大的提高了我們的開發效率,但是後續帶來整體代碼的惡化簡直是我們維護代碼調試代碼的災難。第二種方式內建Server的話,確實很好的解決了我們耦合的問題,但是也帶來了一些問題,其一是性能,數據模型的轉換在頻繁的通信中會帶來性能的損耗,其二是開發效率,因為是協議傳輸,所以一方有變更,另一方也要做相應的變更。那第三種方式介面,因為本身是強引用,所以易於調試和維護,其次也沒有數據模型的轉換。所以我傾向於用介面去解決數據傳輸的問題。

過往的經驗

在過往開發桌面端的經驗中,我們大量運用依賴註入的方式來解決模塊與模塊的耦合問題,同時也可以用來解決模塊與倉儲之間傳輸數據的問題。這也是傳統的桌面端開發中,插件式開發的經典實現。用之前寫過的一個程式舉個例子:

07

整個工程的結構是這樣的:MailAccount作為主程式,MailAccount.InterfaceMailAccount唯一的引用項,MailAccount.Extra.TrialMailAccount.Extra.Standard是完全獨立的dll的項目。

整個程式實現的效果是這樣的,當MailAccount的exe文件運行時,如果目錄下沒有任何其他的dll,則不運行任何內容,只是一個空白頁面,但是當目錄下有Trial或者Standard任意一個dll時,則運行相應dll中的內容。

首先看下Trail和Standard的實現:

namespace MailAccount.Extra.Trial
{
    [Export("Trial", typeof(IUserAction))]
    public class UserTrial : IUserAction
    {
        public bool DoWork<T>(IEnumerable<T> source)
        {
            if(source.Count() > 5)
            {
                return false;
            }
            return true;
        }
    }
}
namespace MailAccount.Extra.Standard
{
    [Export("Standard", typeof(IUserAction))]
    public class UserStandard : IUserAction
    {
        public bool DoWork<T>(IEnumerable<T> source)
        {
            return true;
        }
    }
}

Trail和Standard都實現了IUserAction的介面,並對其中功能做了自己的具體實現。

再看下MailAccount中啟動的時候做了什麼:

public class Bootstrapper
    {
        private const string SEARCH_PATTERN = "MailAccount.Extra.*.dll";

        protected CompositionContainer MainContainer { get; private set; }

        public void Run()
        {
            Container container = this.CreateContainer();

            InfrastructureCatalog baseCatalog = this.CreateBaseCatalog();

            VarifyAndLoadBaseCatalog(baseCatalog, container);

            container.Configure();

            this.MainContainer = container.MainContainer;

            CreateMainWindow();
        }

        private Container CreateContainer()
        {
            return new Container();
        }

        private InfrastructureCatalog CreateBaseCatalog()
        {
            InfrastructureCatalog baseCatalog = new InfrastructureCatalog();
            baseCatalog.Add(this.GetType().Assembly);
            DirectoryInfo dirInfo = new DirectoryInfo(@".\");
            foreach (FileInfo fileInfo in dirInfo.EnumerateFiles(SEARCH_PATTERN))
            {
                try
                {
                    baseCatalog.Add(fileInfo.FullName);
                }
                catch(Exception ex)
                {
                    LogHelper.Error(ex.Message);
                }
            }


            return baseCatalog;
        }

        private void VarifyAndLoadBaseCatalog(InfrastructureCatalog baseCatalog, Container container)
        {
            if (baseCatalog != null && baseCatalog.Items != null)
            {
                foreach (AssemblyCatalog catalog in baseCatalog.Items.Distinct())
                {
                    if (container.AggregateCatalog.Catalogs.FirstOrDefault(c => c.ToString() == catalog.ToString()) == null)
                    {
                        container.AggregateCatalog.Catalogs.Add(catalog);
                    }
                }
            }
        }

        public void CreateMainWindow()
        {
            MainWindow mainWindow = MainContainer.GetExportedValue<MainWindow>("MainWindow");

            IUserAction trial = null;
            IUserAction standard = null;

            try
            {
                trial = MainContainer.GetExportedValue<IUserAction>("Trial");
            }
            catch(Exception ex)
            {
                LogHelper.Error(ex.Message);
            }

            try
            {
                standard = MainContainer.GetExportedValue<IUserAction>("Standard");
            }
            catch (Exception ex)
            {
                LogHelper.Error(ex.Message);
            }

            if (standard != null)
            {
                mainWindow.userAction = standard;
            }
            else if(trial != null)
            {
                mainWindow.userAction = trial;
            }
            else
            {
                MessageBox.Show("程式驗證失敗");
                Environment.Exit(0);
            }

            Application.Current.MainWindow = mainWindow;
            Application.Current.MainWindow.Show();
        }
    }
}

在MailAccount啟動時,我們初始化一個容器,然後遍歷當前目錄下的與MailAccount.Extra.*.dll能匹配的dll,在放入到容器中。因為我們在dll中顯式的Export並聲明瞭key為Standard,所以容器能夠在MainContainer.GetExportedValue<IUserAction>("Standard")的時候找到這個類並初始化。當然我們也可以生成新的符合要求的dll,實現熱拔插,當然這是後話。

Android中實踐的前期準備

因為過往的經驗,所以我在檢索Android這邊信息的時候,是嘗試用插件化或者組件化這種字眼來搜索的。但是我卻發現了一個有趣的現象,在Android這個圈子裡,組件化或者插件化,大家都預設這個技術是用來實現熱更新的,並且當我看了各個開源repo的開始文檔後,發現總有各種限制或者缺陷,比如在特定手機上無法進行資源轉換,不支持Activity(process、configChanges)的部分屬性等等。總之真正的工程實踐中會面臨很多缺陷。

08

所以我放棄了這些開源repo,繼續走依賴註入框架的方式。在Android的開發中,我們常用的依賴註入的框架其實有兩個RoboGuiceDagger。不過真正意義上講,雖然Dagger2也叫Dagger,但是開發商變了(square->google),實現方式也變了(運行時反射->編譯時生成),所以可以理解為我們其實有三個依賴註入的框架可選。在框架的選擇上,我最終還是選擇了Dagger2,原因有兩個,第一個是大廠質量能保證,第二個是我不存在熱拔插的需求,運行時編譯生成能提高程式的運行效率。

實踐

選擇完依賴註入的框架後,我定義了整個程式的層次結構。每一個框都作為一個獨立的Module存在,方便單獨的Module的管理。那麼我在Plugin這塊是怎麼實踐的呢?假設我們要新增一個plugin,我需要做些什麼?正如開始所說的,因為通信方式中,我選擇了使用介面,所以我首先要定義一個介面。以出參監視功能為例,我需要定義一個IOutParaPlugin介面:

public interface IOutParaPlugin extends IPlugin {
    boolean isGathering();
    void setIsGathering(boolean isGathering);
    void registerOutPara(OutPara outPara);
    void setOutPara(OutPara outPara, String value);
    void clientDisconnect(String pkgName);
}

IPlugin是我們所有插件的介面類,其主要功能是提供功能的名稱和功能入口UI:

public interface IPlugin {
    String getPluginName();
    Fragment getPluginFragment();
    ILBApp getApp();
}

這個定義的介面放置在我們的Abs的Module中,我們可以新建一個plugin.op的module來實現我們的功能。

除了實現了基本的Plugin的功能外,我們還要聲明一個module的類來供Dagger生成編譯時的信息。

@Module
public abstract class OutParaModule {
    @Provides
    @Named(AliasName.OUT_PARA_PLUGIN)
    @Singleton
    public static IOutParaPlugin provideOutParaPlugin(ILBApp app,
                                                      @Named(AliasName.OUT_PARA_BRIDGE) Lazy<UIOutParaBridge> outParaBridgeLazy) {
        return new OutParaPlugin(app, outParaBridgeLazy);
    }

    @Provides
    @Named(AliasName.OUT_PARA_BRIDGE)
    @Singleton
    public static UIOutParaBridge provideOutParaBridge(ILBApp app,
                                                       @Named(AliasName.CLIENT_MANAGER) IClientManager clientManager,
                                                       @Named(AliasName.OUT_PARA_PLUGIN) Lazy<IOutParaPlugin> outParaPluginLazy) {
        return new UIOutParaBridge(app, clientManager, outParaPluginLazy);
    }

    @PreActivity
    @ContributesAndroidInjector
    abstract OutParaDetailActivity outParaDetailActivityInjector();

    @PreFragment
    @ContributesAndroidInjector
    abstract OutParaFragment outParaFragmentInjector();
}

在Module中,我們定義了我們的OutParaPlugin在外部可以被註入,當然我們還給了它一個別名,當有多處註入不同實現IOutParaPlugin類時,我們可以用別名來區分。

定義完Module以後,我們要在主app中引用:

@Singleton
@Component(modules = {
        LBAppModule.class,
        ClientModule.class,
        OutParaModule.class,
        InParaModule.class,
        LogModule.class,
        FloatingModule.class
})
interface LBComponent extends AndroidInjector<LBApp> {
    @Component.Builder
    abstract class Builder extends AndroidInjector.Builder<LBApp> {

    }
}

這樣,在主app的MainActiviy中我們可以這樣引用:

@Inject @Named(AliasName.OUT_PARA_PLUGIN) IOutParaPlugin outPlugin;

在Activity被onCreate的時候註入並獲取OutParaPlugin的實例:

@Override
protected void onCreate(Bundle savedInstanceState) {
    AndroidInjection.inject(this);
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    ButterKnife.bind(this);
    active = true;

    initPlugin(outPlugin, inPlugin, logPlugin);
    initDrawer();
    initFragment();

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        requestDrawOverLays();
    }
}

此時,我們獲取plugin後,可以用getPluginName()初始化側滑欄的菜單,當用戶點擊時,再通過getPluginFragment()進入Plugin內部相關的UI。我們也可以實現新的IOutParaPlugin的module來快速替換現有的plugin。

總結

將工程插件化,是約束代碼邊界的一個很好的實踐。將龐大的工程拆分成一個個子工程,從編譯上做到隔離,將不同的工程交由不同的人負責,避免了相互之間代碼更改,同時提高了代碼的可維護性。

參考信息

微信Android模塊化架構重構實踐
Prism6下的MEF:第一個Hello World
Android Dagger (2.10/2.11) Butterknife MVP
Prism PubSub Event


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 第三部分:流程式控制制語句 JavaScript代碼是書寫位置: JavaScript代碼應該寫在<script type=”text/javascript”></script>這一對標記中。 或者作為外部引用<script src="JavaScript代碼路徑"></script> JavaScri ...
  • 最近幾年隨著響應式佈局的發展,一次開發多次使用,自適應屏幕的響應式網站的需求越來越多。但是怎樣使得網站能自適應屏幕呢?這裡就需要提到一個css3裡面新增的技術了-media媒體查詢器。 那麼什麼是media媒體查詢器呢? Media媒體查詢器是CSS3新增的一個可以檢測打開網站的終端的屏幕解析度的技 ...
  • 以前我也是老搞不懂a++和++a的區別, 後來看了很多資料, 終於總結出來一條規律, 小白專用! 看完這個例子就懂了: 例1:$a = 8, 求 ++a + a++ - --a + a-- + ++a得多少? 舊值: 8 9 10 9 8 ++a + a++ - --a + a-- + ++a 新值 ...
  • 普及:瀏覽器的相容性問題,往往是個別瀏覽器(沒錯,就是那個與眾不同的瀏覽器)對於一些標準的定義不一致導致的。俗話說:沒有IE就沒有傷害。 貼士:內容都是自己總結的,不免會出現錯誤或者bug,歡迎更正和補充,本帖也會不斷更新。 Normalize.css 不同瀏覽器的預設樣式存在差異,可以使用 Nor ...
  • 最終效果圖: ...
  • ...
  • jYD是一個類似於jQuery的框架,包含常用的功能:如Dom操作,事件,樣式,表單和Ajax交互。 ...
  • 寫在前面 本文只是個人在熟悉 的一些個人筆記,因此我只根據我自己的情況來熟悉 ,所以很多 並沒有被列舉到,比如常規的 等等操作,這些 我認為只要你自己稍微看一下 "官網" 的介紹都可以知道怎麼用。本文所有的代碼請參看本人的 地址 "https://github.com/Rynxiao/immutab ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...