RxJava+Retrofit2緩存庫:RxCache中文文檔

RxCache官方文檔翻譯

本文翻譯自:RxCache官方GitHub地址
版本號:RxCache 1.8.1-2.x

歡迎轉載,轉發請注明文章來源
http://write.blog.csdn.net/mdeditor#!postId=78056742 @卻把清梅嗅

中文文檔已發布GitHub,詳情請點擊

RxCache基本使用方法、Demo請參閱筆者的相關文章:

Android RxCache使用詳解

Android RxCache原理解析


1 概述

2 基本使用

2.1 依賴配置

2.2 接口配置

2.3 新建Provider實例并使用它

2.4 再次回顧整個流程

3 RxCache使用場景

4 RxCache API

4.1 EvictProvider:驅逐緩存數據

4.2 DynamicKey:篩選數據

4.3 DynamicKeyGroup:分頁和過濾

5 Actionable RxCache API

6 高級選項

6.1 數據遷移

6.2 數據加密

6.3 常規配置

6.3.1 配置要保留的數據的大小限制(以兆字節為單位)

6.3.2 如果未加載到數據,使用過期的緩存數據

6.4 Android注意事項

6.5 和Retrofit搭配使用

7 其他

7.1 RxCache原理

7.2 代碼混淆

7.3 關于作者

7.4 RxCache Swift版本

7.5 作者其它使用RxJava的庫

8 關于中文文檔

<h2 id="1">概述</h2>

本庫的 目標 很簡單: 就像Picasso 緩存您的圖片一樣,毫不費力緩存您的數據對象。

每個Android Application都是一個客戶端應用程序,這意味著僅僅為緩存數據創建數據庫并進行維護毫無意義。

事實上,傳統方式通過數據庫來緩存數據并沒有解決根本性的問題:以更加靈活簡單的方式配置緩存。

靈感來源于 Retrofit , RxCache是一個用于Android和Java的響應式緩存庫,它可將您的緩存需求轉換為一個接口進行配置。

當提供一個 observable, single, maybe or flowable (這些是RxJava2支持的響應式數據類型) 這些由耗時操作提供的數據,RxCache確定是否需要subscribe,或覆蓋先前緩存的數據。

此決定是基于RxCache的Providers進行配置的。

<h2 id="2">基本使用</h2>

<h3 id="2.1">依賴配置</h3>

在您的Project級的build.gradle中添加JitPack倉庫:

allprojects {
    repositories {
        jcenter()
        maven { url "https://jitpack.io" }
    }
}

將下列的依賴添加到Module的build.gradle中:

dependencies {
    compile "com.github.VictorAlbertos.RxCache:runtime:1.8.1-2.x"
    compile "io.reactivex.rxjava2:rxjava:2.0.6"
}

因為RxCache在內部使用 Jolyglot 對數據進行序列化和反序列化, 您需要選擇下列的依賴中選擇一個進行添加:

dependencies {
    // To use Gson 
    compile 'com.github.VictorAlbertos.Jolyglot:gson:0.0.3'
    
    // To use Jackson
    compile 'com.github.VictorAlbertos.Jolyglot:jackson:0.0.3'
    
    // To use Moshi
    compile 'com.github.VictorAlbertos.Jolyglot:moshi:0.0.3'
}

<h3 id="2.2">接口配置</h3>

聲明一個接口,常規使用方式中(以Retrofit網絡請求為例),創建和API需求同樣多的Providers來緩存您的數據。

這意味著,項目中Retrofit的APIService接口有多少個抽象方法的API需要實現緩存,一一對應,就需要Providers提供多少個緩存API方法

interface Providers {
        
        @ProviderKey("mocks")
        Observable<List<Mock>> getMocks(Observable<List<Mock>> oMocks);
    
        @ProviderKey("mocks-5-minute-ttl")
        @LifeCache(duration = 5, timeUnit = TimeUnit.MINUTES)   //緩存有效期5分鐘
        Observable<List<Mock>> getMocksWith5MinutesLifeTime(Observable<List<Mock>> oMocks);
    
        @ProviderKey("mocks-evict-provider")
        Observable<List<Mock>> getMocksEvictProvider(Observable<List<Mock>> oMocks, EvictProvider evictProvider);
    
        @ProviderKey("mocks-paginate")
        Observable<List<Mock>> getMocksPaginate(Observable<List<Mock>> oMocks, DynamicKey page);
    
        @ProviderKey("mocks-paginate-evict-per-page")
        Observable<List<Mock>> getMocksPaginateEvictingPerPage(Observable<List<Mock>> oMocks, DynamicKey page, EvictDynamicKey evictPage);
        
        @ProviderKey("mocks-paginate-evict-per-filter")
        Observable<List<Mock>> getMocksPaginateWithFiltersEvictingPerFilter(Observable<List<Mock>> oMocks, DynamicKeyGroup filterPage, EvictDynamicKey evictFilter);
}

RxCache暴露了evictAll()方法來清除一行中的整個緩存。

RxCache的Provider配置中,方法所需要的參數用來配置Provider處理緩存的方式:

  • 無論如何,必不可少的參數是RxJava提供的響應式基本數據類型(如Observable),這個參數的意義是將你想緩存的Retrofit接口作為參數傳入,并以相同的RxJava數據類型作為返回。

    這意味著,您可以不配置任何可選項,但是您必須將您要緩存的數據作為參數交給RxCache進行緩存.

  • EvictProvider 是否驅逐與該Provider相關聯的所有緩存數據.

    該對象通過構造方法進行實例化,創建時需要傳入boolean類型的參數,當參數為true時,RxCache會直接驅逐該Provider的緩存數據,進行最新的網絡請求并進行緩存;若參數為false,若緩存數據未過期,正常加載緩存數據

  • @ProviderKey 保護用戶數據的Provider方法的注解,強烈建議使用這個注解! 如果不使用該注解,該方法的名稱會被作為該Provider的key進行文件緩存, 使用了代碼混淆的用戶很快會遇到問題,詳情請參閱 Proguard . 如果不使用代碼混淆,該注解也很有用,因為它可以確保您可以隨心所欲修改Provider數據緩存的方法名,而無需為舊緩存文件遷移問題而苦惱。

    該注解是最近版本添加的,在考慮到代碼混淆(方法名的改變導致緩存文件命名的改變)和緩存數據遷移,強烈建議使用該注解!

  • EvictDynamicKey 是否驅逐具體的緩存數據 DynamicKey.

    緩存數據驅逐范圍比EvictProvider小(后者是驅逐所有緩存),比EvictDynamicKeyGroup大(后者是驅逐更精細分類的緩存),舉例,若將userId(唯一)作為參數傳入DynamicKey,清除緩存時,僅清除該userId下的對應緩存

  • EvictDynamicKeyGroup 是否驅逐更加具體的緩存數據 DynamicKeyGroup.

    和EvictDynamicKey對比,上述案例中,DynamicKeyGroup可以filter到某userId下緩存的某一頁數據進行驅逐,其他緩存不驅逐

  • DynamicKey 通過傳入一個對象參數(比如userId)實現和對應緩存數據的綁定, 清除該key相關聯的緩存數據請使用 EvictDynamicKey.
  • DynamicKeyGroup 通過傳入一個Group參數(比如userId,數據的分類)實現和對應緩存數據的綁定, 清除該keyGroup相關聯的緩存數據請使用EvictDynamicKeyGroup.

Supported annotations:

<h3 id="2.3">新建Provider實例并使用它</h3>

最后,使用RxCache.Builder實例化Provider接口,并提供一個有效的文件系統路徑,這將允許RxCache在磁盤上寫入緩存數據。

//獲取緩存的文件存放路徑
File cacheDir = getFilesDir();
Providers providers = new RxCache.Builder()
                            .persistence(cacheDir, new GsonSpeaker())//配置緩存的文件存放路徑,以及數據的序列化和反序列化
                            .using(Providers.class);    //和Retrofit相似,傳入緩存API的接口

<h3 id="2.4">再次回顧整個流程</h3>

interface Providers {
    //配置要緩存的數據,以及是否驅逐緩存數據并請求網絡
    @ProviderKey("mocks-evict-provider")
    Observable<List<Mock>> getMocksEvictProvider(Observable<List<Mock>> oMocks, EvictProvider evictProvider);
    
    //配置要緩存的數據,簡單的緩存數據分類,以及是否驅逐該分類下的緩存數據并請求網絡
    @ProviderKey("mocks-paginate-evict-per-page")
    Observable<List<Mock>> getMocksPaginateEvictingPerPage(Observable<List<Mock>> oMocks, DynamicKey page, EvictDynamicKey evictPage);
    
    //配置要緩存的數據,復雜的緩存數據分類,以及是否驅逐該詳細分類下的緩存數據并請求網絡
    @ProviderKey("mocks-paginate-evict-per-filter")
    Observable<List<Mock>> getMocksPaginateWithFiltersEvictingPerFilter(Observable<List<Mock>> oMocks, DynamicKeyGroup filterPage, EvictDynamicKey evictFilter);
}
public class Repository {
    private final Providers providers;
    
    //初始化RxCache的Provider
    public Repository(File cacheDir) {
        providers = new RxCache.Builder()
                .persistence(cacheDir, new GsonSpeaker())
                .using(Providers.class);
    }
    
    //參數update:是否加載最新數據
    public Observable<List<Mock>> getMocks(final boolean update) {
        return providers.getMocksEvictProvider(getExpensiveMocks(), new EvictProvider(update));
    }
    
    //參數page:第幾頁的數據,update:是否加載該頁的最新數據
    public Observable<List<Mock>> getMocksPaginate(final int page, final boolean update) {
        return providers.getMocksPaginateEvictingPerPage(getExpensiveMocks(), new DynamicKey(page), new EvictDynamicKey(update));
    }
    
    //參數filter:某個條件(比如userName),參數page:第幾頁數據,參數updateFilter:是否加載該userName該頁的最新數據
    public Observable<List<Mock>> getMocksWithFiltersPaginate(final String filter, final int page, final boolean updateFilter) {
        return providers.getMocksPaginateWithFiltersEvictingPerFilter(getExpensiveMocks(), new DynamicKeyGroup(filter, page), new EvictDynamicKey(updateFilter));
    }

    //這個方法的返回值代替了現實開發中,您通過耗時操作獲得的數據類型(比如Observable<T>)
    //如果這里您使用了Retrofit進行網絡請求,那么可以說是拿來即用。
    private Observable<List<Mock>> getExpensiveMocks() {
        return Observable.just(Arrays.asList(new Mock("")));
    }
}

<h2 id="3">RxCache使用場景</h2>

  • 使用經典的RxCache API進行文件的讀寫操作。
  • 使用Actionable的API,專用于文件的寫操作。

<h2 id="4">RxCache API</h2>

下面的用例說明了一些常見的情況,這將有助于您了解“DynamicKey”和“DynamicKeyGroup”類的使用以及清除數據。

<h3 id="4.1">EvictProvider:驅逐緩存數據</h3>

不驅逐數據

Observable<List<Mock>> getMocks(Observable<List<Mock>> oMocks);

驅逐數據

Observable<List<Mock>> getMocksEvictProvider(Observable<List<Mock>> oMocks, EvictProvider evictProvider);

業務代碼中使用:


//接收到Observable時驅逐該Provider所有緩存數據并重新請求
getMocksEvictProvider(oMocks, new EvictProvider(true))

//這行會拋出一個IllegalArgumentException:“提供了EvictDynamicKey但沒有提供任何DynamicKey”
getMocksEvictProvider(oMocks, new EvictDynamicKey(true))

<h3 id="4.2">DynamicKey:篩選數據</h3>

指定某個條件,不驅逐該條件下的緩存數據

Observable<List<Mock>> getMocksFiltered(Observable<List<Mock>> oMocks, DynamicKey filter);

指定某個條件,可選擇是否驅逐該條件下的緩存數據

Observable<List<Mock>> getMocksFilteredEvict(Observable<List<Mock>> oMocks, DynamicKey filter, EvictProvider evictDynamicKey);

業務代碼中使用:


//接收到Observable時驅逐該Provider所有緩存數據并重新請求
getMocksFilteredEvict(oMocks, new DynamicKey("actives"), new EvictProvider(true))

//通過使用EvictDynamicKey,接收到Observable時,驅逐該DynamicKey("actives")下所有緩存數據并重新請求
getMocksFilteredEvict(oMocks, new DynamicKey("actives"), new EvictDynamicKey(true))

//這行拋出一個IllegalArgumentException:“提供了EvictDynamicKeyGroup,但沒有提供任何DynamicKeyGroup”
getMocksFilteredEvict(oMocks, new DynamicKey("actives"), new EvictDynamicKeyGroup(true))

<h3 id="4.3">DynamicKeyGroup:分頁和過濾</h3>

List數據的分頁和過濾,不驅逐緩存數據

Observable<List<Mock>> getMocksFilteredPaginate(Observable<List<Mock>> oMocks, DynamicKey filterAndPage);

List數據的分頁和過濾,包含是否驅逐緩存數據選項

Observable<List<Mock>> getMocksFilteredPaginateEvict(Observable<List<Mock>> oMocks, DynamicKeyGroup filterAndPage, EvictProvider evictProvider);

運行時使用:


//接收到Observable時驅逐該Provider所有緩存數據并重新請求
getMocksFilteredPaginateEvict(oMocks, new DynamicKeyGroup("actives", "page1"), new EvictProvider(true))

//通過使用EvictDynamicKey,接收到Observable時,驅逐該DynamicKey("actives", "page1")下所有緩存數據并重新請求
getMocksFilteredPaginateEvict(oMocks, new DynamicKeyGroup("actives", "page1"), new EvictDynamicKey(true))

//通過使用EvictDynamicKey,接收到Observable時,驅逐該DynamicKeyGroup("actives", "page1")下所有緩存數據并重新請求
getMocksFilteredPaginateInvalidate(oMocks, new DynamicKeyGroup("actives", "page1"), new EvictDynamicKeyGroup(true))

正如你所看到的,使用“DynamicKey”或“DynamicKeyGroup”以及“EvictProvider”類的重點就是在根據不同范圍下,驅逐緩存數據對象。

上述示例代碼中展示了方法接收“EvictProvider”的參數,以及EvictProvider的子類DynamicKey、DynamicKeyGroup,保證更詳細的數據分類和篩選,并進行緩存。

上述代碼中,我已經做到了這一點,您總是可以通過自己的篩選,將數據的key類別縮小到你真正需要驅逐的類型。對于最后一個例子,我將在實際產品代碼中使用“EvictDynamicKey”,因為這樣我就可以對已過濾的項目進行分頁,并將其按過濾器排除,例如通過刷新來觸發。

這里還有完整的例子 Android and Java projects.

<h2 id="5">Actionable RxCache API</h2>

限制:目前actionable的API僅支持Observable的數據類型。

這個actionable的API提供了一種Application執行文件寫入操作的簡單方法。 盡管使用RxCache經典的api也可以實現寫入操作,但經典的api有著復雜性且容易出錯。實際上,Actions類是圍繞經典api的進行了一層包裝。

為了能夠使用該actionable API,首先,你需要添加 repository compiler 的依賴到您的build.gradle:

dependencies {
     // other classpath definitions here
     classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
 }

然后確保在您的app / build.gradle中應用該插件,并添加編譯器依賴關系:

apply plugin: 'com.neenbedankt.android-apt'

dependencies {
    // apt command comes from the android-apt plugin
    apt "com.github.VictorAlbertos.RxCache:compiler:1.8.0-1.x"
}

配置完成后,為每個Provider添加注解 @Actionable annotation

編譯器會生成一個新的類,該類與接口名稱相同,但是附加了一個“Actionable”后綴,并暴露出和該接口同樣多的方法

參數供應中的順序必須與以下示例保持一致:

public interface RxProviders {
    @Actionable
    Observable<List<Mock.InnerMock>> mocks(Observable<List<Mock.InnerMock>> message, EvictProvider evictProvider);

    @Actionable
    Observable<List<Mock>> mocksDynamicKey(Observable<List<Mock>> message, DynamicKey dynamicKey, EvictDynamicKey evictDynamicKey);

    @Actionable
    Observable<List<Mock>> mocksDynamicKeyGroup(Observable<List<Mock>> message, DynamicKeyGroup dynamicKeyGroup, EvictDynamicKeyGroup evictDynamicKey);
}

請注意,Observable內的值必須是“List”類型,否則將拋出異常。

這樣上面的RxProviders接口將會在生成的“RxProvidersActionable”類中暴露出下面的方法:

RxProvidersActionable.mocks(RxProviders proxy);
RxProvidersActionable.mocksDynamicKey(RxProviders proxy, DynamicKey dynamicKey);
RxProvidersActionable.mocksDynamicKeyGroup(RxProviders proxy, DynamicKeyGroup dynamicKeyGroup);

這些方法返回“Actions”類的一個實例,現在你已經可以嘗試使用每個可用的寫操作 Actions .建議您瀏覽ActionsTest類,以查看哪些操作適合更適合你的現實需求。

一些示例代碼:

ActionsProviders.mocks(rxProviders)
    .addFirst(new Mock())
    .addLast(new Mock())
    //Add a new mock at 5 position
    .add((position, count) -> position == 5, new Mock())

    .evictFirst()
    //Evict first element if the cache has already 300 records
    .evictFirst(count -> count > 300)
    .evictLast()
    //Evict last element if the cache has already 300 records
    .evictLast(count -> count > 300)
    //Evict all inactive elements
    .evictIterable((position, count, mock) -> mock.isInactive())
    .evictAll()

    //Update the mock with id 5
    .update(mock -> mock.getId() == 5, mock -> {
        mock.setActive();
        return mock;
    })
    //Update all inactive mocks
    .updateIterable(mock -> mock.isInactive(), mock -> {
        mock.setActive();
        return mock;
    })
    .toObservable()
    .subscribe(processedMocks -> {})

之前的每個Action只有在composed的observable接收到subscribe之后才會執行。

<h2 id="6">高級選項</h2>

<h3 id="6.1">數據遷移</h3>

RxCache提供了一種處理版本之間緩存數據遷移的簡單方式。

簡單來說,最新的版本中某個接口返回值類型內部發生了改變,從而獲取數據的方式發生了改變,但是存儲在本地的數據,是未改變的版本,這樣在反序列化時就可能發生錯誤,為了規避這個風險,作者就加入了數據遷移的功能

您需要為您的Provider接口添加注解 @SchemeMigration. 這個注解接受一個數組 @Migration ,反過來,Migration注釋同時接受一個版本號和一個Classes的數組,這些數組將從持久層中刪除。

@SchemeMigration({
            @Migration(version = 1, evictClasses = {Mock.class}),
            @Migration(version = 2, evictClasses = {Mock2.class}),
            @Migration(version = 3, evictClasses = {Mock3.class})
    })
interface Providers {}

只有當RxCache使用的Class類中數據結構發生了改變,才需要添加新的遷移注解。

比如說,您的緩存數據User中有 int userId這個屬性,新的版本中變成了 long userId,這樣緩存數據的反序列化就會出現問題,因此需要配置遷移注解

刪除類或刪除類的字段將由RxCache自動處理,因此當字段或整個類被刪除時,不需要遷移新的注解。

@SchemeMigration({
            @Migration(version = 1, evictClasses = {Mock.class}),
            @Migration(version = 2, evictClasses = {Mock2.class})
    })
interface Providers {}

但是現在,“Mock”類已經從項目中刪除了,所以不可能再引用它的類了。 要解決這個問題,只需刪除這行遷移的注解即可。

@SchemeMigration({
            @Migration(version = 2, evictClasses = {Mock2.class})
    })
interface Providers {}

因為RxCache需要內部進程才能清理內存,所以數據最終將被全部清除。

<h3 id="6.2">數據加密</h3>

RxCache提供了一種加密數據的簡單機制。

您需要為您的Provider接口添加注解@EncryptKey. 這個annotation接受一個字符串作為加密/解密數據所必需的key。 但是,您需要使用@Encrypt對Provider的緩存進行注解,以便緩存數據加密。 如果沒有設置@Encrypt,則不會進行加密。

重要提示:如果提供的“key”值 @EncryptKey 在編譯期間進行了修改,那么以前的緩存數據將無法被RxCache驅逐/獲取。

@EncryptKey("myStrongKey-1234")
interface Providers {
        @Encrypt
        Observable<List<Mock>> getMocksEncrypted(Observable<List<Mock>> oMocks);

        Observable<List<Mock>> getMocksNotEncrypted(Observable<List<Mock>> oMocks);
}

<h3 id="6.3">常規配置</h3>

RxCache允許在構建Provider實例時設置某些參數:

<h4 id="6.3.1">配置要保留的數據的大小限制(以兆字節為單位)</h4>

默認情況下,RxCache將限制設置為100M,但您可以在構建Provider實例時調用setMaxMBPersistenceCache方法來更改此值。

new RxCache.Builder()
            .setMaxMBPersistenceCache(maxMgPersistenceCache)
            .persistence(cacheDir)
            .using(Providers.class);

當達到此限制時,RxCache將無法繼續緩存數據。 這就是為何當緩存數據容量即將達到閾值時,RxCache有一個自動化的過程來驅逐任何記錄,即使沒有滿足失效時間的緩存數據也被驅逐。

唯一的例外是,當您的Provider的某方法用@Expirable 注解注釋,并將其值設置為false將會被保存,而不會被RxCache自動化驅逐。

interface Providers {
    //即使緩存數據達到閾值,也不會被RxCache自動驅逐
    @Expirable(false)
    Observable<List<Mock>> getMocksNotExpirable(Observable<List<Mock>> oMocks);
}

<h4 id="6.3.2">如果未加載到數據,使用過期的緩存數據</h4>

默認情況下,如果緩存的數據已過期并且observable loader返回的數據為空,RxCache將拋出RuntimeException異常。

您可以修改此行為,允許RxCache在這種情況下提供被驅逐的數據,使用方式很簡單,通過將useExpiredDataIfLoaderNotAvailable的值設置為true:

new RxCache.Builder()
            .useExpiredDataIfLoaderNotAvailable(true)   //RxCache提供被驅逐的數據
            .persistence(cacheDir)
            .using(Providers.class);

<h3 id="6.4">Android注意事項</h3>

要構建由RxCache提供的接口實例,您需要提供對文件系統的引用。 在Android上,您可以從Application類獲取文件引用調用getFilesDir()。

此外,建議您在應用程序的整個生命周期中使用此Android應用程序類來提供RxCache的唯一實例(全局單例)。

為了在子線程上執行Observable,并通過主UI線程上的onNext發出結果,您應該使用RxAndroid提供的內置方法。

即 observable.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe();

你可以查看Demo: Android example

<h3 id="6.5">和Retrofit搭配使用</h3>

RxCache和Retrofit完美搭配,兩者配合可以實現從始至終的自動管理緩存數據庫。
您可以檢查RxCache和Retrofit的一個示例

<h2 id="7">其他</h2>

<h3 id="7.1">RxCache原理</h3>

RxCache的數據來源取決于下面三個數據層中某一層:

*內存層 - >由Apache ReferenceMap提供支持。
*持久層 - > RxCache內部使用Jolyglot來對對象進行序列化和反序列化。
*加載器層(由客戶端庫提供的Observable請求,比如網絡請求)

*如果請求的數據在內存中,并且尚未過期,則從內存中獲取。
*否則請求的數據在持久層中,并且尚未過期,則從持久層獲取。
*否則從加載器層請求獲取數據。

<h3 id="7.2">代碼混淆</h3>

-dontwarn io.rx_cache.internal.**
-keepclassmembers enum io.rx_cache.Source { *; }

<h3 id="7.3">關于作者</h3>

Víctor Albertos

<h3 id="7.4">RxCache Swift版本:</h3>

RxCache: Reactive caching library for Swift.

<h3 id="7.5">作者其它使用RxJava的庫:</h3>

  • Mockery: Android and Java library for mocking and testing networking layers with built-in support for Retrofit.
  • RxActivityResult: A reactive-tiny-badass-vindictive library to break with the OnActivityResult implementation as it breaks the observables chain.
  • RxFcm: RxJava extension for Android Firebase Cloud Messaging (aka fcm).
  • RxSocialConnect: OAuth RxJava extension for Android.

<h2 id="8">關于中文文檔</h2>

翻譯

參考

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 推薦:看到如此多的 MVP+Dagger2+Retrofit+Rxjava 項目, 輕松拿 star, 心動了嗎?...
    JessYan閱讀 46,379評論 68 183
  • Spring Boot 參考指南 介紹 轉載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 46,958評論 6 342
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,948評論 18 139
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,353評論 25 708
  • 評委辯友觀眾晚上好,我方觀點是:艾滋病是醫學問題,不是社會問題。 主要證據如下:第一點、艾滋病的定義是醫學定義,本...
    鯨翎閱讀 5,453評論 0 5