在 Android 窗口管理中,所有的窗口都是以树形数据结构进行组织管理的,认知这棵 WMS 的树有助于我们理解窗口的管理和显示,同时,WMS 的层级也决定了其在 SurfaceFlinger
的层级结构,这恰恰决定了它的显示规则。
在 Android 12 中,所有窗口树形管理都继基于 WindowContainer,
每个 WindowContainer
都有一个父节点和若干个子节点,我们先看看框架中 WindowContainer
都有哪些类型:
在开始之前大概整理了一下系统中各个节点之间的关系:
从上图可以看到,节点之间的嵌套关系还是比较复杂的( 而且这还是不包括下面章节中提到的引入 Feature 之后的层级关系),层级的最顶端就是 RootWindowContainer,
而它的子节点只能是: DisplayContent
1 | // RootWindowContainer.java |
再来看看 DisplayContent
的构造方法,核心逻辑就只有一句,依靠 DisplayAreaPolicy
进行层级初始化
1 | // DisplayContent.java |
1 | // DisplayAreaPolicy.java |
在 Android 12 上,Feature
正式派上用场了,原生添加了以下 Feature:
WindowMagnificationGestureHandler#toggleMagnification
FullScreenMagnificationController.SpecAnimationBridge#setMagnificationSpecLocked
最后调用 DisplayContent#applyMagnificationSpec
方法实现节点放大。不过源码中并不是通过这个 Feature 来实现相关层级放大的,改造得还不彻底我们知道,Android 系统是有 Z 轴概念的,不同的窗口有不同的高度,所有的窗口类型对应到 WMS 都会有一个 layer 值,layer 越大,显示在越上面,WMS 规定 1~36 层级,每一个 Feature
都指定了它所能影响到的 layer 层。这里用颜色对不同 Feature
能影响 layer 图层进行颜色标记:
标记完之后,就需要根据图表生成窗口层级了,首先对标记好的图表进行上移,上移规则: 如果色块上方是空白的,则可以上移,直至上方是颜色块(不知道大家有没有玩过 2048 这款游戏,上移逻辑是一样的~)
上移之后,我们得到了最终的图表,接下来用以下规则进行层级构建:
Feature
节点,从左到右根据颜色不断生成节点,同一行所有节点挂在同一个父节点下Feature
节点再添加一个节点,根据子节点代表的 layer 不一样,最后添加的节点也不一样DisplayArea.Tokens
(这类节点后续只能添加 WindowToken
节点)APPLICATION_LAYER
)的节点,挂载 TaskDisplayArea
ImeContainer
通过上述构建规则后,我们可以获得一个树形的层级,并且这棵树有以下特点:
Feature
都生成了对应的父节点,用以控制其所能影响的 layer生成了这棵树后,我们会保存两样东西:
Map<Feature, List<DisplayArea>>
形式保存的所有 Feature
节点,方便我们后取出某 Feature
对应的所有节点现在,虽说我们的 WMS 层级是构建好了,但对于这些 Feature
有何作用还完全没有涉及,这块打算放在 WM Shell
专题里进行说明~~
通过上面 Feature
的说明可以知道,不同的 Feature
是父子节点的关系,那如果我想划分一个逻辑显示区域,对这块区域配置不同的 Feature
该如何呢? 这时候就可以使用 DisplayAreaGroup
了,框架允许我们添加多个 DisplayAreaGroup,
并为其配置不同的 Feature
。
就像原生提供的 demo 一样,我们可以创建两个 DisplayAreaGroup
并将屏幕一分为二分别放置这两个,这两个区域都是可以作为应用容器的,和分屏不一样的是,这两块区域可以有不同的 Feature 规则以及其他特性,比如设置不同的 DisplayArea#setIgnoreOrientationRequest
值
DisplayAreaGroup
和 DisplayContent
都是 RootDisplayArea
的直接子类,DisplayAreaGroup
可以认为是一个 Display 划分出的多个逻辑 Display
吧。当然,AOSP 虽然引入了这个概念和代码,但其实并未使用,我们只能从测试代码 DualDisplayAreaGroupPolicyTest
中略窥一二了~
WMS 相关的内容体系实在太多,本文也仅仅是分析 WMS 窗口层级最顶层的结构,对于具体的窗口添加移除管理这些尚未涉及,同样,原生新增的 Feature 节点使用也没有涉及(这大部分都被打包进 WM Shell 中去了)
]]>在开发过程中,经常会遇到各种各样的窗口问题,比如动画异常、窗口异常、闪屏、黑屏、错位显示..
以前对于这些问题,我们可以通过添加日志,调试分析代码等手段去解决,但这些 UI 问题往往出现在一瞬间,很难把握出现的时机,录制下来的日志往往也是巨大的,从海量的日志中提取有效的信息是一个枯燥且繁琐的事情。
Android 也意识到了这个问题,WinScope
的出现有效的帮助我们跟踪窗口和显示问题。它向开发者提供一个可视化的工具,让开发者能使用工具跟踪整个界面的变化过程,让我们可以观察到细微的变化。迭代了几个版本后,Android 12 上 WinScope 变得更好用了,下面来看看大概的效果:
Android 12 平台的 WinScope
工具可以通过源码编译获得,具体也可以查阅 development/tools/winscope
目录下的 README.md 文档,这里提供一个 Ubuntu 平台的编译步骤:
1 | 1. cd development/tools/winscope |
编译过程中遇到一个问题,看上去是在执行 kotlin 优化的时候,报了个内存不足的问题:
1 | Caused by: java.lang.OutOfMemoryError: GC overhead limit exceeded |
可以在执行 yarn build 前通过 export JAVA_OPTS="-XX:-UseGCOverheadLimit"
禁用掉 GC overhead limit exceeded
检测
编译完之后,在当前目录下会一个 dist
目录,再把 adb_proxy/winscope_proxy.py
(一个帮我们开启 trace 抓取命令的脚本,这样我们就可以告别繁琐的命令啦),文件也拷贝进 dist
目录方便我们后面使用
使用比较简单了,连接手机后:
dist
目录下的 index.html
文件python3 winscope_proxy.py
PS: 这里建议大家可以设置个 alias 一键使用,比如:
1 | alias winscope_s="xdg-open ~/tools/winscope_s/index.html && python3 ~/tools/winscope_s/winscope_proxy.py" |
输入 python 命令后,终端可能会生成一个 token
,把它复制到浏览器即可
接下来就会出现下面这个界面:
在选择 START TRACE
之后,就可以在手机端进行录制操作,操作完后结束录制即可
相比与 Android 11,新的 WinScope
工具在界面上更友好了,重要的改进如下:
ProtoLog
后,通过箭头控制时间推移简直是太难用了Transaction
和 Log
浏览界面InputMethodService
、InputMethodManagerService
和 Client
的事件不得不说确实比以前好用一些,赶紧尝尝鲜吧~
]]>随着越来越多大屏和折叠屏设备出现,很多应用并未对不同尺寸的设备进行 UI 适配,这时候应用选择以特定的宽高比显示(虽然 Google 不建议这这样做,官方还是希望开发者可以对不同的屏幕尺寸进行自适应布局~),当应用的宽高比和它的容器比例不兼容的时候,就会以 Letterbox 模式打开。
Letterbox 模式下界面会以指定的比例显示,周围空白区域可以填充壁纸或者颜色。至于 Letterbox 的外观可受以下因素影响:
config_letterboxActivityCornersRadius
: 界面圆角大小config_letterboxBackgroundType
: 背景填充类型,分别有: LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND
: 颜色受 android:colorBackground
影响LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING
: 颜色受 android:colorBackgroundFloating
影响LETTERBOX_BACKGROUND_SOLID_COLOR
: 颜色受 config_letterboxBackgroundColor
影响LETTERBOX_BACKGROUND_WALLPAPER
: 显示壁纸,此选项和 FLAG_SHOW_WALLPAPER
类似,会导致壁纸窗口显示config_letterboxBackgroundWallpaperBlurRadius
: 壁纸模糊程度config_letterboxBackgroundWallaperDarkScrimAlpha
: 壁纸变暗程度Letterbox 的触发条件一般有:
android:resizeableActivity=false
且应用声明的宽高比与容器不兼容的时候(如屏幕宽高超出 android:maxAspectRatio
)setIgnoreOrientationRequest(true)
系统设置忽略屏幕方向后,以横屏模式下打开一个强制竖屏的界面Letterbox 显示的实现并不复杂,Android 12 在 ActivityRecord
中增加了 LetterboxUiController
用以控制 Letterbox
的布局和显示,先来看看处于 Letterbox 模式时 SurfaceFlinger 状态:
可以看到,跟正常情况相比,除了界面本身的大小和位置被缩放到指定比例外,四周还多了两个 Layer,挂在 ActiviRecord 节点下面,这两个 Layer 可根据配置进行指定的颜色填充,如果背景是壁纸的话,还可以设置壁纸的 dim 值和模糊程度,这些都可以通过 SurfaceControl 接口轻松实现。
下面简单分析一下代码:
1 | // LetterboxUiController.java |
1 | // Letterbox.LetterboxSurface.java |
本文只是简单分析了下 Letterbox 模式的触发条件和显示的大概逻辑,还有很多细节没有涉及,比如详细的触发逻辑判断可以查看 LetterboxUiController#shouldShowLetterboxUi
方法
OverScroller
在 Android 系统中承担着为 ListView、RecyclerView、ScrollView 这些滚动控件计算实时滑动位置的任务,这些位置算法直接影响着每一次滚动的体验
众所周知,Android 的动画体验远不如 iOS,即便如今 Android 已普遍支持 120Hz 高刷,体验起来也不是非常舒服。究其原因已经不是硬件性能限制,而是其中很多动画设计本身就有问题。苹果早在很早之前就发布了 Designing Fluid Interfaces 致力于打造一个丝滑流畅的用户体验,反观 Android,对于一个日常使用中使用最多的滑动工具类 OverScroller
近几年改进竟然寥寥无几,几乎没有,实在是有点想吐槽
这个系列分为两篇,第一篇主要讲述 Android 实现滚动的核心工具类 OverScroller
的使用方法和原理,第二篇我们将探索如何进行改进,希望我们每一次探索都能给用户体验带来提升
在使用之前,先来看看 OverScroller 能做什么:
startScroll | fling | springBack |
---|---|---|
在代码中使用也很简单:
1 | // 1. 启动一个滚动 |
上面就是启动一个不断滚动并刷新 View 的最小逻辑(当然更工程化的实践也可以把 Runnable 的逻辑放在 View#computeScroll 里再通过 invalidate 触发)。fling
和 springBack
的启动方式也是一样的,这里就不再进行赘述了。
在上面代码中可知,启动一个滚动任务后,是通过不断地调用 computeScrollOffset 来计算位置的,接下来看下代码实现
1 |
|
startScroll 的逻辑非常的简单,只是根据参数标记了一下开始位置、结束位置、开始时间、动画时长,还没涉及位置计算(因为位置计算是放在 computeScrollOffset 的呀)
再看看定时调用的 computeScrollOffset 逻辑:
1 | public class OverScroller { |
逻辑也是很简单,实在没太多可说的……就是把插值器曲线映射到位移曲线,时长如果不指定的话,默认是 250ms,插值器需要通过构造方法传入,如果不指定的话,系统默认会指定一个 ViscousFluidInterpolator
,下面是这个插值器的曲线,可以看到是一个先缓后快再缓的动画
fling
和 springBack
为什么要一起说呢?因为 fling
的动画比较复杂,springBack
算是属于 fling
的其中一个子状态,考虑以下这个情况:
我们看到当以比较大的速度执行 fling 的时候,是很容易碰到边界的,fling 会根据预设的边界值执行越界并回弹,可以把整个动画过程分解成三个阶段:
springBack
后执行的动画,其实就是 fling
的 CUBIC
阶段,所以干脆就放在一起说了
这个命名其实也挺有意思,这三个分别是样条曲线、弹道曲线、三次曲线,从命名上大致也可以推断出,三个阶段采用的「时间-位置」曲线是不一样的。
当然了,Android 很多控件在 fling 的时候,都把越界回弹效果取消掉,取而代之的是显示一个 EdgeEffect
。也就是说执行完 SPLINE 阶段动画后,是看不到 BALLISTIC
和 CUBIC
的,只能看到一个边缘辉光效果,列表到达顶/底部的时候,往往一下子停在那里了~
先来看看启动 fling 的入口函数:
1 | public class OverScroller { |
看代码前先来看看上面的图,图中说明了 start、min、max、over 等位置的意义,这里简要说明一下
fling 函数主要功能:
滑动距离和时长的计算公式中,可以把 mPhysicalCoeff 也看做常数,把时长-速度公式和图像列出来:
$$
y=1000\cdot \exp \left( \frac{\ln \left( \frac{0.35x}{2140.47} \right)}{1.358} \right)
$$
再看看距离-速度公式:
$$
y=2140.47\cdot \exp \left( 1.74\cdot \ln \left( \frac{0.35\cdot x}{2140.47} \right) \right)
$$
可以看到距离和时长都是随着速度增大而增大的,只不过时长的增长速度在后期会有一定的收敛,保证动画时长不至于太长
还有一点要注意的是,$ \frac{Distance} {Duration} $ 的比值是一个线性函数,也就是初速度越大,平均速度越大,两者是线性增长的:
$$
y=\frac{2140.47\cdot \exp \left( 1.74\cdot \ln \left( \frac{0.35\cdot x}{2140.47} \right) \right)}{1000\cdot \exp \left( \frac{\ln \left( \frac{0.35x}{2140.47} \right)}{1.358} \right)}
$$
但是说实话,目前我暂时没想明白这两个公式的物理意义,有明白的大佬求告知~ 难道是利用了对数函数收敛的特性确定了时长公式,然后设定平均速度线性增长后,推导出距离公式?
目前只确定了滑动总距离和时长,那么中间过程是怎么更新位置的呢:
1 | public class OverScroller { |
SPLINE
阶段的位置和速度完全是由预设的 SPLINE_POSITION
样条曲线决定的,它是一个大小为 101 的数组,里面存储了一条曲线平均采样 100 次的坐标值,初始化如下:
1 | private static final int NB_SAMPLES = 100; |
看代码很难想象它长什么样,直接看看它的图像吧:
也就是说,SPLINE 和 startScroll 很像,位置曲线都是由一根预置的曲线决定的,把预置曲线映射真实的距离,只是 SPLINE 没有使用插值器曲线,而是使用了一根缓停的样条曲线
SPLINE 阶段结束后,会通过 continueWhenFinished 进入下一阶段:越界阶段(前提是此时已经到达边界)。越界阶段的原理相对简单,就是一段匀减速运动(直至速度降为 0),默认加速度 a 为 -2000.0f,到达边界进入 BALLISTIC 阶段的初始化逻辑在 onEdgeReached
中:
1 | private void onEdgeReached() { |
速度:
$$
v_t=v_0+at
$$
距离:
$$
s_t=v_0t + \frac{at^2} {2}
$$
这里有个小吐槽,加速度固定是 2000,这也太小了吧,也就是说,如果越界速度为 10000, 那么需要 5s 之后,速度才能降为 0,一个 5s 的动画童鞋们估计都知道意味着多久吧?
上一节内容知道,BALLISTIC 阶段结束时,速度已经降低为 0,我们终于来到最后一段, 从 continueWhenFinished 里会调用 startSpringback
作为 CUBIC 的初始化:
1 | private void startSpringback(int start, int end, int velocity) { |
时长计算依然使用匀加速直线运动的逻辑,想象一段初速度为 0,加速度为 a, 距离为 delta 的情况:
$$
delta=\frac{(v_0 + v_t)} {2} t
$$
则:
$ v_0=0,v_t=at $,那么 $ delta=\frac{at^2} {2} $,$ t=\sqrt{ \frac{2*delta} {a}}$
在 update
方法中,更新 CUBIC 的核心逻辑是:
1 | case CUBIC: { |
核心逻辑是这个 3.0f * t2 - 2.0f * t * t2
, 这其实是一个比较常用的三次曲线:
在 [0, 1] 区间内,是一个缓入缓出的曲线。至此,CUBIC 的运动规律也摸清楚了,在固定时间内,把时间映射到 [0, 1] 的区间,再把 y 坐标映射实际的位置
目前为止,我们终于把 startScroll
和 fling
各阶段的曲线看了一遍,至于 springBack
和其他一些情况都大同小异,就不细述了。
很多时候 OverScroller 都是只是使用的固定的曲线映射真正的曲线,比如 startScroll
、SPLINE
和 CUBIC
,那如果想改变效果的话,是不是修改一下曲线形态就可以了呢?但一条曲线是否真的能在不同速度下都有比较好的表现吗?或许我们还要有很多的实践和尝试才能做出一段让用户舒服滑动,而这些探索和尝试,将放在下一篇文章中详细讨论~
从 Android 7 开始,Android 源码编译时默认使用 Ninja,编译时,会先把 makefile 和 bp 转换成 ninja 再进行编译。这个转换过程非常慢(需要遍历处理所有关联的 makefile、bp 文件),即使只是通过 mm
或 mmm
编译某个模块,也会有很多因素触发 ninja 文件的重新生成,而这对基于源码开发的模块很不友好,编译好慢!
AOSP 在源码中已经内置了一个 ninja 执行文件,路径为:./prebuilts/build-tools/linux-x86/bin/ninja
我们先看看它的 help:
1 | ➜ ~ ./prebuilts/build-tools/linux-x86/bin/ninja -h |
简单使用的话,我们关注它的两个参数就行了
-f
:这个参数指定的就是输入文件,也就是 makefile 和 bp 转换后的 ninja 文件,一般位于 ./out
目录,后面会说targets
:目标,这个和 makefile 是类似的,就是我们最终需要的产物,例如:Launcher3QuickStep、SystemUI。那么这些 targets 名是哪里定义的呢?要知道对应模块的对应的 target 名,只需要:LOCAL_PACKAGE_NAME
或 LOCAL_MODULE
等对应的值举个栗子:
1 | ➜ android-10.0.0_r11 ./prebuilts/build-tools/linux-x86/bin/ninja -f out/combined-aosp_walleye.ninja Launcher3QuickStep |
就这样,不需要通过 mm
或者 mmm
命令,目标产物同样生成了。我们看看耗时:
1 | ➜ android-10.0.0_r11 time ./prebuilts/build-tools/linux-x86/bin/ninja -f out/combined-aosp_walleye.ninja Launcher3QuickStep |
可以看到,整个编译在 18s 完成了,相比动辄七八分钟的 mmm
,效率提升还是很可观的。
虽然 ninja 很方便,但要用它来编译单个模块,还是有一些限制和注意事项的:
mmm
都可以)./out/combined-[TARGET-PRODUCT].ninja
mmm
编译后,生成的 ninja 文件为:./out/combined-[TARGET-PRODUCT]-_[path_to_your_module_makefile].ninja
,比如:./out/combined-aosp_walleye-_packages_apps_Launcher3_Android.mk.ninja
为 Launcher 和 SystemUI 准备一份开箱即用的指令,尽情玩耍吧~
Launcher:
1 | ./prebuilts/build-tools/linux-x86/bin/ninja -f out/combined-qssi-_packages_apps_Launcher3_Android.mk.ninja Launcher3QuickStep |
SystemUI:
1 | ./prebuilts/build-tools/linux-x86/bin/ninja -f out/combined-qssi-_frameworks_base_packages_SystemUI_Android.mk.ninja SystemUI |
随着 Android Q 发布,「黑暗模式」或者说是「夜间模式」终于在此版本中得到了支持,官方介绍见:https://developer.android.com/guide/topics/ui/look-and-feel/darktheme,再看看效果图:
其实这个功能魅族在两年前就已支持,不得不说 Android 有点落后了,今天我们就来看看原生是怎么实现全局夜间模的吧
从文档上我们可以可知,打开夜间模式有三个方法:
打开后,我们会发现,除原生几个应用生效外,其他应用依然没有变成深色主题,那么应用该如何适配呢?官方提供了下面两种方法:
DayNight
主题1 | <style name="AppTheme" parent="Theme.AppCompat.DayNight"> |
或者继承自
1 | <style name="AppTheme" parent="Theme.MaterialComponents.DayNight"> |
继承后,如果当前开启了夜间模式,系统会自动从 night-qualified 中加载资源,所以应用的颜色、图标等资源应尽量避免硬编码,而是推荐使用新增 attributes 指向不同的资源,如
1 | ?android:attr/textColorPrimary |
另外,如果应用希望主动切换夜间/日间模式,可以通过 AppCompatDelegate.setDefaultNightMode()
接口主动切换
如果应用不想自己去适配各种颜色,图标等,可以通过在主题中添加 android:forceDarkAllowed="true"
标记,这样系统在夜间模式时,会强制改变应用颜色,自动进行适配(这个功能也是本文主要探讨的)。不过如果你的应用本身使用的就是 DayNight
或 Dark Theme
,forceDarkAllowed 是不会生效的。
另外,如果你不希望某个 view 被强制夜间模式处理,则可以给 view 添加 android:forceDarkAllowed="false"
或者 view.setForceDarkAllowed(false),设置之后,即使打开了夜间模式且主题添加了 forceDarkAllowed,该 view 也不会变深色。比较重要的一点是,这个接口只能关闭夜间模式,不能开启夜间模式,也就是说,如果主题中没有显示声明 forceDarkAllowed,view.setForceDarkAllowed(true)
是没办法让 view 单独变深色的。如果 view 关闭了夜间模式,那么它的子 view 也会强制关闭夜间模式
总结如下:
DayNight
或 Dark Theme
主题,则所有 forceDarkAllowed 都不生效通过继承主题适配夜间模式的原理本质是根据 ui mode 加载 night-qualified 下是资源,这个并非 Android Q 新增的东西,我们这里不再描述。现在主要来看看 forceDarkAllowed 是如何让系统变深色的。
既然一切的源头都是 android:forceDarkAllowed
这个属性,那我们就从它入手吧,首先我们要知道,上面我们说的 android:forceDarkAllowed
其实是分为两个用处,它们分别的定义如下:
frameworks/base/core/res/res/values/attrs.xml
1 | <declare-styleable name="View"> |
一个是 View 级别的,一个是 Theme 级别的。
从上面的总结来看,Theme 级别的开关优先级是最高的,控制粒度也最大,我们看看源码里面使用它的地方
1 | // frameworks/base/core/java/android/view/ViewRootImpl.java |
这段代码还是比较简单,判断系统:
三者同时为 true 时才会设置夜间模式,而 updateForceDarkMode 调用的时机分别是在 ViewRootImpl#setView
和 ViewRootImpl#updateConfiguration
,也就是初始化和夜间模式切换的时候都会调用,确保夜间模式能及时启用和关闭。继续跟踪 HardwareRenderer#setForceDark
发现,这是一个 native 方法,所以接下来让我们进入 native 世界,nSetForceDark 对应的实现位于
1 | // frameworks/base/core/jni/android_view_ThreadedRenderer.cpp |
最终就是设置了一个 CanvasContext 的变量值而已,什么都还没有做,那么这个变量值的作用是什么,什么时候生效呢?我们进一步查看使用的地方:
1 | // frameworks/base/libs/hwui/TreeInfo.cpp |
进一步看看 disableForceDark 使用的地方
1 | // frameworks/base/libs/hwui/RenderNode.cpp |
1 | // frameworks/base/libs/hwui/RecordingCanvas.cpp |
color_transform_fn 宏定义展开
1 | template <class T> |
让我们再一次看看 map 方法
1 | template <typename Fn, typename... Args> |
贴了一大段代码,虽然代码中已经包含了注释,但还是可能比较晕,我们先来整理下:
接下来让我们来看 paint 和 colorfilter 的变色实现
1 | bool transformPaint(ColorTransform transform, SkPaint* paint) { |
逻辑很简单,就是对颜色进行变换,进一步看看变色逻辑:
1 | // 提亮颜色 |
到此,对 paint 的变换结束,看来无非就是反转明度。
再来看看对图片的变换:
1 | bool transformPaint(ColorTransform transform, SkPaint* paint, BitmapPalette palette) { |
终于,bitmap 的变换也分析完了,呼~
但是,还没完呢~还记得我们最开始说的,除了 Theme 级别,还有一个 View 级别的 forceDarkAllowed,通过 View 级别 forceDarkAllowed 可以关掉它及它的子 view 的夜间模式开关。依然从 java 层看下去哈
1 | // rameworks/base/core/java/android/view/View.java |
1 | // frameworks/base/core/jni/android_view_RenderNode.cpp |
和 Theme 级别的一样,仅仅只是设置到变量中而已,关键是要看哪里使用这个变量,经过查找,我们发现,它的使用同样在 RenderNode 的 prepareTreeImpl 中:
1 | void RenderNode::prepareTreeImpl(TreeObserver& observer, TreeInfo& info, bool functorsNeedLayer) { |
本文到目前为止,总算把 Android Q 夜间模式实现原理梳理了一遍,总的来说实现不算复杂,说白了就是把 paint 中的颜色转换一下或者叠加一个 colorfilter,虽然中间还有关联知识没有细说,如 RenderThread、DisplayList、RenderNode 等图形相关的概念,限于文章大小,请读者自行了解
另外,由于水平有限,难免文中有错漏之处,若哪里写的不对,请大家及时指出,蟹蟹啦~
]]>使用 Android Studio 查看 Android Framework 代码体验非常好,无论是索引还是界面都让人很满意,但是当你跟踪代码,发现进入 native 逻辑时,就会发现 Android Studio 对 native 代码的支持非常不好,不能索引不支持符号搜索不能跳转等,这些让人非常抓狂。那么如何能在 IDE 愉快地查看 native 代码呢?在 Windows 上,Source Insight 的表现也很好,但苦于只有 Windows 平台支持且界面不好,经过一番折腾,还真是找到了方法,下面我们将一步一步打造丝滑的 native 代码阅读环境。
先看一下效果:
能让 IDE 正确地建立索引,我们需要让 IDE 能正确地知道源文件、头文件、宏定义等各种数据,庆幸的是,我们发现 AOSP 在编译过程中,可以帮我们生成这些数据,详见:http://androidxref.com/9.0.0_r3/xref/build/soong/docs/clion.md
通过文档我们可知,只需要按照以下步骤完成一次编译,即可自动生成各模块对应的 CMake 文件。至于 Cmake 文件是什么,这里就不做赘述了,大家可以自行了解。
1 | export SOONG_GEN_CMAKEFILES=1 |
1 | make -j16 |
或者只编译你需要的模块
1 | make frameworks/native/service/libs/ui |
生成的文件存放在 out 目录,比如刚刚编译的 libui 模块对应的路径为:
1 | out/development/ide/clion/frameworks/native/libs/ui/libui-arm64-android/CMakeLists.txt |
生成了 CMake 后,我们发现,CMake 文件是按模块生成的。这样的话,会导致 IDE 只能单独导入一个模块,而我们平时不可能只看一个模块的代码,如果把多个模块都 include 进来呢?
我们可以在 out/development/ide/clion
路径新建一个 CMakeLists.txt
文件,并添加一下内容:
1 | # 指定 CMake 最低版本 |
这样,我们就把多个模块合并在一起了,用 IDE 去打开这个总的 CMake 文件即可
只要生成 CMake 文件后,剩下的事情就好办了,现在能识别 CMake 工程的 IDE 非常多,大家可以根据个人喜好选择,如:
这里以 CLion 为例讲一下如何导入
CMakeLists.txt
的目录,如我们在上一个步骤中说的 out/development/ide/clion
(这个目录的 CMakeLists.txt 包含了多个模块,还记得吗?)当然,CLion 也有一个缺点,收费!!如何能免费使用就看大家各显神通了
set(ANDROID_ROOT /Volumes/AndroidSource/M1882_QOF7_base)
,这里大家要注意,如果把 CMakeLists.txt 拷贝到别的工程使用,记得修正一下路径set(CMAKE_CXX_COMPILER "${ANDROID_ROOT}/prebuilts/clang/host/linux-x86/clang-3977809/bin/clang++")
这里指定的是 linux-x86 的编译器,记得替换成 darwin-x86
,如果对应目录下没有 clang++,那就从 AOSP 源码拷一个吧如果使用遇到其他问题,欢迎联系告知,谢谢
所谓工欲善其事,必先利其器。通过这种方法建立的索引包含了 AOSP 所有模块,最重要是它还会根据编译环境,把相关 FLAGS 和宏都设置好。
]]>很多 Android 开发者都会希望编译 Android 源码并刷进自己的手机里面,但网上教程很多都仅仅是告诉你 lunch、make 等等,但你手里有一台设备时却发现,你编译出的镜像由于驱动关系是不能直接烧进手机的。这里整理了一下步骤,帮助大家可以按照流程编译并烧写镜像。
本篇文章以 Pixel 2 && Android 10 为例
这块没啥说,官方教程就够了,参考:https://source.android.com/setup/build/initializing 就行了
在 https://source.android.com/setup/start/build-numbers 查找 QP1A.190711.020 对应的分支:android-10.0.0_r2,记住分支名
下载 AOSP 源码
注意在下载 aosp 前要安装 repo 工具,参考:https://source.android.com/setup/build/downloading
1 | mkdir Pixel2 |
把步骤1中选中的两个驱动下载到 aosp 源码根目录并解压
分别执行解压后的文件,注意,执行后要同意 License,确保正确解压到 aosp 根目录的 vendor 目录
1 | ./extract-qcom-walleye.sh |
export ANDROID_PRODUCT_OUT=/home/chenhang/source/Pixel2/out/target/product/walleye
fastboot flashall -w
fastboot reboot
编译出来的 aosp 默认没有 google 全家桶,可以通过以下方式进行安装
Protocol Buffers 是 google 的一种数据交换的格式,它独立于语言,独立于平台。google 提供了多种语言的实现:java、c#、c++、go 和 python,每一种实现都包含了相应语言的编译器以及库文件。由于它是一种二进制的格式,比使用 xml 进行数据交换快许多。可以把它用于分布式应用之间的数据通信或者异构环境下的数据交换。作为一种效率和兼容性都很优秀的二进制数据传输格式,可以用于诸如网络传输、配置文件、数据存储等诸多领域。
至于protobuf是什么、使用场景、有什么好处,本文不做说明,这里将会为大家介绍怎么用 protobuf
来定义我们的交互协议,包括 .proto
的语法以及如何根据proto文件生成相应的代码。本文基于proto3,读者也可以点击了解proto2
首先我们来定义一个 Search 请求,在这个请求里面,我们需要给服务端发送三个信息:
于是我们可以这样定义:
1 | // 指定使用proto3,如果不指定的话,编译器会使用proto2去编译 |
一个 proto 文件可以定义多个 message ,比如我们可以在刚才那个 proto 文件中把服务端返回的消息结构也一起定义:
1 | message SearchRequest { |
message 可以嵌套定义,比如 message 可以定义在另一个 message 内部
1 | message SearchResponse { |
定义在 message 内部的 message 可以这样使用:
1 | message SomeOtherMessage { |
在刚才的例子之中,我们使用了2个标准值类型
: string 和 int32,除了这些标准类型之外,变量的类型还可以是复杂类型,比如自定义的枚举
和自定义的 message
这里我们把标准类型列举一下protobuf内置的标准类型以及跟各平台对应的关系:
.proto | 说明 | C++ | Java | Python | Go | Ruby | C# | PHP |
---|---|---|---|---|---|---|---|---|
double | double | double | float | float64 | Float | double | float | |
float | float | float | float | float32 | Float | float | float | |
int32 | 使用变长编码,对负数编码效率低,如果你的变量可能是负数,可以使用sint32 | int32 | int | int | int32 | Fixnum or Bignum (as required) | int | integer |
int64 | 使用变长编码,对负数编码效率低,如果你的变量可能是负数,可以使用sint64 | int64 | long | int/long | int64 | Bignum | long | integer/string |
uint32 | 使用变长编码 | uint32 | int | int/long | uint32 | Fixnum or Bignum (as required) | uint | integer |
uint64 | 使用变长编码 | uint64 | long | int/long | uint64 | Bignum | ulong | integer/string |
sint32 | 使用变长编码,带符号的int类型,对负数编码比int32高效 | int32 | int | int | int32 | Fixnum or Bignum (as required) | int | integer |
sint64 | 使用变长编码,带符号的int类型,对负数编码比int64高效 | int64 | long | int/long | int64 | Bignum | long | integer/string |
fixed32 | 4字节编码, 如果变量经常大于$ 2^{28} $ 的话,会比uint32高效 | uint32 | int | int | int32 | Fixnum or Bignum (as required) | uint | integer |
fixed64 | 8字节编码, 如果变量经常大于$ 2^{56} $ 的话,会比uint64高效 | uint64 | long | int/long | uint64 | Bignum | ulong | integer/string |
sfixed32 | 4字节编码 | int32 | int | int | int32 | Fixnum or Bignum (as required) | int | integer |
sfixed64 | 8字节编码 | int64 | long | int/long | int64 | Bignum | long | integer/string |
bool | bool | boolean | bool | bool | TrueClass/FalseClass | bool | boolean | |
string | 必须包含utf-8编码或者7-bit ASCII text | string | String | str/unicode | string | String (UTF-8) | string | string |
bytes | 任意的字节序列 | string | ByteString | str | []byte | String (ASCII-8BIT) | ByteString | string |
补充说明:
关于标准值类型,还可以参考Scalar Value Types
如果你想了解这些数据是怎么序列化和反序列化的,可以点击 Protocol Buffer Encoding 了解更多关于protobuf编码内容。
每一个变量在message内都需要自定义一个唯一的数字Tag,protobuf会根据Tag从数据中查找变量对应的位置,具体原理跟protobuf的二进制数据格式有关。Tag一旦指定,以后更新协议的时候也不能修改,否则无法对旧版本兼容。
Tag的取值范围最小是1,最大是$ 2^{29} $-1,但 19000~19999 是 protobuf 预留的,用户不能使用。
虽然 Tag 的定义范围比较大,但不同 Tag 也会对 protobuf 编码带来一些影响:
使用频率高的变量最好设置为1 ~ 15,这样可以减少编码后的数据大小,但由于Tag一旦指定不能修改,所以为了以后扩展,也记得为未来保留一些 1 ~ 15 的 Tag
在 proto3 中,可以给变量指定以下两个规则:
singular
:0或者1个,但不能多于1个repeated
:任意数量(包括0)当构建 message 的时候,build 数据的时候,会检测设置的数据跟规则是否匹配
在proto2中,规则为:
用//
表示注释开头,如
1 | message SearchRequest { |
上面我们说到,一旦 Tag 指定后就不能变更,这就会带来一个问题,假如在版本1的协议中,我们有个变量:
1 | int32 number = 1; |
在版本2中,我们决定废弃对它的使用,那我们应该如何修改协议呢?注释掉它?删除掉它?如果把它删除了,后来者很可能在定义新变量的时候,使新的变量 Tag = 1 ,这样会导致协议不兼容。那有没有办法规避这个问题呢?我们可以用 reserved
关键字,当一个变量不再使用的时候,我们可以把它的变量名或 Tag 用 reserved
标注,这样,当这个 Tag 或者变量名字被重新使用的时候,编译器会报错
1 | message Foo { |
当解析 message 时,如果被编码的 message 里没有包含某些变量,那么根据类型不同,他们会有不同的默认值:
注意,收到数据后反序列化后,对于标准值类型的数据,比如bool,如果它的值是 false,那么我们无法判断这个值是对方设置的,还是对方压根就没给这个变量设置值。
在 protobuf 中,我们也可以定义枚举,并且使用该枚举类型,比如:
1 | message SearchRequest { |
枚举定义在一个消息内部或消息外部都是可以的,如果枚举是 定义在 message 内部,而其他 message 又想使用,那么可以通过 MessageType.EnumType
的方式引用。定义枚举的时候,我们要保证第一个枚举值必须是0,枚举值不能重复,除非使用 option allow_alias = true
选项来开启别名。如:
1 | enum EnumAllowingAlias { |
枚举值的范围是32-bit integer,但因为枚举值使用变长编码,所以不推荐使用负数作为枚举值,因为这会带来效率问题。
在proto语法中,有两种引用其他 proto 文件的方法: import
和 import public
,这两者有什么区别呢?下面举个例子说明:
1 | // my.proto |
1 | // first.proto |
1 | // second.proto |
升级更改 proto 需要遵循以下原则
Any可以让你在 proto 文件中使用未定义的类型,具体里面保存什么数据,是在上层业务代码使用的时候决定的,使用 Any 必须导入 import google/protobuf/any.proto
1 | import "google/protobuf/any.proto"; |
Oneof 类似union,如果你的消息中有很多可选字段,而同一个时刻最多仅有其中的一个字段被设置的话,你可以使用oneof来强化这个特性并且节约存储空间,如
1 | message LoginReply { |
这样,name 和 age 都是 LoginReply 的成员,但不能给他们同时设置值(设置一个oneof字段会自动清理其他的oneof字段)。
protobuf 支持定义 map 类型的成员,如:
1 | map<key_type, value_type> map_field = N; |
使用 map 要注意:
为了防止不同消息之间的命名冲突,你可以对特定的.proto文件提指定 package 名字。在定义消息的成员的时候,可以指定包的名字:
1 | package foo.bar; |
1 | message Foo { |
Options 分为 file-level options(只能出现在最顶层,不能在消息、枚举、服务内部使用)、 message-level options(只能在消息内部使用)、field-level options(只能在变量定义时使用)
1 | option java_package = "com.example.foo"; |
这个其实和gRPC相关,详细可参考:gRPC, 这里做一个简单的介绍
要定义一个服务,你必须在你的 .proto 文件中指定 service
1 | service RouteGuide { |
然后在我们的服务中定义 rpc
方法,指定它们的请求的和响应类型。gRPC
允许你定义4种类型的 service 方法
客户端使用 Stub 发送请求到服务器并等待响应返回,就像平常的函数调用一样,这是一个阻塞型的调用
1 | // Obtains the feature at a given position. |
客户端发送请求到服务器,拿到一个流去读取返回的消息序列。客户端读取返回的流,直到里面没有任何消息。从例子中可以看出,通过在响应类型前插入 stream
关键字,可以指定一个服务器端的流方法
1 | // Obtains the Features available within the given Rectangle. Results are |
客户端写入一个消息序列并将其发送到服务器,同样也是使用流。一旦客户端完成写入消息,它等待服务器完成读取返回它的响应。通过在请求类型前指定 stream
关键字来指定一个客户端的流方法
1 | // Accepts a stream of Points on a route being traversed, returning a |
双方使用读写流去发送一个消息序列。两个流独立操作,因此客户端和服务器可以以任意喜欢的顺序读写:比如, 服务器可以在写入响应前等待接收所有的客户端消息,或者可以交替的读取和写入消息,或者其他读写的组合。每个流中的消息顺序被预留。你可以通过在请求和响应前加 stream
关键字去制定方法的类型
1 | // Accepts a stream of RouteNotes sent while a route is being traversed, |
使用 protoc
工具可以把编写好的 proto
文件“编译”为Java, Python, C++, Go, Ruby, JavaNano, Objective-C,或C#代码, protoc
可以从点击这里进行下载。protoc
的使用方式如下:
1 | protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR --go_out=DST_DIR --ruby_out=DST_DIR --javanano_out=DST_DIR --objc_out=DST_DIR --csharp_out=DST_DIR path/to/file.proto |
参数说明:
--proto_path
装饰模式(Decorator)也叫包装器模式(Wrapper),是指动态地给一个对象添加一些额外的职责,就增加功能来说装饰模式比生成子类更为灵活。它通过创建一个包装对象,也就是装饰来包裹真实的对象
我们先来分析这样一个画图形的需求:
就先列这三个简单的需求吧,下面让我们比较下各种实现的优缺点
来看看我们用继承是如何实现的,首先,抽象出一个Shape
接口我想大家都不会有意见的是不是?
1 | /** |
然后我们定义各种情况下的子类,结构如下,看到这么多的子类,是不是有点要爆炸的感觉?真是想想都可怕
而且如果再新增一种需求,比如现在要画椭圆,那么维护的人员估计就要爆粗了吧?
为了避免写出上面的代码,聪明的童鞋们可能会提出第二种方案:
1 | /** |
这样,根据不同的画图需求,只需要设置不同的属性就可以了,这样确实避免了类爆炸增长的问题,但这种方式违反了开放封闭原则,比如画正方形的方式变了,需要对ShapeImpl
进行修改,或者如果新增需求,如画椭圆,也需要对ShapeImpl
进行修改。而且这个类不方便扩展,子类将继承一些对自身并不合适的方法。
装饰模式(Decorator)也叫包装器模式(Wrapper),是指动态地给一个对象添加一些额外的职责
以下情况使用Decorator模式:
但这种灵活也会带来一些缺点,这种比继承更加灵活机动的特性,也同时意味着更加多的复杂性。装饰模式会导致设计中出现许多小类,如果过度使用,会使程序变得很复杂
下面来看看装饰模式的结构:
代码示例如下:
1 | /** |
1 | public class ConcreteComponent implements Component { |
1 | public class Decorator implements Component { |
1 | public class ConcreteDecoratorA extends Decorator { |
1 | public class ConcreteDecoratorB extends Decorator { |
上面说了一堆结构和示例代码,但大家可能还是不太好理解,下面用装饰模式来重新实现画图的功能
先上结构图
首先定义可动态扩展对象的抽象
1 | public interface Shape { |
定义具体的组件,每一个组件代表一个形状
1 | public class Square implements Shape { |
1 | public class Trilateral implements Shape { |
1 | public class Circle implements Shape { |
定义可装饰者的抽象类
1 | public class ShapeDecorator implements Shape { |
定义具体的装饰者
1 | public class Blue extends ShapeDecorator { |
1 | public class Green extends ShapeDecorator { |
1 | public class Red extends ShapeDecorator { |
1 | public class Shadow extends ShapeDecorator { |
好了,现在让我们看看具体怎么使用:
1 | public class Test { |
可以看到,装饰模式是非常灵活的,通过不同的装饰,实现不同的效果
这里再列举一些用到了装饰模式的情景,童鞋们可以根据这些场景加深对装饰模式的理解
IO
设计Context
和ContextWrapper
的设计装饰模式是为已有功能动态地添加功能的一种方式,它把每个要装饰的功能放在单独的类中,并让这个类包括要装饰的对象,有效地把核心职能和装饰功能区分开了。但它带来灵活的同时,也容易导致别人不了解自己的设计方式,不知如何使用。就像Java中I/O库,人们第一次接触的时候,往往无法轻易理解它。这其中的平衡取舍,就看自己咯
]]>现在我们要实现这样一个功能:发送消息。从业务上看,消息又分成普通消息、加急消息和特急消息多种,不同的消息类型,业务功能处理是不一样的,比如加急消息是在消息上添加“加急”字样,而特急消息除了添加特急外,还会做一条催促的记录,多久不完成会继续催促。从发送消息的手段上看,又有系统内短消息、手机短消息、邮件等等。现在要实现这样的发送提示消息的功能,该如何实现呢?
先实现一个简单点的版本:消息只是实现发送普通消息,发送的方式先实现系统内短消息和邮件。其它的功能,等这个版本完成过后,再继续添加,这样先把问题简单化,实现起来会容易一点。由于发送普通消息会有两种不同的实现方式,为了让外部能统一操作,因此,把消息设计成接口,然后由两个不同的实现类,分别实现系统内短消息方式和邮件发送消息的方式。此时系统结构如下:
先来看看消息的统一接口,示例代码如下:
1 | public interface Message { |
再来分别看看两种实现方式,这里只是为了示意,并不会真的去发送Email和站内短消息,先看站内短消息的方式,示例代码如下:
1 | public class CommonMessageSMS implements Message { |
同样的,实现以Email的方式发送普通消息,示例代码如下:
1 | public class CommonMessageEmail implements Message { |
上面的实现,看起来很简单,对不对。接下来,添加发送加急消息的功能,也有两种发送的方式,同样是站内短消息和Email的方式。
加急消息的实现跟普通消息不同,加急消息会自动在消息上添加加急,然后再发送消息;另外加急消息会提供监控的方法,让客户端可以随时通过这个方法来了解对于加急消息处理的进度,比如:相应的人员是否接收到这个信息,相应的工作是否已经开展等等。因此加急消息需要扩展出一个新的接口,除了基本的发送消息的功能,还需要添加监控的功能,这个时候,系统的结构如图所示:
先看看扩展出来的加急消息的接口,示例代码如下:
1 | public interface UrgencyMessage extends Message { |
相应的实现方式还是发送站内短消息和Email两种,同样需要两个实现类来分别实现这两种方式,先看站内短消息的方式,示例代码如下:
1 | public class UrgencyMessageSMS implements UrgencyMessage { |
再看看Emai的方式,示例代码如下:
1 | public class UrgencyMessageEmail implements UrgencyMessage { |
事实上,在实现加急消息发送的功能上,可能会使用前面发送不同消息的功能,也就是让实现加急消息处理的对象继承普通消息的相应实现,这里为了让结构简单一点,清晰一点,所以没有这样做。
上面这样实现,好像也能满足基本的功能要求,可是这么实现好不好呢?有没有什么问题呢?
我们继续向下来添加功能实现,为了简洁,就不再去进行代码示意了,通过实现的结构示意图就可以看出实现上的问题。
特急消息不需要查看处理进程,只要没有完成,就直接催促,也就是说,对于特急消息,在普通消息的处理基础上,需要添加催促的功能。而特急消息、还有催促的发送方式,相应的实现方式还是发送站内短消息和Email两种,此时系统的结构如图所示:
仔细观察上面的系统结构示意图,会发现一个很明显的问题,那就是:通过这种继承的方式来扩展消息处理,会非常不方便。
你看,实现加急消息处理的时候,必须实现站内短消息和Email两种处理方式,因为业务处理可能不同;在实现特急消息处理的时候,又必须实现站内短消息和Email这两种处理方式。
这意味着,以后每次扩展一下消息处理,都必须要实现这两种处理方式,是不是很痛苦,这还不算完,如果要添加新的实现方式呢?继续向下看吧。
如果看到上面的实现,你还感觉问题不是很大的话,继续完成功能,添加发送手机消息的处理方式
仔细观察现在的实现,如果要添加一种新的发送消息的方式,是需要在每一种抽象的具体实现里面,都要添加发送手机消息的处理的。也就是说:发送普通消息、加急消息和特急消息的处理,都可以通过手机来发送。这就意味着,需要添加三个实现。此时系统结构如图所示:
这下能体会到这种实现方式的大问题了吧。
采用通过继承来扩展的实现方式,有个明显的缺点:扩展消息的种类不太容易,不同种类的消息具有不同的业务,也就是有不同的实现,在这种情况下,每个种类的消息,需要实现所有不同的消息发送方式。
更可怕的是,如果要新加入一种消息的发送方式,那么会要求所有的消息种类,都要加入这种新的发送方式的实现。
要是考虑业务功能上再扩展一下呢?比如:要求实现群发消息,也就是一次可以发送多条消息,这就意味着很多地方都得修改,太恐怖了。
那么究竟该如何实现才能既实现功能,又能灵活的扩展呢?
用来解决上述问题的一个合理的解决方案,就是使用桥接模式。那么什么是桥接模式呢?
桥接模式定义:
将抽象部分和实现部分分离,使它们都可以独立地变化
应用桥接模式来解决的思路
仔细分析上面的示例,根据示例的功能要求,示例的变化具有两个维度,一个维度是抽象的消息这边,包括普通消息、加急消息和特急消息,这几个抽象的消息本身就具有一定的关系,加急消息和特急消息会扩展普通消息;另一个维度在具体的消息发送方式上,包括站内短消息、Email和手机短信息,这几个方式是平等的,可被切换的方式。这两个维度一共可以组合出9种不同的可能性来。
现在出现问题的根本原因,就在于消息的抽象和实现是混杂在一起的,这就导致了,一个维度的变化,会引起另一个维度进行相应的变化,从而使得程序扩展起来非常困难。
要想解决这个问题,就必须把这两个维度分开,也就是将抽象部分和实现部分分开,让它们相互独立,这样就可以实现独立的变化,使扩展变得简单。
桥接模式通过引入实现的接口,把实现部分从系统中分离出去;那么,抽象这边如何使用具体的实现呢?肯定是面向实现的接口来编程了,为了让抽象这边能够很方便的与实现结合起来,把顶层的抽象接口改成抽象类,在里面持有一个具体的实现部分的实例。
这样一来,对于需要发送消息的客户端而言,就只需要创建相应的消息对象,然后调用这个消息对象的方法就可以了,这个消息对象会调用持有的真正的消息发送方式来把消息发送出去。也就是说客户端只是想要发送消息而已,并不想关心具体如何发送。
桥接模式的结构图:
先看看Implementor接口的定义,示例代码如下:
1 | public interface Implementor { |
再看看Abstraction接口的定义,注意一点,虽然说是接口定义,但其实是实现成为抽象类。示例代码如下:
1 | public abstract class Abstraction { |
该来看看具体的实现了,示例代码如下:
1 | public class ConcreteImplementorA implements Implementor { |
另外一个实现,示例代码如下:
1 | public class ConcreteImplementorB implements Implementor { |
最后来看看扩展Abstraction接口的对象实现,示例代码如下:
1 | public class RefinedAbstraction extends Abstraction { |
学习了桥接模式的基础知识过后,该来使用桥接模式重写前面的示例了。通过示例,来看看使用桥接模式来实现同样的功能,是否能解决“既能方便的实现功能,又能有很好的扩展性”的问题。
要使用桥接模式来重新实现前面的示例,首要任务就是要把抽象部分和实现部分分离出来,分析要实现的功能,抽象部分就是各个消息的类型所对应的功能,而实现部分就是各种发送消息的方式。
其次要按照桥接模式的结构,给抽象部分和实现部分分别定义接口,然后分别实现它们就可以了。
从相对简单的功能开始,先实现普通消息和加急消息的功能,发送方式先实现站内短消息和Email这两种。使用桥接模式来实现这些功能的程序结构如图所示
还是看看代码实现,会更清楚一些。先看看消息发送器接口,示例代码如下:
1 | /** |
再看看抽象部分定义的接口,示例代码如下:
1 | /** |
看看如何具体的实现发送消息,先看站内短消息的实现吧,示例代码如下:
1 | /** |
再看看Email方式的实现,示例代码如下:
1 | /** |
接下来该看看如何扩展抽象的消息接口了,先看普通消息的实现,示例代码如下:
1 | public class CommonMessageController extends AbstractMessageController { |
再看看加急消息的实现,示例代码如下:
1 | public class UrgencyMessageController extends AbstractMessageController { |
看了上面的实现,发现使用桥接模式来实现也不是很困难啊,关键得看是否能解决前面提出的问题,那就来添加还未实现的功能看看,添加对特急消息的处理,同时添加一个使用手机发送消息的方式。该怎么实现呢?
很简单,只需要在抽象部分再添加一个特急消息的类,扩展抽象消息就可以把特急消息的处理功能加入到系统中了;对于添加手机发送消息的方式也很简单,在实现部分新增加一个实现类,实现用手机发送消息的方式,也就可以了。
这么简单?好像看起来完全没有了前面所提到的问题。的确如此,采用桥接模式来实现过后,抽象部分和实现部分分离开了,可以相互独立的变化,而不会相互影响。因此在抽象部分添加新的消息处理,对发送消息的实现部分是没有影响的;反过来增加发送消息的方式,对消息处理部分也是没有影响的。
接着看看代码实现,先看看新的特急消息的处理类,示例代码如下:
1 | public class SpecialUrgencyMessageController extends AbstractMessageController { |
再看看使用手机短消息的方式发送消息的实现,示例代码如下:
1 | public class MessageSenderMobile implements MessageSender { |
看了上面的实现,可能会感觉得到,使用桥接模式来实现前面的示例过后,添加新的消息处理,或者是新的消息发送方式是如此简单,可是这样实现,好用吗?写个客户端来测试和体会一下,示例代码如下:
1 | public class Client { |
运行结果如下:
1 | 使用站内短消息的方式,发送消息'请喝一杯茶'给小李 |
前面三条是使用的站内短消息,后面三条是使用的手机短消息,正确的实现了预期的功能。看来前面的实现应该是正确的,能够完成功能,且能灵活扩展。
使用Java编写程序,一个很重要的原则就是“面向接口编程”,说得准确点应该是“面向抽象编程”,由于在Java开发中,更多的使用接口而非抽象类,因此通常就说成“面向接口编程”了。接口把具体的实现和使用接口的客户程序分离开来,从而使得具体的实现和使用接口的客户程序可以分别扩展,而不会相互影响。
桥接模式中的抽象部分持有具体实现部分的接口,最终目的是什么,还不是需要通过调用具体实现部分的接口中的方法,来完成一定的功能,这跟直接使用接口没有什么不同,只是表现形式有点不一样。再说,前面那个使用接口的客户程序也可以持有相应的接口对象,这样从形式上就一样了。
也就是说,从某个角度来讲,桥接模式不过就是对“面向抽象编程”这个设计原则的扩展。正是通过具体实现的接口,把抽象部分和具体的实现分离开来,抽象部分相当于是使用实现部分接口的客户程序,这样抽象部分和实现部分就松散耦合了,从而可以实现相互独立的变化。
这样一来,几乎可以把所有面向抽象编写的程序,都视作是桥接模式的体现,至少算是简化的桥接模式,就算是广义的桥接吧。而Java编程很强调“面向抽象编程”,因此,广义的桥接,在Java中可以说是无处不在。
如果各位童鞋看到这里仍然对桥接模式还是不太清楚,在这里给大家举个在Android中非常常用的桥接模式栗子:AbsListView
与ListAdapter
之间的桥接模式。童鞋们可以根据这个栗子体会一下桥接模式的好处。
我们执行一个功能的函数时,经常需要在其中写入与功能不是直接相关但很有必要的代码,如日志记录、信息发送、安全和事务支持等,这些枝节性代码
虽然是必要的,但它会带来以下麻烦:
毫无疑问,枝节性代码和功能性代码需要分开来才能降低耦合程度,我们可以使用代理模式(委托模式)完成这个要求。代理模式的作用是:为其它对象提供一种代理以控制对这个对象的访问。在某些情况下,一 个客户不想直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介作用。
代理模式一般涉及到三个角色:
常见的代理应用场景有:
接下来,我们用代码来说明什么是代理模式
先看看代理模式的结构图:
下面给出一个小栗子说明代理模式,先定义一个抽象角色,也就是一个公共接口,声明一些需要代理的方法,本文定义一个Subject
接口,为了简单说明,只是在里面定义一个request方法:
1 | public interface Subject { |
定义Subject的实现类RealSubject
,它是一个真实角色:
1 | public class RealSubject implements Subject { |
定义一个代理角色ProxySubject
,跟RealSubject一样,它也继承了Subject接口:
1 | public class ProxySubject implements Subject { |
客户端调用代码
1 | public class Client { |
这样,一个简易的代理模式模型就建立了,客户端在使用过程中,无需关注RealSubject,只需要关注ProxySubject就行了,并且可以在ProxySubject中插入一些非功能信的代码,比如输出Log,统计执行时间等等
远程代理,对一个位于不同的地址空间对象提供一个局域代表对象。这样说大家可能比较抽象,不太能理解,但其实童鞋们可能在就接触过了,在Android中,Binder的使用就是典型的远程代理。比如ActivityManager:
在启动Activity的时,会调用ActivityManager
的startActivity方法,我们看看Activity是怎么获取的:
1 | static public IActivityManager asInterface(IBinder obj) { |
可以看到,最终是返回了一个ActivityManager的代理类,因为真正的ActivityManager是运行在内核空间的,Android应用无法直接访问得到,那么就可以借助这个ActivityManagerProxy,通过Binder与真正的ActivityManager,也就是ActivityManagerService
交互。其中ActivityManagerService和ActivityManagerProxy都实现了同一个接口:IActivityManager
。这个就是Android中典型的代理模式的栗子了。至于ActivityManagerService和ActivityManagerProxy是如何通过Binder实现远程调用,这个就是另一个话题Binder的内容了,这里不再做阐述
根据需要将一个资源消耗很大或者比较复杂的对象,延迟加载,在真正需要的时候才创建。假设我们创建RealSubject需要耗费一定的资源,那么,我们可以把创建它延迟到实际调用的时候,优化Client初始化速度,比如,这样修改ProxySubject以达到延迟加载:
1 | public class ProxySubject implements Subject { |
Client在实例化ProxySubject的时候,不需消耗资源,而是等到真正调用request的时候,才会加载RealSubject,达到延时加载的效果
可以在Proxy类中加入进行权限,验证是否具有执行真实代码的权限,只有权限验证通过了才进行真实对象的调用
1 | public class ProxySubject implements Subject { |
通过引入代理类,可以方便地在功能性代码前后插入扩展,如Log输出,调用统计等,实现对原代码的无侵入式代码扩展,如:
1 | public class ProxySubject implements Subject { |
静态代理和动态代理的概念和使用可以参考我另一篇文章:Java动态代理:http://blog.csdn.net/shensky711/article/details/52872249
]]>在开发过程中,为了实现解耦,我们经常使用依赖注入,常见的依赖注入方式有:
下面用一个小栗子来说明三种方式的用法:
1 | public class PersonService implements DependencyInjecter { |
我们来看下使用一般的依赖注入方法时,代码会是怎么样的:
1 | public class MainActivity extends AppCompatActivity { |
看起来还好是吧?但现实情况下,依赖情况往往是比较复杂的,比如很可能我们的依赖关系如下图:
PersonDaoImpl依赖类A,类A依赖B,B依赖C和D…在这种情况下,我们就要写出下面这样的代码了:
1 | public class MainActivity extends AppCompatActivity { |
MainActivity只是想使用PersonService而已,却不得不关注PersonService的依赖是什么、PersonDaoImpl依赖的依赖是什么,需要把整个依赖关系搞清楚才能使用PersonService。而且还有一个不好的地方,一旦依赖关系变更了,比如A不再依赖B了,那么就得修改所有创建A的地方。那么,有没有更好的方式呢?Dagger就是为此而生的,让我们看看使用Dagger后,MainActivity会变成什么模样:
1 | public class MainActivity extends AppCompatActivity { |
之前创建A、B、C、D、PersonDaoImpl等依赖的代码全不见了,只需要调用一个注入语句就全搞定了。调用了注入语句之后,mService就可以正常使用了,是不是挺方便呢?至于这句注入语句具体干了什么,读者现在可以先不管,后面会有详细说明,这里只是做一个使用演示而已。
我们大概猜想一下,在MainActivity使用PersonService需要做哪些?
其实Dagger做的也就是上面这些事情了,接下来就让我们真正开始学习Dagger吧
首先我们应该用javax.inject.Inject
去注解需要被自动注入的对象,@Inject是Java标准的依赖注入(JSR-330)注解。比如下面栗子中,需要注入的对象就是MainActivity的mService。这里有个要注意的地方,被@Inject注解的变量不能用private修饰
1 | public class MainActivity extends AppCompatActivity { |
在执行依赖注入的时候,Dagger会查找@Inject注解的成员变量,并尝试获取该类的实例,Dagger最直接的方式就是直接new出相应的对象了。实例化对象的时候,会调用对象的构造方法,但假如有多个构造方法,具体用哪个构造方法来实例化对象?Dagger肯定是不会帮我们“擅自做主”的,用哪个构造方法来实例化对象应该是由我们做主的,所以我们需要给相应的构造方法添加@Inject注解。
当Dagger需要实例化该对象的时候,会调用@Inject注解的构造方法来实例化对象:
1 | public class PersonService implements DependencyInjecter { |
聪明的你应该发现了,调用PersonService的构造方法需要传入PersonDao实例,所以要实例化PersonService,必须先要实例化PersonDao,Dagger会帮我们自动分析出这个依赖关系,并把它添加到依赖关系图里面!Dagger会尝试先去实例化一个PersonDao,如果PersonDao又依赖于另外一个对象A,那么就先尝试去实例化A……以此类推,是不是很像递归?当所有依赖都被实例化出来之后,我们的PersonService当然也被构造出来了。
问题又来了,如果PersonDao是一个接口呢?Dagger怎么知道这个接口应该怎么实现?答案是不知道的,那么Dagger怎么实例化出一个接口出来?这个就是Module存在的意义之一了。关于Module的讲解我们会在后面详细说明,我们现在只要知道,Module里面会定义一些方法,这些方法会返回我们的依赖,就像:
1 |
|
Dagger根据需求获取一个实例的时候,并不总是通过new出来的,它会优先查找Module
中是否有返回相应实例的方法,如果有,就调用Module的方法来获取实例。
比如你用@Inject注解了一个成员变量,Dagger会查找Module中是否有用@Provides注解的,返回该类实例的方法,有的话就会调用provide方法来获得实例,然后注入,如果没有的话Dagger就会尝试new出一个实例。就像我们现在这个栗子,PersonService依赖于PersonDao接口,Dagger不能直接为我们new出一个接口,但我们可以提供一个Module,在Module中定义一个返回PersonDao接口实例的方法,这样,Dagger就可以解决实例化PersonDao的问题了。
我们再梳理一下流程,如果我们用@Inject注解了一个成员变量,并调用注入代码之后,Dagger会这样处理:
所以假如一个变量被@Inject注解,要么在Module中提供provide方法获取实例,要么该类提供一个被@Inject注解的构造方法,否则Dagger会出错
一般而言,Dagger会获取所有依赖的实例,比如当需要一个TestBean
的时候,会通过new TestBean()
创建实例并注入到类中。但是,以下情况会就不好处理了:
为了解决以上问题,我们需要定义一个被@Module注解的类,在里面定义用@Provides
注解的方法。用该方法返回所需的实例。
1 |
|
就像providePersonDao
返回了PersonDao接口实例,Dagger虽然不能直接实例化出PersonDao接口,但却可以调用Module的providePersonDao方法来获得一个实例。providePersonDao方法需要传入A的实例,那么这里也构成了一个依赖关系图。Dagger会先获取A的实例,然后把实例传递给providePersonDao方法。
到目前为止,我们虽然知道了:
看样子需要注入的依赖可以获取了,但是不是总觉得还有点“零碎”,整个流程还没连贯起来?比如,Module既然是一个类,生成依赖图的时候,怎么知道跟哪个Module挂钩?即使最后生成了需要的实例,注入的“目的地”是哪里?怎么才能把它注入到“目的地”?残缺的这部分功能,正是Component提供的,Component起到了一个桥梁的作用,贯通Module和注入目标。我们来看看最开始那个例子,我们是怎么进行依赖注入的:
1 | public class MainActivity extends AppCompatActivity { |
这个DaggerPersonServiceComponent是什么鬼?DaggerPersonServiceComponent其实是Dagger为我们自动生成的类,它实现了一个Component接口(这个接口是需要我们自己写的),我们来看下它实现的接口长什么样子:
1 | /** |
这个接口被Component注解修饰,它里面可以定义3种类型的方法:
既然获取实例的时候,有可能用到Module,那么就必须为这个Component指定使用的Module是什么。具体做法就是在@Component注解中指定modules。
定义好Component之后,Dagger会自动帮我们生成实现类,这就是Dagger强大的地方!生成的类名格式是:Dagger+Component名。
Component提供了2种方法,一个是注入式方法,一个是获取实例方法。具体用什么方法,就看个人需求了。一个Component其实也对应了一个依赖图,因为Component使用哪个Module是确定不变的,依赖关系无非也就是跟Module和类的定义有关。一旦这些都确定下来了,在这个Component范围内,依赖关系也就被确定下来了。额外再说一点,在Dagger1中,Component的功能是由ObjectGraph
实现的,Component是用来代替它的。
Component定义好之后,build一下工程,Dagger就会自动为我们生成实现类了,就可以使用自动生成的实现类来进行依赖注入了。到现在为止,我们已经通过Dagger完成了依赖注入。可能看起来比正常方法麻烦得多,但是Dagger框架可以让依赖的注入和配置独立于组件之外,它帮助你专注在那些重要的功能类上。通过声明依赖关系和指定规则构建整个应用程序。
熟悉完Dagger基本的使用之后,接下来我们来讲解一些稍微高级一点的用法:
在Dagger中,Component之间可以有两种关系:Subcomponents和Component dependencies。他们有什么作用呢?比如在我们应用中,经常会有一些依赖我们在各个界面都使用得到,比如操作数据库、比如网络请求。假设我们有个ServerApi的接口,在页面A、B、C都使用到了,那么我们要在页面A、B、C的Component里面都能获取到ServerApi的实例,但显然,获取ServerApi实例的方法都是一样的,我们不想写重复的代码。于是我们可定义一个ApplicationComponent,在里面返回ServerApi实例,通过Component之间的关系便可以共享ApplicationComponent提供的依赖图。
下面通过Android中的一个小栗子来说明Subcomponents和Component dependencies如何使用
先说明下各个模块之间的关系
首先,我们定义一个ApplicationComponent,它定义了一个方法,通过它来获得ServerApi实例。ApplicationComponent还关联了ApplicationModule,这个Module是ServerApi实例的提供者,注意,这个Moduld还可以返回Context实例
1 | (modules = ApplicationModule.class) |
1 |
|
1 | public class DemoApplication extends Application { |
MainActivity使用MVP模式,在MainPresenter里面需要传入一个ServerApi对象
1 | // 注意,这里有个dependencies声明 |
1 |
|
1 | public class MainPresenter { |
先抛开dependencies,我们分析这个这个依赖树是怎么样的
Component中getMainPresenter的目的很简单,就是返回MainPresenter,而MainPresenter又依赖MainView和ServerApi,MainView还好说,在MainPresenterModule中有provide方法,但是ServerApi呢?就像上面说的那样,如果我们在这个Moduld中也添加相应的provide方法,那真是太麻烦了(当然,这样做完全是可以实现的),所以我们依赖了ApplicationComponent,通过dependencies,在被依赖的Component暴露的对象,在子Component中是可见的。这个是什么意思呢?意思有两个:
对于第一点应该比较好理解,就像这个栗子,MainPresenterComponent生成MainPresenter需要ServerApi,而ApplicationComponent中有接口暴露了ServerApi,所以MainPresenterComponent可以获得ServerApi
对于第二点,假设MainPresenter还需要传入一个Context对象,我们注意到,ApplicationModule是可以提供Context的,那MainPresenterComponent能不能通过ApplicationComponent获取Context实例?答案是不行的,因为ApplicationComponent没有暴露这个对象。想要获取Context,除非ApplicationComponent中再添加一个getContext的方法。
他们之间的关系可以用下图描述:
Subcomponents 实现方法一:
让我们对上面的栗子改造改造:
去除MainPresenterComponent的Component注解,改为Subcomponent:
1 | (modules = MainPresenterModule.class) |
在ApplicationComponent中新增plus方法(名字可随意取),返回值为MainPresenterComponent,参数为MainPresenterModule:
1 | (modules = ApplicationModule.class) |
这样,就构建了一个ApplicationComponent的子图:MainPresenterComponent。子图和dependencies的区别就是,子图可以范围父图所有的依赖,也就是说,子图需要的依赖,不再需要在父Component中暴露任何对象,可以直接通过父图的Moduld提供!他们的关系变为了:
这里需要注意的是,以上代码直接在父 Component 返回子 Component 的形式,要求子 Component 依赖的 Module 必须包含一个无参构造函数,用以自动实例化。如果 Module 需要传递参数,则需要使用 @Subcomponent.builder
的方式,实现方法二实现步骤如下:
代码如下:
1 |
|
在Dagger2中,一般都是使用@provide方法注入接口。在Android 中,一般我们会这样做,创建一个接口 Presenter 命名 为 HomePresenter
1 | public interface HomePresenter { |
然后创建一个这个接口的实例:HomePresenterImp
1 | public class HomePresenterImp implements HomePresenter { |
然后在 Module 中,提供实例化的 provide 方法:
1 |
|
但是,如果我们需要添加一个依赖到 presenter 叫 UserService,那就意味着,我们也要在 module 中添加一个 provide 方法提供这个 UserService,然后在 HomePresenterImp 类中加入一个 UserService 参数的构造方法。
有没有觉得这种方法很麻烦呢?我们还可以用 @Binds 注解,如:
1 |
|
除了方便,使用 @Binds 注解还可以让 dagger2 生成的代码效率更高。但是需要注意的是,由于 Module 变为抽象类,Module 不能再包含非 static 的带 @Provides 注解的方法。而且这时候,依赖此 Module 的 Component 也不需要传入此 Module 实例了(也实例化不了,因为它是抽象的)。相当于此 Module 仅仅作为描述依赖关系的一个类
Scopes可是非常的有用,Dagger2可以通过自定义注解限定注解作用域。@Singleton是被Dagger预先定义的作用域注解。
我们通常的ApplicationComponent都会使用Singleton注解,也就会是说我们如果自定义component必须有自己的scope。读者到这里,可能还不能理解Scopes的作用,我们先来看下默认提供的Singlton到底有什么作用,然后再讨论Scopes的意义:
Singletons是java提供的一个scope,我们来看看Singletons能做什么事情。
为@Provides注释的方法或可注入的类添加添加注解@Singlton,构建的这个对象图表将使用唯一的对象实例,比如我们有个ServerApi
方法一:用@Singleton注解类:
1 |
|
方法二:用@Singleton注解Module的provide方法:
1 |
|
然后我们有个Component:
1 |
|
然后执行依赖注入:
1 | public class MainActivity extends AppCompatActivity { |
使用了以上两种方法的任意一种,我们都会发现,通过component.getServerApi()获得的实例都是同一个实例。不过要注意一点的是,如果类用@Singleton注解了,但Module中又存在一个provide方法是提供该类实例的,但provide方法没有用@Singleton注解,那么Component中获取该实例就不是单例的,因为会优先查找Module的方法。
这个单例是相对于同一个Component而言的,不同的Component获取到的实例将会是不一样的。
既然一个没有scope的component不可以依赖一个有scope的组件component,那么我们必然需要自定义scope来去注解自己的Component了,定义方法如下:
1 |
|
定义出来的FragmentScoped在使用上和Singleton是一样的,那它和Singleton除了是不一样的注解之外,还有什么不一样呢?答案是没有!我们自定义的scope和Singleton并没有任何不一样,不会因为Singleton是java自带的注解就会有什么区别。
那么,这个scope的设定是为了什么呢?
scope除了修饰provide方法可以让我们获得在同一个Component实例范围内的单例之外,主要的作用就是对Component和Moduld的分层管理以及依赖逻辑的可读性。
这里借用一个网络上的图片说明:
ApplicationComponent一般会用singleton注解,相对的,它的Module中provide方法也只能用singleton注解。UserComponent是用UserSCope能直接使用ApplicationModule吗?不能!因为他俩的scope不一致,这就是这个设定带来的好处,防止不同层级的组件混乱。另外,因为有了scope的存在,各种组件的作用和生命周期也变得可读起来了
有时可能会需要延迟获取一个实例。对任何绑定的 T,可以构建一个 Lazy
1 | public class MainActivity extends AppCompatActivity implements MainView { |
跟Lazy注入不一样的是,有时候我们希望每次调用get的时候,获取到的实例都是不一样的,这时候可以用Provider注入
1 | public class MainActivity extends AppCompatActivity implements MainView { |
到目前为止,我们的demo里,Moduld的provide返回的对象都是不一样的,但是下面这种情况就不好处理了:
1 |
|
provideServerApiA和provideServerApiB返回的都是ServerApi,Dagger是无法判断用哪个provide方法的。这时候就需要添加Qualifiers了:
1 |
|
通过这样一个限定,就能区分出2个方法的区别了,当然,在使用过程中,也同样要指明你用哪个name的实例,Dagger会根据你的name来选取对应的provide方法:
1 | public class MainPresenter { |
除了用Named注解,你也可以创建你自己的限定注解:
1 |
|
Dagger 包含了一个注解处理器(annotation processor)来验证模块和注入。这个过程很严格而且会抛出错误,当有非法绑定或绑定不成功时。下面这个例子缺少了 Executor:
1 |
|
当编译时,javac 会拒绝绑定缺少的部分:
1 | [ERROR] COMPILATION ERROR : |
可以通过给方法 Executor 添加@Provides注解来解决这个问题,或者标记这个模块是不完整的。不完整的模块允许缺少依赖关系
1 | false) (complete = |
第一次接触用Dagger框架写的代码时候,如果不了解各种注解作用的时候,那真会有一脸懵逼的感觉,而且单看文章,其实还是很抽象,建议大家用Dagger写个小demo玩玩,很快就上手了,这里提供几个使用Dagger的栗子,希望可以帮助大家上手Dagger
]]>Android的单元测试可以分为两部分:
如果使用Local测试,需要保证测试过程中不会调用Android系统API,否则会抛出RuntimeException异常,因为Local测试是直接跑在本机JVM的,而之所以我们能使用Android系统API,是因为编译的时候,我们依赖了一个名为“android.jar”的jar包,但是jar包里所有方法都是直接抛出了一个RuntimeException,是没有任何任何实现的,这只是Android为了我们能通过编译提供的一个Stub!当APP运行在真实的Android系统的时候,由于类加载机制,会加载位于framework的具有真正实现的类。由于我们的Local是直接在PC上运行的,所以调用这些系统API便会出错。
那么问题来了,我们既要使用Local测试,但测试过程又难免遇到调用系统API那怎么办?其中一个方法就是mock objects,比如借助Mockito,另外一种方式就是使用Robolectric
, Robolectric就是为解决这个问题而生的。它实现一套JVM能运行的Android代码,然后在unit test运行的时候去截取android相关的代码调用,然后转到他们的他们实现的Shadow代码去执行这个调用的过程
1 | testCompile "org.robolectric:robolectric:3.1.4" |
Robolectric在第一次运行时,会下载一些sdk依赖包,每个sdk依赖包大概50M,下载速度比较慢,用户可以直接在网上下载相应依赖包,放置在本地maven仓库地址中,默认路径为:C:\Users\username\.m2\repository\org\robolectric
为测试用例添加注解,指定测试运行器为RobolectricTestRunner。注意,这里要通过Config指定constants = BuildConfig.class
,Robolectric 会通过constants推导出输出路径,如果不进行配置,Robolectric可能不能找到你的manifest、resources和assets资源
1 | (RobolectricTestRunner.class) |
Shadow是Robolectric的立足之本,如其名,作为影子,一定是变幻莫测,时有时无,且依存于本尊。Robolectric定义了大量模拟Android系统类行为的Shadow类,当这些系统类被创建的时候,Robolectric会查找对应的Shadow类并创建一个Shadow类与原始类关联。每当系统类的方法被调用的时候,Robolectric会保证Shadow对应的方法会调用。这些Shadow对象,丰富了本尊的行为,能更方便的对Android相关的对象进行测试。
比如,我们可以借助ShadowActivity验证页面是否正确跳转了
1 | /** |
可以通过@Config
定制Robolectric的运行时的行为。这个注解可以用来注释类和方法,如果类和方法同时使用了@Config,那么方法的设置会覆盖类的设置。你可以创建一个基类,用@Config配置测试参数,这样,其他测试用例就可以共享这个配置了
Robolectric会根据manifest文件配置的targetSdkVersion选择运行测试代码的SDK版本,如果你想指定sdk来运行测试用例,可以通过下面的方式配置
1 | (sdk = Build.VERSION_CODES.JELLY_BEAN) |
Robolectric会根据manifest文件配置的Application配置去实例化一个Application类,如果你想在测试用例中重新指定,可以通过下面的方式配置
1 | (application = CustomApplication.class) |
Robolectric可以让你配置manifest、resource和assets路径,可以通过下面的方式配置
1 | "some/build/path/AndroidManifest.xml", (manifest = |
当Robolectric测试的时候,会尝试加载所有应用提供的资源,但如果你需要使用第三方库中提供的资源文件,你可能需要做一些特别的配置。不过如果你使用gradle来构建Android应用,这些配置就不需要做了,因为Gradle Plugin会在build的时候自动合并第三方库的资源,但如果你使用的是Maven,那么你需要配置libraries变量:
1 | (RobolectricTestRunner.class) |
Android会在运行时加载特定的资源文件,如根据设备屏幕加载不同分辨率的图片资源、根据系统语言加载不同的string.xml,在Robolectric测试当中,你也可以进行一个限定,让测试程序加载特定资源.多个限定条件可以用破折号拼接在在一起。
1 | /** |
如果你嫌通过注解配置上面的东西麻烦,你也可以把以上配置放在一个Properties文件之中,然后通过@Config指定配置文件,比如,首先创建一个配置文件robolectric.properties:
1 | # 放置Robolectric的配置选项: |
然后把robolectric.properties文件放到src/test/resources目录下,运行的时候,会自动加载里面的配置
以上设置可以通过Gradle进行配置,如:
1 | android { |
利用ActivityController
我们可以让Activity执行相应的生命周期方法,如:
1 |
|
通过ActivityController,我们可以模拟各种生命周期的变化。但是要注意,我们虽然可以随意调用Activity的生命周期,但是Activity生命周期切换有自己的检测机制,我们要遵循Activity的生命周期规律。比如,如果当前Activity并非处于stop状态,测试代码去调用了controller.restart方法,此时Activity是不会回调onRestart和onStart的。
除了控制生命周期,还可以在启动Activity的时候传递Intent:
1 | /** |
onRestoreInstanceState回调中传递Bundle:
1 | /** |
在真实环境下,视图是在onCreate之后的某一时刻在attach到Window上的,在此之前,View是处于不可操作状态的,你不能点击它。在Activity的onPostResume方法调用之后,View才会attach到Window之中。但是,在Robolectric之中,我们可以用控制器的visible
方法使得View变为可见,变为可见之后,就可以模拟点击事件了
1 |
|
为了减少依赖包的大小,Robolectric的shadows类成了好几部分:
SDK Package | Robolectric Add-On Package |
---|---|
com.android.support.support-v4 | org.robolectric:shadows-support-v4 |
com.android.support.multidex | org.robolectric:shadows-multidex |
com.google.android.gms:play-services | org.robolectric:shadows-play-services |
com.google.android.maps:maps | org.robolectric:shadows-maps |
org.apache.httpcomponents:httpclient | org.robolectric:shadows-httpclient |
用户可以根据自身需求添加以下依赖包,如
1 | dependencies { |
有参构造方法
,在Shadow类中定义public void类型的名为__constructor__
的方法,且方法参数与原始类的构造方法参数一直下面我们来创建RobolectricBean的Shadow类
原始类:
1 | public class RobolectricBean { |
Shadow类:
1 | /** |
Shadow类中可以定义一个原始类的成员变量,并用@RealObject注解,这样,Shadow类就能访问原始类的field了,但是注意,通过@RealObject注解的变量调用方法,依然会调用Shadow类的方法,而不是原始类的方法,只能用它来访问原始类的field。
1 | (Point.class) |
在Config注解中添加shadows
参数,指定对应的Shadow生效
1 | (RobolectricTestRunner.class) |
注意,自定义的Shadow类不能通过Shadows.shadowOf()
获取,需要用ShadowExtractor.extract()
来获取,获取之后进行类型转换:
1 | ShadowRobolectricBean shadowBean = (ShadowRobolectricBean) ShadowExtractor.extract(bean); |
1 | /** |
1 | /** |
1 | /** |
1 | /** |
1 | (RobolectricTestRunner.class) |
首先看下广播接收器:
1 | public class MyReceiver extends BroadcastReceiver { |
广播的测试点可以包含两个方面
1 | (RobolectricTestRunner.class) |
Service和Activity一样,都有生命周期,Robolectric也提供了Service的生命周期控制器,使用方式和Activity类似,这里就不做详细解释了
1 | (RobolectricTestRunner.class) |
我们知道,OOP三个基本特征是:封装、继承、多态。通过继承,我们可以基于差异编程,也就是说,对于一个满足我们大部分需求的类,可以创建它的一个子类并只改变我们不期望的那部分。但是在实际使用中,继承很容易被过度使用,并且过度使用的代价是比较高的,所以我们减少了继承的使用,使用组合或委托代替
优先使用对象组合而不是类继承
在本文中,我们会分别介绍模板方法模式
和策略模式
,这两个模式分别使用了继承和委托两种方式。这两种模式解决的问题是类似的,经常可以互换使用,它们都可以分离通用的算法和具体的上下文。比如我们有一个通用的算法,算法有不同的实现方式,为了遵循依赖倒置原则,我们希望算法不依赖于具体实现。
本文冒泡排序法来进行举例说明:
1 | /** |
这是我们实现的冒泡排序算法,这个sort方法可以对int数组进行排序。但我们发现,这种写法的扩展性是不强的,如果我们要实现double数组排序呢?如果我们需要排序的是一个对象数组?难道需要各自定义一个方法吗?如果它们都使用冒泡排序算法,那么sort的算法逻辑肯定是相似的,有没有一种方法能让这个算法逻辑复用呢?下面用模板方法模式和策略模式对它进行改造
模板方法模式:定义一个算法的骨架,将骨架中的特定步骤延迟到子类中。模板方法模式使得子类可以不改变算法的结构即可重新定义该算法的某些特定步骤
下图是用模板方法模式对冒泡排序重构后的结构图:
首先,我们在BubbleSorter的sort方法中定义算法骨架,再定义一些延迟到子类中的抽象方法:
1 | /** |
有了BubbleSorter
类,我们就可以创建任意不同类型的对象排序的简单派生类,比如创建IntBubbleSorter
去排序整型数组:
1 | public class IntBubbleSorter extends BubbleSorter<int[]> { |
再比如创建DoubleBubbleSorter
去排序双精度型数组:
1 | public class DoubleBubbleSorter extends BubbleSorter<double[]> { |
甚至我们不仅限于对数组排序,还可以对List集合排序,比如创建IntegerListBubbleSorter
对List集合进行冒泡排序:
1 | public class IntegerListBubbleSorter extends BubbleSorter<List<Integer>> { |
定义上述类之后,我们看下怎么使用上面的类:
1 | public class Test { |
模板方法模式展示了经典重用的一种形式,通用算法被放在基类中,通过继承在不同的子类中实现该通用算法。我们通过定义通用类BubbleSorter,把冒泡排序的算法骨架放在基类,然后实现不同的子类分别对int数组、double数组、List集合进行排序。但这样是有代价的,因为继承是非常强的关系,派生类不可避免地与基类绑定在一起了。但如果我现在需要用快速排序而不是冒泡排序来进行排序,但快速排序却没有办法重用setArray
、getLength
、needSwap
和swap
方法了。不过,策略模式提供了另一种可选的方案
策略模式属于对象的行为模式。其用意是针对一组算法,将每一个算法封装到具有共同接口的独立的类中,从而使得它们可以相互替换,下面用策略模式对冒泡排序进行重构
下图是用策略模式对冒泡排序重构后的结构图:
首先定义一个BubbleSorter类,它持有一个抽象策略接口:
1 | public class BubbleSorter<T> { |
定义抽象策略接口:
1 | public interface SortHandler<T> { |
创建具体的策略类IntSortHandler
对整型数组进行操作:
1 | public class IntSortHandler implements SortHandler<int[]> { |
创建具体的策略类DoubleSortHandler
对双精度型数组进行操作:
1 | public class DoubleSortHandler implements SortHandler<double[]> { |
创建具体的策略类IntegerListSortHandler
对List集合进行操作:
1 | public class IntegerListSortHandler implements SortHandler<List<Integer>> { |
定义上述类之后,我们看下怎么使用策略模式
1 | public class Test { |
策略模式不是将通用方法放到基类中,而是把它放进BubbleSorter
的sort方法中,把排序算法中必须调用的抽象方法定义在SortHandler
接口中,从这个接口中派生出不同的子类。把派生出的子类传给BubbleSorter后,sort方法就可以把具体工作委托给接口去完成。注意:SortHandler对BubbleSorter是一无所知的,它不依赖于冒泡排序的具体实现,这个和模板方法模式是不同的。如果其他排序算法也需要用到SortHandler,完全也可以在相关的排序算法中使用SortHandler
模板方法模式和策略模式都可以用来分离高层的算法和低层的具体实现细节,都允许高层的算法独立于它的具体实现细节重用。但策略模式还有一个额外的好处就是允许具体实现细节独立于高层的算法重用,但这也以一些额外的复杂性、内存以及运行事件开销作为代价
文中示例代码下载:https://github.com/hanschencoder/awesome-demo/tree/master/Patterns
]]>根据依赖倒置原则
,我们知道,我们应优先依赖抽象类而不是具体类。在应用开发过程中,有很多实体类都是非常易变的,依赖它们会带来问题,所以我们更应该依赖于抽象接口,已使我们免受大多数变化的影响。工厂模式(Factory)
允许我们只依赖于抽象接口就能创建出具体对象的实例,所以在开发中,如果具体类是高度易变的,那么该模式就非常有用。
接下来我们就通过代码举例说明什么是工厂模式
假设我们现在有个需求:把一段数据用Wi-Fi或者蓝牙发送出去。
需求很简单是吧?刷刷刷就写下了以下实现:
1 | private String mode; //Wi-Fi|Bluetooth |
但是上面的代码扩展性并不高,违反了开放封闭原则。比如现在又有了个新的需求,需要用zigbee把数据发送出去,就得再新增一个sendDataByZigbee方法了,而且还得修改onClick里面的逻辑。那么比较好的方法是怎么样的呢?
定义一个数据发送器类:
1 | /** |
实现WiFi数据发送:
1 | /** |
实现蓝牙数据发送:
1 | /** |
这样,原来发送数据的地方就改为了:
1 | private String mode; //Wi-Fi|Bluetooth |
有没有觉得代码优雅了一点?但是随着发送器Sender的实现类越来越多,每增加一个实现类,就需要在onClick里面实例化相应的实现类,能不能用一个单独的类来做这个创造实例的过程呢?这就是我们讲到的工厂。我们新增一个工厂类:
1 | /** |
这样一来,怎么实例化数据发送器我们也不用管了,最终代码变为:
1 | private String mode; //Wi-Fi|Bluetooth |
好了,到这里我们就完成了简单工厂模式的应用了,下图就是简单工厂模式的结构图:
简单工厂模式的优点在于工厂类包含了必要的判断逻辑,根据传入的参数动态实例化相关的类,对于客户端来说,去除了与具体产品的依赖。但是这里还是会有个问题,假设上面例子中新增了一个zigbee发送器,那么一定是需要修改简单工厂类的,也就是说,我们不但对扩展开放了,对修改也开放了,这是不好的。解决的方法是使用工厂方法模式,工厂方法模式是指定义一个用于创建对象的接口,让子类决定实例化哪一个类。下面还是通过代码来说明:
在简单工厂模式的基础上,让我们对工厂类也升级一下,首先定义一个工厂类接口:
1 | public interface SenderFactory { |
然后为每一个发送器的实现类各创建一个具体的工厂方法去实现这个接口
定义WiFiSender的工厂类:
1 | public class WiFiSenderFactory implements SenderFactory { |
定义BluetoothSender的工厂类:
1 | public class BluetoothSenderFactory implements SenderFactory { |
这样,即使有新的Sender实现类加进来,我们只需要新增相应的工厂类就行了,不需要修改原有的工厂,下图就是工厂方法模式的结构图:
客户端调用代码:
1 | private String mode; //Wi-Fi|Bluetooth |
细心的读者可能已经发现了,工厂方法模式实现时,客户端需要决定实例化哪一个工厂类,相比于简单工厂模式,客户端多了一个选择判断的问题,也就是说,工厂方法模式把简单工厂模式的内部逻辑判断移到了客户端!你想要加功能,本来是修改简单工厂类的,现在改为修改客户端。但是这样带来的好处是整个工厂和产品体系都没有“修改”的变化,只有“扩展”的变化,完全符合了开放封闭原则。
简单工厂模式和工厂方法模式都封装了对象的创建,它们使得高层策略模块在创建类的实例时无需依赖于这些类的具体实现。但是两种工厂模式之间又有差异:
Fragment
表示 Activity 中的行为或用户界面部分。您可以将多个 Fragment 组合在一个 Activity 中来构建多窗格 UI,以及在多个 Activity 中重复使用某个 Fragment。您可以将 Fragment 视为 Activity 的模块化组成部分,它具有自己的生命周期,能接收自己的输入事件,并且您可以在 Activity 运行时添加或移除 Fragment。
Fragment 必须始终嵌入在 Activity 中,其生命周期直接受宿主 Activity 生命周期的影响。 例如,当 Activity 暂停时,其中的所有 Fragment 也会暂停;当 Activity 被销毁时,所有 Fragment 也会被销毁。 不过,当 Activity 正在运行(处于已恢复生命周期状态)时,您可以独立操纵每个 Fragment,如添加或移除它们。 当您执行此类 Fragment 事务时,您也可以将其添加到由 Activity 管理的返回栈 — Activity 中的每个返回栈条目都是一条已发生 Fragment 事务的记录。 返回栈让用户可以通过按返回按钮撤消 Fragment 事务(后退)。
当您将 Fragment 作为 Activity 布局的一部分添加时,它存在于 Activity 视图层次结构的某个 ViewGroup 内部,并且 Fragment 会定义其自己的视图布局。您可以通过在 Activity 的布局文件中声明Fragment,将其作为 <fragment>
元素插入您的 Activity 布局中,或者通过将其添加到某个现有 ViewGroup,利用应用代码进行插入。不过,Fragment 并非必须成为 Activity 布局的一部分;您还可以将没有自己 UI 的 Fragment 用作 Activity 的不可见工作线程。
本文将通过分析源码,对 Fragment 的创建、销毁以及生命周期做一个更深入的认识。
建议读者在看这篇文章的时候,先看下Fragment事务管理源码分析,对Fragment管理类先有一个比较清楚的认识。
1 | /** |
上面的代码就是动态地往containerViewId
里添加一个Fragment并让它显示出来,可以看到,这个涉及到Fragment的事务管理,详细可以参考Fragment事务管理源码分析,这里就不再阐述了。
调用了commit之后,真正执行的地方是在BackStackRecord的run方法:
1 | public void run() { |
因为我们调用的是add操作,所以执行的代码片段是:
1 | case OP_ADD: { |
参数解释:
1 | public void addFragment(Fragment fragment, boolean moveToStateNow) { |
addFragment里面把Fragment加入mActive和mAdded列表,并且设置标记为fragment.mAdded
为true,fragment.mRemoving
为false。
执行完ADD操作后,执行moveToState,moveToState顾名思义,就是把Fragment变为某种状态
1 | //mManager.mCurState的状态很重要,我们下面会分析它现在处于什么状态 |
我们知道Fragment的生命周期是依赖于Activity的,比如Activity处于onResume,那么Fragment也会处于onResume状态,这里的参数mManager.mCurState对应的状态有:
1 | static final int INVALID_STATE = -1; // Invalid state used as a null value. |
mCurState的初始状态是Fragment.INITIALIZING,那么在BackStackRecord中调用moveToState的时候,mCurState是什么值呢?它是会受Activity生命周期影响而变化的,我们来看下FragmentActivity
的代码
1 | "deprecation") ( |
1 | public void dispatchCreate() { |
在onCreate中把mCurState变为Fragment.CREATED
状态了,Activity的其他生命周期方法回调的时候,也会改变这个状态,大致整理如下:
下面是一张状态迁移图:
所以随着Activity生命周期的推进,Activity内所有Fragment的生命周期也会跟着推进。从Activity创建到显示出来,最后会处于onResume状态,那么我们这次就直接分析当前Activity处于onResume调用之后的情形好了。所以假定现在mCurState为Fragment.RESUMED,
让我们继续跟踪FragmentManagerImpl
1 | void moveToState(int newState, int transit, int transitStyle, boolean always) { |
设置最新的mCurState状态,通过上面的分析,我们知道newState等于Fragment.RESUMED。遍历mActive列表中保存的Fragment,改变Fragment状态,这里又调用了一个moveToState
方法,这个方法就是真正回调Fragment生命周期的地方
1 | void moveToState(Fragment f, int newState, int transit, int transitionStyle, |
这段代码逻辑还是比较长,我把注释写在代码里了。可以看到,这个代码写得很巧妙,通过switch case控制,可以一层一层地把Fragment的生命周期推进下去,比如当前fragnemt的state是Fragment.STARTED
,那么它就只会执行performResume,如果Fragment的状态是Fragment.INITIALIZING
,那么就会从switch的最开始依次执行下来,把Fragment的生命周期onAttach–>onResume依次调用。
简要说明下上面的代码:
很多Fragment的生命周期是通过Fragment的performXxx()方法去调用的,比如:
1 | void performCreate(Bundle savedInstanceState) { |
有些童鞋们可能会有疑问,上面只分析到了onAttach->onResume生命周期的回调,那onPause、onDestroy等方法又是什么时候执行的呢?我们再看下刚才的代码
1 | if (f.mState < newState) { |
答案就是在else if分支里面,比如当Acivity锁屏的时候,就Activity生命周期会自动回调onPause,从而触发dispatchPause,在里面调用moveToState(Fragment.STARTED, false);
由于Fragment当前的状态是RESUMED状态,大于newState,所以就会走else if的分支,触发相应的生命周期方法。else if分支的逻辑和state升级的差不多,这里就再进行分析了
最后,放张官网上公布的Fragment生命周期图,通过代码分析,我们发现代码的中生命周期的调用顺序和图中确实是一致的
本文大致地从源码的角度分析了Fragment创建、生命周期回调的过程,如果读者对Fragment的remove
、replace
、hide
、detach
、attach
等操作有兴趣的话,可以自行分析,核心代码主要在BackStackRecord类的run方法以及FragmentManagerImpl的moveToState方法中。
在Fragment使用中,有时候需要对Fragment进行add
、remove
、show
、hide
、replace
等操作来进行Fragment的显示隐藏等管理,这些管理是通过FragmentTransaction
进行事务管理的。事务管理是对于一系列操作进行管理,一个事务包含一个或多个操作命令,是逻辑管理的工作单元。一个事务开始于第一次执行操作语句,结束于Commit。通俗地将,就是把多个操作缓存起来,等调用commit的时候,统一批处理。下面会对Fragmeng的事务管理做一个代码分析
1 | /** |
上面是一个简单的显示Fragment的栗子,简单判断一下Fragment是否已添加过,添加过就直接show,否则构造一个Fragment,最后提交事务。
上图是获取FragmentManager的大体过程
要管理Fragment事务,首先是需要拿到FragmentManager,在Activity中可以通过getFragmentManager()
方法获取(使用兼容包的话,通过FragmentActivity#getSupportFragmentManager()
),在这里我们就不对兼容包进行分析了
1 | final FragmentController mFragments = FragmentController.createController(new HostCallbacks()); |
FragmentManager是一个抽象类,它是通过mFragments.getFragmentManager()来获取的,mFragments是FragmentController对象,它通过FragmentController.createController(new HostCallbacks())
生成,这是一个静态工厂方法:
1 | public static final FragmentController createController(FragmentHostCallback<?> callbacks) { |
在这里面直接new了一个FragmentController对象,注意FragmentController的构造方法需要传入一个FragmentHostCallback
1 | private final FragmentHostCallback<?> mHost; |
构造方法很简单,传入了一个FragmentHostCallback实例
1 | public FragmentManager getFragmentManager() { |
这里又调用了mHost的getFragmentManagerImpl方法,希望童鞋们没有被绕晕,mHost是一个FragmentHostCallback实例,那我们回过头来看看它传进来的地方
这个FragmentHostCallback是一个抽象类,我们可以看到,在Activity中是传入了 Activity#HostCallbacks
内部类,这个就是FragmentHostCallback的实现类
1 | final FragmentManagerImpl mFragmentManager = new FragmentManagerImpl(); |
终于找到FragmentManager的真身FragmentManagerImpl
了
1 |
|
可以看到,所谓的FragmentTransaction其实就是一个BackStackRecord。到现在,FragmentManager和FragmentTransaction我们都找到了。下图就是各个类之间的关系:
下面开始真正的事务管理分析,我们先选择一个事务add来进行分析
1 | public FragmentTransaction add(int containerViewId, Fragment fragment, String tag) { |
add的操作步骤为:
这里用到了一个类:
1 | static final class Op { |
这是一个操作链表节点。所有add、remove、hide等事物最终会形成一个操作链
等所有操作都插入后,最后我们需要调用FragmentTransaction的commit方法,操作才会真正地执行。
1 | public int commit() { |
1 | /** |
这里把操作添加到mPendingActions
列表里去。并通过mHost.getHandler()获取Handler发送执行请求。从上面的分析知道,mHost就是Activity的HostCallbacks,构造方法中把Activity的mHandler传进去了,这里执行的mHost.getHandler()
获取到的也就是Activity中的mHandler,这样做是因为需要在主线程中执行
1 | final Handler mHandler = new Handler(); |
再看看mExecCommit中做了什么操作:
1 | Runnable mExecCommit = new Runnable() { |
插入了事物之后,就是在主线程中把需要处理的事务统一处理,处理事务是通过执行mTmpActions[i].run()
进行的,这个mTmpActions[i]就是前面我们通过enqueueAction方法插入的BackStackRecord,童鞋们可能没注意到,它可是一个Runnable,我们来看看它的定义
1 | final class BackStackRecord extends FragmentTransaction implements |
兜兜转转,我们又回到了BackStackRecord
1 | public void run() { |
到这一步,提交的事务就被真正执行了,我们知道,即使commit了事务之后,也不是同步执行的,是通过Handler发送到主线程执行的。
所有事务的处理都是在run方法里面执行,但是我们留意到,想要搞清楚add、remove等事务背后真正做了什么,还需要深入了解FragmentManagerImpl。
本文主要讲解Fragment事务的流程,FragmentManagerImpl的分析准备放到下一篇分析文章Fragment源码分析中,相信通过分析之后,就可以对Fragment的生命周期也有一个很好的认识了
]]>动态代理是java的一大特性,动态代理的优势就是实现无侵入式的代码扩展。它可以增强我们原有的方法,比如常用的日志监控,添加缓存等,也可以实现方法拦截,通过代理方法修改原方法的参数和返回值等。
要了解动态代理,我们需要先看看什么是静态代理
首先你有一个接口:
1 | public interface Api { |
这个接口有一个原始的实现:
1 | public class ApiImpl implements Api { |
现在问题来了,有一个新的需求,我需要在所有调用doSomething
的地方都添加一个log,那怎么办呢?我们当然可以在原有代码上直接加上log,但是ApiImpl里面的log真的是那段代码需要的吗?如果不修改原有代码,能不能实现?当然可以,如,我们添加一个代理类:
1 | public class ApiProxy implements Api { |
这样,通过ApiProxy我们就是实现静态代理,这里只是简单的添加了log,我们完全可以在ApiProxy的doSomething方法里面,篡改输入参数input以及返回值,从而做一些坏事~
在上面静态代理例子中,我们已经实现了代理的功能,那为何还需要动态代理呢?设想一下以下两种情况
对于第一种情况,如果使用静态代理,那就只能这样了:
1 | public class ApiProxy implements Api { |
而对于第二种情况,就只能新建100个代理类了。这种处理方式肯定不是我们喜欢的,怎么优雅地去解决了?动态代理这时候终于可以上场了。
JDK提供了动态代理方式,可以简单理解为JVM可以在运行时帮我们动态生成一系列的代理类,这样我们就不需要手写每一个静态的代理类了,比如:
实现InvocationHandler
1 | public class ApiHandler implements InvocationHandler { |
动态创建代理类
1 | private static void proxyTest() { |
这样,一个动态代理就完成了,但这里有个需要注意的,动态代理只能代理接口,也就是说interfaces数组里面,只能放接口Class
代理有比原始对象更强大的能力,如果我们自己创建代理对象,然后把原始对象替换为我们的代理对象,那么就可以在这个代理对象为所欲为了;修改参数,替换返回值,我们称之为Hook。
首先我们得找到被Hook的对象,也就是Hook点;什么样的对象比较适合Hook呢?静态变量和单例;在一个进程之内,静态变量和单例变量是不容易发生变化的,所以容易定位,而普通的对象则要么无法标志,要么容易改变,我们根据这个原则找到所谓的Hook点。
一般Hook的步骤有:
Android开发者应该都遇到了64K最大方法数限制的问题,针对这个问题,google也推出了multidex分包机制,在生成apk的时候,把整个应用拆成n个dex包(classes.dex、classes2.dex、classes3.dex),每个dex不超过64k个方法。使用multidex,在5.0以前的系统,应用安装时只安装main dex(包含了应用启动需要的必要class),在应用启动之后,需在Application的attachBaseContext
中调用MultiDex.install(base)
方法,在这时候才加载第二、第三…个dex文件,从而规避了64k问题。
当然,在attachBaseContext
方法中直接install启动second dex会有一些问题,比如install方法是一个同步方法,当在主线程中加载的dex太大的时候,耗时会比较长,可能会触发ANR。不过这是另外一个问题了,解决方法可以参考:Android最大方法数和解决方案 http://blog.csdn.net/shensky711/article/details/52329035。
本文主要分析的是MultiDex.install()
到底做了什么,如何把secondary dexes中的类动态加载进来。
/data/data/<packagename>/files
目录,一般通过openFileOutput方法输出文件到该目录/data/data/<packagename>
目录代码入口很简单,简单粗暴,就调用了一个静态方法MultiDex.install(base);
,传入一个Context对象
1 |
|
下面是主要的代码
1 | public static void install(Context context) { |
这段代码的主要逻辑整理如下:
先来看看MultiDexExtractor.load(context, e, dexDir, false)
1 | /** |
首先判断以下是否需要强制从apk文件中解压,再进行下CRC校验,如果不需要从apk重新解压,就直接从缓存目录中读取已解压的文件返回,否则解压apk中的classes文件到缓存目录,再把相应的文件返回。这个方法再往下的分析就不贴出来了,不复杂,大家可以自己去看看。读取后会把解压信息保存到sharedPreferences中,里面会保存时间戳、CRC校验和dex数量。
得到dex文件列表后,要做的就是把dex文件关联到应用,这样应用findclass的时候才能成功。这个主要是通过installSecondaryDexes
方法来完成的
1 | /** |
可以看到,对于不同的SDK版本,分别采用了不同的处理方法,我们主要分析SDK>=19的情况,其他情况大同小异,读者可以自己去分析。
1 | private static final class V19 { |
在Android中,有两个ClassLoader,分别是DexPathList
和PathClassLoader
,它们的父类都是BaseDexClassLoader
,DexPathList和PathClassLoader的实现都是在BaseDexClassLoader之中,而BaseDexClassLoader的实现又基本是通过调用DexPathList的方法完成的。DexPathList里面封装了加载dex文件为DexFile对象(调用了native方法,有兴趣的童鞋可以继续跟踪下去)的方法。
上述代码中的逻辑如下:
当把dex文件加载到pathList的dexElements数组之后,整个multidex.install基本上就完成了。
但可能还有些童鞋还会有些疑问,仅仅只是把Element数组合并到ClassLoader就可以了吗?还是没有找到加载类的地方啊?那我们再继续看看,当用到一个类的时候,会用ClassLoader去加载一个类,加载类会调用类加载器的findClass方法
1 |
|
于是继续跟踪:
1 | public Class findClass(String name, List<Throwable> suppressed) { |
到现在就清晰了,当加载一个类的时候,会遍历dexElements数组,通过native方法从Element元素中加载类名相应的类
到最后,总结整个multidex.install流程,其实很简单,就做了一件事情,把apk中的secondary dex文件通过ClassLoader转换成Element数组,并把输出的数组合与ClassLoader的Element数组合并。
]]>