如何利用mitmproxy来批量修改Android中HTTP流量

背景

有时候我们常常在调试Android程序时,常常需要对API返回的response进行修改,以达到测试特殊情况的目的。比如有时候我们需要某个字符串显示超过某个限制来看看此时Android上面显示是否正常,有时候我们需要特定高度的图片来显示页面是否异常。

对于这些场景,其实通过调试工具mitmproxy就能完成。利用mitmproxy可以让我们的APP开发调试事半功倍。对于从来没有听说过mitmproxy的朋友,推荐查看我之前的两篇博客:如何调试 Android 上 HTTP(S) 流量 和 mitmproxy基础实践教程

之前所讲的关于mitmproxy的使用教程都是比较基础的。比如我们可以拦截某些特定的request,然后修改这个request的某些属性,有必要还可以修改response的某些属性,这样就可以解决本文开头所讲的场景里面的问题。但是,这样做的效率很低,我在手机上发起某个API请求,然后被电脑上的mitmproxy截获了,然后我更改这个请求返回的response,然后这个response就会到了手机上了。

Mitmproxy的Inline Scripts 特性介绍

这样听起来好像有点麻烦。可以不可以自动的修改这些resquest和respones。当然可以!这就是我们今天要说的主题,mitmproxy还一个非常极客的特性,叫做 Inline Scripts。有了它,我们就不要在手机和电脑上来来回回地调试了。mitmproxy的Inline Scripts可以让我们对通过mitmproxy的所有HTTP(S)流量进行可编程的定制,这里的script指的是python脚本。Inline Scripts是一种靠HTTP中的事件进行驱动的API,这有点类似于Android的Activity的生命周期。Inline Scripts提供了HTTP请求中各个时间点的hook函数,比如HTTP请求启动的时候,request到达mitmproxy的时候,response到达mitmproxy的时候等等。下面我就简单介绍一下Inline Scripts中的一些Event:

Inline Scripts 中常用的Event介绍

  1. start(context, argv)
    HTTP请求开始启动的时候,这个event在所有event之前。
  2. clientconnect(context, root_layer)
    客户端向代理(mitmproxy)建立一个connection的时候,一个connection可以对应多个request。
  3. request(context, flow)
    客户端的HTTP请求被代理(mitmproxy)接收到的时候。flow里面包含了request对象,比如request的方法,request的url,参数等等。
  4. serverconnect(context, server_conn)
    代理(mitmproxy)向服务器端建立一个connection的时候,同理一个connection可以对应多个request。
  5. responseheaders(context, flow)
    当服务器的response header被代理(mitmproxy)接收的时候,这个event在接下来的response event之前。
  6. response(context, flow)
    当服务器的response被代理(mitmproxy)接收的时候,这个event在responseheaders事件之后。
  7. error(context, flow)
    当flow出现异常的时候会产生该事件。比如connection被中断等。
  8. serverdisconnect(context, server_conn)
    当代理(mitmproxy)断开到服务器的连接时
  9. clientdisconnect(context, root_layer)
    当客户端断开到代理(mitmproxy)的连接时
  10. done(context, argv)
    Inline Scripts被关闭的时候。

Inline Scripts 使用示例

 

下面的示例为了说明 mitmproxy 中的 Inline Scripts是如何使用的。具体思路:

  1. 首先我们将已有的相关API 流量通过mitmproxy截获
  2. 将这些API中response中特性属性的value全部改为我们想要的value

假设我们要测试的某个请求返回的数据格式如下:

{"data":[{"key":"value1"},{"key":"value2"},{"key":"value3"}]}

因此接下来我们就可以写相应的python脚本(change.py)了:

from libmproxy.protocol.http import decoded  #来自mitmproxy中的库
from libmproxy.protocol.http import HTTPResponse
import json

#mitmproxy中代理所有HTTP请求的response全部会经过这里
def response(context, flow):
    #如果request的url里面包含了某个关键字(需要按照你的需要设置)
    if 'KEYWORD' in flow.request.pretty_url(hostheader=True):
        #解码请求的response
        with decoded(flow.response):
           #使用json封装response
           body = json.loads(flow.response.content)  
           if body['data']:  
               list = body['data']
               for item in list:
                   if item['key']:
                       #将符合条件的属性进行更改
                       item['key']='value_special'  
           #将更改后的数据重新封装为response 
           flow.response.content = json.dumps(body)

然后咱们再结合之前两篇教程,我们只需执行以下命令就可以完成我们的需求:

./mitmproxy -b YOUR_LOCAL_IP_ADDRESS -p PORT -s change.py

当这个命令行运行的时候,每次手机请求这个api,请求经过mitmproxy的时候,mitmproxy会把请求转发给服务器端,接着服务器端讲response返回给mitmproxy。mitmproxy拿到数据之后,将response中的数据按照前面的python脚本进行了更改,最后这个被修改过的response回到手机上了。

这一切都是自动的,你只需拿着手机发起请求就行了。

如何在Android开发中让你的代码更有效率

最近看了Google IO 2012年的一个视频,名字叫做Doing More With Less: Being a Good Android Citizen,主要是讲如何用少少的几句代码来改善Android App的性能。在这个视频里面,演讲者以一个图片app为例讲解如何应用Android中现有的东西来改善app性能问题。这个图片app的代码在这里。ppt在这里。现在我将视频里面的内容记录如下:

使用LruCache避免OOM

首先我们的图片app是用来展示手机里面保存的图片。当app里面需要展示大量的图片的时候,我们需要将这些图片从disk加载到内存当中。如果我们来回地滑动activity,系统会重复许多disk I/O;而且在一个activity里面同时加载多张图片将会占用大量内存,造成系统内存紧张,进而影响用户体验。

如何使用LruCache

这个时候我们可以引入LruCache,将我们最常用的图片缓存到内存里面,这样可以避免大量重复的disk I/O,还可以让app加载图片时内存占用不超过设定值。具体代码如下:

//这里假设通过picture的id(Long)来作为key获取对应的bitmap
public class PictureCache extends LruCache<Long, Bitmap>{
    //设定最大的byte值,也就是说整个缓存所能占用的最大内存
    public PictureCache(int maxByteSizes){
        super(maxByteSizes);
    }
    // 计算每次添加bitmap的时候,给缓存所添加的数字,默认就是数量,
    //这里因为添加的是bitmap,所以每次添加都是计算bitmap对应的字节数
    protected int sizeOf(Long key,Bitmap value){
        return value.getByteCount();
    }
    
    public void put(Long key, Bitmap value);
    public Bitmap get(Long key);
}

 如何确定Cache大小

一般我们是通过ActivityManager.getMemorySize()来确定Cache的大小。ActivityManager.getMemorySize()表明了在系统正常运行的前提下一个App所占内存的极限。所以我们可以使用它来作为Cache大小的一个衡量,比方如下的代码中,我们使用它的一半来作为Cache的大小:

final ActivityManager am = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
final int memoryClassBytes = am.getMemoryClass() * 1024 * 1024;
PictureCache mCache = new PictureCache(memoryClassBytes / 2);

 app跑到后台去了

当我们用这个图片app浏览完图片之后呢,我们回到Android 主界面,开始玩游戏。大家都知道,游戏很耗内存。可能我们在玩的过程中,直接用完了剩下可用的所有内存都还不够,那怎么办呢?Android会在这个时候kill一些后台的app获取相应的内存。

这里需要先说一命令。我们如何获取一个app的内存占用?很简单,使用 “adb shell procrank”命令行。这个命令行显示所有系统运行的进程所占内存大小,一般包括Vss、Rss、Pss、Uss,其中Uss对我们来说最重要,Uss表示如果这个进程被系统干掉了,那么系统可以从这个进程上面获得多少的可用内存。

好了回到之前的情景。Android会kill掉一些后台程序来供给游戏所需的内存。假设Android因为内存紧张kill掉了图片app,我在玩了一会游戏之后又打开了图片app。这个时候图片app又要重新布局,重新加载图片,整个体验对用户来说非常不好。有什么办法让Android在kill应用之前通知app一声,好让app有所准备?

这个时候我们就要用到ComponentCallbacks2,详情看文档。app在系统处于不同的内存环境时会有相应的callback,我们只需在activity里面重写这个onTrimMemory方法即可,具体示例代码如下:

public void onTrimMemory(int level) {
        super.onTrimMemory(level);

        if (level >= TRIM_MEMORY_MODERATE) { // 60
            // 这个app已经进入后台有一段时间了,基本上表示用户接下来
            //不会重新打开这个app,我们可以清掉所有缓存所占内存
            Log.v(TAG, "evicting entire thumbnail cache");
            mCache.evictAll();

        } else if (level >= TRIM_MEMORY_BACKGROUND) { // 40
            // 表示app刚进入后台,我们可以缩减一部分缓存所占内存
            // 来保证其他前台app的内存需要
            Log.v(TAG, "evicting oldest half of thumbnail cache");
            mCache.trimToSize(mCache.size() / 2);
        }
    }

 

这个callback只是建议,不一定会被系统调用。系统在内存紧张的时候可能会直接kill掉app而不去调用这个callback。但是如果这些callback可以调用的话,这将大大地提升我们app的用户体验。

善用Android自带容器

现在我们需要为这个app添加一些新特性,我们要让这个app可以进行收藏操作(为图片添加一个是否被收藏的属性即可)。我们会一次性收藏多张图片,那么我们可以使用GridView的多选模式。按照常理来说,我们可以使用HashMap()<Long, Boolean>来存储哪些照片需要被收藏。不过这里使用HashMap有点大材小用,效率不高。我们可以使用SparseBooleanArray等Android特有的容器来代替HashMap,节省系统开销(主要是autoboxing带来的开销)

SQLite中读写操作优化

当我们获得了要收藏的图片信息(保存在SparseBooleanArray中)之后,我们需要讲这些数据保存在SQLite当中,示例代码如下:

SQLiteDatabase db;
long[] mPhotoIds;
ContentValues values = new ContentValues();
for (long photoId : mPhotoIds) {
     values.put(COLUMN_ID, photoId);
     values.put(COLUMN_FAVORITE, favorite);
     db.insert(TABLE_FAVORITE, null, values);
}

 

上面的代码中,每次insert都有开销。这个时候我们可以考虑使用transaction。代码如下:

SQLiteDatabase db;
long[] mPhotoIds;
ContentValues values = new ContentValues();
db.beginTransaction();
try{
    for (long photoId : mPhotoIds) {
       values.put(COLUMN_ID, photoId);
       values.put(COLUMN_FAVORITE, favorite);
       db.insert(TABLE_FAVORITE, null, values);
    }
    db.setTransactionSuccessful();
}finally{
    db.endTransaction();
}

但是使用这个transaction的时候,db会被锁住,而碰到更重要的操作只能等待。碰到这种情况怎么办?使用db.yieldIfContendedSafely,这个方法表示我现在执行我的多次数据库操作,如果碰到其他的数据库操作,我先让别的操作完 再执行我的操作。具体代码如下:

SQLiteDatabase db;
long[] mPhotoIds;
ContentValues values = new ContentValues();
db.beginTransaction();
try{
    for (long photoId : mPhotoIds) {
       values.put(COLUMN_ID, photoId);
       values.put(COLUMN_FAVORITE, favorite);
       db.insert(TABLE_FAVORITE, null, values);
       db.yieldIfContendedSafely();
    }
    db.setTransactionSuccessful();
}finally{
    db.endTransaction();
}

 

使用RenderScript

如果这个时候我想让这个图片app拥有一些简单的滤镜,大家可能一下就想到使用NDK来实现相应的图片处理工作。其实Android提供RenderScript也能完成这样的任务,而且优点不少呢:

  1. RenderScript能充分利用用户设备的硬件资源。比如设备的CPU是双核的,那么RenderScript便会相应地开辟两个worker线程来进行图片处理工作
  2. RenderScript面向所有架构,包括ARM、X86、MIPS等等。其中原理是首先RenderScript在第一阶段编译成中间代码并打包到apk文件里面,接着在apk被安装到设备上面之后,中间代码会被再次编译成与设备平台相关的native代码
  3. RenderScript会自动生成相应的JNI 胶水代码

以上这三点相对于NDK来说,节省了开发者许多精力,让开发者更专注于效果代码的实现。这个示例相应的RenderScript代码在这里

这个视频是专门介绍RenderScript的一些用法,也是来自于Google IO。

善用Broadcast

如果我们需要对用户新增的照片默认添加滤镜效果,怎么实现?很好办啦,因为Android在API14的时候添加了一个新的Broadcast Intent,叫做”android.hardware.action.NEW_PICTURE”,就是在系统新增了照片时,我们截获这个intent就可以了。然后我们就在相应的BroadcastReceiver里面进行处理,相关代码如下:

public void onReceive(Context context, Intent intent) {
    if (isAutoApplyEnabled) {
        // 执行相应的滤镜操作
        final Intent serviceIntent = new Intent(context, EffectService.class);
        serviceIntent.setData(intent.getData());
        context.startService(serviceIntent);
    } else {
        Log.d(TAG, "Processed no-op broadcast!");
    }
}

上面的isAutoApplyEnabled变量表示系统是否开启自动对新增照片进行滤镜的操作。那如果关闭默认滤镜功能,即将isAutoApplyEnabled的值设为false呢?结果就是每次新增照片都会有intent过来,只是在onReceive方法里面没有做操作。这样造成的结果就是每次intent都会被系统传递,只是走了不同的分支,系统照样消耗资源。

因此比较好的方案就是系统中isAutoApplyEnabled变量的值在变化的时候,我们需要相应地对BroadcastReceiver进行开闭操作。具体需要用到PackageManage.setComponentEnabledSetting()方法。

最后的几句话

本文以一个图片app为背景讲述了Android开发中官方推荐的小tip。了解了这些可以让你的app性能更上一个台阶。本文涉及的所有代码可以在这里找到。

 

Gson在 Android开发中可能存在的陷阱

引言

本文来自于Foursquare的团队技术博客。Foursquare团队最近发现自家的Android app在使用过程中并没有传说当中的“如黄油般顺滑”,而且通过查看logcat发现app会频繁地发起 GC_FOR_ALLOC 调用,Activity和Fragment之间的跳转也没有想象当中那样快。于是好学的工程师们开始去挖掘这个背后的内容。

一切都是因为JSON解析的问题

通过初步的性能测试,我们发现以上现象的很大部分原因来自于app在解析JSON的时候耗时过多。于是我们决定深入去看看app的JSON解析代码有些什么问题?

Foursquare的Android应用都是通过一个 JSON API来同服务器进行交互的,JSON的解析则是使用Google的 Gson 库,也就是说使用Gson来反序列化JSON字符串生成相应的Java对象供Android开发者进行下一步处理。我们一般是这样使用Gson将一个描述venue(场地)的字符串转化成Java对象的:

String venueJson = "{"name": "Starbucks" ,"city": "New York"}”;
Gson gson = new Gson();
Venue venue = gson.fromJson(venueJson, Venue.class);
 
// Do something with object.
String name = venue.getName();

程序是这样没问题,可是实际上我们只需要venue的Name属性,但是上面的程序却解析了整个JSON字符串。是不是这里造成了一部分性能问题?如果我们只解析我们想要的属性,会不会降低JSON解析的时间?于是他们开始使用 Gson streaming API ,确保Android在解析JSON的时候省去了将Reader转化为String的步骤,直接解析Reader,示例代码如下:

我们从上面的代码可以发现 JsonReader stream 被直接传入 read() 方法而不是传入一个 JsonElement 树。通过上面的改进之后,即把我们自定义的 deserializer 改为继承 TypeAdapters 和 TypeAdapterFactorys ,我们app在解析大的响应的时候解析时间减少了 50% 以上。更重要的是,整个 app 感觉比以前要快多了app的滑动也顺畅多了。

一些感悟

  • 尽量使用 GSON 的 streaming APIs,特别是在像Android这样的内存制约较多的平台上。 The memory savings for non-trivial JSON strings are significant.
  • 使用 TypeAdapters 实现的deserializer 代码肯定要比使用 JsonDeserializers 实现的deserializer 代码要丑,因为TypeAdapters 比起JsonDeserializers 更偏向于底层了,或者说JsonDeserializers 要比TypeAdapters 更抽象一下,而越是底层的代码越具体。相应的JsonDeserializers 肯定也要比TypeAdapters 方案要灵活些。
  • 尽管存在这些缺点,但是很多时候代码的美观程度和灵活性相对于内存的使用,优先级还是要排在后面的
  • 尽可能少用自定义的 deserialization ,因为代码的复杂度增加了。

mitmproxy实践教程之调试 Android 上 HTTP流量

之前写了一篇博客介绍了mitmproxy,不过有些同学对这个工具到底该怎么使用还是有点不清楚,于是自己花了一天时间把自己对这个工具的理解和使用过程进行了一番整理,形成了这篇文章。接下来我就以一些简单的场景来说明mitmproxy的用法。

实践环境

iMac一台、华为B199(Android 4.4)手机一部,这两者都连着同一个wifi。mitmproxy安装版本为0.11.3。iMac的ip地址为:192.168.0.104

mitmproxy安装与设置

    1. 下载mitmproxy。推荐下载二进制安装包(当前版本为0.11.3),因为使用pip安装会由于墙的原因安装失败。下载成功之后解压,便可直接点击名为“mitmproxy”的文件了。
    2. 在iMac上运行和设置mitmproxy。由于直接点击运行是没有设置mitmproxy参数的,所以我们需要在命令行里面输入mitmproxy进行设置:
      $ [YOUR_DOWNLOAD_PATH]/mitmproxy -b 192.168.0.104 -p 9527

      注意,此处的[YOUR_DOWNLOAD_PATH]为你下载mitmproxy二进制文件解压后所对应的目录,就我而言的目录就是:~/Downloads/osx-mitmproxy-0.11.3 .  其中ip设置为iMac的ip,端口则按照你的个人喜好来。
      初始界面如下:

      mitmproxy初始运行界面
      mitmproxy初始运行界面

      输入“?”便可查看帮助界面(输入“q”回到正常界面):

      mitmproxy帮助界面
      mitmproxy帮助界面
    3. 在Android上设置代理。按照之前的步骤将Android所连接的wifi代理ip设置为:192.168.0.1,端口设置为:9527 。现在Android已经连上代理,iMac端便开始展示它所抓取到的包了(我在刷微博:)):
      mitmproxy抓包显示
      mitmproxy抓包显示

       

mitmproxy常见操作

mitmproxy的操作主要是通过命令行来解决的,很多操作和vi相同。一般说来,“q”代表返回。当mitmproxy回到抓包列表界面时,输入“q”就代表退出mitmproxy了。

清除抓包结果

当你处于抓包列表界面时,觉得太多的信息让你十分恼火,直接输入大写字母C便可以清楚所有的抓包结果。

查看抓包

在步骤3中我们可以看到一个黄色的指标,我们可以通过上下箭头(或者使用 “j”/”k”来上下选择)。直接回车便可以查看指标所选定的包的详细信息:

mitmproxy详细信息
mitmproxy抓包的Request详细信息

此时,我们可以使用tab键在“Request”和“Response”之间切换。这是“Response”的详细信息(这是一个张图片,所以下面乱码了):

mitmproxy抓去response信息
mitmproxy抓去response信息

当我们发现“Response”的信息比较乱的时候,我们可以选择合适的形式来展示,这个时候我们只要输入“m”便可以看到mitmproxy提供的不同展现形式,输入对应高亮的字母便可以看到相应的结果(输入“s”便可以以json形式展示):

mitmproxy 展示模式
mitmproxy 展示模式

mitmproxy修改抓包

如果这个时候我们需要编辑“Request”怎么办,很简单,输入字母“e”即可(注意图片最下方出现的提示(Edit request (query,path,url,header,form,raw body,method)?

mitmproxy编辑request
mitmproxy编辑request

我从mitmproxy给出的提示中选择了“method”,也就是编辑当前包的http方法,于是mitmproxy便给出了常见的方法:屏幕快照 2015-01-27 下午2.20.09在这些方法中,我要选择“trace”,便输入高亮的字母“t”即可,最后原来“Request”里面的方法就变成了TRACE了:

mitmproxy 修改request method
mitmproxy 修改request method

 

但是这个时候修改的request还没有生效,因为现在mitmproxy所展示的抓包信息只是记录了Android和server端之间的通信,因此我们现在编辑的抓包是已经传输过了,所以怎么办呢?很简单,重新request一下,输入字母“r”(代表“replay”)。这个时候我们就可以看到我们修改过的request得到的结果了:

mitmproxy 抓包replay效果
mitmproxy 抓包replay效果

此时我们输入“q”便可以回到之前的抓包列表了。这些都是历史抓包纪录,能不能实时地抓包呢?当然可以,这就是下面要说的拦截(Intercept)了。

mitmproxy拦截

何为拦截?之前开篇就介绍过mitmproxy是一个中间人代理工具,它位于客户端和Server 端之间,它可以获取客户端的Request然后修改,发送给Server 端;Server端得到Request之后在发出相应的Response又会被mitmproxy拦截,如果你想修改response,便可修改后再发给客户端。注意:这与之前修改抓包信息不同的是,前者的抓包已经发送给了服务器,而mitmproxy拦截则是获取了抓包,还未发送给服务器。也就是说如果我拦截了我手机上所有的request,那么我的手机将无法获取来自服务器的response。

那么我们如果进行拦截,怎么进行特定的拦截?输入字母“i”(代表Intercept filter)即可,此时界面便会让你输入想要拦截的条件:

mitmproxy 拦截
mitmproxy 拦截

mitmproxy的条件拦截在默认情况下是过滤抓包的URL的。也就是说当你直接输入要拦截的条件(比如输入“weibo”),那么接下来要出现抓包会将匹配的抓包整体变黄:

mitmproxy条件过滤效果
mitmproxy条件拦截效果

这些橘黄色的数据包都代表被拦截了,还未发送给服务器,这个时候你就可以对这些数据包进行修改,我们选择一个数据包enter进入:

mitmproxy 拦截 选择数据包
mitmproxy 拦截 选择数据包

与之前的类似,输入“e”,进行request编辑模式,然后输入“h”代表要编辑request的头部:

mitmproxy 编辑拦截包的头部
mitmproxy 编辑拦截包的头部

输入enter便可对高亮的User-Agent的值进行修改,上图的weibo版本之前是5.0的,被我改成了6.0 。我们还可以对header进行添加属性,输入“a”即可,然后使用tab分别键入key和value。这里我添加了“test-test”键值对:

mitmproxy 拦截header添加键值对
mitmproxy 拦截header添加键值对

至此,我对拦截的request header已经修改完毕,现在要做的就是我要认可接受这个修改,然后发给服务器。所以我们输入“a”(代表“accept”)即可,等到服务器响应后,注意,mitmproxy便又了拦截服务器发过来的response(注意那个“Response intercepted”):

mitmproxy 拦截response
mitmproxy 拦截response

现在如果你想修改这个response也可以,方式同上面修改request一样。这个时候我再输入“a”,代表我接受了这个response,然后这个response便可发给客户端了:

mitmproxy 拦截response之后accept
mitmproxy 拦截response之后accept

 

更多类型的mitmproxy拦截

同时mitmproxy还支持不同类型的条件过滤,之前在拦截字符串前面加上特定的参数比如我要拦截所有的POST request怎么办?输入:~m POST 即可(m代表method):

mitmproxy 拦截特定的request 方法
mitmproxy 拦截特定的request 方法

拦截所有的request: ~q

拦截特定的header: ~h

拦截特定的domain: ~d

拦截特定的响应代码(404之类的): ~c

其他还有很多类型,请参看mitmproxy官方文档

最后的几句话

在使用的过程中,虽然是命令行操作,但是mitmproxy都会在窗口的右下方给出醒目的提示,而且这些快捷键虽然多,但是都有对应的意思,只要我们理解一下,还是很好上手的。这些天接触了mitmproxy这个工具之后,发现用它来调试Android上的HTTP流量有点屈才了,它本身还提供了一个可供用户调用的库——libmproxy来定制自己的工具,它还提供了改进版的tcpdump——mitmdump。这个小玩意还真的挺不错的,也欢迎各位体验过后一起来交流交流。

如何调试 Android 上 HTTP(S) 流量

前面的话

在Android开发中我们常常会和API 打交道,可能你不想,但是这是避不开的。大部分情况下,调试发送网络请求和接收响应的过程都是十分痛苦的。

有多少次我们经过调试发现API的调用失败仅仅是因为我们的编码错了或者丢失了一个HTTP头部参数?在调试的过程中,我们发现出现错误的原因千奇百怪。总之我们要看到最终的请求是什么样子的不是一件容易的事情,响应也是如此

如果碰到困难,请使用代理

解决这样的问题有各种各样的方法和门道,但是我今天要分享的是在我看来最快,最容易也是最可行的方法。

由于调试HTTP请求和响应是相当繁琐的(调试HTTPS更加繁琐),因此在这种前提下你能使用的最好工具就是代理。当然,你肯定知道什么是代理,所以我就不啰嗦它的定义了。

现有的代理工具中它们的用法,特性以及操作系统的兼容性都各不相同,如何选择一款合适的代理呢?要我说,简单点,就用mitmproxy

mitmproxy

我已经无法用语言来形容mitmproxy的方便和强大了。用mitmproxy 官网上的介绍一句话,就是“an interactive, SSL-capable man-in-the-middle proxy for HTTP with a console interface”。没错,你可以通过控制台来操作它;没错,它还支持HTTPS;没错,它允许“对流量进行拦截,检测,修改和回放”;没错,它还是跨平台的,因为它是用Python写的。对了,我说了它是完全免费的这件事了吗?

使用它也是相当容易的。一旦你安装了它,便可通过在控制台输入以下命令启动它:

$ mitmproxy [-b IP_ADDR] [-p PORT]

代理已经在运行,你可以开始拦截网络流量了。接下来你就需要配置Android手机,将所有的流量都重定向到之前命令里设置的代理服务器地址上去(IP_ADDR + PORT)。

代理设置App: “Proxy Settings”

现在我们需要在Android手机上进行代理配置。具体步骤如下:

  1. 进入Android的Wi-Fi设置
  2. 长按当前连接的网络
  3. 选择“修改网络”选项
  4. 勾选“显示高级选项”
  5. 启用代理服务器并将代理设置为“手动”
  6. 输入之前设置好的的IP地址和端口。

如果要禁用代理同理。现在Android端的设置就大功告成了。

是不是觉得上面的步骤有点头疼?不急!幸亏还有Proxy Settings 这个App。用了Proxy Settings,上面繁琐的步骤就变得异常简单了。有了这个轻量级的App,你只需轻轻一点,便可轻松地开启或禁用网络连接里的代理设置。

Proxy Settings 的使用截图
Proxy Settings 的使用截图

通过Proxy Settings ,你可以创建多个代理配置。当你想要启用其中一个,只需点击当前连接的网络,启用代理,并选择你需要的配置。如果你要禁用代理,点击连接,并禁用它。

就这么简单,对了,它还不需要root权限哦?

那如何调试 HTTPS?

正如前面所提到的,mitmproxy还支持HTTPS拦截。这个特性让你在保持API endpoint 不变的情况下调试生产环境里的API变得异常方便。然而由于HTTPS的工作方式,我们需要安装一个自定义的SSL证书,让mitmproxy对所拦截的流量进行解密。

听起来HTTPS拦截的步骤好像有点麻烦,其实很简单。你只需按照mitmproxy官网的这篇设置指南便可轻易地完成设置。记得在启动mitmproxy的时候需要额外添加一个参数,如下命令所示:

$ mitmproxy -a IP_ADDR -p PORT —-no-upstream-cert

不过此处还有个小问题。Android是以一种加密的形式来保存SSL证书的,而这种加密的形式即用户设置的手机解锁信息。也就是说,你在Android上安装任何SSL证书之前,系统都会提示你设置解锁手机的方式,比如输入PIN码,密码或者是某种图案。

调试3G/4G

3G/4G这种情况下设置代理更难了。系统设置中没有为3G/4G连接提供代理设置接口。但是!你可以通过你的手机“移动热点”功能来实现在3G/4G这种情况下设置代理。你要准备的就是:

  1. 一台可以连接3G/4G网络的Android手机
  2. 一台开启了“开发者模式”的手机(另一台了)
  3. 你的笔记本电脑

接下来的步骤就简单啦:

  1. 在第一台手机上激活“移动热点”功能
  2. 将你的笔记本电脑和第二台手机(“开启了开发者模式”)连上移动热点
  3. 在笔记本电脑上启动 mitmproxy,并将IP地址设为hotspot所对应的(通常是192.168.43.xxx 之类的)
  4. 在第二台手机上使用Proxy Settings设置代理

OK,在3G/4G这种情况下设置代理就是这样子的。

 

总结

到目前为止,我还试过其他两种代理工具,CharlesFiddler。 但是mitmproxy完爆他们两个。Charles 可能是在功能,灵活性以及操作系统支持这几个方面和mitmproxy最接近的,但是它一不免费,二不开源。 对于 Fiddler, 它只能在Windows平台上使用。

mitmproxy + Proxy Settings 的结合让我省了不少心,我希望它们也能为你所用。

如果你有更好的解决方法,记得告诉我。☺

本文翻译自音乐App musiXmatch 公司高级Android工程Sebastiano Gottardo。原文地址:https://medium.com/@rotxed/how-to-debug-http-s-traffic-on-android-7fbe5d2a34

UPDATE:有同学反应对这个工具到底怎么使用不是很清楚,于是我打算在本文基础上再增加一些实践相关的文字,后来发现篇幅过于臃肿,干脆新写了一篇关于mitmproxy实践的文章,希望能给大家一些启发。

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 技术博客