【Chromium中文文檔】跨平臺開發的約定與模式

轉載請注明出處:
https://ahangchen.gitbooks.io/chromium_doc_zh/content/zh/General_Architecture/Conventions_and_patterns_for_multi-platform_development.html

全書地址

Chromium中文文檔 for https://www.chromium.org/developers/design-documents
持續更新ing,歡迎star
gitbook地址:https://ahangchen.gitbooks.io/chromium_doc_zh/content/zh//
github地址: https://github.com/ahangchen/Chromium_doc_zh

Chromium是一個巨大而復雜的跨平臺產品。我們試圖在不同平臺間共享盡可能多的代碼,同時為每個平臺用最合適的方式實現UI和操作系統集成。這提供了一個更好的用戶體驗,但它給代碼增加了額外的復雜度。這個文檔描述了保持這種跨平臺代碼簡潔性的推薦實踐。

我們使用大量不同帶后綴的文件來表示一個文件應該被使用的時機:

  • Mac文件中,低層級文件使用_mac后綴,Cocoa(Mac UI)文件使用_cocoa后綴。
  • iOS文件使用_ios后綴(盡快iOS使用一些特定的_mac文件)。
  • Linux文件中,低層級文件使用_linux后綴,GTK相關文件使用_gtk后綴,X Windows(不使用GTK)特定文件使用_x后綴。
  • Windows文件使用_win后綴。
  • Mac,iOS和Linux共享的Posix文件使用_posix后綴。
  • Chrome view UI相關布局系統文件(在Windows和實驗室環境GTK上)使用_views后綴。

獨立的瀏覽器后端文件放在他們自己的目錄里:

  • Mac Cocoa: chrome/browser/ui/cocoa
  • Linux GTK: chrome/browser/ui/gtk
  • Windows Views (和實驗室GTK-views): chrome/browser/ui/views

編碼風格 頁面列出一些風格上影響平臺相關定義的規則。

如何隔離平臺相關代碼

小的平臺差異: #ifdefs

當你有一個有著許多共享函數或數據成員和些許不同之處的類,在平臺相關的部分使用#ifdefs。如果沒有顯著的差異,這會讓每個人將每件事隔離開更加容易。

小的平臺差異在頭文件處理,大的差異在實現中處理:片段實現

可能有這樣的情況,頭文件幾乎沒有差別,部分實現有巨大的實現差異。例如base/waitable_event.h定義了一個通用的有著大量平臺差異的API。

有著顯著的實現差異,實現文件可以被隔離出來。這可以避免你陷入一個必須在include必要文件中為每個平臺寫一大堆#ifdef,并且使得追蹤源碼更容易(三個版本的函數集的代碼放在同一個文件里可能令人困惑)。每個平臺可以有不同的.cc文件,正如base/waitable_event_posix.cc中實現posix相關函數。如果在這個類里有跨平臺的函數,他們應該被丟到一個名為base/waitable_event.cc的函數。

全平臺實現和調用器:隔離實現

當抽象層面沒有東西實現,就要在每個單獨的文件里分別實現類。

如果所有的實現都在跨平臺目錄中,比如base,他們應該用平臺的名字命名,比如base/foo_bar_win.h中的FooBarWin。這種例子通常很少,因為這些跨平臺的文件通常設計用于跨平臺代碼,獨立的頭文件使得這種例子變得不可能。在一些地方,我們已經在不同的文件里定義了一個普通命名的類,所以PlatformDevice定義在skia/ext/platform_device_win.h, skia/ext/platform_device_linux.h, and skia/ext/platform_device_mac.h。如果你真的需要在跨平臺代碼里引用這個類,這是OK的。但通常,這種例子會變得遵循下面的這種規則。

如果實現存在于平臺相關目錄,比如chrome/browser/ui/cocoa或chrome/browser/ui/views,這個類就沒有機會用于跨平臺代碼了。這種情況下,這個類和文件名應該忽略平臺的名字,因為它是多余的。所以FooBar是在chrome/browser/ui/cocoa/foo_bar.h中實現的。

不要為每個平臺創建不同的類,又把它們用typedef定義為同一個名字。我們曾經在PlatformCanvas上使用這種套路,根據平臺,它被typedef為PlatformCanvasMac, PlatformCanvasLinux, 或 PlatformCanvasWin。這樣就不可能提前聲明這個類,而這是一個減小依賴的重要工具。

什么時候使用抽象的接口

通常,抽象接口與工廠不應該作為隔離平臺差異的唯一目的。相反的,它應該用于將接口與優化代碼設計的實現隔離開來。這最經常出現在從model中抽離view的實現中,比如TabContentsView或者RenderWidgetHostView。在這些例子里,model不依賴view的實現是有必要的。在許多情況下,多個平臺的view只有一個實現,但為將來的開發提供了更干凈的隔離與更多的可擴展性。

在有些地方,像TabContentsView,抽象層沒有非抽象的、在平臺間共享的函數。避免這種寫法。如果不同view之間的代碼總是一樣,它可能首先就不應該在view中。

實現平臺相關的UI

通常,從已有的平臺相關的用戶界面元素構建其他平臺相關的用戶界面元素。例如,view相關的類BrowserView負責構建許多瀏覽器對話框盒子。一種方法是,在一個平臺無關的接口里包裝UI元素,然后通過一個工廠,從一個model構造出它來。這是相當不必要的,因為它讓迷亂了歸屬關系:大多數工廠構造的例子里,UI元素最后歸屬于創建它的model。然而在許多例子里,UI元素最容易由它所屬的UI框架管理。例如,一個views::View歸屬于它的view層級,并且在包含它的window被銷毀時,會自動被銷毀。如果有一個對話框 views::View實現了一個平臺無關的接口,然后被另一個對象擁有,那么views::View實例現在需要顯式地告訴它的view層級不要去干涉它的生命周期。

e.g. 推薦這種寫法:

// browser.cc:

Browser::ExecuteCommand(..) {
  ...
  case IDC_COMMAND_EDIT_FOO:
    window()->ShowFooDialog();
    break;
  ...
}

// browser_window.h:

class BrowserWindow {
...
  virtual void ShowFooDialog() = 0;
...
};

// browser_view.cc:

BrowserView::ShowFooDialog() {
  views::Widget::CreateWindow(new FooDialogView)->Show();
}

// foo_dialog_view.cc:

// FooDialogView和FooDialogController在window被關閉的時候會被自動清理
class FooDialogView : public views::View {
  ...
 private:
  scoped_ptr<FooDialogController> controller_; // 跨平臺狀態控制邏輯
  ...
}

不推薦這種


// browser.cc:

Browser::ExecuteCommand(..) {
  ...
  case IDC_COMMAND_EDIT_FOO: {
    FooDialogController::instance()->ShowUI();
    break;
  }
  ...
}

// foo_dialog_controller.h:

class FooDialog {
 public:
  static FooDialog* CreateFooDialog(FooDialogController* controller);
  virtual void Show() = 0;
  virtual void Bar() = 0;
};

class FooDialogController {
 public:
  ...
  static FooDialogController* instance() {
    static FooDialogController* instance = NULL;
    if (!instance)
      instance = Singleton<FooDialogController>::get();
    return instance;
  }
  ...
 private:
  ...
  void ShowUI() {
    if (!dialog_.get())
      dialog_.reset(FooDialog::CreateFooDialog(this));
    dialog_->Show();
  }

  // 為什么要把FooDialog或者甚至FooDialogController放在外面?
  // 大多數dialog起始很少被用到
  scoped_ptr<FooDialog> dialog_;
};

// foo_dialog_win.cc:

class FooDialogView : public views::View,
                      public FooDialogController {
 public:
  ...
  explicit FooDialogView(FooDialogController* controller) {
    set_parent_owned(false); // Now necessary due to scoped_ptr in FooDialogController.
  }
  ...
};

FooDialog* FooDialog::CreateFooDialog(FooDialogController* controller) {
  return new FooDialogView(controller);
}

有時候后一種模式是必要的,但這些情況很稀少,并且非常容易被前端的團隊所理解。移植的時候,如果UI元素有時候像dialog box一樣簡單的話,考慮把后一種模式轉為前一種。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容