Categories
程式開發

从开源项目中总结出的几条编码经验


一、背景

之前从事过几年 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 processLogcat(List rawLogcat) {
   List out = new ArrayList(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 = 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
http2HeadersList(Request request) { Headers headers = request.headers(); List
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 等等其他集合类来说也都是类似的。代码雷同,也就不再赘述。

三、总结

对这些编码细节上的考究,很难对业务性能指标产生可量化的提升。更有意义的点在于,我们在实际开发时,避免不了要经常参考开源项目对一些功能的实现。如果不了解这些实现细节,当读到这些代码的时候,难免对细节产生疑惑,干扰我们去理解核心实现思路。反过来说,如果我充分了解了这些细节,当读到它们的时候,往往会泯然一笑,心说我知道作者为什么要这样写,赞赏作者对代码实现的优雅,对这些开源项目的作者也产生出充分的认同感。

作者 | 阿里文娱无线开发专家 观竹