ARouter使用解析

ARouter

ARouter 是阿里云出品的Android中間件,負責處理盅跳轉頁面時的邏輯,簡化和優化之前跳轉頁面的方法。同時他也是組件化的基礎之一,實現了模塊間的解耦。

ARouter使用

項目的主頁有提供ARouter的使用方法,主要就是

  • 注解可以跳轉的類(Activity,Service,ContentProvider,Fragment等)
  • 要跳轉頁面的時候使用Arouter,類似于ARouter.getInstance().build("/kotlin/test").withString("name", "老王").withInt("age", 23).navigation();

用法是不能再簡單了,猜想是把一個String與一個Activity對應起來就可以了,然而實際代碼應該比猜想復雜N多倍。下面一起分析一下這個中間,挖掘他的所有信息。

ARouter源碼解析

下面分析源碼分為幾部分

  • 跳轉頁面流程分析
  • 在類上和成員上的注解都做了什么
  • 解釋在ARouter文檔上的所有功能特點和典型應用是如何實現的,把這些特點抄到下面來,方便查看,我們會挨個把所有的特點分析一遍

一、功能介紹

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

二、典型應用

  1. 從外部URL映射到內部頁面,以及參數傳遞與解析
  2. 跨模塊頁面跳轉,模塊間解耦
  3. 攔截跳轉過程,處理登陸、埋點等邏輯
  4. 跨模塊API調用,通過控制反轉來做組件解耦

跳轉頁面流程分析

使用的方法是

ARouter.getInstance()
        .build("/kotlin/test")
        .withString("name", "老王")
        .withInt("age", 23)
        .navigation();

ARouter使用了單例,內部存儲了頁面的映射表,初始化狀態,debug信息等,等一下都會用到的信息(其實在保存在_ARouter單例中),同時也方便在使用的時候直接使用ARouter的靜態方法。
實際上除了ARouter之外還有一個_ARouter類,ARouter中幾乎是把所有的方法都直接給了_ARouter處理,這一層的轉換把_ARouter中復雜的方法轉化為ARouter中簡單的方法向外暴露,算是一種門面吧,值得學習一下。
build方法不例外地轉給了_ARouter

    public Postcard build(String path) {
        return _ARouter.getInstance().build(path);
    }

在_ARouter中構造了PostCard

/**
* Build postcard by path and default group
*/
protected Postcard build(String path) {
    if (TextUtils.isEmpty(path)) {
        throw new HandlerException(Consts.TAG + "Parameter is invalid!");
    } else {
        //這個Service里可以將傳入的path處理,換在另外一個,也相當于一個攔截器
        PathReplaceService pService = ARouter.getInstance().navigation(PathReplaceService.class);
        if (null != pService) {
            path = pService.forString(path);
        }
        return build(path, extractGroup(path));
    }
}

其中PathReplaceService相關處理邏輯是根據當前的path換成另外的path繼續走下面的流程,所以主流程還是build方法,其中有有個extractGroup(path)方法調用,將path中第一部分抽取出來作為group,繼續看主線

/**
 * Build postcard by path and group
 */
protected Postcard build(String path, String group) {
    if (TextUtils.isEmpty(path) || TextUtils.isEmpty(group)) {
        throw new HandlerException(Consts.TAG + "Parameter is invalid!");
    } else {
        PathReplaceService pService = ARouter.getInstance().navigation(PathReplaceService.class);
        if (null != pService) {
            path = pService.forString(path);
        }
        return new Postcard(path, group);
    }
}

這里又找了一次PathReplaceService,出于什么目的,很簡單,因為這個方法有可能不是上面那個路徑調過來的,_ARouter還有兩個參數的builde方法,直接調用了這個方法,但是不做任何區分直接再找一次也有點可以優化的空間??
返回的結果就是最后構造出來的一個PostCard,這個類上的解釋是A container that contains the roadmap.,包含這一次路由過程中所要的所有信息。PostCard的構造方法沒有什么邏輯,就是另外構造了一個bundle放在了PostCard里面備下面使用。

到這里就返回到了最初調用build的地方,再往下是兩個withXXX方法,就是向PostCard中放入幾個跳轉頁面要帶過去的信息,都是直接放到了bundle里面,當然這不是主線。
繼續看下面的navigation方法。

/**
 * Navigation to the route with path in postcard.
 * No param, will be use application context.
 */
public Object navigation() {
    //后面會用application的Context
    return navigation(null);
}

給出的例子都是使用沒有參數所navigation方法,后面拿applicationContext,要設置intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);感覺有點誤導的感覺,這里正正經經地傳個activity過來才應該是最廣泛的使用方法。

/**
 * Navigation to the route with path in postcard.
 *
 * @param context Activity and so on.
 */
public Object navigation(Context context) {
    //這里沒有caback,這個callback有onFound,onLost,onArrival,和onInterrupt方法,都是跳轉時的各種回調,單獨的跳轉降級就是通過這個callback完成的
    return navigation(context, null);
}

/**
 * Navigation to the route with path in postcard.
 *
 * @param context Activity and so on.
 */
public Object navigation(Context context, NavigationCallback callback) {
    //這里又增加了一個-1的參數,不求不需求forResult
    return ARouter.getInstance().navigation(context, this, -1, callback);
}

到這里方法調用就出了PostCard,到了ARouter中,當然又會委托給_ARouter進行真正的業務。
看_ARouter的方法。

/**
 * Use router navigation.
 */
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...
        }
        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);
    }

    //主流程,根據是否綠色通道走攔截的邏輯
    //這里還有個友情提示: It must be run in async thread, maybe interceptor cost too mush time made ANR.
    if (!postcard.isGreenChannel()) {  
        interceptorService.doInterceptions(postcard, new InterceptorCallback() {
            @Override
            public void onContinue(Postcard postcard) {
                //走完攔截器并通過的,繼續走跳轉的邏輯
                _navigation(context, postcard, requestCode, callback);
            }
            @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;
}

主流程沒有幾名話,先補全路由的信息,走攔截邏輯,然后真正的跳轉,其中補全PostCard是重要過程,我們看一下方法詳情

/**
 * Completion the postcard by route metas
 * @param postcard Incomplete postcard, should complete by this method.
 */
public synchronized static void completion(Postcard postcard) {
    RouteMeta routeMeta = Warehouse.routes.get(postcard.getPath());
    if (null == routeMeta) {    // Maybe its does't exist, or didn't load.
        //沒有找到RouteMeta,檢查他所有的組是否還沒有加載,如果已經加載,則異常,沒有加載去加載
        Class<? extends IRouteGroup> groupMeta = Warehouse.groupsIndex.get(postcard.getGroup());  // Load route meta.
        if (null == groupMeta) {
            throw new NoRouteFoundException(...);
        } else {
            // Load route and cache it into memory, then delete from metas.
            try {
                IRouteGroup iGroupInstance = groupMeta.getConstructor().newInstance();
                //加載所在的組,如果加載完成就把這個group從未加載列表中刪除
                iGroupInstance.loadInto(Warehouse.routes);
                //將些組從未加載中移除,防止重復加載,
                Warehouse.groupsIndex.remove(postcard.getGroup());
            } catch (Exception e) {
                throw new HandlerException(...);
            }
            // 加載了組信息,重新去重新去走補全的邏輯
            completion(postcard);   // Reload
    } else {
        將RouteMeta的信息放到PostCard中,
        如果是通過uri跳轉的話再將路徑中的信息解析出來放到postCard的bound中

        下面又對兩種類型的跳轉做的特殊處理
        1. PROVIDER
           從倉庫中找到provider的實例,疳賦值給postCard
           設置綠色通道,防止攔截
        2. FRAGMENT
           設置綠色通道,防止攔截
    }
}

此時post的信息已經完全了,我們路過攔截的邏輯,直接看下面真正的跳轉方法,感覺是沒有必要把代碼再拿出來,就是根據類型區分了一下,activity就直接new Intent進行跳轉,如果是Privider,Fragment就返回實例。
到這里基本完成了一次跳轉頁面所走的全部路徑。并沒有高深難懂的邏輯,一個比較好玩的就是PathReplaceService,DegradeService,SerializationService等都是通過注冊一個Service完成的,這就大大增加了這個框架的靈活性,而且框架向外提供的這個功能,自己內部已經先用起來了,這個也是挺有意思的。

ARouter中的注解有什么用,是怎么起作用的

@Route

作用:注解一個類,這個類就可以通過ARouter找到使用
Route主要有兩個屬性,path和group,在RouteProcessor中處理這個注解,在注解處理的方法中會根據注解的類型創建上面使用過的RouteMeta

for (Element element : routeElements) {
    TypeMirror tm = element.asType();
    Route route = element.getAnnotation(Route.class);
    RouteMeta routeMeta = null;

    if (types.isSubtype(tm, type_Activity)) {                 // Activity
        logger.info(">>> Found activity route: " + tm.toString() + " <<<");

        // Get all fields annotation by @Autowired
        Map<String, Integer> paramsType = new HashMap<>();
        for (Element field : element.getEnclosedElements()) {
            if (field.getKind().isField() && field.getAnnotation(Autowired.class) != null && !types.isSubtype(field.asType(), iProvider)) {
                // It must be field, then it has annotation, but it not be provider.
                Autowired paramConfig = field.getAnnotation(Autowired.class);
                paramsType.put(StringUtils.isEmpty(paramConfig.name()) ? field.getSimpleName().toString() : paramConfig.name(), typeUtils.typeExchange(field));
            }
        }
        routeMeta = new RouteMeta(route, element, RouteType.ACTIVITY, paramsType);
    } else if (types.isSubtype(tm, iProvider)) {         // IProvider
        logger.info(">>> Found provider route: " + tm.toString() + " <<<");
        routeMeta = new RouteMeta(route, element, RouteType.PROVIDER, null);
    } else if (types.isSubtype(tm, type_Service)) {           // Service
        logger.info(">>> Found service route: " + tm.toString() + " <<<");
        routeMeta = new RouteMeta(route, element, RouteType.parse(SERVICE), null);
    } else if (types.isSubtype(tm, fragmentTm) || types.isSubtype(tm, fragmentTmV4)) {
        logger.info(">>> Found fragment route: " + tm.toString() + " <<<");
        routeMeta = new RouteMeta(route, element, RouteType.parse(FRAGMENT), null);
    } else {
        throw new RuntimeException("ARouter::Compiler >>> Found unsupported class type, type = [" + types.toString() + "].");
    }

    categories(routeMeta);
    // if (StringUtils.isEmpty(moduleName)) {   // Hasn't generate the module name.
    //     moduleName = ModuleUtils.generateModuleName(element, logger);
    // }
}

分別構建出來RouteMeta,還構建出來一個分組的信息,下面將這些信息構建兩個java文件。類似于這樣

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);
  }
}

rootInfo,存放所有的組的信息,就是上面找不到RouteMeta的時候會從這里找到對應的組,再找到組信息對應的類,然后加載
還有一個組詳細信息的類,類似開這樣

public class ARouter$$Group$$test implements IRouteGroup {
    @Override
    public void loadInto(Map<String, RouteMeta> atlas) {
      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/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));
    }
}  

加載的時候把這些信息加載到map中,以后跳轉使用

@Interceptor

作用:設置全局跳轉的攔截器,可以設置優先級
處理注解基本和和@Route一樣,得到類,得到屬性,javapoet寫出一個類似于這樣的類:

public class ARouter$$Interceptors$$app implements IInterceptorGroup {
  @Override
  public void loadInto(Map<Integer, Class<? extends IInterceptor>> interceptors) {
    interceptors.put(7, Test1Interceptor.class);
  }
}

這個map是個特別的map,根據key的值自動排序,如果key重復會異常,也也是這個攔截器可以按優先級排序的原因

@Autowired

作用:自動裝配,注解成員后,可以自動從Intent中解出數據并賦值給變量
實現也很相似,找到被注解的成員,生成一個helper,在需要將intent的數據解出來的時候使用helper的inject方法,ARouter又使用了一個AutowiredService專門做這個事,只要將要注入的類傳過來就可以了

@Override
public void autowire(Object instance) {
    String className = instance.getClass().getName();
    try {
        if (!blackList.contains(className)) {
            // 只有一個inject方法
            ISyringe autowiredHelper = classCache.get(className);
            if (null == autowiredHelper) {  // No cache.
                autowiredHelper = (ISyringe) Class.forName(instance.getClass().getName() + SUFFIX_AUTOWIRED).getConstructor().newInstance();
            }
            // autowiredHelper就是根據注解生成的特定helper
            autowiredHelper.inject(instance);
            classCache.put(className, autowiredHelper);
        }
    } catch (Exception ex) {
        blackList.add(className);    // This instance need not autowired.
    }
}

javaPoet實在是有點煩瑣,真的不愿把他的代碼拿來。有意的同學可以直接去arouter查看

解釋所有官方列舉的特點

  1. 支持直接解析標準URL進行跳轉,并自動注入參數到目標頁面中

在Manifast頁面中注冊了兩個filter

<intent-filter>
    <data
        android:host="m.aliyun.com"
        android:scheme="arouter"/>
    <action android:name="android.intent.action.VIEW"/>
    <category android:name="android.intent.category.DEFAULT"/>
    <category android:name="android.intent.category.BROWSABLE"/>
</intent-filter>

<!-- App Links -->
<intent-filter android:autoVerify="true">
    <action android:name="android.intent.action.VIEW"/>
    <category android:name="android.intent.category.DEFAULT"/>
    <category android:name="android.intent.category.BROWSABLE"/>
    <data
        android:host="m.aliyun.com"
        android:scheme="http"/>
    <data
        android:host="m.aliyun.com"
        android:scheme="https"/>
</intent-filter>

第一個是處理特定的協議,如果協議中有arouter,則會用這個Acitity處理
還有一個是處理applink的,在網頁上點擊特定連接,也是在這個Activity中處理
在這個Activity主要代碼就兩行:

Uri uri = getIntent().getData();
ARouter.getInstance().build(uri).navigation(this, new NavCallback() {
            @Override
            public void onArrival(Postcard postcard) {
                finish();
            }
        });

這里使用的是一個URI,在構造PostCard的時候會將URI后面掛的參數直接轉化到bound中去,也就解釋了第一個特征。

  1. 支持多模塊工程使用

各模塊只是依賴一個String,編譯時會掃描整個所有的工程,所以直接就支持多模塊的工程。但是有個問題就是別的頁面要跳轉的時候都要將字符串寫死進去,如果定義常量的話會出現多個模塊依賴一個常量類的情況。

  1. 支持添加多個攔截器,自定義攔截順序
    攔截器注解定義

如果設置了這個優先級別,生成的java代碼中會將這個優先級做為key,放到傳過來的一個容器中,而這個窗口的定義在com.alibaba.android.arouter.core.Warehouse中,是一個UniqueKeyTreeMap,保證key是唯一的,并且按key進行排序

UniqueKeyTreeMap

這里也就解釋了自定義攔截順序的特點

  1. 支持依賴注入,可單獨作為依賴注入框架使用

不知道講的是什么。。
navigation方法返回的是一個Object

  1. 支持InstantRun
  2. 支持MultiDex(Google方案)

這里看到源碼中找了一個所有的的dex文件,再從這些所有的dex中查找要找的router的類,應該就是處理這個問題。等于沒有說。。就簡單看一下調用鏈吧

Arouter.init() -> LogisticsCenter.init(mContext, executor) 
-> ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);//這里就是是指定的ali的包名,就是生成的那些類包名
-> ClassUtils.getSourcePaths(context);//從多dex下找類 
-> ClassUtils.tryLoadInstantRunDexFile(applicationInfo)//instantRun
-> 找到所有生成的注冊router的類
  1. 映射關系按組分類、多級管理,按需初始化

路由的注解中可以注冊一個group信息,如果不定義這個group信息,arouter會拿路徑中的第一段做為group。處理注解的時候會生成兩組信息,第一是組信息,其中有所有group的信息,每一組都會指向一個描述這個組中所有路徑的類。
初始化時僅僅加載了組的信息,并沒有加載每一組內的所有路由,使用路由時會先查找有沒有這個路由信息,如果沒有的話就去加載這一組所有的路由。做到了按需初始化。
這個過程上面路由過程已經用代碼分析過了。

  1. 支持用戶指定全局降級與局部降級策略

每一次使用路由時可以傳入一個callback,作為單次路由失敗的降級策略,其實也不僅僅是降級策略,callback提供了多個回調方法使用:

public interface NavigationCallback {
    //找到路由
    void onFound(Postcard postcard);
    //沒有找到,降級吧
    void onLost(Postcard postcard);
    //向android發出了startActivity的請求
    void onArrival(Postcard postcard);
    //使用攔截器時
    void onInterrupt(Postcard postcard);
}

也可以注冊一個IProvider,用來處理所有的降級策略。

  1. 頁面、攔截器、服務等組件均自動注冊到框架

使用注解,編譯期處理,運行時直接無反射運行(多dex什么的還是要反射)

  1. 支持多種方式配置轉場動畫

支持,無特殊

  1. 支持獲取Fragment

navigate的時候支持返回一個fragment,只要注冊了路由的fragment,都可以通過路由來得到實例。

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

推薦閱讀更多精彩內容

  • 組件化 模塊化、組件化與插件化 在項目發展到一定程度,隨著人員的增多,代碼越來越臃腫,這時候就必須進行模塊化的拆分...
    silentleaf閱讀 4,961評論 2 12
  • ARouter源碼解讀 以前看優秀的開源項目,看到了頁面路由框架ARouter,心想頁面路由是個啥東東,于是乎網上...
    陸元偉閱讀 546評論 0 1
  • 盛夏將盡,時光染暖 答應會在 牽起的手小心翼翼 仿佛那是稀世珍寶,稍一用力就會破碎 盛夏將盡,溫度剛好 約定了一世...
    戀物念一樣閱讀 289評論 1 2
  • 軒兒閱讀 95評論 0 0
  • 無形的網 我們都是些 負重的 帶殼兒的蟲 動彈不了 被它粘著 全都低著頭 專注著 蠶食每本起皺的書 多刻苦 每當翻...
    燦7閱讀 248評論 1 5