從開源項目中總結出的幾條編碼經驗

一、背景

之前從事過幾年 chromium(chrome 瀏覽器內核)和 android framework 的維護開發工作,這兩個項目在開源界無論從應用範圍、設計模式、技術深度等都是出類拔萃的項目。通過閱讀這些優秀的源碼,摘錄出一些優秀的代碼片段和編碼技巧。最近兩年把這些“片段”放到應用層的開發工作上,不僅在代碼細節上有些許的性能提高,也能讓項目的代碼風格向這些頂尖項目靠近。同時,熟悉這些編碼風格後,當我們在翻閱這些開源項目源碼時,也能在一定程度上減少閱讀障礙。

下面分享幾個摘錄出來的代碼片段,再結合着這些代碼在優酷項目上的使用,進行一一說明。希望對大家的開發工作起到一些借鑑意義。

二、使用註解,保證方法入參的合法性

當模塊對外暴露一些 API 時,特別是輸出 SDK 給外界使用時,爲了保證調用方對方法入參的合法性,使用註解的方式來完成是個很好的解決方式,也可以減少不同模塊開發人員間的溝通成本。

  1. 先來看看 chromium 使用註解的實際案例
public final class ViewportFit {
   private static final boolean IS_EXTENSIBLE = false;

   public static final int AUTO = 0;
   public static final int CONTAIN = 1; // AUTO + 1
   public static final int COVER = 2; // CONTAIN + 1
   public static final int COVER_FORCED_BY_USER_AGENT = 3; // COVER + 1

   public static boolean isKnownValue(int value) {
      return value >= 0 && value <= 3;
   }

   public static void validate(int value) {
      if (IS_EXTENSIBLE || isKnownValue(value)) return;
      throw new org.chromium.mojo.bindings.DeserializationException("Invalid enumvalue.");
   }

   private ViewportFit() {}
}

(https://cs.chromium.org/chromium/src/content/public/android/java/src/org/chromium/content_public/browser/WebContentsObserver.java)
   /**
    * The Viewport Fit Type passed to viewportFitChanged. This is mirrored
    * in an enum in display_cutout.mojom.
    */
   @Retention(RetentionPolicy.SOURCE)
   @IntDef({ViewportFit.AUTO, ViewportFit.CONTAIN, ViewportFit.COVER})
   public @interface ViewportFitType {}

之後在使用上面的註解修飾方法的入參,(https://cs.chromium.org/chromium/src/content/public/android/java/src/org/chromium/content/browser/webcontents/WebContentsObserverProxy.java?q=webcontentsobserverproxy.java

   @Override
   @CalledByNative
   public void viewportFitChanged(@WebContentsObserver.ViewportFitType int value) {
      for (mObserversIterator.rewind(); mObserversIterator.hasNext();) {
         mObserversIterator.next().viewportFitChanged(value);
      }
   }

對於 viewportFitChanged()這個方法來說,通過使用@WebContentsObserver.ViewportFittype對入參進行修飾,在編譯期檢查參數合法性,在方法內部也就不再需要對參數的合法性進行檢查。

  1. 再來看看 github 上的一個項目對註解的使用
public class DiagonalLayoutSettings {

   @Retention(SOURCE)
   @IntDef({ BOTTOM, TOP, B_T})
   public @interface Position {
   }

   public final static int LEFT = 1;
   public final static int RIGHT = 2;
   public final static int BOTTOM = 4;
   public final static int TOP = 8;
   public final static int B_T = 16;

   @Retention(SOURCE)
   @IntDef({ DIRECTION_LEFT, DIRECTION_RIGHT })
   public @interface Direction {
   }

   public final static int DIRECTION_LEFT = 1;
   public final static int DIRECTION_RIGHT = 2;

   ...

}

用註解去修飾方法參數.

public class DiagonalLayout extends FrameLayout {

   DiagonalLayoutSettings settings;


   public void setPosition(@DiagonalLayoutSettings.Position int position) {
      settings.setPosition(position);
      postInvalidate();
   }

}

setPosition 這個方法,通過註解來限制參數取值範圍的作用很清晰了,不再贅述.

  1. 註解在優酷上的使用

舉一個例子,去年我們和 UC 有個漫畫合作項目,優酷輸出端側 SDK 給 UC 集成,並且同一套 SDK 也要在優酷中使用。因此,SDK 在初始化時,需要把集成方的標識設定進來。在設計給 UC 方調用的 API 時,就使用到了註解修飾參數的方法來避免集成方對 API 的調用錯誤。

   public void init(@NonNull Context context, @ConfigManager.Key String key, @NonNullIAppConfigAdapter appConfigAdapter,
   IUiAdapter uiAdapter, @NonNull INetAdapter netAdapter, IPayViewAdapterpayViewAdapter, IPayAdapter payAdapter,
   @NonNull IUserAdapter userAdapter, IWebViewAdapter webViewAdapter,
   @NonNull IComicImageAdapter imageAdapter) {
      ...
   }

在這裏對參數 key,使用@ConfigManager.Key 做了限制.

註解的定義:

public class ConfigManager {

   /**
   * 分場標識key
   */
   public static final String KEY_YK = "yk";
   public static final String KEY_UC = "uc";

   @Retention(SOURCE)
   @StringDef({KEY_YK, KEY_UC})
   public @interface Key {
   }

}

優酷場對這個 API 的調用:

private void initAliComicSdk() {
      AliComicSDKEngine.getInstance().init(instance, ConfigManager.KEY_YK, newIAppConfigAdapterImpl(),
         new IUiAdapterImpl(), new INetAdapterImpl(), new IPayViewAdapterImpl(), null,
         new IUserAdapterImpl(), new IWebViewAdapterImpl(), newIComicImageAdapterImpl());
   }

三、以指定初始容量的方式來創建集合類對象

以 ArrayList 爲例,通常我們創建對象時,使用 new ArrayList<>()是最常用的方式. 當我們閱讀 chromium 或是像 okhttp 這些開源代碼時會發現它們在構建 ArrayList 對象時,會有意識的使用 ArrayList(int initialCapacity)這個構造方法,“刻意”使用這種方式的原因其實是值得我們細細品味一下的。

  1. 還是以 chromium 爲例,摘取一段它的源碼.
protected static List<String> processLogcat(List<String> rawLogcat) {
   List<String> out = new ArrayList<String>(rawLogcat.size());
   for (String ln : rawLogcat) {
      ln = elideEmail(ln);
      ln = elideUrl(ln);
      ln = elideIp(ln);
      ln = elideMac(ln);
      ln = elideConsole(ln);
      out.add(ln);
   }
   return out;
}

再直接看ArrayList 兩種構造方法的源碼, 無參方法會默認創建10 個元素的list.

/**
* Constructs an empty list with the specified initial capacity.
*
* @param initialCapacity the initial capacity of the list
* @throws IllegalArgumentException if the specified initial capacity
* is negative
*/
public ArrayList(int initialCapacity) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
this.elementData = new Object[initialCapacity];
}
/**
* Constructs an empty list with an initial capacity of ten.
*/
public ArrayList() {
super();
this.elementData = EMPTY_ELEMENTDATA;
}

兩者的區別就在於,當我們往 arrayList 中添加元素髮現容量不夠時,它會通過調用 grow() 方法來擴容。grow()內部會以之前容量爲基準,擴大一倍容量,併發生一次“耗時”的數組拷貝。因此當業務上預知 ArrayList 未來要存儲大量元素時,更優雅的方式是在創建時設置初始容量,以此來避免未來內存上的頻繁拷貝操作。

/**
 * Increases the capacity to ensure that it can hold at least the
 * number of elements specified by the minimum capacity argument.
 *
 * @param minCapacity the desired minimum capacity
 */
private void grow(int minCapacity) {
   // overflow-conscious code
   int oldCapacity = elementData.length;
   int newCapacity = oldCapacity + (oldCapacity >> 1);
   if (newCapacity - minCapacity < 0)
      newCapacity = minCapacity;
   if (newCapacity - MAX_ARRAY_SIZE > 0)
      newCapacity = hugeCapacity(minCapacity);
   // minCapacity is usually close to size, so this is a win:
   elementData = Arrays.copyOf(elementData, newCapacity);
}
  1. 再來看一下 okhttp 中的例子

以 request 中 headers 的 size+4 作爲初始容量來創建 ArrayList 對象,因爲運行時這個 result list 內部幾乎每次都是要大於 10 個元素的。對於像 okhttp 這種廣泛被使用的 sdk 來說,任何對代碼細節的調優都是有可觀收益的,同時也體現出作者對代碼細節的考究。

public static List<Header> http2HeadersList(Request request) {
 Headers headers = request.headers();
 List<Header> result = new ArrayList<>(headers.size() + 4);
 result.add(new Header(TARGET_METHOD, request.method()));
 result.add(new Header(TARGET_PATH, RequestLine.requestPath(request.url())));
 String host = request.header("Host");
 if (host != null) {
  result.add(new Header(TARGET_AUTHORITY, host)); // Optional.
 }
 result.add(new Header(TARGET_SCHEME, request.url().scheme()));
 for (int i = 0, size = headers.size(); i < size; i++) {
  // header names must be lowercase.
  String name = headers.name(i).toLowerCase(Locale.US);
  if (!HTTP_2_SKIPPED_REQUEST_HEADERS.contains(name)
    || name.equals(TE) && headers.value(i).equals("trailers")) {
   result.add(new Header(name, headers.value(i)));
  }
 }
 return result;
}

因此在我們的優酷項目中,當每次要創建 ArrayList 時,都會下意識的想想業務上在使用這個 ArrayList 時,未來大致要存儲多大量級的數據,有沒有必要設置它的初始容量。

上面說的這些,不僅是對 ArrayList 有效,對像 StringBuilder 等等其他集合類來說也都是類似的。代碼雷同,也就不再贅述。

三、總結

對這些編碼細節上的考究,很難對業務性能指標產生可量化的提升。更有意義的點在於,我們在實際開發時,避免不了要經常參考開源項目對一些功能的實現。如果不瞭解這些實現細節,當讀到這些代碼的時候,難免對細節產生疑惑,干擾我們去理解核心實現思路。反過來說,如果我充分了解了這些細節,當讀到它們的時候,往往會泯然一笑,心說我知道作者爲什麼要這樣寫,讚賞作者對代碼實現的優雅,對這些開源項目的作者也產生出充分的認同感。

作者 | 阿里文娛無線開發專家 觀竹

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章