加速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閱讀 228,936評論 6 535
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,744評論 3 421
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,879評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,181評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,935評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,325評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,384評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,534評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,084評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,892評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,067評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,623評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,322評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,735評論 0 27
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,990評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,800評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,084評論 2 375

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,799評論 18 139
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,677評論 25 708
  • 文/ZYRzyr原文鏈接:http://www.lxweimin.com/p/06e6b5633054 前言 在An...
    ZYRzyr閱讀 3,344評論 3 51
  • 一.榜單介紹 排行榜包括四大類: 單一框架:僅提供路由、網絡層、UI層、通信層或其他單一功能的框架 混合開發框架:...
    偉子男閱讀 5,254評論 0 161
  • 當城市被熾熱的漩渦包圍 裹著厚重衣物的你 只顧著拾那地上的殘骸 門內與門外是兩個世界 唯有頭頂那隔著薄布的陰涼 予...
    Jaydenwu閱讀 266評論 0 5