Android 为什么不要SoftReference了

关于 SoftReference 在缓存中的使用问题,Android 在官方文档 SoftReference,明确指出

Avoid Soft References for Caching

而给出的原因是:

The runtime doesn't have enough information on which references to clear and which to keep. 
Most fatally, it doesn't know what to do when given the choice between clearing a soft reference and growing the heap.

即:

Runtime 没有足够的信息来判别应该清除哪个 SoftReference(持有的对象),
更无法判定当 App 要求更多内存的时候,是应该清除 SoftReference,还是增大 App 的Heap。
 

听着是不是很合理,但是这个根本说不过去啊。

因为在正常的 JVM 中,只要不会触发 OOM(达到系统内存上限或者到达 JVM 设定的内存上限),JVM 就应该毫不留情的增大 Heap 来维持应用的正常运行。
而没有必要考虑是先清理 SoftReference,还是增大 Heap 这种无聊的问题。

Android Runtime与 JVM 不一样的是:用户 App 通常没有权限来设定自己的最大可用内存,这个是由系统全权把握, 单个 App 使用的最大内存容量是固定的:

1
Runtime.getRuntime().maxMemory()

其他就是跟 JVM 差不多了,Android 在启动每一个 App 的时候,也并不是一开始就给每个 App 分配固定的上限内存,也是按需动态分配,所以,这应该不是技术问题。

我个人觉得,这只是用户体验上的取舍问题,Android 与 PC 系统最大的区别就是:
通常情况下,PC 系统不会杀死任何一个用户应用来运行另一个应用,而 Android 可能在任意时刻杀掉哪些优先级比较低的 App 来释放内存,从而给优先级高的 App 提供资源。

在 Android 上面,App 的优先级通常是用户的交互行为赋予的。
当一个 App 被用户调到前台,Android 会尽量满足这个 App 的一切非特权要求,如果此 App 占用的内存比较大,
那么,会挤压其他后台运行 App 的生存空间,使得 Android 能够缓存的应用数目减少,同时减缓应用被再次调起的速度;
另一方面,如果后台 App 占用的内存比较大,那么自身被杀死的几率也会相应增大。
考虑到这些情况,在有内存需求的情况下,如果 Android Runtime 总是一味增大对应 App 的 Heap,那么,最终损害的只是用户的整体体验,这他妈就很尴尬了。

因此,Android 对 SoftReference 这个“墙头草”采取了比较激进的措施,即尽可能的多的找机会来清除它们,结果就是还不如使用 WeakReference 呢,起码效果和语义都是可预期的。
同时,Android Runtime 也将内存控制的一部分措施让渡给开发者,这里面就包括 SoftReference。
开发者可以采取各自的策略来处理类似的内存问题,换句话说,就是用“开发者智能”替换“运行时策略”。如果开发者置之不理,那么结果就是要么是没有缓存,要么是内存暴涨。
Android 官方推荐的是 LruCache来进行缓存。

SoftReference 与 LruCache

LruCache 虽然挺有用,但是有些问题还是需要了解的。

  1. LruCache 需要设置容量大小,不同的机型可用内存不一样,因此,不同手机上这个大小使应该动态调整的,自古“一刀切”都是没有好结果的。
  2. LruCache 是面向开发者的,也就是只有明确使用 LruCache,缓存策略才有效。如果在 LruCache 之外想缓存或者分配大对象,没有办法自动来释放 LruCache 的空间以容纳新的对象。
    在这一点上,SoftReference 是符合预期的,因为 SoftReference 是被 Runtime 控制的,可以在全局进程来动态调整回收。

Volley缺陷

1. 解析 内部直接使用 Memory buffers 来缓存响应

在 BasicNetwork 实现中,从服务器获取到 Entity 之后,会将 Entity 转换成 byte 数组,缓存在 ByteArrayPool 中。
本来 ByteArrayPool 的出发点是为了减少虚拟机在堆上动态分配内存的开销。但是,如果响应本身就比较大(比如一张大图),这个时候就会有问题了。
一次性向 ByteArrayPool 申请较大内存,不仅引起内存占用量上升,甚至可能引起 OOM 错误。

2. 内部依赖 Apache HttpClient

从 Android 6 开始,Apache HttpClient 被废弃了,这就导致如果想要使用 Volley,要么将 Compile SDK 修改为 23 以下,要么使用

    android {
        useLibrary 'org.apache.http.legacy'
    }

来增加 HttpClient 支持。

3. 粗暴的 Retry Policy

默认超时事件 2.5s,基本算是鸡肋的配置,因为除了 wifi 环境,没可能在 2.5s 之内拿到响应,所以每次都得重新设置。

Android NDK与SWIG

本文主要是《Android C++ 高级编程——使用 NDK》的笔记。
国内专门讲 NDK 的书籍寥寥无几,这本貌似是唯一一本翻译的,当然,国外还是有好几本关注 NDK 的书籍,但是都没有中文译本而已。

因为偷懒,实践的时候从网上拷贝的代码,结果某些作者太不靠谱,抄写的代码都是错的,坑死我了。

SWIG 基础

可以参考:

  1. 开发人员 SWIG 快速入门
  2. swig 官网

在 Android 中的使用

ubuntu 14.04 + eclipse

在 jni 文件夹中定义接口文件,SWIG 会基于此接口文件来生成相应的集成代码:

下面是接口文件 Unix.i:

1
2
3
4
5
6
7
8
9
10
11
12
%module Unix

/* unistd.h 是 C 和 C++ 中提供对 POSIX API 支持的头文件 */

%{
#include <unistd.h>
%}

typedef unsigned int uid_t;

extern uid_t getuid(void);

这个时候可以直接调用 swig 来生成集成代码,

1
swig -java -package dev.xesam.ndk -outdir dev/xesam/ndk Unix.i

注意:outdir 一定要事先就存在

不过为了方便,还是直接与 eclipse 整合比较好。在 jni 文件夹定义一个 swig.mk,将 swig 处理单独出来,swig.mk 内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 定义包名,对应 -package 参数
ifndef MY_SWIG_PACKAGE
$(error MY_SWIG_PACKAGE is not defined.)
endif

# 定义输出目录,对应 -outdir 参数
# subst 表示替换,即用 "/" 替换 包名中的 "."
MY_SWIG_OUTDIR :=$(NDK_PROJECT_PATH)/src/$(subst .,/,$(MY_SWIG_PACKAGE))

# 定义生成文件类型,这里默认是 c
ifndef MY_SWIG_TYPE
MY_SWIG_TYPE := c
endif

# 如果目标源文件是 c++,那么在执行 swig 命令的时候就需要加上 −c++ 参数
ifeq ($(MY_SWIG_TYPE),cxx)
MY_SWIG_MODE := −c++
else
MY_SWIG_MODE :=
endif

# 将生成的 .c 文件加入编译文件中
LOCAL_SRC_FILES += $(foreach MY_SWIG_INTERFACE,$(MY_SWIG_INTERFACES),$(basename $(MY_SWIG_INTERFACE))_wrap.$(MY_SWIG_TYPE))

# 定义 target,每个待生成的 XXX_wrap.c 源文件都依赖与之对应的 XXX.i 接口文件
# 由于 outdir 一定要存在,所以先创建 outdir 目录结构
# $< 表示第一个依赖文件,也就是对应的 XXX.i 接口文件
%_wrap.$(MY_SWIG_TYPE) : %.i
$(call host-mkdir, $(MY_SWIG_OUTDIR))
swig \
-java \
$(MY_SWIG_MODE) \
-package $(MY_SWIG_PACKAGE) \
-outdir $(MY_SWIG_OUTDIR) \
$<

注意,按照 Makefile 的规范来写,特别是空格与 TAB 的区别。

上面定义的 MY_SWIG_PACKAGE 等变量都定义在 Android.mk 中,将 swig.mk 加入到 Android.mk 即可。Android.mk 内容如下;

1
2
3
4
5
6
7
8
9
10
11
12
13
LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS) # 清除除了 LOCAL_PATH 之外的 LOCAL_* 变量

LOCAL_MODULE := hello-ndk # 设定一个唯一的名称

MY_SWIG_PACKAGE := dev.xesam.ndk
MY_SWIG_INTERFACES := Unix.i
MY_SWIG_TYPE := c

include $(LOCAL_PATH)/swig.mk

include $(BUILD_SHARED_LIBRARY)

定义完毕之后,工程大致结构如下:

project_dir
    |--src
    |--jni
        |--Android.mk
        |--Application.mk
        |--swig.mk
        |--Unix.i
    

在项目根目录运行

1
ndk-build

输入大致如下:

1
2
3
4
5
6
7
8
9
10
11
mkdir -p  ./src/dev/xesam/ndk
swig \
-java \
\
-package dev.xesam.ndk \
-outdir ./src/dev/xesam/ndk \
jni/Unix.i
[armeabi] Compile thumb : hello-ndk <= Unix_wrap.c
[armeabi] SharedLibrary : libhello-ndk.so
[armeabi] Install : libhello-ndk.so => libs/armeabi/libhello-ndk.so

运行完毕之后,工程大致结构如下:

project_dir
    |--src
        |--dev
            |--xesam
                |--ndk
                    |--Unix.java
                    |--UnixJNI.java
    |--jni
        |--Android.mk
        |--Application.mk
        |--swig.mk
        |--Unix.i
        |--Unix_wrap.c
    |--libs
        |--armeabi
            |--libhello-ndk.so
            

libhello-ndk.so 可以在工程代码直接使用,当然,生成的 UnixJNI.java 需要补充 loadLibrary 调用:

1
2
3
4
5
6
public class UnixJNI {
...
static{
System.loadLibrary("hello-ndk");
}
}

测试Activity:

1
2
3
4
5
6
7
8
9
10
11
12
13
14

import dev.xesam.ndk.Unix;

public class MainActivity extends Activity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView tv = (TextView) findViewById(R.id.tv);
tv.setText(Unix.getuid() + "");
}
}

Android NDK与Eclipse

AS 对 NDK 的支持一直不太好,所以基本所有关于的 NDK 的书籍都是基于 Eclipse 开发的,不过 Eclipse 已经放弃支持了,
AS 支持 NDK 的实验版 gradle 插件已经出来了,可以试试。
本文简单记录《Android.NDK.Beginner’s.Guide》的 Eclipse 配置而已。

环境

ubuntu: 14.04
eclipse: Mars.2 Release (4.5.2) 安装 ADT,CDT

准备工作

添加环境变量:

JAVA_HOME
ANDROID_HOME
ANDROID_NDK

javah 生成头文件

主要使用 External Tools:

Run --> External Tools --> External Tools Configurations 

新建一个 Configuration:

ndk-eclipse-1

${system_path:javah}

javah 命令的路径,前提是 javah 已经加入到 $PATH 中,不然需要指定 javah 的完整路径。

${project_loc}/jni

生成的 *.h 文件目录,${project_loc} 表示工程目录

-cp "${project_classpath}:${env_var:ANDROID_HOME}/platforms/android-23/android.jar" ${java_type_name}

指定 classpath,javah 命令需要用到。由于 javah 是从 class 文件中生成对应的 *.h 文件,因此,src 中改动之后需要先 build 一下,javah 才能正确生成。
所以,最好是开启 Build Automatically。

如果要生成对应的 .h 文件,先在 src 中选择要生成的 .java 源文件,然后运行

Run --> External Tools --> ndk_header(这里是刚添加的,也可以是其他的名字)

ndk-build

将 ndk-build 命令集成到 eclipse 中:

  1. 先将 Android 工程转成 C/C++ 工程,

     New --> Other --> C/C++ --> Convert to a C/C++ Project
    
  2. 工程上右键 Properties

    C/C++ Build: 配置编译选项

         --> Builder Settings --> Build command 改为 ndk-build
     
    

    ndk-eclipse-2

         --> Behaviour --> Build on resource save .每次修改保存的时候即时编译
     
    

    ndk-eclipse-3

    C/C++ General: 添加各种头文件

         --> Paths and Symbols 具体位置视情况而定
         /apps/android-sdk-linux/ndk-bundle/platforms/android-9/arch-arm/usr/include
         /apps/android-sdk-linux/ndk-bundle/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64/lib/gcc/arm-linux-androideabi/4.9.x/include
         
    

    ndk-eclipse-4

    每种语言都加上,所以添加的时候选择 all 就行。

    ndk-eclipse-4

    Builders :配置 build 顺序,将 CDT Builder 调整到 Android Package Builder 的正上方:

    ndk-eclipse-5

  3. 自动运行 javah

    这个其实没必要,因为没必要每次都生成 .h 文件,就算生成了,也没有可用的实现,所以个人觉得还是手动调用比较靠谱。
    操作就是添加一个 Builder 放到 CDT Builder 的正上方:

     Builders --> New --> Program
     
    

    所有参数配置与 “javah生成头文件的配置”一样即可,

一切完成之后,每次 Run 的时候,都会将最新的 so 文件打包编译打包进去。

调试

上面的的 ndk-build 命令需要添加参数

ndk-build NDK_DEBUG=1

Google 官方的 ADT 只更新到 23.0.7,如果你的 ndk 比较新,那么,调试的时候会出现各种乱七八糟的 bug ,比如,找不到 gdb 之类。
所以,先把 ADT 更新到最新,野生版本参见https://github.com/khaledev/ADT

然后直接 Debug AS “Android Native Application” 就行了,也不需要像书中那般配置,因为书的配置已经过时了。

Android 的SoundPool

SoundPool 适合播放小而短促的声音,比如声效。可以同时播放多个声音,效率高。
MediaPlayer 适合播放大片段的声音,比如音乐。一次只能播放一个声音,新的声音会中断当前正在播放的声音。

简单使用

看名字就知道,SoundPool 是一个“池”。因此,SoundPool 会先将多个的声音文件全部加载进内存中,需要播放的时候就可以立即播放。

所以,大致的步骤应该是:

  1. 创建一个池
  2. 加载声音
  3. 等待声音加载完成
  4. 播放声音

一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

//创建 SoundPool
//新版本推荐使用 SoundPool.Builder 来创建
SoundPool mSoundPool = new SoundPool(2, AudioManager.STREAM_MUSIC, 0);

//加载声音文件,load 方法有多个重载,按需使用
int soundId = mSoundPool.load("/storage/0/1.aac", 1);

//设置加载监听
mSoundPool.setOnLoadCompleteListener(new SoundPool.OnLoadCompleteListener() {
@Override
public void onLoadComplete(SoundPool soundPool, int sampleId, int status) {

}
});

//播放声音,播放一遍,然后重复10遍,具体参数参见文件
final int streamId = mSoundPool.play(soundID, 1, 1, 0, 10, 1);

new Handler().post(new Runnable(){
@Override
public void run(){
mSoundPool.stop(streamId);
mSoundPool.unload(soundId);
}
});

几点说明

soundId 与 streamId

注意上面代码的区别. SoundPool#load 加载完成返回的是 soundId, SoundPool#unload 卸载的也是 soundId。

SoundPool#play 播放的也是 soundId,但是 SoundPool#play 播放返回的是 streamId,同时 SoundPool#stop 停止的是 streamId。

对于同一个声音文件,每次加载 SoundPool#load 的时候,返回的 soundId 都是不同的。
对于同一个 soundId,每次播放 SoundPool#play(soundId) 的时候,返回的 streamId 也是不同的,因此这个不能重用。
因此,在这两种 ID 的处理上,需要谨慎。

Sound IdStream Id
load(√)返回 soundId×
unload(√)传入 soundId×
play(√)传入 soundId(√)返回 streamId
pause×(√)传入 streamId
resume×(√)传入 streamId
stop×(√)传入 streamId

资源释放

这里的释放包括两方面:

  1. 释放 sound。即 unload(soundId),释放已经加载的声音文件。
  2. 释放 stream。即 stop(streamId),释放每次 play 的播放流,可以起到停止声音的效果,如果只是想暂停/恢复,可以用 pause/resume。

通常来说,运行过程中没必要释放sound,因为使用 SoundPool 通常是为了迅速播放小文件,如果每次都释放 sound,反而会影响效率。在程序结束的时候再统一释放即可。

可能问题

  1. play 没有声音

    可能原因有:没有正确加载 sound,或者同时加载的声音数目超过上限, 或者 sound 还未 load 完毕。

    因此,可以先检测是否有加载过的 soundId,如果有就直接播放,如果没有则启动加载过程,在 OnLoadCompleteListener 回调中再进行播放。

    如果是因为加载的声音数目超过上限,需要先 release 资源,然后再重新 load,注意 unload 不一定会释放数目。

  2. 声音播放结束

    SDK 并没有提供方法来获知一段声音是否播放完毕

priority 参数

SoundPool#play 中的 priority 参数,只在同时播放的流的数量超过了预先设定的最大数量时起作用,管理器将自动终止优先级低的播放流。
如果存在多个同样优先级的流,再进一步根据其创建事件来处理,新创建的流的优先级是最低的,将被终止或者忽略。

SoundPool#load 中的 priority 参数暂时没什么用,我估计也是用来自动处理 sound 加载数目的问题的。

参考

https://developer.android.com/reference/android/media/SoundPool.html
http://blog.csdn.net/qduningning/article/details/8680575

fork与printf

测试 fork 的时候,出现以下的一个情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <unistd.h>

int main(int argc, char const *argv[]){
printf("%s,parent:%d,current:%d\n", "start", getppid(), getpid());
pid_t fpid;
fpid = fork();
if (fpid < 0){ //error
printf("%s:%d\n", "error", fpid);
}else if (fpid == 0){ //child
printf("%s,parent:%d,current:%d, fpid:%d\n", "child", getppid(), getpid(), fpid);
}else{ //host
printf("%s,parent:%d,current:%d, fpid:%d\n", "host", getppid(), getpid(), fpid);
}
return 0;
}

按理来说,结果应该是

start,parent:15111,current:19431
host,parent:15111,current:19431, fpid:19432
child,parent:19431,current:19432, fpid:0

但是在 sublime 使用 CTRL + SHIFT + B 执行的时候,结果却是:

start,parent:15111,current:19431
host,parent:15111,current:19431, fpid:19432
start,parent:15111,current:19431
child,parent:19431,current:19432, fpid:0

Read More

Javascript Canvas路径

Javascript 中的 Canvas 是基于路径的,同一时刻时能有一条路径而且肯定会有一条路径。
路径中可以包含很多条子路径,即 N 条子路径构成了一条路径。与路径相关的操作:

Context#beginPath() 用于创建一条新的路径,这也就意味着先前的路径丢失了,因为只能有一条路径。
moveTo/lineTo/... 之类的命令用于创建子路径。

说到 beginPath,就不得不提到 closePath,两者是不是有很“紧”的联系呢?答案是几乎没有关系。
closePath 的意思不是结束路径,而是闭合路径,它会试图从当前路径的终点连一条子路径到起点,让当前整个路径闭合起来。
但是,这并不意味着它之后的路径就是新路径了!

绘图指令有两种:

  1. 保留绘图指令(retained draw command),即不产生直接效果的指令,比如 moveTo, lineTo, (Curve)To, arc, rect。
  2. 直接绘图指令(direct draw command),这些指令既不需要创建新路径,也不对当前路径产生影响,比如 fill/strokeRect, fill/strokeText, drawImage, get/putImageData.

所以,关于路径的规则就是:

  1. 如果你使用 retained draw command,则需要 beginPath
  2. 如果你使用 direct draw command,则无需 beginPath

save() & restore()

save() & restore() 不会影响路径,影响的是什么?影响各种设置:

  1. 渲染设置 : strokeStyle / fillStyle / globalCompositeOperation / globalAlpha / lineWidth / font / textAlign/ shadow / …
  2. 变换(transform)设置 translate/scale/rotate/…
  3. 区域(clip)设置

Immediate Mode
Retained Mode

Android 生命周期监听

navi,这个库比较好玩,实现了我早就想要的一种开发方式:让组件主动监听 Activity 以及 Fragment 的生命周期,然后注册相应的回调。

在日常开发中,经常会有某些操作或者对象需要响应 Activity 以及 Fragment 的生命周期转换,这个时候要么我们得在 Activity 的生命周期方法里面主动来处理,
要么提供一个包装过的 Activity,让目标 Activity 继承这个包装 Activity,当然,实际上都是一样的。
比如,如果我们想在 OnResume 的时候开启一个定时任务,在 OnPause 的时候关闭定时器,我们会这么写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

public class Main3Activity extends AppCompatActivity {
HandlerTimer handlerTimer = new HandlerTimer();

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main3);
}

@Override
protected void onResume() {
super.onResume();
handlerTimer.start();
}

@Override
protected void onPause() {
super.onPause();
handlerTimer.stop();
}

public static class HandlerTimer extends Handler {
String TAG = getClass().getName();

public HandlerTimer() {
}

@Override
public void handleMessage(Message msg) {
if (msg.what == 1) {
Log.e(TAG, "Tick");
sendEmptyMessageDelayed(1, 1000);
}
}

public void start() {
Log.e(TAG, "Start");
sendEmptyMessageDelayed(1, 1000);
}

public void stop() {
Log.e(TAG, "Stop");
removeMessages(1);
}
}
}

这样写不仅特别繁琐,还需要各种第三方代码(这里是 HandlerTimer )暴露每一个对应的生命周期方法。
于是,我们的考虑就是:这些响应生命周期状态转换的工作应该交给第三方代码自己去完成,第三方代码可以主动去监听 Activity 的生命周期变化,从而做出自己认为最合适的动作。

navi 就是基于这种目的而产生的。运用 navi, 上面的代码可以改写为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class Main2Activity extends NaviAppCompatActivity {

HandlerTimer handlerTimer = new HandlerTimer();

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main2);
handlerTimer.bind(this);
}

public static class HandlerTimer extends Handler {
String TAG = getClass().getName();

public HandlerTimer() {
}

@Override
public void handleMessage(Message msg) {
if (msg.what == 1) {
Log.e(TAG, "Tick");
sendEmptyMessageDelayed(1, 1000);
}
}

public void bind(NaviAppCompatActivity activity) {

activity.addListener(Event.RESUME, new Listener<Void>() {
@Override
public void call(Void aVoid) {
Log.e(TAG, "Start");
sendEmptyMessageDelayed(1, 1000);
}
});
activity.addListener(Event.PAUSE, new Listener<Void>() {
@Override
public void call(Void aVoid) {
Log.e(TAG, "Stop");
removeMessages(1);
}
});
}
}
}

更多细节可以参考 navi 的示例与源码。

需要指出的是,个人觉得这个库还不是很完善,现在只能支持 Activity 和 Fragment,而且支持方式也略具侵入性。
尽管如此,这种设计我还是很喜欢的,把 Activity 变为一个纯容器,模式运用会更灵活,当然,坑也更多。

Android 使用MockWebServer来进行单元测试

MockWebServer 是 square 出品的跟随 okhttp 一起发布,用来 Mock 服务器行为的库,用来做单元测试挺好。

有一个单独的文档https://github.com/square/okhttp/tree/master/mockwebserver

MockWebServer mock 了 http 协议栈,所以基本上可以用来调试所有的 http 请求。

基本使用

使用也比较简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Test
public void test() throws Exception{
// 1. 创建服务器
MockWebServer server = new MockWebServer();

// 2. 添加预置的响应,响应会按照先进先出的顺序依次返回
server.enqueue(new MockResponse().setResponseCode(503).setBody("hello, world!"));
server.enqueue(new MockResponse().setBody("i am xesam"));
server.enqueue(new MockResponse().setResponseCode(404).setBody("not found"));

// 3. 启动服务器
server.start();

// 4. 获取请求 url,不能使用普通的 URL,一定要使用 server.url() 返回的 URL,不然没法进入 Mock 服务器
HttpUrl baseUrl = server.url("/reg1");

// 5. 发送请求
HttpRequest.get(baseUrl.url()).body()

// server.takeRequest() 是一个阻塞操作,直到接收到请求
RecordedRequest request1 = server.takeRequest();

// 这里可以查看服务器获取的请求信息,可以查看 http 报文头之类的协议信息
assertEquals("/v1/chat/", request1.getPath());
}

MockResponse 默认是状态码 200, body 为空,但是可以定制所有的协议内容:

1
2
3
4
5
MockResponse response = new MockResponse()
.addHeader("Content-Type", "application/json; charset=utf-8")
.addHeader("Cache-Control", "no-cache")
.setBody("{}");

还可以模拟网速比较慢的情况:

1
response.throttleBody(1024, 1, TimeUnit.SECONDS);  //每秒传递 1024 字节

按照最开始的描述,可以用 enqueue 来添加预置响应,其实可以做得更像服务器一些,即根据 url 来进行响应分发:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
final Dispatcher dispatcher = new Dispatcher() {

@Override
public MockResponse dispatch(RecordedRequest request) throws InterruptedException {

if (request.getPath().equals("/v1/login/auth/")){
return new MockResponse().setResponseCode(200);
} else if (request.getPath().equals("v1/check/version/")){
return new MockResponse().setResponseCode(200).setBody("version=9");
} else if (request.getPath().equals("/v1/profile/info")) {
return new MockResponse().setResponseCode(200).setBody("{\\\"info\\\":{\\\"name\":\"Lucas Albuquerque\",\"age\":\"21\",\"gender\":\"male\"}}");
}
return new MockResponse().setResponseCode(404);
}
};
server.setDispatcher(dispatcher);

如此一来,其实就可以在通常的开发中将后台的接口提前 mock 出来,加快开发进度。

这里需要注意的是不要在 UI 线程启动这个服务器,如果想在 App 里面增加 Mock 支持,最好包装在一个新线程里面。

异步测试

在单元测试中,如果有异步调用,通常就比较麻烦,因为在异步回调还没回来的时候,测试已经完成了,所有这个时候就需要让进程等待或者挂起。
比如,使用 MockWebServer 来测试 Volley:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class Test1 extends InstrumentationTestCase {
public void testVolley() throws Exception {
MockWebServer server = new MockWebServer();
server.enqueue(new MockResponse().setResponseCode(200).setBody("hello, world!"));
server.start();

HttpUrl baseUrl = server.url("/p1");
final BlockingQueue<Object> queue = new ArrayBlockingQueue<>(10);

Volley.newRequestQueue(getInstrumentation().getContext())
.add(new StringRequest(baseUrl.url().toString(), new Response.Listener<String>() {
@Override
public void onResponse(String response) {
queue.add(response);
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
queue.add(error);
}
}));

RecordedRequest request1 = server.takeRequest();
assertEquals("/p1", request1.getPath());

Object obj = queue.take();
if (obj instanceof String) {
Assert.assertEquals("hello, world!", obj.toString());
}

server.shutdown();
}
}

上面的例子中使用一个 BlockingQueue 来等待 Volley 的请求结束,然后验证结果。

当然,也可以用锁,但是其实都挺麻烦。

ThreadLocal设计意图

核心原理

  1. ThreadLocal 处理的是线程的专属对象,各个线程的对象都是独立的。
  2. ThreadLocal 用来辅助平衡效率与资源分配。
  3. ThreadLocal 不是同步机制,也不解决共享对象的多线程竞态条件问题。

基本设计

首先看一个熟悉的场景:排队买票。

从理论上看,针对“排队买票”这个场景,我们可以有以下几种方案设计:

第一种. 固定设置一个售票窗口,每新来一个购票者就排在队伍的最后面。如图:

1

第二种. 动态设置售票窗口数量,每新来一个购票者就新开一个窗口来进行接待。如图:

1

第三种. 固定设置 N 个售票窗口,每新来一个购票者就选择一个售票窗口。如图:

1

比较一下上面的三种方案,很明显现实社会中使用的是第三种,那么各自有什么优势与缺点呢?

Read More