Android組件化最佳實踐 ARetrofit原理

開源ARetrofit也有一段時間了绘面,陸續(xù)有用戶反饋希望有文章講述實現(xiàn)的原理蹭秋,由于本人寫作水平有限一直沒有動筆凿将。趁著現(xiàn)在空閑下來終于還打算寫下此文腾夯,文筆有限颊埃,這里非常感謝大家的理解和支持蔬充。無論是ARetrofit的用戶也好,還是對源碼感興趣的同學(xué)也好班利,希望能從這篇文章中有所收獲饥漫。

簡介

ARetrofit是一款針對Android組件之間通信的框架,實現(xiàn)組件之間解耦的同時還可以通信罗标。

源碼鏈接

歡迎star庸队、issues、fork

組件化架構(gòu) APP Demo, ARetrofit 使用實例

組件化

Android組件化已經(jīng)不是一個新鮮的概念了闯割,出來了已經(jīng)有很長一段時間了彻消,大家可以自行Google,可以看到一堆相關(guān)的文章宙拉。

簡單的來說宾尚,所謂的組件就是Android Studio中的Module,每一個Module都遵循高內(nèi)聚的原則鼓黔,通過ARetrofit來實現(xiàn)無耦合的代碼結(jié)構(gòu)央勒,如下圖:

組件化結(jié)構(gòu).png

每一個Module可單獨作為一個project運行,而打包到整體時Module之間的通信通過ARetrofit完成澳化。

ARetrofit原理

講原理之前,我想先說說為什么要ARetrofit稳吮。開發(fā)ARetrofit這個項目的思路來源其實是Retrofit缎谷,Retrofit是Square公司開發(fā)的一款針對Android網(wǎng)絡(luò)請求的框架,這里不對Retrofit展開來講灶似。主要是Retrofit框架使用非常多的設(shè)計模式列林,可以說Retrofit這個開源項目將Java的設(shè)計模式運用到了極致,當(dāng)然最終提供的API也是非常簡潔的酪惭。如此簡潔的API希痴,使得我們APP中的網(wǎng)絡(luò)模塊實現(xiàn)變得非常輕松,并且維護起來也很舒服春感。因此我覺得有必要將Android組件之間的通信也變得輕松砌创,使用者可以優(yōu)雅的通過簡潔的API就可以實現(xiàn)通信,更重要的是維護起來也非常的舒服鲫懒。

ARetrofit基本原理可以簡化為下圖所示:


基本原理圖.png
  1. 通過注解聲明需要通信的Activity/Fragment或者Class
  2. 每一個module通過annotationProcessor在編譯時生成待注入的RouteInject的實現(xiàn)類和AInterceptorInject的實現(xiàn)類嫩实。
    這一步在執(zhí)行app[build]時會輸出日志,可以直觀的看到窥岩,如下圖所示:
注: AInjecton::Compiler >>> Apt interceptor Processor start... <<<
注: AInjecton::Compiler enclosindClass = null
注: AInjecton::Compiler value = 3
注: AInjecton::Compiler auto generate class = com$$sjtu$$yifei$$eCGVmTMvXG$$AInterceptorInject
注: AInjecton::Compiler add path= 3 and class= LoginInterceptor
....
注: AInjecton::Compiler >>> Apt route Processor start... <<<
注: AInjecton::Compiler enclosindClass = null
注: AInjecton::Compiler value = /login-module/ILoginProviderImpl
注: AInjecton::Compiler enclosindClass = null
注: AInjecton::Compiler value = /login-module/LoginActivity
注: AInjecton::Compiler enclosindClass = null
注: AInjecton::Compiler value = /login-module/Test2Activity
注: AInjecton::Compiler enclosindClass = null
注: AInjecton::Compiler value = /login-module/TestFragment
注: AInjecton::Compiler auto generate class = com$$sjtu$$yifei$$VWpdxWEuUx$$RouteInject
注: AInjecton::Compiler add path= /login-module/TestFragment and class= null
注: AInjecton::Compiler add path= /login-module/LoginActivity and class= null
注: AInjecton::Compiler add path= /login-module/Test2Activity and class= null
注: AInjecton::Compiler add path= /login-module/ILoginProviderImpl and class= null
注: AInjecton::Compiler >>> Apt route Processor succeed <<<
  1. 將編譯時生成的類注入到RouterRegister中甲献,這個類主要用于維護路由表和攔截器,對應(yīng)的[build]日志如下:
TransformPluginLaunch >>> ========== Transform scan start ===========
TransformPluginLaunch >>> ========== Transform scan end cost 0.238 secs and start inserting ===========
TransformPluginLaunch >>> Inserting code to jar >> /Users/yifei/as_workspace/ARetrofit/app/build/intermediates/transforms/TransformPluginLaunch/release/8.jar
TransformPluginLaunch >>> to class >> com/sjtu/yifei/route/RouteRegister.class
InjectClassVisitor >>> inject to class:
InjectClassVisitor >>> com/sjtu/yifei/route/RouteRegister{
InjectClassVisitor >>>        public *** init() {
InjectClassVisitor >>>            register("com.sjtu.yifei.FBQWNfbTpY.com$$sjtu$$yifei$$FBQWNfbTpY$$RouteInject")
InjectClassVisitor >>>            register("com.sjtu.yifei.klBxerzbYV.com$$sjtu$$yifei$$klBxerzbYV$$RouteInject")
InjectClassVisitor >>>            register("com.sjtu.yifei.JmhcMMUhkR.com$$sjtu$$yifei$$JmhcMMUhkR$$RouteInject")
InjectClassVisitor >>>            register("com.sjtu.yifei.fpyxYyTCRm.com$$sjtu$$yifei$$fpyxYyTCRm$$AInterceptorInject")
InjectClassVisitor >>>        }
InjectClassVisitor >>> }
TransformPluginLaunch >>> ========== Transform insert cost 0.017 secs end ===========
  1. Routerfit.register(Class<T> service) 這一步主要是通過動態(tài)代理模式實現(xiàn)接口中聲明的服務(wù)颂翼。

前面講的是整體的框架設(shè)計思想晃洒,便于讀者從全局的覺得來理解ARetrofit的框架的架構(gòu)慨灭。接下來,將待大家個個擊破上面提到的annotationProcessor球及、 transform在項目中如何使用氧骤,以及動態(tài)代理、攔截器功能的實現(xiàn)等細節(jié)桶略。

一语淘、annotationProcessor生成代碼

annotationProcessor(注解處理器)是javac內(nèi)置的一個用于編譯時掃描和處理注解(Annotation)的工具。簡單的說际歼,在源代碼編譯階段惶翻,通過注解處理器,我們可以獲取源文件內(nèi)注解(Annotation)相關(guān)內(nèi)容鹅心。Android Gradle 2.2 及以上版本提供annotationProcessor的插件吕粗。
在ARetrofit中annotationProcessor對應(yīng)的module是auto-complier,在使用annotationProcessor之前首先需要聲明好注解旭愧。關(guān)于注解不太了解或者遺忘的同學(xué)可直接參考我之前寫的Java注解這篇文章颅筋,本項目中聲明的注解在auto-annotation這個module中,主要有:

  • @Extra 路由參數(shù)
  • @Flags intent flags
  • @Go 路由路徑key
  • @Interceptor 聲明自定義攔截器
  • @RequestCode 路由參數(shù)
  • @Route路由
  • @Uri
  • @IMethod 用于標(biāo)記注冊代碼將插入到此方法中(transform中使用)
  • @Inject 用于標(biāo)記需要被注入類输枯,最近都將插入到標(biāo)記了#com.sjtu.yifei.annotation.IMethod的方法中(transform中使用)

創(chuàng)建自定義的注解處理器议泵,具體使用方法可參考利用注解動態(tài)生成代碼,本項目中的注解處理器如下所示:

//這是用來注冊注解處理器要處理的源代碼版本桃熄。
@SupportedSourceVersion(SourceVersion.RELEASE_8)
//這個注解用來注冊注解處理器要處理的注解類型先口。有效值為完全限定名(就是帶所在包名和路徑的類全名
@SupportedAnnotationTypes({ANNOTATION_ROUTE, ANNOTATION_GO})
//來注解這個處理器,可以自動生成配置信息
@AutoService(Processor.class)
public class IProcessor extends AbstractProcessor {
    

}

生成代碼的關(guān)鍵部分在GenerateAInterceptorInjectImplGenerateRouteInjectImpl中瞳收,以下貼出關(guān)鍵代碼:

public void generateAInterceptorInjectImpl(String pkName) {
        try {
            String name = pkName.replace(".",DECOLLATOR) + SUFFIX;
            logger.info(String.format("auto generate class = %s", name));
            TypeSpec.Builder builder = TypeSpec.classBuilder(name)
                    .addModifiers(Modifier.PUBLIC)
                    .addAnnotation(Inject.class)
                    .addSuperinterface(AInterceptorInject.class);

            ClassName hashMap = ClassName.get("java.util", "HashMap");

            //Map<String, Class<?>>
            TypeName wildcard = WildcardTypeName.subtypeOf(Object.class);
            TypeName classOfAny = ParameterizedTypeName.get(ClassName.get(Class.class), wildcard);
            TypeName string = ClassName.get(Integer.class);

            TypeName map = ParameterizedTypeName.get(ClassName.get(Map.class), string, classOfAny);

            MethodSpec.Builder injectBuilder = MethodSpec.methodBuilder("getAInterceptors")
                    .addModifiers(Modifier.PUBLIC)
                    .addAnnotation(Override.class)
                    .returns(map)
                    .addStatement("$T interceptorMap = new $T<>()", map, hashMap);

            for (Map.Entry<Integer, ClassName> entry : interceptorMap.entrySet()) {
                logger.info("add path= " + entry.getKey() + " and class= " + entry.getValue().simpleName());
                injectBuilder.addStatement("interceptorMap.put($L, $T.class)", entry.getKey(), entry.getValue());
            }
            injectBuilder.addStatement("return interceptorMap");

            builder.addMethod(injectBuilder.build());

            JavaFile javaFile = JavaFile.builder(pkName, builder.build())
                    .build();
            javaFile.writeTo(filer);

        } catch (Exception e) {
            e.printStackTrace();
        }

    }

public void generateRouteInjectImpl(String pkName) {
        try {
            String name = pkName.replace(".",DECOLLATOR) + SUFFIX;
            logger.info(String.format("auto generate class = %s", name));
            TypeSpec.Builder builder = TypeSpec.classBuilder(name)
                    .addModifiers(Modifier.PUBLIC)
                    .addAnnotation(Inject.class)
                    .addSuperinterface(RouteInject.class);

            ClassName hashMap = ClassName.get("java.util", "HashMap");

            //Map<String, String>
            TypeName wildcard = WildcardTypeName.subtypeOf(Object.class);
            TypeName classOfAny = ParameterizedTypeName.get(ClassName.get(Class.class), wildcard);
            TypeName string = ClassName.get(String.class);

            TypeName map = ParameterizedTypeName.get(ClassName.get(Map.class), string, classOfAny);

            MethodSpec.Builder injectBuilder = MethodSpec.methodBuilder("getRouteMap")
                    .addModifiers(Modifier.PUBLIC)
                    .addAnnotation(Override.class)
                    .returns(map)
                    .addStatement("$T routMap = new $T<>()", map, hashMap);

            for (Map.Entry<String, ClassName> entry : routMap.entrySet()) {
                logger.info("add path= " + entry.getKey() + " and class= " + entry.getValue().enclosingClassName());
                injectBuilder.addStatement("routMap.put($S, $T.class)", entry.getKey(), entry.getValue());
            }
            injectBuilder.addStatement("return routMap");

            builder.addMethod(injectBuilder.build());

            JavaFile javaFile = JavaFile.builder(pkName, builder.build())
                    .build();
            javaFile.writeTo(filer);

        } catch (Exception e) {
            e.printStackTrace();
        }

    }

二碉京、Transform

Android Gradle 工具在 1.5.0 版本后提供了 Transfrom API, 允許第三方 Plugin在打包dex文件之前的編譯過程中操作 .class 文件。這一部分面向高級Android工程師的螟深,面向字節(jié)碼編程谐宙,普通工程師可不做了解。

寫到這里也許有人會有這樣一個疑問界弧,既然annotationProcessor這么好用為什么還有Transform面向字節(jié)碼注入呢凡蜻?這里需要解釋以下,annotationProcessor具有局限性夹纫,annotationProcessor只能掃描當(dāng)前module下的代碼咽瓷,且對于第三方的jar、aar文件都掃描不到舰讹。而Transform就沒有這樣的局限性茅姜,在打包dex文件之前的編譯過程中操作.class 文件。

關(guān)于Transfrom API在Android Studio中如何使用可以參考Transform API?—?a real world example,順便提供一下字節(jié)碼指令方便我們讀懂ASM钻洒。

本項目中的Transform插件在AInject中奋姿,實現(xiàn)源碼TransformPluginLaunch如下,貼出關(guān)鍵部分:

/**
 *
 * 標(biāo)準(zhǔn)transform的格式素标,一般實現(xiàn)transform可以直接拷貝一份重命名即可
 *
 * 兩處todo實現(xiàn)自己的字節(jié)碼增強/優(yōu)化操作
 */
class TransformPluginLaunch extends Transform implements Plugin<Project> {

    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        super.transform(transformInvocation)
 
        //todo step1: 先掃描
        transformInvocation.inputs.each {
            TransformInput input ->
                input.jarInputs.each { JarInput jarInput ->
                   ...
                }

                input.directoryInputs.each { DirectoryInput directoryInput ->
                    //處理完輸入文件之后称诗,要把輸出給下一個任務(wù)
                  ...
                }
        }
       
        //todo step2: ...完成代碼注入
        if (InjectInfo.get().injectToClass != null) {
          ...
        }
  
    }

    /**
     * 掃描jar包
     * @param jarFile
     */
    static void scanJar(File jarFile, File destFile) {
        
    }

    /**
     * 掃描文件
     * @param file
     */
    static void scanFile(File file, File dest) {
       ...
    }
}

注入代碼一般分為兩個步驟

  • 第一步:掃描
    這一部分主要是掃描的內(nèi)容有:
    注入類和方法的信息,是AutoRegisterContract的實現(xiàn)類和其中@IMethod头遭,@Inject的方法寓免。
    待注入類的和方法信息,是RouteInject 和 AInterceptorInject實現(xiàn)類且被@Inject注解的计维。
  • 第二步:注入
    以上掃描的結(jié)果袜香,將待注入類注入到注入類的過程。這一過程面向ASM操作鲫惶,可參考字節(jié)碼指令來讀懂以下的關(guān)鍵注入代碼:
class InjectClassVisitor extends ClassVisitor {
...
    class InjectMethodAdapter extends MethodVisitor {

        InjectMethodAdapter(MethodVisitor mv) {
            super(Opcodes.ASM5, mv)
        }

        @Override
        void visitInsn(int opcode) {
            Log.e(TAG, "inject to class:")
            Log.e(TAG, own + "{")
            Log.e(TAG, "       public *** " + InjectInfo.get().injectToMethodName + "() {")
            if (opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) {
                InjectInfo.get().injectClasses.each { injectClass ->
                    injectClass = injectClass.replace('/', '.')
                    Log.e(TAG, "           " + method + "(\"" + injectClass + "\")")
                    mv.visitVarInsn(Opcodes.ALOAD, 0)
                    mv.visitLdcInsn(injectClass)
                    mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, own, method, "(Ljava/lang/String;)V", false)
                }
            }
            Log.e(TAG, "       }")
            Log.e(TAG, "}")
            super.visitInsn(opcode)
        }
...
    }
...
}

三蜈首、動態(tài)代理

定義:為其它對象提供一種代理以控制對這個對象的訪問控制;在某些情況下欠母,客戶不想或者不能直接引用另一個對象欢策,這時候代理對象可以在客戶端和目標(biāo)對象之間起到中介的作用。
Routerfit.register(Class<T> service) 這里就是采用動態(tài)代理的模式赏淌,使得ARetrofit的API非常簡潔踩寇,使用者可以優(yōu)雅定義出路由接口。關(guān)于動態(tài)代理的學(xué)習(xí)難度相對來說還比較小六水,想了解的同學(xué)可以參考這篇文章java動態(tài)代理姑荷。

本項目相關(guān)源碼:

public final class Routerfit {
...
      private <T> T create(final Class<T> service) {
        RouterUtil.validateServiceInterface(service);
        return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[]{service}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, @Nullable Object[] args) throws Throwable {
                // If the method is a method from Object then defer to normal invocation.
                if (method.getDeclaringClass() == Object.class) {
                    return method.invoke(this, args);
                }
                ServiceMethod<Object> serviceMethod = (ServiceMethod<Object>) loadServiceMethod(method, args);
                if (!TextUtils.isEmpty(serviceMethod.uristring)) {
                    Call<T> call = (Call<T>) new ActivityCall(serviceMethod);
                    return call.execute();
                }
                try {
                    if (serviceMethod.clazz == null) {
                        throw new RouteNotFoundException("There is no route match the path \"" + serviceMethod.routerPath + "\"");
                    }
                } catch (RouteNotFoundException e) {
                    Toast.makeText(ActivityLifecycleMonitor.getApp(), e.getMessage(), Toast.LENGTH_SHORT).show();
                    e.printStackTrace();
                }
                if (RouterUtil.isSpecificClass(serviceMethod.clazz, Activity.class)) {
                    Call<T> call = (Call<T>) new ActivityCall(serviceMethod);
                    return call.execute();
                } else if (RouterUtil.isSpecificClass(serviceMethod.clazz, Fragment.class)
                        || RouterUtil.isSpecificClass(serviceMethod.clazz, android.app.Fragment.class)) {
                    Call<T> call = new FragmentCall(serviceMethod);
                    return call.execute();
                } else if (serviceMethod.clazz != null) {
                    Call<T> call = new IProviderCall<>(serviceMethod);
                    return call.execute();
                }

                if (serviceMethod.returnType != null) {
                    if (serviceMethod.returnType == Integer.TYPE) {
                        return -1;
                    } else if (serviceMethod.returnType == Boolean.TYPE) {
                        return false;
                    } else if (serviceMethod.returnType == Long.TYPE) {
                        return 0L;
                    } else if (serviceMethod.returnType == Double.TYPE) {
                        return 0.0d;
                    } else if (serviceMethod.returnType == Float.TYPE) {
                        return 0.0f;
                    } else if (serviceMethod.returnType == Void.TYPE) {
                        return null;
                    } else if (serviceMethod.returnType == Byte.TYPE) {
                        return (byte)0;
                    } else if (serviceMethod.returnType == Short.TYPE) {
                        return (short)0;
                    } else if (serviceMethod.returnType == Character.TYPE) {
                        return null;
                    }
                }
                return null;
            }
        });
    }
...
}

這里ServiceMethod是一個非常重要的類,使用了外觀模式缩擂,主要用于解析方法中的被注解所有信息并保存起來。

四添寺、攔截器鏈實現(xiàn)

本項目中的攔截器鏈設(shè)計胯盯,使得使用者可以非常優(yōu)雅的處理業(yè)務(wù)邏輯。如下:

@Interceptor(priority = 3)
public class LoginInterceptor implements AInterceptor {

    private static final String TAG = "LoginInterceptor";
    @Override
    public void intercept(final Chain chain) {
        //Test2Activity 需要登錄
        if ("/login-module/Test2Activity".equalsIgnoreCase(chain.path())) {
            Routerfit.register(RouteService.class).launchLoginActivity(new ActivityCallback() {
                @Override
                public void onActivityResult(int i, Object data) {
                    if (i == Routerfit.RESULT_OK) {//登錄成功后繼續(xù)執(zhí)行
                        Toast.makeText(ActivityLifecycleMonitor.getTopActivityOrApp(), "登錄成功", Toast.LENGTH_LONG).show();
                        chain.proceed();
                    } else {
                        Toast.makeText(ActivityLifecycleMonitor.getTopActivityOrApp(), "登錄取消/失敗", Toast.LENGTH_LONG).show();
                    }
                }
            });
        } else {
            chain.proceed();
        }
    }

}

這一部分實現(xiàn)的思想是參考了okhttp中的攔截器计露,這里使用了java設(shè)計模式責(zé)任鏈模式博脑,具體實現(xiàn)歡迎閱讀源碼。

結(jié)束語

到此票罐,ARetrofit開源項目基本都以及講完叉趣,在做這個項目的過程中其實遇到了各種各樣的問題,其中ASM這塊耗費的時間較長该押,對于當(dāng)時的我還是個小白疗杉。當(dāng)然收獲也是頗多,這也是本人的第一個開源項目蚕礼,存在的不足之處歡迎讀者和用戶提出烟具,可以直接在qq群里提出梢什。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市朝聋,隨后出現(xiàn)的幾起案子嗡午,更是在濱河造成了極大的恐慌,老刑警劉巖冀痕,帶你破解...
    沈念sama閱讀 216,997評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件荔睹,死亡現(xiàn)場離奇詭異,居然都是意外死亡言蛇,警方通過查閱死者的電腦和手機僻他,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,603評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來猜极,“玉大人中姜,你說我怎么就攤上這事「” “怎么了丢胚?”我有些...
    開封第一講書人閱讀 163,359評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長受扳。 經(jīng)常有香客問我携龟,道長,這世上最難降的妖魔是什么勘高? 我笑而不...
    開封第一講書人閱讀 58,309評論 1 292
  • 正文 為了忘掉前任峡蟋,我火速辦了婚禮,結(jié)果婚禮上华望,老公的妹妹穿的比我還像新娘蕊蝗。我一直安慰自己,他們只是感情好赖舟,可當(dāng)我...
    茶點故事閱讀 67,346評論 6 390
  • 文/花漫 我一把揭開白布蓬戚。 她就那樣靜靜地躺著,像睡著了一般宾抓。 火紅的嫁衣襯著肌膚如雪子漩。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,258評論 1 300
  • 那天石洗,我揣著相機與錄音幢泼,去河邊找鬼。 笑死讲衫,一個胖子當(dāng)著我的面吹牛缕棵,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 40,122評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼挥吵,長吁一口氣:“原來是場噩夢啊……” “哼重父!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起忽匈,我...
    開封第一講書人閱讀 38,970評論 0 275
  • 序言:老撾萬榮一對情侶失蹤房午,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后丹允,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體郭厌,經(jīng)...
    沈念sama閱讀 45,403評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,596評論 3 334
  • 正文 我和宋清朗相戀三年雕蔽,在試婚紗的時候發(fā)現(xiàn)自己被綠了折柠。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,769評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡批狐,死狀恐怖扇售,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情嚣艇,我是刑警寧澤承冰,帶...
    沈念sama閱讀 35,464評論 5 344
  • 正文 年R本政府宣布,位于F島的核電站食零,受9級特大地震影響困乒,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜贰谣,卻給世界環(huán)境...
    茶點故事閱讀 41,075評論 3 327
  • 文/蒙蒙 一娜搂、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧吱抚,春花似錦百宇、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,705評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至憋肖,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間婚苹,已是汗流浹背岸更。 一陣腳步聲響...
    開封第一講書人閱讀 32,848評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留膊升,地道東北人怎炊。 一個月前我還...
    沈念sama閱讀 47,831評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親评肆。 傳聞我的和親對象是個殘疾皇子债查,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,678評論 2 354

推薦閱讀更多精彩內(nèi)容