加速 Robolectric 下载依赖库及原理剖析

之前在《App组件化与业务拆分那些事》说过要写一篇怎么Android怎么做业务拆分的技术文,由于开发中遇到一些繁琐问题,打算延后一点再写。

为了及时给点干货读者们,今天笔者写写如雷贯耳的 Robolectric 吧!


Robolectric 的第一次

从我做单元测试开始,一直有小伙伴在群上反映第一次 robolectric 运行太慢了,大半天都更新不完依赖库。

上两天把项目的 robolectric 从3.1.2升到3.2.2,本来已经下好的第三方依赖库,3.2.2要求更高版本,只能再下更高版本的库。用过robolectric都懂的,如下图(gif):

笔者的第一次用robolectric,翻了墙,大概用了半小时下载依赖库。之前除了翻墙,也没什么好办法,后来研究一下,解决的办法还不止一种,接下来分析一下。

Robolectric 到底在做什么?

简单的robolectric test case:

@RunWith(RobolectricTestRunner.class)
public class RoboTest {

    @Test
    public void firstTest() {
        System.out.println("first test");
    }
}

分析日志

截取其中一部分日志:

WARNING: No manifest file found at .\AndroidManifest.xml.
Falling back to the Android OS resources only.
...

Downloading: org/robolectric/android-all/4.1.2_r1-robolectric-0/android-all-4.1.2_r1-robolectric-0.pom from repository sonatype at https://oss.sonatype.org/content/groups/public/
Transferring 1K from sonatype
Downloading: org/robolectric/android-all/4.1.2_r1-robolectric-0/android-all-4.1.2_r1-robolectric-0.jar from repository sonatype at https://oss.sonatype.org/content/groups/public/
Transferring 30702K from sonatype
...

只要英文不太烂,都知道日志说“正在从 https://oss.sonatype.org/content/groups/public/ 下载 android-all-4.1.2_r1-robolectric-0.pom、android-all-4.1.2_r1-robolectric-0.jar ...”。

oss.sonatype.org 是什么?(已科学上网)在浏览器输入https://oss.sonatype.org/

综合判断:

oss.sonatype.org是一个Nexus搭建的maven仓库。robolectric第一次运行,从https://oss.sonatype.org/ 下载一些必要的依赖包。

oss.sonatype.org服务器在哪?
ping oss.sonatype.org:

C:\Users\kkmike999>ping oss.sonatype.org
正在 Ping oss.sonatype.org [52.22.249.229] 具有 32 字节的数据:
请求超时。
请求超时。
请求超时。
请求超时。
52.22.249.229 的 Ping 统计信息:
数据包: 已发送 = 4,已接收 = 0,丢失 = 4 (100% 丢失)

没错,oss.sonatype.org是外国的网站,百度一下52.22.249.229这个IP:

IP地址: 52.22.249.229美国

笔者甚至用国外的vps服务器(vultr.com)来ping oss.sonatype.org,也一直超时。

迅雷下载......想太多

那我们找“4.1.2_r1-robolectric-0”在oss.sonatype.org上的路径,浏览https://oss.sonatype.org/content/groups/public/org/robolectric/android-all/4.1.2_r1-robolectric-0/,如下图:

可以看到 android-all-4.1.2_r1-robolectric-0.pom、android-all-4.1.2_r1-robolectric-0.jar等文件。

小白:“既然知道android-all-4.1.2_r1-robolectric-0.jar网址,直接下载吧,我有迅雷会员,离线下载,妥妥的!”

1小时后,小白下载并看完两集 波多野老师。再看看android-all-4.1.2_r1-robolectric-0.jar的迅雷任务,呃...


Gradle、Jcenter、第三方库

gradle从哪里下载第三方库

我们尝试用gradle下载android-all-4.1.2_r1-robolectric-0。在http://mvnrepository.com/ 找到 android-all-4.1.2_r1-robolectric-0,找到gradle引用它的语句testCompile group: 'org.robolectric', name: 'android-all', version: '4.1.2_r1-robolectric-0'。

在app/build.gradle加入引用:

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:25.1.1'
    testCompile 'junit:junit:4.12'

    testCompile "org.robolectric:robolectric:3.2.2"
    testCompile group: 'org.robolectric', name: 'android-all', version: '4.1.2_r1-robolectric-0'
}

Sync gradle后,Android Studio底部显示下载进度:

可以看到gradle从 https://jcenter.bintray.com下载android-all-4.1.2_r1-robolectric-0依赖库。这个jar有30MB,jcenter有几十KB速度,需要点时间才能下载完。

从jcenter下载的库本地目录

Android Studio project窗口,External Libraries已经有android-all-4.1.2_r1-robolectric-0,证明已经把库下载到本地。

右键->Library Properties

原来jar保存在 C:\Users\{User Name}\.gradle\caches\modules-2\files-2.1\目录下。

再次运行robolectric单元测试

小白:“既然gradle从jcenter下好了android-all-4.1.2_r1-robolectric-0.jar,那这下robolectric就能依赖了吧!?”
于是,小白跑一次刚才的test case...

非常遗憾!robolectric显然不认~/.gradle/的账。

robolectric依赖的本地目录 与 gradle依赖的本地目录不相同

robolectric的依赖库,本地放在哪?

用过eclipse或者inteliJ的同学应该知道,从maven仓库同步回来的库,会存在本地一个目录,这个目录就是~/.m2/。

默认情况:

windows:C:\Users{用户名}.m2\repository\
mac:\Users{用户名}.m2\repository\

如果你自定义了maven本地路径,那就找到设置后的~/.m2/目录。

如果刚才通过gradle从oss.sonatype.org同步了一点点文件回来,这时应该存在 C:\Users\{用户名}\.m2\repository\org\robolectric\android-all\4.1.2_r1-robolectric-0\,目录下有几个文件:

android-all-4.1.2_r1-robolectric-0.jar.tmp
android-all-4.1.2_r1-robolectric-0.pom
android-all-4.1.2_r1-robolectric-0.pom.sha1
android-all-4.1.2_r1-robolectric-0.pom.tmp.sha1.tmp

结论:

robolectric下载的库放在本地目录 ~/.m2/repository/

至于为什么robolectric会依赖~/.m2/,在下一节源码剖析,会说明一下。

robolectric 源代码

RobolectricTestRunner

public class RobolectricTestRunner extends BlockJUnit4ClassRunner {

    private DependencyResolver dependencyResolver;

    protected DependencyResolver getJarResolver() {
        if (dependencyResolver == null) {
        if (Boolean.getBoolean("robolectric.offline")) {
            String dependencyDir = System.getProperty("robolectric.dependency.dir", ".");
            dependencyResolver = new LocalDependencyResolver(new File(dependencyDir));
        } else {
            File cacheDir = new File(new File(System.getProperty("java.io.tmpdir")), "robolectric");

            if (cacheDir.exists() || cacheDir.mkdir()) {
              Logger.info("Dependency cache location: %s", cacheDir.getAbsolutePath());
              dependencyResolver = new CachedDependencyResolver(new MavenDependencyResolver(), cacheDir, 60 * 60 * 24 * 1000);
            } else {
              dependencyResolver = new MavenDependencyResolver();
            }
        }

        URL buildPathPropertiesUrl = getClass().getClassLoader().getResource("robolectric-deps.properties");
        if (buildPathPropertiesUrl != null) {
            Logger.info("Using Robolectric classes from %s", buildPathPropertiesUrl.getPath());

            FsFile propertiesFile = Fs.fileFromPath(buildPathPropertiesUrl.getFile());
            try {
              dependencyResolver = new PropertiesDependencyResolver(propertiesFile, dependencyResolver);
            } catch (IOException e) {
                throw new RuntimeException("couldn't read " + buildPathPropertiesUrl, e);
            }
        }
    }

    return dependencyResolver;
  }
}

我们找到DependencyResolver dependencyResolver成员和跟dependencyResolver密切相关的getJarResolver()方法。

debug 一下 test case,并在getJarResolver()里面打 Breakpoints:

你发现调用了:

dependencyResolver = new CachedDependencyResolver(new MavenDependencyResolver(), cacheDir, 60 * 60 * 24 * 1000);

运行完getJarResolver(),在Android Studio Debug工具查看RobolectricTestRunner的变量:

关键的东西在这里,CachedDependencyResolver dependencyResolver里面还有一个变量MavenDependencyResolver dependencyResolver,这个MavenDependencyResolver有变量及其值:

repositoryUrl = https://oss.sonatype.org/content/groups/public
repositoryId = sonatype

这个就是robolectric为什么从https://oss.sonatype.org下载依赖库的原因,只要把repositoryUrl替换其他url,就可以改变maven仓库网址了。

CachedDependencyResolver、MavenDependencyResolver

CachedDependencyResolver:
```
public class CachedDependencyResolver implements DependencyResolver {

private final DependencyResolver dependencyResolver;// MavenDependencyResolver

@Override
public URL[] getLocalArtifactUrls(DependencyJar... dependencies) {
...
final URL[] urls = dependencyResolver.getLocalArtifactUrls(dependencies);
...
return urls;
}

top Created with Sketch.