android图片加载框架(android课程设计小项目)

android图片加载框架(android课程设计小项目)

前言:本文结合个人在架构设计上的思考和理解,介绍如何从0到1设计一个大型Android项目架构。一 引导本文篇幅较长,可结合下表引导快速了解全文主脉络。章节标题章节简介项目架构演进总结一个普通Android项目从0到1再到N的架构演进历程。项目架构拆解对一个架构完整的大型Android项目中各层级及其核心模块进行展开讲解。核心原理总结对各框架SDK中一些共性的底层原理进行抽象总结。通用设计方案梳理一些架构设计和项目开发中遇到的通用问题及其解决方案。总结总结架构设计的适用范围。二 项目架构演进该章节主要对一个Android项目架构从0到1再到N的演进历程做出总结(由于项目的开发受业务、团队和排期等各方面因素影响,因此该总结不会严格匹配每一步的演进历程,但足以说明项目发展阶段的一般性规律)。1 单项目阶段对于一个新开启项目而言,每端的开发人员通常非常有限,往往只有1-2个。这时候比项目的架构设计和各种开发细节更重要的是开发周期,快速将idea进行落地是该阶段最重要的目标。现阶段项目的架构往往是这样

此时项目中几乎所有的代码都会写在一个独立的app模块中,在时间为王的背景下,最原始的开发模式往往就是最好最高效的。2 抽象基础库阶段随着项目最小化MVP已经开发完成,接下来打算继续完善App。此时大概率会遇到以下几个问题代码的版本控制问题,为保证项目加快迭代,团队新招1-3名开发同学,多人同时在一个项目上开发时,Git代码合并总会出现冲突,非常影响开发效率;项目的编译构建问题,随着项目代码量逐渐增多,运行App都是基于源码编译,以至于首次整包编译构建的速度逐渐变慢,甚至会出现为了验证一行代码的改动而需要等待大几分钟或者更久时间的现象;多应用的代码复用问题,公司可能同时在进行多个App的开发,同样的代码总是需要通过复制粘贴的方式进行复用,维持同一个功能在多个App之间的逻辑一致性也会存在问题;基于以上的一种或多种原因,我们往往会把那些相对于整个项目而言,一旦开发完成后就很少再改动的功能进行模块化封装。

该层会涉及到很多核心能力的建设,这里不做过多赘述,下文会对以上各个模块做详细展开。注:从全局视角来看,基础层和核心层也能作为一个整体,共同支撑上层业务。这里将其分为两层,主要考虑到前者是必选项,是整体架构的必要组成部分;后者是可选项,但同时也是衡量一个App中台能力的核心指标。4 模块化阶段随着业务规模继续扩大,App的产品经理(下简称PD)会从一个变为多个,每个PD负责独立的一条业务线,比如App中包含首页、商品和我的等多个模块,则每个PD会对应这里的一个模块。但该调整会带来一个很严重的问题项目的版本迭代时间是确定的,只有一个PD的时候,每个版本会提一批需求,开发能按时交付就上线,不能交付就把这个迭代适当顺延,这样不会有什么问题;但如今多个业务线并行,很难在绝对意义上保证各个业务线的需求迭代都能正常交付,就好像你组织一个活动约定了几点集合,但总会有人会遇到一些特殊的情况不能及时赶到。同理,这种难以完全保持一致的情况在项目开发中也会遇到。在当前的项目架构下,业务上虽然拆分了业务线,但我们工程项目的业务模块还是一个整体,内部包含着各种错综复杂的依赖关系网,即使每个业务线按分支区分,也很难规避这个问题。这时候我们需要在架构层面做项目的模块化,使得多业务线不相互依赖,如图

业务层中,可以按照开发人员或者小组进行更细粒度的划分,以保证业务间的解耦合和开发职责的界定。5 跨平台开发阶段业务规模和用户体量继续扩大,为了应对随之而来的是业务需求暴增,整个端侧团队开始考虑研发成本问题。为什么每个业务需求都至少需要Android和iOS两端都实现一遍?有没有什么方案能够满足一份代码能运行在多个平台?这样岂不是既降低了沟通成本,又提升了研发效率。答案当然是肯定的,此时端侧部分业务开始进入了跨平台开发的阶段。

至此,一个相对完整的端侧系统架构已经初具雏形了。后续业务上会继续有着更多的迭代,但项目的整体结构基本都不会偏离太多,更多的是针对于当前架构中的某些节点做更深层次的改进和完善。以上是对Android项目架构迭代过程的总结,接下来我会对最终的架构图按照自下而上的层级顺序进行逐一展开,并对每层中涉及到的核心模块和可能遇到的问题进行分析和总结。三 项目架构拆解1 基础层基础UI模块抽取出基础的UI模块,主要有两个目的:统一App全局基础样式比如App的主色调、普通正文的文字颜色和大小、页面的内外边距、网络加载失败的默认提示文案、空列表的默认UI等等,尤其是在下文提到项目模块化之后这些基础的UI样式统一会变得非常重要。复用基础UI组件在项目和团队规模逐渐发展扩大时,为了提高上层业务的开发效率,秉承DRY的开发原则,我们有必要对一些高频UI组件进行统一封装,以供给业务上层调用;另外一个角度来看,必要的抽象封装还能够降低最终构建的安装包大小,以免一份语义的资源文件在多处出现。基础UI组件通常包含内部开发和外部引用两部分,内部开发无可厚非,根据业务需求进行开发和封装即可;外部引用要着重强调一下,Github上有大量可复用、经过很多项目验证过的优秀UI组件库,如果是为了快速满足业务开发诉求,这些都将不失为一种很不错的选择。选择一个合适的UI库,会给整个开发进程带来很大的加速,自己手动去实现也许没问题,但会非常花费时间和精力,如果不是为了研究实现原理或深度定制,建议优先选择成熟的UI库。网络模块绝大多数的App应用都需要联网,网络模块几乎成为了所有App必不可少的部分。框架选择基础框架的选择往往参考几个大原则:维护团队和社区比较大,遇到问题后能够有足够多的自助解决空间;底层功能强大,支撑尽可能多的上层应用场景;拓展能力灵活,支持在框架基础上做能力拓展和AOP处理;Api侧友好,降低上层的理解和使用成本;这里不做具体展开,如果不是基础层对网络层有自己额外的定制,则推荐直接使用Retrofit2作为网络库首选,上层Java Interface风格的Api,面向开发者非常友好;下层依赖功能强大的Okhttp框架也几乎能够满足绝大多数场景的业务诉求。官网的用例参考// 0. 初始化Retrofit retrofit = new Retrofit.Builder() .baseUrl(“https://api.github.com/”) .build();// 1. 声明服务接口public interface GitHubService { @GET(“users/{user}/repos”) Call<List<Repo>> listRepos(@Path(“user”) String user);}// 2. 通过Retrofit获取服务接口实例GitHubService service = retrofit.create(GitHubService.class);// 3. 业务层调用Call<List<Repo>> repos = service.listRepos(“octocat”);用例中对Retorfit声明式接口的优势做了很好的展现,不需要手动实现接口,声明即可使用,其背后的原理是基于Java的动态代理来做的。统一拦截处理无论上一步选择的是什么网络库,都需要考虑到该网络库对于统一拦截的能力支持。比如我们想在App的整个运行过程中,打印所有请求的日志,就需要有一个支持配置类似Interceptor这样的全局拦截器。举一个具体的例子,在现如今服务端很多分布式部署的场景,传统的session方式已经无法满足对客户端状态记录的诉求。有一个比较公认的解决方案是JWT(JSON WEB TOKEN),它需要客户端侧在登录认证之后,把包含用户状态的请求头信息传递给服务端,此时就需要在网络层做类似于下面的统一拦截处理。Retrofit retrofit = new Retrofit.Builder() .baseUrl(“https://xxx.xxxxxx.xxx”) .client(new OkHttpClient.Builder() .addInterceptor(new Interceptor() { @NonNull @Override public Response intercept(@NonNull Chain chain) throws IOException { // 添加统一请求头 Request newRequest = chain.request().newBuilder() .addHeader(“Authorization”, “Bearer ” token) .build(); return chain.proceed(newRequest); } }) .build() ) .build();此外还有一点需要额外说明,如果应用中有一些跟业务强相关的信息,也建议根据实际业务情况考虑直接通过请求头进行统一传递。比如社区App的社区Id、门店App的门店Id等,这类参数有个普遍性特点,一旦切换过来之后,接下来的很多业务网络请求都会需要该参数信息,而如果每个接口都手动传入将会降低开发效率,也更容易引发一些不必要的人为错误。图片模块图片库和网络库不同的是,目前行业里比较流行的几个库差异性并没有那么大,这里建议根据个人喜好和熟悉度自行选择。以下是我从各个图片库官网整理出来的使用示例。PicassoPicasso.get().load(“http://i.imgur.com/DvpvklR.png”).into(imageView);FrescoUri uri = Uri.parse(“https://raw.githubusercontent.com/facebook/fresco/main/docs/static/logo.png”);SimpleDraweeView draweeView = (SimpleDraweeView) findViewById(R.id.my_image_view);draweeView.setImageURI(uri);GlideGlide.with(fragment) .load(myUrl) .into(imageView);另外,这里附上各个库在Github上的star,供参考。

图片库的选型比较灵活,但是它的基础原理我们需要弄清楚,以便在图片库出问题时有足够的应对解决策略。另外需要着重提出来的是,对于图片库最核心的是对图片缓存的设计,有关该部分的延伸可以参考下文的「核心原理总结」章节。异步模块在Android开发中异步会使用的非常之多,同时其中也包含很多知识点,因此这里将该部分单独抽出来讲解。1)Android中的异步定理总结下来一句话就是,主线程处理UI操作,子线程处理耗时任务操作。如果反其道而行之就会出现以下问题主线程做网络请求,会出现NetworkOnMainThreadException异常;

主线程做耗时任务,很可能会出现ANR(全称Application Not Responding,指应用无响应);

子线程做UI操作,会出现CalledFromWrongThreadException异常(这里只做一般性讨论,实际上在满足某些条件下子线程也能更新UI,参《Android 中子线程真的不能更新 UI 吗?》,本文不讨论该情况);2)子线程调用主线程如果当前在子线程,想要调用主线程的方法,一般有以下几种方式通过主线程Handler的post方法private static final Handler UI_HANDLER = new Handler(Looper.getMainLooper());@WorkerThreadprivate void doTask() throws Throwable { Thread.sleep(3000); UI_HANDLER.post(new Runnable() { @Override public void run() { refreshUI(); } });}通过主线程Handler的sendMessage方法private final Handler UI_HANDLER = new Handler(Looper.getMainLooper()) { @Override public void handleMessage(@NonNull Message msg) { if (msg.what == MSG_REFRESH_UI) { refreshUI(); } }};@WorkerThreadprivate void doTask() throws Throwable { Thread.sleep(3000); UI_HANDLER.sendEmptyMessage(MSG_REFRESH_UI);}通过Activity的runOnUiThread方法public class MainActivity extends Activity { // … @WorkerThread private void doTask() throws Throwable { Thread.sleep(3000); runOnUiThread(new Runnable() { @Override public void run() { refreshUI(); } }); }}通过View的post方法private View view;@WorkerThreadprivate void doTask() throws Throwable { Thread.sleep(3000); view.post(new Runnable() { @Override public void run() { refreshUI(); } });}3)主线程调用子线程如果当前在子线程,想要调用主线程的方法,一般也对应几种方式,如下通过新开线程@UiThreadprivate void startTask() { new Thread() { @Override public void run() { doTask(); } }.start();}通过ThreadPoolExecutorprivate final Executor executor = Executors.newFixedThreadPool(10);@UiThreadprivate void startTask() { executor.execute(new Runnable() { @Override public void run() { doTask(); } });}通过AsyncTask@UiThreadprivate void startTask() { new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(Void… voids) { doTask(); return null; } }.execute();}异步编程痛点Android开发使用的是Java和Kotlin这两种语言,如果我们的项目中引入了Kotlin当然是最好,对于异步调用时只需要按照如下方式进行调用即可。Kotlin方案val one = async { doSomethingUsefulOne() }val two = async { doSomethingUsefulTwo() }println(“The answer is ${one.await() two.await()}”)这里适当延伸一下,类似于async await的异步调用方式,在其他很多语言都已经得到了支持,如下Dart方案Future<String> fetchUserOrder() => Future.delayed(const Duration(seconds: 2), () => ‘Large Latte’);Future<String> createOrderMessage() async { var order = await fetchUserOrder(); return ‘Your order is: $order’;}JavaScript方案function resolveAfter2Seconds(x) { return new Promise(resolve => { setTimeout(() => { resolve(x); }, 2000); });}async function f1() { var x = await resolveAfter2Seconds(10); console.log(x); // 10}f1();但是如果我们的项目中还是纯Java项目,在复杂的业务交互场景下,常常会遇到串行异步的业务逻辑,此时我们的代码可读性会变得很差,一种可选的应对方案是通过引入RxJava来解决,参考如下RxJava方案source .operator1() .operator2() .operator3() .subscribe(consumer)2 核心层动态配置业务开关、ABTest对于线上功能的动态配置背景Android(Native开发)不同于Web能够随时发布上线,Android发布几乎都要走应用平台的审核;业务上很多时候需要做AB测试或一些配置开关,以满足业务的多样性;基于以上几点,就决定了我们在Android开发过程中,对代码逻辑有动态配置的诉求。

基于这个最基本的模型单元,业务上可以演化出非常丰富的玩法,比如配置启动页停留时长、配置商品中是否展示大图、配置每页加载多少条数据、配置要不要是否允许用户进入某个页面等等。分析客户端获取配置信息通常有两种方案,分别对应推和拉。推是指通过建立客户端与服务端的长连接,服务端一旦有配置发生变化,就将变化的数据推到客户端以进行更新;拉是指客户端每次通过主动请求来读取最新配置;基于这两种模式,还会演化出推拉结合的方式,其本质就是两种方式都使用,技术层面没有新变化,这里不做赘述。下面将推拉两种方式进行对比

综合来看,如果业务上对时效性要求没有非常高的情况下,我个人还是倾向于选择拉的方式,主要原因更改配置是低频事件,为了这个低频事件去做C-S的长连接,会有种牛刀杀鸡的感觉。实现推配置的实现思考相对清晰,有配置下发客户端更新即可,但需要做好长连接断开后的重连逻辑。拉配置的实现,这里有些需要我们思考的地方,这里总结以下几点按照namespace进行多模块划分配置,避免全局一个大而全的配置;

每个namespace在初始化和每次改动时都会有个flag标识,以标识当前版本;

客户端每个业务请求都在请求头处统一拉上各flag或他们共同组合的md5等标识,为了在服务端统一拦截时进行flag时效性校验;

服务端时效性检验结果通过统一响应头下发,与业务接口隔离,上层业务方不感知;

简化配置的读写接口,让上层业务方尽可能少感知实现细节。比如,我们不需要让上层感知到本地配置的持久化信息写入的是SharedPreferences还是SQLite,而只需提供一个写入的API即可。

日志监控环境隔离、本地持久化、日志上报客户端的日志监控主要用来排查用户在使用App过程中出现的Crash等异常问题,对于日志部分总结几项值得注意的点环境隔离,release包禁止log输出;本地持久化,对于关键重要日志(如某个位置错误引起的Crash)要做本地持久化保存;日志上报,在用户授权允许的情况下,将暂存用户本地的日志进行上传,并分析具体的操作链路;这里推荐两个开源的日志框架:logger

timber

在技术侧,对于该部分做出以下关键点总结客户端埋点一般分为P点(页面级别)、E点(事件级别)和C点(自定义点);

埋点分为收集和上报两个步骤,用户量级较大时要注意对上报埋点进行合并、压缩等优化处理;

埋点逻辑是辅逻辑,产品业务是主逻辑,客户端资源紧张时要做好资源分配的权衡;热修复热修复(Hotfix)是一种对已发布上线的App在不进行应用升级的情况下进行动态更新原代码逻辑的技术方案。主要同于以下场景应用出现重大缺陷并严重影响到用户使用时,比如,在某些系统定制化较强的机型(如小米系列)上一旦进入商品详情页就出现应用Crash;

应用出现明显阻塞问题并影响到用户正常交互时,比如,在某些极端场景下,用户无法关闭页面对话框;

应用出现资损、客诉、舆论风波等产品形态问题,比如将价格单位“元”误显示为“分”;有关热修复相关的技术方案探究,可以延展出很大篇幅,本文的定位是Android项目整体的架构,因此不做详细展开。3 应用层抽象和封装对于抽象和封装,主要取决于我们日常Coding过程中对一些痛点和冗余编码的感知和思考能力。比如,下面是一段Android开发过程中常写的列表页面的标准实现逻辑public class GoodsListActivity extends Activity { private final List<GoodsModel> dataList = new ArrayList<>(); private Adapter adapter; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_goods_list); RecyclerView recyclerView = findViewById(R.id.goods_recycler_view); recyclerView.setLayoutManager(new LinearLayoutManager(this)); adapter = new Adapter(); recyclerView.setAdapter(adapter); // 加载数据 dataList.addAll(…); adapter.notifyDataSetChanged(); } private class Adapter extends RecyclerView.Adapter<ViewHolder> { @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int position) { LayoutInflater inflater = LayoutInflater.from(parent.getContext()); View view = inflater.inflate(R.layout.item_goods, parent, false); return new ViewHolder(view); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { GoodsModel model = dataList.get(position); holder.title.setText(model.title); holder.price.setText(String.format(“%.2f”, model.price / 100f)); } @Override public int getItemCount() { return dataList.size(); } } private static class ViewHolder extends RecyclerView.ViewHolder { private final TextView title; private final TextView price; public ViewHolder(View itemView) { super(itemView); title = itemView.findViewById(R.id.item_title); price = itemView.findViewById(R.id.item_price); } }}这段代码看上去没有逻辑问题,能够满足一个列表页的功能诉求。面向RecyclerView框架层,为了提供框架的灵活和拓展能力,所以把API设计到足够原子化,以支撑开发者千差万别的开发诉求。比如,RecyclerView要做对多itemType的支持,所以内部要做根据itemType开分组缓存vitemView的逻辑。但实际业务开发过程中,就会抛开很多特殊性,我们页面要展示的绝大多数列表都是单itemType的,在连续写很多个这种单itemType的列表之后,我们就开始去思考一些问题为什么每个列表都要写一个ViewHolder?

为什么每个列表都要写一个Adapter?

为什么Adapter中对itemView的创建和数据绑定要在onCreateViewHolder和onBindViewHolder两个方法中分开进行?

为什么Adapter每次设置数据之后,都要调用对应的notifyXXX方法?

公共UI,页面顶部状态栏和ActionBar、页面常用的下拉刷新能力实现、页面耗时操作时的加载进度条;

权限处理,进入当前页面所需要的权限申请,用户授权后的回调逻辑和拒绝后的异常处理逻辑;

统一拦截,结合前面提到的统一拦截对进入页面后添加支持动态配置交互的定制能力;模块化背景这里提到的模块化是指,基于App的业务功能对项目工程进行模块化拆分,主要为了解决大型复杂业务项目的协同开发困难问题。

借助框架EventBus发送事件;

基于观察者模式自实现消息转发器来发送事件;

抽象出Request=>Response的通信协议,协议层负责完成

先将通过调用方传递的 Request 路由到被调用方的协议实现层;再将实现层返回结果转化为泛化的 Response对象;最后将 Response 返回给调用方;相对于 biz-service,该方案的中间层不包含任何业务语义,只定义泛化调用所需要的关键参数。4 跨平台层跨平台层,主要是为了提高开发人效,一套代码能够在多平台运行。跨平台一般有两个接入的时机,一个是在最开始的前期项目调研阶段,直接技术选型为纯跨平台技术方案;另一个是在已有Native工程上需要集成跨平台能力的阶段,此时App属于混合开发的模式,即Native 跨平台相结合。有关更多跨平台的选型和细节不在本文范畴内,具体可以参考《移动跨平台开发框架解析与选型》,文中对于整个跨平台技术的发展、各框架原理及优劣势讲得很详细。参跨平台技术演进图

双缓存方案的核心思想就是,对时效性低或更改较少的网络资源,尽可能采取用空间换时间的方式。我们知道一般的数据获取效率:内存>磁盘>网络,因此该方案的本质就是将获取效率低的渠道向效率高的取到进行资源拷贝。基于该方案,我们在实际开发中还能拓展另一个场景,对于业务上一些时效性低或更改较少的接口数据,为了提高它们的加载效率,也可以结合该思路进行封装,这样就将一个依赖网络请求页面的首帧渲染时长从一般的几百ms降到几十ms以内,优化效果相当明显。2 线程池线程池在Android开发中使用到的频率非常高,比如在开发框架中,网络库和图片库获取网络资源需用到线程池;在项目开发中,读写SQLite和本地磁盘文件等IO操作需要用到线程池;在类似于AsyncTask这种提供任务调度的API中,其底层也是依赖线程池来完成的;如此多的场景会用到线程池,如果我们希望对项目的全局观把握的更加清晰,熟悉线程池的一些核心能力和内部原理是尤为重要的。就其直接暴露出来的API而言,最核心的方法就两个,分别是线程池构造方法和执行子任务的方法。// 构造线程池ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, keepAliveTimeUnit, workQueue, threadFactory, rejectedExecutionHandler);// 提交子任务executor.execute(new Runnable() { @Override public void run() { // 这里做子任务操作 }});其中,提交子任务就是传入一个 Runnable 类型的对象实例不做赘述,需要重点说明也是线程池中最核心的是构造方法中的几个参数。// 核心线程数int corePoolSize = 5;// 最大线程数int maximumPoolSize = 10;// 闲置线程保活时长int keepAliveTime = 1;// 保活时长单位TimeUnit keepAliveTimeUnit = TimeUnit.MINUTES;// 阻塞队列BlockingDeque<Runnable> workQueue = new LinkedBlockingDeque<>(50);// 线程工厂ThreadFactory threadFactory = new ThreadFactory() { @Override public Thread newThread(Runnable r) { return new Thread(r); }};// 任务溢出的处理策略RejectedExecutionHandler rejectedExecutionHandler = new ThreadPoolExecutor.AbortPolicy();网上有关线程池的文章和教程有很多,这里不对每个具体参数做重复表述;但我下面要把对理解线程池内部原理至关重要的——子任务提交后的扭转机制进行单独说明。

上图表明的是往线程池中不断提交子任务且任务来不及执行时线程池内部对任务的处理机制,该图对理解线程池内部原理和配置线程池参数尤为重要。3 反射和注解反射和注解都是Java语言里一种官方提供的技术能力,前者用来在程序运行期间动态读写对象实例(或静态)属性、执行对象(静态)方法;后者用来在代码处往类、方法、方法入参、类成员变量和局部变量等指定域添加标注信息。通过反射和注解技术,再结合对代码的抽象和封装思维,我们可以非常灵活的实现很多泛化调用的诉求,比如前面「热修复」章节,基于ClassLoader 的方案,其内部实现几乎全是通过反射进行 dex 更改的;

前面「网络模块」章节,Retorfit 只需要声明一个接口再添加注解即可使用,其底层也是利用了反射注解和下面要介绍的动态代理技术;

依赖注入框架dagger和androidannotations利用 Java 的 APT 预编译技术再结合编译时注解做注入代码生成;

如果了解 Java 服务端开发,主流的开发框架 SpringBoot 其内部大量运用了注射和注解的技术;反射和注解在开发中适用的场景有哪些?下面列举几点依赖注入场景普通方式public class DataManager { private UserHelper userHelper = new UserHelper(); private GoodsHelper goodsHelper = new GoodsHelper(); private OrderHelper orderHelper = new OrderHelper();}注入方式public class DataManager { @Inject private UserHelper userHelper; @Inject private GoodsHelper goodsHelper; @Inject private OrderHelper orderHelper; public DataManager() { // 注入对象实例 (内部通过反射 注解实现) InjectManager.inject(this); }}注入方式的优势是,对使用方屏蔽依赖对象的实例化过程,这样方便对依赖对象进行统一管理。调用私有或隐藏API场景有个包含私有方法的类。public class Manager { private void doSomething(String name) { // … }}我们拿到 Manager 的对象实例后,希望调用到 doSomething 这个私有方法,按照一般的调用方式如果不更改方法为 public 就是无解的。但利用反射可以做到try { Class<?> managerType = manager.getClass(); Method doSomethingMethod = managerType.getMethod(“doSomething”, String.class); doSomethingMethod.setAccessible(true); doSomethingMethod.invoke(manager, “<name参数>”);} catch (Exception e) { e.printStackTrace();}诸如此类的场景在开发中会有很多,可以说熟练掌握反射和注解技术,既是掌握 Java 高阶语言特性的表现,也能够让我们在对一些通用能力进行抽象封装时提高认知和视角。4 动态代理动态代理是一种能够在程序运行期间为指定接口提供代理能力的技术方案。在使用动态代理时,通常都会伴随着反射和注解的应用,但相对于反射和注解而言,动态代理的作用相对会比较晦涩难懂。下面结合一个具体的场景来看动态代理的作用。背景项目开发过程中,需要调用到服务端接口,因此客户端封装一个网络请求的通用方法。public class HttpUtil { /** * 执行网络请求 * * @param relativePath url相对路径 * @param params 请求参数 * @param callback 回调函数 * @param <T> 响应结果类型 */ public static <T> void request(String relativePath, Map<String, Object> params, Callback<T> callback) { // 实现略.. }}由于业务上有多个页面都需要查询商品列表数据,因此需要封装一个 GoodsApi 的接口。public interface GoodsApi { /** * 分页查询商品列表 * * @param pageNum 页面索引 * @param pageSize 每页数据量 * @param callback 回调函数 */ void getPage(int pageNum, int pageSize, Callback<Page<Goods>> callback);}并针对于该接口添加 GoodsApiImpl 实现类。public class GoodsApiImpl implements GoodsApi { @Override public void getPage(int pageNum, int pageSize, Callback<Page<Goods>> callback) { Map<String, Object> params = new HashMap<>(); params.put(“pageNum”, pageNum); params.put(“pageSize”, pageSize); HttpUtil.request(“goods/page”, params, callback); }}基于当前封装,业务便能够直接调用。GoodsApi goodsApi = new GoodsApiImpl();goodsApi.getPage(1, 50, new Callback<Page<Goods>>() { @Override public void onSuccess(Page<Goods> data) { // 成功回调 } @Override public void onError(Error error) { // 失败回调 }});问题业务需要再添加如下的查询商品详情接口。/** * 查询商品详情 * * @param id 商品ID * @param callback 回调函数 */void getDetail(long id, Callback<Goods> callback);我们需要在实现类添加实现逻辑。@Overridepublic void getDetail(long id, Callback<Goods> callback) { Map<String, Object> params = new HashMap<>(); params.put(“id”, id); HttpUtil.request(“goods/detail”, params, callback);}紧接着,又需要添加 create 和 update 接口,我们继续实现。@Overridepublic void create(Goods goods, Callback<Goods> callback) { Map<String, Object> params = new HashMap<>(); params.put(“goods”, goods); HttpUtil.request(“goods/create”, params, callback);}@Overridepublic void update(Goods goods, Callback<Void> callback) { Map<String, Object> params = new HashMap<>(); params.put(“goods”, goods); HttpUtil.request(“goods/update”, params, callback);}不仅如此,接下来还要加 OrderApi、ContentApi、UserApi 等等,并且每个类都需要这些列表。我们会发现业务每次需要添加新接口时,都得写一遍对HttpUtil#request方法的调用,并且这段调用代码非常机械化。分析前面提到接口实现代码的机械化,接下来我们尝试着将这段机械化的代码,抽象出一个伪代码的调用模板,然后进行分析。 Map<String, Object> params = new HashMap<>(); // 遍历当前方法参数, 执行以下语句 params.put(“<参数名>”, <参数值>); HttpUtil.request(“<接口路径>”, params, callback);透过每个方法内部代码实现的现象看其核心的本质,我们可以抽象归纳为以上的“模板”逻辑。有没有一种技术可以让我们只需要写网络请求所必需的请求协议相关参数,而不需要每次都要做以下几步重复琐碎的编码?手动写一个Map;往Map中塞参数键值对;调用HttpUtil#request执行网络请求;此时动态代理便能解决这个问题。封装分别定义路径和参数注解。@Target({ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)public @interface Path { /** * @return 接口路径 */ String value();}@Target({ElementType.PARAMETER})@Retention(RetentionPolicy.RUNTIME)public @interface Param { /** * @return 参数名称 */ String value();}基于这两个注解,便能封装动态代理实现(以下代码为了演示核心链路,忽略参数校验和边界处理逻辑)。@SuppressWarnings(“unchecked”)public static <T> T getApi(Class<T> apiType) { return (T) Proxy.newProxyInstance(apiType.getClassLoader(), new Class[]{apiType}, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 解析接口路径 String path = method.getAnnotation(Path.class).value(); // 解析接口参数 Map<String, Object> params = new HashMap<>(); Parameter[] parameters = method.getParameters(); // 注: 此处多偏移一位, 为了跳过最后一项callback参数 for (int i = 0; i < method.getParameterCount() – 1; i ) { Parameter parameter = parameters[i]; Param param = parameter.getAnnotation(Param.class); params.put(param.value(), args[i]); } // 取最后一项参数为回调函数 Callback<?> callback = (Callback<?>) args[args.length – 1]; // 执行网络请求 HttpUtil.request(path, params, callback); return null; } });}效果此时需要通过注解在接口声明处添加网络请求所需要的必要信息。public interface GoodsApi { @Path(“goods/page”) void getPage(@Param(“pageNum”) int pageNum, @Param(“pageNum”) int pageSize, Callback<Page<Goods>> callback); @Path(“goods/detail”) void getDetail(@Param(“id”) long id, Callback<Goods> callback); @Path(“goods/create”) void create(@Param(“goods”) Goods goods, Callback<Goods> callback); @Path(“goods/update”) void update(@Param(“goods”) Goods goods, Callback<Void> callback);}外部通过 ApiProxy 获取接口实例。// 之前GoodsApi goodsApi = new GoodsApiImpl();// 现在GoodsApi goodsApi = ApiProxy.getApi(GoodsApi.class);相比之前,上层的调用方式只有极小的调整;但内部的实现却有了很大的改进,直接省略了所有的接口实现逻辑,参考如下代码对比图。

前面讲了架构设计过程中涉及到的核心框架原理,接下来会讲到架构设计里的通用设计方案。五 通用设计方案我们进行架构设计的场景下通常是不同的,但有些问题的底层设计方案是相通的,这一章节会对这些相通的设计方案进行总结。通信设计一句话概括,通信的本质就是解决A和B之间如何调用的问题,下面按抽象出来的 AB 模型依赖关系进行逐一分析。直接依赖关系关系范式:A => B这是最常见的关联关系,A类中直接依赖B,只需要通过最基本的方法调用和设置回调即可完成。

场景页面 Activity(A)与按钮Button(B)的关系。参考代码public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // 略… // A调用B button.setText(“”); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // B回调A } }); }}间接依赖关系关系范式:A => C => B通信方式同直接依赖,但需要添加中间层进行透传。

《Android 中子线程真的不能更新 UI 吗?》:https://juejin.cn/post/6844904131136618510

《移动跨平台开发框架解析与选型》:https://segmentfault.com/a/1190000039122907

大数据Impala教程

发表评论

登录后才能评论