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

发布于:2020 年 5 月 17 日 07:58

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

一、背景

之前从事过几年 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 等等其他集合类来说也都是类似的。代码雷同,也就不再赘述。

三、总结

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

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

阅读数:706 发布于:2020 年 5 月 17 日 07:58

评论

发布
暂无评论