代理模式實現Android路由框架

在項目的開發過程中,我們可能會遇到一些很重的App,涉及到很多業務線,N個團隊的共同開發。這個時候,如果還是單app的開發方式的話,可能會導致開發中的編譯時間很長,每個團隊之間的代碼互相存在干擾。這個時候,可以考慮組件化開發。

所謂組件化開發,其實主要就是將一個module拆分成多個module,每個團隊負責自己的module,在發布時再將所有的module合并到一起發布。

這種分module的方式比起普通的分模塊的方式的好處就在于:不同的module之間是互相隔離的,只能通過接口訪問,能更好的保證:高內聚低耦合。開發時,每個module都可以獨立打包安裝,編譯時間短。各個module的res資源是隔離的,不像以前都是混在一起,一旦哪個module不需要了,很方便將代碼和資源文件一起干掉,避免代碼腐爛。

上面說了組件化開發,那路由框架和組件化開發有什么關系呢?上面我們知道,不同的組件之間是互相隔離的,那當需要從一個組件的頁面跳轉到另一個組件的頁面時,該怎么辦呢?這個時候就需要路由框架了,組件之間不直接打交道,而是通過路由來轉發。這樣方便我們實行動態跳轉、攔截、統一轉發等額外操作。

下面是我模仿Retrofit的代理模式來實現的一套簡單的Android路由框架。主要是參考的Android輕量級路由框架LiteRouter,不過個別地方根據自己的習慣做了點改動。雖然是模仿,但在自己敲的過程中,對Retrofit的代理模式又有了更深的理解,同時,LiteRouter的攔截器很簡單,只有一個,我這里又模仿OkHttp的責任鏈模式對它的攔截器做了簡單的拓展,支持多個攔截器,在實現的過程中,又過了一遍OkHttp的源碼,受益匪淺。不得不說,多看源碼真的挺有好處的,大家不妨也去看一看,第一次看不懂很正常,多看幾次就有感覺了,碼讀百遍,其意自現。

使用方法

1、定義接口

/**
 * 定義Intent之間跳轉的協議
 * <p>
 * 作者:余天然 on 2017/5/25 上午11:21
 */
public interface IntentService {

    @ClassName("com.soubu.routerdemo.Demo1Activity")
    void gotoDemo1(Context context);

    @ClassName("com.soubu.routerdemo2.Demo2Activity")
    void gotoDemo2(Context context, @Key("request") String request, @Key("response") String response);

}

2、創建Router實例,可以按需要添加攔截器

package com.soubu.routerhost;

import com.soubu.router.IntentRouter;

/**
 * 作者:余天然 on 2017/5/25 下午5:43
 */
public class IntentClient {

    public static IntentClient instance;

    private IntentRouter intentRouter;

    /**
     * 單例
     */
    public static IntentClient getInstance() {
        if (instance == null) {
            instance = new IntentClient();
        }
        return instance;
    }

    /**
     * 創建頁面跳轉輔助類
     */
    private IntentClient() {
        //通過動態代理,獲得具體的跳轉協議實現類,可自定義攔截器
        intentRouter = new IntentRouter.Builder()
                .addInterceptor(new Interceptor1())
                .addInterceptor(new Interceptor2())
                .addInterceptor(new Interceptor3())
                .build();
    }

    /**
     * 創建各個模塊的頁面接口
     */
    public IntentService intentService() {
        return intentRouter.create(IntentService.class);
    }

}

攔截器,可以統一對Intent做一些額外的處理

/**
 * Intent攔截器
 * 
 * 作者:余天然 on 2017/5/25 下午4:05
 */
public class Interceptor1 implements Interceptor {
    @Override
    public IntentWrapper intercept(Chain chain) {
        IntentWrapper request = chain.request();
        LogUtil.print("處理請求-1");
        Bundle reqExtras = request.getIntent().getExtras();
        reqExtras.putString("request", "1");
        request.getIntent().putExtras(reqExtras);
        IntentWrapper response = chain.process(request);
        LogUtil.print("處理響應-1");
        Bundle respExtras = response.getIntent().getExtras();
        respExtras.putString("response", "1");
        response.getIntent().putExtras(respExtras);
        return response;
    }
}

3、在每個module中,通過Router進行跳轉

IntentClient.getInstance()
        .intentService()
        .gotoDemo1(activity);

源碼流程

在創建Router實例時,通過動態代理創建了我們定義的跳轉接口的實現類。

/**
 * 作者:余天然 on 2017/5/25 上午11:06
 */
public class IntentRouter {

    private List<Interceptor> interceptors;

    IntentRouter(List<Interceptor> interceptors) {
        this.interceptors = interceptors;
    }

    /**
     * create router class service
     *
     * @param service router class
     * @param <T>
     * @return
     */
    public <T> T create(final Class<T> service) {
        return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[]{service},
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, final Method method, final Object... args) throws Throwable {
                        //責任鏈模式,方便定制攔截器
                        Parser parser = new Parser();
                        IntentWrapper request = parser.parse(method, args);
                        Interceptor.Chain chain = new RealInterceptorChain(request, interceptors, 0);
                        IntentWrapper response = chain.process(request);
                        response.start();
                        //設置返回值,不過感覺沒卵用
                        Class returnTYpe = method.getReturnType();
                        if (returnTYpe == void.class) {
                            return null;
                        } else if (returnTYpe == IntentWrapper.class) {
                            return response;
                        }
                        throw new RuntimeException("method return type only support 'void' or 'IntentWrapper'");
                    }
                });
    }

    public static final class Builder {
        private List<Interceptor> interceptors;

        public Builder() {
            this.interceptors = new ArrayList<>();
        }

        public Builder addInterceptor(Interceptor interceptor) {
            this.interceptors.add(interceptor);
            return this;
        }

        public IntentRouter build() {
            interceptors.add(new DefaultInterceptor());
            return new IntentRouter(interceptors);
        }
    }
}

在動態代理的invoke方法中,通過一個反射解析器來獲取接口的方法的注解信息,根據這些注解自動幫其創建Intent,避免重復的體力活。

/**
 * 作者:余天然 on 2017/5/25 上午11:56
 */
public class Parser {

    private int requestCode;

    private String className;
    private int mFlags;
    private Bundle bundleExtra;

    private Context context;

    public Parser() {

    }

    public Parser addFlags(int flags) {
        mFlags |= flags;
        return this;
    }

    public IntentWrapper parse(Method method, Object... args) {
        // 解析方法注解
        parseMethodAnnotations(method);
        // 解析參數注解
        parseParameterAnnotations(method, args);
        //創建Intent
        Intent intent = new Intent();
        intent.setClassName(context, className);
        intent.putExtras(bundleExtra);
        intent.addFlags(mFlags);
        requestCode = method.isAnnotationPresent(RequestCode.class) ? requestCode : -1;
        return new IntentWrapper(context, intent, requestCode);
    }

    /**
     * 解析參數注解
     */
    private void parseParameterAnnotations(Method method, Object[] args) {
        //參數類型
        Type[] types = method.getGenericParameterTypes();
        // 參數名稱
        Annotation[][] parameterAnnotationsArray = method.getParameterAnnotations();
        bundleExtra = new Bundle();
        for (int i = 0; i < types.length; i++) {
            // key
            String key = null;
            Annotation[] parameterAnnotations = parameterAnnotationsArray[i];
            for (Annotation annotation : parameterAnnotations) {
                if (annotation instanceof Key) {
                    key = ((Key) annotation).value();
                    break;
                }
            }
            parseParameter(bundleExtra, types[i], key, args[i]);
        }
        if (context == null) {
            throw new RuntimeException("Context不能為空");
        }
    }

    /**
     * 解析方法注解
     */
    void parseMethodAnnotations(Method method) {
        Annotation[] methodAnnotations = method.getAnnotations();
        for (Annotation annotation : methodAnnotations) {
            if (annotation instanceof ClassName) {
                ClassName className = (ClassName) annotation;
                this.className = className.value();
            } else if (annotation instanceof RequestCode) {
                RequestCode requestCode = (RequestCode) annotation;
                this.requestCode = requestCode.value();
            }
        }
        if (className == null) {
            throw new RuntimeException("JumpTo annotation is required.");
        }
    }

    /**
     * 解析參數注解
     *
     * @param bundleExtra 存儲的Bundle
     * @param type        參數類型
     * @param key         參數名稱
     * @param arg         參數值
     */
    void parseParameter(Bundle bundleExtra, Type type, String key, Object arg) {
        Class<?> rawParameterType = getRawType(type);
        if (rawParameterType == Context.class) {
            context = (Context) arg;
        }
        if (rawParameterType == String.class) {
            bundleExtra.putString(key, arg.toString());
        } else if (rawParameterType == String[].class) {
            bundleExtra.putStringArray(key, (String[]) arg);
        } else if (rawParameterType == int.class || rawParameterType == Integer.class) {
            bundleExtra.putInt(key, Integer.parseInt(arg.toString()));
        } else if (rawParameterType == int[].class || rawParameterType == Integer[].class) {
            bundleExtra.putIntArray(key, (int[]) arg);
        } else if (rawParameterType == short.class || rawParameterType == Short.class) {
            bundleExtra.putShort(key, Short.parseShort(arg.toString()));
        } else if (rawParameterType == short[].class || rawParameterType == Short[].class) {
            bundleExtra.putShortArray(key, (short[]) arg);
        } else if (rawParameterType == long.class || rawParameterType == Long.class) {
            bundleExtra.putLong(key, Long.parseLong(arg.toString()));
        } else if (rawParameterType == long[].class || rawParameterType == Long[].class) {
            bundleExtra.putLongArray(key, (long[]) arg);
        } else if (rawParameterType == char.class) {
            bundleExtra.putChar(key, arg.toString().toCharArray()[0]);
        } else if (rawParameterType == char[].class) {
            bundleExtra.putCharArray(key, arg.toString().toCharArray());
        } else if (rawParameterType == double.class || rawParameterType == Double.class) {
            bundleExtra.putDouble(key, Double.parseDouble(arg.toString()));
        } else if (rawParameterType == double[].class || rawParameterType == Double[].class) {
            bundleExtra.putDoubleArray(key, (double[]) arg);
        } else if (rawParameterType == float.class || rawParameterType == Float.class) {
            bundleExtra.putFloat(key, Float.parseFloat(arg.toString()));
        } else if (rawParameterType == float[].class || rawParameterType == Float[].class) {
            bundleExtra.putFloatArray(key, (float[]) arg);
        } else if (rawParameterType == byte.class || rawParameterType == Byte.class) {
            bundleExtra.putByte(key, Byte.parseByte(arg.toString()));
        } else if (rawParameterType == byte[].class || rawParameterType == Byte[].class) {
            bundleExtra.putByteArray(key, (byte[]) arg);
        } else if (rawParameterType == boolean.class || rawParameterType == Boolean.class) {
            bundleExtra.putBoolean(key, Boolean.parseBoolean(arg.toString()));
        } else if (rawParameterType == boolean[].class || rawParameterType == Boolean[].class) {
            bundleExtra.putBooleanArray(key, (boolean[]) arg);
        } else if (rawParameterType == Bundle.class) {
            if (TextUtils.isEmpty(key)) {
                bundleExtra.putAll((Bundle) arg);
            } else {
                bundleExtra.putBundle(key, (Bundle) arg);
            }
        } else if (rawParameterType == SparseArray.class) {
            if (type instanceof ParameterizedType) {
                ParameterizedType parameterizedType = (ParameterizedType) type;
                Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
                Type actualTypeArgument = actualTypeArguments[0];

                if (actualTypeArgument instanceof Class) {
                    Class<?>[] interfaces = ((Class) actualTypeArgument).getInterfaces();
                    for (Class<?> interfaceClass : interfaces) {
                        if (interfaceClass == Parcelable.class) {
                            bundleExtra.putSparseParcelableArray(key, (SparseArray<Parcelable>) arg);
                            return;
                        }
                    }
                    throw new RuntimeException("SparseArray的泛型必須實現Parcelable接口");
                }
            } else {
                throw new RuntimeException("SparseArray的泛型必須實現Parcelable接口");
            }
        } else if (rawParameterType == ArrayList.class) {
            if (type instanceof ParameterizedType) {
                ParameterizedType parameterizedType = (ParameterizedType) type;
                Type[] actualTypeArguments = parameterizedType.getActualTypeArguments(); // 泛型類型數組
                if (actualTypeArguments == null || actualTypeArguments.length != 1) {
                    throw new RuntimeException("ArrayList的泛型必須實現Parcelable接口");
                }

                Type actualTypeArgument = actualTypeArguments[0]; // 獲取第一個泛型類型
                if (actualTypeArgument == String.class) {
                    bundleExtra.putStringArrayList(key, (ArrayList<String>) arg);
                } else if (actualTypeArgument == Integer.class) {
                    bundleExtra.putIntegerArrayList(key, (ArrayList<Integer>) arg);
                } else if (actualTypeArgument == CharSequence.class) {
                    bundleExtra.putCharSequenceArrayList(key, (ArrayList<CharSequence>) arg);
                } else if (actualTypeArgument instanceof Class) {
                    Class<?>[] interfaces = ((Class) actualTypeArgument).getInterfaces();
                    for (Class<?> interfaceClass : interfaces) {
                        if (interfaceClass == Parcelable.class) {
                            bundleExtra.putParcelableArrayList(key, (ArrayList<Parcelable>) arg);
                            return;
                        }
                    }
                    throw new RuntimeException("ArrayList的泛型必須實現Parcelable接口");
                }
            } else {
                throw new RuntimeException("ArrayList的泛型必須實現Parcelable接口");
            }
        } else {
            if (rawParameterType.isArray()) // Parcelable[]
            {
                Class<?>[] interfaces = rawParameterType.getComponentType().getInterfaces();
                for (Class<?> interfaceClass : interfaces) {
                    if (interfaceClass == Parcelable.class) {
                        bundleExtra.putParcelableArray(key, (Parcelable[]) arg);
                        return;
                    }
                }
                throw new RuntimeException("Object[]數組中的對象必須全部實現了Parcelable接口");
            } else // 其他接口
            {
                Class<?>[] interfaces = rawParameterType.getInterfaces();
                for (Class<?> interfaceClass : interfaces) {
                    if (interfaceClass == Serializable.class) {
                        bundleExtra.putSerializable(key, (Serializable) arg);
                    } else if (interfaceClass == Parcelable.class) {
                        bundleExtra.putParcelable(key, (Parcelable) arg);
                    } else {
                        throw new RuntimeException("Bundle不支持的類型, 參數: " + key);
                    }
                }
            }

        }
    }

    /**
     * 獲取返回類型
     *
     * @param type
     * @return
     */
    Class<?> getRawType(Type type) {
        if (type == null) throw new NullPointerException("type == null");

        if (type instanceof Class<?>) {
            // Type is a normal class.
            return (Class<?>) type;
        }
        if (type instanceof ParameterizedType) {
            ParameterizedType parameterizedType = (ParameterizedType) type;

            // I'm not exactly sure why getRawType() returns Type instead of Class. Neal isn't either but
            // suspects some pathological case related to nested classes exists.
            Type rawType = parameterizedType.getRawType();
            if (!(rawType instanceof Class)) throw new IllegalArgumentException();
            return (Class<?>) rawType;
        }
        if (type instanceof GenericArrayType) {
            Type componentType = ((GenericArrayType) type).getGenericComponentType();
            return Array.newInstance(getRawType(componentType), 0).getClass();
        }
        if (type instanceof TypeVariable) {
            // We could use the variable's bounds, but that won't work if there are multiple. Having a raw
            // type that's more general than necessary is okay.
            return Object.class;
        }
        if (type instanceof WildcardType) {
            return getRawType(((WildcardType) type).getUpperBounds()[0]);
        }

        throw new IllegalArgumentException("Expected a Class, ParameterizedType, or "
                + "GenericArrayType, but <" + type + "> is of type " + type.getClass().getName());
    }
}

這里的Intent我是用一個IntentWrapper包裝了一下,主要就是傳入了context對象,方便startActivity等方法的統一調用

public class IntentWrapper {

    private Context context;
    private Intent intent;
    private int requestCode = -1;

    public IntentWrapper(Context context, Intent intent, int requestCode) {
        this.context = context;
        this.intent = intent;
        this.requestCode = requestCode;
    }

    public Intent getIntent() {
        return intent;
    }

    public Context getContext() {
        return context;
    }

    public int getRequestCode() {
        return requestCode;
    }

    public void start() {
        if (requestCode == -1) {
            startActivity();
        } else {
            startActivityForResult(requestCode);
        }
    }

    public void startActivity() {
        if (!(context instanceof Activity)) {
            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        }
        context.startActivity(intent);
    }

    public void startActivityForResult(int requestCode) {
        if (!(context instanceof Activity)) {
            throw new RuntimeException("startActivityForResult only works for activity context");
        }
        ((Activity) context).startActivityForResult(intent, requestCode);
    }

}

其實不要攔截器,也是完全可以的。但是為了擁抱變化(其實是為了裝逼),我們對Parser創建的IntenWrapper又做了一點點加工,在中間添加了一道責任鏈,雖然這里輸入的是IntenWrapper,輸出的也是IntenWrapper,但是,這中間我們是可以自己做一點額外的處理的,比如給每個Intent添加一個額外的參數、統一修改Intent的信息。
為了大家更對OkHttp中的責任鏈有一個更直觀的感受,我這里插播一張圖


看上圖,我們可以定義多個攔截器:RetryAndFollowupInterceptor來負責失敗重試和重定向、BridgeInterceptor來處理request和response的header里面的信息、CacheInterceptor來實現各種緩存策略、ConnectInterceptor來建立與服務器的連接、CallServerInterceptor來與服務器交互數據。這幾個其實是OkHttp自帶的攔截器,其實我們在開發時,通常會額外的加幾個自定義的攔截器,比如對token的統一處理啊、網絡日志的打印啊等等。

攔截器的設計,可以像工廠流水線一樣,傳遞用戶發起的請求 Request,每一個攔截器完成相應的功能,從失敗重試和重定向實現、請求頭的修改和Cookie 的處理,緩存的處理,建立 TCP 和 SSH 連接,發送 Request 和讀取 Response,每一個環節由專門的 Interceptor 負責。

定義Interceptor和Chain的接口。

public interface Interceptor {
    IntentWrapper intercept(Chain chain);

    interface Chain {
        IntentWrapper request();

        IntentWrapper process(IntentWrapper request);
    }
}

自定義RealInterceptorChain實現Chain接口,其實就是內部維護了一個List<Interceptor>,源碼比較簡單,主要功能就是將多個Interceptor合并成一串。

/**
 * 作者:余天然 on 2017/5/25 下午4:35
 */
public class RealInterceptorChain implements Interceptor.Chain {

    private IntentWrapper originalRequest;
    private List<Interceptor> interceptors;
    private int index;

    public RealInterceptorChain(IntentWrapper request, List<Interceptor> interceptors, int index) {
        this.originalRequest = request;
        this.interceptors = interceptors;
        this.index = index;
    }

    @Override
    public IntentWrapper request() {
        return originalRequest;
    }

    @Override
    public IntentWrapper process(IntentWrapper request) {
        Interceptor interceptor = interceptors.get(index);
        RealInterceptorChain next = new RealInterceptorChain(request, interceptors, index + 1);
        IntentWrapper response = interceptor.intercept(next);
        return response;
    }
}

默認的攔截器,其實啥都沒做,主要就是方便RealInterceptorChain中至少有一個攔截器可以傳輸數據,但是總覺得這里有點怪,大家要是有更好的方法的話可以省掉這個類的話,歡迎提出來。

public class DefaultInterceptor implements Interceptor {

    @Override
    public IntentWrapper intercept(Chain chain) {
        return chain.request();
    }

}

在我們定義接口的時候,其實還用到了幾個注解:@ClassName、@Key、@RequestCode。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ClassName {
    String value();
}

這里只發一個吧,另外兩個和它除了名字不一樣都是一樣的,就不發了,免得大家說我湊字數。

以上就是這個路由框架的實現思路了,其實這個框架的實用性不是很大,因為現在阿里巴巴已經開源了一個ARouter,這個比我們這個更好一點,它沒有反射,也沒有接口定義類,而是在每個Activity上添加注解,然后通過apt在編譯時獲取這些注解信息,動態創建一個輔助類來實現路由的。我們這個框架是用的運行時注解+反射,性能上比不上編譯時注解,并且,我們的接口定義類不知道放在哪個module比較好,它依賴了router框架,同時又被所有的module都依賴。

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

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,677評論 25 708
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,799評論 18 139
  • 引子 這篇文章會告訴你 什么是路由,是為了解決什么問題才產生的 業界現狀是怎么樣的,我們可以做什么來優化當前的問題...
    信念著了火閱讀 35,459評論 50 237
  • by海靈格 婚姻是一個很大的教導,它是一個學習的機會,學習說依賴并不是愛,依賴意味著沖突、憤怒、恨、嫉妒、占有、和...
    Freesia閱讀 482評論 0 1
  • 看樂評,看唱片銷量,看巡演成績,看社交網絡影響力每個方面都可以評選出不同的冠軍。 但多種音樂獎項無疑是各種綜合的體...
    藝麓軒閱讀 394評論 0 0