如何在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实践的文章,希望能给大家一些启发。

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。

 

深入解析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。

你的Android应用完全不需要那么多的权限

Android系统的权限从用户的角度来看有时候的确有点让人摸不着头脑。有时候可能你只需要做一些简单的事情(对联系人的信息进行编辑),却申请了远超你应用所需的权限(比如访问所有联系人信息的权限)。

这很难不让用户对你保存戒备。如果你的应用还是闭源的那用户也没办法验证是否你的应用正在把他的联系人信息上传到应用服务器上面去。即使你向用户解释你为什么申请这个权限,他们最后也可能不会相信你。所以我在过去开发Android应用的时候避免去用一些奇技淫巧,因为这会额外去申请权限,用户也会对你不信任。

经过一段时间实践后,我有这样一个体会:你在完成某些操作的时候并不一定需要申请权限的

比如Android系统中有这样一个权限: android.permission.CALL_PHONE. 你需要这个权限来让你从你的应用中调用拨号器,对吗?下面的代码就是你如果拨打电话的,对吧?

Intent intent = new Intent(Intent.ACTION_CALL);
intent.setData(Uri.parse("1234567890"))
startActivity(intent);

错!这个权限可以让你的手机在没有用户操作的情况下打电话!也就是说如果我的应用用了这个权限,我可以在你不知情的情况下每天凌晨三点去拨打骚扰电话。

其实正确的做法是这样的——使用 ACTION_VIEW 或者 ACTION_DIAL:

Intent intent = new Intent(Intent.ACTION_DIAL);
intent.setData(Uri.parse("1234567890"))
startActivity(intent);

这个方案的动人之处在于你的应用就不用申请权限啦。 为什么不需要权限呢?因为你使用的这个 Intent 会启动拨号器,并将你设置好的号码预先拨号。比起之前的方案,现在还需要用户点击“拨号”来打电话,没有用户的参与,这个电话就打不出了。说实话,这让我感觉很好,现在很多应用申请的权限让人有点不知所措。


另外一个例子:我为我的妻子写了一个叫做 Quick Map 应用,这个应用主要是为了解决她对现有的导航应用的吐槽。她只想要一个联系人列表和一条导航到这些联系人所在地的路径。

看到这里你可能觉得我需要申请访问所有联系人信息的申请来完成这个应用:哈哈哈,你又错了!如果你看了我的源码,你就知道其实我用了 ACTION_PICK 这个Intent 启动相关应用来获取联系人地址的:

Intent intent = new Intent(Intent.ACTION_PICK);
intent.setType(StructuredPostal.CONTENT_TYPE);
startActivityForResult(intent, 1);

这意味着我的应用不但不需要申请权限,而且还不要额外的UI。这让应用的用户体验也提升了不少。


在我看来,Android系统最酷的部分之一就是 它的 Intent 系统。因为Intent 意味着我不需要任何东西都要自己来实现。每个应用都会在Android注册它所擅长处理的数据领域,比如电话号码,短信或者联系人信息。如果什么事情都要一个应用来解决,那么这个应用会变得十分臃肿。

Android系统另外一个优点就是我可以利用其它应用所申请的权限,这样我的应用就不需要再次申请了。Android系统中的以上两点可以让你的应用变得更加简单。拨号器需要权限来拨打电话,但是我只需要一个拨打电话的intent就行了,不需要权限。因为用户信任Android自带的拨号器,但不信任我的应用,这很好啊。

我写这篇博客的意义在于在你申请权限之前,你应该至少好好读读关于Intent的官方文档,看看是否可以通过其他应用来完成你的操作。如果你想更深入的了解,你可以研究一下这篇关于权限的官方文档,里面介绍更多更精细的权限。

总之,使用更少的权限不但可以让你获取更多的用户信任,对用户来说,也让他们获得了很好的用户体验。

source:Dan Lew  I don’t need your permission!

巧用Drawable 实现Android UI 元素间距效果

本文翻译自:Grid Spacing on Android     原文作者:Cyril Mottier

在大部分的移动UI或者Web UI都是基于网格概念而设计的。这种网格一般都是有一些对其的方块组成,然后它们组合成为一个块。使用网格这样的设计原则可以有助于对齐UI元素,提升UI的一致性,同时还能让用户更加容易的获取UI上面包含的内容。简而言之,网格是一个相当的强大的设计工具。

开发者在使用网格设计原则的时候需要在UI 元素之间添加一些额外的间距,比如padding、margin或者spacing(根据你的设计方案来选择使用哪种间距) 。这些间距有利于在不同的块之间设置清晰的分隔带同时不会整体UI的可读性。这些间距对我们Android 开发者来说也不陌生,我们在设计Android 界面时,也会使用View 的padding 和 margin 来达到类似的效果。在Android 开发中,为了将UI 和业务逻辑分隔,我们会使用 XML来定义UI。这种做法对于比较固定的UI很有效果,但当这些UI元素需要根据业务逻辑来确定隐藏或者显示的状态时,这种做法就有点困难了。这篇文章就根据这种情况提出了一些Android开发技巧来应对动态的网格UI。

没有间距的UI

首先让我们来看一个简单的例子。我们创建一个简单的 LinearLayout 。然后我们在TextView (显示“Application logo”)下方再内置一个 LinearLayout ,我们在其中水平依次放置3个Button。最后得到的效果图如下图所示:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:padding="@dimen/spacing_medium">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="128dp"
        android:background="@color/light_gray"
        android:gravity="center"
        android:text="@string/application_logo"
        android:textAppearance="@android:style/TextAppearance.Material.Display1" />

    <LinearLayout
        android:id="@+id/buttons_container"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <Button
            android:id="@+id/btn_first"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:background="@drawable/purple"
            android:text="@string/button_1" />

        <Button
            android:id="@+id/btn_second"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:background="@drawable/indigo"
            android:text="@string/button_2" />

        <Button
            android:id="@+id/btn_third"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:background="@drawable/teal"
            android:text="@string/button_3" />

    </LinearLayout>

</LinearLayout>

take_1

添加间距后的UI

上图的所展示的UI就是基于网格设计的。当时UI里面的元素之间都没有间距。为了让用户更好地区分这些UI元素,我们给id 为 @id/buttons_container 的 LinearLayout 添加属性 android:layout_marginTop="@dimen/spacing_medium" ;给id 为 @id/btn_first@id/btn_second 的两个 Button 分别添加属性 android:layout_marginRight="@dimen/spacing_medium" ;这时的UI效果如下图所示:

take_2_1

添加了间距之后,整体的UI效果好多了,可读性更强了。可当我们动态的隐藏某些 View 的时候就会出现一些问题了。我们假设第三个Button 会根据用户的设备是否安装了 Google Play Services 来决定它的展示。如果这个设备没有 Google Play Services,那我们就把这个 Button to View.GONE 的 visibility 属性设为 View.GONE, 所得效果如下图:

take_2_2

出来的效果与我们预料中的一样,第三个 Button 没有再显示了,但是第二个 Button 的右边没有与上面的TextView 右边对齐。出现这种问题的原因是:拥有 margin 属性的view 会认为margin相应方向存在邻接 view。例如,每个拥有right/top margin view会认为它的 right/top 方向有一个邻接 view,因此,这个对应 margin 也就会生效,就算这个邻接view已经隐藏了。

设置间距的折衷方案——Java 和 GridLayout

一个比较直接的解决方案就是在Java 代码里面手动改变相应的margin 值,但说实话这不是一个好的方案。另一个方案就是使用能够自动处理元素之间的间距的布局。GridLayout 就符合这样的要求。但是这个布局让人蛋疼的是元素之间的间距不能自定义,只能使用默认的间距。

 

设置间距的最佳方案——LinearLayout 的divider

实际上 LinearLayout 已经有一个处理这种元素之间的间距的属性了。这个属性却没怎么被大家发现,一直很低调,但它的效果相当神奇。所以我们说的第三个方案就是使用一个固定高宽的 Drawable 作为 LinearLayout 的 元素分隔线(divider):

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">

    <size
        android:width="@dimen/spacing_medium"
        android:height="@dimen/spacing_medium" />

    <solid android:color="@android:color/transparent" />

</shape>

现在你就可以把这个新创建的 Drawable 设为LinearLayout 的 divider,这样这个Drawable 就能让元素之间产生间距了:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:divider="@drawable/spacer_medium"
    android:orientation="vertical"
    android:padding="@dimen/spacing_medium"
    android:showDividers="middle">

    <!-- TextView -->

    <LinearLayout
        android:id="@+id/buttons_container"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:divider="@drawable/spacer_medium"
        android:orientation="horizontal"
        android:showDividers="middle">

        <!-- Buttons -->

    </LinearLayout>

</LinearLayout>

take_3

 

总结

Android 框架里面有许多的特性可以用来实现一些不常见的方案,而且最后效果出其不意。定义 Drawable 就是其中一种途径。如果你能吃透Android 里面的  Drawable  ,那么你的代码也可能大大地精简。

注意:文章LinearLayout的divider 属性设置是Android API 11 之后加进去的,这意味着Android API 11之前的设备要使用这个divider需要LinearLayoutCompat

如何给你的Android 安装文件(APK)瘦身

本文翻译自:Putting Your APKs on Diet           原作者:Cyril Mottier

Android的apk文件越来越大了这已经是一个不争的事实。在Android 还是最初版本的时候,一个app的apk文件大小也还只有2 MB左右,到了现在,一个app的apk文件大小已经升级到10MB到20MB这个范围了。apk文件大小的爆炸式增长主要是因为用户对app质量的期待越来越高以及开发者的开发经验增长,具体体现在以下几个方面:

  • Android设备 dpi 的多样化 ([l|m|tv|h|x|xx|xxx]dpi)
  • Android平台的进化,开发工具的改进以及开源类库生态系统的丰富
  • 用户对高质量UI的期待
  • 其他原因

Android开发者在设计一个app的时候应该将最终发布一个轻量级app作为一个最佳实践来考虑。为什么?首先这就意味着你拥有了一个简洁,易维护代码基础。其次,官方应用商店对超过50MB的apk设置了拓展包文件下载选项,apk文件在50MB以下更容易让用户下载。最后,我们的应用程序环境是一个带宽有限,存储空间有限的环境,apk安装文件越小,下载就会越快,安装也会更快,良性循环,最后说不定用户因为这个给好评。

在大部分情况下,apk大小的增长是为了满足消费者的需要和期待。然而,我认为apk大小的增速已经超过了用户对app期待的增速。所以,很大程度上,官方应用商店里面的那些程序可以瘦身至它们现在大小的一半甚至更多。在这篇文章里面,我将写下一些关于如何给apk文件瘦身的招式,希望你们能够喜欢。

reducing_apk_file_size

 

APK 文件格式

在说如何给apk瘦身之前,让我们先来看看apk文件内部的结构到底是怎么一回事。说简单点,一个apk文件就是包含一些文件的压缩包。作为开发者,我们通过使用 unzip 命令解压这个apk文件一探apk的内部结构。下面的文件结构就是我们使用 unzip <your_apk_name>.apk1这个命令所获得的:

/assets
/lib
  /armeabi
  /armeabi-v7a
  /x86
  /mips
/META-INF
  MANIFEST.MF
  CERT.RSA
  CERT.SF
/res
AndroidManifest.xml
classes.dex
resources.arsc

我们可能对上面大部分的文件和目录都很熟悉。它们和我们在实际开发app的时候所看到得项目结构一样,包含了: /assets, /lib, /res, AndroidManifest.xml. 还有一些文件可能是我们第一次看到。一般说来,classes.dex, 它包含了我们所写的Java代码经过编译后class文件;resources.arsc 包含了预编译之后的资源文件(比如values文件,XML drawables 文件等。)。

由于apk文件只是一个简单地压缩文件,这就意味着它有两种大小:即压缩前的大小和压缩后的大小。这篇文章我将主要关注压缩后的大小。

如何减少apk文件大小

减少apk文件大小可以从几个方面入手。由于每个app都是不同的,所以没有什么绝对规则来给apk文件瘦身。作为apk文件的三个重要组成部分,我们可以考虑从它们开始入手:

  • Java 源代码
  • 资源文件(resources/assets)
  • native code

所以接下来的招式都是由减少这些组件的大小出发,进而减少整个app的大小。

掌握良好的编码习惯

这是减少apk文件至关重要的第一步。你要对自己的代码了如子掌。你要移除掉所有无用处的dependency libraries,让你的代码一天比一天优秀,持续地优化你的代码。总而言之,保持一个简洁,最新的代码基础是减少apk文件至关重要的一环。

当然,从零开始一个项目并为这个项目保持一份简洁的代码基础很容易。项目越老,这个工作就越困难。事实上,拥有一大段历史背景的项目必须要去处理各种死代码和无用代码。还好有许多的开发工具可以帮我们来做这些事情……

使用 Proguard

Proguard 是一个很强悍的工具,它可以帮你在代码编译时对代码进行混淆,优化和压缩。它有一个专门用来减少apk文件大小的功能叫做 tree-shaking。Proguard 会遍历你的所有代码然后找出无用处的代码。所有这些不可达(或者不需要)的代码都会在生成最终的apk文件之前被清除掉。Proguard 也会重命名你的类属性,类和接口,然整个代码尽可能地保持轻量级水平。

也许现在你会认为 Proguard 是一个相当有效地工具。但是能力越大,责任也就越大。现在许多开发这认为Proguard有点让人不省心,因为它会重度依赖反射。哪些类或者属性需要被处理或者不能处理都要开发者对 Proguard 进行配置。

广泛使用 Lint

Proguard 只会对 Java 代码起作用,那么对哪些资源文件呢?比如一张图片 my_image 在 res/drawable 文件夹中,没有被使用,Proguard 只会移除掉 R 类中的引用,但是图片依然还在文件夹中。

Lint 一个静态的代码分析器,你只需通过调用 ./gradlew lint这个简单地命令它就能帮你检查所有无用的资源文件。它在检测完之后会提供一份详细的资源文件清单,并将无用的资源列在“UnusedResources: Unused resources” 区域之下。只要你不通过反射来反问这些无用资源,你就可以放心地移除这些文件了。

Lint 会分析资源文件(比如 /res 文件夹下面的文件) ,但是会跳过 assets 文件 ( /assets 文件夹下面的文件)。事实上assets 文件是可以通过它们的文件名直接访问的,而不需要通过Java引用或者XML引用。因此,Lint 也不能判定某个 asset 文件在项目中是否有用。这全取决于开发者对这个文件夹的维护了。如果你没有使用某个asset 文件,那么你就可以直接清除这个文件。

对资源文件进行取舍

Android 支持多种设备。Android的系统设计让它可以支持设备的多样性:屏幕密度,屏幕形状,屏幕大小等等。到了Android 4.4,它支持的屏幕密度包括: ldpi, mdpi, tvdpi, hdpi, xhdpi, xxhdpi and xxxhdpi。但是你要知道的一点是,Android 支持这么多的屏幕密度并不意味着你需要为每一个屏幕密度提供相应的资源文件。

如果你知道某些屏幕密度的设备只有很少部分用户在使用,那么你就可以直接不需要使用相应屏幕密度的资源文件。就我个人而言,我只会为我的应用提供 hdpi, xhdpi and xxhdpi2 这几个屏幕密度的支持。如果某些设备不是这几个屏幕密度的,不用担心,Android 系统会自动使用存在的资源为设备计算然后提供资源文件。

我这么做得原因很简单。首先,这些设备屏幕密度就能覆盖我 80% 的用户。其次,xxxhdpi 这个屏幕密度只是在为未来的设备做准备,但是未来还未到来。最后,我真的不怎么关心低屏幕密度(比如mdpi 和 ldpi),无论我为这几个屏幕密度努力,结果都是令人伤心地,还不如直接让Android系统对 hdpi 资源文件进行适当地缩放来匹配相应地低端机型。

同样地,在 drawable-nodpi 文件夹里面维持一个文件也能节省空间。当然前提是你觉得对这个文件进行相应地缩放之后呈现的效果你能接受或者这个文件出现的几率很少。

资源文件最少化配置

Android 开发经常会依赖各种外部开源代码库,比如Android Support Library, Google Play Services, Facebook SDK 等等。但是这些库里面并不是所有的资源文件你都会用到。比如, Google Play Services 里面会有一些为其他语种提供翻译,而你的app又不需要这个语种的翻译,而且这个库里面还包含了我的app中不支持的 mdpi 资源文件

还好从Android Gradle Plugin 0.7 开始,你可以配置你的app的build系统。这主要是通过配置 resConfig 和 resConfigs 以及默认的配置选项。下面的 DSL (Domain Specific Language)就会阻止 aapt(Android Asset Packaging Tool)打包app中不需要的资源文件。

defaultConfig {
    // ...

    resConfigs "en", "de", "fr", "it"
    resConfigs "nodpi", "hdpi", "xhdpi", "xxhdpi", "xxxhdpi"
}

 

 

 

 

压缩图片

Aapt(Android Asset Packaging Tool)就内置了 保真图像压缩算法。例如,一个只需 256 色的真彩PNG图片会被aapt 通过一个颜色调色板转化成一个 8-bit PNG 文件。这可以帮助你减少图片文件的大小。当然你还可以通过Google查找相应的优化工具,比如 pngquant, ImageAlpha 和 ImageOptim 等。你可以从中选择一个适合你的工具。

还有一种只在Android平台上存在的图片文件也可以优化,它就是 9-patches。就我目前所知道,我还没发现有这个文件的优化工具。然而你只需要求你的设计师将它的可扩展区域和内容区域尽可能地减少即可。这不但可以减少资源文件的大小,还能使得以后资源文件的维护变得更加简单。

限制app支持的cpu 架构的数目

一般说来Android 使用Java 代码即可以满足大部分需求,不过还是有一小部分案例需要使用一些 native code。就像之前对资源文件那样opinionated,你可以这些 native code opinionated。 在当前的Android 生态系统中,让你的app支持 armabi 和 x86 架构就够了。这里有一篇相当不错的关于如何瘦身native 代码库的文章,你可以参考参考。

尽可能地重用

重用资源可能是你在进行移动开发时需要了解的最重要的优化技巧之一。比如在一个 ListView 或者 RecyclerView,重用可以帮助你在列表滚动时保持界面流畅。重用还可以帮你减少apk文件的大小。例如,Android 提供了几个工具为一个asset文件重新着色,在Android L中你可以使用 android:tintandroid:tintMode 来达到效果,在老版本中则可以使用 ColorFilter 。

如果系统中有两种图片,一种图片是另一种图片翻转180°得到的,那么你就可以移除一种图片,通过代码实现。比如你现在有两种图片分别命名为 ic_arrow_expandic_arrow_collapse :

expand_collapse

 

你可以直接移除掉 ic_arrow_collapse 文件,然后在ic_arrow_expand 的基础上创建一个 RotateDrawable 。这个方法也可以让你减少设计人员的工作:

<?xml version="1.0" encoding="utf-8"?>
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/ic_arrow_expand"
    android:fromDegrees="180"
    android:pivotX="50%"
    android:pivotY="50%"
    android:toDegrees="180" />

 

在合适的时候使用代码渲染图像

在某些情况下,直接使用Java 代码渲染图像也能获得不错的效果。比如逐帧动画就是一个很好的例子。最近我都在尝试一些Android Wear 的开发,了解了一下Android wearable support library。就像其他的Android support library 一样,这个库里面也有一些工具来处理穿戴设备的。

不过让我吃惊的是,当我简单地构建了一个 “Hello World”示例,最后得到的apk文件竟然有1.5MB。于是我快速地研究了一下 wearable-support.aar 文件,发现这个库有两个逐帧动画,并分别支持了3种不同的屏幕密度:一个 “success” 动画 (31 frames) 和一个 “open on phone” 动画 (54 frames)。

wearable_support

 

这个逐帧success动画是被一个叫做 AnimationDrawable 所定义的:

<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android" android:oneshot="true">
    <item android:drawable="@drawable/generic_confirmation_00163" android:duration="33"/>
    <item android:drawable="@drawable/generic_confirmation_00164" android:duration="33"/>
    <item android:drawable="@drawable/generic_confirmation_00165" android:duration="33"/>
    <item android:drawable="@drawable/generic_confirmation_00166" android:duration="33"/>
    <item android:drawable="@drawable/generic_confirmation_00167" android:duration="33"/>
    <item android:drawable="@drawable/generic_confirmation_00168" android:duration="33"/>
    <item android:drawable="@drawable/generic_confirmation_00169" android:duration="33"/>
    <item android:drawable="@drawable/generic_confirmation_00170" android:duration="33"/>
    <item android:drawable="@drawable/generic_confirmation_00171" android:duration="33"/>
    <item android:drawable="@drawable/generic_confirmation_00172" android:duration="33"/>
    <item android:drawable="@drawable/generic_confirmation_00173" android:duration="33"/>
    <item android:drawable="@drawable/generic_confirmation_00174" android:duration="33"/>
    <item android:drawable="@drawable/generic_confirmation_00175" android:duration="333"/>
    <item android:drawable="@drawable/generic_confirmation_00185" android:duration="33"/>
    <item android:drawable="@drawable/generic_confirmation_00186" android:duration="33"/>
    <item android:drawable="@drawable/generic_confirmation_00187" android:duration="33"/>
    <item android:drawable="@drawable/generic_confirmation_00188" android:duration="33"/>
    <item android:drawable="@drawable/generic_confirmation_00189" android:duration="33"/>
    <item android:drawable="@drawable/generic_confirmation_00190" android:duration="33"/>
    <item android:drawable="@drawable/generic_confirmation_00191" android:duration="33"/>
    <item android:drawable="@drawable/generic_confirmation_00192" android:duration="33"/>
    <item android:drawable="@drawable/generic_confirmation_00193" android:duration="33"/>
</animation-list>

 

这样做得好处就是 (我当然在讽刺) 每帧显示33ms,这使得整个动画保持在30fps的频率。如果每帧16ms这将会导致整个库是之前的两倍大。如果你去看源码你会发现很有趣。在 generic_confirmation_00175 这一帧 (15 行) 将持续显示 333ms。 generic_confirmation_00185 紧跟着它。这个优化节省了9个类似的帧 (包含了从176 帧到 184 帧) 。不过最后神奇的是 wearable-support.aar 竟然神奇的包含了这个9个完全无用的帧,而且还以3中屏幕密度展示。3

在代码中来渲染这样的动画明显会很花时间。然而当你维持动画运行在60fps这样的频率可以大幅度的减少apk的大小。在写这篇博文的时候,Android还没提供工具来渲染这样的动画。但是我希望Google正在开发新一代的轻量级实时渲染系统来保证material design的细节呈现。当然“Adobe After Effect to VectorDrawable” 之类的设计工具也能提供很多方便。

如何更进一步?

上面所有的招式都集中在app或者library 开发者。也许我们还可以在app分发渠道方面为apk大小做出一些改变?我想可以在app 分发服务器端做一些改进,或者在官方应用商店。例如,我们可以期待官方应用商店在用户安装app的时候为设备绑定相应的native 库而摒弃那些无用的。

同样地,我们还可以想象只根据目标设备的配置来打包应用。不幸的是,这可能破坏Android 生态一个重要的功能特性:配置热置换。事实上,Android一开始就是位处理各种实时的配置更改(语言,屏幕转向)而设计的。如果我们移除掉与目标屏幕不兼容的资源文件,这可以极大的减少文件大小。不过Android需要处理实时的屏幕密度更改。即便我们假设废除这种功能,我们仍然需要处理为不同的屏幕密度设计的图片以及其他配置(比如屏幕朝向,最小宽度等)。

服务器端的apk打包看起来很强大。但这样会冒很大得风险,因为最终传送给用户的apk会于开发者发给的服务器的完全不同。分发一些缺失资源文件的apk可能会导致app崩溃。

总结

设计就是在一个约束集里面找出最好的方案。显然apk文件的大小就是一个约束。不要害怕为了让多个方面变得更好而放松一个方面的约束。例如,当你要降低UI的渲染效果时,不要犹豫,因为这可以让apk的大小减小,同时使得app的运行也更加流畅。你99%的用户是感受不到UI质量变低的,但是他们会注意到apk文件变小了,运行也更加流畅了。总之,你需要将app各方面进行整体考虑,而不是仅仅几个方面的斟酌。

Android Studio 开发技巧集锦二

本文翻译自:http://www.developerphil.com/android-studio-tips-of-the-day-roundup-2/

行复制

Mac OS: Command+D
Windows & Linux: Ctrl+D

这个快捷键的厉害之处在于它会复制当前行同时又不会影响系统的剪贴板。效果如下图:

11-duplicate_lines

扩大或者缩小选择范围

Mac OS : alt+/
Windows & Linux: (Ctrl+w )/(Ctrl+Shift+w)

这个操作会扩大(或者缩小)当前的选择范围。比如当前选中一个变量,此操作会把选中范围扩展至句子,方法等。具体效果见下图:

12-expand_shrink_selection

包裹代码段

Mac OS : Command+Alt+t
Windows & Linux : Ctrl+Alt+t

此操作会选中一段代码段,然后使用if语句或者循环等结构包裹这段代码。当然,如果你什么都没选择,那么它会默认选择当前行代码。效果如下图:13-surround_with

查询最近编辑文件

Mac OS : Command+e
Windows & Linux : Ctrl+e

此操作将会调出最近编辑文件列表。

14-recents

代码模板

Mac OS : Command+j
Windows & Linux : Ctrl+j

此快捷键可以让你迅速地在文件中插入预留的代码片段。更有趣的是你还可以通过模板定制这些代码片段,即可以为这些代码模板设置参数。15-live_templates

 

方法整体移动

Mac OS : Command+Alt+/
Windows & Linux : Ctrl+Shift+/

这个快捷键和之前介绍过的代码行移动有点相似,只不过这个是移动整个方法体。这样就可以免掉剪切复制的累赘操作了。

15-movemethods

代码补全

Mac OS : Command+Shift+Enter
Windows & Linux : Ctrl+Shift+Enter

这个操作会将还未完成的代码结构补全。一般可以补全的情形如下:

  • 在代码行后面添加分号;
  • 为 if, while 或者 for 语句添加括号
  • 为方法声明添加括号。

16-completestatement

 

回到上次最后编辑位置

Mac OS : Command+Shift+backspace
Windows & Linux : Ctrl+Shift+backspace

让光标自动移到上次最后编辑的位置,具体效果如图:17-navigate-previous-changes

 

代码行合并

Mac OS : Ctrl+Shift+j
Windows & Linux : Ctrl+Shift+j

按照语法规则将可以合并的代码行进行合并,保持代码精简。它主要适用以下规则:

  • 合并两行评论代码时,评论变成一行,会去掉另一行的“//”,
  • 合并多行字符串的时候,会移掉“+“和双引号
  • 将变量声明定义和赋值合并。

18-joinlines

 

操作当前文件信息

Mac OS : Alt+F1
Windows & Linux : Alt+F1

此快捷键可以让你迅速地查询文件结构,存储位置,项目视图等等。一般用来查询文件的存储位置。

19-select-in

 

移除包裹代码

Mac OS : Command+Shift+Delete

Windows & Linux : Ctrl+Shift+Delete

移除代码结构中的包裹代码,比如 if 语句,  while 循环, 或者 try/catch 语句20-unwrap