在本文中,我们将详细介绍大型Web应用插件化架构探索的各个方面,并为您提供关于web插件化开发的相关解答,同时,我们也将为您带来关于Android组件化架构概要、Android组件化架构资料、Andr
在本文中,我们将详细介绍大型 Web 应用插件化架构探索的各个方面,并为您提供关于web插件化开发的相关解答,同时,我们也将为您带来关于Android 组件化架构概要、Android 组件化架构资料、Android组件化架构、Android组件化架构学习笔记——组件化编程之静态变量/资源/混淆/多渠道打包的有用知识。
本文目录一览:- 大型 Web 应用插件化架构探索(web插件化开发)
- Android 组件化架构概要
- Android 组件化架构资料
- Android组件化架构
- Android组件化架构学习笔记——组件化编程之静态变量/资源/混淆/多渠道打包
大型 Web 应用插件化架构探索(web插件化开发)
现代文本编辑器沉浮
2018年微软 GitHub 后,Atom便经常被拿来调侃,所谓一山不容二虎。在 VS Code 已经成为一众前端工程师编辑器首选的当下,Atom 的地位显得很尴尬,论性能被同为 Electron 的 VS Code 秒杀,论插件,VS Code 去年插件总数就已经突破 1w 大关,而早发布一年多的 Atom 至今还停留在 8k +。再加上微软官方主导的 LSP/DAP 等重量级协议的普及,时至今日 Atom 作为曾经 Web/Electron 技术标杆应用的地位早已被 VS Code 斩落马下。
知乎上关于 Atom 的日渐衰落的讨论,始终离不开性能。Atom 的确太慢了,究其原因很大程度上是被其插件架构所拖累的。尤其是 Atom 在 UI 层面开放过多的权限给插件开发者定制,插件质量良萎不齐以及 UI 完全开放给插件后带来的安全隐患都成为 Atom 的阿喀琉斯之踵。甚至其主界面的 FileTree、Tab 栏、Setting Views 等重要组件都是通过插件实现的。相比之下 VS Code 则封闭很多,VS Code 插件完全运行在 Node.js 端,对于 UI 的定制性只有极个别被封装为纯方法调用的 API。
但另一方面,VS Code 这种相对封闭的插件 UI 方案,一些需要更强定制性的功能便无法满足,更多插件开发者开始魔改 VS Code 底层甚至源码来实现定制。例如社区很火的 VS Code Background,这款插件通过强行修改 VS Code 安装文件中的 CSS 来实现编辑器区域的背景图。而另一款 VSC Netease Music 则更激进,因为 VS Code 捆绑包中的 Electron 剔除了 FFmpeg 导致在 Webview 视图下无法播放音视频,使用此插件需要自行替换 FFmpeg 的动态链接库。而这些插件不免会对 VS Code 安装包造成一定程度的破坏,导致用户需要卸载重装。
不止编辑器 - 飞个马
Figma 是一个在线协作式 UI 设计工具, 相比 Sketch 它具有跨平台、实时协作等优点,近年来逐渐受到 UI 设计师们的青睐。而近期 Figma 也正式上线了其插件系统。
作为一个 Web 应用,Figma 的插件系统自然也是基于 JavaScript 构建的,这一定程度上降低了开发门槛。自去年6月份 Figma 官方宣布开放插件系统测试以来,已经有越来越多的 Designner/Developer 开发了300+ 插件,其中包括图形资源、文件归档、甚至是导入 3D 模型等。
▐ Figma 的插件系统是如何工作的?
这是一个基于 TypeScript + React 技术栈,使用 Webpack 构建的 Figma 插件目录结构
.
├── README.md
├── figma.d.ts
├── manifest.json
├── package-lock.json
├── package.json
├── src
│ ├── code.ts
│ ├── logo.svg
│ ├── ui.css
│ ├── ui.html
│ └── ui.tsx
├── tsconfig.json
└── webpack.config.js
在其 manifest.json 文件中包含了一些简单的信息。
{
"name": "React Sample",
"id": "738168449509241862",
"api": "1.0.0",
"main": "dist/code.js",
"ui": "dist/ui.html"
}
可以看出 Figma 将插件入口分为了 main 与 ui 两部分, main 中包含了插件实际运行时的逻辑,而 ui 则是一个插件的 HTML 片段。即 UI 与逻辑分离。安装一个Color Search 插件后观察页面结构可以发现 main 中的 js 文件被包裹在一个 iframe 里加载到页面上,关于 main 入口的沙箱机制后文中有详细的阐述。而 ui 中的 HTML 最终也被包裹在一个 iframe 里渲染出来,这将有效的避免插件 UI 层 CSS 代码导致全局样式污染。
Figma Developers 文档中 有一章节 How Plugins Run 对其插件系统运行机制进行了简单的介绍,简单来说 Figma 为插件中逻辑层的 main 入口创建了一个最小的 JavaScript 执行环境,它运行在浏览器主线程上,在这个执行环境中插件代码无法访问到一些浏览器全局的 API,从而也就无法在代码层面对 Figma 本身运行造成影响。而 UI 层有且仅有一份 HTML 代码片段,在插件被激活后被渲染到一个弹窗中。
Figma 官方博客中对其插件的沙箱机制做了详细的阐述。起初他们尝试的方案是 iframe,一个浏览器自带的沙箱环境。将插件代码由 iframe 包裹起来,由于 iframe 天然的限制,这将确保插件代码无法操作 Figma 主界面上下文,同时也可以只开放一份白名单 API 供插件调用。乍一看似乎解决了问题,但由于 iframe 中的插件脚本只能通过 postMessage 与主线程通信,这导致插件中的任何 API 调用都必须被包装为一个异步 async/await 的方法,这无疑对 Figma 的目标用户非专业前端开发者的设计师不够友好。其次对于较大的文档,postMessage 通信序列化的性能成本过高,甚至会导致内存泄漏。
Figma 团队选择回到浏览器主线程,但直接将第三方代码运行在主线程,由此引发的安全问题是不可避免的。最终他们发现了一个尚在 stage2 阶段的草案 Realm API。Realm 旨在创建一个领域对象,用于隔离第三方 JavaScript 作用域的 API。
let g = window; // outer global
let r = new Realm(); // root realm
let f = r.evaluate("(function() { return 17 })");
f() === 17 // true
Reflect.getPrototypeOf(f) === g.Function.prototype // false
Reflect.getPrototypeOf(f) === r.globalThis.Function.prototype // true
值得注意的是,Realm 同样可以使用 JavaScript 目前已有的特性来实现,即 with 与 Proxy。这也是目前社区比较流行的沙箱方案。
const whitelist = {
windiw: undefined,
document: undefined,
console: window.console,
};
const scopeProxy = new Proxy(whitelist, {
get(target, prop) {
if (prop in target) {
return target[prop]
}
return undefined
}
});
with (scopeProxy) {
eval("console.log(document.write)") // Cannot read property ''write'' of undefined!
eval("console.log(''hello'')") // hello
}
前文中 Figma 插件被 iframe 所包裹的插件 main 入口即包含了一个被 Realm 接管的作用域,你可以认为是类似这段示例代码中的一份 白名单 API,毕竟维护一份白名单比屏蔽黑名单实现起来更简洁。但事实上由于 JavaScript 的原型式继承,插件仍然可以通过 console.log 方法的原型链访问到外部对象,理想的解决方案是将这些白名单 API 在 Realm 上下文中包装一次,从而彻底隔离原型链。
const safeLogFactory = realm.evaluate(`
(function safeLogFactory(unsafeLog) {
return function safeLog(...args) {
unsafeLog(...args);
}
})
`);
const safeLog = safeLogFactory(console.log);
const outerIntrinsics = safeLog instanceOf Function;
const innerIntrinsics = realm.evaluate(`log instanceOf Function`, { log: safeLog });
if (outerIntrinsics || !innerIntrinsics) throw new TypeError();
realm.evaluate(`log("Hello outside world!")`, { log: safeLog });
显然为每一个白名单中的 API 做这样操作的工作是非常繁杂且容易出错的。那么如何构建一个安全且易于添加 API 的沙箱环境呢?
Duktape 是一个由 C++ 实现的用于嵌入式设备的 JavaScript 解释器,它不支持任何浏览器 API,自然地它可以被编译到 WebAssembly,Figma 团队将 Duktape 嵌入到 Realm 上下文中,插件最终通过 Duktape 解释执行。这样可以安全的实现插件所需 API,且不用担心插件会通过原型链访问到沙箱外部。
这是一种被称为 Membrane Pattern 的防御性的编程模式,用于在程序中与子组件(广义上)实现一层中介。简单来说就是代理(Proxy),为一个对象创建一个可控的访问边界,使得它可以保留一部分特性给第三方嵌入脚本,而屏蔽一部分不希望被访问到的特性。关于 Membrane 的详细论述可以查看 Isolating application sub-components with membranes 与 Membranes in JavaScript 这两篇文章。
这是最终 Figma 的插件方案,它运行在主线程,不需要担心 postMessage 通信带来的传输损耗。多了一次 Duktape 解释执行的消耗,但得益于 WebAssembly 出色的性能,这部分消耗并不是很大。
另外 Figma 还保留了最初的 iframe ,允许插件可以自行创建 iframe ,并在其中插入任意 JavaScript ,同时它可以与沙箱中的 JavaScript 脚本通过 postMessage 相互通信。
▐ 鱼和熊掌如何兼得?
我们把这类插件的需求总结为在 Web 应用中运行第三方代码及其自定义控件,它有与开头提到的微前端架构非常相似的一些问题。
一定程度上的 JavaScript 代码沙箱隔离机制,应用主体对第三方代码(或子应用)有一定的管控能力
样式强隔离,第三方代码样式不对应用主体产生 CSS 污染
JavaScript 沙箱
JavaScript 沙箱隔离在社区是个经久不衰的话题,最简单的 iframe 标签 Sandbox 属性就已经能做到 JavaScript 运行时的隔离,社区较为流行的是利用一些语言特性(with、realm、Proxy 等 API )屏蔽(或代理) Window、Document 等全局对象,建立白名单机制,对可能潜在危险操作的 API 重写(如阿里云 Console OS - Browser VM)。另外还有 Figma 这种尝试嵌入平台无关的 JavaScript 解释器,所有第三方代码都通过嵌入的解释器来执行。以及利用 Web Worker 做 DOM Diff 计算,并将计算结果发送回 UI 线程来进行渲染,这个方案早在 2013 年就已经有人进行了实践,这篇论文中作者将 JSDOM 这一 Node.js 平台广泛流行的测试库运行在 Web Worker。而近些年来也有 preact-worker-demo 、react-worker-dom 等项目基于 Web Worker 的 DOM Renderer 尝试将 DOM API 代理到 Worker 线程。而 Google AMP Project 在JSCONF 2018 US 对外公布的 worker-dom 则将 DOM API 在 Web Worker 端实现了 DOM API,虽然实践下来还存在一些问题(例如同步方法无法模拟),但 WorkerDOM 在性能和隔离性上都取得了一定成果。
以上这些解决方案被广泛的应用在各种插件化架构的 Web 应用中,但大多都是 Case By Case,每种解决方案都有各自的成本与取舍。
CSS 作用域
CSS 样式隔离方案中,如上文中 Figma 使用 iframe 渲染插件界面,牺牲一部分性能换来了相对完美的样式隔离。而在现代前端工程化体系下,可以通过 CSS Module 在转译时对 class 添加 hash 或 namespace 等方式实现,这类方案较为依赖插件代码编译过程。而更新潮的是利用 Web Component 的 Shadow DOM,将插件元素用 Web Component 包裹起来,Shadow Root 外部样式无法作用于内部,同样 Shadow Root 内部的样式也无法影响到外部。
最后
本文列举了目前编辑器、设计工具这类大型 Web 应用插件化架构下所面临的的一些问题,以及社区实践的解决方案。不论是让人又爱又恨的 iframe ,还是 Realm、Web Worker 、 Shadow DOM 等,目前来说每种方案都有各自的优势与不足。但随着 Web 应用的复杂度增长,插件化这一需求也逐渐被各大标准化组织所重视起来。下一篇将着重介绍 KAITIAN IDE 中插件架构的探索与实践,包括 JavaScript 沙箱、CSS 隔离、Web Worker 等。
淘宝技术部-前端工程&商家业务-招贤纳士
本文分享自微信公众号 - 淘系技术(AlibabaMTT)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。
Android 组件化架构概要
组件化相信大家都很清楚他是什么,Android的一种开发架构,并且备受团队推崇,确实组件化在我看来对团队协作方面是帮助巨大的,随着应用开发的业务不断累积,我觉得我们更加应该关注架构的搭建,而不是一味的实现代码,这种重复工作以及无效时间就太多了,最近也阅读了很多关于架构的书籍,收获良多,所以今天的文章,就带领大家来领略一下组件化架构的美妙。
我在网的Android新课核心知识点如下:
Android X/音视频开发/社交匹配算法/即时通信/语音识别/App优化/安全加固
手把手完成商业级社交App开发进阶Android高级工程师
一.组件化概括
相对于Mvp来讲,组件化更多的是面向Module,而不是为了解耦而解耦,在我看来,组件化更多的是团队协作分工清晰,且划分明确,每个人都有自己负责的模块,避免无意义的穿插以及Git的代码冲突。
我们常见的架构分层,包括MVC,MVP,都是以如下的架构形式出现:
主流的一个App和一个BaseModule,或者多个BaseModule,不管你是MVC,还是MVP都是如此,针对的是代码的解耦,而不是真正意义上的业务解耦,如果项目十分的庞大,你可能还会有BaseLibrary等存在,随着时间的推移,互相调用代码的揉合,你会发现框架会变得越来越糟糕,哪怕你使用MVP去解耦,也如下图中所示,纯属针对代码层的解耦,而不是真正意义上的架构解耦。
这个时候就要反思以及思考如何解决这个问题了,组件化就诞生了,组件化面对的是组件
“多Module划分业务和基础功能”
这就是组件化的核心思想了,我们一定要分清楚,组件,模块,以及库,是不一样的。
组件 :指单一职责的功能模块,如视频组件,支付组件,数据库组件。
模块 :指具体的业务逻辑模块,比如微信主页的四个页面,我们就可以定义为四个模块,由于模块的对象是业务,所以比组件的广度来讲要大许多,因为随着他的业务进展,他会包含越来越多的组件。
库 :单一职责的提供某个或者某些功能。
有了这些概念,就会诞生出很多的抽象名称,比如组件化,模块化,插件化等,追源溯本你会发现,其实他们的出发点,都是为了架构的不断演进而努力,只是趋向于目标不一致,所以文章开头我才说MVC,MVP之类的是针对代码的解耦,也就是让代码看得更加的清晰,一个View中实现一个Presenter后单一职责即可,但是组件化就不一样,他是针对业务模块进行区分,并且它也算是海纳百川,你具体代码怎么写,他也不负责,他维护好更大的框架即可。
其实说白了,就是将业务多Module的分层,比如大家常常用微信来举例子,微信有四个Tab,分别是:微信,通讯录,发现,我,那么我们是不是可以将其分成四个Module呢?
的确是的,如图:
从图中可以看到,App作为一个UI的基础空壳,他将主页的逻辑实现即可,然后则是引入四个Module,他们都将依赖BaseModule,而BaseModule纯粹就是一个Library,主要封装一些工具类,管理类,和帮助类,我们的组件化历程则是从四个Module开始,他们既可以作为Module存在让App互相调用,又可以作为App单独存在调试,假设组内五个人开发,刚好可以让组长负责App和BaseModule,而其余四人分别负责微信,联系人,发现,我,即可,分工明确。
那么我们现在分工是明确了,如何在Android Studio中搭建组件化,则又是另外一回事儿了,既要考虑到App作为单独存在时的配置,也要考虑合并时主程序的运行。所以困难的一点在于如何初始化这个组件化项目的基础上,那么我们来开始搭建吧。
二.组件化项目
1.开发思路
我们既然要创建一个组件化的项目,那么首先就必须理清楚开发思路,首当其冲就是组件间的跳转,由于组件之间并没有直接的依赖关系,那么startActivity显然是无法使用了,这个时候我们可以使用阿里的Arouter来实现跳转:
ARouter
紧接着就是合并问题了,我们可以定义一个自定义的Gradle文件来控制是否Module单独编译。
其次就是注解了,说实话,注解的帮助还是很大的,至少我们少了很多的findViewById,我们可以使用Butterknife黄油刀来实现初始化,不过这个是选学,可以选择性添加,不添加也不碍事。
Butterknife
那么回到问题的根本,开发思路就是如此,先构建好每个Module的Gradle文件,然后再来统筹,不过这些都是给予一个完整的App Module来实现的,想单一的运行Module为Apk,我们至少还需要单独创建一个Activity作为启动项,以及一个清单文件作为配置项才行。
那么我们开始吧…
2.创建Module
首先我们创建五个Module,只要点击 File - New - New Module - Android Library 即可,以此类推,我们分别创建 :
- ChatModule
- ContactModule
- FindModule
- MeModule
- BaseModule
分别对应的是微信,联系人,发现,我和基类。
3.自定义Gradle
我们在Project的根目录创建一个config.gradle来统筹我们的依赖和版本信息
里面比较关键的还是我们配置的 IS_BUILD_MODULE 变量,如果可以设置当为true的时候,Module可以单独运行,即Application,如果为false,那么我们就只能让Module作为Library来执行,至于其他的,都是一些普通的配置。
IS_BUILD_MODULE 这个开关还可以放在gradle.properties中,实际上我觉得放在gradle.properties会更好,这样对Git提交也不会在不严谨的时候造成代码冲突,不过既然团队已经在构建组件化,想必技术选型也其实早就配置好了,那么该用到哪些框架,也都提前配置好了,倒不会引起太大的骚乱。
4.主App配置
主App的配置主要集中于如何添加依赖,不管是组件Module还是BaseModule,都是一个过程
[图片上传失败…(image-aa2eb4-1607329172143)]
我们可以看到,BaseModule的添加是必然的,其次就是四个组件了,四个组件我们通过判断IS_BUILD_MODULE这个值来确定是否添加,下面是ARouter和Butterknife的添加,这两个比较特殊,所以才每个build.gradle都要添加,而普通的依赖项,我们只需要在BaseModule中添加即可。
而其他要关注的点就如下:
我们要添加黄油刀的插件,以及在defaultConfig中加上路由的配置,以及支持Java 8 ,这三个配置在每一个Module中都需要配置。
如果没有添加路由的配置,则无法跳转,如果没有添加Java 8 的支持,则会报错:
Exception from call site #1 bootstrap method
当然,我们配置黄油刀,还需要在Project的build.gradle中配置插件
5.组件的配置
组件的配置在于两方面,一方面是作为依赖,一方面作为一个独立的App
还是先来看下他的build.gradle
[图片上传失败…(image-5b7df3-1607329172143)]
可以看到,这就是他的配置文件,其实很普通,路由和黄油刀的标配,以及添加BaseModule,之后哪怕是添加其他第三方框架也是在BaseModule中配置。当然,正如我们主Module的配置一样,组件Module也需要配置黄油刀的插件,以及在defaultConfig中加上路由的配置,以及支持Java 8 ,这是必然的,除此之外,他有两个特殊的地方:
可以看到,我们会根据我们定义的变量来加载他到底是App还是Module,这样就能做到我们的随意切换了,如果是App的话,我们还需要给他配置一个包名ID,你可以给他设置相同的包名,我这里做了一些小区分而已。
还有一个地方则是清单文件的配置了 [图片上传失败…(image-6b3943-1607329172143)] sourceSets可以帮助我们替换加载的文件,我们如果是App的话,重新配置了一个清单文件,因为他包含了App所需要的一切,如果是module,则不处理。来看下他们之间的区别:
首先是作为Module的清单文件:
可以看出,他只有必要的四大组件需要配置的时候则配置,不然的话就是空的了
而再看下作为App的清单文件:
他可能比你想象的东西要多,他有一个Application根节点配置App的参数,还需要一个启动项Activity,不然也启动不起来。
6.BaseModule
基类的Module给我们更多的是封装的帮助,我们需要使用到的资源以及第三方框架都是可以放在这里的,老规矩,来看下他的build.gradle
你理解了他,他就变得透明,就是一些常规的添加,并且黄油刀的插件,以及在defaultConfig中加上路由的配置,以及支持Java 8 必不可少。
7.Base类
作为高度统筹的项目架构,在Base方面实际上也是有一定的约束的,这里我以三个类为案例,分别是BaseApp,BaseActivity以及BaseFragment
A.BaseApp
BaseApp方面主要还是做一些初始化的作用,不过你也可以在BaseModule中添加一个统一初始化的方法,BaseApp需要注意的点:
- 1.组件统一Context可以单例Application
- 2.组件Module需要继承自BaseApp
B.BaseActivity
BaseActivity中,我们初始化了黄油刀,以及封装了View,这样的好处就是不用写这么多onCreate了,使用起来也异常的方便
[图片上传失败…(image-66e0c9-1607329172143)]
C.BaseFragment
至于Fragment,和Activity的封装是一样的
唯一要注意的就是黄油刀的使用了,不过这个我们会在黄油刀中所讲到
8.ARouter
路由是帮助我们组件间跳转的,根据Github上添加好依赖之后,我们需要注意一下的几点
- 1.Path至少是两个层级 类似于 /Test/AppMain 程序会先去找Test再去找AppMain
- 2.Path第一层级不能重复,如果又出现一个 /Test/AppTest 则会出现找不到的异常
只要在Activity前一行添加path即可定义目标地址
这里我封装了一个帮助类提供跳转 [图片上传失败…(image-6213ee-1607329172143)]
地址也是自己定义的 [图片上传失败…(image-aff4f2-1607329172143)]
9.Butterknife
黄油刀只是作为高效开发的组件,你也同样可以使用原生的findViewById,那么如果我们使用到黄油刀,需要注意什么呢?
- 1.Activity中绑定ButterKnife.bind(this);
- 2.Fragment以及Adapter中绑定ButterKnife.bind(this,mView);
- 3.Module中引用ID需要使用R2,如图
三.组件化的实现
这里我基于此架构写了一个仿微信UI的Demo内容,让大家更加清晰的认识组件化,那么我们先来看下预览:
首先我们App的空壳就是引用的四个组件,每个组件中都存在一个Fragment,App Module 只是做整合,每个组件Module单独负责一个业务,并且他们都是可以单独作为Apk存在的
[图片上传失败…(image-9b5e6c-1607329172143)]
我们可以全部都单独的实现运行
而在模拟器上的表现如下:
四.组件化的问题
1.资源名冲突
如果资源名冲突,要么他改,要么你改,如果他改不了,那就你改,如果你也改不了,那么我们就通过Gradle去转换了,在Project的build.gradle中添加
2.包名问题
部分第三方SDK需要填写包名,那么这个时候如果你填写主Module的包名的话,申请的Key实际上组件Module是用不了的,这里可以选择组件Module使用同一个包名,或者通过BaseModule的包名去申请,这样在BaseModule中再封装一层,提供给外部访问即可。
3.组件间的通讯
组件间通讯,可以选择EventBus,广播等方式,复杂交互的话,ARouter提供了一个Provider供我们数据交互。
4.组件化与MVP
看到不少开源使用了组件化还使用了MVP,我自己也尝试写了一个,只能感叹代码量增加很多,敲得手抖疼了,随即开始思考这个架构,组件化应对的是业务解耦,而MVP,更倾向于代码解耦,结合起来肯定代码更加清晰,更加完善,但是我无法提供一个这么庞大的项目练手,所以如果你的项目不是很庞大,可以择优选择,架构再好,也要看适不适合。
五.ARouter拦截
我们在一些特殊的场景的时候,需要拦截跳转,一般的例子比如跳转登录页的时候就可以拦截了,那么ARouter的拦截怎么处理的呢?我们来看下代码
我们只需要配置Interceptor的注解即可,其中优先级和name可以自己定义,而init是初始化的,并且只会走一次,主要还是在process中我们可以根据判断条件来控制是否拦截,这里我写了一些伪代码,如果想获取当前Activity对象的话,可以在Application中监听Activity生命周期获得
六.结尾
组件化好处还是很多的,比如提升编译速度,毕竟都是单Module开发,高度统筹的情况下,还能基于BaseModule来减少重复代码,要是发现哪个Module还单独写工具类,拉出去枪毙。
看那文章毕竟是片面的,我还是由衷的希望读者在看完文章之后下载代码来运行一遍,看看代码的结构,那么理解这个demo也就八九不离十了,再结合自己的理解,就会看到更加广阔的天地了。
源代码地址: AndroidComponent
@H_839_301@本文在开源项目:https://github.com/Android-Alvin/Android-LearningNotes 中已收录,里面包含不同方向的自学编程路线、面试题集合/面经、及系列技术文章等,资源持续更新中…
Android 组件化架构资料
1.Andromeda —— 适用于多进程架构的组件通信框架
2.AndroidModulePattern
3.Android组件化框架设计与实践
4.Android组件化:stitch框架
5.从零开始搭建Android组件化框架
6.安居客 Android 项目架构演进
7.Android彻底组件化demo发布
8.组件化知识梳理
9.Android-Sun-Framework 模块化开发框架
10.仿京东app 采用组件化架构
11.业界首个支持渐进式组件化改造的Android组件化开源框架
12.一套完整有效的android组件化方案DDComponentForAndroid
13.Android项目组件化架构
14.终极组件化框架项目方案
15.)Android官方MVVM框架实现组件化之整体结构
16.组件化架构漫谈
17.Android 组件化探索与思考
Android组件化架构
文章目录
- 首语
- 简介
- 特点
- 组件化编程
- 组件化Application
- 组件间通信
- 第三方总线框架
- 对比
- 总结
- 组件间跳转
- ARouter路由跳转
- 组件化存储
- 组件化权限管理
- 动态权限框架
- 组件化资源冲突
- AndroidMainfest冲突
- 包冲突
- 资源名冲突
- 组件化混淆
- 组件化多渠道
- Gradle优化
- 调试优化
- 总结
首语
时间似流水,已经是2021年的三月了,抖擞精神。新的一年继续努力奋斗。
简介
在项目开发中,将公用的代码提取到common_module中,将某些单独功能封装到lib_module中,再根据业务划分module,团队成员分别开发各自的模块。
但随着项目的迭代,功能越来越多,增加了一些业务模块后,相互调用的情况会增多,就会发生各个业务模块之间的耦合非常严重,导致代码难以维护且扩展性很差。组件化就应用而生了。
组件化基础:多module划分业务和基础功能。
组件:单一的功能组件,如适配,支付,路由组件等,可单独抽出来形成SDK。
模块:独立的业务模块,如直播,首页模块等。模块可能包含多个不同组件。
特点
- 避免重复造轮子,节省开发,维护成本。
- 通过组件和模块合理的安排人力,提高开发效率。
- 不同项目公用一个组件或模块,保证技术方案的统一性。
- 未来插件化公用一套底层模型做准备。
组件化编程
组件化Application
如果功能module有Application,主module没有自定义Application,自然引用功能module的Application。
如果功能module有两个自定义Application,会编译出错,需要解决冲突。可以使用tools:replace="android:name"
解决,因为App编译最终只会允许声明一个Application。
组件间通信
组件中的模块是相互独立的,并不存在依赖,没有依赖无法传递信息。这时,需要借助基础层(CommonModule),组件层的模块都依赖于CommonModule,它是模块间信息交流的基础。
Android中Activity,Fragment及Service信息传递较复杂,通过广播的形式实现消息传递耗时且不安全,产生了事件总线机制。它是对发布-订阅模式的一种实现。它是一种集中式事件处理机制,允许不同的组件之间进行彼此通信而又不需要相互依赖,达到一种解耦的目的。
第三方总线框架
- EventBus
EventBus是一个Android端优化的publish/subscribe消息总线,简化了应用程序内各组件间、组件与后台线程间的通信。具体使用方法可参考我的博客:Android事件总线之EventBus。 - RxBus
RxBus是基于RxJava响应式编程衍生而来的一种组件间通信的模式,目前项目开发网络请求都是使用Retofit+RxJava框架搭配实现的,具体使用方法可参考我的博客:Android RxJava的使用;Retrofit。
对比
在线程调度方面,RxJava的线程调度更加优秀,且通过多种操作符,链式编写代码,是优于Eventbus的,但因为没有使用反射机制,运行效率低于EventBus。
总结
在实际项目开发中,通信事件要放在CommonModule中,CommonModule也需要依赖总线框架。但是不同模块增删时都需要添加或删除消息模型,让事件总线整个架构显得非常臃肿且复杂,违背了组件化的原则。解决方案是抽离出一个事件总线模块,CommonModule依赖这个模块,消息模型都在事件总线模块中。
组件间跳转
在组件化中,两个功能模块不存在直接依赖的,通过CommonModule间接依赖。一般一个Activity跳转到另外一个Activity中,使用startActivity发送一个intent,但是引用不了其它模块的Activity。可通过隐式Action方式实现跳转。需要注意的是移除模块时同时也要移除跳转,否则会发生崩溃。
ARouter路由跳转
隐式Action并不是最好的跳转方式,ARouter此时就出现了。
ARouter是阿里巴巴Android技术团队开源的一款用于帮助 Android App 进行组件化改造的路由框架,支持模块间的路由、通信、解耦。
Github地址:https://github.com/alibaba/ARouter
- 使用
首先在CommonModule中添加依赖:
implementation 'com.alibaba:arouter-api:x.x.x'
annotationProcessor 'com.alibaba:arouter-compiler:x.x.x'
然后annotationProcessor会使用javaCompileOptions 这个配置来获取当前module的名字,在各个模块的build.gradle的defaultConfig属性中加入:
android {
defaultConfig {
...
javaCompileOptions {
annotationProcessorOptions {
arguments = [AROUTER_MODULE_NAME: project.getName()]
}
}
}
}
每个模块的dependencies属性需要ARouter apt的引用,不然无法在apt中生成索引文件,不能跳转成功。
dependencies {
annotationProcessor 'com.alibaba:arouter-compiler:x.x.x'
}
在Application中初始化:
if (isDebug()) {
ARouter.openLog();
ARouter.openDebug();
}
ARouter.init(mApplication);
以Activity跳转为例,首先需要在跳转Activity添加注解Route,path是路径
@Route(path = RouterPath.LOGIN_PAGE)
public class LoginActivity extends BaseActivity<activityloginBinding> {}
//路由跳转尽量统一管理,可以module路径命名。
String SURROUNDING_PAGE = "/surrounding/ui";
String TRAVEL_PAGE = "/travel/ui";
String CITY_SERVICE_PAGE = "/city_service/ui";
需要跳转Activity时,使用如下,build参数为跳转Activity路径。
ARouter.getInstance().build(RouterPath.LOGIN_PAGE).navigation();
具体使用参考官方中文说明文档:https://github.com/alibaba/ARouter/blob/master/README_CN.md
组件化存储
Android原生存储方式有五种,在组件化中也完全通用。
组件化中流行的数据库有Jetpack套件中的Room。它通过注解的形式完成数据库的创建、增删改查等操作。使用简单、高效。
组件化设计中考虑到解耦,将数据库层独立为一个模块,关于数据库的操作都在此module中,且依赖于CommonModule。
组件化权限管理
在各个module的AndroidManifest.xml中,我们可以看到各个module的权限申请,最终会合并到根AndroidManifest.xml文件中。
在组件化开发中,我们将normal级别的权限放在CommonModule中,在每个module中分别申请dangerous级别的权限,这样的好处是当添加或移除某个模块时移除dangerous级别权限,做到最大程度的解耦。
动态权限框架
RxPermission是基于RxJava的Android动态权限申请框架。
Github地址:https://github.com/tjianssbruyelle/RxPermissions。
public void initPermissions(String[] permissions, PermissionResult permissionResult) {
if (rxPermissions == null) {
rxPermissions = new RxPermissions(this);
}
rxPermissions.requestEachCombined(permissions)
.subscribe(permission -> {
if (permission.granted) {
permissionResult.onSuccess();
} else if (permission.shouldShowRequestPermissionRationale) {
permissionResult.onFailure();
} else {
permissionResult.onFailureWithNeverAsk();
}
});
}
RxPermission与RxJava结合,非常精简,简单实用。
组件化资源冲突
AndroidMainfest冲突
AndroidMainfest中引用了Application的app:name属性,冲突时使用tools:replace="android:name"来声明Application是可被替换的。
包冲突
当包冲突出现时,使用gradle dependencies命令查看依赖目录树,依赖标注了*号的,表示依赖被忽略。因为有其它顶级依赖也依赖于这个依赖,可以使用exclude排除依赖,例如:
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2', {
exclude group: 'com.android.support', module: 'support-annotations'
}
资源名冲突
在多module开发中,无法保证多个module中全部资源的命名不同,出现相同资源名选取的规则是后编译的模块会覆盖之前编译的模块的资源字段中的内容,出现相同会造成资源引用错误的问题。解决办法有两种:
第一种:资源出现冲突时进行重命名。
第二种:gradle的命名提示机制,使用resourcePrefix字段:
android {
resourcePrefix "组件名_"
}
所有的资源命必须以指定的字符串作为前缀,否则会报错,但是resourcePrefix不能限定图片资源,图片资源的还需要手动去修改资源名。
组件化混淆
Android Studio使用ProGuard进行混淆,它是一个压缩、优化和混淆Java字节码文件的工具,可以删除无用的类和注释,最大程度优化字节码文件。
混淆会删除项目无用的资源,有效减少apk安装包的大小。
混淆增加了逆向工程的难度,更加安全。
混淆有Shrinking(压缩)、Optimization(优化)、Obfuscation(混淆)、Preverification(预校验)四项操作。
buildTypes {
release {
//是否打卡混淆
minifyEnabled false
//是否打开资源压缩
shrinkResources true
设置proguard的规则路径
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
每个module在创建之后,都会自带一个proguard-rules.pro的自定义混淆文件,每个module可以有自己的混淆规则。
组件化中,如果每个module都使用自身混淆,会出现重复混淆的现象,造成查询不到资源文件的问题。我们需要保证apk生成时候只需要一次混淆。
方案:将固定的第三方库混淆放到CommonModule中,每个module独有的引用库混淆放在各自的的proguard-rules.pro中,最后在App 的proguard-rules.pro中放入Android基础属性混淆声明,例如四大组件和全局混淆等配置。可以最大限度的完成混淆解耦工作。
组件化多渠道
当项目开发中需要生成用户端和管理端,又或者某些版本不需要支付、分享等,我们没必要嵌入这些模块,同时可以减少业务量和包容量。
我们需要输出多个App时,维护和开发成本会提升,如何降低开发成本,并且合理解耦呢,就需要使用到多渠道了。例如:
productFlavors {
phone {
applicationId "com.zdww.enjoyluoyang"
manifestPlaceholders = [name:"用户端",icon:"@mipmap/logo"]
}
terminal {
applicationId "com.zdww.enjoyluoyang_terminal"
versionCode 2
versionName "1.1.0"
manifestPlaceholders = [name:"管理端",icon:"@mipmap/logo"]
}
}
phoneImplementation project(path: ":module_my")
我们通过productFlavors设置多渠道,manifestPlaceholders设置不同渠道的不同属性,这些属性在AndroidMainfest中声明才能使用,设置xxxImplementation可以配置不同渠道需要引用的module。在Android Studio中左侧边栏可以找到Build Variants选择不同的Active Build Variant。
对于不同渠道需要引入新的类或文件,可在项目目录下新建不同渠道文件夹,将文件放入其中,各为其用。
Gradle优化
Gradle本质是一个自动化构建工具,基于Groovy的特定领域语言(DSL)来声明项目设置,Android Studio构建工程时,利用gradle编写的插件来加载工程配置和编译文件。
组件化中,每个module都有一个build.gradle文件,每个module的build.gradle文件都拥有一些必需的属性,同一个Android工程,在不同模块要求这些属性一致,例如compileSdkVersion等,如果引用不一致,属性不会被合并并引入到工程中,会造成资源的重复,降低编译效率。
必须有一个统一、基础的Gradle配置,创建一个version.gradle文件,编写一些变量,在project的build.gradle下buildscript添加
apply from :"versions.gradle"
类似引用静态变量的方式来引用属性,也可以将项目使用的仓库在version.gradle中统一配置。只需在project.gradle中添加即可。
ext.deps = [:]
def versions = [:]
versions.gradle = "4.0.1"
versions.appcompat = "1.2.0"
versions.constraintlayout = "2.0.4"
versions.junit = "4.12"
versions.ext_junit = "1.1.2"
versions.espresso_core = "3.3.0"
versions.multidex = "1.0.3"
def build_versions = [:]
build_versions.compileSdk = 29
build_versions.minSdk = 19
build_versions.targetSdk = 29
build_versions.versionCode = 11
build_versions.versionName = "1.4.5"
build_versions.application_id = "com.example.yhj"
build_versions.gradle = "com.android.tools.build:gradle:$versions.gradle"
ext.build_versions = build_versions
def view = [:]
view.constraintlayout = "androidx.constraintlayout:constraintlayout:$versions.constraintlayout"
view.recyclerview = "androidx.recyclerview:recyclerview:$versions.recyclerview"
view.glide = "com.github.bumptech.glide:glide:$versions.glide"
view.glide_compiler = "com.github.bumptech.glide:compiler:$versions.glide_compiler"
view.circleimageview = "de.hdodenhof:circleimageview:$versions.circleimageview"
view.gif_drawable = "pl.droidsonroids.gif:android-gif-drawable:$versions.gif_drawable"
view.material = "com.google.android.material:material:$versions.material"
deps.view = view
def addRepos(RepositoryHandler handler) {
handler.google()
handler.jcenter()
handler.flatDir { dirs project(':lib_common').file('libs') }
handler.maven { url "https://jitpack.io" }
}
ext.addRepos = this.&addRepos
然后我们在module的build.gradle下只需这样使用即可。
android {
compileSdkVersion build_versions.compileSdk
defaultConfig {
minSdkVersion build_versions.minSdk
targetSdkVersion build_versions.targetSdk
versionCode build_versions.versionCode
versionName build_versions.versionName
api deps.android.appcompat
api deps.view.constraintlayout
//glide
api deps.view.glide
annotationProcessor deps.view.glide_compiler
这样统一参数变量配置,使得项目不会引用到多个不同版本的Android工具库,且统一配置,避免增加apk容量。
调试优化
组件化支持将单一模块做成App启动,然后用于调试测试,保证了单独模块可以分离调试。
需要变更的地方:
apply plugin: 'com.android.library'——>apply plugin: 'com.android.application'
在src中建立debug文件夹,debug文件夹用于放置调试需要的AndroidMainfest.xml文件,java文件,res文件等,且需要设置默认启动的Activity。
我们可以设置一个isModule的变量来作为集成开发和组件开发模式的开关,在module的build.gradle中可以这样判断:
if (isModule.toBoolean()) {
apply plugin: 'com.android.application'
} else {
apply plugin: 'com.android.library'
}
同时集成开发模式下需要排除debug文件夹下的所有文件。
sourceSets {
main {
if (isModule.toBoolean()) {
manifest.srcFile 'src/main/module/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
//集成开发模式下排除debug文件夹中的所有文件
java {
exclude 'debug/**'
}
}
}
}
原App的build.gradle需要移除已经单独调试的模块依赖。
dependencies {
if (!isModule.toBoolean()) {
implementation project(path: ':module_my')
}
}
总结
Android项目中进行组件化实践可以提高复用性,降低耦合,本文主要对项目中组件化常用使用场景进行总结,更多相关场景在项目开发中再进行总结。
Android组件化架构学习笔记——组件化编程之静态变量/资源/混淆/多渠道打包
一.组件化的静态变量:
- R.java的生成:
各个module会生成aar文件,并且被引用到Application module中,最终合并为apk文件。当各个次级module在Application module中被解压后,在编译时资源R.java会被重新解压到build/generated/source/r/debug(release)/包名/R.java中。
当每个组件中的aar文件汇总到App module中时,也就是编译的初期解析资源阶段,其每个module的R.java释放的同时,会检测到全部的R.java文件,然后通过合并,最后合并成唯一的一份R.java资源。
- R2.java及ButterKnife:
ButterKnife是一个专注于Android View的注入框架,可以大量的减少findViewById和setonClickListener操作的第三方库。
注解中只能使用常量,如不是常量会提示attribute value must be contant的错误。可以在使用替代方法,原理是将R.java文件复制一份,命名为R2.java。然后给R2.java变量加上final修饰符,在相关的地方直接引用R2资源。
如项目中已经使用ButterKnife维护迭代了一段时间,那么使用R2.java的方案适配成本是最低的。
最好的解决方式还是使用findViewById,不使用注解生成的机制。
下面可以使用泛型来封装findViewById,以减少编写的代码量:
@Override
protected void onCreate(@androidx.annotation.Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
TextView textView = generateFindViewById(R.id.rl_full_view);
}
protected <T extends View> T generateFindViewById(int id) {
//return 返回view时加上泛型T
return (T)findViewById(id);
}
二.资源冲突:
在组件化中,Base module和功能module的根本是Library module,编译时会依次通过依赖规则进行编译,最底层的Base module会被先编译成aar文件,然后上一层编译时因为通过compile依赖,也会将依赖的aar文件解压到模块的build中。
AndroidMainfest冲突问题:
AndroidMainfest中引用了application的app:name属性,当出现冲突时,需要使用tool:replace= "android:name"来声明application是可被替代的。某些AndroidMainfest.xml中的属性被替代的问题,可以使用tool:replace来解决冲突。
包冲突:
如想使用优先级低的依赖,可以使用exclude排除依赖的方式。
compile('') {
exclude group:''
}
资源名冲突:
在多个module开发中,无法保证多个module中全部资源的命名是不同的。假如出现相同的情况,就可能造成资源引用错误的问题。一般是后后编译的模块会覆盖之前编译的模块的资源字段中的内容。
解决方法:一种是当资源出现冲突时使用重命名的方式解决。这就要要求我们在一开始命名的时候,不同的模块间的资源命名都不一样,这是代码编写规范的约束;另一种时Gradle的命名提示机制,使用字段:
android {
resourcePrefix "组件名_"
}
所有的资源名必须以指定的字符串作为前缀,否者会报错,resourcePrefix这个值只能限定xml中资源,并不能限定图片资源,所有图片资源仍然需要手动去修改资源名。
三.组件化混淆:
混淆基础:
混淆包括了代码压缩/代码混淆及资源压缩等优化过程。
Android Studio使用ProGuard进行混淆,ProGuard是一个压缩/优化和混淆Java字节码文件的工具,可以删除无用的类/字段/方法和属性,还可以删除无用的注释,最大限度地优化字节码文件。它还可以使用简短并无意义的名称来重命名已经存在的类/字段/方法和属性。
混淆的流程针对Android项目,将其主项目及依赖库未被使用的类/类成员/方法/属性移除,有助于规避64k方法的瓶颈;同时,将类/类成员/方法重命名为无意义的简短名称,增加了逆向工程的难度。
混淆会删除项目无用的资源,有效减少apk安装包的大小。
混淆有Shrinking(压缩)/Optimiztion(优化)/Obfuscation(混淆)/Preverfication(预校验)四项操作。
buildTypes {
release {
minifyEnabled false //是否打开混淆
shrinkResources true //是否打开资源混淆
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
//用于设置proguard的规则历经
}
}
每个module在创建时就会创建出混淆文件proguard-rules.pro,里面基本是空的。
#指定压缩级别
-optimizationpasses 5
#不跳过非公共的库的类成员
-dontskipnonpubliclibraryclassmembers
#混淆时采用的算法
-optimization !code/simpliffcation/arithetic,!field/*,!class/merging/*
#把混淆类中的方法名也混淆了
-useuniqueclassmembernames
#优化时允许访问并修改修饰符的类和类成员
-allowaccessmodification
#将文件来源重命名为“SourceFile”字符串
-renamesourefileattribute SoureFile
#保留行号
-keepattributes SoureFile,LineNumberTable
以下时打印出的关键的流程日志:
-dontpreverify
#混淆时是否记录日志
-verbose
#apk包内所有class的内部结构
-dump class_files.txt
#未混淆的类和成员
-printseeds seed.txt
#列出从apk中删除的代码
-printusage unused.txt
#混淆前后的映射
-printmapping mapping.txt
以下情形不能使用混淆:
- 反射中使用的元素,需要保证类名/方法名/属性名不变,否则混淆后会反射不了;
- 最好不让一些bean对象混淆;
- 四大组件不建议混淆,四大组件在AndroidManifest中注册申明,而混淆后类名会发生更改,这样不符合四大组件的注册机制;
-keep public class * extend android.app.Activity
-keep public class * extend android.app.Application
-keep public class * extend android.app.Service
-keep public class * extend android.app.content.broadcastReceiver
-keep public class * extend android.app.content.ContentProvider
-keep public class * extend android.app.backup.broadAgentHelper
-keep public class * extend android.app.preference.Preference
-keep public class * extend android.app.view.View
-keep public class * extend android.app.verding.licensing.ILicensingService
- 注解不能混淆,很多场景下注解被用于在运行时反射一些元素;
-keepattributes *Annotation
- 不能混淆枚举中的value和valueOf方法,因为这两个方法时静态添加到代码中运行,也会被反射使用,所以无法混淆这两种方法。应用使用枚举将添加很多方法,增加了包中的方法数,将增加dex的大小;
-keepclassmembers enum * {
public static **[] values();
public static ** vauleOf(java.lang.String);
}
- JNI调用Java方法,需要通过类名和方法名构成的地址形成;
- Java使用Native方法,Native是C/C++编写的,方法是无法一同混淆的;
-keepclasswithmembername class * {
native <methods>;
}
- JS调用Java方法;
-keepattributes *JavascriptInterface*
- WebView中JavaScript调用方法不能混淆;
-keepclassmembers class fqcn.of.javascript.interface.for.Webview {
public *;
}
-keepclassmembers class * extends android.webkit.WebViewClient {
public void *(android.webkit.Web,java.lang.String,android.graphics.Bitmap);
public boolean *(android.webkit.Web,java.lang.String);
}
-keepclassmembers class * extends android.webkit.WebViewClicent {
public void *(android.webkit.Web,java.lang.String);
}
- 第三方库建议使用其自身混淆规则;
- Parcelable的子类和Creator的静态成员变量不能混淆,否则会出现android.os.Bad-ParcelableExeception;
-keep class * implement android.os.Parcelable {
public static final android.os.Parcelable$Creator *;
}
-keepclassmembers class * implements java.io.Seriablizable {
static final long seriablVersonUID;
private static final java.io.ObjectStreamField[] seriablPersistentFields;
private void writeObject(java.io.ObjectOutputStream);
private void readOject(java.io.ObjectInputStream);
java.lang.Object writeReplace();
java.lang.Object readResolve();
}
- Gson的序列号和反序列化,其实质上是使用反射获取类解析的;
-keep class com.google.gson.** {*;}
-keep class sun.misc.Unsafe {*;}
-keep class com.google.gson.stream.** {*;}
-keep class com.google.gson.examples.android.modle.**{*;}
-keep class com.google.** {
<fields>;
<methods>;
}
-dontwarn com.google.gson.**
- 使用keep注解的方式,哪里不想混淆就“keep”哪里,先建立注解类;
package com.demo.annotation;
//@Target(ElementType.METHOD)
public @interface Keep {
}
@Target可以控制其可用范围为类/方法变量。人后在proguard-rules.pro声明;
-dontskipnonpubliclibrayclassmember
-printconfiguration
-keep,allowobfusation @interfaces android.support.annotation.Keep
-keep @andriod.support.annotation.Keep class *
-keepclassmen=mbers class * {
@android.support.annotation.Keep *;
}
只要记住一个混淆原则:混淆改变Java路径名,那么保持所在路径不被混淆就是至关重要的。
资源混淆:
ProGuard是Java混淆工具,而它只能混淆Java文件,事实上还可以继续深入混淆,可以混淆资源文件路径。
资源混淆,其实也是资源名的混淆。可以采取的方式有三种:
- 源码级别上的修改,将代码和XML中的R.string.xxx替换为R.string.a,并将一些图片资源xxx.png重命名为a.png,然后再交给Android进行编译;
- 所有的资源ID都编译为32位int值,可以看到R.java文件保存了资源数值,直接修改为resources.arsc的二进制数据,不改变打包流程,在生成resources.arsc之后修改它,同时重命名资源文件;
- 直接处理安装包,解压后直接修改resources.arsc文件,修改后重新打包。
微信的AndResGuard的资源混淆机制。
组件化混淆:
每个module在创建之后,都会自带一个proguard-rule.pro的自定义混淆文件。每个module也可以有自己混淆的规则。
但在组件化中,如果每个module都是用自身的混淆,则会出现重复混淆的现象,造成查询不到资源文件的问题。
解决这个问题是,需要保证apk生成的时候有且只有一次混淆。
- 第一种方案是:最简单也是最直观的,只在Application module中设置混淆,其他module都关闭混淆。那么混淆的规则就都会放到Application module的proguard-rule.pro文件中。这种混淆方式的缺点是,当某些模块移除后,混淆规则需要手动移除。虽然理论上混淆添加多了不会造成奔溃或者编译不通过,但是不需要的混淆过滤还是会对编译效率造成影响;
- 第二种方案是:当Application module混淆时,启动一个命令将引用的多个module的proguard-rule.pro文件合成,然后再覆盖Application module中的混淆文件。这种方式可以把混淆条件解耦到每个module中,但是需要编写Gradle命令来配置操作,每次生成都会添加合成操作,也会对编译效率造成影响;
- 第三种方案是:Library module自身拥有将proguard-rule.pro文件打包到aar中的设置。 开源库中可以依赖consumerProguardFiles标志来指定库的混淆方式,consumerProguardFiles属性会将*.pro文件打包进aar中,库混淆时会自动使用此混淆配置文件。
当Application module将全部打代码汇总混淆的时候,Library module会打包为release.aar,然后被引用汇总,通过proguard.txt规则各自混淆,保证只混淆一次。
这里将固定的第三方混淆放到Base module proguard-rule.pro中,每个module独有的引用库混淆放到各自的proguard-rule.pro中。最后再App module的proguard-rule.pro文件中放入Android基础属性混淆声明。
四.多渠道打包:
将开发工具看作生产工厂,让代码和资源作为原料,利用最少的代码消耗去构建不同渠道,不同版本的产品。
多渠道基础:
当需要统计哪个渠道用户多变,哪个渠道用户粘性强,哪个渠道又需要更加个性化的设计时,通过Android系统的方法可以获取到应用版本号/版本名称/系统版本/机型等各种信息,唯独应用商店(渠道)的信息时没办法从系统获取到的,我们只能认为在apk中添加渠道信息。
多渠道打包中我们需要关注有两件事情:
- 将渠道信息写入apk文件;
- 将apk中的渠道信息传输到后台。
打包必须经过签名这个步骤,而Android的签名有两种不同的方法:
- Android7.0以前,使用v1签名方式,是jar signature,源于JDK;
- Android7.0以后,引入v2签名方式,是Android独有的apk signature,只对Android7.0以上有效,Android7.0以下无效。
signingConfigs{
release{
v2SigningEnabled false
}
}
apk本省是zip格式文件,v2签名与普通zip格式打包的不同在于普通的zip文件有三个区块,而v2签名的apk拥有四个区块,多出来的区块用于v2签名验证。如其他三个区块被修改了,都逃不过v2验证,直接导致验证失败,所以这是v2签名比v1更加安全的原因。
批量打包:
使用原生的Gradle进行打包,工程大,打多渠道包将非常耗时,如打包过程中发现错误需要继续修复问题,那么速度将增倍。因此,批量打包技术就开始流行。
1.使用Python打包:
- 下载安装Python环境,推荐使用AndroidMultiChanneBuildTool。这个工具只支持v1签名,将ChannelUtil.Java代码即成到工程中,在app启动时获取渠道号并传送给后台(AnalyticsConfig.setChannel(ChannelUtil.getChannel(this)));
- 把生成好的apk包(项目/build/outputs/release.apk)放到PythonTool文件夹中;
- 在PythonTool/info/channel.txt中编辑渠道列表,以换行隔开;
- PythonTool目录下有一个AndroidMultiChannelBuildTool.py文件,双击运行该文件,就会开始打包。完成后在PythonTool目录下会心出现一个output_app-release文件夹,里面就是打包的渠道包了。
2.使用官方提供的方式实现多渠道打包:
- 在AndroidManifest.xml中加入渠道区分标识,写入一个Meta标签;
<Meta-data android:name="channel" android:value="${channel}"/>
- 在app目录的build.gradle中配置productFlavors:
productFlavors {
qihu360{}
yingyongbao{}
productFlavors.all {
flavor -> flavor.manifestPlaceholders = [channel : name]
}
}
- 在Android Studio Build ->Generate signed apk中选择设置渠道。
这样就可以打包不同渠道的包了,在Android Studio左下角Build Variants之后,还可以选择编译debug版本和release版本,一次打出全部的包,只需使用Gradle命令: ./gradlew build
3.在apk文件后添加zip Comment
apk文件本质上是一个带签名信息zip文件,符合zip文件的格式规范。签过名的apk文件拥有四个区块,签名区块的末尾就是zip文件注释,包含Comment Length和File Comment两个字段,前者表示注释长度,后者表示注释内容,正确修改这两个内容不会对zip文件造成破坏。利用这个字段可以添加渠道信息的数据,推荐使用packer-ng-pugin进行打包。
4.兼容v2签名的美团批量打包工具walle
以上四种打包在速度和兼容性上,zip comment和美团的walle的打包方式,无须重新编译,只做解压/添加渠道信息在打包的操作并且能兼容v1和v2签名打包。兼容最好的是原生的Gradle打包。
多渠道模块配置:
当需要多渠道或者多场景定制一些需求时,就必须使用原生Gradle来构建app了。
以下是演示例子:
productFlavors {
//用户版本
client {
manifestPlacehoders = [
channel:"10086", //渠道号
verNum:"1", //版本号
app_name:"Gank" //app名
]
}
//服务版本
server {
manifestPlacehoders = [
channel:"10087", //渠道号
verNum:"1", //版本号
app_name:"Gank服务版" //app名
]
}
}
dependencies {
clientCompile project(':settings') //引入客户版特定module
clientCompile project(':submit')
clientCompile project(':server_settings') //引入服务版特定module
}
这里通过productFlavors属性来设置多渠道,而manifestPlaceholders设置不同渠道中的不同属性,这些属性需要在AndroidMainfest中声明才能使用。设置xxxCompile来配置不同渠道需要引用的module文件。
接下来在app module的AndroidMainfest.xml中声明:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.example.demo1">
<application
android:name=".basemodule.BaseApplication"
android:allowBackup="true"
android:extractNativeLibs="true"
<!--app名引用-->
android:label="${app_name}"
tools:replace="label"
android:supportsRtl="true"/>
<!--版本号声明-->
<Meta-data android:name="verNum" android:value="${verNum}"/>
<!--渠道名声明-->
<Meta-data android:name="channel" android:value="${channel}"/>
</manifest>
android:label属性用于更改签名,${xxx}会自动引用manifestPlaceholders对应的key值。最后替换属性名需要添加tool:replace属性,提示编译器需要替换的属性。
声明Meta-data用于某些额外自定义的属性,这些属性都可以通过代码读取包信息来获取:
public class AppMetaUtils {
public static int channelNum = 0;
/**
* 获取Meta-data值
* @param context
* @param MetaName
* @return
*/
public static Object getMetaData(Context context,String MetaName) {
Object obj = null;
try {
if (context != null) {
String pkgName = context.getPackageName();
ApplicationInfo applicationInfo = context.getPackageManager().getApplicationInfo(pkgName
, PackageManager.GET_Meta_DATA);
}
}catch (Exception e){
Log.e("AppMetaUtils",e.toString());
}finally {
return obj;
}
}
/**
* 获取渠道号
* @param context
* @return
*/
public static int getChannelNum(Context context) {
if (channelNum <= 0) {
Object object = AppMetaUtils.getMetaData(context,"channel");
if (object != null && object instanceof Integer){
return (int)object;
}
}
return channelNum;
}
}
使用getApplicationInfo方法来获取应用信息,然后读取Meta-data中不同的key值来进一步获取渠道号。
/**
* 跳转到设置页面
*/
public void navigationSettings() {
String path = "/gank_setting";
if (channel == 10086) {
path +="/1";
}else if (channel == 10087){
path += "_server/1";
}
ARouter.getInstance().build(path).navigation();
}
以上是值调用的实例。如需要使用某个类调用,则可以直接将路径以值的形式来传递,然后使用反射的方式就能完成对象的创建:
productFlavors {
//用户版本
client {
manifestPlacehoders = [
channel:"10086", //渠道号
verNum:"1", //版本号
app_name:"Gank" //app名
setting_info:"material.com.setting.SettingInfo"//设置数据文件
]
}
//服务版本
server {
if(!project.ext.isLib) {
application project.ext.applicationId + '.server' //appId
}
manifestPlacehoders = [
channel:"10087", //渠道号
verNum:"1", //版本号
app_name:"Gank服务版" //app名
setting_info:"material.com.server_setting.ServerSettingInfo"//设置数据文件
]
}
}
声明一个用于传递类名的Meta-data:
<Meta-data android:name="setting_info" android:value="${setting_info}"/>
通过之前封装好的getMetaData获取需要调用的类:
/**
* 获取设置信息路径
* @param context
* @return
*/
public static String getSettingInfo(Context context) {
if (settingInfo == null){
Object object = AppMetaUtils.getMetaData(context,"setting_info");
if (object != null && object instanceof Integer) {
return (String)object;
}
}
return settingInfo;
}
然后还需要一个公共的方法调用,可以使用接口的形式,在Base module中声明一个接口,在功能module中扩展使用。
public interface SettingImp {
void setData(String data);
}
在client和server中各自继承这个接口实现方法:
public class SettingInfo implements SettingImp{
@Override
public void setData(String data) {
//进行数据处理
}
}
public class ServerSettingInfo implements SettingImp {
@Override
public void setData(String data) {
//进行数据处理
}
}
接下来就可以在Base module中再次封装并获取调用方法:
public static void SettingData(Context context,String data) {
if (getSettingInfo(context) != null){
Log.e("AppMetaUtils","setting_info is no found");
}
try{
Class<?> clazz = Class.forName(getSettingInfo(context));
SettingImp imp = (SettingImp)clazz.newInstance();
imp.setData(data);
}catch (ClassNotFoundException e) {
Log.e("AppMetaUtils","getSettingInfo error:"+e.toString());
}catch (InstantiationException e) {
Log.e("AppMetaUtils","getSettingInfo error:"+e.toString());
} catch (illegalaccessexception e) {
Log.e("AppMetaUtils","getSettingInfo error:"+e.toString());
}
}
利用反射的方式来初始化接口,把接口做成共性调用的方式。更深层次的运用需要在实际的需求中调整。
今天关于大型 Web 应用插件化架构探索和web插件化开发的讲解已经结束,谢谢您的阅读,如果想了解更多关于Android 组件化架构概要、Android 组件化架构资料、Android组件化架构、Android组件化架构学习笔记——组件化编程之静态变量/资源/混淆/多渠道打包的相关知识,请在本站搜索。
本文标签: