背景 最近在寫一個基於Android的IPC實現的一個小工具,主要實現的就是能夠在手機查看被監視程式的值的變化和日誌等。因為用了入侵的方式,所以需要被監視APK集成一個SDK。程式界面一覽: <! 工程結構以及SDK簡單示例: ! 大概還是一個半成品的樣子,後續會寫一些Root以後才有的功能。 遇到 ...
背景
最近在寫一個基於Android的IPC實現的一個小工具,主要實現的就是能夠在手機查看被監視程式的值的變化和日誌等。因為用了入侵的方式,所以需要被監視APK集成一個SDK。程式界面一覽:
大概還是一個半成品的樣子,後續會寫一些Root以後才有的功能。
遇到的問題
在實際的開發中,因為主程式中會包含各個集成SDK的Client端的數據,所以主程式的數據接受到最後UI的呈現,就面臨了一個傳輸的選擇。在客戶端的開發中,我們解決內部的通信問題一般有三種方式:
- 基於事件匯流排(EventBus, PubSubEvent等);
- 協議化(內建Server,接收方和發送方約定好數據格式);
- 介面。
第一種方式極大的提高了我們的開發效率,但是後續帶來整體代碼的惡化簡直是我們維護代碼調試代碼的災難。第二種方式內建Server的話,確實很好的解決了我們耦合的問題,但是也帶來了一些問題,其一是性能,數據模型的轉換在頻繁的通信中會帶來性能的損耗,其二是開發效率,因為是協議傳輸,所以一方有變更,另一方也要做相應的變更。那第三種方式介面,因為本身是強引用,所以易於調試和維護,其次也沒有數據模型的轉換。所以我傾向於用介面去解決數據傳輸的問題。
過往的經驗
在過往開發桌面端的經驗中,我們大量運用依賴註入的方式來解決模塊與模塊的耦合問題,同時也可以用來解決模塊與倉儲之間傳輸數據的問題。這也是傳統的桌面端開發中,插件式開發的經典實現。用之前寫過的一個程式舉個例子:
整個工程的結構是這樣的:MailAccount
作為主程式,MailAccount.Interface
是MailAccount
唯一的引用項,MailAccount.Extra.Trial
和MailAccount.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)的部分屬性等等。總之真正的工程實踐中會面臨很多缺陷。
所以我放棄了這些開源repo,繼續走依賴註入框架的方式。在Android的開發中,我們常用的依賴註入的框架其實有兩個RoboGuice和Dagger。不過真正意義上講,雖然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