当前位置: 首页 > 科技观察

一个Android客户端架构设计分享

时间:2023-03-11 22:04:54 科技观察

前言:科技的发展日新月异。业界Android客户端架构设计多种多样,但我们不能简单的说哪种架构更好,因为脱离业务谈架构是没有意义的,好的架构适合业务。而且架构不是静态的。随着业务的发展,可能原来设计的架构已经不足以支撑现在的业务,需要改变之前的架构。接下来分享一下我们Android客户端的架构设计,对App业务发展的某个阶段或许有一定的借鉴意义。分层和模块化分层和模块化应该是任何软件开发的共识。Android应用开发中的分层通常可以分为以下几层:SDK层:主要是AndroidSDK和第三方SDK(可能基于AndroidSDK,也可能是独立的SDK),这些SDK为上层框架提供核心功能支持。基础框架层:这里所谓的基础框架是指大部分APP所需要的基本功能,是具体业务逻辑实现的基础。主要包括网络请求功能、图片加载缓存功能、SQLite数据库管理功能、Log管理功能等。当然,根据对业务逻辑的支持,基础框架层的功能支持可能不尽相同。以上应该是大部分App都支持的。当然,Crash监控和常用工具也可以归到这一层。每个基础框架的实现没有限制。比如网络功能可以使用Volley、OkHttp或者自己封装来实现网络请求逻辑;图片管理功能,可以使用Glide、Fresco、Picasso,也可以自己实现……总之,每一个基础框架都必须遵循一定的实现原则,保持功能模块的独立性,与具体服务解耦,提供良好的交互性与外界的接口。业务逻辑层:如果把App架构比作一座高层建筑,那么以上两层就是地基。地基打好后,就可以在上面自由发挥了。至于怎么玩,还要结合实际业务需求。不同的应用往往有不同的业务功能模块。另一方面,业务功能模块并不完全在一个层次上,一些业务逻辑也可以抽象为通用功能模块,比如登录、分享、扫描、统计等,可能会被其他业务模块调用功能。这里需要注意的是,SDK层和基础框架层并不是一成不变的,只是它们的变化周期往往比较长。一般来说,当基础功能不能满足顶层的业务逻辑时,就需要进行扩展。由于基础框架层的功能模块已经划分为功能级粒度,扩展往往是模块级的扩展,通常是增加新的基础功能框架,而不是修改原有的基础功能框架,这也符合“开放-封闭”“原则。模块化关于模块化,是对分层进行更细粒度的划分,即每一层细分为不同的模块,每个功能模块尽可能遵循“高内聚、低耦合”的原则。功能模块仅提供必要的交互接口。至于基础框架层,从上图可以看出,往往是按照功能来划分的。这里基础框架层细分为网络支持功能、图片库、日志系统、数据库支持等模块。如果不足以支持业务发展,可能会增加其他基础功能模块。业务逻辑层主要由业务需求决定,如扫描功能、电子商务、快递查询等模块。业务逻辑层模块化还有一个驱动因素,就是对通用功能的封装。每个人都应该经历过这一点。随着App业务逻辑的增加,不同业务功能之间可能会使用相同的功能,比如用户登录、分享功能等,我们不想在每个需要的地方都重写相关代码,所以需要抽取将通用功能拆分成独立于具体业务需求的模块,如登录模块、分享模块,在模块内部实现通用功能的业务逻辑,同时对外暴露调用接口,不同业务只需要调用通用模块即可。业务数据流程设计由于业务逻辑、数据处理逻辑或网络框架的不同,相信每个应用程序都有自己的一套数据请求流程。最直接的方式就是从Activity或者Fragment调用网络请求的方法,然后通过回调将结果返回给Activity或者Fragment。虽然流程是最清晰的,但是这种方式存在几个严重的问题:网络数据直接返回到Activity或者Fragment中,后续需要对数据进行解析、过滤、转换、缓存等操作,这些工作会大大增加Activity或Fragment的负担。Activity或者Fragment的代码量增长很快,逻辑复杂(不仅是View的逻辑,还有数据处理的逻辑)从整个应用来看,每个页面甚至每个界面都需要重复相同的以上多余的工作,完全可以抽象出来。以上设计思路需要摒弃。结合自身业务和架构演进,我们没有跟风MVP和MVVM,而是设计了如下一套业务数据请求流程:首先,视图层通常用一个Activity或者Fragment来表示,由视图发起层数据请求,与上面不同的是,视图层不直接与网络框架打交道,而是先将数据请求发送给数据代理层DataAgent。需要注意的是,视图层和数据代理层之间并没有直接通信,而是插入了一个消息调度器MessageScheduler中继。这样做的好处是将视图层与数据代理层解耦。视图层不需要关注数据代理层的具体实现。有了MessageScheduler,视图层要做的就是发送一个数据请求消息,然后就可以安静的等待一个回复消息,回复消息会附上最终需要的数据对象,这样数据处理的逻辑就是免于视图层,结果可以直接显示在UI上。使用这种方法,一般来说,Activity或者Fragment的代码三五百行就可以搞定,UI逻辑或者界面逻辑(比如一个多界面的页面)比较复杂的代码量基本可以控制在左右1,000行,这是非常符合逻辑的。令人耳目一新。消息调度器将视图层的请求消息转发给数据代理层后,DataAgent解析出数据请求类型DataType(该类型对应具体的数据对象模型)、必要参数(接口参数、是否缓存结果、页码等),然后执行具体操作:如果要取缓存数据,DataAgent直接向缓存模块发送请求。缓存的数据可以是初始的JSON数据,也可以是解析处理后得到的数据对象Model,可以根据具体需要配置。如果从缓存中取出JSON,DataAgent首先要解析处理得到对应的Model;如果Model是从缓存中取出来的,则不做任何处理,然后将Model打包回消息调度器,再由MessageScheduler分发给具体的请求者,比如Activity或者Fragment。由于Android的数据来源很多,如果数据来自于持久化存储,如SQLite或File等,DataAgent仍然统一与它们通信,获取并处理数据并通过MessageScheduler传回视图层.最常见的是从服务器获取数据。在这种场景下,DataAgent会与网络框架进行交互,将从MessageScheduler获取的参数提供给网络框架来构建请求url。网络框架使用Volley或OkHttp或其他并不重要。网络框架负责向服务器请求数据,数据通常以JSON格式返回。DataAgent收到返回的JSON数据后,根据DataType对JSON数据进行校验后丢给解析器,解析器将JSON解析成视图层需要的Model。当然,数据解析过程可能伴随着数据过滤、转换等逻辑。另外需要注意的是,还需要根据视图层的要求对数据进行缓存。您可以选择是缓存JSON还是模型。经过一系列的操作,得到最终的Model后,DataAgent通过MessageScheduler将其传回视图层。当然,由于数据请求过程比较耗时,以上步骤都是通过线程池进行的,上图中没有表示。数据代理层DataAgent在上面已经简单提到过。它的主要功能是对数据的一系列操作,包括实际的数据请求、数据分析处理、数据缓存等逻辑。下图展示了从服务端接口获取并处理JSON数据的过程:从上图可以看出DataAgent的大致工作流程如下:DataAgent向各个数据源发送真实的数据请求。数据源可能是缓存、SQLite或文件,但Data通常是从服务器获取的,所以DataAgent会将数据请求发送到网络框架层,然后等待数据返回。由于数据源不同,返回的数据也可能不同,这里简化为两种:原始JSON或Model。DataAgent获取数据后,开始数据处理过程。以网络请求的JSON数据为例,首先对返回的JSON进行数据校验,检查数据的合法性和正确性。如果数据校验通过,则根据需求决定是否写入缓存,然后进行数据处理(如精度处理、数据拼接、数据裁剪等),最后进行数据分析,得到所需的Model视图层。如果数据校验失败,尝试从缓存中读取。从缓存中读取后,还需要进行校验(检查数据的时效性、有效性、正确性)。验证通过后,还要进行数据处理和分析。过程。如果从缓存中读取模型,则可以省略数据处理和解析的过程。DataAgent得到最终的Model后,将其包发送给MessageScheduler。另外,DataAgent必须具备一定的容错功能,因为任何数据源都不能保证返回合法的数据。如果数据错误不具备容错能力,可能无法解析到对应的Model中,导致视图层无数据甚至异常。如果接口和缓存都不能返回正确的数据,DataAgent需要做特殊处理,保证视图层能够反馈给用户。业务视图逻辑虽然不同的业务页面有不同的视图逻辑,这里我们以一个应用中最常见的页面为例,假设该页面有一个列表。大家都知道ListView(这里是一个大概的参考,可能大家都在用RecyclerView)是怎么工作的。它需要ViewHolder来填充视图,需要Adapter来填充数据。如果每个需要ListView的接口都维护自己的一套ViewHolder和Adapter,那么页面逻辑就会变得臃肿。我们在实践中是这样做的:封装一个公共的Adapter处理类,并提供各种构造函数,其中有一个类型参数表示需要使用哪个ViewHolder。封装一个ViewHolder抽象类,定义数据设置的逻辑,交给具体的ViewHolder实现。构建一个名为ViewHolderFactory的类。顾名思义,这个类的主要功能就是构建ViewHolder。它主要提供createViewHolder()和createConvertView()两个方法,其中createConvertView()是生成ViewHolder的中间方法。在Adapter的getView方法中,根据上述类型参数,获取具体的ViewHolder实现,调用设置数据的逻辑。经过上面的封装,视图层只需要传递一个类型参数给Adapter公共处理类,就可以得到对应的Adapter;数据返回到视图层后,再将数据传递给Adapter公共处理类,其他就不用管了。可以显示列表数据。原本需要大量代码实现的逻辑从视图层分离出来后,视图层只需要几行代码就可以完成一个列表展示。自从Hybrid框架在Android诞生以来,就一直存在着NativeApp和WebApp的争论。虽然这两种开发方式各有优缺点,但NativeApp始终占据上风。最近一两年,移动端应用中的Web页面越来越多,而纯Native应用相对减少了。但是,纯WebApp由于其渲染效率、性能问题、硬件调用的限制等问题,并没有得到广泛的应用。于是一个折中的方案成为了主流,那就是HybridApp。所谓HybridApp就是混合开发方式,部分功能使用Native开发,部分功能使用H5开发。为了充分利用Web开发的优势,避免其劣势,并不是所有的业务功能都适合Web开发。在我们的应用中,H5主要用于以下几个方面:节日活动或游戏页面等时效性页面,秒杀或团购页面。说明、公告等页面显示偏少,交互性差。更新频繁、交互较少、不涉及硬件调用的页面或模块,如电商商品首页展示、积分兑换模块等。截至目前,网页在我们app中的占比有所提升,约占全部功能的25%。使用web开发的优势非常明显,可以支持多变的UI视图效果,节省开发人力(Android和iOS共享),无需发布App即可在线修复bug等。为了满足App的网页需求,我们在基础框架层扩展了一个Hybrid功能模块。该框架主要是对Android原生的WebView控件进行了自身封装,分为不同级别的封装,可以根据需要灵活使用。容器是Activity或Fragment。支持部分网页,即部分页面的内容用H5实现,可以使用自定义WebView单独使用,也可以嵌入Fragment中。定义了一套比较完整的交互协议,支持Native和JS相互调用,典型场景如H5页面点击跳转到Native功能页面(支持传参),JS调用Native对话框或Toast等,Java可以也可以调用JS函数。基于这套交互协议,基本可以满足日常App中Web开发的需求。避免了JS注入漏洞。支持在同一个网页中混合使用Http和Https的场景。接口暴露给业务逻辑层,WebViewClient和WebChromeClient可以根据需求定制。对外提供接口,根据需求控制缩放、cookie管理、缓存管理、硬件加速等。经过测试和探索,兼容各种Android设备和版本。ReactNative虽然后来出现,但由于学习成本和Android版本的限制,结合我们自己团队的人力资源,我们一直没有正式在应用中使用它。目前,Hybrid开发仍然是主体,在整个应用中的比例越来越大。因此,Hybrid框架是我们架构的重要组成部分。在消息调度中心前端的业务数据流设计中,在视图层和数据代理层之间插入了一个消息调度器——MessageScheduler。MessageScheduler的主要功能是管理消息和消息调度。MessageScheduler的核心原理是维护一个哈希表。当接收到来自视图层的数据请求时,将发起者以唯一键保存在哈希表中,以便后面接收到DataAgent返回的数据后可以找到发起者。.存储消息发起者信息后,向DataAgent发送数据请求。多个数据请求可以并行,主要得益于线程池的线程数控制机制。DataAgent返回数据后,MessageScheduler根据唯一键找到初始请求者,同样利用消息机制将请求结果返回给视图层,同时清空哈希表中的元素。原理图如下:由于消息分发器有消息调度机制,所以需要消息分发器MessageDispatcher负责发送消息。MessageDispatcher本质上是利用Android的消息机制来封装和扩展业务需求。阅读了AndroidFramework层的源码后,你会发现,其实Android框架本身在很多地方都是使用消息机制进行通信的。Android的消息机制可以在模块页面和线程之间进行通信,甚至可以在进程之间使用Messenger通信(Messenger的方式就是利用消息机制,当然还有其他的进程间通信方式)。MessageDispatcher的功能比较简单,支持两种方式:点对点通信,比如两个页面之间,通信目标是唯一的,如上所述,从视图层向消息调度器发送数据请求消息。点对面的通信类似于广播,也有点像EventBus。发送消息时,所有注册(或订阅)的页面都可以收到通知;也可以通过Tag进一步控制,实现一对一发送。示意图如下:在一个完整的模块路由中心应用中,模块与功能页面之间的跳转是不可避免的。当然,在需要的地方可以通过Intent实现跳转,但这不是一个好的解决方案。很明显,不同模块或页面之间的耦合度增加了。我们的原则是尽可能将模块和页面解耦,所以我们设计了一个模块路由(ModuleRouting)中心,app中所有的页面跳转都由它来控制。模块路由的核心原则是对功能页面进行唯一编码,编码的逻辑可以按照产品版本定义在应用中,保证与之前版本的兼容性。这样,你只需要将对应模块页面的代码发送到应用中任意位置的模块路由中心,由模块路由负责打开目标页面。需要注意以下几点:功能页面的代码在整个应用中必须是唯一的。打开一些功能页面,除了具体的代码外,可能还需要额外的参数。如果打开商品详情页,除了知道商品详情页的代码外,还需要商品ID,模块路由需要提供额外参数的支持。模块路由支持打开网页,即Hybrid页面也支持上述特定编码,所以点击网页跳转到Native页面使用的协议也是模块路由支持的。使用模块路由的好处是:大大减少应用中的跳转Intent模块和页面之间的解耦和适配变化,统一管理,修改重要部分方便。当然Android提供了Log相关的API,但是不建议零星使用,否则想要统一控制Tags或者关闭Logs会很麻烦。建议简单封装LogAPI或者使用已有的第三方Log库,将Log功能分离出来,统一调用接口、电平控制、开关控制。清晰度做出了一点贡献。在线崩溃监控在线应用的崩溃监控是提高应用稳定性、优化应用性能的重要手段。我们搭建了一个小型的全局监控系统,主要有以下几个特点:对用户不可见,用户不感知全局注册启动监控,捕获在线崩溃,并保存到本地文件在线崩溃信息根据上传到服务器以一定的策略,上传然后同时删除本地文件。崩溃信息主要包括Android设备信息(如手机型号、系统版本等)、App版本号、异常信息等。服务器收到上传的在线崩溃信息后,也会根据邮件通知开发者一定的策略。以便开发者及时修复异常。在线死机监控系统虽然小巧简单,但其功能却非常重要。使用在线崩溃反馈可以有效提高应用的稳定性。建议在应用设计中为其预留位置。统计系统认为大部分应用都有统计分析后台,可以统计应用的日活跃度、PV、UV或其他用户行为,部分应用还可能使用第三方统计功能,如友盟。客户结合公司BI部门的统计需求,为Android和iOS客户端设计了一套统计解决方案。之所以不用第三方统计,主要是因为我们不能根据需要自由定制,数据不在我们自己的服务器上。另一方面,也存在轻微的数据泄露风险。基于客户端的统计系统主要包括三个功能:数据采集、数据存储和数据上传。对于数据采集,主要是针对统计部门的需求,比如采集设备信息、位置信息、App启动次数、PV、UV,甚至用户行为等,比如点击,切换Tab,页面流量跟踪等。为了避免每次采集数据后立即上传,需要进行数据存储,将采集到的统计数据暂存在本地,一般使用SQLite。然后使用一定的策略上传,比如当数据累积到50或者应用切换到后台时上传。对于数据上传,除了上传时机的选择策略外,还必须遵循一定的结构字段,可根据数据统计部门的需要定义结构。数据上传过程也可以使用之前的数据请求框架,但是返回值可能是成功提示。基于以上功能,我们的自定义统计功能模块提供了方便的调用接口,支持灵活扩展。目前完全可以支持日常的统计需求,调用也非常简单。只需要在需要统计的地方插入一行代码即可。能。域名劫持攻略最近遇到域名劫持问题,真是头疼。另一方面也说明我们的流量已经引起了运营商的重视。目前主流的解决方案有以下几种:向运营商投诉。这种方法非常被动且无效,并且完全掌握在操作员手中。使用httpDNS。这种方法利用http直接获取***IP,绕过了localDNS的解析,可以说彻底解决了域名劫持。先用域名试试,域名失效后再用IP试试。该方案属于容灾方案,无法避免域名劫持。理论上第二种是最好的方案,但是由于httpDNS是第三方服务,效果无法保证,加上付费和接入成本等因素,我们暂时采用??第三种容灾方案。主要实现逻辑如下:应用预先内置IP。每次启动应用获取***IP保存在应用本地。请求数据时,先使用域名,遵循正常逻辑。一旦遇到疑似劫持问题,直接使用本机IP尝试连接。上面的步骤其实是有漏洞的。比如获取***IP的接口在启动时被劫持,那么就无法获取到***IP。别无他法。但是同时满足以上两个条件的概率比较小,所以这个方案可以用来解决很大一部分域名劫持问题。另外,如果从服务端获取的IP有多个,还需要增加一些策略,即考虑负载均衡、访问速度、稳定性、网络运营商等因素,如何确定客户端获取到哪个是**IP,当然这个可以优化,但是保证用户能先看到页面数据可能更重要。上述应对域名劫持的策略不能单独作为一个模块,我们将其集成为网络框架的扩展。综上所述,上面提到的就是我们Android应用架构的核心部分。你可能会发现,没有花里胡哨的新潮东西,没有MVP,没有RxAndroid,没有插件,没有hotfix……但仅此而已,它仍然支持数亿用户。世界上没有完美的架构,只有适合自己业务的架构。上述架构仍有许多不足之处。我们也在有选择地、有步骤地进行改造。随着业务需求的扩展,架构也会不断演进。***希望本文能给大家带来一点参考。