普通视图

发现新文章,点击刷新页面。
昨天以前HansChen 的博客

Android 12 - Letterbox 模式

作者 chenhang
2021年10月21日 00:00

1. 简介

随着越来越多大屏和折叠屏设备出现,很多应用并未对不同尺寸的设备进行 UI 适配,这时候应用选择以特定的宽高比显示(虽然 Google 不建议这这样做,官方还是希望开发者可以对不同的屏幕尺寸进行自适应布局~),当应用的宽高比和它的容器比例不兼容的时候,就会以 Letterbox 模式打开。

2021-10-21-18-17-07

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: 壁纸变暗程度

2. 何时触发

Letterbox 的触发条件一般有:

  • android:resizeableActivity=false 且应用声明的宽高比与容器不兼容的时候(如屏幕宽高超出 android:maxAspectRatio)
  • setIgnoreOrientationRequest(true) 系统设置忽略屏幕方向后,以横屏模式下打开一个强制竖屏的界面

3. 实现方案

Letterbox 显示的实现并不复杂,Android 12 在 ActivityRecord 中增加了 LetterboxUiController 用以控制 Letterbox 的布局和显示,先来看看处于 Letterbox 模式时 SurfaceFlinger 状态:

2021-10-22-10-33-26

可以看到,跟正常情况相比,除了界面本身的大小和位置被缩放到指定比例外,四周还多了两个 Layer,挂在 ActiviRecord 节点下面,这两个 Layer 可根据配置进行指定的颜色填充,如果背景是壁纸的话,还可以设置壁纸的 dim 值和模糊程度,这些都可以通过 SurfaceControl 接口轻松实现。

下面简单分析一下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// LetterboxUiController.java
void updateLetterboxSurface(WindowState winHint) {
final WindowState w = mActivityRecord.findMainWindow();
if (w != winHint && winHint != null && w != null) {
return;
}
// 对界面四周需要显示的 Layer 进行位置计算
layoutLetterbox(winHint);
if (mLetterbox != null && mLetterbox.needsApplySurfaceChanges()) {
// 对 Surface 执行创建、参数设置等操作
mLetterbox.applySurfaceChanges(mActivityRecord.getSyncTransaction());
}
}

void layoutLetterbox(WindowState winHint) {
final WindowState w = mActivityRecord.findMainWindow();
if (w == null || winHint != null && w != winHint) {
return;
}
updateRoundedCorners(w);
updateWallpaperForLetterbox(w);
// 是否进入 Letterbox 模式的关键判断
if (shouldShowLetterboxUi(w)) {
if (mLetterbox == null) {
// 把具体逻辑委托给 Letterbox
mLetterbox = new Letterbox(() -> mActivityRecord.makeChildSurface(null),
mActivityRecord.mWmService.mTransactionFactory,
mLetterboxConfiguration::isLetterboxActivityCornersRounded,
this::getLetterboxBackgroundColor,
this::hasWallpaperBackgroudForLetterbox,
this::getLetterboxWallpaperBlurRadius,
this::getLetterboxWallpaperDarkScrimAlpha);
mLetterbox.attachInput(w);
}
mActivityRecord.getPosition(mTmpPoint);
// Get the bounds of the "space-to-fill". The transformed bounds have the highest
// priority because the activity is launched in a rotated environment. In multi-window
// mode, the task-level represents this. In fullscreen-mode, the task container does
// (since the orientation letterbox is also applied to the task).
final Rect transformedBounds = mActivityRecord.getFixedRotationTransformDisplayBounds();
final Rect spaceToFill = transformedBounds != null
? transformedBounds
: mActivityRecord.inMultiWindowMode()
? mActivityRecord.getRootTask().getBounds()
: mActivityRecord.getRootTask().getParent().getBounds();
// 位置计算
mLetterbox.layout(spaceToFill, w.getFrame(), mTmpPoint);
} else if (mLetterbox != null) {
mLetterbox.hide();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// Letterbox.LetterboxSurface.java
public void applySurfaceChanges(SurfaceControl.Transaction t) {
if (!needsApplySurfaceChanges()) {
// Nothing changed.
return;
}
mSurfaceFrameRelative.set(mLayoutFrameRelative);
if (!mSurfaceFrameRelative.isEmpty()) {
if (mSurface == null) {
// 创建挂在 ActivityRecord 节点下的 Surface,设置为 ColorLayer 类型
createSurface(t);
}
// 设置颜色、位置、裁剪
mColor = mColorSupplier.get();
t.setColor(mSurface, getRgbColorArray());
t.setPosition(mSurface, mSurfaceFrameRelative.left, mSurfaceFrameRelative.top);
t.setWindowCrop(mSurface, mSurfaceFrameRelative.width(),
mSurfaceFrameRelative.height());

// 对壁纸背景设置透明度和模糊度
mHasWallpaperBackground = mHasWallpaperBackgroundSupplier.get();
updateAlphaAndBlur(t);

t.show(mSurface);
} else if (mSurface != null) {
t.hide(mSurface);
}
if (mSurface != null && mInputInterceptor != null) {
mInputInterceptor.updateTouchableRegion(mSurfaceFrameRelative);
t.setInputWindowInfo(mSurface, mInputInterceptor.mWindowHandle);
}
}

4. 小结

本文只是简单分析了下 Letterbox 模式的触发条件和显示的大概逻辑,还有很多细节没有涉及,比如详细的触发逻辑判断可以查看 LetterboxUiController#shouldShowLetterboxUi 方法

Android Q 黑暗模式(Dark Mode)源码解析

作者 chenhang
2019年10月22日 00:00

1. 简介

随着 Android Q 发布,「黑暗模式」或者说是「夜间模式」终于在此版本中得到了支持,官方介绍见:https://developer.android.com/guide/topics/ui/look-and-feel/darktheme,再看看效果图:

2019-10-21-17-21-50.png

其实这个功能魅族在两年前就已支持,不得不说 Android 有点落后了,今天我们就来看看原生是怎么实现全局夜间模的吧

2. 打开与关闭

从文档上我们可以可知,打开夜间模式有三个方法:

  • 设置 -> 显示 -> 深色主题背景
  • 下拉通知栏中开启
  • Pixel 手机开启省点模式时会自动激活夜间模式

3. 如何适配

打开后,我们会发现,除原生几个应用生效外,其他应用依然没有变成深色主题,那么应用该如何适配呢?官方提供了下面两种方法:

3.1. 让应用主题继承 DayNight 主题

1
<style name="AppTheme" parent="Theme.AppCompat.DayNight">

或者继承自

1
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight">

继承后,如果当前开启了夜间模式,系统会自动从 night-qualified 中加载资源,所以应用的颜色、图标等资源应尽量避免硬编码,而是推荐使用新增 attributes 指向不同的资源,如

1
2
?android:attr/textColorPrimary
?attr/colorControlNormal

另外,如果应用希望主动切换夜间/日间模式,可以通过 AppCompatDelegate.setDefaultNightMode() 接口主动切换

3.2. 通过 forceDarkAllowed 启用

如果应用不想自己去适配各种颜色,图标等,可以通过在主题中添加 android:forceDarkAllowed="true" 标记,这样系统在夜间模式时,会强制改变应用颜色,自动进行适配(这个功能也是本文主要探讨的)。不过如果你的应用本身使用的就是 DayNightDark Theme,forceDarkAllowed 是不会生效的。

另外,如果你不希望某个 view 被强制夜间模式处理,则可以给 view 添加 android:forceDarkAllowed="false" 或者 view.setForceDarkAllowed(false),设置之后,即使打开了夜间模式且主题添加了 forceDarkAllowed,该 view 也不会变深色。比较重要的一点是,这个接口只能关闭夜间模式,不能开启夜间模式,也就是说,如果主题中没有显示声明 forceDarkAllowed,view.setForceDarkAllowed(true) 是没办法让 view 单独变深色的。如果 view 关闭了夜间模式,那么它的子 view 也会强制关闭夜间模式

总结如下:

  • 主题若添加 forceDarkAllowed=false,无论 view 是否开启 forceDarkAllowed 都不会打开夜间模式
  • 主题若添加 forceDarkAllowed=true,view 可以通过 forceDarkAllowed 关闭夜间模式,一旦关闭,子 view 的夜间模式也会被关闭
  • 如果父 view 或主题设置了 forceDarkAllowed=false,子 view 无法通过 forceDarkAllowed=true 单独打开夜间模式为
  • 若使用的是 DayNightDark Theme 主题,则所有 forceDarkAllowed 都不生效

4. 实现原理

通过继承主题适配夜间模式的原理本质是根据 ui mode 加载 night-qualified 下是资源,这个并非 Android Q 新增的东西,我们这里不再描述。现在主要来看看 forceDarkAllowed 是如何让系统变深色的。

既然一切的源头都是 android:forceDarkAllowed 这个属性,那我们就从它入手吧,首先我们要知道,上面我们说的 android:forceDarkAllowed 其实是分为两个用处,它们分别的定义如下:

frameworks/base/core/res/res/values/attrs.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<declare-styleable name="View">
<!-- <p>Whether or not the force dark feature is allowed to be applied to this View.
<p>Setting this to false will disable the auto-dark feature on this View draws
including any descendants.
<p>Setting this to true will allow this view to be automatically made dark, however
a value of 'true' will not override any 'false' value in its parent chain nor will
it prevent any 'false' in any of its children. -->
<attr name="forceDarkAllowed" format="boolean" />
</declare-styleable>

<declare-styleable name="Theme">
<!-- <p>Whether or not the force dark feature is allowed to be applied to this theme.
<p>Setting this to false will disable the auto-dark feature on everything this
theme is applied to along with anything drawn by any children of views using
this theme.
<p>Setting this to true will allow this view to be automatically made dark, however
a value of 'true' will not override any 'false' value in its parent chain nor will
it prevent any 'false' in any of its children. -->
<attr name="forceDarkAllowed" format="boolean" />
</declare-styleable>

一个是 View 级别的,一个是 Theme 级别的。

4.1. Theme 级别 forceDarkAllowed

从上面的总结来看,Theme 级别的开关优先级是最高的,控制粒度也最大,我们看看源码里面使用它的地方

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// frameworks/base/core/java/android/view/ViewRootImpl.java
private void updateForceDarkMode() {
// 渲染线程为空,直接返回
if (mAttachInfo.mThreadedRenderer == null) return;

// 系统是否打开了黑暗模式
boolean useAutoDark = getNightMode() == Configuration.UI_MODE_NIGHT_YES;

if (useAutoDark) {
// forceDarkAllowed 默认值,开发者模式是否打开了强制 smart dark 选项
boolean forceDarkAllowedDefault =
SystemProperties.getBoolean(ThreadedRenderer.DEBUG_FORCE_DARK, false);
TypedArray a = mContext.obtainStyledAttributes(R.styleable.Theme);
// useAutoDark = 使用浅色主题 && 主题中声明的 forceDarkAllowed 值
useAutoDark = a.getBoolean(R.styleable.Theme_isLightTheme, true)
&& a.getBoolean(R.styleable.Theme_forceDarkAllowed, forceDarkAllowedDefault);
a.recycle();
}

// 关键代码,设置是否强制夜间模式
if (mAttachInfo.mThreadedRenderer.setForceDark(useAutoDark)) {
// TODO: Don't require regenerating all display lists to apply this setting
invalidateWorld(mView);
}
}

// frameworks/base/graphics/java/android/graphics/HardwareRenderer.java
public boolean setForceDark(boolean enable) {
if (mForceDark != enable) {
mForceDark = enable;
// native 代码,mNativeProxy 其实是 RenderThread 代理类的指针
nSetForceDark(mNativeProxy, enable);
return true;
}
return false;
}

这段代码还是比较简单,判断系统:

  • 是否打开了夜间模式
  • 是否使用浅色主题
  • Theme_forceDarkAllowed 是否为 true

三者同时为 true 时才会设置夜间模式,而 updateForceDarkMode 调用的时机分别是在 ViewRootImpl#setViewViewRootImpl#updateConfiguration,也就是初始化和夜间模式切换的时候都会调用,确保夜间模式能及时启用和关闭。继续跟踪 HardwareRenderer#setForceDark 发现,这是一个 native 方法,所以接下来让我们进入 native 世界,nSetForceDark 对应的实现位于

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// frameworks/base/core/jni/android_view_ThreadedRenderer.cpp
static void android_view_ThreadedRenderer_setForceDark(JNIEnv* env, jobject clazz,
jlong proxyPtr, jboolean enable) {
RenderProxy* proxy = reinterpret_cast<RenderProxy*>(proxyPtr);
proxy->setForceDark(enable);
}

// frameworks/base/libs/hwui/renderthread/RenderProxy.cpp
void RenderProxy::setForceDark(bool enable) {
mRenderThread.queue().post([this, enable]() { mContext->setForceDark(enable); });
}

// frameworks/base/libs/hwui/renderthread/CanvasContext.h
class CanvasContext : public IFrameCallback {
public:

...

void setForceDark(bool enable) { mUseForceDark = enable; }

bool useForceDark() {
return mUseForceDark;
}

...

private:

...
// 默认关闭强制夜间模式
bool mUseForceDark = false;

...
};

最终就是设置了一个 CanvasContext 的变量值而已,什么都还没有做,那么这个变量值的作用是什么,什么时候生效呢?我们进一步查看使用的地方:

1
2
3
4
5
6
7
8
9
10
11
// frameworks/base/libs/hwui/TreeInfo.cpp
TreeInfo::TreeInfo(TraversalMode mode, renderthread::CanvasContext& canvasContext)
: mode(mode)
, prepareTextures(mode == MODE_FULL)
, canvasContext(canvasContext)
, damageGenerationId(canvasContext.getFrameNumber())
// 初始化 TreeInfo 的 disableForceDark 变量,注意变量值意义的变化,0 代表打开夜间模式,>0 代表关闭夜间模式
, disableForceDark(canvasContext.useForceDark() ? 0 : 1)
, screenSize(canvasContext.getNextFrameSize()) {}

}

进一步看看 disableForceDark 使用的地方

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// frameworks/base/libs/hwui/RenderNode.cpp
/**
* 这个可以说是核心方法了,handleForceDark 方法调用栈如下:
* - RenderNode#prepareTreeImpl
* - RenderNode#pushStagingDisplayListChanges
* - RenderNode#syncDisplayList
* - RenderNode#handleForceDark
*
* 而 RenderNode#prepareTree 是绘制的必经之路,每一个节点都会走一遍这个流程
*/
void RenderNode::handleForceDark(android::uirenderer::TreeInfo *info) {
// 若没打开强制夜间模式,直接退出
if (CC_LIKELY(!info || info->disableForceDark)) {
return;
}

// 根据是否有文字、是否有子节点、子节点数量等情况,得出当前 Node 属于 Foreground 还是 Background
auto usage = usageHint();
const auto& children = mDisplayList->mChildNodes;
if (mDisplayList->hasText()) {
usage = UsageHint::Foreground;
}
if (usage == UsageHint::Unknown) {
if (children.size() > 1) {
usage = UsageHint::Background;
} else if (children.size() == 1 &&
children.front().getRenderNode()->usageHint() !=
UsageHint::Background) {
usage = UsageHint::Background;
}
}
if (children.size() > 1) {
// Crude overlap check
SkRect drawn = SkRect::MakeEmpty();
for (auto iter = children.rbegin(); iter != children.rend(); ++iter) {
const auto& child = iter->getRenderNode();
// We use stagingProperties here because we haven't yet sync'd the children
SkRect bounds = SkRect::MakeXYWH(child->stagingProperties().getX(), child->stagingProperties().getY(),
child->stagingProperties().getWidth(), child->stagingProperties().getHeight());
if (bounds.contains(drawn)) {
// This contains everything drawn after it, so make it a background
child->setUsageHint(UsageHint::Background);
}
drawn.join(bounds);
}
}

// 根据 UsageHint 设置变色策略:Dark(压暗)、Light(提亮)
mDisplayList->mDisplayList.applyColorTransform(
usage == UsageHint::Background ? ColorTransform::Dark : ColorTransform::Light);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// frameworks/base/libs/hwui/RecordingCanvas.cpp
void DisplayListData::applyColorTransform(ColorTransform transform) {
// transform: Dark 或 Light
// color_transform_fns 是一个对应所有绘制指令的函数指针数组,主要是对 op 的 paint 变色或对 bitmap 添加 colorfilter
this->map(color_transform_fns, transform);
}

template <typename Fn, typename... Args>
inline void DisplayListData::map(const Fn fns[], Args... args) const {
auto end = fBytes.get() + fUsed;
// 遍历当前的绘制的 op
for (const uint8_t* ptr = fBytes.get(); ptr < end;) {
auto op = (const Op*)ptr;
auto type = op->type;
auto skip = op->skip;
// 根据 type 找到对应的 fn,根据调用关系,我们知道 fns 数组对应 color_transform_fns,这个数组其实是一个函数指针数组,下面看看定义
if (auto fn = fns[type]) { // We replace no-op functions with nullptrs
// 执行
fn(op, args...); // to avoid the overhead of a pointless call.
}
ptr += skip;
}
}

#define X(T) colorTransformForOp<T>(),
static const color_transform_fn color_transform_fns[] = {
X(Flush)
X(Save)
X(Restore)
X(SaveLayer)
X(SaveBehind)
X(Concat)
X(SetMatrix)
X(Translate)
X(ClipPath)
X(ClipRect)
X(ClipRRect)
X(ClipRegion)
X(DrawPaint)
X(DrawBehind)
X(DrawPath)
X(DrawRect)
X(DrawRegion)
X(DrawOval)
X(DrawArc)
X(DrawRRect)
X(DrawDRRect)
X(DrawAnnotation)
X(DrawDrawable)
X(DrawPicture)
X(DrawImage)
X(DrawImageNine)
X(DrawImageRect)
X(DrawImageLattice)
X(DrawTextBlob)
X(DrawPatch)
X(DrawPoints)
X(DrawVertices)
X(DrawAtlas)
X(DrawShadowRec)
X(DrawVectorDrawable)
};
#undef X

color_transform_fn 宏定义展开

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
template <class T>
constexpr color_transform_fn colorTransformForOp() {
if
// op 变量中是否同时包含 paint 及 palette 属性,若同时包含,则是绘制 Image 或者 VectorDrawable 的指令
// 参考:frameworks/base/libs/hwui/RecordingCanvas.cpp 中各 Op 的定义
constexpr(has_paint<T> && has_palette<T>) {

return [](const void* opRaw, ColorTransform transform) {
const T* op = reinterpret_cast<const T*>(opRaw);
// 关键变色方法,根据 palette 叠加 colorfilter
transformPaint(transform, const_cast<SkPaint*>(&(op->paint)), op->palette);
};
}
else if
// op 变量中是否包含 paint 属性,普通绘制指令
constexpr(has_paint<T>) {
return [](const void* opRaw, ColorTransform transform) {
const T* op = reinterpret_cast<const T*>(opRaw);
// 关键变色方法,对 paint 颜色进行变换
transformPaint(transform, const_cast<SkPaint*>(&(op->paint)));
};
}
else {
// op 变量不包含 paint 属性,返回空
return nullptr;
}
}

static const color_transform_fn color_transform_fns[] = {
// 根据 Flush、Save、DrawImage等不同绘制 op,返回不同的函数指针
colorTransformForOp<Flush>
...
};

让我们再一次看看 map 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template <typename Fn, typename... Args>
inline void DisplayListData::map(const Fn fns[], Args... args) const {
auto end = fBytes.get() + fUsed;
for (const uint8_t* ptr = fBytes.get(); ptr < end;) {
auto op = (const Op*)ptr;
auto type = op->type;
auto skip = op->skip;
if (auto fn = fns[type]) { // We replace no-op functions with nullptrs
// 对 op 的 paint 进行颜色变换或叠加 colorfilter
fn(op, args...); // to avoid the overhead of a pointless call.
}
ptr += skip;
}
}

贴了一大段代码,虽然代码中已经包含了注释,但还是可能比较晕,我们先来整理下:

  • CanvasContext.mUseForceDark 只会影响 TreeInfo.disableForceDark 的初始化
  • TreeInfo.disableForceDark 若大于 0,RenderNode 在执行 handleForceDark 就会直接退出
  • handleForceDark 方法里会根据 UsageHint 类型,对所有 op 中的 paint 颜色进行变换,如果是绘制图片,则叠加一个反转的 colorfilter。变换策略有:Dark、Light

接下来让我们来看 paint 和 colorfilter 的变色实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
bool transformPaint(ColorTransform transform, SkPaint* paint) {
applyColorTransform(transform, *paint);
return true;
}

static void applyColorTransform(ColorTransform transform, SkPaint& paint) {
if (transform == ColorTransform::None) return;

// 对画笔颜色进行颜色变换
SkColor newColor = transformColor(transform, paint.getColor());
paint.setColor(newColor);

if (paint.getShader()) {
SkShader::GradientInfo info;
std::array<SkColor, 10> _colorStorage;
std::array<SkScalar, _colorStorage.size()> _offsetStorage;
info.fColorCount = _colorStorage.size();
info.fColors = _colorStorage.data();
info.fColorOffsets = _offsetStorage.data();
SkShader::GradientType type = paint.getShader()->asAGradient(&info);

if (info.fColorCount <= 10) {
switch (type) {
case SkShader::kLinear_GradientType:
for (int i = 0; i < info.fColorCount; i++) {
// 对 shader 中的颜色进行颜色变换
info.fColors[i] = transformColor(transform, info.fColors[i]);
}
paint.setShader(SkGradientShader::MakeLinear(info.fPoint, info.fColors,
info.fColorOffsets, info.fColorCount,
info.fTileMode, info.fGradientFlags, nullptr));
break;
default:break;
}

}
}

if (paint.getColorFilter()) {
SkBlendMode mode;
SkColor color;
// TODO: LRU this or something to avoid spamming new color mode filters
if (paint.getColorFilter()->asColorMode(&color, &mode)) {
// 对 colorfilter 中的颜色进行颜色变换
color = transformColor(transform, color);
paint.setColorFilter(SkColorFilter::MakeModeFilter(color, mode));
}
}
}

逻辑很简单,就是对颜色进行变换,进一步看看变色逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 提亮颜色
static SkColor makeLight(SkColor color) {
// 转换成 Lab 模式
Lab lab = sRGBToLab(color);
// 对明度进行反转,明度越高,反转后越低
float invertedL = std::min(110 - lab.L, 100.0f);
if (invertedL > lab.L) {
// 反转后的明度高于原明度,则使用反转后的明度
lab.L = invertedL;
return LabToSRGB(lab, SkColorGetA(color));
} else {
return color;
}
}

// 压暗颜色
static SkColor makeDark(SkColor color) {
// 转换成 Lab 模式
Lab lab = sRGBToLab(color);
// 对明度进行反转,明度越高,反转后越低
float invertedL = std::min(110 - lab.L, 100.0f);
if (invertedL < lab.L) {
// 反转后的明度低于原明度,则使用反转后的明度
lab.L = invertedL;
// 使用 rgb 格式返回
return LabToSRGB(lab, SkColorGetA(color));
} else {
// 直接返回原颜色
return color;
}
}

static SkColor transformColor(ColorTransform transform, SkColor color) {
switch (transform) {
case ColorTransform::Light:
return makeLight(color);
case ColorTransform::Dark:
return makeDark(color);
default:
return color;
}
}

到此,对 paint 的变换结束,看来无非就是反转明度。

再来看看对图片的变换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
bool transformPaint(ColorTransform transform, SkPaint* paint, BitmapPalette palette) {
// 根据 palette 和 colorfilter 判断图片是亮还是暗的
palette = filterPalette(paint, palette);
bool shouldInvert = false;
if (palette == BitmapPalette::Light && transform == ColorTransform::Dark) {
// 图片本身是亮的,但是要求变暗,反转
shouldInvert = true;
}
if (palette == BitmapPalette::Dark && transform == ColorTransform::Light) {
// 图片本身是暗的,但是要求变亮,反转
shouldInvert = true;
}
if (shouldInvert) {
SkHighContrastConfig config;
config.fInvertStyle = SkHighContrastConfig::InvertStyle::kInvertLightness;
// 叠加一个亮度反转的 colorfilter
paint->setColorFilter(SkHighContrastFilter::Make(config)->makeComposed(paint->refColorFilter()));
}
return shouldInvert;
}

终于,bitmap 的变换也分析完了,呼~

4.2. View 级别 forceDarkAllowed

但是,还没完呢~还记得我们最开始说的,除了 Theme 级别,还有一个 View 级别的 forceDarkAllowed,通过 View 级别 forceDarkAllowed 可以关掉它及它的子 view 的夜间模式开关。依然从 java 层看下去哈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// rameworks/base/core/java/android/view/View.java
public class View implements Drawable.Callback, KeyEvent.Callback,
AccessibilityEventSource {

public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
final TypedArray a = context.obtainStyledAttributes(
attrs, com.android.internal.R.styleable.View, defStyleAttr, defStyleRes);
final int N = a.getIndexCount();
for (int i = 0; i < N; i++) {
int attr = a.getIndex(i);
switch (attr) {
case R.styleable.View_forceDarkAllowed:
// 注意,这个默认是 true 的
mRenderNode.setForceDarkAllowed(a.getBoolean(attr, true));
break;
}
}
}
}

// frameworks/base/graphics/java/android/graphics/RenderNode.java
public final class RenderNode {
public boolean setForceDarkAllowed(boolean allow) {
// 又是 native 方法
return nSetAllowForceDark(mNativeRenderNode, allow);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// frameworks/base/core/jni/android_view_RenderNode.cpp
static jboolean android_view_RenderNode_setAllowForceDark(jlong renderNodePtr, jboolean allow) {
return SET_AND_DIRTY(setAllowForceDark, allow, RenderNode::GENERIC);
}

// frameworks/base/libs/hwui/RenderProperties.h
class ANDROID_API RenderProperties {
public:
bool setAllowForceDark(bool allow) {
// 设置到 mPrimitiveFields.mAllowForceDark 变量中
return RP_SET(mPrimitiveFields.mAllowForceDark, allow);
}

bool getAllowForceDark() const {
return mPrimitiveFields.mAllowForceDark;
}
}

和 Theme 级别的一样,仅仅只是设置到变量中而已,关键是要看哪里使用这个变量,经过查找,我们发现,它的使用同样在 RenderNode 的 prepareTreeImpl 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
void RenderNode::prepareTreeImpl(TreeObserver& observer, TreeInfo& info, bool functorsNeedLayer) {

...
// 1. 如果 view 关闭了夜间模式,会在这里让 info.disableForceDark 加 1
// 2. info.disableForceDark 正是 handleForceDark 中关键变量,还记得吗?
// 3. nfo.disableForceDark 大于 0 会让此 RenderNode 跳过夜间模式处理
// 4. 如果 info.disableForceDark 本身已经大于 0 了,view.setForceDarkAllowed(true) 也毫无意义
if (!mProperties.getAllowForceDark()) {
info.disableForceDark++;
}

prepareLayer(info, animatorDirtyMask);
if (info.mode == TreeInfo::MODE_FULL) {
// 这里面会调用 handleForceDark 方法处理夜间模式
pushStagingDisplayListChanges(observer, info);
}

if (mDisplayList) {
info.out.hasFunctors |= mDisplayList->hasFunctor();
// 递归调用子 Node 的 prepareTreeImpl 方法
bool isDirty = mDisplayList->prepareListAndChildren(
observer, info, childFunctorsNeedLayer,
[](RenderNode* child, TreeObserver& observer, TreeInfo& info,
bool functorsNeedLayer) {
child->prepareTreeImpl(observer, info, functorsNeedLayer);
});
if (isDirty) {
damageSelf(info);
}
}

...
// 重要,把 info.disableForceDark 恢复回原来的值,不让它影响 Tree 中同级的其他 RenderNode
// 但是本 RenderNode 的子节点还是会受影响的,这就是为什么父 view 关闭了夜间模式,子 view 也会受影响的原因
// 因为还原 info.disableForceDark 操作是在遍历子节点之后执行的
if (!mProperties.getAllowForceDark()) {
info.disableForceDark--;
}
...
}

5. 总结

本文到目前为止,总算把 Android Q 夜间模式实现原理梳理了一遍,总的来说实现不算复杂,说白了就是把 paint 中的颜色转换一下或者叠加一个 colorfilter,虽然中间还有关联知识没有细说,如 RenderThread、DisplayList、RenderNode 等图形相关的概念,限于文章大小,请读者自行了解

另外,由于水平有限,难免文中有错漏之处,若哪里写的不对,请大家及时指出,蟹蟹啦~

如何顺滑地查看 Android Native 代码

作者 chenhang
2019年10月11日 00:00

1. 简介

使用 Android Studio 查看 Android Framework 代码体验非常好,无论是索引还是界面都让人很满意,但是当你跟踪代码,发现进入 native 逻辑时,就会发现 Android Studio 对 native 代码的支持非常不好,不能索引不支持符号搜索不能跳转等,这些让人非常抓狂。那么如何能在 IDE 愉快地查看 native 代码呢?在 Windows 上,Source Insight 的表现也很好,但苦于只有 Windows 平台支持且界面不好,经过一番折腾,还真是找到了方法,下面我们将一步一步打造丝滑的 native 代码阅读环境。

先看一下效果:

2019-10-11-15-02-40.gif

2. CMake

能让 IDE 正确地建立索引,我们需要让 IDE 能正确地知道源文件、头文件、宏定义等各种数据,庆幸的是,我们发现 AOSP 在编译过程中,可以帮我们生成这些数据,详见:http://androidxref.com/9.0.0_r3/xref/build/soong/docs/clion.md

通过文档我们可知,只需要按照以下步骤完成一次编译,即可自动生成各模块对应的 CMake 文件。至于 Cmake 文件是什么,这里就不做赘述了,大家可以自行了解。

  1. 打开以下两个开关,CMakeLists.txt 就会根据编译环境自动生成
1
2
export SOONG_GEN_CMAKEFILES=1
export SOONG_GEN_CMAKEFILES_DEBUG=1
  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
  1. 合并多个模块

生成了 CMake 后,我们发现,CMake 文件是按模块生成的。这样的话,会导致 IDE 只能单独导入一个模块,而我们平时不可能只看一个模块的代码,如果把多个模块都 include 进来呢?
我们可以在 out/development/ide/clion 路径新建一个 CMakeLists.txt 文件,并添加一下内容:

1
2
3
4
5
6
7
# 指定 CMake 最低版本
cmake_minimum_required(VERSION 3.6)
# 指定工程名,随意
project(aosp)
# 把你需要的模块通过 add_subdirectory 添加进来,注意子目录必须也包含 CMakeLists.txt 文件
add_subdirectory(frameworks/native)
#add_subdirectory(frameworks/base/core/jni/libandroid_runtime-arm64-android)

这样,我们就把多个模块合并在一起了,用 IDE 去打开这个总的 CMake 文件即可

3. 导入 IDE

只要生成 CMake 文件后,剩下的事情就好办了,现在能识别 CMake 工程的 IDE 非常多,大家可以根据个人喜好选择,如:

  • CLion
  • Eclipse
  • Visual Studio

这里以 CLion 为例讲一下如何导入

  1. 打开 CLion
  2. 选择「New CMake Project from Sources」
  3. 指定包含 CMakeLists.txt 的目录,如我们在上一个步骤中说的 out/development/ide/clion(这个目录的 CMakeLists.txt 包含了多个模块,还记得吗?)
  4. 选择「Open Existing Project」
  5. Enjoy your journey …

当然,CLion 也有一个缺点,收费!!如何能免费使用就看大家各显神通了

4. 遇到的一些问题

  • 生成的 CMakeLists.txt 里指定路径可能会使用绝对路径,如: set(ANDROID_ROOT /Volumes/AndroidSource/M1882_QOF7_base),这里大家要注意,如果把 CMakeLists.txt 拷贝到别的工程使用,记得修正一下路径
  • Mac 用户留意,如果你的 CMakeLists.txt 是从 linux 平台生成拷贝过来的,生成的 CMakeLists.txt 里指定的 c++ 编译器 set(CMAKE_CXX_COMPILER "${ANDROID_ROOT}/prebuilts/clang/host/linux-x86/clang-3977809/bin/clang++") 这里指定的是 linux-x86 的编译器,记得替换成 darwin-x86,如果对应目录下没有 clang++,那就从 AOSP 源码拷一个吧
  • 如果 CMake 中列出的源文件在工程中找不到,会导致 CLion 停止索引,如果出现不一致的时候,移除 CMake 中源文件的声明即可

如果使用遇到其他问题,欢迎联系告知,谢谢

5. 总结

所谓工欲善其事,必先利其器。通过这种方法建立的索引包含了 AOSP 所有模块,最重要是它还会根据编译环境,把相关 FLAGS 和宏都设置好。

AOSP 编译和烧写

作者 chenhang
2019年9月12日 00:00

1. 简介

很多 Android 开发者都会希望编译 Android 源码并刷进自己的手机里面,但网上教程很多都仅仅是告诉你 lunch、make 等等,但你手里有一台设备时却发现,你编译出的镜像由于驱动关系是不能直接烧进手机的。这里整理了一下步骤,帮助大家可以按照流程编译并烧写镜像。

本篇文章以 Pixel 2 && Android 10 为例

2. 环境准备

这块没啥说,官方教程就够了,参考:https://source.android.com/setup/build/initializing 就行了

3. 源码下载

  1. 根据 https://developers.google.com/android/drivers 选择一个设备对应 Android 版本号和驱动,比如我们选择:Android 10.0.0 (QP1A.190711.020),下载驱动,记住 Build 号
    2019-9-12-16-53-40.png
  1. https://source.android.com/setup/start/build-numbers 查找 QP1A.190711.020 对应的分支:android-10.0.0_r2,记住分支名
    2019-9-12-16-56-41.png

  2. 下载 AOSP 源码
    注意在下载 aosp 前要安装 repo 工具,参考:https://source.android.com/setup/build/downloading

    1
    2
    3
    4
    5
    mkdir Pixel2
    cd Pixel2
    repo init -u https://android.googlesource.com/platform/manifest -b android-10.0.0_r2 --depth=1
    repo sync -j8
    repo start android-10.0.0_r2 --all
  3. 把步骤1中选中的两个驱动下载到 aosp 源码根目录并解压

  4. 分别执行解压后的文件,注意,执行后要同意 License,确保正确解压到 aosp 根目录的 vendor 目录

    1
    2
    ./extract-qcom-walleye.sh
    ./extract-google_devices-walleye.sh

4. 源码编译

  1. 在 aosp 源码根目录执行:source build/envsetup.sh(注意,执行前终端请选bash,不要使用zsh等,在终端键入bash回车即可)
  2. 在 aosp 源码根目录执行:lunch
  3. 选择对应的版本,比如 Pixel2 就选择:aosp_walleye-userdebug
  4. 执行:make -j8

5. 镜像烧写

  1. 编译完后,执行:export ANDROID_PRODUCT_OUT=/home/chenhang/source/Pixel2/out/target/product/walleye
  2. 执行:fastboot flashall -w
  3. 烧写完成后,执行:fastboot reboot

6. Gapps 安装

编译出来的 aosp 默认没有 google 全家桶,可以通过以下方式进行安装

  1. https://opengapps.org/ 根据系统版本、芯片类型选择需要的 Gapps 全家桶,可以选 stock 版本
  2. 下载后把全家桶 push 到手机 sdcard(不用解压)
  3. https://twrp.me/devices/ 搜索你的设备,如: https://twrp.me/google/googlepixel2.html
    2019-9-12-17-3-22.png
  4. 下载 twrp.img 后根据截图中的命令,把 twrp 加载到手机, 选择 install 刷入 twrp.zip (这是一个 recovery 版本),重启后,通过 adb reboot recovery 进入 twrp 的recovery 系统
  5. 在手机上选择 install, 选择步骤2中 push 到手机 sdcard 的全家桶,安装结束后选择擦除 dalvik cache 和 cache,重启即可

设计模式之桥接模式

作者 chenhang
2017年3月1日 00:00

场景问题

发送消息

现在我们要实现这样一个功能:发送消息。从业务上看,消息又分成普通消息、加急消息和特急消息多种,不同的消息类型,业务功能处理是不一样的,比如加急消息是在消息上添加“加急”字样,而特急消息除了添加特急外,还会做一条催促的记录,多久不完成会继续催促。从发送消息的手段上看,又有系统内短消息、手机短消息、邮件等等。现在要实现这样的发送提示消息的功能,该如何实现呢?

不用模式的解决方案

实现简化版本

先实现一个简单点的版本:消息只是实现发送普通消息,发送的方式先实现系统内短消息和邮件。其它的功能,等这个版本完成过后,再继续添加,这样先把问题简单化,实现起来会容易一点。由于发送普通消息会有两种不同的实现方式,为了让外部能统一操作,因此,把消息设计成接口,然后由两个不同的实现类,分别实现系统内短消息方式和邮件发送消息的方式。此时系统结构如下:
2019-9-2-12-30-41.png

先来看看消息的统一接口,示例代码如下:

1
2
3
4
5
6
7
8
9
10
public interface Message {

/**
* 发送消息
*
* @param message 要发送的消息内容
* @param toUser 消息发送的目的人员
*/
void send(String message, String toUser);
}

再来分别看看两种实现方式,这里只是为了示意,并不会真的去发送Email和站内短消息,先看站内短消息的方式,示例代码如下:

1
2
3
4
5
6
7
public class CommonMessageSMS implements Message {

@Override
public void send(String message, String toUser) {
System.out.println("使用站内短消息的方式,发送消息'" + message + "'给" + toUser);
}
}

同样的,实现以Email的方式发送普通消息,示例代码如下:

1
2
3
4
5
6
7
public class CommonMessageEmail implements Message {

@Override
public void send(String message, String toUser) {
System.out.println("使用Email的方式,发送消息'" + message + "'给" + toUser);
}
}

实现发送加急消息

上面的实现,看起来很简单,对不对。接下来,添加发送加急消息的功能,也有两种发送的方式,同样是站内短消息和Email的方式。
加急消息的实现跟普通消息不同,加急消息会自动在消息上添加加急,然后再发送消息;另外加急消息会提供监控的方法,让客户端可以随时通过这个方法来了解对于加急消息处理的进度,比如:相应的人员是否接收到这个信息,相应的工作是否已经开展等等。因此加急消息需要扩展出一个新的接口,除了基本的发送消息的功能,还需要添加监控的功能,这个时候,系统的结构如图所示:
2019-9-2-12-31-39.png

先看看扩展出来的加急消息的接口,示例代码如下:

1
2
3
4
5
6
7
8
9
10
public interface UrgencyMessage extends Message {

/**
* 监控某消息的处理过程
*
* @param messageId 被监控的消息的编号
* @return 包含监控到的数据对象,这里示意一下,所以用了Object
*/
Object watch(String messageId);
}

相应的实现方式还是发送站内短消息和Email两种,同样需要两个实现类来分别实现这两种方式,先看站内短消息的方式,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class UrgencyMessageSMS implements UrgencyMessage {

@Override
public void send(String message, String toUser) {
message = "加急:" + message;
System.out.println("使用站内短消息的方式,发送消息'" + message + "'给" + toUser);
}

@Override
public Object watch(String messageId) {
//获取相应的数据,组织成监控的数据对象,然后返回
return null;
}
}

再看看Emai的方式,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class UrgencyMessageEmail implements UrgencyMessage {

@Override
public void send(String message, String toUser) {
message = "加急:" + message;
System.out.println("使用Email的方式,发送消息'" + message + "'给" + toUser);
}

@Override
public Object watch(String messageId) {
//获取相应的数据,组织成监控的数据对象,然后返回
return null;
}
}

事实上,在实现加急消息发送的功能上,可能会使用前面发送不同消息的功能,也就是让实现加急消息处理的对象继承普通消息的相应实现,这里为了让结构简单一点,清晰一点,所以没有这样做。

有何问题

上面这样实现,好像也能满足基本的功能要求,可是这么实现好不好呢?有没有什么问题呢?
我们继续向下来添加功能实现,为了简洁,就不再去进行代码示意了,通过实现的结构示意图就可以看出实现上的问题。

继续添加特急消息的处理

特急消息不需要查看处理进程,只要没有完成,就直接催促,也就是说,对于特急消息,在普通消息的处理基础上,需要添加催促的功能。而特急消息、还有催促的发送方式,相应的实现方式还是发送站内短消息和Email两种,此时系统的结构如图所示:
2019-9-2-12-32-9.png

仔细观察上面的系统结构示意图,会发现一个很明显的问题,那就是:通过这种继承的方式来扩展消息处理,会非常不方便。
你看,实现加急消息处理的时候,必须实现站内短消息和Email两种处理方式,因为业务处理可能不同;在实现特急消息处理的时候,又必须实现站内短消息和Email这两种处理方式。
这意味着,以后每次扩展一下消息处理,都必须要实现这两种处理方式,是不是很痛苦,这还不算完,如果要添加新的实现方式呢?继续向下看吧。

继续添加发送手机消息的处理方式

如果看到上面的实现,你还感觉问题不是很大的话,继续完成功能,添加发送手机消息的处理方式
仔细观察现在的实现,如果要添加一种新的发送消息的方式,是需要在每一种抽象的具体实现里面,都要添加发送手机消息的处理的。也就是说:发送普通消息、加急消息和特急消息的处理,都可以通过手机来发送。这就意味着,需要添加三个实现。此时系统结构如图所示:
2019-9-2-12-32-31.png

这下能体会到这种实现方式的大问题了吧。

小结一下出现的问题

采用通过继承来扩展的实现方式,有个明显的缺点:扩展消息的种类不太容易,不同种类的消息具有不同的业务,也就是有不同的实现,在这种情况下,每个种类的消息,需要实现所有不同的消息发送方式。
更可怕的是,如果要新加入一种消息的发送方式,那么会要求所有的消息种类,都要加入这种新的发送方式的实现。
要是考虑业务功能上再扩展一下呢?比如:要求实现群发消息,也就是一次可以发送多条消息,这就意味着很多地方都得修改,太恐怖了。
那么究竟该如何实现才能既实现功能,又能灵活的扩展呢?

解决方案

桥接模式来解决

用来解决上述问题的一个合理的解决方案,就是使用桥接模式。那么什么是桥接模式呢?
桥接模式定义:

将抽象部分和实现部分分离,使它们都可以独立地变化

应用桥接模式来解决的思路

仔细分析上面的示例,根据示例的功能要求,示例的变化具有两个维度,一个维度是抽象的消息这边,包括普通消息、加急消息和特急消息,这几个抽象的消息本身就具有一定的关系,加急消息和特急消息会扩展普通消息;另一个维度在具体的消息发送方式上,包括站内短消息、Email和手机短信息,这几个方式是平等的,可被切换的方式。这两个维度一共可以组合出9种不同的可能性来。
现在出现问题的根本原因,就在于消息的抽象和实现是混杂在一起的,这就导致了,一个维度的变化,会引起另一个维度进行相应的变化,从而使得程序扩展起来非常困难。
要想解决这个问题,就必须把这两个维度分开,也就是将抽象部分和实现部分分开,让它们相互独立,这样就可以实现独立的变化,使扩展变得简单。
桥接模式通过引入实现的接口,把实现部分从系统中分离出去;那么,抽象这边如何使用具体的实现呢?肯定是面向实现的接口来编程了,为了让抽象这边能够很方便的与实现结合起来,把顶层的抽象接口改成抽象类,在里面持有一个具体的实现部分的实例。
这样一来,对于需要发送消息的客户端而言,就只需要创建相应的消息对象,然后调用这个消息对象的方法就可以了,这个消息对象会调用持有的真正的消息发送方式来把消息发送出去。也就是说客户端只是想要发送消息而已,并不想关心具体如何发送。

模式结构和说明

桥接模式的结构图:
2019-9-2-12-32-50.png

  • Abstraction:抽象部分的接口。通常在这个对象里面,要维护一个实现部分的对象引用,在抽象对象里面的方法,需要调用实现部分的对象来完成。这个对象里面的方法,通常都是跟具体的业务相关的方法。
  • RefinedAbstraction:扩展抽象部分的接口,通常在这些对象里面,定义跟实际业务相关的方法,这些方法的实现通常会使用Abstraction中定义的方法,也可能需要调用实现部分的对象来完成。
  • Implementor:定义实现部分的接口,这个接口不用和Abstraction里面的方法一致,通常是由Implementor接口提供基本的操作,而Abstraction里面定义的是基于这些基本操作的业务方法,也就是说Abstraction定义了基于这些基本操作的较高层次的操作。
  • ConcreteImplementor:真正实现Implementor接口的对象。

桥接模式示例代码

先看看Implementor接口的定义,示例代码如下:

1
2
3
4
public interface Implementor {

void operationImpl();
}

再看看Abstraction接口的定义,注意一点,虽然说是接口定义,但其实是实现成为抽象类。示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public abstract class Abstraction {

/**
* 持有一个实现部分的对象
*/
protected Implementor impl;

/**
* 构造方法,传入实现部分的对象
*
* @param impl 实现部分的对象
*/
public Abstraction(Implementor impl) {
this.impl = impl;
}

public void operation() {
impl.operationImpl();
}
}

该来看看具体的实现了,示例代码如下:

1
2
3
4
5
6
public class ConcreteImplementorA implements Implementor {

public void operationImpl() {
//真正的实现
}
}

另外一个实现,示例代码如下:

1
2
3
4
5
6
public class ConcreteImplementorB implements Implementor {

public void operationImpl() {
//真正的实现
}
}

最后来看看扩展Abstraction接口的对象实现,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class RefinedAbstraction extends Abstraction {

public RefinedAbstraction(Implementor impl) {
super(impl);
}

/**
* 示例操作,实现一定的功能
*/
public void otherOperation() {

//实现一定的功能,可能会使用具体实现部分的实现方法,
//但是本方法更大的可能是使用Abstraction中定义的方法,
//通过组合使用Abstraction中定义的方法来完成更多的功能
}
}

使用桥接模式重写示例

学习了桥接模式的基础知识过后,该来使用桥接模式重写前面的示例了。通过示例,来看看使用桥接模式来实现同样的功能,是否能解决“既能方便的实现功能,又能有很好的扩展性”的问题。
要使用桥接模式来重新实现前面的示例,首要任务就是要把抽象部分和实现部分分离出来,分析要实现的功能,抽象部分就是各个消息的类型所对应的功能,而实现部分就是各种发送消息的方式。
其次要按照桥接模式的结构,给抽象部分和实现部分分别定义接口,然后分别实现它们就可以了。

从简单功能开始

从相对简单的功能开始,先实现普通消息和加急消息的功能,发送方式先实现站内短消息和Email这两种。使用桥接模式来实现这些功能的程序结构如图所示
2019-9-2-12-33-19.png

还是看看代码实现,会更清楚一些。先看看消息发送器接口,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 消息发送器
*
* @author HansChen
*/
public interface MessageSender {

/**
* 发送消息
*
* @param message 要发送的消息内容
* @param toUser 消息发送的目的人员
*/
void send(String message, String toUser);
}

再看看抽象部分定义的接口,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* 抽象的消息对象
*
* @author HansChen
*/
public class AbstractMessageController {

/**
* 持有一个实现部分的对象
*/
MessageSender impl;

/**
* 构造方法,传入实现部分的对象
*
* @param impl 实现部分的对象
*/
AbstractMessageController(MessageSender impl) {
this.impl = impl;
}

/**
* 发送消息,转调实现部分的方法
*
* @param message 要发送的消息内容
* @param toUser 消息发送的目的人员
*/
protected void sendMessage(String message, String toUser) {
impl.send(message, toUser);
}
}

看看如何具体的实现发送消息,先看站内短消息的实现吧,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 以站内短消息的方式发送消息
*
* @author HansChen
*/
public class MessageSenderSMS implements MessageSender {

@Override
public void send(String message, String toUser) {
System.out.println("使用站内短消息的方式,发送消息'" + message + "'给" + toUser);
}
}

再看看Email方式的实现,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 以Email的方式发送消息
*
* @author HansChen
*/
public class MessageSenderEmail implements MessageSender {

@Override
public void send(String message, String toUser) {
System.out.println("使用Email的方式,发送消息'" + message + "'给" + toUser);
}
}

接下来该看看如何扩展抽象的消息接口了,先看普通消息的实现,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
public class CommonMessageController extends AbstractMessageController {

public CommonMessageController(MessageSender impl) {
super(impl);
}

@Override
public void sendMessage(String message, String toUser) {
//对于普通消息,什么都不干,直接调父类的方法,把消息发送出去就可以了
super.sendMessage(message, toUser);
}
}

再看看加急消息的实现,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class UrgencyMessageController extends AbstractMessageController {

public UrgencyMessageController(MessageSender impl) {
super(impl);
}

@Override
protected void sendMessage(String message, String toUser) {
message = "加急:" + message;
super.sendMessage(message, toUser);
}

/**
* 扩展自己的新功能:监控某消息的处理过程
*
* @param messageId 被监控的消息的编号
* @return 包含监控到的数据对象,这里示意一下,所以用了Object
*/
public Object watch(String messageId) {
//获取相应的数据,组织成监控的数据对象,然后返回
return null;
}
}

添加功能

看了上面的实现,发现使用桥接模式来实现也不是很困难啊,关键得看是否能解决前面提出的问题,那就来添加还未实现的功能看看,添加对特急消息的处理,同时添加一个使用手机发送消息的方式。该怎么实现呢?
很简单,只需要在抽象部分再添加一个特急消息的类,扩展抽象消息就可以把特急消息的处理功能加入到系统中了;对于添加手机发送消息的方式也很简单,在实现部分新增加一个实现类,实现用手机发送消息的方式,也就可以了。
这么简单?好像看起来完全没有了前面所提到的问题。的确如此,采用桥接模式来实现过后,抽象部分和实现部分分离开了,可以相互独立的变化,而不会相互影响。因此在抽象部分添加新的消息处理,对发送消息的实现部分是没有影响的;反过来增加发送消息的方式,对消息处理部分也是没有影响的。

接着看看代码实现,先看看新的特急消息的处理类,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class SpecialUrgencyMessageController extends AbstractMessageController {

public SpecialUrgencyMessageController(MessageSender impl) {
super(impl);
}

@Override
protected void sendMessage(String message, String toUser) {
message = "特急:" + message;
super.sendMessage(message, toUser);
}

public void hurry(String messageId) {
//执行催促的业务,发出催促的信息
}
}

再看看使用手机短消息的方式发送消息的实现,示例代码如下:

1
2
3
4
5
6
7
public class MessageSenderMobile implements MessageSender {

@Override
public void send(String message, String toUser) {
System.out.println("使用手机的方式,发送消息'" + message + "'给" + toUser);
}
}

测试一下功能

看了上面的实现,可能会感觉得到,使用桥接模式来实现前面的示例过后,添加新的消息处理,或者是新的消息发送方式是如此简单,可是这样实现,好用吗?写个客户端来测试和体会一下,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class Client {

public static void main(String[] args) {
//创建具体的实现对象
MessageSender impl = new MessageSenderSMS();

//创建一个普通消息对象
AbstractMessageController controller = new CommonMessageController(impl);
controller.sendMessage("请喝一杯茶", "小李");

//创建一个紧急消息对象
controller = new UrgencyMessageController(impl);
controller.sendMessage("请喝一杯茶", "小李");

//创建一个特急消息对象
controller = new SpecialUrgencyMessageController(impl);
controller.sendMessage("请喝一杯茶", "小李");


//把实现方式切换成手机短消息,然后再实现一遍
impl = new MessageSenderMobile();
controller = new CommonMessageController(impl);
controller.sendMessage("请喝一杯茶", "小李");

controller = new UrgencyMessageController(impl);
controller.sendMessage("请喝一杯茶", "小李");

controller = new SpecialUrgencyMessageController(impl);
controller.sendMessage("请喝一杯茶", "小李");
}
}

运行结果如下:

1
2
3
4
5
6
使用站内短消息的方式,发送消息'请喝一杯茶'给小李
使用站内短消息的方式,发送消息'加急:请喝一杯茶'给小李
使用站内短消息的方式,发送消息'特急:请喝一杯茶'给小李
使用手机的方式,发送消息'请喝一杯茶'给小李
使用手机的方式,发送消息'加急:请喝一杯茶'给小李
使用手机的方式,发送消息'特急:请喝一杯茶'给小李

前面三条是使用的站内短消息,后面三条是使用的手机短消息,正确的实现了预期的功能。看来前面的实现应该是正确的,能够完成功能,且能灵活扩展。

广义桥接-Java中无处不桥接

使用Java编写程序,一个很重要的原则就是“面向接口编程”,说得准确点应该是“面向抽象编程”,由于在Java开发中,更多的使用接口而非抽象类,因此通常就说成“面向接口编程”了。接口把具体的实现和使用接口的客户程序分离开来,从而使得具体的实现和使用接口的客户程序可以分别扩展,而不会相互影响。

桥接模式中的抽象部分持有具体实现部分的接口,最终目的是什么,还不是需要通过调用具体实现部分的接口中的方法,来完成一定的功能,这跟直接使用接口没有什么不同,只是表现形式有点不一样。再说,前面那个使用接口的客户程序也可以持有相应的接口对象,这样从形式上就一样了。

也就是说,从某个角度来讲,桥接模式不过就是对“面向抽象编程”这个设计原则的扩展。正是通过具体实现的接口,把抽象部分和具体的实现分离开来,抽象部分相当于是使用实现部分接口的客户程序,这样抽象部分和实现部分就松散耦合了,从而可以实现相互独立的变化。

这样一来,几乎可以把所有面向抽象编写的程序,都视作是桥接模式的体现,至少算是简化的桥接模式,就算是广义的桥接吧。而Java编程很强调“面向抽象编程”,因此,广义的桥接,在Java中可以说是无处不在。

桥接模式在Android中的应用

如果各位童鞋看到这里仍然对桥接模式还是不太清楚,在这里给大家举个在Android中非常常用的桥接模式栗子:AbsListViewListAdapter之间的桥接模式。童鞋们可以根据这个栗子体会一下桥接模式的好处。

设计模式之代理模式

作者 chenhang
2016年12月27日 00:00

概述

我们执行一个功能的函数时,经常需要在其中写入与功能不是直接相关但很有必要的代码,如日志记录、信息发送、安全和事务支持等,这些枝节性代码虽然是必要的,但它会带来以下麻烦:

  • 枝节性代码游离在功能性代码之外,它下是函数的目的
  • 枝节性代码会造成功能性代码对其它类的依赖,加深类之间的耦合
  • 枝节性代码带来的耦合度会造成功能性代码移植困难,可重用性降低

毫无疑问,枝节性代码和功能性代码需要分开来才能降低耦合程度,我们可以使用代理模式(委托模式)完成这个要求。代理模式的作用是:为其它对象提供一种代理以控制对这个对象的访问。在某些情况下,一 个客户不想直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介作用。

代理模式一般涉及到三个角色:

  • 抽象角色:声明真实对象和代理对象的共同接口
  • 代理角色:代理对象内部包含有真实角色的引用,从而可以操作真实角色,同时代理对象 与真实对象有相同的接口,能在任何时候代替真实对象,代理对象可以在执行真实对 象前后加入特定的逻辑以实现功能的扩展。
  • 真实角色:代理角色所代表的真实对象,是我们最终要引用的对象

常见的代理应用场景有:

  • 远程代理:对一个位于不同的地址空间对象提供一个局域代表对象,如RMI中的stub
  • 虚拟代理:根据需要将一个资源消耗很大或者比较复杂的对象,延迟加载,在真正需要的时候才创建
  • 保护代理:控制对一个对象的访问权限
  • 智能引用:提供比目标对象额外的服务和功能

接下来,我们用代码来说明什么是代理模式

代理模式

UML图

先看看代理模式的结构图:
2019-9-2-12-29-26.png

代码

下面给出一个小栗子说明代理模式,先定义一个抽象角色,也就是一个公共接口,声明一些需要代理的方法,本文定义一个Subject接口,为了简单说明,只是在里面定义一个request方法:

1
2
3
4
public interface Subject {

void request();
}

定义Subject的实现类RealSubject,它是一个真实角色:

1
2
3
4
5
6
7
public class RealSubject implements Subject {

@Override
public void request() {
System.out.print("do real request");
}
}

定义一个代理角色ProxySubject,跟RealSubject一样,它也继承了Subject接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ProxySubject implements Subject {

private RealSubject mSubject;

public ProxySubject() {
mSubject = new RealSubject();
}

@Override
public void request() {
System.out.print("before");
mSubject.request();
System.out.print("after");
}
}

客户端调用代码

1
2
3
4
5
6
7
8
public class Client {

public static void main(String[] args) {

Subject subject = new ProxySubject();
subject.request();
}
}

这样,一个简易的代理模式模型就建立了,客户端在使用过程中,无需关注RealSubject,只需要关注ProxySubject就行了,并且可以在ProxySubject中插入一些非功能信的代码,比如输出Log,统计执行时间等等

远程代理

远程代理,对一个位于不同的地址空间对象提供一个局域代表对象。这样说大家可能比较抽象,不太能理解,但其实童鞋们可能在就接触过了,在Android中,Binder的使用就是典型的远程代理。比如ActivityManager:
2019-9-2-12-29-57.png

在启动Activity的时,会调用ActivityManager的startActivity方法,我们看看Activity是怎么获取的:

1
2
3
4
5
6
7
8
9
10
11
12
static public IActivityManager asInterface(IBinder obj) {
if (obj == null) {
return null;
}
IActivityManager in =
(IActivityManager)obj.queryLocalInterface(descriptor);
if (in != null) {
return in;
}
// 返回代理类
return new ActivityManagerProxy(obj);
}

可以看到,最终是返回了一个ActivityManager的代理类,因为真正的ActivityManager是运行在内核空间的,Android应用无法直接访问得到,那么就可以借助这个ActivityManagerProxy,通过Binder与真正的ActivityManager,也就是ActivityManagerService交互。其中ActivityManagerService和ActivityManagerProxy都实现了同一个接口:IActivityManager。这个就是Android中典型的代理模式的栗子了。至于ActivityManagerService和ActivityManagerProxy是如何通过Binder实现远程调用,这个就是另一个话题Binder的内容了,这里不再做阐述

延迟加载

根据需要将一个资源消耗很大或者比较复杂的对象,延迟加载,在真正需要的时候才创建。假设我们创建RealSubject需要耗费一定的资源,那么,我们可以把创建它延迟到实际调用的时候,优化Client初始化速度,比如,这样修改ProxySubject以达到延迟加载:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ProxySubject implements Subject {

private RealSubject mSubject;

public ProxySubject() {
}

@Override
public void request() {
// 延时加载
if (mSubject == null) {
mSubject = new RealSubject();
}
mSubject.request();
}
}

Client在实例化ProxySubject的时候,不需消耗资源,而是等到真正调用request的时候,才会加载RealSubject,达到延时加载的效果

保护代理

可以在Proxy类中加入进行权限,验证是否具有执行真实代码的权限,只有权限验证通过了才进行真实对象的调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ProxySubject implements Subject {

private RealSubject mSubject;
private User mUser;

public ProxySubject(User user) {
this.mUser = user;
}

@Override
public void request() {
// 验证权限
if (mUser.isLogin()) {
mSubject.request();
}
}
}

额外功能

通过引入代理类,可以方便地在功能性代码前后插入扩展,如Log输出,调用统计等,实现对原代码的无侵入式代码扩展,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ProxySubject implements Subject {

private RealSubject mSubject;

public ProxySubject() {
mSubject = new RealSubject();
}

@Override
public void request() {
System.out.print("Log: before");
mSubject.request();
System.out.print("Log: after");
}
}

静态代理和动态代理

静态代理和动态代理的概念和使用可以参考我另一篇文章:Java动态代理:http://blog.csdn.net/shensky711/article/details/52872249

依赖注入利器 - Dagger ‡

作者 chenhang
2016年12月18日 00:00

概述

在开发过程中,为了实现解耦,我们经常使用依赖注入,常见的依赖注入方式有:

  • 构造方法注入:在构造方法中把依赖作为参数传递进去
  • setter方法注入:添加setter方法,把依赖传递进去
  • 接口注入:把注入方法抽到一个接口中,然后实现该接口,把依赖传递进去

下面用一个小栗子来说明三种方式的用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class PersonService implements DependencyInjecter {

private PersonDao personDao;

// 构造方法注入
public PersonService(PersonDao personDao) {
this.personDao = personDao;
}

// setter方法注入
public void setPersonDao(PersonDao personDao) {
this.personDao = personDao;
}

// 接口注入:实现DependencyInjecter接口
@Override
public void injectPersonDao(PersonDao personDao) {
this.personDao = personDao;
}

... ...
}

我们来看下使用一般的依赖注入方法时,代码会是怎么样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MainActivity extends AppCompatActivity {

private PersonService mService;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

// 创建PersonService的依赖:personDao
PersonDao personDao = new PersonDaoImpl();
// 通过构造方法注入依赖
mService = new PersonService(personDao);
}
}

看起来还好是吧?但现实情况下,依赖情况往往是比较复杂的,比如很可能我们的依赖关系如下图:
2019-9-2-11-38-41.png

PersonDaoImpl依赖类A,类A依赖B,B依赖C和D…在这种情况下,我们就要写出下面这样的代码了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class MainActivity extends AppCompatActivity {

private PersonService mService;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

// 创建依赖D
D d = new D();
// 创建依赖C
C c = new C();
// 创建依赖B
B b = new B(c, d);
// 创建依赖A
A a = new A(b);
// 创建PersonService的依赖:personDao
PersonDao personDao = new PersonDaoImpl(a);
// 通过构造方法注入依赖
mService = new PersonService(personDao);
}
}

MainActivity只是想使用PersonService而已,却不得不关注PersonService的依赖是什么、PersonDaoImpl依赖的依赖是什么,需要把整个依赖关系搞清楚才能使用PersonService。而且还有一个不好的地方,一旦依赖关系变更了,比如A不再依赖B了,那么就得修改所有创建A的地方。那么,有没有更好的方式呢?Dagger就是为此而生的,让我们看看使用Dagger后,MainActivity会变成什么模样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MainActivity extends AppCompatActivity {

@Inject
PersonService mService;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

// Dagger注入,读者现在可先不关注里面做了什么操作
DaggerPersonServiceComponent.create().inject(MainActivity.this);

// 注意,mService已经是非空了,可以正常使用
mService.update(1, "HansChen");
......
}
}

之前创建A、B、C、D、PersonDaoImpl等依赖的代码全不见了,只需要调用一个注入语句就全搞定了。调用了注入语句之后,mService就可以正常使用了,是不是挺方便呢?至于这句注入语句具体干了什么,读者现在可以先不管,后面会有详细说明,这里只是做一个使用演示而已。

我们大概猜想一下,在MainActivity使用PersonService需要做哪些?

  1. 分析生成依赖关系图,如PersonService–>PersonDaoImpl–>A–>B–>C&D
  2. 根据依赖关系图获取相关依赖,比如依次创建D、C、B、A、PersonDaoImpl、PersonService的实例
  3. 把生成的PersonService实例传递给MainActivity的mService成员变量

其实Dagger做的也就是上面这些事情了,接下来就让我们真正开始学习Dagger吧

声明需要注入的对象

首先我们应该用javax.inject.Inject去注解需要被自动注入的对象,@Inject是Java标准的依赖注入(JSR-330)注解。比如下面栗子中,需要注入的对象就是MainActivity的mService。这里有个要注意的地方,被@Inject注解的变量不能用private修饰

1
2
3
4
5
6
7
public class MainActivity extends AppCompatActivity {

// 注意,不能被private修饰
@Inject
PersonService mService;
......
}

如何实例化出依赖?

在执行依赖注入的时候,Dagger会查找@Inject注解的成员变量,并尝试获取该类的实例,Dagger最直接的方式就是直接new出相应的对象了。实例化对象的时候,会调用对象的构造方法,但假如有多个构造方法,具体用哪个构造方法来实例化对象?Dagger肯定是不会帮我们“擅自做主”的,用哪个构造方法来实例化对象应该是由我们做主的,所以我们需要给相应的构造方法添加@Inject注解
当Dagger需要实例化该对象的时候,会调用@Inject注解的构造方法来实例化对象:

1
2
3
4
5
6
7
8
9
10
11
12
public class PersonService implements DependencyInjecter {

private PersonDao personDao;

// 用@Inject注解,相当于告诉Dagger需要实例化PersonService的时候,请调用这个构造方法
@Inject
public PersonService(PersonDao personDao) {
this.personDao = personDao;
}

......
}

聪明的你应该发现了,调用PersonService的构造方法需要传入PersonDao实例,所以要实例化PersonService,必须先要实例化PersonDao,Dagger会帮我们自动分析出这个依赖关系,并把它添加到依赖关系图里面!Dagger会尝试先去实例化一个PersonDao,如果PersonDao又依赖于另外一个对象A,那么就先尝试去实例化A……以此类推,是不是很像递归?当所有依赖都被实例化出来之后,我们的PersonService当然也被构造出来了。

问题又来了,如果PersonDao是一个接口呢?Dagger怎么知道这个接口应该怎么实现?答案是不知道的,那么Dagger怎么实例化出一个接口出来?这个就是Module存在的意义之一了。关于Module的讲解我们会在后面详细说明,我们现在只要知道,Module里面会定义一些方法,这些方法会返回我们的依赖,就像:

1
2
3
4
5
6
7
8
9
10
11
@Module
public class PersonServiceModule {

/**
* 提供PersonDao接口实例
*/
@Provides
PersonDao providePersonDao(A a) {
return new PersonDaoImpl(a);
}
}

Dagger根据需求获取一个实例的时候,并不总是通过new出来的,它会优先查找Module
中是否有返回相应实例的方法,如果有,就调用Module的方法来获取实例。

比如你用@Inject注解了一个成员变量,Dagger会查找Module中是否有用@Provides注解的,返回该类实例的方法,有的话就会调用provide方法来获得实例,然后注入,如果没有的话Dagger就会尝试new出一个实例。就像我们现在这个栗子,PersonService依赖于PersonDao接口,Dagger不能直接为我们new出一个接口,但我们可以提供一个Module,在Module中定义一个返回PersonDao接口实例的方法,这样,Dagger就可以解决实例化PersonDao的问题了。

我们再梳理一下流程,如果我们用@Inject注解了一个成员变量,并调用注入代码之后,Dagger会这样处理:

  1. 查找Module中是否有用@Provides注解的,返回该类实例的方法
  2. 如果有,就调用那个provide方法来获得实例,然后注入
  3. 如果没有,就尝试调用相应的类中被@Inject注解的构造方法new出一个实例,然后注入
  4. 如果没有一个构造方法被@Inject注解,Dagger会因不能满足依赖而出错

所以假如一个变量被@Inject注解,要么在Module中提供provide方法获取实例,要么该类提供一个被@Inject注解的构造方法,否则Dagger会出错

Module的使用

一般而言,Dagger会获取所有依赖的实例,比如当需要一个TestBean的时候,会通过new TestBean()创建实例并注入到类中。但是,以下情况会就不好处理了:

  1. 需要生成的是一个接口,而Dagger不能直接实例化接口
  2. 不能在第三方库的类中添加注解
  3. 可配置的对象必须是配置的

为了解决以上问题,我们需要定义一个被@Module注解的类,在里面定义用@Provides注解的方法。用该方法返回所需的实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Module
public class PersonServiceModule {

@Provides
D provideD() {
return new D();
}

@Provides
C provideC() {
return new C();
}

@Provides
B provideB(C c, D d) {
return new B(c, d);
}

@Provides
A provideA(B b) {
return new A(b);
}

/**
* 提供PersonDao实例
*/
@Provides
PersonDao providePersonDao(A a) {
return new PersonDaoImpl(a);
}
}

就像providePersonDao返回了PersonDao接口实例,Dagger虽然不能直接实例化出PersonDao接口,但却可以调用Module的providePersonDao方法来获得一个实例。providePersonDao方法需要传入A的实例,那么这里也构成了一个依赖关系图。Dagger会先获取A的实例,然后把实例传递给providePersonDao方法。

Component的使用

到目前为止,我们虽然知道了:

  • Dagger怎么获取实例:
    • 从Module的provide方法中获取
    • 通过@Inject注解的构造方法new出新的实例
  • Dagger会推导provide方法和构造方法的参数,形成依赖图,并“满足”我们依赖图的需求,获取依赖的实例

看样子需要注入的依赖可以获取了,但是不是总觉得还有点“零碎”,整个流程还没连贯起来?比如,Module既然是一个类,生成依赖图的时候,怎么知道跟哪个Module挂钩?即使最后生成了需要的实例,注入的“目的地”是哪里?怎么才能把它注入到“目的地”?残缺的这部分功能,正是Component提供的,Component起到了一个桥梁的作用,贯通Module和注入目标。我们来看看最开始那个例子,我们是怎么进行依赖注入的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MainActivity extends AppCompatActivity {

@Inject
PersonService mService;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

PersonServiceComponent component = DaggerPersonServiceComponent.builder()
.personServiceModule(new PersonServiceModule())
.build();
// 注入,所有@Inject注解的成员变量都会同时注入
component.inject(MainActivity.this);

// 通过component获取实例,注意,这里只是演示用法,其实mService在component.inject的时候已经完成了注入
mService = component.getPersonService();
}
}

这个DaggerPersonServiceComponent是什么鬼?DaggerPersonServiceComponent其实是Dagger为我们自动生成的类,它实现了一个Component接口(这个接口是需要我们自己写的),我们来看下它实现的接口长什么样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 指定PersonServiceModule,当需要获取某实例的时候,会查找PersonServiceModule中是否有返回相应类型的方法,有的话就通过该方法获得实例
*
* @author HansChen
*/
@Component(modules = PersonServiceModule.class)
public interface PersonServiceComponent {

/**
* 查找activity中被@Inject注解的成员变量,并尝试获取相应的实例,把实例赋给activity的成员变量
* 注意函数格式:返回值为空、带有一个参数
*/
void inject(MainActivity activity);

/**
* Dagger会尝试从Module中获取PersonService实例,如果Module中不能获取对应实例,则通过PersonService的构造方法new出一个实例
* 注意函数格式:参数为空,返回值非空
*/
PersonService getPersonService();
}

这个接口被Component注解修饰,它里面可以定义3种类型的方法:

  • 返回值为空,有一个参数:查找参数中被@Inject注解的成员变量,并尝试获取相应的实例(通过Module的provide方法或@Inject注解的构造方法new出新的实例),把实例赋给参数的成员变量
  • 返回值非空,参数为空:获取相应实例并返回
  • 返回值是Component,参数是Moduld,通过该方法可以创建SubComponent实例

既然获取实例的时候,有可能用到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的进阶使用

Components之间的关系

在Dagger中,Component之间可以有两种关系:Subcomponents和Component dependencies。他们有什么作用呢?比如在我们应用中,经常会有一些依赖我们在各个界面都使用得到,比如操作数据库、比如网络请求。假设我们有个ServerApi的接口,在页面A、B、C都使用到了,那么我们要在页面A、B、C的Component里面都能获取到ServerApi的实例,但显然,获取ServerApi实例的方法都是一样的,我们不想写重复的代码。于是我们可定义一个ApplicationComponent,在里面返回ServerApi实例,通过Component之间的关系便可以共享ApplicationComponent提供的依赖图。

下面通过Android中的一个小栗子来说明Subcomponents和Component dependencies如何使用

dependencies

先说明下各个模块之间的关系
首先,我们定义一个ApplicationComponent,它定义了一个方法,通过它来获得ServerApi实例。ApplicationComponent还关联了ApplicationModule,这个Module是ServerApi实例的提供者,注意,这个Moduld还可以返回Context实例
2019-9-2-11-41-18.png

1
2
3
4
5
@Component(modules = ApplicationModule.class)
public interface ApplicationComponent {

ServerApi getServerApi();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Module
public class ApplicationModule {

private final Context mAppContext;

ApplicationModule(Context context) {
mAppContext = context.getApplicationContext();
}

@Provides
Context provideAppContext() {
return mAppContext;
}

@Provides
ServerApi provideServerApi(Context context) {
return new ServerApiImpl(context);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class DemoApplication extends Application {

private ApplicationComponent mAppComponent;

@Override
public void onCreate() {
super.onCreate();
mAppComponent = DaggerApplicationComponent.builder().applicationModule(new ApplicationModule(this)).build();
}

public ApplicationComponent getAppComponent() {
return mAppComponent;
}
}

MainActivity使用MVP模式,在MainPresenter里面需要传入一个ServerApi对象
2019-9-2-11-43-13.png

1
2
3
4
5
6
// 注意,这里有个dependencies声明
@Component(dependencies = ApplicationComponent.class, modules = MainPresenterModule.class)
public interface MainPresenterComponent {

MainPresenter getMainPresenter();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Module
public class MainPresenterModule {

private MainView mMainView;

public MainPresenterModule(MainView mainView) {
this.mMainView = mainView;
}

@Provides
MainView provideMainView() {
return mMainView;
}
}
1
2
3
4
5
6
7
8
9
10
11
public class MainPresenter {

private MainView mMainView;
private ServerApi mServerApi;

@Inject
public MainPresenter(MainView mainView, ServerApi serverApi) {
this.mMainView = mainView;
this.mServerApi = serverApi;
}
}

先抛开dependencies,我们分析这个这个依赖树是怎么样的
2019-9-2-11-43-43.png
Component中getMainPresenter的目的很简单,就是返回MainPresenter,而MainPresenter又依赖MainView和ServerApi,MainView还好说,在MainPresenterModule中有provide方法,但是ServerApi呢?就像上面说的那样,如果我们在这个Moduld中也添加相应的provide方法,那真是太麻烦了(当然,这样做完全是可以实现的),所以我们依赖了ApplicationComponent,通过dependencies,在被依赖的Component暴露的对象,在子Component中是可见的。这个是什么意思呢?意思有两个:

  1. 被依赖Component接口暴露的对象,可以添加到依赖者的依赖图中
  2. Component接口没有暴露的对象,依赖者是不可见的

对于第一点应该比较好理解,就像这个栗子,MainPresenterComponent生成MainPresenter需要ServerApi,而ApplicationComponent中有接口暴露了ServerApi,所以MainPresenterComponent可以获得ServerApi
对于第二点,假设MainPresenter还需要传入一个Context对象,我们注意到,ApplicationModule是可以提供Context的,那MainPresenterComponent能不能通过ApplicationComponent获取Context实例?答案是不行的,因为ApplicationComponent没有暴露这个对象。想要获取Context,除非ApplicationComponent中再添加一个getContext的方法。

他们之间的关系可以用下图描述:
2019-9-2-11-44-12.png

Subcomponents

Subcomponents 实现方法一:

  • 先定义子 Component,使 用@Subcomponent 标注(不可同时再使用 @Component)
  • 父 Component 中定义获得子 Component 的方法

让我们对上面的栗子改造改造:
去除MainPresenterComponent的Component注解,改为Subcomponent:

1
2
3
4
5
6
7
@Subcomponent(modules = MainPresenterModule.class)
public interface MainPresenterComponent {

void inject(MainActivity activity);

MainPresenter getMainPresenter();
}

在ApplicationComponent中新增plus方法(名字可随意取),返回值为MainPresenterComponent,参数为MainPresenterModule:

1
2
3
4
5
@Component(modules = ApplicationModule.class)
public interface ApplicationComponent {

MainPresenterComponent plus(MainPresenterModule module);
}

这样,就构建了一个ApplicationComponent的子图:MainPresenterComponent。子图和dependencies的区别就是,子图可以范围父图所有的依赖,也就是说,子图需要的依赖,不再需要在父Component中暴露任何对象,可以直接通过父图的Moduld提供!他们的关系变为了:
2019-9-2-11-44-34.png

这里需要注意的是,以上代码直接在父 Component 返回子 Component 的形式,要求子 Component 依赖的 Module 必须包含一个无参构造函数,用以自动实例化。如果 Module 需要传递参数,则需要使用 @Subcomponent.builder 的方式,实现方法二实现步骤如下:

  • 在子 Component,定义一个接口或抽象类(通常定义为 Builder),使用 @Subcomponent.Builder 标注
    • 编写返回值为 Builder,方法的参数为需要传入参数的 Module
    • 编写返回值为当前子 Component的 无参方法
  • 父 Component 中定义获得子 Component.Builder 的方法

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Module
public class TestModule {
public TestModule(String test) {
}

@Provides
AuthManager provideAuthManager() {
return AuthManager.getInstance();
}
}

@Subcomponent(modules = {TestModule.class})
public interface TestComponent {

AuthManager getAuthManager();

@Subcomponent.Builder
interface Builder {

Builder createBuilder(TestModule module);

TestComponent build();
}
}

@Singleton
@Component(modules = ApplicationModule.class)
public interface ApplicationComponent {
...
TestComponent.Builder testComponentBuilder();
}


// 使用
TestComponent testComponent = mApplicationComponent.testComponentBuilder().createBuilder(new TestModule("test")).build();

Binds注解

在Dagger2中,一般都是使用@provide方法注入接口。在Android 中,一般我们会这样做,创建一个接口 Presenter 命名 为 HomePresenter

1
2
3
public interface HomePresenter {
Observable<List<User>> loadUsers()
}

然后创建一个这个接口的实例:HomePresenterImp

1
2
3
4
5
6
7
8
public class HomePresenterImp implements HomePresenter {
public HomePresenterImp(){
}
@Override
public Observable<List<User>> loadUsers(){
//Return user list observable
}
}

然后在 Module 中,提供实例化的 provide 方法:

1
2
3
4
5
6
7
@Module
public class HomeModule {
@Provides
public HomePresenter providesHomePresenter(){
return new HomePresenterImp();
}
}

但是,如果我们需要添加一个依赖到 presenter 叫 UserService,那就意味着,我们也要在 module 中添加一个 provide 方法提供这个 UserService,然后在 HomePresenterImp 类中加入一个 UserService 参数的构造方法。
有没有觉得这种方法很麻烦呢?我们还可以用 @Binds 注解,如:

1
2
3
4
5
6
7
@Module
public abstract class HomeModule {
// 变为 abstract 方法, 同时 Module 也必须声明为 abstract, 传入的参数必须为返回参数的实现类
// 当需要 HomePresenter 时,dagger 会自动实例化 HomePresenterImp 并返回
@Binds
public abstract HomePresenter bindHomePresenter(HomePresenterImp homePresenterImp);
}

除了方便,使用 @Binds 注解还可以让 dagger2 生成的代码效率更高。但是需要注意的是,由于 Module 变为抽象类,Module 不能再包含非 static 的带 @Provides 注解的方法。而且这时候,依赖此 Module 的 Component 也不需要传入此 Module 实例了(也实例化不了,因为它是抽象的)。相当于此 Module 仅仅作为描述依赖关系的一个类

Scopes

Scopes可是非常的有用,Dagger2可以通过自定义注解限定注解作用域。@Singleton是被Dagger预先定义的作用域注解。

  • 没有指定作用域的@Provides方法将会在每次注入的时候都创建新的对象
  • 一个没有scope的component不可以依赖一个有scope的组件component
  • 子组件和父组件的scope不能相同
  • Module中provide方法的scope需要与Component的scope一致

我们通常的ApplicationComponent都会使用Singleton注解,也就会是说我们如果自定义component必须有自己的scope。读者到这里,可能还不能理解Scopes的作用,我们先来看下默认提供的Singlton到底有什么作用,然后再讨论Scopes的意义:

Singlton

Singletons是java提供的一个scope,我们来看看Singletons能做什么事情。
为@Provides注释的方法或可注入的类添加添加注解@Singlton,构建的这个对象图表将使用唯一的对象实例,比如我们有个ServerApi
方法一:用@Singleton注解类:

1
2
3
4
5
6
7
8
9
10
11
@Singleton
public class ServerApi {

@Inject
public ServerApi() {
}

public boolean login(String username, String password) {
return "HansChen".equals(username) && "123456".equals(password);
}
}

方法二:用@Singleton注解Module的provide方法:

1
2
3
4
5
6
7
8
9
@Module
public class ApplicationModule {

@Singleton
@Provides
ServerApi provideServerApi() {
return new ServerApi();
}
}

然后我们有个Component:

1
2
3
4
5
6
@Singleton
@Component(modules = ApplicationModule.class)
public interface ApplicationComponent {

ServerApi getServerApi();
}

然后执行依赖注入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MainActivity extends AppCompatActivity {

@Inject
ServerApi mService;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

ApplicationComponent component = DaggerApplicationComponent.create();
Log.d("Hans", component.getServerApi().toString());
Log.d("Hans", component.getServerApi().toString());
Log.d("Hans", component.getServerApi().toString());
}
}

使用了以上两种方法的任意一种,我们都会发现,通过component.getServerApi()获得的实例都是同一个实例。不过要注意一点的是,如果类用@Singleton注解了,但Module中又存在一个provide方法是提供该类实例的,但provide方法没有用@Singleton注解,那么Component中获取该实例就不是单例的,因为会优先查找Module的方法。
这个单例是相对于同一个Component而言的,不同的Component获取到的实例将会是不一样的。

自定义Scope

既然一个没有scope的component不可以依赖一个有scope的组件component,那么我们必然需要自定义scope来去注解自己的Component了,定义方法如下:

1
2
3
4
5
@Documented
@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface FragmentScoped {
}

定义出来的FragmentScoped在使用上和Singleton是一样的,那它和Singleton除了是不一样的注解之外,还有什么不一样呢?答案是没有!我们自定义的scope和Singleton并没有任何不一样,不会因为Singleton是java自带的注解就会有什么区别。

那么,这个scope的设定是为了什么呢?

scope的作用

scope除了修饰provide方法可以让我们获得在同一个Component实例范围内的单例之外,主要的作用就是对Component和Moduld的分层管理以及依赖逻辑的可读性。
这里借用一个网络上的图片说明:
2019-9-2-11-44-58.png

ApplicationComponent一般会用singleton注解,相对的,它的Module中provide方法也只能用singleton注解。UserComponent是用UserSCope能直接使用ApplicationModule吗?不能!因为他俩的scope不一致,这就是这个设定带来的好处,防止不同层级的组件混乱。另外,因为有了scope的存在,各种组件的作用和生命周期也变得可读起来了

Lazy注入

有时可能会需要延迟获取一个实例。对任何绑定的 T,可以构建一个 Lazy 来延迟实例化直至第一次调用 Lazy 的 get() 方法。注入之后,第一次get的时会实例化出 T,之后的调用都会获取相同的实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MainActivity extends AppCompatActivity implements MainView {

// 懒加载
@Inject
Lazy<MainPresenter> mPresenter;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

MainPresenterComponent component = DaggerMainPresenterComponent.builder()
.mainPresenterModule(new MainPresenterModule(this))
.applicationComponent(((DemoApplication) getApplication()).getAppComponent())
.build();
component.inject(this);
Log.d("Hans", mPresenter.get().toString()); // 实例化MainPresenter
Log.d("Hans", mPresenter.get().toString()); // 跟上次获取的实例是同一个实例
}
}

Provider注入

跟Lazy注入不一样的是,有时候我们希望每次调用get的时候,获取到的实例都是不一样的,这时候可以用Provider注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MainActivity extends AppCompatActivity implements MainView {

// Provider
@Inject
Provider<MainPresenter> mPresenter;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

MainPresenterComponent component = DaggerMainPresenterComponent.builder()
.mainPresenterModule(new MainPresenterModule(this))
.applicationComponent(((DemoApplication) getApplication()).getAppComponent())
.build();
component.inject(this);
Log.d("Hans", mPresenter.get().toString()); // 实例化MainPresenter
Log.d("Hans", mPresenter.get().toString()); // 获取新的MainPresenter实例
}
}

Qualifiers注入

到目前为止,我们的demo里,Moduld的provide返回的对象都是不一样的,但是下面这种情况就不好处理了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Module
public class ApplicationModule {

......

// 返回ServerApi实例
@Provides
ServerApi provideServerApiA(Context context) {
return new ServerApiImplA(context);
}

// 返回ServerApi实例
@Provides
ServerApi provideServerApiB(Context context) {
return new ServerApiImplB(context);
}
}

provideServerApiA和provideServerApiB返回的都是ServerApi,Dagger是无法判断用哪个provide方法的。这时候就需要添加Qualifiers了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Module
public class ApplicationModule {

......

@Provides
@Named("ServerApiImplA")
ServerApi provideServerApiA(Context context) {
return new ServerApiImplA(context);
}

@Provides
@Named("ServerApiImplB")
ServerApi provideServerApiB(Context context) {
return new ServerApiImplB(context);
}
}

通过这样一个限定,就能区分出2个方法的区别了,当然,在使用过程中,也同样要指明你用哪个name的实例,Dagger会根据你的name来选取对应的provide方法:

1
2
3
4
5
6
7
8
9
10
11
public class MainPresenter {

private MainView mMainView;
private ServerApi mServerApi;

@Inject
public MainPresenter(MainView mainView, @Named("ServerApiImplA") ServerApi serverApi) {
this.mMainView = mainView;
this.mServerApi = serverApi;
}
}

除了用Named注解,你也可以创建你自己的限定注解:

1
2
3
4
5
6
@Qualifier
@Documented
@Retention(RUNTIME)
public @interface YourQualifier {
String value() default "";
}

编译时验证

Dagger 包含了一个注解处理器(annotation processor)来验证模块和注入。这个过程很严格而且会抛出错误,当有非法绑定或绑定不成功时。下面这个例子缺少了 Executor:

1
2
3
4
5
6
@Module
class DripCoffeeModule {
@Provides Heater provideHeater(Executor executor) {
return new CpuHeater(executor);
}
}

当编译时,javac 会拒绝绑定缺少的部分:

1
2
[ERROR] COMPILATION ERROR :
[ERROR] error: java.util.concurrent.Executor cannot be provided without an @Provides-annotated method.

可以通过给方法 Executor 添加@Provides注解来解决这个问题,或者标记这个模块是不完整的。不完整的模块允许缺少依赖关系

1
2
3
4
5
6
@Module(complete = false)
class DripCoffeeModule {
@Provides Heater provideHeater(Executor executor) {
return new CpuHeater(executor);
}
}

小结

第一次接触用Dagger框架写的代码时候,如果不了解各种注解作用的时候,那真会有一脸懵逼的感觉,而且单看文章,其实还是很抽象,建议大家用Dagger写个小demo玩玩,很快就上手了,这里提供几个使用Dagger的栗子,希望可以帮助大家上手Dagger

Robolectric使用教程

作者 chenhang
2016年12月10日 00:00

概述

Android的单元测试可以分为两部分:

  1. Local unit tests:运行于本地JVM
  2. Instrumented test:运行于真机或者模拟器

如果使用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为运行器

为测试用例添加注解,指定测试运行器为RobolectricTestRunner。注意,这里要通过Config指定constants = BuildConfig.class,Robolectric 会通过constants推导出输出路径,如果不进行配置,Robolectric可能不能找到你的manifest、resources和assets资源

1
2
3
4
5
@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class)
public class MainActivityTest {

}

什么是Shadow类

Shadow是Robolectric的立足之本,如其名,作为影子,一定是变幻莫测,时有时无,且依存于本尊。Robolectric定义了大量模拟Android系统类行为的Shadow类,当这些系统类被创建的时候,Robolectric会查找对应的Shadow类并创建一个Shadow类与原始类关联。每当系统类的方法被调用的时候,Robolectric会保证Shadow对应的方法会调用。这些Shadow对象,丰富了本尊的行为,能更方便的对Android相关的对象进行测试。
比如,我们可以借助ShadowActivity验证页面是否正确跳转了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 验证点击事件是否触发了页面跳转,验证目标页面是否预期页面
*
* @throws Exception
*/
@Test
public void testJump() throws Exception {
// 默认会调用Activity的生命周期: onCreate->onStart->onResume
MainActivity activity = Robolectric.setupActivity(MainActivity.class);
// 触发按钮点击
activity.findViewById(R.id.activity_main_jump).performClick();

// 获取对应的Shadow类
ShadowActivity shadowActivity = Shadows.shadowOf(activity);
// 借助Shadow类获取启动下一Activity的Intent
Intent nextIntent = shadowActivity.getNextStartedActivity();
// 校验Intent的正确性
assertEquals(nextIntent.getComponent().getClassName(), SecondActivity.class.getName());
}

@Config配置

可以通过@Config定制Robolectric的运行时的行为。这个注解可以用来注释类和方法,如果类和方法同时使用了@Config,那么方法的设置会覆盖类的设置。你可以创建一个基类,用@Config配置测试参数,这样,其他测试用例就可以共享这个配置了

配置SDK版本

Robolectric会根据manifest文件配置的targetSdkVersion选择运行测试代码的SDK版本,如果你想指定sdk来运行测试用例,可以通过下面的方式配置

1
2
3
4
5
6
7
@Config(sdk = Build.VERSION_CODES.JELLY_BEAN)
public class SandwichTest {

@Config(sdk = Build.VERSION_CODES.KITKAT)
public void getSandwich_shouldReturnHamSandwich() {
}
}

配置Application类

Robolectric会根据manifest文件配置的Application配置去实例化一个Application类,如果你想在测试用例中重新指定,可以通过下面的方式配置

1
2
3
4
5
6
7
@Config(application = CustomApplication.class)
public class SandwichTest {

@Config(application = CustomApplicationOverride.class)
public void getSandwich_shouldReturnHamSandwich() {
}
}

指定Resource路径

Robolectric可以让你配置manifest、resource和assets路径,可以通过下面的方式配置

1
2
3
4
5
6
7
8
9
@Config(manifest = "some/build/path/AndroidManifest.xml",
assetDir = "some/build/path/assetDir",
resourceDir = "some/build/path/resourceDir")
public class SandwichTest {

@Config(manifest = "other/build/path/AndroidManifest.xml")
public void getSandwich_shouldReturnHamSandwich() {
}
}

使用第三方Library Resources

当Robolectric测试的时候,会尝试加载所有应用提供的资源,但如果你需要使用第三方库中提供的资源文件,你可能需要做一些特别的配置。不过如果你使用gradle来构建Android应用,这些配置就不需要做了,因为Gradle Plugin会在build的时候自动合并第三方库的资源,但如果你使用的是Maven,那么你需要配置libraries变量:

1
2
3
4
5
6
7
@RunWith(RobolectricTestRunner.class)
@Config(libraries = {
"build/unpacked-libraries/library1",
"build/unpacked-libraries/library2"
})
public class SandwichTest {
}

使用限定的资源文件

Android会在运行时加载特定的资源文件,如根据设备屏幕加载不同分辨率的图片资源、根据系统语言加载不同的string.xml,在Robolectric测试当中,你也可以进行一个限定,让测试程序加载特定资源.多个限定条件可以用破折号拼接在在一起。

1
2
3
4
5
6
7
8
9
10
11
/**
* 使用qualifiers加载对应的资源文件
*
* @throws Exception
*/
@Config(qualifiers = "zh-rCN")
@Test
public void testString() throws Exception {
final Context context = RuntimeEnvironment.application;
assertThat(context.getString(R.string.app_name), is("单元测试Demo"));
}

Properties文件

如果你嫌通过注解配置上面的东西麻烦,你也可以把以上配置放在一个Properties文件之中,然后通过@Config指定配置文件,比如,首先创建一个配置文件robolectric.properties:

1
2
3
4
5
# 放置Robolectric的配置选项:
sdk=21
manifest=some/build/path/AndroidManifest.xml
assetDir=some/build/path/assetDir
resourceDir=some/build/path/resourceDir

然后把robolectric.properties文件放到src/test/resources目录下,运行的时候,会自动加载里面的配置

系统属性配置

  • robolectric.offline:true代表关闭运行时获取jar包
  • robolectric.dependency.dir:当处于offline模式的时候,指定运行时的依赖目录
  • robolectric.dependency.repo.id:设置运行时获取依赖的Maven仓库ID,默认是sonatype
  • robolectric.dependency.repo.url:设置运行时依赖的Maven仓库地址,默认是https://oss.sonatype.org/content/groups/public/
  • robolectric.logging.enabled:设置是否打开调试开关

以上设置可以通过Gradle进行配置,如:

1
2
3
4
5
6
7
8
9
android {

testOptions {
unitTests.all {
systemProperty 'robolectric.dependency.repo.url', 'https://local-mirror/repo'
systemProperty 'robolectric.dependency.repo.id', 'local'
}
}
}

驱动Activity生命周期

利用ActivityController我们可以让Activity执行相应的生命周期方法,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Test
public void testLifecycle() throws Exception {
// 创建Activity控制器
ActivityController<MainActivity> controller = Robolectric.buildActivity(MainActivity.class);
MainActivity activity = controller.get();
assertNull(activity.getLifecycleState());

// 调用Activity的performCreate方法
controller.create();
assertEquals("onCreate", activity.getLifecycleState());

// 调用Activity的performStart方法
controller.start();
assertEquals("onStart", activity.getLifecycleState());

// 调用Activity的performResume方法
controller.resume();
assertEquals("onResume", activity.getLifecycleState());

// 调用Activity的performPause方法
controller.pause();
assertEquals("onPause", activity.getLifecycleState());

// 调用Activity的performStop方法
controller.stop();
assertEquals("onStop", activity.getLifecycleState());

// 调用Activity的performRestart方法
controller.restart();
// 注意此处应该是onStart,因为performRestart不仅会调用restart,还会调用onStart
assertEquals("onStart", activity.getLifecycleState());

// 调用Activity的performDestroy方法
controller.destroy();
assertEquals("onDestroy", activity.getLifecycleState());
}

通过ActivityController,我们可以模拟各种生命周期的变化。但是要注意,我们虽然可以随意调用Activity的生命周期,但是Activity生命周期切换有自己的检测机制,我们要遵循Activity的生命周期规律。比如,如果当前Activity并非处于stop状态,测试代码去调用了controller.restart方法,此时Activity是不会回调onRestart和onStart的。

除了控制生命周期,还可以在启动Activity的时候传递Intent:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 启动Activity的时候传递Intent
*
* @throws Exception
*/
@Test
public void testStartActivityWithIntent() throws Exception {
Intent intent = new Intent();
intent.putExtra("test", "HelloWorld");
Activity activity = Robolectric.buildActivity(MainActivity.class).withIntent(intent).create().get();
assertEquals("HelloWorld", activity.getIntent().getExtras().getString("test"));
}

onRestoreInstanceState回调中传递Bundle:

1
2
3
4
5
6
7
8
9
10
11
/**
* savedInstanceState会在onRestoreInstanceState回调中传递给Activity
*
* @throws Exception
*/
@Test
public void testSavedInstanceState() throws Exception {
Bundle savedInstanceState = new Bundle();
Robolectric.buildActivity(MainActivity.class).create().restoreInstanceState(savedInstanceState).get();
// verify something
}

在真实环境下,视图是在onCreate之后的某一时刻在attach到Window上的,在此之前,View是处于不可操作状态的,你不能点击它。在Activity的onPostResume方法调用之后,View才会attach到Window之中。但是,在Robolectric之中,我们可以用控制器的visible方法使得View变为可见,变为可见之后,就可以模拟点击事件了

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void testVisible() throws Exception {
ActivityController<MainActivity> controller = Robolectric.buildActivity(MainActivity.class);
MainActivity activity = controller.get();

// 调用Activity的performCreate并且设置视图visible
controller.create().visible();
// 触发点击
activity.findViewById(R.id.activity_main_button1).performClick();

// 验证
assertEquals(shadowOf(activity).getNextStartedActivity().getComponent().getClassName(), SecondActivity.class.getName());
}

追加模块

为了减少依赖包的大小,Robolectric的shadows类成了好几部分:

SDK PackageRobolectric Add-On Package
com.android.support.support-v4org.robolectric:shadows-support-v4
com.android.support.multidexorg.robolectric:shadows-multidex
com.google.android.gms:play-servicesorg.robolectric:shadows-play-services
com.google.android.maps:mapsorg.robolectric:shadows-maps
org.apache.httpcomponents:httpclientorg.robolectric:shadows-httpclient

用户可以根据自身需求添加以下依赖包,如

1
2
3
4
5
6
7
8
9
dependencies {
... ...
testCompile 'org.robolectric:robolectric:3.1.4'
testCompile 'org.robolectric:shadows-support-v4:3.1.4'
testCompile 'org.robolectric:shadows-multidex:3.1.4'
testCompile 'org.robolectric:shadows-play-services:3.1.4'
testCompile 'org.robolectric:shadows-maps:3.1.4'
testCompile 'org.robolectric:shadows-httpclient:3.1.4'
}

自定义Shadow类

  1. Shadow类需要一个public的无参构造方法以方便Robolectric框架可以实例化它,通过@Implements注解与原始类关联在一起
  2. 若原始类有有参构造方法,在Shadow类中定义public void类型的名为__constructor__的方法,且方法参数与原始类的构造方法参数一直
  3. 定义与原始类方法签名一致的方法,在里面重写实现,Shadow方法需用@Implementation进行注解

下面我们来创建RobolectricBean的Shadow类
原始类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class RobolectricBean {

String name;
int color;

public RobolectricBean(String name) {
this.name = name;
}

public String getName() {
return name;
}

public int getColor() {
return color;
}

public void setColor(int color) {
this.color = color;
}
}

Shadow类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/**
* 创建{@link RobolectricBean}的影子类
*
* @author HansChen
*/
@Implements(RobolectricBean.class)
public class ShadowRobolectricBean {

/**
* 通过@RealObject注解可以访问原始对象,但注意,通过@RealObject注解的变量调用方法,依然会调用Shadow类的方法,而不是原始类的方法
* 只能用来访问原始类的field
*/
@RealObject
RobolectricBean realBean;

/**
* 需要一个无参构造方法
*/
public ShadowRobolectricBean() {

}

/**
* 对应原始类的构造方法
*
* @param name 对应原始类构造方法的传入参数
*/
public void __constructor__(String name) {
realBean.name = name;
}

/**
* 原始对象的方法被调用的时候,Robolectric会根据方法签名查找对应的Shadow方法并调用
*/
@Implementation
public String getName() {
return "Hello, I ma shadow of RobolectricBean: " + realBean.name;
}

@Implementation
public int getColor() {
return realBean.color;
}

@Implementation
public void setColor(int color) {
realBean.color = color;
}
}

Shadow类中访问原始类的field

Shadow类中可以定义一个原始类的成员变量,并用@RealObject注解,这样,Shadow类就能访问原始类的field了,但是注意,通过@RealObject注解的变量调用方法,依然会调用Shadow类的方法,而不是原始类的方法,只能用它来访问原始类的field。

1
2
3
4
5
6
7
8
9
@Implements(Point.class)
public class ShadowPoint {
@RealObject private Point realPoint;
...
public void __constructor__(int x, int y) {
realPoint.x = x;
realPoint.y = y;
}
}

如何在测试用例中让Shadow生效

在Config注解中添加shadows参数,指定对应的Shadow生效

1
2
3
4
5
6
@RunWith(RobolectricTestRunner.class)
@Config(shadows = ShadowRobolectricBean.class)
public class RobolectricBeanTest {

... ...
}

注意,自定义的Shadow类不能通过Shadows.shadowOf()获取,需要用ShadowExtractor.extract()来获取,获取之后进行类型转换:

1
ShadowRobolectricBean shadowBean = (ShadowRobolectricBean) ShadowExtractor.extract(bean);

常用测试场景

页面跳转验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 验证点击事件是否触发了页面跳转,验证目标页面是否预期页面
*
* @throws Exception
*/
@Test
public void testJump() throws Exception {
// 默认会调用Activity的生命周期: onCreate->onStart->onResume
MainActivity activity = Robolectric.setupActivity(MainActivity.class);
// 触发按钮点击
activity.findViewById(R.id.activity_main_jump).performClick();

// 获取对应的Shadow类
ShadowActivity shadowActivity = Shadows.shadowOf(activity);
// 借助Shadow类获取启动下一Activity的Intent
Intent nextIntent = shadowActivity.getNextStartedActivity();
// 校验Intent的正确性
assertEquals(nextIntent.getComponent().getClassName(), SecondActivity.class.getName());
}

UI组件状态验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 验证UI组件状态
*
* @throws Exception
*/
@Test
public void testCheckBoxState() throws Exception {
MainActivity activity = Robolectric.setupActivity(MainActivity.class);
CheckBox checkBox = (CheckBox) activity.findViewById(R.id.activity_main_check_box);
// 验证CheckBox初始状态
assertFalse(checkBox.isChecked());

// 点击按钮反转CheckBox状态
activity.findViewById(R.id.activity_main_switch_check_box).performClick();
// 验证状态是否正确
assertTrue(checkBox.isChecked());

// 点击按钮反转CheckBox状态
activity.findViewById(R.id.activity_main_switch_check_box).performClick();
// 验证状态是否正确
assertFalse(checkBox.isChecked());
}

验证Dialog

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 验证Dialog是否正确弹出
*
* @throws Exception
*/
@Test
public void testDialog() throws Exception {
MainActivity activity = Robolectric.setupActivity(MainActivity.class);
AlertDialog dialog = ShadowAlertDialog.getLatestAlertDialog();
// 判断Dialog尚未弹出
assertNull(dialog);

activity.findViewById(R.id.activity_main_show_dialog).performClick();
dialog = ShadowAlertDialog.getLatestAlertDialog();
// 判断Dialog已经弹出
assertNotNull(dialog);
// 获取Shadow类进行验证
ShadowAlertDialog shadowDialog = shadowOf(dialog);
assertEquals("AlertDialog", shadowDialog.getTitle());
assertEquals("Oops, now you see me ~", shadowDialog.getMessage());
}

验证Toast

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 验证Toast是否正确弹出
*
* @throws Exception
*/
@Test
public void testToast() throws Exception {
MainActivity activity = Robolectric.setupActivity(MainActivity.class);
Toast toast = ShadowToast.getLatestToast();
// 判断Toast尚未弹出
assertNull(toast);

activity.findViewById(R.id.activity_main_show_toast).performClick();
toast = ShadowToast.getLatestToast();
// 判断Toast已经弹出
assertNotNull(toast);
// 获取Shadow类进行验证
ShadowToast shadowToast = shadowOf(toast);
assertEquals(Toast.LENGTH_SHORT, shadowToast.getDuration());
assertEquals("oops", ShadowToast.getTextOfLatestToast());
}

验证Fragment

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, application = CustomApplication.class)
public class MyFragmentTest {

private MyFragment myFragment;

@Before
public void setUp() throws Exception {
myFragment = new MyFragment();
// 把Fragment添加到Activity中
FragmentTestUtil.startFragment(myFragment);
}

@Test
public void testFragment() throws Exception {
assertNotNull(myFragment.getView());
}
}

验证BroadcastReceiver

首先看下广播接收器:

1
2
3
4
5
6
public class MyReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
// do something
}
}

广播的测试点可以包含两个方面

  1. 验证应用程序是否注册了该广播
  2. 验证广播接收器的处理逻辑是否正确,关于逻辑是否正确,可以直接人为的触发onReceive()方法,让然后进行验证
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, application = CustomApplication.class)
public class MyReceiverTest {


@Test
public void restRegister() throws Exception {
ShadowApplication shadowApplication = ShadowApplication.getInstance();

String action = "ut.cn.unittestdemo.receiver";
Intent intent = new Intent(action);

// 验证是否注册了相应的Receiver
assertTrue(shadowApplication.hasReceiverForIntent(intent));
}

@Test
public void restReceive() throws Exception {

String action = "ut.cn.unittestdemo.receiver";
Intent intent = new Intent(action);
intent.putExtra("EXTRA_USERNAME", "HansChen");

MyReceiver myReceiver = new MyReceiver();
myReceiver.onReceive(RuntimeEnvironment.application, intent);
// verify something
}
}

验证Service

Service和Activity一样,都有生命周期,Robolectric也提供了Service的生命周期控制器,使用方式和Activity类似,这里就不做详细解释了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, application = CustomApplication.class)
public class TestServiceTest {

private ServiceController<TestService> controller;
private TestService testService;

@Before
public void setUp() throws Exception {
controller = Robolectric.buildService(TestService.class);
testService = controller.get();
}

/**
* 控制Service生命周期进行验证
*
* @throws Exception
*/
@Test
public void testLifecycle() throws Exception {

controller.create();
// verify something

controller.startCommand(0, 0);
// verify something

controller.bind();
// verify something

controller.unbind();
// verify something

controller.destroy();
// verify something
}
}

设计模式之工厂模式(Factory)

作者 chenhang
2016年11月26日 00:00

概述

根据依赖倒置原则,我们知道,我们应优先依赖抽象类而不是具体类。在应用开发过程中,有很多实体类都是非常易变的,依赖它们会带来问题,所以我们更应该依赖于抽象接口,已使我们免受大多数变化的影响。
工厂模式(Factory)允许我们只依赖于抽象接口就能创建出具体对象的实例,所以在开发中,如果具体类是高度易变的,那么该模式就非常有用。

接下来我们就通过代码举例说明什么是工厂模式

简单工厂模式

假设我们现在有个需求:把一段数据用Wi-Fi或者蓝牙发送出去。
需求很简单是吧?刷刷刷就写下了以下实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private String mode; //Wi-Fi|Bluetooth

public void onClick() {
byte[] data = {0x00, 0x01};

if ("Wi-Fi".equals(mode)) {
sendDataByWiFi(data);
} else {
sendDataByBluetooth(data);
}
}

private void sendDataByWiFi(byte[] data) {
// send data via Wi-Fi
}

private void sendDataByBluetooth(byte[] data) {
// send data via Bluetooth
}

但是上面的代码扩展性并不高,违反了开放封闭原则。比如现在又有了个新的需求,需要用zigbee把数据发送出去,就得再新增一个sendDataByZigbee方法了,而且还得修改onClick里面的逻辑。那么比较好的方法是怎么样的呢?

定义一个数据发送器类:

1
2
3
4
5
6
7
8
9
/**
* 数据发送器Sender
*
* @author HansChen
*/
public interface Sender {

void sendData(byte[] data);
}

实现WiFi数据发送:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* Sender的实现类,通过Wi-Fi发送数据
*
* @author HansChen
*/
public class WiFiSender implements Sender {

@Override
public void sendData(byte[] data) {
System.out.println("Send data by Wi-Fi");
}
}

实现蓝牙数据发送:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* Sender的实现类,通过蓝牙发送数据
*
* @author HansChen
*/
public class BluetoothSender implements Sender {

@Override
public void sendData(byte[] data) {
System.out.println("Send data by Bluetooth");
}
}

这样,原来发送数据的地方就改为了:

1
2
3
4
5
6
7
8
9
10
11
12
13
private String mode; //Wi-Fi|Bluetooth

public void onClick() {
byte[] data = {0x00, 0x01};

Sender sender;
if ("Wi-Fi".equals(mode)) {
sender = new WiFiSender();
} else {
sender = new BluetoothSender();
}
sender.sendData(data);
}

有没有觉得代码优雅了一点?但是随着发送器Sender的实现类越来越多,每增加一个实现类,就需要在onClick里面实例化相应的实现类,能不能用一个单独的类来做这个创造实例的过程呢?这就是我们讲到的工厂。我们新增一个工厂类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 简单工厂类
*
* @author HansChen
*/
public class SimpleFactory {

public static Sender createSender(String mode) {
switch (mode) {
case "Wi-Fi":
return new WiFiSender();
case "Bluetooth":
return new BluetoothSender();
default:
throw new IllegalArgumentException("illegal type: " + mode);
}
}
}

这样一来,怎么实例化数据发送器我们也不用管了,最终代码变为:

1
2
3
4
5
6
7
8
private String mode; //Wi-Fi|Bluetooth

public void onClick() {
byte[] data = {0x00, 0x01};

Sender sender = SimpleFactory.createSender(mode);
sender.sendData(data);
}

好了,到这里我们就完成了简单工厂模式的应用了,下图就是简单工厂模式的结构图:
2019-9-2-11-28-47.png

工厂方法模式

简单工厂模式的优点在于工厂类包含了必要的判断逻辑,根据传入的参数动态实例化相关的类,对于客户端来说,去除了与具体产品的依赖。但是这里还是会有个问题,假设上面例子中新增了一个zigbee发送器,那么一定是需要修改简单工厂类的,也就是说,我们不但对扩展开放了,对修改也开放了,这是不好的。解决的方法是使用工厂方法模式,工厂方法模式是指定义一个用于创建对象的接口,让子类决定实例化哪一个类。下面还是通过代码来说明:

在简单工厂模式的基础上,让我们对工厂类也升级一下,首先定义一个工厂类接口:

1
2
3
4
public interface SenderFactory {

Sender createSender();
}

然后为每一个发送器的实现类各创建一个具体的工厂方法去实现这个接口

定义WiFiSender的工厂类:

1
2
3
4
5
6
7
public class WiFiSenderFactory implements SenderFactory {

@Override
public Sender createSender() {
return new WiFiSender();
}
}

定义BluetoothSender的工厂类:

1
2
3
4
5
6
7
public class BluetoothSenderFactory implements SenderFactory {

@Override
public Sender createSender() {
return new BluetoothSender();
}
}

这样,即使有新的Sender实现类加进来,我们只需要新增相应的工厂类就行了,不需要修改原有的工厂,下图就是工厂方法模式的结构图:
2019-9-2-11-28-17.png

客户端调用代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private String mode; //Wi-Fi|Bluetooth

public void onClick() {
byte[] data = {0x00, 0x01};

SenderFactory factory;
if ("Wi-Fi".equals(mode)) {
factory = new WiFiSenderFactory();
} else {
factory = new BluetoothSenderFactory();
}
Sender sender = factory.createSender();
sender.sendData(data);
}

细心的读者可能已经发现了,工厂方法模式实现时,客户端需要决定实例化哪一个工厂类,相比于简单工厂模式,客户端多了一个选择判断的问题,也就是说,工厂方法模式把简单工厂模式的内部逻辑判断移到了客户端!你想要加功能,本来是修改简单工厂类的,现在改为修改客户端。但是这样带来的好处是整个工厂和产品体系都没有“修改”的变化,只有“扩展”的变化,完全符合了开放封闭原则。

总结

简单工厂模式和工厂方法模式都封装了对象的创建,它们使得高层策略模块在创建类的实例时无需依赖于这些类的具体实现。但是两种工厂模式之间又有差异:

  • 简单工厂模式:最大的优点在于工厂类包含了必要的判断逻辑,根据客户端的条件动态地实例化相关的类。但这也是它的缺点,当扩展功能的时候,需要修改工厂方法,违反了开放封闭原则
  • 工厂方法模式:符合开放封闭原则,但这带来的代价是扩展的时候要增加相应的工厂类,增加了开发量,而且需要修改客户端代码

Android分包MultiDex源码分析

作者 chenhang
2016年10月18日 00:00

概述

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中的类动态加载进来。

MultiDex使用到的路径解析

  • ApplicationInfo.sourceDir:apk的安装路径,如/data/app/com.hanschen.multidex-1.apk
  • Context.getFilesDir():返回/data/data/<packagename>/files目录,一般通过openFileOutput方法输出文件到该目录
  • ApplicationInfo.dataDir: 返回/data/data/<packagename>目录

源码分析

代码入口

代码入口很简单,简单粗暴,就调用了一个静态方法MultiDex.install(base);,传入一个Context对象

1
2
3
4
5
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
MultiDex.install(base);
}

MultiDex.install分析

下面是主要的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
public static void install(Context context) {
Log.i("MultiDex", "install");
if (IS_VM_MULTIDEX_CAPABLE) {
//VM版本大于2.1时,IS_VM_MULTIDEX_CAPABLE为true,这时候MultiDex.install什么也不用做,直接返回。因为大于2.1的VM会在安装应用的时候,就把多个dex合并到一块
} else if (VERSION.SDK_INT < 4) {
//Multi dex最小支持的SDK版本为4
throw new RuntimeException("Multi dex installation failed. SDK " + VERSION.SDK_INT + " is unsupported. Min SDK version is " + 4 + ".");
} else {
try {
ApplicationInfo e = getApplicationInfo(context);
if (e == null) {
return;
}

Set var2 = installedApk;
synchronized (installedApk) {
String apkPath = e.sourceDir;
//检测应用是否已经执行过install()了,防止重复install
if (installedApk.contains(apkPath)) {
return;
}

installedApk.add(apkPath);

//获取ClassLoader,后面会用它来加载second dex
DexClassLoader classLoader;
ClassLoader loader;
try {
loader = context.getClassLoader();
} catch (RuntimeException var9) {
return;
}

if (loader == null) {
return;
}

//清空目录:/data/data/<packagename>/files/secondary-dexes/,其实我没搞明白这个的作用,因为从后面的代码来看,这个目录是没有使用到的
try {
clearOldDexDir(context);
} catch (Throwable var8) {
}

File dexDir = new File(e.dataDir, "code_cache/secondary-dexes");
//把dex文件缓存到/data/data/<packagename>/code_cache/secondary-dexes/目录,[后有详细分析]
List files = MultiDexExtractor.load(context, e, dexDir, false);
if (checkValidZipFiles(files)) {
//进行安装,[后有详细分析]
installSecondaryDexes(loader, dexDir, files);
} else {
//文件无效,从apk文件中再次解压secondary dex文件后进行安装
files = MultiDexExtractor.load(context, e, dexDir, true);
if (!checkValidZipFiles(files)) {
throw new RuntimeException("Zip files were not valid.");
}

installSecondaryDexes(loader, dexDir, files);
}
}
} catch (Exception var11) {
throw new RuntimeException("Multi dex installation failed (" + var11.getMessage() + ").");
}
}
}

这段代码的主要逻辑整理如下:

  1. VM版本检测,如果大于2.1就什么都不做(系统在安装应用的时候已经帮我们把dex合并了),如果系统SDK版本小于4就抛出运行时异常
  2. 把apk中的secondary dexes解压到缓存目录,并把这些缓存读取出来。应用第二次启动的时候,会尝试从缓存目录中读取,除非读取出的文件校验失败,否则不再从apk中解压dexes
  3. 根据当前的SDK版本,执行不同的安装方法

先来看看MultiDexExtractor.load(context, e, dexDir, false)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* 解压apk文件中的classes2.dex、classes3.dex等文件解压到dexDir目录中
*
* @param dexDir 解压目录
* @param forceReload 是否需要强制从apk文件中解压,否的话会直接读取旧文件
* @return 解压后的文件列表
* @throws IOException
*/
static List<File> load(Context context,
ApplicationInfo applicationInfo,
File dexDir,
boolean forceReload) throws IOException {
File sourceApk = new File(applicationInfo.sourceDir);
long currentCrc = getZipCrc(sourceApk);
List files;
if (!forceReload && !isModified(context, sourceApk, currentCrc)) {
try {
//从缓存目录中直接查找缓存文件,跳过解压
files = loadExistingExtractions(context, sourceApk, dexDir);
} catch (IOException var9) {
files = performExtractions(sourceApk, dexDir);
putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1);
}
} else {
//把apk中的secondary dex文件解压到缓存目录,并把解压后的文件返回
files = performExtractions(sourceApk, dexDir);
//把解压信息保存到sharedPreferences中
putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1);
}

return files;
}

首先判断以下是否需要强制从apk文件中解压,再进行下CRC校验,如果不需要从apk重新解压,就直接从缓存目录中读取已解压的文件返回,否则解压apk中的classes文件到缓存目录,再把相应的文件返回。这个方法再往下的分析就不贴出来了,不复杂,大家可以自己去看看。读取后会把解压信息保存到sharedPreferences中,里面会保存时间戳、CRC校验和dex数量。

得到dex文件列表后,要做的就是把dex文件关联到应用,这样应用findclass的时候才能成功。这个主要是通过installSecondaryDexes方法来完成的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* 安装dex文件
*
* @param loader 类加载器
* @param dexDir 缓存目录,用以存放opt之后的dex文件
* @param files 需要安装的dex
* @throws IllegalArgumentException
* @throws IllegalAccessException
* @throws NoSuchFieldException
* @throws InvocationTargetException
* @throws NoSuchMethodException
* @throws IOException
*/
private static void installSecondaryDexes(ClassLoader loader,
File dexDir,
List<File> files) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {

if (!files.isEmpty()) {
//对不同版本的SDK做不同处理
if (VERSION.SDK_INT >= 19) {
MultiDex.V19.install(loader, files, dexDir);
} else if (VERSION.SDK_INT >= 14) {
MultiDex.V14.install(loader, files, dexDir);
} else {
MultiDex.V4.install(loader, files);
}
}

}

可以看到,对于不同的SDK版本,分别采用了不同的处理方法,我们主要分析SDK>=19的情况,其他情况大同小异,读者可以自己去分析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
private static final class V19 {
private V19() {
}

/**
* 安装dex文件
*
* @param loader 类加载器
* @param additionalClassPathEntries 需要安装的dex
* @param optimizedDirectory 缓存目录,用以存放opt之后的dex文件
* @throws IllegalArgumentException
* @throws IllegalAccessException
* @throws NoSuchFieldException
* @throws InvocationTargetException
* @throws NoSuchMethodException
*/
private static void install(ClassLoader loader,
List<File> additionalClassPathEntries,
File optimizedDirectory) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException {

//通过反射获取ClassLoader对象中的pathList属性,其实是ClassLoader的父类BaseDexClassLoader中的成员
Field pathListField = MultiDex.findField(loader, "pathList");
//通过属性获取该属性的值,该属性的类型是DexPathList
Object dexPathList = pathListField.get(loader);

ArrayList suppressedExceptions = new ArrayList();
//通过反射调用dexPathList的makeDexElements返回Element对象数组。方法里面会读取每一个输入文件,生成DexFile对象,并将其封装进Element对象
Object[] elements = makeDexElements(dexPathList, new ArrayList(additionalClassPathEntries), optimizedDirectory, suppressedExceptions);

//将elements数组跟dexPathList对象的dexElements数组合并,并把合并后的数组作为dexPathList新的值
MultiDex.expandFieldArray(dexPathList, "dexElements", elements);

//处理异常
if (suppressedExceptions.size() > 0) {
Iterator suppressedExceptionsField = suppressedExceptions.iterator();

while (suppressedExceptionsField.hasNext()) {
IOException dexElementsSuppressedExceptions = (IOException) suppressedExceptionsField.next();
Log.w("MultiDex", "Exception in makeDexElement", dexElementsSuppressedExceptions);
}

Field suppressedExceptionsField1 = MultiDex.findField(loader, "dexElementsSuppressedExceptions");
IOException[] dexElementsSuppressedExceptions1 = (IOException[]) ((IOException[]) suppressedExceptionsField1.get(loader));
if (dexElementsSuppressedExceptions1 == null) {
dexElementsSuppressedExceptions1 = (IOException[]) suppressedExceptions.toArray(new IOException[suppressedExceptions
.size()]);
} else {
IOException[] combined = new IOException[suppressedExceptions.size() + dexElementsSuppressedExceptions1.length];
suppressedExceptions.toArray(combined);
System.arraycopy(dexElementsSuppressedExceptions1, 0, combined, suppressedExceptions.size(), dexElementsSuppressedExceptions1.length);
dexElementsSuppressedExceptions1 = combined;
}

suppressedExceptionsField1.set(loader, dexElementsSuppressedExceptions1);
}

}

private static Object[] makeDexElements(Object dexPathList,
ArrayList<File> files,
File optimizedDirectory,
ArrayList<IOException> suppressedExceptions) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
Method makeDexElements = MultiDex.findMethod(dexPathList, "makeDexElements", new Class[]{ArrayList.class, File.class, ArrayList.class});
return (Object[]) ((Object[]) makeDexElements.invoke(dexPathList, new Object[]{files, optimizedDirectory, suppressedExceptions}));
}
}

在Android中,有两个ClassLoader,分别是DexPathListPathClassLoader,它们的父类都是BaseDexClassLoader,DexPathList和PathClassLoader的实现都是在BaseDexClassLoader之中,而BaseDexClassLoader的实现又基本是通过调用DexPathList的方法完成的。DexPathList里面封装了加载dex文件为DexFile对象(调用了native方法,有兴趣的童鞋可以继续跟踪下去)的方法。
上述代码中的逻辑如下:

  1. 通过反射获取pathList对象
  2. 通过pathList把输入的dex文件输出为elements数组,elements数组中的元素封装了DexFile对象
  3. 把新输出的elements数组合并到原pathList的dexElements数组中
  4. 异常处理

当把dex文件加载到pathList的dexElements数组之后,整个multidex.install基本上就完成了。
但可能还有些童鞋还会有些疑问,仅仅只是把Element数组合并到ClassLoader就可以了吗?还是没有找到加载类的地方啊?那我们再继续看看,当用到一个类的时候,会用ClassLoader去加载一个类,加载类会调用类加载器的findClass方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
//调用pathList的findClass方法
Class c = pathList.findClass(name, suppressedExceptions);
if (c == null) {
ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
for (Throwable t : suppressedExceptions) {
cnfe.addSuppressed(t);
}
throw cnfe;
}
return c;
}

于是继续跟踪:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public Class findClass(String name, List<Throwable> suppressed) {
//遍历dexElements数组
for (Element element : dexElements) {

DexFile dex = element.dexFile;
if (dex != null) {
//继续跟踪会发现调用的是一个native方法
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}

到现在就清晰了,当加载一个类的时候,会遍历dexElements数组,通过native方法从Element元素中加载类名相应的类

总结

到最后,总结整个multidex.install流程,其实很简单,就做了一件事情,把apk中的secondary dex文件通过ClassLoader转换成Element数组,并把输出的数组合与ClassLoader的Element数组合并。

❌
❌