代理模式实现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阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,431评论 25 707
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,594评论 18 139
  • 引子 这篇文章会告诉你 什么是路由,是为了解决什么问题才产生的 业界现状是怎么样的,我们可以做什么来优化当前的问题...
    信念着了火阅读 35,360评论 50 237
  • by海灵格 婚姻是一个很大的教导,它是一个学习的机会,学习说依赖并不是爱,依赖意味着冲突、愤怒、恨、嫉妒、占有、和...
    Freesia阅读 461评论 0 1
  • 看乐评,看唱片销量,看巡演成绩,看社交网络影响力每个方面都可以评选出不同的冠军。 但多种音乐奖项无疑是各种综合的体...
    艺麓轩阅读 371评论 0 0