移动架构这么多,如何一次搞定所有

我是一个Android猴, 主要从Android端来谈一下对各种结构的看法,总结一下基础架构的核心是什么?因此,这篇文章对于对架构一词不是很了解,或者一知半解的朋友来讲, 梳理一下大局观;并且我觉得同样的作为移动开发,IOS与Android的架构差异化并不是很大。

从移动端谈架构,其实有点夸大了。因为移动端的项目往往不是很大,或者模块不是很大。一般架构这个词,可能用在Web端比较好一点,也更有效点,架构好了,意味这更稳健的运行效率, 更大体量。从移动端来谈架构,无非是让代码可以优雅一点,解决一下常见的耦合等问题。

从Android诞生至今,移动端的架构变更了很多次,从最初的MVC到MVP, 从冷门的Flutter(由RN引入到移动端)到Google的AAC/MVVM;好像架构的思想一直在变,但是大抵都是换汤不换药的,为什么这么说呢 ? 让我们来总结一下。

MVC
MVP
MVVM
Flutter
AAC

以上的架构中MVX系列先不说,剩下的两个是什么? 先来解释一下。

Flutter
此Flutter非目前炒得火热的Flutter, 而是由React Native衍生而来的,适用于移动端的框架。是的,这也是一种框架思想。Flutter的元素分为3种: View(不必多说), Model(也不必解释吧), Store(这个要说一下,用于处理Action的核心类,类似Presenter的作用), Dispatcher(Action路由), Action(事件)。该框架类似于MVP, 只是通信模块由接口,改为路由系统。

AAC([Android Architecture components]
不知道的可以查看一下官网:https://developer.android.com/topic/libraries/architecture/guide.html

public class UserProfileViewModel extends ViewModel {
    private String userId;
    private User user;

    public void init(String userId) {
        this.userId = userId;
    }
    public User getUser() {
        return user;
    }
}
public class UserProfileFragment extends Fragment {
    private static final String UID_KEY = "uid";
    private UserProfileViewModel viewModel;

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        String userId = getArguments().getString(UID_KEY);
        viewModel = ViewModelProviders.of(this).get(UserProfileViewModel.class);
        viewModel.init(userId);
    }

    @Override
    public View onCreateView(LayoutInflater inflater,
                @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        return inflater.inflate(R.layout.user_profile, container, false);
    }
}

以上是来自官网的AAC-Demo的源码,看结构其实就清楚了,如果允许自己命名的话,或者我们可以称为MVVM了;ViewModel这个模块,是不是像极了Controller或者Presenter。


通过以上框架的分析, 我们应该可以得出结论:

局部的架构,为什么说是局部架构?因为项目级得架构肯定就是大结构的组件化与插件化。

1.分层

从代码的实现解耦。对于现在狭义上的架构,M和V是必然单独的两层,因为数据处理和UI嘛,界限很清楚。难以划分层次的就是逻辑实现,也就是我们的业务处理。而Controller, Presenter, VM这些模块的功能都是一致的

所以分层的维度几乎已经确定,就是 数据处理(Model), UI显示(View), 业务处理(X)

2.通信

不同的模块,好像层次划分都是一致的,虽然骚气的取了不同的名字。但是区别就在于通信方式。

MVC/AAC的通信方式是对象, View与Model的交互是完全通过对象来实现的,如下

public class Model {

    /**
     * 这是Model模块,负责数据处理。处理的方式不尽相同,但是本质一样。如网络请求, 数据库, 文件等等
     */

    public void postLogin(String username, String password, Callback callback) {
        // 执行登陆请求,验证帐号密码是否正确
        callback.onResponse("登录结果");
    }

    public interface Callback {
        void onResponse(String result);
    }
}
public class View {

    X controllerOrPresenter;

    public void onClick() {
        // 点击登录
        controllerOrPresenter.login("xxx", "xxx", new Model.Callback() {
            @Override
            public void onResponse(String result) {
                if ("success".equals(result)) {
                    // 登录成功,则提示登录成功,保存信息
                } else {
                    // 登录失败,则提示失败
                }
            }
        });
    }
}
public class X {

    Model model;
    View view;

    public void login(String username, String password, Model.Callback callback) {
        // 验证帐号, 密码格式是否正确
        model.postLogin(username, password, callback);
    }

}

以上代码是MVC模式, 是不是这么处理的?通过持有对象来达到模块之间的通信。当时可能会觉得这样耦合度比较高,这样就出现了解决方案。于是MVC做成了MVP(哦,当然,那时候还不这么叫),通过接口的方式来通信,或许接口化没那么彻底而已。AAC通过绑定页面的周期做到随页面释放并包装


MVP的通信方式是完全接口式通信, V和P之间,甚至M和P之间也可以

MVP的方式来解释一下,V和P分别实现自己的接口, IV和IP。然后分别传如自己的接口达到调用目的。代码如下:

public interface IP {
    void login(String username, String password);
}
public interface IV {

    void loginSuccess();

    void loginFailed();
}
public class View implements IV{

    X iP;

    public void onClick() {
        // 点击登录
        iP.login("xxx", "xxx");
    }

    @Override
    public void loginSuccess() {
        // 保存信息
        // 提示登录成功了
    }

    @Override
    public void loginFailed() {
        // 提示登录失败
    }
}
public class X implements IP{

    /**
     * X 相当于MVP中的Presenter
     */

    Model model;
    IV iV;

    public void login(String username, String password) {
        // 验证帐号, 密码格式是否正确
        model.postLogin(username, password, new Model.Callback() {
            @Override
            public void onResponse(String result) {
                if("success".equals(result)) {
                    // 登录成功
                    iV.loginSuccess();
                }else {
                    // 登录失败
                    iV.loginFailed();
                }
            }
        });
    }

}

以上是MVP的通信方式,完全的接口相互调用。可能一些架构思维比较前卫的公司,在前期就把MVC改造成了如今的MVP,还是那句话,只是那时候不那么叫罢了。


Flutter是以路由机制来实现解耦通信

在现在组件化风行的时代,相信各位对路由没那么陌生了。就算不了解, 那么路由器总知道吧,那么先说路由器。

路由或路由器,分两个模式: 接收信号, 发出信号。分别是多对一和一对多的关系。

举个例子, 现实中的路由器,接收只有一个入口,但发出口有很多个,毕竟如果只有一个出口,那么路由器就没用了。在路由机制中,入口也可以有多个。所以就是上面说的,接收是多对一(一当然是路由器), 发出是一对多(一还是路由器)。

简单由一个图来说明一下路由机制,图不是很规整,明白就好。

image.png

在Flutter模式下,我们如何对应呢?

M/V/P或C不同模块之间不能有耦合,即不持有对象,且不持有接口,完全解耦。那么各模块怎么通信呢?通过向路由器发信号。所以M V P/C 都是上图的信号源

M/V/P或C 不同模块要交互,那么怎么得到信号呢? 这时候他们的角色就转变了,不仅可以发信号,也可以接口信号,来做对应处理。 所以M V P/C 也同样是上图的手机(信号接口器)

以上的解释, 不同模块既可以发出信号,也可以接收信号。和Android中的一个组件很相似,就是Handler. Handler既可以发出消息,同时消息又在里面处理。

既然有发送信号的, 有接收信号的,那么必然有一个路由器负责接收与发送,在Flutter中就是Dispatcher。Dispatcher保存了不同信号与接收器的对应关系, 以此完成消息的分发

上面都提到了消息,现在正式的介绍一下,消息是角色是Action

看一下Flutter的元素

Model
View                                       
Store(Controller或Presenter)   处理业务逻辑
Action 消息对象,带有动作与数据
Dispathcer 路由,管理消息与发送消息,保存有一张对应表

接下来通过代码了解一下

public class Model {

    public void postLogin(User user, Callback callback) {
        // 请求登录
        callback.onResponse(1);
    }

    public interface Callback {
        void onResponse(int result);
    }
}
public class User {
    public String username;
    public String password;

    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }
}
public class View implements Dispatcher.IReceiver {


    public void onCreate() {
        Dispatcher.getDispatcher().register(this);
    }

    public void onDestroy() {
        Dispatcher.getDispatcher().unregister(this);
    }

    void onClick() {
        Dispatcher.getDispatcher()
                .sendEvent(
                        new Action("login", new User("xxx", "xxx")));
    }

    @Override
    public void onReceive(Action action) {
        if (action.name.equals("login-success")) {
            // 登录成功提示
        } else {
            // 登录失败提示
        }
    }
}
public class X implements Dispatcher.IReceiver{

    Model model;

    public X() {
        model = new Model();
        Dispatcher.getDispatcher().register(this);
    }

    public void clearX() {
        model = null;
        Dispatcher.getDispatcher().unregister(this);
    }

    @Override
    public void onReceive(Action action) {
        if(action.name.equals("login")) {
            model.postLogin((User) action.data, new Model.Callback() {
                @Override
                public void onResponse(int result) {
                    if(result == 1) {
                        // 登录成功
                        Dispatcher.getDispatcher().sendEvent(new Action("login-success", null));
                    }else {
                        // 登录失败
                        Dispatcher.getDispatcher().sendEvent(new Action("login-failed", null));
                    }
                }
            });
        }
    }
}
public class Action<T> {
    public String name; // 执行动作,比如“登录”
    public T data; // 数据,比如username, password

    public Action(String name, T data) {
        this.name = name;
        this.data = data;
    }
}
public class Dispatcher {
    private static Dispatcher dispatcher = new Dispatcher();
    private List<IReceiver> receivers = new ArrayList<>();

    private Dispatcher(){}

    public static Dispatcher getDispatcher() {
        return dispatcher;
    }

    public void register(IReceiver receiver) {
        if (receivers.contains(receiver)) {
            throw new IllegalStateException("receiver has been registerd yet!");
        } else {
            receivers.add(receiver);
        }
    }

    public void unregister(IReceiver receiver) {
        if (receivers.contains(receiver)) {
            receivers.remove(receiver);
        }
    }

    public void sendEvent(Action action) {
        if (action != null && action.name != null && action.name.length() > 0) {
            for (IReceiver r : receivers) {
                if (r != null) {
                    r.onReceive(action);
                }
            }
        }
    }

    public interface IReceiver {
        void onReceive(Action action);
    }
}

解析一下以上代码, 首先我们说过, 所有模块既是消息发出者,也是消息接收者。代码中,View和X都分别发出了消息进行登录以及登录结果成功或失败;同时View和X也都注册了接收器的接口,在onReceiver中可以接收消息。这样做的好处是什么? View和X完全没有耦合,既不持有对象, 也不持有接口,中间的通信都是通过Dispatcher进行分发的,解耦已经很彻底了。Dispathcer就是路由器,负责接收,并分发消息,应该很好理解,哪个组件想接收消息,那么就注册一个接收器,这样有合适的消息自然就接收到了。

当然,以上的代码有点简陋,可以从不同组件再行封装,比如Action, 比如Dispatcher, 比如BaseView等等。我们可以通过给路由注册机制添加Group与Tag(Action name)概念来优化效率问题,这里只是说思想。


我们知道了什么?

  1. 我称移动端的架构思维为MVX,即是说按这个规则的分工被开发市场所接受了,我们不用费尽心思考虑狭义架构的分层问题了,就沿用Model-View-X来就可以。当然还可以自己加一些辅助的模块层,如Worker负责异步, Converter负责转换, Verify负责校验等

2.移动端目前的架构,差异化在于通信机制。通过以上说明,通信机制主要分为3种:
1) 对象持有
2) 接口持有
3) 路由

  1. 通信方式中,对象持有是比较原始的,解耦率最低,建议放弃; 接口持有是个不错的选择,极大程度上实现解耦的诉求,但是解耦不彻底,相互持有交互方的接口。 路由机制也是个不错的选择,可以实现完全解耦,就像组件化一样。但是路由机制的设计是个技术难点,怎么设计效率最高?更健壮?代码可查阅性更好?这些都是值得思考的问题。

4.对于路由机制的优化,阿里的ARouter(用于组件通信)中,采用了分组的模式,我们可以采用;其次可以根据AnnotationProcessor的处理,为每一个注册接收器的组件实现一个SupportActions来确保消息只发送给注册了指定类型的模块,也是个不错的选择。

通过以上分析与总结,可以看到,移动端的框架核心是一定的,只要理解了架构的核心思想,其实出多少新的框架,不过是加了一些解决部分问题的实现罢了。看懂了这篇文章,可能再出什么架构模式,或许看一眼就明白是怎么实现了呢?

如果你觉得这篇文章对你有帮助,请帮我打Call, 谢谢!!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容