Contents
  1. 项目GitHub地址:https://github.com/ms-liu/AndroidRemoteDemo
  2. 一、项目为什么要分成多Module
  3. 二、解决Module之间页面跳转问题
    1. 1、问题分析
    2. 2、问题解决
    3. 3、具体代码编写
      1. (一)定义操作接口
      2. (二)基础实现类
      3. (三)具体Activity这种处理
      4. (四)Manager管理者编写
      5. (五)代言者——Remote编写
      6. (六)测试
  4. (三)拔高与提升
    1. 1、创建RemoteAnnotation Module
    2. 2、创建RemoteCompiler Module
    3. 3、具体调用
  5. 四、总结
  6. 实际代码请下载或者Frok项目。
  7. 项目GitHub地址:https://github.com/ms-liu/AndroidRemoteDemo
  8. 欢迎大家给出中肯的建议和提高意见,你的鼓励将是我最大的动力。
  9. 个人邮箱:ms_liu163@163.com

我过我要的生活
不是生活过我就好
只要是我想要的
期待多久都有情调              

开篇推荐一首歌曲,送给一直默默奋斗、坚持,但有时又感到迷茫的你。猛戳《过我的生活》


项目GitHub地址:https://github.com/ms-liu/AndroidRemoteDemo

一、项目为什么要分成多Module

  先感受下传统的两种划分模式:业务功能分包、不同组件分包。
业务功能划分

不同组件划分
  这两种分包都是放在一个App这一个Module里面的。
  每次编译往往都是需要将项目的所有业务都编译一遍,过程繁重。对于单元测试也只是想想就好,因为和普通测试根本没差。另外经常在开发人员之间发生的事情,就是为谁修改了谁的代码而发生互怼事件 [斜眼笑]
  总体体现:

  • 编译困难
  • 单元测试困难
  • 开发、维护困难

  因此将项目分成多个Module是很有必要的,这样可以:

  • 提高灵活性——每次只需要编译我们自己想要的Module即可,同时提升编译速度。
  • 方便单元测试——可以轻松的针对每个Module作出单元测试。
  • 让职责分工明确——可以更好的让开发人员去维护各自的模块

多Module划分

二、解决Module之间页面跳转问题

1、问题分析

  项目被分成多Module之后,由于项目业务往往是交叉的,所以Module当中包含的页面跳转往往也是相互的。
  为了能够让页面Module之间的页面能够相互跳转,我们往往需要将每个Module之间相互compile,这样无异于又增加了模块之间耦合性,并且还有可能会报出一个循环编译的警告(warning recycle compile)

一般多模块之间关系

2、问题解决

  通过参考Web页面的跳转方式,不难想到的是我们完全可以仿照Web跳转,也先去为每一个Activity注册一个地址,然后,然后…..(PS:然后就没有思路了,MDZZ)。这时看图一张:

多Module路由关系

  首先去关心每一个需要被其它Module调用的页面,我们将它配置到Common Module中的Remote Configure文件。
  下面我们就是在Remote模块当中编写一些具体Router Operator类。下面我们就需要思考对一个路由地址究竟需要进行怎样的处理了。

  • 我们需要拿到这个地址(注册)——通过put/add方法
  • 我们需要让路由地址生效(调用)——通过invoke方法
3、具体代码编写
(一)定义操作接口

核心方法就是put和invoke

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
* ==============================================
* <p>
* 类名:IOperator
* &nbsp 基本操作接口
* <p>
* 作者:M-Liu
* <p>
* 时间:2017/3/27
* <p>
* 邮箱:ms_liu163@163.com
* <p>
* ==============================================
*/
public interface IOperator<T,K> {
/**
* 添加路由地址
* @param uri 路由地址
* @param clazz 路由类型
*/
void put(String uri,Class<T> clazz);
/**
* 执行路由路线
* @param context Context
* @param uri 路由地址
* @return {@link BaseIntentOperator#invoke(Context, String)}
*/
K invoke(Context context,String uri);
/**
* 检查当前路由路线 是否存在
* @param uri 路由地址
* @return
*/
boolean check(String uri);
}
(二)基础实现类

因为是Demo讲解,所以只是针对Intent这一种

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
/**
* ==============================================
* <p>
* 类名:BaseIntentOperator
* &nbsp 返回类型是Intent的基础操作类
* <p>
* 作者:M-Liu
* <p>
* 时间:2017/3/27
* <p>
* 邮箱:ms_liu163@163.com
* <p>
* ==============================================
*/
public abstract class BaseIntentOperator<T> implements IOperator<T,Intent> {
private HashMap<String,Class<T>> mIntentContainer;
public BaseIntentOperator(){
mIntentContainer = new LinkedHashMap<>();
}
/**
* {@inheritDoc}
*/
@Override
public void put(String uri, Class<T> clazz) {
if (mIntentContainer != null){
mIntentContainer.put(uri,clazz);
}
}
/**
* {@inheritDoc}
*/
@Override
public Intent invoke(Context context, String uri) {
Class<T> clazz = null;
if (check(uri)){
clazz = mIntentContainer.get(uri);
}
if (clazz == null){
throwException(uri);
}
return new Intent(context,clazz);
}
public abstract void throwException(String uri);
/**
* {@inheritDoc}
*/
@Override
public boolean check(String uri) {
return mIntentContainer != null && mIntentContainer.keySet().contains(uri);
}
}

(三)具体Activity这种处理

  当中定义了PROTOCOL 常量,相当于Http这种协议,在Demo中是预留,因为没有处理Service等其它组件的情况,所以没有用到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* ==============================================
* <p>
* 类名:ActivityIntentOperator
* &nbsp 针对Activity路由操作
* <p>
* 作者:M-Liu
* <p>
* 时间:2017/3/28
* <p>
* 邮箱:ms_liu163@163.com
* <p>
* ==============================================
*/
public class ActivityIntentOperator extends BaseIntentOperator<AppCompatActivity>
{
public static final String PROTOCOL = "activity://";
@Override
public void throwException(String uri) {
throw new NotFoundRuleException(ActivityIntentOperator.class.getCanonicalName(),uri);
}
}
(四)Manager管理者编写
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
public class RemoteOperatorManager {
private static RemoteOperatorManager mRemoteManager;
//路由操作管理池
private HashMap<String,IOperator> mOperatorPool;
private RemoteOperatorManager(){
mOperatorPool = new LinkedHashMap<>();
putDefaultOperator();
}
//初始化默认路由操作
private void putDefaultOperator() {
if (mOperatorPool != null){
mOperatorPool.put(ActivityIntentOperator.PROTOCOL,new ActivityIntentOperator());
}
}
/**
* 获取RemoteOperatorManager
* @return RemoteOperatorManager
*/
public static RemoteOperatorManager get(){
if (mRemoteManager == null){
synchronized (RemoteOperatorManager.class){
mRemoteManager = new RemoteOperatorManager();
}
}
return mRemoteManager;
}
/**
* 添加自定义 路由操作
* @param protocol 路由协议 {@link ActivityIntentOperator#PROTOCOL}
* @param operator 具体操作类
*/
public RemoteOperatorManager putCustomOperator(String protocol,IOperator operator){
if (mOperatorPool != null){
mOperatorPool.put(protocol,operator);
}
return mRemoteManager;
}
/**
* 检查当前路由操作 是否存在
* @param uri 路由地址
* @return false 不存在 true 存在
*/
public boolean checkOperatorForURI(String uri){
if (!TextUtils.isEmpty(uri)){
IOperator<?, ?> operator = getOperator(uri);
if (operator == null){
throw new NotFoundRuleException(uri);
}
return true;
}else {
throw new NotFountRemotePathException();
}
}
public boolean checkOpratorForProtocol(String protocol){
return mOperatorPool != null && mOperatorPool.keySet().contains(protocol);
}
/**
* 根据Uri获取路由操作类
* @param uri 路由地址
*/
public <T,V> IOperator<T,V> getOperator(String uri){
IOperator<T,V> operator = null;
if (mOperatorPool != null){
Set<String> protocols = mOperatorPool.keySet();
for (String protocol :
protocols) {
if (uri.startsWith(protocol)){
operator = mOperatorPool.get(protocol);
break;
}
}
}
return operator;
}
public <T> RemoteOperatorManager putRemoteUri(String uri, Class<T> clazz) {
if (checkOperatorForURI(uri)){
IOperator<T, ?> operator = getOperator(uri);
operator.put(uri,clazz);
}
return mRemoteManager;
}
}
(五)代言者——Remote编写

为什么是代言者,是因为其实每一个具体方法都是由Manager去完成的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class Remote {
private final static String PATTERN = "";
/**
* 添加自定义路由操作
* @param protocol 路由协议
* @param operator 路由操作类
* @return
*/
public static RemoteOperatorManager putCoustomOprator(String protocol, IOperator operator){
return RemoteOperatorManager.get().putCustomOperator(protocol,operator);
}
/**
* 添加路由地址
* @param uri 路由地址
* @return
*/
public static<T> RemoteOperatorManager putRemoteUri(String uri,Class<T> clazz){
return RemoteOperatorManager.get().putRemoteUri(uri,clazz);
}
/**
* 启用路由地址
* @param ctx Context
* @param uri 路由地址
* @return
*/
public static <V> V invoke(Context ctx, String uri){
if (checkUri(uri)){
IOperator<?, V> operator = RemoteOperatorManager.get().getOperator(uri);
return operator.invoke(ctx,uri);
}else {
throw new NotFoundRuleException(uri);
}
}
/**
* 路由地址检查
* @param uri 路由地址
* @return
*/
public static boolean checkUri(String uri){
return RemoteOperatorManager.get().checkOperatorForURI(uri);
}
}

(六)测试
  • 在Application中添加、注册路由

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class RemoteApp extends Application {
    @Override
    public void onCreate() {
    super.onCreate();
    initRemote();
    }
    private void initRemote() {
    Remote.putRemoteUri(ActivityIntentOperator.PROTOCOL+ IRemoteUrlConfig.LOGIN_REMOTE_URL, LoginActivity.class);
    }
    }
  • 在MainActivity中调用,并执行跳转

    1
    2
    Intent invoke = Remote.invoke(MainActivity.this, ActivityIntentOperator.PROTOCOL + IRemoteUrlConfig.LOGIN_REMOTE_URL);
    startActivity(invoke);

  至此,我们已经完成了一个路由框架,已经可以解决多Module之间,页面跳转问题。




(三)拔高与提升

  如果已经消化完上面内容,那么就可以再跟随我们做一些拔高与提升了。
  下面我们将会使整个框架提升到通过注解Annotation的形式,来完成所有的工作,达到简化使用的目的。

1、创建RemoteAnnotation Module
  • 创建注解@Module——指明当前页面所在Module
    1
    2
    3
    4
    5
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.CLASS)
    public @interface Module {
    String value()default "";
    }
  • 创建注解@Modules——指明当前有多少个Module
    1
    2
    3
    4
    5
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.CLASS)
    public @interface Modules {
    String[]value();
    }
2、创建RemoteCompiler Module
  • 创建配置文件
1
2
3
4
5
6
7
public class Config {
public static final String PACKAGE_NAME = "com.mmyz.router";
public static final String PAGE_PREFIX = "Module_";
public static final String CLASS_NAME = "AutoRegisterRemote";
public static final String METHOD_NAME = "autoRegister";
public static final String PAGE_METHOD_NAME = "autoInvoke";
}
  • 创建RemoteProcessor——注解处理类
    对此处不太了解的可以移步到 http://hannesdorfmann.com/annotation-processing/annotationprocessing101

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    @AutoService(Processor.class)
    public class RemoteProcessor extends AbstractProcessor {
    private Filer mFiler;
    private Messager mMessager;
    private List<String> mStaticRemoteUriList = new ArrayList<>();
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
    super.init(processingEnvironment);
    //文件操作
    mFiler = processingEnvironment.getFiler();
    //消息输出
    mMessager = processingEnvironment.getMessager();
    }
    /**
    * 当前java 源码版本
    * java compiler version
    * @return
    */
    @Override
    public SourceVersion getSupportedSourceVersion() {
    return SourceVersion.latest();
    }
    /**
    * 指明需要关心的注解
    * need handle Annotation type
    * @return
    */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
    Set<String> types = new HashSet<>();
    types.add(Modules.class.getCanonicalName());
    types.add(Module.class.getCanonicalName());
    types.add(StaticRemote.class.getCanonicalName());
    return types;
    }
    /**
    * 具体处理
    * @param set
    * @param re
    * @return
    */
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment re) {
    //清除URL维护集合
    mStaticRemoteUriList.clear();
    try {
    Set<? extends Element> modules = re.getElementsAnnotatedWith(Modules.class);
    if (modules != null && !modules.isEmpty()){
    patchModulesClass(modules);
    return true;
    }
    processModule(re);
    }catch (Exception e){
    mMessager.printMessage(Diagnostic.Kind.NOTE,e.getMessage());
    }
    return true;
    }
    private void processModule(RoundEnvironment re) {
    try {
    Set<? extends Element> staticElementSet = re.getElementsAnnotatedWith(StaticRemote.class);
    if (staticElementSet != null && !staticElementSet.isEmpty()) {
    for (Element e :
    staticElementSet) {
    if (!(e instanceof TypeElement)) {
    continue;
    }
    TypeElement te = (TypeElement) e;
    mStaticRemoteUriList.add(te.getAnnotation(StaticRemote.class).value());
    }
    }
    Set<? extends Element> module = re.getElementsAnnotatedWith(Module.class);
    patchModuleClass(module);
    }catch (Exception e) {
    mMessager.printMessage(Diagnostic.Kind.NOTE,e.getMessage());
    }
    }
    /**
    * 创建class文件
    * create class
    *
    * package com.mmyz.router;
    *
    * public class Page_Login(){
    * public static autoInvoke(){
    * Remote.putRemoteUriDefaultPattern("activity://com.mmyz.account.LoginActivity");
    * }
    * }
    *
    */
    private void patchModuleClass(Set<? extends Element> module) {
    try {
    if (module == null || module.isEmpty())
    return;
    mMessager.printMessage(Diagnostic.Kind.NOTE,module.toString());
    Element next = module.iterator().next();
    Module annotation = next.getAnnotation(Module.class);
    String pageName = annotation.value();
    String className = Config.PAGE_PREFIX+pageName;
    JavaFileObject file = mFiler.createSourceFile(className, next);
    PrintWriter printWriter = new PrintWriter(file.openWriter());
    printWriter.println("package "+ Config.PACKAGE_NAME +";");
    printWriter.println("import "+ Config.PACKAGE_NAME+".Remote;");
    printWriter.println("import "+ Config.PACKAGE_NAME+".exception.NotFoundClassException;");
    printWriter.println("public class "+className +" {");
    printWriter.println("public static void "+ Config.PAGE_METHOD_NAME+"(){");
    printWriter.println("try{");
    for (String uri :
    mStaticRemoteUriList) {
    printWriter.println("Remote.putRemoteUriDefaultPattern(\""+uri+"\");");
    }
    printWriter.println("}catch(NotFoundClassException e){");
    printWriter.println("e.printStackTrace();");
    printWriter.println("}");
    printWriter.println("}");
    printWriter.println("}");
    printWriter.flush();
    printWriter.close();
    } catch (Exception e) {
    e.printStackTrace();
    }
    }
    /**
    * 创建class文件
    * Create class
    *
    * package com.mmyz.remote;
    * public class AutoRegisterRemote{
    * public void autoRegister(){
    * Page_Login.autoInvoke();
    * }
    * }
    */
    private void patchModulesClass(Set<? extends Element> modules) {
    try {
    TypeElement moduleTypeElement= (TypeElement) modules.iterator().next();
    JavaFileObject file = mFiler.createSourceFile(Config.CLASS_NAME, moduleTypeElement);
    PrintWriter writer = new PrintWriter(file.openWriter());
    writer.println("package "+ Config.PACKAGE_NAME+";");
    writer.println("public class "+ Config.CLASS_NAME +" {");
    writer.println("public static void "+ Config.METHOD_NAME +" () {");
    Modules modulesAnnotation = moduleTypeElement.getAnnotation(Modules.class);
    String[] value = modulesAnnotation.value();
    for (String item :
    value) {
    writer.println(Config.PACKAGE_NAME+"."+ Config.PAGE_PREFIX+item+"."+ Config.PAGE_METHOD_NAME +"();");
    }
    writer.println("}");
    writer.println("}");
    writer.flush();
    writer.close();
    } catch (IOException e) {
    e.printStackTrace();
    mMessager.printMessage(Diagnostic.Kind.ERROR,e.getMessage());
    }
    }
    }
  • 创建反射调用类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public class RemoteRegister {
    public static void register(){
    try {
    Class<?> clazz = Class.forName(Config.PACKAGE_NAME + "." + Config.CLASS_NAME);
    Method method = clazz.getDeclaredMethod(Config.METHOD_NAME);
    method.invoke(null);
    } catch (ClassNotFoundException e) {
    e.printStackTrace();
    } catch (NoSuchMethodException e) {
    e.printStackTrace();
    } catch (InvocationTargetException e) {
    e.printStackTrace();
    } catch (IllegalAccessException e) {
    e.printStackTrace();
    }
    }
    }
  • 改进Remote类——添加两个自动处理方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    /**
    * 根据默认规则自动解析Uri
    * @param uri 路由地址 Ac
    */
    public static RemoteOperatorManager putRemoteUriDefaultPattern(String uri) throws NotFoundClassException {
    // (activity://com.mmyz.account.LoginActivity)
    Pattern pattern = Pattern.compile("[/]+");
    String[] infos = pattern.split(uri);
    String protocol = infos[0];
    String page = infos[1];
    try {
    putRemoteUri(uri,Class.forName(page));
    } catch (ClassNotFoundException e) {
    throw new NotFoundClassException(page, uri);
    }
    return RemoteOperatorManager.get();
    }
    /**
    * Activity路由跳转
    * @param context Context
    * @param uri 路由地址
    * @param invokeCallback 调用回调,处理Intent传值
    */
    public static void startActivity(Context context,String uri,BaseInvokeCallback<Intent> invokeCallback){
    Intent intent = invoke(context, uri);
    intent = invokeCallback.invokeCallback(intent);
    if (intent != null){
    context.startActivity(intent);
    }else {
    throw new NotFoundIntentException();
    }
    }
3、具体调用
  • 将两个Module编译到每个模块中

    1
    2
    3
    4
    5
    6
    //remoteannotation
    compile project(':remoteannotation')
    //remote
    compile project(':remote')
    //compiler
    compile project(':remotecompiler')
  • 在App模块的Application中调用注册

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    @Modules({
    RemoteModuleConfig.ACCOUNT_MODULE,
    RemoteModuleConfig.PRODUCT_MODULE,
    RemoteModuleConfig.ORDER_MODULE})
    public class RemoteApp extends Application {
    @Override
    public void onCreate() {
    super.onCreate();
    initRemote();
    }
    private void initRemote() {
    // Remote.putRemoteUri(ActivityIntentOperator.PROTOCOL+ IRemoteUrlConfig.LOGIN_REMOTE_URL, LoginActivity.class);
    RemoteRegister.register();
    // try {
    // Remote.putRemoteUriDefaultPattern(ActivityIntentOperator.PROTOCOL+ LoginActivity.REMOTE_URL);
    // } catch (NotFoundClassException e) {
    // Log.e("=========",e.getMessage());
    // e.printStackTrace();
    // }
    }
    }
  • 在被需要的页面添加@Module和@StaticRemote注解

    1
    2
    3
    @Module(RemoteModuleConfig.ACCOUNT_MODULE)
    @StaticRemote(ActivityIntentOperator.PROTOCOL+ RemoteUrlConfig.REGISTER_REMOTE_URL)
    public class RegisterActivity extends AppCompatActivity {
  • 调用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    btnProduct.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
    Remote.startActivity(
    MainActivity.this,
    ActivityIntentOperator.PROTOCOL + RemoteUrlConfig.PRODUCT_REMOTE_URL,
    new BaseInvokeCallback<Intent>());
    }
    });

四、总结

  通过多Module的划分,可以很好解决多人协作开发中代码冲突问题。另外通过路由方式是也可以很好的解决在采用多Module划分后,页面之间的跳转问题,同时让页面跳转逻辑更加清晰。所以相对来说本人还是比较推崇,因为无论是解耦程度可扩展性可维护性都得到了极大的提升。使用和学习成本都不高。当然目前这个是Demo项目用于讲解,如果真的想开发实际项目还需进一步完善,或者去搜集大手写的路由框架

实际代码请下载或者Frok项目。

项目GitHub地址:https://github.com/ms-liu/AndroidRemoteDemo

欢迎大家给出中肯的建议和提高意见,你的鼓励将是我最大的动力。

个人邮箱:ms_liu163@163.com

Contents
  1. 1. 项目GitHub地址:https://github.com/ms-liu/AndroidRemoteDemo
  2. 2. 一、项目为什么要分成多Module
  3. 3. 二、解决Module之间页面跳转问题
    1. 3.1. 1、问题分析
    2. 3.2. 2、问题解决
    3. 3.3. 3、具体代码编写
      1. 3.3.1. (一)定义操作接口
      2. 3.3.2. (二)基础实现类
      3. 3.3.3. (三)具体Activity这种处理
      4. 3.3.4. (四)Manager管理者编写
      5. 3.3.5. (五)代言者——Remote编写
      6. 3.3.6. (六)测试
  4. 4. (三)拔高与提升
    1. 4.1. 1、创建RemoteAnnotation Module
    2. 4.2. 2、创建RemoteCompiler Module
    3. 4.3. 3、具体调用
  5. 5. 四、总结
  6. 6. 实际代码请下载或者Frok项目。
  7. 7. 项目GitHub地址:https://github.com/ms-liu/AndroidRemoteDemo
  8. 8. 欢迎大家给出中肯的建议和提高意见,你的鼓励将是我最大的动力。
  9. 9. 个人邮箱:ms_liu163@163.com