Google IO 2014 Android ART Runtime 讲解(英文字幕)

 

http://v.youku.com/v_show/id_XODQ5MjE0NDU2.html

(推荐全屏观看。由于目前还在转码中,所以现在还只支持标清。理论上是可以超清的。)

为什么只有英文字幕?

  1. 我懒
  2. 我之前很早之前尝试过在翻译Google IO视频的字幕,后面发现很多英文就我来说根本翻不出我理解的意思,于是干脆作罢
  3. 逼着自己用英文去理解,同时学习英文的技术思维,顺便锻炼英文,反正看美剧也差不多呢,看这个逼格更高。

视频内容简介

ART 是Android runtime 的一次比较大的改进,ART是在Android 4.4 开始成为可选的runtime(还有Dalvik)。ART带来的改进主要体现在垃圾回收算法,线程,锁模型,编译器和runtime的性能等方面。这个视频的主要内容就是对这些改进进行讲解。

演讲人介绍

Anwar Ghuloum

Facebook工程师是如何改进他们Android客户端的

safe_image

本文来源于 Facebook工程师博客

作为世界上最大的社交网络,Facebook的Android客户端面临着各种各样的使用环境(地理环境、Android设备以及移动网络等环境的差异)。也正是这个原因,为了检测自家Android客户端在发展中国家的性能表现,Android的产品经理、工程师在2013年的时候去了一趟非洲。当时我看到这个新闻的时候觉得有点怪异,后来看到他们这篇博客才有点理解他们这样做的原因了。

这群Facebook的工程师来到非洲之后,并在当地使用Facebook的最新版本的Android客户端。测试的结果的确让他们印象深刻:

  1. 当地的网络环境十分糟糕,App经常中断网络连接。
  2. 当地人民使用的Android设备内存小,导致App加载缓慢,而且经常崩溃。
  3. 他们的月流量在40分钟之内就用完了。

经过这个让人印象深刻的测试的之后,Facebook的工程师们开始对他们的Android客户端进行了一系列的优化。

性能优化

这里主要是改进了App在低端机上的性能问题。

  • 问题:单核的Android手机在启动Facebook的时候更慢,这是因为app在启动的时候并行初始化了多个模块。
    解决方案:在单核手机上延缓这些初始化过程到启动之后,甚至只有在某个模块要被使用的时候才开始初始化这个模块。
  • 问题:信息流在网络环境差时加载速度慢。
    解决方案:尽早地从服务器抓取信息流数据,用更多的时间来建立连接,并下载信息流的内容。

最终的效果是App的启动时间减少了50%。

数据处理效率的优化

非洲的旅程让工程师们发现流量在发展中国家非常昂贵,而且作为Facebook重要体验一环的照片则是流量花费的大头,于是为了让人民在不担心流量的前提下安心享用Facebook,工程师们决定对App里面的照片动刀:

  1. 寻找现有图片格式的替换者。经过工程师们的调研,在众多的图片格式中,最后工程师选择了Google的WebP。原因很简单:压缩效率高,而且对Android的支持好(毕竟就是Google提出来的)。使用 WebP 之后,相对于JPG格式的图片,流量省了将近 25% 到 35 %;相对于 PNG 格式的图片,流量省了将近80%。最重要的是使用WebP之后图片质量还没改变。
  2. 按照设备处理图片的能力来加载图片。在之前,Facebook的App都是统一加载最大分辨率的图片,这样做是为了让用户可以自由的缩放图片。后来改进之后,app最先加载的图片大小适合显示这个图片窗口大小一样。如果需要缩略图,app就只加载缩略图大小的图片,用户需要更高分辨率的图片,app也能加载,而且之前的统一加载最大分辨率的图片了。
  3. 调整缓存和重用图片的策略。工程师测试了不同的缓存策略,不同的缓存大小,最后综合出最优方案。

最后的效果也是讲流量花费减少了50%。

网络优化

由于许多地区的网络环境比较差,这让Facebook的App的体验也变得十分糟糕,于是工程师也对app的网络效率和可靠性进行了一番改进。

  1. 使用OkHttpFacebook 很早就开始使用Square公司开发的 OkHttp(一个开源的网络协议栈)了,现在Google 官方也从Android 4.4开始使用 OkHttp作为HttpURLConnection的默认实现了。  OkHttp 支持在糟糕的网络环境下面更快的重试,并且还能利用 SPDY 协议进行快速的并发网络请求。
  2. 利用Okhttp调整图片的预先抓取算法,确保app中下载队列前面的图片被优先处理,防止队列阻塞时间过长。

经过优化后,图片加载慢或者加载 失败的反馈少了将近90%。

App文件大小优化

工程师在非洲的时候发现人们使用数量最多的手机磁盘空间很小,也就是说这给用户升级带来的障碍,进而可以推断这些人们因为手机的空间问题而一直使用旧版本的app,那么他们也就无法升级享受前面提到过的优化后的app体验。于是工程师开始致力于如何对app文件大小进行优化:

  1. 利用Google Play提供的功能为不同的Android版本、不同的Android屏幕分辨率的手机提供不同的安装文件。这样就可以在不同的设备上面进行功能的取舍了。
  2. 当然在这个过程中需要监测工具和测试工具来保证优化app文件大小之后app功能的正常性。现在Facebook的工程师已经开发出一套可以计算每个特性对Facebook Android App贡献了多大的空间。

经过优化之后的文件大小减少了将近65%。

反思

Facebook 工程师们的非洲之旅让他们更加理解了移动app性能、数据处理的有效性、网络的可靠性以及app的文件大小对发展中国家移动市场意味着什么。

工程师在这之后开始对每次app新添加的特性都会进行各方面的测试验证,而且Facebook还有一套工具可以直接获得用户对这些特性的反馈,而且工程师开始将这些实践延伸到 Messenger 和 Instagram 的Android App。

 

Luis von Ahn这个神人

p19930693

前面的话

上次看了Fenng的一篇文章,大意说的是拥有较好的写作能力能够帮助自己更好的去思考,看了之后自己挺认同的。

我对自己的写作能力还是相当悲观的,你要知道我是那种高中写作文第一件事是找到800百字的位置,做个标记,然后开始了挤牙膏似的向那个『800百字』标记进军的人;大学每年的学年总结我都是照着标准格式来填的。本身就烂的底子再大学这七年的荒废,可想而知现在我的写作水平到了哪个境界了?所以我写这个博客有一部分原因也是想借此提高自己的写作能力。现在我决定用“随想”这个类别来写一些我觉得有趣、好玩的事情、东西或者人,涉及的领域可能五花八门。

恰好之前我在北京实习的时候,公司要求每个人每周都要进行一次分享,内容不限,当时轮到我的时候,我就向大家分享了一下Luis von Ahn这个人以及他所做的事情。那我今天就把之前的那个分享在充实一下,遂作此文。

验证码

看到CAPTCHA这个单词你熟悉么?嘿嘿,估计我们在编程的时候见过。其实它不是一个单词,它是一个缩略词,它是取 Completely Automated Public Turing Test To Tell Computers and Humans Apart 这几个单词的首写字母组成的(其实我也是刚刚搜出来的)。它翻译出来其实还挺拗口的,叫做全自动区分计算机和人类的图灵测试。

CAPTCHA其实在我们的网络生活中是随处可见,几乎每天都会碰到它。基本上我们所有的网站登录界面都会用到它。没错!它就是『验证码』。所以,验证码还有这个洋气的名字,叫做『全自动区分计算机和人类的图灵测试』。说到这个验证码,我们不得不来了解一下这个验证码背后的男人——Luis von Ahn。

验证码背后的男人

Luis von Ahn是一位卡内基梅隆大学的副教授,他其实最开始是研究加密的,后来开始研究如何区分在网络中人和计算机。

你可以想象一下在Luis von Ahn之前没有验证码的日子吗?买票的网站因为没有验证码几万张票被黄牛通过刷票程序全部买走?网站被人一次性注册几百万个垃圾帐号,然后产生各种垃圾信息,还可以操纵投票,使网站失去了公正性。

这还只是举了简单的几个例子。没有验证码的日子网络世界一片狼藉。Luis von Ahn觉得自己应该可以做点什么,要知道他是研究如何区分在网络中人和计算机的。上面两个例子就是因为有人使用计算机非法的对网站进行操作。

于是Luis von Ahn就想如果我能把人和计算机分开就好了,刚好他的研究里面就有一个方法可以区分计算机和人,那就是人可以用肉眼很容易的识别出图片里面的东西,但是计算机不能。Luis von Ahn就利用这个特性发明了CAPTCHA,也就是『全自动区分计算机和人类的图灵测试』,也就是『验证码』。

那么验证码是怎么让杜绝那些计算机对网站进行非法操作呢?很简单,首先计算机是可以像人一样去模拟登录的,但是当它碰到验证码的时候,计算机就傻眼了,它不认得这个验证码里面是什么东西,但是人的话就一眼认出来,然后输入验证码,就通过啦。就这么简单。人和计算机就被区分开来了。这个小小的发明为世界上的所有网站拦截了大部分的恶意注册和垃圾信息。

游戏还可以这样玩

其实在这之前 Luis von Ahn还有一个研究领域就是如果利用人在上网过程的中作用,说简单点就是如何让人在上网的同时不知不觉的还完成了一些不可思议的事情。于是他基于这个研究开发了一个图片游戏,这个游戏的玩法就是给任意随机的玩家A和B发送同一张图片,然后要A和B在有限的时间内对这个图片进行描述,当这个两个人的描述接近的时候就算这两个人匹配成功,然后获胜得分。

你能想到人在玩这个游戏的时候不知不觉的完成那些任务了么?就像上面说的那样,计算机是很难识别图片,但是人就可以很简单的识别图片,并对图片进行描述,于是乎,人们在玩这个游戏的时候不知不觉的就对这些图片进行了相当精确(因为是两个人同时在想,而且限时,而且必须描述相近)的标记。哈哈哈,想法是不是相当高明。认为这个想法高明的不止有你,还有当时的搜索巨头Google(谷歌)。Google当时就收购了这个游戏,并把这个想法应用到了Google的图片搜索。

验证码的升级版

Luis von Ahn在发明验证码之后,有一段时间变得很郁闷。为啥呢?因为他得到了一些数据——全世界的网民每天数据验证码将近2亿次,而每次验证码的输入时间将近10秒,这样算下来,每天网民要在验证码上面话费50万个小时。Luis von Ahn看着这些数字陷入了沉思,因为自己的发明,网民每天要多花这么多时间,有没有什么办法利用这些时间呢?

『有了!』Luis von Ahn突然在办公室跳起来,他想起之前被Google收购的那个游戏了,他知道该怎么做了。于是Luis von Ahn在CAPTCHA的基础上进一步改进,并把新的验证码叫做『reCAPTCHA』。『re』就是重新的意思嘛。

那么这次他是怎么改的呢?在我看来,Luis von Ahn的这次改进简直就是天才的想法。他的想法就是既然人们在输入验证码的时候有10秒钟的时间,那何不利用这10秒来讲那些古老的书籍或者图片的门牌号给识别出来。

这里需要说一下为什么要进行书籍的电子化。信息时代的一个特点就是要把我们生活的世界进行信息化,尽可能地把一切都可以索引。那么我们把书籍电子化就可以让人们更方便的去查找某一本书的某一句话,而不需要你翻烂一本书都找不到出处。

但是古老的书籍实在信息时代之前出现的,因此需要人为的进行电子化。但是电脑在扫描这些古老的书籍的时候总是错误率很高,根本不能用,这也是图书领域的一个大难题。不过,有了Luis von Ahn的天才想法,这些都不是问题。让我们来看看他是怎么解决这个问题的:

验证码升级版的验证过程

  1. 将古老书籍进行扫描(比如我们扫描《西游记》)
  2. 将扫描得到的图片分成单个词的片段 (将《西游记》扫描的图片按照单个字进行截取)
  3. 系统随机生成一个词A和扫描图片得到的词B组成一个验证码(假如随机生成的词A为『我』,扫描图片的词B为『俺老孙』,当然了用户是不知道『我』还是『俺老孙』哪一个是系统产生的)
  4. 当用户正确输入A之后就会被认为这是人在操作,那么B也就会被认为是人在认这个词(于是只要用户输入正确的A的答案为『我』,那么系统也会人会后面输入词就是扫描图片上面的词了)
  5. 然后B这个词就被人认出来了,以此类推,书就可以被全部认出来了。(以此类推,《西游记》就会被人在输入验证码的时候就被电子化了。)

说了这么多,咱们还是来看看真图(其中『morning』这个词是书里扫描出来的,后面这个『upon』是系统随机产生的,伪装的很像哈):
psb

升级版的验证码效果

那么这个升级版的验证码效果如何呢?当时是有35万个网站使用这个验证码,一天可以数字化1亿个单词,一年可以将250万本古老书籍电子化。这真的是一个功德无量的时候,它可以让老一辈的智慧通过电子化继续流传下去(用书的话说不定哪天就被烧了呢)。

这个天才的想法再次Google收购。唉,Luis von Ahn是名副其实的人生赢家了。

Luis von Ahn在我看来是一个闲不下来的人。按理来说,这个家伙的做的东西两次被Google收购,还是国际顶级名校的副教授,吃穿都应该不愁了吧。可是人家的境界还是比我不知道高到哪里去了,他貌似又看到了一些新的东西。

再次出发,Duolingo教你学外语

Luis von Ahn有一次给他带的研究生出了一个问题——如何让一亿网民免费来将互联网的主要内容翻译成各个主要语种。后来Luis von Ahn带着他的研究生开启一个全新的项目,叫做『Duolingo』,中文名叫做『多邻国』。

这是一个什么样的项目呢? 在Luis von Ahn看来,现在的互联网的优质内容还主要集中在英语,如果想让全球人民来无障碍来享用这些优质内容,这些优质内容必须被翻译成他们相应的语言版本。同时其中一大部分还是很有热情去学习一门外语,甚至花钱都可以。

于是Luis von Ahn又想到一个两全其美的办法让人们即可以免费学习最正宗的外语,还能提供专业级的翻译。他是这样想的:

  1. 首先那些提供优质内容的网站(比如纽约时报,英国广播电台BBC,美国有线电视CNN)会付费把他们要翻译的内容提供给『Duolingo』。
  2. 有了语料之后,『Duolingo』会在相关语种的语法专家的帮助下将这些内容分解成『Duolingo』的学习材料。这些内容会被分成简单的小句子,而且句子中的每个单词的意思都会给出。
  3. 有了这些单词的意思,『Duolingo』的用户就可以使用这些单词的意思翻译自己的语言版本,在这个过程中你的翻译还会被系统进行评价,通过即可获得积分和等级。

『Duolingo』里面的积分和等级代表了你的外语能力,你的积分和等级越高,你要翻译的东西越复杂。于是在整个过程中,你通过翻译学习了英语,同时你还帮助『Duolingo』完成了企业客户提供的语料翻译。

那『Duolingo』的效率如何呢?还是来看数据吧——将维基百科翻译成西班牙语:在十万用户的前提下,5周可以翻译完成;在100万用户的前提下,80个小时就可以了。多么神奇的一个工具呀。

如果你想体验『Duolingo』学英语(还可以学西班牙语、日语、韩语)的感觉,直接去豌豆荚下载吧 :http://www.wandoujia.com/apps/com.duolingo

总算结束了

不容易,总算接近尾声了。Luis von Ahn在我看来完全是一个优质偶像啊,你看发明了验证码,又改进验证码,让人们在输验证码的时候顺便帮忙把那些古老的书籍电子化,让人类的文明得到更好的传承。后来看到大家很多人都在学外语,他通过『Duolingo』为人们免费提供高效的外语学习平台,同时还帮助把互联网的优质内容翻译成其他语言版本,让更多的人享受更优质的内容。在我看来牛逼的地方在于他的两次创业成果都被Google收购,而且他还不满足,继续着自己的奋斗!

最后我想给大家推荐一下Luis von Ahn在TED的这个经典演讲,内容基本概况的他的主要工作内容,更重要的是他的演讲能力也很好,很会与听众互动,知道如何幽默地演讲。演讲链接地址

参考链接:

  1. Luis von Ahn 个人主页
  2. Luis von Ahn wikipedia
  3. TED 演讲

深入解析Android的自定义布局

layouts-post

写在前面的话:

这篇文章是前Firefox Android工程师(现在跳槽去Facebook了) 

 

只要你写过Android程序,你肯定使用过Android平台内建的几个布局——RelativeLayout, LinearLayout, FrameLayout等等。 它们能帮助我们很好的构建Android UI。

这些内建的布局已经提供了很多方便的构件,但很多情况下你还是需要来定制自己的布局。

总结起来,自定义布局有两大优点:

  1. 通过减少view的使用和更快地遍历布局元素让你的UI显示更加有效率;
  2. 可以构建那些无法由已有的view实现的UI。

在这篇博文中,我将实现四种不同的自定义布局,并对它们的优缺点进行比较。它们分别是: composite view, custom composite view, flat custom view, 和 async custom views

这些代码实现可以在我的github上的 android-layout-samples 项目里找到。这个app使用上面说到的四种自定义布局实现了相同的UI效果。它们使用 Picasso 来加载图片。这个app的UI只是twitter timeline的简化版本——没有交互,只有布局。

好啦,我们先从最常见的自定义布局开始吧: composite view。

Composite View

Composite views (也被称为 compound views) 是众多将多个view结合成为一个可重用UI组件的方法中最简单的。这种方法的实现过程是这样的:

  1. 继承相关的内建的布局。
  2. 在构造函数里面填充一个 merge 布局。
  3. 初始化成员变量并通过 findViewById()指向内部view。
  4. 添加自定义的API来查询和更新view的状态。

TweetCompositeViewcode 就是一个 composite view。它继承于 RelativeLayout,并填充了 tweet_composite_layout.xmlcode 布局文件,最后向外界暴露了 update()方法来更新它在adaptercode里面的状态。

Custom Composite View

上面提到的TweetCompositeView 这种实现方式能满足大部分的情况。但是碰到某些情况就不灵了。假设你现在想要减少子视图的数量,让布局元素的便利更加有效。

这个时候我们可以回过头来看看,尽管 composite views 实现起来比较简单,但是使用这些内建的布局还是有不少的开销的——特别是 LinearLayout 和RelativeLayout这种比较复杂的容器。由于Android平台内建布局的实现,在一次布局元素遍历中,系统需要处理许多布局的结合和子视图的多次测量——LinearLayout的 layout_weight 的属性就是常见例子。

因此你可以为你的app量身定做一套子视图的计算和定位逻辑,这样的话你就可以极大的优化你的UI了。这种做法就是我接下来要介绍的 custom composite view.

顾名思义,一个 custom composite view 就是一个重写了onMeasure() 和onLayout() 方法的 composite view 。因此相比之前的composite view继承了 RelativeLayout,现在我们需要更进一步——继承更抽象的ViewGroup。

TweetLayoutViewcode 就是通过这种技术实现的。注意现在这个实现不像 TweetComposiveView 继承了LinearLayout ,这也就避免了 layout_weightcode这个属性的使用了。

这个大费周折的过程通过ViewGroup’s 的measureChildWithMargins() 方法和背后的 getChildMeasureSpec() 方法计算出了每个子视图的 MeasureSpec 。

TweetLayoutView 不能正确地处理所有可能的 layout 组合但是它也不必这样。我们肯定需要根据特定需求来优化我们的自定义布局,这种方式可以让我们写出简单高效的布局代码。

Flat Custom View

如你所见,custom composite views 可以简单地通过使用ViewGroup 的API就可以实现了。大部分时候,这种实现是可以满足我们的需求的。

然而我们想更进一步的话——优化我们应用中的关键部分UI,比如 ListViews ,ViewPager等等。如果我们把所有的 TweetLayoutView 子视图合并成一个单一的自定义视图然后统一管理会怎么样呢?这就是我们接下来要讨论的 flat custom view——参看下面的图片。

layouts
左边为CUSTOM COMPOSITE VIEW ,右边是FLAT CUSTOM VIEW

 

flat custom view 就是一个完全自定义的 view ,它完全负责内部的子视图的计算,位置安排,绘制。所以它就直接继承了View 而不是 ViewGroup

如果你想找找现实生活中app是否存在这样的例子,很简单——开启你手机“开发者模式”里面的 “显示布局边界”选项,然后打开 Twitter, Gmail, 或者 Pocket这些app,它们在列表UI里面都采用了 flat custom view。

使用 flat custom view最主要的好处就是可以极大地压缩app 的视图层级,进而可以进行更快的布局元素遍历,最终可以减少内存占用。

Flat custom view 可以给你最大的自由,就好像你在一张白纸上面作画。但是这样的自由是有代价的:你不能使用已有的那些视图元素了,比如 TextView 和 ImageView。没错,在 Canvas 上面描绘文本 的确很简单,但要你实现 ellipsizing(就是对过长的文本截断)呢?同样, 在 Canvas 上面 描绘图片确很简单,但是如何缩放呢?这些限制同样适用于touch events, accessibility, keyboard navigation等等。

所以使用flat custom view的底线就是:只将flat custom view应用于你的app的UI核心部分,其他的就直接依赖Android平台提供的view了。

TweetElementViewcode 就是 flat custom view。为了更容易的实现它,我创建了一个小小的自定义视图框架叫做UIElement。你可以在  canvascode 这个包里找到它。

UIElement 提供了和Android平台类似的 measure/layout API 。它包含了没有图像界面的 TextView 和 ImageView ,这两个元素包含了几个必需的特性——分别参看 TextElementcodeImageElementcode 。它还拥有自己的 inflatercode ,帮助从 布局资源文件code里面实例化UIElement  。

注意: UIElement 还处于非常早期的开发阶段,所以还有很多缺陷,不过将来随着不断的改进UIElement 可能会变得非常有用。

你可能觉得TweetElementView 的代码看起来很简单,这是因为实际代码都在 TweetElementcode里面——实际上TweetElementView 扮演托管的角色code

TweetElement  里面的布局代码和TweetLayoutView‘非常类似,但是它使用 Picasso 请求图片时却不一样code ,因为TweetElement  没有使用ImageView

Async Custom View

总所周知,Android UI 框架时单线程的 。 这样的单线程会带来一些限制。比如,你不能在主线程之外遍历布局元素——然而这对复杂、动态的UI是很有益处的。

假如你的app 在一个ListView 中很布局比较复杂的条目(就像大多数社交app一样),那么你在滑动ListView 就很有可能出现跳帧的现象,因为ListView 需要为列表中即将出现的新内容计算它们的视图大小code和布局code同样的问题也会出现在GridViewsViewPagers等等。

如果我们可以在主线程之外的线程上面对那些还没有出现的子视图进行布局遍历是不是就可以解决上面的问题了?也就是说,在子视图上面调用 measure()layout() 方法都不会占用主线程的时间了。

所以 async custom view 就是一个允许子视图布局遍历过程发生在主线程之外的实验,这个idea是受到Facebook的Paperteam async node framework 这个视频激发所想到的。

既然我们在主线程之外永远接触不到Android平台的UI组件,因此我们需要一个API在不能直接接触到这个视图的前提下对这个视图的内容进行测量、布局。这恰恰就是 UIElement 框架提供给我的功能。

AsyncTweetViewcode 就是一个 async custom view。它使用了一个线程安全的 AsyncTweetElementcode 工厂类code 来定义它的内容。具体过程是一个 Smoothie 子项加载器code 在一个后台线程上对暂时不可见的AsyncTweetElement 进行创建、预测量和缓存(在内存里面,以便后来直接使用)。

当然在实现这个异步UI的过程中我还是妥协了一些,因为你不知道如何显示任意高度的布局占位符。比如,当布局异步传递过来的时候你只能在后台线程对它们的大小进行一次更改。因此当一个 AsyncTweetView 就要显示的时候却无法在内存里面找到合适的AsyncTweetElement ,这个时候框架就会强制在主线程上面创建一个AsyncTweetElement code

还有,预先加载的逻辑和内存缓存过期时间设置都需要比较好的实现来保证在主线程尽可能多地利用内存里面的缓存布局。比如,这个方案中使用 LRU 缓存code 就不是一个明智的选择。

尽管还存在这些限制,但是使用 async custom view 的得到的初步结果还是很有前途的。当然我也会通过重构这个UIElement  框架和使用其他类别的UI在这个领域继续探索。让我们静观其变吧。

总结

在我们涉及到布局的时候,我们自定义的越深,我们能从Android平台所能获得的依赖就越少。所以我们也要避免过早优化,只在确实能实实在在改善app质量和性能的区域进行完全的布局自定义。

这不是一个非黑即白的决定。在使用平台提供的UI元素和完全自定义的两种极端之间还有很多方案——从简单的composite views 到复杂的 async views。实际项目中,你可能会结合文中的几种方案写出优秀的app。

Linkedin工程师是如何优化他们的Java代码的

最近在刷各大公司的技术博客的时候,我在Linkedin的技术博客上面发现了一篇很不错博文。这篇博文介绍了Linkedin信息流中间层Feed Mixer,它为Linkedin的Web主页,大学主页,公司主页以及客户端等多个分发渠道提供支撑(如下图所示)。

feed_mixer_1

在Feed Mixer里面用到了一个叫做SPR(念“super”)的库。博文讲的就是如何优化SPR的java代码。下面就是他们总结的优化经验。

1. 谨慎对待Java的循环遍历

Java中的列表遍历可比它看起来要麻烦多了。就以下面两段代码为例:

  • A:
    private final List<Bar> _bars;
    for(Bar bar : _bars) {
        //Do important stuff
    }

     

  • B:
    private final List<Bar> _bars;
    for(int i = 0; i < _bars.size(); i++) {
    Bar bar = _bars.get(i);
    //Do important stuff
    }

     

代码A执行的时候 会为这个抽象列表创建一个迭代器,而代码B就直接使用 get(i) 来获取元素,相对于代码A省去了迭代器的开销。

实际上这里还是需要一些权衡的。代码A使用了迭代器,保证了在获取元素的时候的时间复杂度是 O(1) (使用了 getNext()hasNext() 方法),最终的时间复杂度为 O(n) 。但是对于代码B,循环里每次在调用 _bars.get(i) 的时候花费的时间复杂度为 O(n)  (假设这个list为一个 LinkedList),那么最终代码B整个循环的时间复杂度就是 O(n^2)  (但如果代码B里面的list是 ArrayList, 那 get(i) 方法的时间复杂度就是 O(1)了)。所以在决定使用哪一种遍历的方式的时候,我们需要考虑列表的底层实现,列表的平均长度以及所使用的内存。最后因为我们需要优化内存,再加上 ArrayList 在大多数情况下查找的时间复杂度为 O(1) ,最后决定选择代码B所使用的方法。

2.在初始化的时候预估集合的大小

从Java的这篇 文档我们可以了解到: “一个HashMap 实例有两个影响它性能的因素:初始大小和加载因子(load factor)。 […] 当哈希表的大小达到初始大小和加载因子的乘积的时候,哈希表会进行 rehash操作 […] 如果在一个HashMap 实例里面要存储多个映射关系时,我们需要设置足够大的初始化大小以便更有效地存储映射关系而不是让哈希表自动增长让后rehash,造成性能瓶颈。”

在Linkedin实践的时候,常常碰到需要遍历一个 ArrayList 并将这些元素保存到 HashMap 里面去。将这个 HashMap 初始化预期的大小可以避免再次哈希所带来的开销。初始化大小可以设置为输入的数组大小除以默认加载因子的结果值(这里取0.7):

  • 优化前的代码:
    HashMap<String,Foo> _map;
    void addObjects(List<Foo> input)
    {
      _map = new HashMap<String, Foo>(); 
      for(Foo f: input)
      {
        _map.put(f.getId(), f);
      }
    }

     

  • 优化后的代码
    HashMap<String,Foo> _map;
    void addObjects(List<Foo> input)
    {
    _map = new HashMap<String, Foo>((int)Math.ceil(input.size() / 0.7)); 
    for(Foo f: input)
    {
    _map.put(f.getId(), f);
    }
    }

     

3. 延迟表达式的计算

在Java中,所有的方法参数会在方法调用之前,只要有方法参数是一个表达式的都会先这个表达式进行计算(从左到右)。这个规则会导致一些不必要的操作。考虑到下面一个场景:使用ComparisonChain比较两个 Foo 对象。使用这样的比较链条的一个好处就是在比较的过程中只要一个 compareTo 方法返回了一个非零值整个比较就结束了,避免了许多无谓的比较。例如现在这个场景中的要比较的对象最先考虑他们的score, 然后是 position, 最后就是 _bar 这个属性了:

public class Foo {
private float _score;
private int _position;
private Bar _bar;
 
public int compareTo (Foo other) {
return ComparisonChain.start().
compare(_score, other.getScore()).
compare(_position, other.getPosition()).
compare(_bar.toString(), other.getBar().toString()). 
result;
}
}

但是上面这种实现方式总是会先生成两个 String 对象来保存 bar.toString() 和other.getBar().toString() 的值,即使这两个字符串的比较可能不需要。避免这样的开销,可以为Bar 对象实现一个 comparator:

public class Foo {
private float _score;
private int _position;
private Bar _bar;
private final BarComparator BAR_COMPARATOR = new BarComparator();
 
public int compareTo (Foo other) {
return ComparisonChain.start().
compare(_score, other.getScore()).
compare(_position, other.getPosition()).
compare(_bar, other.getBar(), BAR_COMPARATOR).
result();
}
private static class BarComparator implements Comparator<Bar> {
@Override
public int compare(Bar a, Bar b) {
return a.toString().compareTo(b.toString());
}
}
}

4. 提前编译正则表达式

字符串的操作在Java中算是开销比较大的操作。还好Java提供了一些工具让正则表达式尽可能地高效。动态的正则表达式在实践中比较少见。在接下来要举的例子中,每次调用 String.replaceAll() 都包含了一个常量模式应用到输入值中去。因此我们预先编译这个模式可以节省CPU和内存的开销。

  • 优化前:
    private String transform(String term) {
    return outputTerm = term.replaceAll(_regex, _replacement); 
    }
  • 优化后:
    private final Pattern _pattern = Pattern.compile(_regex);
    private String transform(String term) {
    String outputTerm = _pattern.matcher(term).replaceAll(_replacement); 
    }

5. 尽可能地缓存Cache it if you can

将结果保存在缓存里也是一个避免过多开销的方法。但缓存只适用于在相同数据集撒花姑娘吗的相同数据操作(比如对一些配置的预处理或者一些字符串处理)。现在已经有多种LRU(Least Recently Used )缓存算法实现,但是Linkedin使用的是 Guava cache (具体原因见这里) 大致代码如下:

private final int MAX_ENTRIES = 1000;
private final LoadingCache<String, String> _cache;
// Initializing the cache
_cache = CacheBuilder.newBuilder().maximumSize(MAX_ENTRIES).build(new CacheLoader<String,String>() {
@Override
public String load(String key) throws Exception {
return expensiveOperationOn(key);
}
}
);
 
//Using the cache
String output = _cache.getUnchecked(input);

 

6. String的intern方法有用,但是也有危险

String 的 intern 特性有时候可以代替缓存来使用。

从这篇文档,我们可以知道:
“A pool of strings, initially empty, is maintained privately by the class String. When the intern method is invoked, if the pool already contains a string equal to this String object as determined by the equals(Object) method, then the string from the pool is returned. Otherwise, this String object is added to the pool and a reference to this String object is returned”.

这个特性跟缓存很类似,但有一个限制,你不能设置最多可容纳的元素数目。因此,如果这些intern的字符串没有限制(比如字符串代表着一些唯一的id),那么它会让内存占用飞速增长。Linkedin曾经在这上面栽过跟头——当时是对一些键值使用intern方法,线下模拟的时候一切正常,但一旦部署上线,系统的内存占用一下就升上去了(因为大量唯一的字符串被intern了)。所以最后Linkedin选择使用 LRU 缓存,这样可以限制最大元素数目。

最终结果

SPR的内存占用减少了75%,进而将feed-mixer的内存占用减少了 50% (如下图所示)。这些优化减少了对象的生成,进而减少了GC得频率,整个服务的延迟就减少了25%。MemUtil_incapacity

注:本文翻译自Linkedin 技术博客