加速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

综合判断:

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美国

笔者甚至用国外的vp*服务器(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;
  }

  @Override
  public URL getLocalArtifactUrl(DependencyJar dependency) {
    ...
    final URL url = dependencyResolver.getLocalArtifactUrl(dependency);
    ...
    return url;
  }
}

MavenDependencyResolver(重点):

public class MavenDependencyResolver implements DependencyResolver {

  private final String repositoryUrl;
  private final String repositoryId;

  // 默认从RoboSetting获取repositoryUrl和repositoryId,RoboSettings相当于Hook
  public MavenDependencyResolver() {
    this(RoboSettings.getMavenRepositoryUrl(), RoboSettings.getMavenRepositoryId());
  }

  public MavenDependencyResolver(String repositoryUrl, String repositoryId) {
    this.repositoryUrl = repositoryUrl;
    this.repositoryId = repositoryId;
  }

  @Override
  public URL[] getLocalArtifactUrls(DependencyJar... dependencies) {
    DependenciesTask dependenciesTask = createDependenciesTask();// AbstractArtifactTask子类
    ...
    RemoteRepository remoteRepository = new RemoteRepository();
    remoteRepository.setUrl(repositoryUrl);// 默认https://oss.sonatype.org/content/groups/public/
    remoteRepository.setId(repositoryId);// 默认sonatype
    dependenciesTask.addConfiguredRemoteRepository(remoteRepository);
    ...
    
    dependenciesTask.execute(); // 调用AbstractArtifactTask.execute()
    ...
  }
}

RoboSettings :

public class RoboSettings {

  private static String mavenRepositoryId;
  private static String mavenRepositoryUrl;

  static {
    mavenRepositoryId = System.getProperty("robolectric.dependency.repo.id", "sonatype");
    mavenRepositoryUrl = System.getProperty("robolectric.dependency.repo.url", "https://oss.sonatype.org/content/groups/public/");// 看到默认以https://oss.sonatype.org/content/groups/public/为resitoryUrl
  }

  public static String getMavenRepositoryId() {
    return mavenRepositoryId;
  }

  public static void setMavenRepositoryId(String mavenRepositoryId) {
    RoboSettings.mavenRepositoryId = mavenRepositoryId;
  }

  public static String getMavenRepositoryUrl() {
    return mavenRepositoryUrl;
  }

  public static void setMavenRepositoryUrl(String mavenRepositoryUrl) {
    RoboSettings.mavenRepositoryUrl = mavenRepositoryUrl;
  }
}

AbstractArtifactTask:

public abstract class AbstractArtifactTask extends Task{

    public void execute()
    {
        ...
        initSettings();
        doExecute(); // 下载或从本地读取依赖库
       ...
    }
    
    private File newFile( String parent, String subdir, String filename )
    {
        return new File( new File( parent, subdir ), filename );
    }
    
    private void initSettings()
    {
        if ( userSettingsFile == null )
        {
            File tempSettingsFile = newFile( System.getProperty( "user.home" ), ".ant", "settings.xml" );
            if ( tempSettingsFile.exists() )
            {
                userSettingsFile = tempSettingsFile;
            }
            else
            {
                tempSettingsFile = newFile( System.getProperty( "user.home" ), ".m2", "settings.xml" );
                if ( tempSettingsFile.exists() )
                {
                    userSettingsFile = tempSettingsFile;
                }
            }
        }
        if ( globalSettingsFile == null )
        {
            File tempSettingsFile = newFile( System.getProperty( "ant.home" ), "etc", "settings.xml" );
            if ( tempSettingsFile.exists() )
            {
                globalSettingsFile = tempSettingsFile;
            }
            else
            {
                // look in ${M2_HOME}/conf
                List<String> env = Execute.getProcEnvironment();
                for ( String var: env )
                {
                    if ( var.startsWith( "M2_HOME=" ) )
                    {
                        String m2Home = var.substring( "M2_HOME=".length() );
                        tempSettingsFile = newFile( m2Home, "conf", "settings.xml" );
                        if ( tempSettingsFile.exists() )
                        {
                            globalSettingsFile = tempSettingsFile;
                        }
                        break;
                    }
                }
            }
        }

        Settings userSettings = loadSettings( userSettingsFile );// 读取并解析配置
        Settings globalSettings = loadSettings( globalSettingsFile );// 读取并解析配置

        SettingsUtils.merge( userSettings, globalSettings, TrackableBase.GLOBAL_LEVEL );
        settings = userSettings;

        if ( StringUtils.isEmpty( settings.getLocalRepository() ) )
        {
            String location = newFile( System.getProperty( "user.home" ), ".m2", "repository" ).getAbsolutePath();// 默认maven目录
            settings.setLocalRepository( location );// 设置默认maven目录
        }
        ...
    }
}

initSetting()主要任务,就是找到默认或setting.xml配置的maven目录,代码大致意思是:

1.加载 $user.home/.ant/setting.xml$user.home/.m2/setting.xml$M2_HOME/conf/setting.xml ,读取并解析配置文件,获取配置的maven目录;
2.如果没找到setting.xml,则默认$user.home/.m2/repository/为maven本地目录。

$user.home变量对应windows默认是C:\Users\{用户名}\,mac默认\Users\{用户名}\。这就知道默认.m2目录是C:\Users\{用户名}\.m2\repository\\Users\{用户名}\.m2\repository\了。

DependenciesTask.doExecute()处理从maven服务器下载依赖库到本地,读取本地依赖库等逻辑,本文不详述了,有兴趣的读者自己看看源码。


加速终极大招

大招1——把依赖文件拷贝到maven目录

既然我们知道robolectric依赖$user.home\.m2\repository\,那直接把下载好的jar拷贝到该目录。例如4.1.2_r1-robolectric-0:

拷贝C:\Users\kkmike999\.gradle\caches\modules-2\files-2.1\org.robolectric\android-all\4.1.2_r1-robolectric-0\aecc8ce5119a25fcea1cdf8285469c9d1261a352\android-all-4.1.2_r1-robolectric-0.jar$user.home\.m2\repository\org\robolectric\android-all\4.1.2_r1-robolectric-0\

或者到http://mvnrepository.com/artifact/org.robolectric/android-all/4.1.2_r1-robolectric-0下载android-all-4.1.2_r1-robolectric-0.jar,再拷贝到该目录。

robolectric有好几个依赖,必须把所有依赖都拷全。笔者不推荐这种做法。

大招2——把oss.sonatype.org改成阿里云maven仓库(推荐)

(2017.3.5更新)

先把$user.home\.m2\repository\org\robolectric\里面未下载完的目录删掉。因为这里可能有pom配置文件,里面的配置还是指向oss.sonatype.org,所以必须删除。

MyRobolectricTestRunner:

public class MyRobolectricTestRunner extends RobolectricTestRunner {
    static {
        // 从源码知道MavenDependencyResolver默认以RoboSettings的repositoryUrl和repositoryId为默认值,因此只需要对RoboSetting进行赋值即可
        MavenRoboSettings.setMavenRepositoryUserName("");
        MavenRoboSettings.setMavenRepositoryPassword("");
        MavenRoboSettings.setMavenRepositoryId("alimaven");
        MavenRoboSettings.setMavenRepositoryUrl("http://maven.aliyun.com/nexus/content/groups/public/");
    }

    public MyRobolectricTestRunner(Class<?> testClass) throws InitializationError {
        super(testClass);
    }
}

test case:

@Config(manifest = "./src/main/AndroidManifest.xml")
@RunWith(MyRobolectricTestRunner.class)
public class RoboTest {

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

运行单元测试:

速度2M/s左右,有时更快。依赖库下载完,并完成单元测试,耗时17s:

(注意,这个速度测试,笔者仅删掉android-all-4.1.2_r1-robolectric-0.jar,实际robolectric还有好些依赖包,实际耗时要更长一些)

启发

可以在project/build.gradle添加阿里云maven仓库:

build.gradle

allprojects {
    repositories {
        maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' }
        jcenter()
    }
}

速度扛扛的!


小结

Robolectric确实是不错的android单元测试第三方库,尽管运行起来有点慢。它能做挺多事情,例如直接测试sqlite(《Android单元测试 - Sqlite、SharedPreference、Assets、文件操作 怎么测?》)。

笔者写本文时,曾反复琢磨,究竟要慢慢分析问题,以实验形式来引出解决方法,还是剖析源码中,寻找解决方法呢?最终平衡了两个需求,成了本文这个样子。

希望更多的同学,在第一次做robolectric单元测试时,阅读本文,避免浪费时间。


关于作者

我是键盘男。
在广州生活,在互联网公司上班,猥琐文艺码农。跑步、喜欢科学、历史,玩玩投资,偶尔旅行。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,637评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,846评论 25 707
  • 文/ZYRzyr原文链接:http://www.jianshu.com/p/06e6b5633054 前言 在An...
    ZYRzyr阅读 3,305评论 3 51
  • 一.榜单介绍 排行榜包括四大类: 单一框架:仅提供路由、网络层、UI层、通信层或其他单一功能的框架 混合开发框架:...
    伟子男阅读 5,240评论 0 161
  • 當城市被熾熱的漩渦包圍 裹著厚重衣物的你 只顧著拾那地上的殘骸 門內與門外是兩個世界 唯有頭頂那隔著薄布的陰涼 予...
    Jaydenwu阅读 261评论 0 5