組件化構想以及ARouter的使用分析

組件化

模塊化、組件化與插件化

在項目發展到一定程度,隨著人員的增多,代碼越來越臃腫,這時候就必須進行模塊化的拆分。在我看來,模塊化是一種指導理念,其核心思想就是分而治之、降低耦合。而在Android工程中如何實施,目前有兩種途徑,也是兩大流派,一個是組件化,一個是插件化。

既然組件化和插件化都是為了模塊化而生的,那么他們有什么區別,我覺得最大的區別應該就是動態修改的能力,這里的動態修改指的是運行期的動態修改,插件化顯然是可以支持的,但是組件化卻不行,它只允許編譯期的動態修改。

所以為什么要做的是組件化而不是插件化,作為RD我覺得理由大概如下吧

插件化有很多坑要躺——插件化框架本身的不穩定讓開發者前赴后繼的躺坑

發不完的版本——插件化可以運行時修改,PM表示非常完美,RD變身成真業務搬磚工

組件化沒有黑科技,穩定——原生能力支持這種靈活配置的方式

組件化工作

代碼解耦

一個比較理想的解耦狀態應當是使用AndroidStudio提供的multiple module能力將主項目中的已有模塊進行拆分,這里的module我們分為兩種

一種是基礎庫library,這些代碼可以直接被其他模塊直接引用,比如網絡庫,我們稱之為library。另一種是一個完整的功能模塊,比如會員中心,我們稱之為Component。拆分后的結果應該是類似于如下樣式

模塊化

那么解耦到什么樣的效果,才是我們需要的呢,顯然主模塊以及各個Component之間不允許有直接的引用,我們解耦的主要目標就是要做到完全隔離的效果,不能直接使用其他Component內的類并且最好不了解其中的實現細節。

組件的單獨調試

其實單獨調試比較簡單,只需要把apply plugin: ‘com.android.library’切換成apply plugin: ‘com.android.application’就可以,但是我們還需要修改一下AndroidManifest文件,因為一個單獨調試需要有一個入口的Actiivity。具體如下

在gradle.properities配置中放入如下參數

##### 是否單獨調試A模塊 #####
DEBUG_MODULE_A=false
##### 主模塊是否需要引入A模塊 #####
NEED_MODULE_A=true

在業務module的build.gradle中添加如下代碼

if (DEBUG_MODULE_A.toBoolean()) {
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
}

android {
    // ...
    
    sourceSets {
        main {
            if (DEBUG_MODULE_A.toBoolean()) {
                manifest.srcFile 'src/main/debug/AndroidManifest.xml'
            } else {
                manifest.srcFile 'src/main/AndroidManifest.xml'
            }
        }
    }

}

在主module的build.gradle中添加如下代碼

// a模塊非debug且需要打包a模塊能力時,才包含a模塊
if (!DEBUG_MODULE_A.toBoolean() && NEED_MODULE_A.toBoolean()) {
    implementation project(':module-a')
}

在業務module的src文件夾下添加對應的debug時需要使用的AndroidMainifest文件

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.baidu.input.module_a" >

    <application>
        <activity android:name="com.baidu.input.module_a.ModuleAMainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>

                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
        <activity android:name="com.baidu.input.module_a.ModuleATestActivity"></activity>
    </application>

</manifest>

在業務module中添加入口Activity類

// 虛擬Activity,用于測試業務內功能
public class ModuleAMainActivity extends Activity {

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

    public void testActivity(View v) {
        // ...
    }

    public void testService(View v) {
        // ...
    }
}

通過上面的步驟,其實我們已經給組件搭了一個測試的環境,從而讓組件的代碼能夠在單獨的環境里運行,結構如下圖所示

組件化

目前這種做法的缺點在于需要手動配置,同時對于manifest文件維護兩份的成本也比較大,后期希望能夠通過插件的方式自動進行配置,對于manifest則采用Android Studio支持的多flavor的manifest自動合并來去做。

組件的通信

上面說到解耦的時候提到了,主項目與各組件之間不允許直接進行引用,那么要實現跨模塊的功能,就必然涉及到了通信的過程,這個過程應該如何進行呢。

比較通用的方式是使用路由來進行這部分的工作。具體后面會以ARouter的使用為例來進行說明。

集成調試

在組件的單獨調試環節我們增加了下面的配置

##### 是否單獨調試A模塊 #####
DEBUG_MODULE_A=false
##### 主模塊是否需要引入A模塊 #####
NEED_MODULE_A=true

其中NEED_MODULE_A的配置就是用于后期集成調試準備的,當A模塊的功能完成時,該配置應當被值為true,便于主模塊對A模塊進行依賴并將A模塊的功能打包到整體APK中。

實際上,比較合適的做法是,在整個開發階段(debug),主模塊中都不應當包含類似于下面的配置

implementation project(':module-a')

這種依賴方式帶來的缺點是主模塊的開發人員會有意無意的直接引用到A模塊中的類,這對于解耦工作來說是一個退化過程,因此可能也需要一個整體的開關用于控制開發階段的依賴問題,比如

if (!DEBUG_MODULE_A.toBoolean() && NEED_MODULE_A.toBoolean() && !DEBUG.toBoolean()) {
    implementation project(':module-a')
}

但是正如單獨調試中提到的,這種手動修改的方式畢竟非常的不友好,而且對于我們目前的項目而言不易于操作,因此考慮使用自定義插件的方式來進行,目前對Gradle插件還不是很熟悉,所以沒有具體去嘗試,大致的想法應該是希望能夠判斷當前build的類型并且根據配置文件的參數來決定是否
implementation對應的模塊。

組件化規劃

實際上組件化是一個比較長期而且耗時的過程,特別是將一個大工程進行組件化,要考慮的內容可以說是非常多的,具體體現在下面幾點

路由庫選擇

正如前面所說的,目前組件化的工作需要進行組件間的通信,因此必須要有一個負責這部分工作的路由模塊,這個模塊應該如何選擇,是自行實現還是選擇第三方等等。

調試環境

目前調試環境的切分方式還不夠自動化,可能需要開發額外的插件

組件化拆分

對于組件化工作而言,大部分時間可能都是消耗在拆分工作上,現在讓我去想這個過程我都可以感覺到是很麻煩,但是細細考慮這部分的工作,還是有法可循的

  • 從產品需求到開發階段到運營階段都有清晰邊界的功能開始拆分
  • 拆分過程中依賴項目的模塊繼續進行拆分,比如埋點、網絡
  • 最終將主模塊變成空殼,僅僅包含一些簡單的拼接邏輯

路由

這里的路由就是根據路由表將請求發送到制定的位置,可以是一個頁面也可以是一個服務抑或是其他形式的內容。目前Android平臺的路由庫還是比較豐富的,那么為什么要有這么一個路由組件主要有下面幾點原因

開發與協作

根據我們對路由的定義,Android原生的路由方案一般是通過顯式intent和隱式intent兩種方式實現的,而在顯式intent的情況下,因為會存在直接的類依賴的問題,導致耦合非常嚴重;而在隱式intent情況下,則會出現規則集中式管理,導致協作變得非常困難。

組件化

組件化是開發和協作中作為開發者所需要面對的問題,而一旦一款APP達到一定體量的時候,業務就會膨脹得比較嚴重,而開發團隊的規模也會越來越大,這時候一般都會提出組件化的概念。組件化就是將APP按照一定的功能和業務拆分成多個小組件,不同的組件由不同的開發小組來負責,這樣就可以解決大型APP開發過程中的開發與協作的問題,將這些問題分散到小的APP中。目前而言組件化已經有非常多比較成熟的方案了,而自定義路由框架也可以非常好地解決整個APP完成組件化之后模塊之間沒有耦合的問題,因為沒有耦合時使用原生的路由方案肯定是不可以的。

Native和H5問題

Native與H5的問題主要是由于現在的APP很少是純Native或者純H5的,一般是將兩者進行結合,那么他們之間需要一個統一負責處理頁面跳轉的管理模塊,使用路由模塊實現的中間跳轉頁就非常適合處理這種問題。
根據之前組件化的工作中的描述,路由是必須使用的一項工具,這里我使用ARouter庫來介紹。

ARouter介紹

ARouter是阿里開源的一個Android平臺中對頁面及服務提供路由功能的中間件,

他有如下特點

  1. 支持直接解析標準URL進行跳轉,并自動注入參數到目標頁面中
  2. 支持多模塊工程使用
  3. 支持添加多個攔截器,自定義攔截順序
  4. 支持依賴注入,可單獨作為依賴注入框架使用
  5. 支持InstantRun
  6. 支持MultiDex(Google方案)
  7. 映射關系按組分類、多級管理,按需初始化
  8. 支持用戶指定全局降級與局部降級策略
  9. 頁面、攔截器、服務等組件均自動注冊到框架
  10. 支持多種方式配置轉場動畫
  11. 支持獲取Fragment
  12. 完全支持Kotlin以及混編

簡單的說ARouter的原理就是在編譯階段根據注解解釋器對路由注解攔截器注解以及自動裝配注解注解進行解釋并生成輔助代碼,待運行期與API接口一起提供給宿主APP使用,其中

路由注解——@Route

路由注解生成的路由表,是核心路由功能,之所以使用注解來實現主要考慮的是大型項目中的界面數量非常多,如果進行手動注冊映射關系會非常麻煩,需要寫很多重復冗余的代碼,并且需要調用很多接口,因此ARouter使用了注解的方式進行幫我們自動注冊。

攔截器注解——@Interceptor

攔截器注解用于對路由過程進行攔截,主要考慮的是原生路由能力無法在頁面跳轉的過程中添加自定義邏輯,而這一能力有時候有非常有必要可以避免許多重復邏輯的實現。ARouter中的攔截器也是通過注解的方式自動注冊的。

自動裝配——@Autowired

編譯期對Autowired注解的字段進行掃描并注冊到映射文件中,如果需要路由的目標界面調用了ARouter.inject(this),那么待運行時ARouter會查找到編譯期為調用方生成的輔助類進行參數注入。

ARouter使用

以一個實際的例子來描述ARouter的使用,項目希望的簡要結構如下
image.png

配置

路由跳轉各個模塊都需要使用,因此在ModuleRouter模塊中引入ARouter所需要的庫,上面一個是api接口,下面一個是注解解釋器

dependencies {
    // 替換成最新版本, 需要注意的是api
    // 要與compiler匹配使用,均使用最新版可以保證兼容
    // 最新版本參考github的鏈接
    api "com.alibaba:arouter-api:${AROUTER_API}"
    annotationProcessor "com.alibaba:arouter-compiler:${AROUTER_COMPILER}"
    ...
}

由于ModuleRouter模塊可能會使用到一些ARouter的注解,因此還需要添加下面的配置代碼

android {
    defaultConfig {
        ...
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [ moduleName : project.getName() ]
            }
        }
    }
}

ModuleA、ModuleB以及APP模塊都需要依賴ModuleRouter,而且需要使用到注解,因此配置如下

android {
    defaultConfig {
        ...
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [ moduleName : project.getName() ]
            }
        }
    }
}

// ...
dependencies {
    annotationProcessor "com.alibaba:arouter-compiler:${AROUTER_COMPILER}"
    implementation project(':modulerouter')
    // ...
}

初始化

ARouter初始化工作推薦盡早進行,因此放在Application的onCreate中

if (isDebug()) {           // 這兩行必須寫在init之前,否則這些配置在init過程中將無效
    ARouter.openLog();     // 打印日志
    ARouter.openDebug();   // 開啟調試模式(如果在InstantRun模式下運行,必須開啟調試模式!線上版本需要關閉,否則有安全風險)
}
ARouter.init(mApplication); // 盡可能早,推薦在Application中初始化

注解

ARouter的路由功能所需要的路由表是在編譯階段根據注解生成輔助類中包含的,這里路由主要包含了路由界面和路由服務兩部分。注解解釋器已經在配置階段在相應模塊中通過配置添加了,但是想使用路由能力,就需要在特定地方加上注解。除此之外還有攔截器注解和自動裝配注解

路由界面

// 在支持路由的頁面上添加注解(必選)
// 這里的路徑需要注意的是至少需要有兩級,/xx/xx
@Route(path = "/modulea/test")
public class ModuleATestActivity extends Activity {
    ...
}

這里對@Route這個注解做個簡單的解釋,path這個字段里最前面的兩個『/』中間的部分是路由表中『組』的標識,后面的內容是具體表示。『組』這個概念用于ARouter的分組加載的管理,避免一次性加載所有節點導致路由表瞬間增大。可以使用group字段進行自定義分組,其余字段部分可以參考源碼中的注釋。

@Route(path = "/com/test" , group = "wangchen")

一旦主動指定分組之后,應用內路由需要使用 ARouter.getInstance().build(path, group) 進行跳轉,手動指定分組,否則無法找到

路由服務

對于需要路由的服務,需要實現IProvider接口

@Route(path = "/modulea/service")
public class ModuleAServiceImpl implements IProvider {
    ...
}

特殊服務

這里提兩個特殊服務

對象解析服務

ARouter中如果要傳遞自定義對象,則需要使用該服務,實現SerializationService,并且使用@Route注解

@Route(path = "/service/json")
public class JsonServiceImpl implements SerializationService {
    @Override
    public void init(Context context) {

    }

    @Override
    public <T> T json2Object(String text, Class<T> clazz) {
        return JSON.parseObject(text, clazz);
    }

    @Override
    public String object2Json(Object instance) {
        return JSON.toJSONString(instance);
    }
}

降級服務

降級服務表示對路由失敗的情況的處理,是全局生效的

// 實現DegradeService接口,并加上一個Path內容任意的注解即可
@Route(path = "/xxx/xxx")
public class DegradeServiceImpl implements DegradeService {
  @Override
  public void onLost(Context context, Postcard postcard) {
    // do something.
  }

  @Override
  public void init(Context context) {

  }
}

攔截器

攔截器全局生效,需要實現IInterceptor接口,priority表示攔截器優先級,優先級高的攔截器優先執行

@Interceptor(priority = 7)
public class Test1Interceptor implements IInterceptor {
    ...
}

自動裝配

自動裝配需要在對應成員變量處加上@Autowired注解

// 為每一個參數聲明一個字段,并使用 @Autowired 標注
// URL中不能傳遞Parcelable類型數據,通過ARouter api可以傳遞Parcelable對象
@Route(path = "/test/activity")
public class Test1Activity extends Activity {
    @Autowired
    public String name;
    @Autowired
    int age;
    @Autowired(name = "girl") // 通過name來映射URL中的不同參數
    boolean boy;
    @Autowired
    TestObj obj;    // 支持解析自定義對象,路由表中需要存在SerializationService服務

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ARouter.getInstance().inject(this);

    // ARouter會自動對字段進行賦值,無需主動獲取
    Log.d("param", name + age + boy);
    }
}

注意要盡可能早的調用

ARouter.getInstance().inject()

如果不需要自動裝配,那么可以不調用ARouter.getInstance().inject(),但是如果希望可以通過URL跳轉的方式進入該界面,則依然需要保留@Autowired注解,否則ARouter不知道應該如何從URL中提取參數類型放入Intent內。

發起路由

路由界面和服務略有不同

路由界面

針對上面定義的ModuleATestActivity,我們需要路由該界面時只需要下面的代碼即可

ARouter.getInstance().build("/modulea/test").navigation();

對于手動指定的分組,則需要這樣

ARouter.getInstance().build("/modulea/test", "module").navigation();

在實際項目開發中,為了協調路路徑的問題,考慮將這部分的靜態代碼下沉至路由模塊,因此在路由模塊中添加如下代碼

package com.baidu.input.imerouter;

/**
 * Created by wangchen on 02/03/18.
 */
public class RouterPath implements IModuleAPath, IModuleBPath {
}

interface IModuleAPath {

    String MODULE_A_TEST = "/modulea/test";

    String MODULE_A_SERVICE = "/modulea/service";

}

interface IModuleBPath {

    String MODULE_B_TEST = "/moduleb/test";
}

因此注解代碼可以修改成

@Route(path = RouterPath.MODULE_A_TEST)
public class ModuleATestActivity extends Activity {
}

@Route(path = RouterPath.MODULE_A_SERVICE)
public class ModuleAServiceImpl implements ModuleAService {
}

發起路由代碼可以修改成

ARouter.getInstance().build(RouterPath.MODULE_A_TEST).navigation();

在ARouter官方給的最佳實踐中描述了這一點,對于所有界面跳轉都建議使用ARouter的方式來進行統一管理,但是對于模塊內的跳轉似乎這么寫又有些難受,因此給出如下的代碼建議

@Route(path = RouterPath.MODULE_A_TEST)
public class ModuleATestActivity extends Activity {

    public static void launch(Activity c) {
        ARouter.getInstance().build(RouterPath.MODULE_A_TEST).navigation(c);
    }
}

這樣對于所有可以直接引用ModuleATestActivity這個類的地方(本模塊)就可以以一個非ARouter的方式進行界面跳轉。

路由服務

首先要說的是,相對于上面的路由服務注冊的代碼,實際上ARouter建議以如下的方式進行路由服務的聲明

定義服務接口

public interface ModuleAService extends IProvider {

    String callModuleAService(String msg);
}

實現服務接口

@Route(path = RouterPath.MODULE_A_SERVICE)
public class ModuleAServiceImpl implements ModuleAService {

    @Override
    public String callModuleAService(String msg) {
        Log.i("ModuleA", msg);
        return "ModuleA receive " + msg;
    }

    @Override
    public void init(Context context) {

    }
}

這樣做的好處是分離了接口和實現,對于后面的模塊解耦有比較大的幫助。

回過頭來看路由服務的使用,ARouter中提供了兩種路由服務的方式——byType和byName,這跟它的路由表實現有關,兩種方式都可以在路由表中找到對應的服務。

byType

ARouter.getInstance().navigation(ModuleAService.class).callModuleAService("msg");

byName

((ModuleAService) ARouter.getInstance().build(RouterPath.MODULE_A_SERVICE).navigation()).callModuleAService("msg");

這兩種方式在ModuleAService接口只有一個實現的時候沒有問題,但是如果出現多實現時會有問題,由于路由表加載是一個map,因此此時實際使用的服務接口實現是自動生成的路由表加載代碼中順序靠后的一個,這種情況建議使用byName的方式來規避沖突。

再來看看為什么要進行接口和實現分離,很多時候我們需要跨模塊進行服務調用,如果不進行分離直接使用實現類,那么根據上面兩種方式,在發起路由的模塊會產生一個對服務提供模塊的直接依賴,這回對模塊解耦產生影響。

但是換成接口和實現分離的形式來做的話,依然會有一個接口類的依賴,為了避免這種直接依賴問題,我們需要將接口類下沉到基礎模塊中,這里就是ModuleRouter,注意下面的package

接口位于路由模塊

package com.baidu.input.imerouter;

import com.alibaba.android.arouter.facade.template.IProvider;

/**
 * Created by wangchen on 02/03/18.
 */
public interface ModuleAService extends IProvider {

    String callModuleAService(String msg);
}

實現位于業務模塊

package com.baidu.input.module_a;

import android.content.Context;
import android.util.Log;

import com.alibaba.android.arouter.facade.annotation.Route;
import com.baidu.input.imerouter.ModuleAService;
import com.baidu.input.imerouter.RouterPath;

/**
 * Created by wangchen on 02/03/18.
 */
@Route(path = RouterPath.MODULE_A_SERVICE)
public class ModuleAServiceImpl implements ModuleAService {

    @Override
    public String callModuleAService(String msg) {
        Log.i("ModuleA", msg);
        return "ModuleA receive " + msg;
    }

    @Override
    public void init(Context context) {

    }
}

值得注意的是,希望一個模塊最多對外提供一個服務接口,以確保路由模塊的接口數量不會膨脹,同時該服務接口必須滿足開閉原則

特殊服務

因為之前討論的byType和byName問題,ARouter的實現對于特殊服務都是使用byType的形式來處理的,因此如果出現多服務實現可能會出現問題。此時建議全局僅使用一個自定義對象加載服務全局降級服務,這兩個服務可以放在路由模塊,便于統一處理。

其他

對于攔截器和自動裝配以及其他路由發起方式的使用,可以參考官方demo,這里不做更多介紹了。

ARouter分析

ARouter注解

首先我們知道ARouter的自動注冊的實現是利用了編譯期自定義注解的處理來完成的。ARouter定義的注解的部分源碼位于arouter-annotation,具體的實現這里不做源碼分析了,只需要知道編譯期ARouter會通過注解解釋器生成幫助類,比如這樣

APT

Routes

我們看下routes這個包下的內容,這個包下的類都是用于生成路由表的

先看Root的內容,可以看出這里做的事情是將路由分組放入map中,這里涉及到兩個分組——service和test

/**
 * DO NOT EDIT THIS FILE!!! IT WAS GENERATED BY AROUTER. */
public class ARouter$$Root$$app implements IRouteRoot {
  @Override
  public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
    routes.put("service", ARouter$$Group$$service.class);
    routes.put("test", ARouter$$Group$$test.class);
  }
}

以其中一個分組test為例看代碼,這里做的事情是將這個test分組內的所有具體路由項添加到一個map中,路由項的具體信息會包裝成RouteMeta類的對象

/**
 * DO NOT EDIT THIS FILE!!! IT WAS GENERATED BY AROUTER. */
public class ARouter$$Group$$test implements IRouteGroup {
  @Override
  public void loadInto(Map<String, RouteMeta> atlas) {
    atlas.put("/test/activity1", RouteMeta.build(RouteType.ACTIVITY, Test1Activity.class, "/test/activity1", "test", new java.util.HashMap<String, Integer>(){{put("pac", 9); put("ch", 5); put("fl", 6); put("obj", 10); put("name", 8); put("dou", 7); put("boy", 0); put("objList", 10); put("map", 10); put("age", 3); put("url", 8); put("height", 3); }}, -1, -2147483648));
    atlas.put("/test/activity2", RouteMeta.build(RouteType.ACTIVITY, Test2Activity.class, "/test/activity2", "test", new java.util.HashMap<String, Integer>(){{put("key1", 8); }}, -1, -2147483648));
    atlas.put("/test/activity3", RouteMeta.build(RouteType.ACTIVITY, Test3Activity.class, "/test/activity3", "test", new java.util.HashMap<String, Integer>(){{put("name", 8); put("boy", 0); put("age", 3); }}, -1, -2147483648));
    atlas.put("/test/activity4", RouteMeta.build(RouteType.ACTIVITY, Test4Activity.class, "/test/activity4", "test", null, -1, -2147483648));
    atlas.put("/test/fragment", RouteMeta.build(RouteType.FRAGMENT, BlankFragment.class, "/test/fragment", "test", null, -1, -2147483648));
    atlas.put("/test/webview", RouteMeta.build(RouteType.ACTIVITY, TestWebview.class, "/test/webview", "test", null, -1, -2147483648));
  }
}

除此之外Interceptor中是攔截器路由項加載的類,而Provider中是服務路由項加載的類

/**
 * DO NOT EDIT THIS FILE!!! IT WAS GENERATED BY AROUTER. */
public class ARouter$$Interceptors$$app implements IInterceptorGroup {
  @Override
  public void loadInto(Map<Integer, Class<? extends IInterceptor>> interceptors) {
    interceptors.put(7, Test1Interceptor.class);
  }
}
/**
 * DO NOT EDIT THIS FILE!!! IT WAS GENERATED BY AROUTER. */
public class ARouter$$Providers$$app implements IProviderGroup {
  @Override
  public void loadInto(Map<String, RouteMeta> providers) {
    providers.put("com.alibaba.android.arouter.demo.testservice.HelloService", RouteMeta.build(RouteType.PROVIDER, HelloServiceImpl.class, "/service/hello", "service", null, -1, -2147483648));
    providers.put("com.alibaba.android.arouter.facade.service.SerializationService", RouteMeta.build(RouteType.PROVIDER, JsonServiceImpl.class, "/service/json", "service", null, 10, -2147483648));
    providers.put("com.alibaba.android.arouter.demo.testservice.SingleService", RouteMeta.build(RouteType.PROVIDER, SingleService.class, "/service/single", "service", null, -1, -2147483648));
    providers.put("com.alibaba.android.arouter.facade.service.SerializationService", RouteMeta.build(RouteType.PROVIDER, TestService.class, "/service/test", "service", null, 50, -2147483648));
  }
}

從Provider這個自動生成類我們還可以發現一個問題,對于服務而言它可以通過Provider自動生成類的loadInto方法加載到路由表中,也可以通過具體所屬組所提供的loadInto方法加載到路由表中,這也是ARouter對服務能提供byTypebyName兩種路由方式的原因

Autowired

對于剩下的自動生成的類,都是以Autowired這個關鍵詞結尾的,表明他們是負責自動裝配的代碼,以其中一個為例

/**
 * DO NOT EDIT THIS FILE!!! IT WAS GENERATED BY AROUTER. */
public class Test1Activity$$ARouter$$Autowired implements ISyringe {
  private SerializationService serializationService;

  @Override
  public void inject(Object target) {
    serializationService = ARouter.getInstance().navigation(SerializationService.class);
    Test1Activity substitute = (Test1Activity)target;
    substitute.name = substitute.getIntent().getStringExtra("name");
    substitute.age = substitute.getIntent().getIntExtra("age", substitute.age);
    substitute.height = substitute.getIntent().getIntExtra("height", substitute.height);
    substitute.girl = substitute.getIntent().getBooleanExtra("boy", substitute.girl);
    substitute.ch = substitute.getIntent().getCharExtra("ch", substitute.ch);
    substitute.fl = substitute.getIntent().getFloatExtra("fl", substitute.fl);
    substitute.dou = substitute.getIntent().getDoubleExtra("dou", substitute.dou);
    substitute.pac = substitute.getIntent().getParcelableExtra("pac");
    if (null != serializationService) {
      substitute.obj = serializationService.parseObject(substitute.getIntent().getStringExtra("obj"), new com.alibaba.android.arouter.facade.model.TypeWrapper<TestObj>(){}.getType());
    } else {
      Log.e("ARouter::", "You want automatic inject the field 'obj' in class 'Test1Activity' , then you should implement 'SerializationService' to support object auto inject!");
    }
    if (null != serializationService) {
      substitute.objList = serializationService.parseObject(substitute.getIntent().getStringExtra("objList"), new com.alibaba.android.arouter.facade.model.TypeWrapper<List<TestObj>>(){}.getType());
    } else {
      Log.e("ARouter::", "You want automatic inject the field 'objList' in class 'Test1Activity' , then you should implement 'SerializationService' to support object auto inject!");
    }
    if (null != serializationService) {
      substitute.map = serializationService.parseObject(substitute.getIntent().getStringExtra("map"), new com.alibaba.android.arouter.facade.model.TypeWrapper<Map<String, List<TestObj>>>(){}.getType());
    } else {
      Log.e("ARouter::", "You want automatic inject the field 'map' in class 'Test1Activity' , then you should implement 'SerializationService' to support object auto inject!");
    }
    substitute.url = substitute.getIntent().getStringExtra("url");
    substitute.helloService = ARouter.getInstance().navigation(HelloService.class);
  }
}

對于這部分的代碼,大部分比較清晰,總體邏輯是從Activity接收到的intent中提取內容并賦值給對應的屬性。需要注意的是如下幾點

  1. @Autowired修飾的屬性不能為private
  2. @Autowired如果修飾的是自定義對象,那么需要有一個SerializationService服務實現
  3. @Autowired可以用來修飾服務,自動裝配的時候會找到對應的服務實現賦值

總結

綜合上面的內容,我們可以得出下面的結論

  1. ARouter 的自動注冊機制一定是通過這些路由清單類來實現的
  2. 我們可以通過兩種方式來找到定義的 PROVIDER 類型的路由節點
  3. 自動賦值功能的實現,一定是在頁面被路由打開時調用了生成的幫助類(ISyringe接口的 inject(Object target) 方法)

初始化

LogisticsCenter.init

ARouter的初始化是通過ARouter.init方法來實現的,這個方法最終是通過LogisticsCenter.init來實現具體的邏輯的

    /**
     * LogisticsCenter init, load all metas in memory. Demand initialization
     */
    public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
        mContext = context;
        executor = tpe;

        try {
            long startInit = System.currentTimeMillis();
            Set<String> routerMap;

            // It will rebuild router map every times when debuggable.
            if (ARouter.debuggable() || PackageUtils.isNewVersion(context)) {
                logger.info(TAG, "Run with debug mode or new install, rebuild router map.");
                // These class was generate by arouter-compiler.
                // 掃描對應包名下(實際上就是routes包)的所有類
                routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);
                if (!routerMap.isEmpty()) {
                    context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).edit().putStringSet(AROUTER_SP_KEY_MAP, routerMap).apply();
                }

                // 保存當前記錄的版本信息
                PackageUtils.updateVersion(context);    // Save new version name when router map update finish.
            } else {
                logger.info(TAG, "Load router map from cache.");
                routerMap = new HashSet<>(context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).getStringSet(AROUTER_SP_KEY_MAP, new HashSet<String>()));
            }

            logger.info(TAG, "Find router map finished, map size = " + routerMap.size() + ", cost " + (System.currentTimeMillis() - startInit) + " ms.");
            startInit = System.currentTimeMillis();

            // 對所有包下的類,分情況進行加載,加載到Warehouse的不同的屬性中
            for (String className : routerMap) {
                if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_ROOT)) {
                    // This one of root elements, load root.
                    ((IRouteRoot) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.groupsIndex);
                } else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_INTERCEPTORS)) {
                    // Load interceptorMeta
                    ((IInterceptorGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.interceptorsIndex);
                } else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_PROVIDERS)) {
                    // Load providerIndex
                    ((IProviderGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.providersIndex);
                }
            }

            logger.info(TAG, "Load root element finished, cost " + (System.currentTimeMillis() - startInit) + " ms.");

            if (Warehouse.groupsIndex.size() == 0) {
                logger.error(TAG, "No mapping files were found, check your configuration please!");
            }

            if (ARouter.debuggable()) {
                logger.debug(TAG, String.format(Locale.getDefault(), "LogisticsCenter has already been loaded, GroupIndex[%d], InterceptorIndex[%d], ProviderIndex[%d]", Warehouse.groupsIndex.size(), Warehouse.interceptorsIndex.size(), Warehouse.providersIndex.size()));
            }
        } catch (Exception e) {
            throw new HandlerException(TAG + "ARouter init logistics center exception! [" + e.getMessage() + "]");
        }
    }

根據這部分的邏輯,我們可以得出下面的結論

  1. loadInto這個方法,其實就是調用了剛剛我們分析的自動生成的那些類的loadInto方法,
  2. 初始化之后的路由表是保存在Warehouse這個類的一些成員變量里的。
  3. 初始化只加載了Root,Provider,Interceptor三個類的路由項

實際上對于第三點,正是之前說的,ARouter是分組懶加載的,所以初始化的時候并未做完全路由加載。

Warehouse

然后可以看看Warehouse里的內容

/**
 * Storage of route meta and other data.
 *
 * @author zhilong <a href="mailto:zhilong.lzl@alibaba-inc.com">Contact me.</a>
 * @version 1.0
 * @since 2017/2/23 下午1:39
 */
class Warehouse {
    // Cache route and metas
    static Map<String, Class<? extends IRouteGroup>> groupsIndex = new HashMap<>();
    static Map<String, RouteMeta> routes = new HashMap<>();

    // Cache provider
    static Map<Class, IProvider> providers = new HashMap<>();
    static Map<String, RouteMeta> providersIndex = new HashMap<>();

    // Cache interceptor
    static Map<Integer, Class<? extends IInterceptor>> interceptorsIndex = new UniqueKeyTreeMap<>("More than one interceptors use same priority [%s]");
    static List<IInterceptor> interceptors = new ArrayList<>();

    static void clear() {
        routes.clear();
        groupsIndex.clear();
        providers.clear();
        providersIndex.clear();
        interceptors.clear();
        interceptorsIndex.clear();
    }
}

這里有6個成員變量,有三個是在剛剛的init過程中初始化的——groupsIndex、providersIndex、interceptorsIndex,另外三個分別描述如下

  1. routes——這個用于保存完整的路由表,是在路由表更新的方法中不斷更新的,懶加載過程對應于LogisticsCenter.completion方法

  2. providers——這個用于緩存服務實現具體的類的對象,避免重復創建服務實現類的對象,懶加載過程對應于LogisticsCenter.completion方法

  3. interceptors——這個用于保存攔截器的優先級順序,因為我們攔截器是按照優先級先后來處理的,因此必然需要一個列表來保存這個優先級,懶加載過程對應于InterceptorServiceImpl.init方法

LogisticsCenter.completion

剛剛提到了LogisticsCenter.completion這個方法,ARouter的初始化過程嚴格來說應該也包含了這個『完善路由』的方法

    /**
     * Completion the postcard by route metas
     *
     * @param postcard Incomplete postcard, should completion by this method.
     */
    public synchronized static void completion(Postcard postcard) {
        if (null == postcard) {
            throw new NoRouteFoundException(TAG + "No postcard!");
        }

        RouteMeta routeMeta = Warehouse.routes.get(postcard.getPath());
        if (null == routeMeta) {    // Maybe its does't exist, or didn't load.
            Class<? extends IRouteGroup> groupMeta = Warehouse.groupsIndex.get(postcard.getGroup());  // Load route meta.
            if (null == groupMeta) {
                throw new NoRouteFoundException(TAG + "There is no route match the path [" + postcard.getPath() + "], in group [" + postcard.getGroup() + "]");
            } else {
                // Load route and cache it into memory, then delete from metas.
                try {
                    if (ARouter.debuggable()) {
                        logger.debug(TAG, String.format(Locale.getDefault(), "The group [%s] starts loading, trigger by [%s]", postcard.getGroup(), postcard.getPath()));
                    }

                    IRouteGroup iGroupInstance = groupMeta.getConstructor().newInstance();
                    iGroupInstance.loadInto(Warehouse.routes);
                    Warehouse.groupsIndex.remove(postcard.getGroup());

                    if (ARouter.debuggable()) {
                        logger.debug(TAG, String.format(Locale.getDefault(), "The group [%s] has already been loaded, trigger by [%s]", postcard.getGroup(), postcard.getPath()));
                    }
                } catch (Exception e) {
                    throw new HandlerException(TAG + "Fatal exception when loading group meta. [" + e.getMessage() + "]");
                }

                completion(postcard);   // Reload
            }
        } else {
            postcard.setDestination(routeMeta.getDestination());
            postcard.setType(routeMeta.getType());
            postcard.setPriority(routeMeta.getPriority());
            postcard.setExtra(routeMeta.getExtra());

            Uri rawUri = postcard.getUri();
            if (null != rawUri) {   // Try to set params into bundle.
                Map<String, String> resultMap = TextUtils.splitQueryParameters(rawUri);
                Map<String, Integer> paramsType = routeMeta.getParamsType();

                if (MapUtils.isNotEmpty(paramsType)) {
                    // Set value by its type, just for params which annotation by @Param
                    for (Map.Entry<String, Integer> params : paramsType.entrySet()) {
                        setValue(postcard,
                                params.getValue(),
                                params.getKey(),
                                resultMap.get(params.getKey()));
                    }

                    // Save params name which need auto inject.
                    postcard.getExtras().putStringArray(ARouter.AUTO_INJECT, paramsType.keySet().toArray(new String[]{}));
                }

                // Save raw uri
                postcard.withString(ARouter.RAW_URI, rawUri.toString());
            }

            switch (routeMeta.getType()) {
                case PROVIDER:  // if the route is provider, should find its instance
                    // Its provider, so it must be implememt IProvider
                    Class<? extends IProvider> providerMeta = (Class<? extends IProvider>) routeMeta.getDestination();
                    IProvider instance = Warehouse.providers.get(providerMeta);
                    if (null == instance) { // There's no instance of this provider
                        IProvider provider;
                        try {
                            provider = providerMeta.getConstructor().newInstance();
                            provider.init(mContext);
                            Warehouse.providers.put(providerMeta, provider);
                            instance = provider;
                        } catch (Exception e) {
                            throw new HandlerException("Init provider failed! " + e.getMessage());
                        }
                    }
                    postcard.setProvider(instance);
                    postcard.greenChannel();    // Provider should skip all of interceptors
                    break;
                case FRAGMENT:
                    postcard.greenChannel();    // Fragment needn't interceptors
                default:
                    break;
            }
        }
    }

這里的PostCard保存的是路由過程需要的一些全部信息,從上面的代碼里我們也可以看出來下面幾點

  1. 對于不在Warehouse.routes路由表中的路由項,需要加載并放在Warehouse.routes中
  2. 對于已經在路由表中的項,會將相應信息放入PostCard中
  3. 如果路由項是Provider(服務)且不存在于服務路由表緩存中時,會實例化服務并放入緩存

InterceptorServiceImpl.init

InterceptorServiceImpl實際上也是一個服務的具體實現,用于管理所有的攔截器的初始化,它在ARouter初始化(LogisticsCenter.init)之后執行,具體看下InterceptorServiceImpl.init的實現代碼

    public void init(final Context context) {
        LogisticsCenter.executor.execute(new Runnable() {
            @Override
            public void run() {
                if (MapUtils.isNotEmpty(Warehouse.interceptorsIndex)) {
                    for (Map.Entry<Integer, Class<? extends IInterceptor>> entry : Warehouse.interceptorsIndex.entrySet()) {
                        Class<? extends IInterceptor> interceptorClass = entry.getValue();
                        try {
                            IInterceptor iInterceptor = interceptorClass.getConstructor().newInstance();
                            iInterceptor.init(context);
                            Warehouse.interceptors.add(iInterceptor);
                        } catch (Exception ex) {
                            throw new HandlerException(TAG + "ARouter init interceptor error! name = [" + interceptorClass.getName() + "], reason = [" + ex.getMessage() + "]");
                        }
                    }

                    interceptorHasInit = true;

                    logger.info(TAG, "ARouter interceptors init over.");

                    synchronized (interceptorInitLock) {
                        interceptorInitLock.notifyAll();
                    }
                }
            }
        });
    }

可以看到攔截器是異步加載的,而且是從interceptorsIndex中提取所有的一次性完全加載到Warehouse管理的內存隊列中的。

綜合上面所有我們可以大概知道ARouter的路由表初始化的整個過程。

發起路由

這里只分析byType和byName兩種發起路由的方式,這兩種方式最終會執行到_ARouter類內的兩個方法

// byType
protected <T> T navigation(Class<? extends T> service) {
    // .............
}
// byName
protected Object navigation(final Context context, final Postcard postcard, final int requestCode, NavigationCallback callback) {
    // .............
}

byType

byType內部實現如下

protected <T> T navigation(Class<? extends T> service) {
        try {
            Postcard postcard = LogisticsCenter.buildProvider(service.getName());

            // Compatible 1.0.5 compiler sdk.
            if (null == postcard) { // No service, or this service in old version.
                postcard = LogisticsCenter.buildProvider(service.getSimpleName());
            }

            LogisticsCenter.completion(postcard);
            return (T) postcard.getProvider();
        } catch (NoRouteFoundException ex) {
            logger.warning(Consts.TAG, ex.getMessage());
            return null;
        }
    }

第一個方法用于構造路由信息PostCard對象,可以看到是從路由表中根據服務接口名找到路由基本信息

    /**
     * Build postcard by serviceName
     *
     * @param serviceName interfaceName
     * @return postcard
     */
    public static Postcard buildProvider(String serviceName) {
        RouteMeta meta = Warehouse.providersIndex.get(serviceName);

        if (null == meta) {
            return null;
        } else {
            return new Postcard(meta.getPath(), meta.getGroup());
        }
    }

然后使用上面分析過的completion方法完善PostCard對象信息,因為byType只用于路由服務,因此最后將Provider實例對象返回

byName

byName內部實現如下

/**
     * Use router navigation.
     *
     * @param context     Activity or null.
     * @param postcard    Route metas
     * @param requestCode RequestCode
     * @param callback    cb
     */
    protected Object navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
        try {
            LogisticsCenter.completion(postcard);
        } catch (NoRouteFoundException ex) {
            logger.warning(Consts.TAG, ex.getMessage());

            if (debuggable()) { // Show friendly tips for user.
                Toast.makeText(mContext, "There's no route matched!\n" +
                        " Path = [" + postcard.getPath() + "]\n" +
                        " Group = [" + postcard.getGroup() + "]", Toast.LENGTH_LONG).show();
            }

            if (null != callback) {
                callback.onLost(postcard);
            } else {    // No callback for this invoke, then we use the global degrade service.
                DegradeService degradeService = ARouter.getInstance().navigation(DegradeService.class);
                if (null != degradeService) {
                    degradeService.onLost(context, postcard);
                }
            }

            return null;
        }

        if (null != callback) {
            callback.onFound(postcard);
        }

        if (!postcard.isGreenChannel()) {   // It must be run in async thread, maybe interceptor cost too mush time made ANR.
            interceptorService.doInterceptions(postcard, new InterceptorCallback() {
                /**
                 * Continue process
                 *
                 * @param postcard route meta
                 */
                @Override
                public void onContinue(Postcard postcard) {
                    _navigation(context, postcard, requestCode, callback);
                }

                /**
                 * Interrupt process, pipeline will be destory when this method called.
                 *
                 * @param exception Reson of interrupt.
                 */
                @Override
                public void onInterrupt(Throwable exception) {
                    if (null != callback) {
                        callback.onInterrupt(postcard);
                    }

                    logger.info(Consts.TAG, "Navigation failed, termination by interceptor : " + exception.getMessage());
                }
            });
        } else {
            return _navigation(context, postcard, requestCode, callback);
        }

        return null;
    }

byName的方式,方法本身包含PostCard對象參數,該參數是根據路由項的Group和Path構造的,從上面的代碼里看到主要做了幾件事

  1. 調用completion方法完善路由表和postcard對象信息
  2. 回調告知路由狀態
  3. 攔截器工作
  4. 執行路由跳轉

我們看下最后執行路由跳轉的方法_navigation(context, postcard, requestCode, callback);

private Object _navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
        final Context currentContext = null == context ? mContext : context;

        switch (postcard.getType()) {
            case ACTIVITY:
                // Build intent
                final Intent intent = new Intent(currentContext, postcard.getDestination());
                intent.putExtras(postcard.getExtras());

                // Set flags.
                int flags = postcard.getFlags();
                if (-1 != flags) {
                    intent.setFlags(flags);
                } else if (!(currentContext instanceof Activity)) {    // Non activity, need less one flag.
                    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                }

                // Navigation in main looper.
                new Handler(Looper.getMainLooper()).post(new Runnable() {
                    @Override
                    public void run() {
                        if (requestCode > 0) {  // Need start for result
                            ActivityCompat.startActivityForResult((Activity) currentContext, intent, requestCode, postcard.getOptionsBundle());
                        } else {
                            ActivityCompat.startActivity(currentContext, intent, postcard.getOptionsBundle());
                        }

                        if ((0 != postcard.getEnterAnim() || 0 != postcard.getExitAnim()) && currentContext instanceof Activity) {    // Old version.
                            ((Activity) currentContext).overridePendingTransition(postcard.getEnterAnim(), postcard.getExitAnim());
                        }

                        if (null != callback) { // Navigation over.
                            callback.onArrival(postcard);
                        }
                    }
                });

                break;
            case PROVIDER:
                return postcard.getProvider();
            case BOARDCAST:
            case CONTENT_PROVIDER:
            case FRAGMENT:
                Class fragmentMeta = postcard.getDestination();
                try {
                    Object instance = fragmentMeta.getConstructor().newInstance();
                    if (instance instanceof Fragment) {
                        ((Fragment) instance).setArguments(postcard.getExtras());
                    } else if (instance instanceof android.support.v4.app.Fragment) {
                        ((android.support.v4.app.Fragment) instance).setArguments(postcard.getExtras());
                    }

                    return instance;
                } catch (Exception ex) {
                    logger.error(Consts.TAG, "Fetch fragment instance error, " + TextUtils.formatStackTrace(ex.getStackTrace()));
                }
            case METHOD:
            case SERVICE:
            default:
                return null;
        }

        return null;
    }

上面的代碼主要也是分路由類別做了不同的事情

  1. Activity——創建intent,塞入flag,extras等內容并執行界面跳轉
  2. Provider——返回Provider實現對象,供byName調用者調用服務接口
  3. Fragment——返回Fragment實例

以上就是對ARouter實現的一個簡單的分析,如果有興趣的話可以參考源碼閱讀更多的內容

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,622評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,716評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,746評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,991評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,706評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,036評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,029評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,203評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,725評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,451評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,677評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,161評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,857評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,266評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,606評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,407評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,643評論 2 380