和人类需要群居一样,程序界的进程、线程也需要通信往来。它们的交流则依赖模块之间、文件之间产生的关系。如何快速地搞清和构建这种关系,同时还能减轻彼此的依赖,需要开发者们认真思考。

我们将这种需求称之为依赖注入(DI,Dependency Injection),这个编程技术由来已久,在讲述之前想来简单回顾下依赖和关联的基本概念。

依赖和关联

像下图示意的那样,模块或类之间的关系大体可以分为依赖(Dependency)和关联(Association)两种。依赖一般表现为局部参数,关联则表现为属性的持有。

按照被关联对象的生命周期的不同,又可以将关联分为聚合(Aggregation)和组合(Composition)。耦合度依次增强。

依赖注入

依赖注入编程技术最终要构建的并非特指上面的依赖关系,也包含了关联关系。只是构建的入口多表现为参数注入。

不依赖框架的情况下我们也可以手动注入这些关系。

  • 通过构造函数传参
  • 通过setter函数传参

但面对依赖纵深复杂的大型项目,手动注入这些依赖非常繁琐和易错,互相的依赖关系难以避免得混乱而丑陋。久而久之,耦合度越来越强,难以扩展。这时候自动注入这些依赖关系显得尤为必要。

自动注入可以选择通过反射在运行阶段构建依赖关系的框架,比如Guice。也可以选择在编译阶段即可构建依赖的更优方案。

比如今天的主角Dagger2,像它的名字一样,是实现DI技术的一把利器。

Dagger和Dagger2

Dagger由开源了okHttp的Square公司开发,广为人知。但其部分功能仍然依靠反射来实现,美中不足。

github.com/square/dagg…

于是Google接过了接力棒,在其基础之上进行了改善,推出了Dagger2。它通过APT在编译阶段解析注解生成代码实现依赖注入。

github.com/google/dagg…

Dagger2简单实战

我们通过Dagger2、ViewModel和Retrofit查询电影接口,简单演示下如何使用Dagger2。

DEMO: github.com/ellisonchan…

1. 导入框架

apply plugin: 'kotlin-kapt'
dependencies {
implementation 'com.google.dagger:dagger:2.x'
kapt 'com.google.dagger:dagger-compiler:2.x'
}

2. 创建Dagger组件接口

@Singleton
@Component(modules = [NetworkModule::class])
interface ApplicationGraph {
fun inject(activity: DemoActivity)
} class MyApplication: Application() {
val appComponent = DaggerApplicationComponent.create()
}

3. Activity字段注入ViewModel

class DemoActivity: Activity() {
@Inject
lateinit var movieViewModel: MovieViewModel
override fun onCreate(savedInstanceState: Bundle?) {
(applicationContext as MyApplication).appGraph.inject(this)
super.onCreate(savedInstanceState)
val binding = ActivityDaggerBinding.inflate(layoutInflater)
...
}
}

注意:Activity由系统实例化只能通过字段注入。

4. 声明ViewModel需要注入MovieRepository

class MovieViewModel @Inject constructor(
private val movieRepository: MovieRepository
)

5. 声明MovieRepository和Data的注入

@Singleton
class MovieRepository @Inject constructor(
private val localData: MovieLocalData,
private val remoteData: MovieRemoteData
) class MovieLocalData @Inject constructor() @Singleton
class MovieRepository @Inject constructor(
private val localData: MovieLocalData,
private val remoteData: MovieRemoteData
)

6. 添加Network模块

@Module
class NetworkModule {
...
@Singleton
@Provides
fun provideLoginService(okHttpClient: OkHttpClient, gsonConverterFactory: GsonConverterFactory): MovieService {
return Retrofit.Builder()
.baseUrl("http://omdbapi.com/")
.addConverterFactory(gsonConverterFactory)
.client(okHttpClient)
.build()
.create(MovieService::class.java)
}
}

依赖关系图

Dagger2的功能十分强大,上述实战仍有不少未提及的高阶用法,感兴趣者可进一步尝试。

  • 自定义作用域的@Scope
  • 注释子组件的@Subcomponent
  • 注释抽象方法的@Binds
  • 一个接口指定多个实现的@Named

...

Dagger2导航的支持

Android Studio针对Dagger2的导航进行了支持,方便开发者快速回溯依赖关系。

  • 点击向上的箭头可以查看该实例注入的提供方
  • 点击向下的树形图会将您转到或展开该实例被用作依赖项的位置或列表

Dagger2在SystemUI上应用

对于小型项目而言,引入DI框架显得大材小用、大动干戈。而且对于后期接手人员,如果对于DI框架不熟悉的话,维护将变得尤为困难。似乎只有大型项目才能让它自由地施展拳脚。

前些年我在调查某个导航栏Bug的时候查阅过SystemUI的代码,当时意外地发现大量的模块包括StatusBar、Recents、Keyguard等都是DI方式引入的。虽然对Dagger略有耳闻,但仍看得云里雾里,不得其解。

SystemUI作为Android系统里最核心最复杂的App,称之为大型项目毫不过分。现在就来看看Dagger2如何助力这个大型App管理大量的系统组件。

※ 源码版本:Android 11

SystemUI中主要的依赖实例都管理在Denpency类中。

public class Dependency {
...
@Inject @Background Lazy<Executor> mBackgroundExecutor;
@Inject Lazy<ClockManager> mClockManager;
@Inject Lazy<ActivityManagerWrapper> mActivityManagerWrapper;
@Inject Lazy<DevicePolicyManagerWrapper> mDevicePolicyManagerWrapper;
@Inject Lazy<PackageManagerWrapper> mPackageManagerWrapper;
@Inject Lazy<SensorPrivacyController> mSensorPrivacyController;
@Inject Lazy<DockManager> mDockManager;
@Inject Lazy<INotificationManager> mINotificationManager;
@Inject Lazy<SysUiState> mSysUiStateFlagsContainer;
@Inject Lazy<AlarmManager> mAlarmManager;
@Inject Lazy<KeyguardSecurityModel> mKeyguardSecurityModel;
@Inject Lazy<DozeParameters> mDozeParameters;
@Inject Lazy<IWallpaperManager> mWallpaperManager;
@Inject Lazy<CommandQueue> mCommandQueue;
@Inject Lazy<Recents> mRecents;
@Inject Lazy<StatusBar> mStatusBar;
@Inject Lazy<DisplayController> mDisplayController;
@Inject Lazy<SystemWindows> mSystemWindows;
}

后面以StatusBar实例的注入为例阐述下SystemUI里Dagger2的注入流程。

随着SystemServer发出启动SystemUIService的请求,SystemUI的Application将首先被实例化。在实例化之前,指定的AppComponentFactory实现类将会收到回调。

// AndroidManifest.xml
<application
android:name=".SystemUIApplication"
...
tools:replace="android:appComponentFactory"
android:appComponentFactory=".SystemUIAppComponentFactory">
</Application>

调用super得到Application实例之后向其注册Context准备完毕的回调,该回调会执行SystemUIFactory和DI组件的初始化。

public class SystemUIAppComponentFactory extends AppComponentFactory {
@Inject
public ContextComponentHelper mComponentHelper;
...
@Override
public Application instantiateApplicationCompat(
@NonNull ClassLoader cl, @NonNull String className)
throws InstantiationException, IllegalAccessException, ClassNotFoundException {
Application app = super.instantiateApplicationCompat(cl, className);
if (app instanceof ContextInitializer) {
// 注册Context成功取得的回调
((ContextInitializer) app).setContextAvailableCallback(
context -> {
SystemUIFactory.createFromConfig(context);
SystemUIFactory.getInstance().getRootComponent().inject(
SystemUIAppComponentFactory.this);
}
);
} return app;
}
...
}

Application的onCreate()回调的时候意味着Context已准备完毕,接着执行上述回调。

public class SystemUIApplication extends Application implements
SystemUIAppComponentFactory.ContextInitializer {
...
@Override
public void setContextAvailableCallback(
SystemUIAppComponentFactory.ContextAvailableCallback callback) {
mContextAvailableCallback = callback;
} @Override
public void onCreate() {
...
log.traceBegin("DependencyInjection");
mContextAvailableCallback.onContextAvailable(this);★
mRootComponent = SystemUIFactory.getInstance().getRootComponent();
mComponentHelper = mRootComponent.getContextComponentHelper();
...
}
}

回调将先创建SystemUIFactory实例,并初始化SystemUI App的Dagger组件。之后初始化DI子组件并向Dependency实例注入依赖。

public class SystemUIFactory {
public static void createFromConfig(Context context) {
...
try {
Class<?> cls = null;
cls = context.getClassLoader().loadClass(clsName);
// 1\. 创建SystemUIFactory实例
mFactory = (SystemUIFactory) cls.newInstance();
mFactory.init(context);
}
} private void init(Context context) {
// 2\. 取得SystemUI的Dagger组件实例
mRootComponent = buildSystemUIRootComponent(context);
// 3\. 创建Dependency实例并绑定到DependencyInjector子组件中
Dependency dependency = new Dependency();
mRootComponent.createDependency().createSystemUI(dependency);
// 4\. 初始化Dependency
dependency.start();
} // 初始化Dagger组件
protected SystemUIRootComponent buildSystemUIRootComponent(Context context) {
return DaggerSystemUIRootComponent.builder()
.dependencyProvider(new DependencyProvider())
.contextHolder(new ContextHolder(context))
.build();
}
...
}

Dependency类里掌管着各式各样的依赖,被依赖的各实例通过Map管理。但并不是在初始化的时候就缓存它们。而先将各实例对应的懒加载回调缓存进去。其后在各实例确实需要使用的时候通过注入的懒加载获取和缓存。

public class Dependency {
// 使用class作为key将对应实例缓存的Map
private final ArrayMap<Object, Object> mDependencies = new ArrayMap<>();
// 缓存实例的懒加载回调的Map
private final ArrayMap<Object, LazyDependencyCreator> mProviders = new ArrayMap<>(); protected void start() {
mProviders.put(ActivityStarter.class, mActivityStarter::get);
mProviders.put(Recents.class, mRecents::get);
mProviders.put(StatusBar.class, mStatusBar::get);
mProviders.put(NavigationBarController.class, mNavigationBarController::get);
...
} // 根据class查询缓存,尚未缓存的话通过懒加载回调获取注入的实例并缓存
private synchronized <T> T getDependencyInner(Object key) {
T obj = (T) mDependencies.get(key);
if (obj == null) {
obj = createDependency(key);
mDependencies.put(key, obj);
if (autoRegisterModulesForDump() && obj instanceof Dumpable) {
mDumpManager.registerDumpable(obj.getClass().getName(), (Dumpable) obj);
}
}
return obj;
} protected <T> T createDependency(Object cls) {
Preconditions.checkArgument(cls instanceof DependencyKey<?> || cls instanceof Class<?>);
LazyDependencyCreator<T> provider = mProviders.get(cls);
return provider.createDependency();
} private interface LazyDependencyCreator<T> {
T createDependency();
}
}

Application创建好之后SystemUI的主Service将启动起来,并逐个启动其他Service。

public class SystemUIService extends Service {
...
@Override
public void onCreate() {
super.onCreate();
// Start all of SystemUI
((SystemUIApplication) getApplication()).startServicesIfNeeded();
...
}
}

通过ContextComponentHelper解析预设的service类名得到实例并启动。

public class SystemUIApplication {
public void startServicesIfNeeded() {
String[] names = SystemUIFactory.getInstance().getSystemUIServiceComponents(getResources());
startServicesIfNeeded(/* metricsPrefix= */ "StartServices", names);
} private void startServicesIfNeeded(String metricsPrefix, String[] services) {
...
final int N = services.length;
for (int i = 0; i < N; i++) {
String clsName = services[i];
try {
// 从ContextComponentHelper里获取对应的实例
SystemUI obj = mComponentHelper.resolveSystemUI(clsName);
if (obj == null) {
Constructor constructor = Class.forName(clsName).getConstructor(Context.class);
obj = (SystemUI) constructor.newInstance(this);
}
mServices[i] = obj;
} mServices[i].start();
...
}
mRootComponent.getInitController().executePostInitTasks();
}
}

配置的Service列表。

// config.xml
<string-array name="config_systemUIServiceComponents" translatable="false">
...
<item>com.android.systemui.recents.Recents</item>
<item>com.android.systemui.volume.VolumeUI</item>
<item>com.android.systemui.stackdivider.Divider</item>
<item>com.android.systemui.statusbar.phone.StatusBar</item> ★
...
</string-array>

ContextComponentHelper单例已声明由Dagger组件提供。

@Singleton
@Component(modules = {...})
public interface SystemUIRootComponent {
...
/**
* Creates a ContextComponentHelper.
*/
@Singleton
ContextComponentHelper getContextComponentHelper();
}

模块SystemUIModule负责注入ContextComponentHelper实例,实际注入的是ContextComponentResolver实例。

@Module(...)
public abstract class SystemUIModule {
...
/** */
@Binds
public abstract ContextComponentHelper bindComponentHelper(
ContextComponentResolver componentHelper);
}

ContextComponentResolver用于解析Activity和Service等实例,通过class实例从Map查询得到的Provider里取得对应的Service实例。 它的构造函数注释了@Inject。它依赖几个Map参数,比如StatusBar的Provider是注入到其中的SystemUI Map里。

@Singleton
public class ContextComponentResolver implements ContextComponentHelper {
@Inject
ContextComponentResolver(Map<Class<?>, Provider<Activity>> activityCreators,
Map<Class<?>, Provider<Service>> serviceCreators,
Map<Class<?>, Provider<SystemUI>> systemUICreators,
Map<Class<?>, Provider<RecentsImplementation>> recentsCreators,
Map<Class<?>, Provider<BroadcastReceiver>> broadcastReceiverCreators) {
mSystemUICreators = systemUICreators;
...
}
...
@Override
public SystemUI resolveSystemUI(String className) {
return resolve(className, mSystemUICreators);
} // 依据名称得到的class实例去查询Provider实例,进而取得对应SystemUI的实例
private <T> T resolve(String className, Map<Class<?>, Provider<T>> creators) {
try {
Class<?> clazz = Class.forName(className);
Provider<T> provider = creators.get(clazz);
return provider == null ? null : provider.get();
} catch (ClassNotFoundException e) {
return null;
}
}
}

在SystemUIBinder的Module里声明了以ClassKey为StatusBar.class,value由StatusBarPhoneModule模块注入到Map里。而Provider#get()的实例将拿到provideStatusBar注入的实例。(StatusBar构造器的参数竟有76个之多,简直恐怖。。。)

@Module(includes = {RecentsModule.class, StatusBarModule.class...})
public abstract class SystemUIBinder {
/** Inject into StatusBar. */
@Binds
@IntoMap
@ClassKey(StatusBar.class)
public abstract SystemUI bindsStatusBar(StatusBar sysui);
...
} @Module(includes = {StatusBarPhoneModule.class...})
public interface StatusBarModule {
} @Module(includes = {StatusBarPhoneDependenciesModule.class})
public interface StatusBarPhoneModule {
@Provides
@Singleton
static StatusBar provideStatusBar(
Context context,
NotificationsController notificationsController,
LightBarController lightBarController,
AutoHideController autoHideController,
KeyguardUpdateMonitor keyguardUpdateMonitor,
StatusBarIconController statusBarIconController,
...) {
return new StatusBar(...);
}
}

SystemUI里DI关系图

结语

回顾下依赖注入技术的必要性。

  • 代码的复用:通过参数传递复用实例减少样板代码
  • 测试的方便:通过注入模拟参数可以快速测试逻辑
  • 耦合度降低:类专注于自己的逻辑互相之间只通过参数连接

是否一定非要选择Dagger2这种自动方案呢?我觉得依据对项目的了解程度决定。

因为无论是采用手动还是自动的依赖注入方案,都需要我们理清各模块各类之前的关系,正确地定位每个类的角色,把握每个实例的作用域。

况且必须要认识到Dagger2这种框架的局限性。

  • 使用起来比较复杂,存在一定的学习门槛
  • 一定程度上更适合大型项目

技术人的脚步永远不会停滞不前,优化和改善是他们永恒的追求。

Google在Dagger2的基础上再次进行了改良,一来简化了DI的使用,二来强化了Android上的使用。这个框架也收录在Jetpack系列中,命名为Hilt

针对Hilt的解读已经安排,尽情期待。

本文作者

TechMerger

本文DEMO

github.com/ellisonchan…

推荐阅读

架构师学习笔记

耗时268天,7大模块、2983页58万字,Android开发核心知识笔记!

Java基础、计算机网络、系统

史上最全!押题率90%的 Android 中高级工程师面试复习大纲及真题答案整理(上篇)

Android基础夯实99题,点这里

史上最全!押题率90%的 Android 中高级工程师面试复习大纲及真题答案整理(中篇)

Android高级面试题(上)

史上最全!押题率90%的 Android 中高级工程师面试复习大纲及真题答案整理(下篇)

Android高级面试题(下)

史上最全!押题率90%的 Android 中高级工程师面试复习大纲及真题答案整理(终章)

【Android开发高手笔记】Dagger2和它在SystemUI上的应用的更多相关文章

  1. android开发学习笔记000

    使用书籍:<疯狂android讲义>——李刚著,2011年7月出版 虽然现在已2014,可我挑来跳去,还是以这本书开始我的android之旅吧. “疯狂源自梦想,技术成就辉煌.” 让我这个 ...

  2. Android开发高手课NOTE

    最近学习了极客时间的<Android开发高手课>很有收获,记录总结一下. 欢迎学习老师的专栏:Android开发高手课 内存优化 卡顿的原因 频繁 GC 造成卡顿.物理内存不足时系统会触发 ...

  3. Android开发自学笔记(基于Android Studio1.3.1)—1.环境搭建(转)

    一.引言    本套学习笔记的开发环境是Windows 10 专业版和Android Studio 的最新版1.3.1. Android Studio 是一个Android开发环境,基于Intelli ...

  4. Android开发自学笔记(Android Studio1.3.1)—3.Android应用结构解析

    一.R文件是什么?      如上图所示,我们可以通过findViewById方法通过传入R.id.show找到我们的TextView元素,findViewById方法也很好理解,从View中通过Id ...

  5. android开发学习笔记系列(1)-android起航

    前言 在学习安卓的过程中,我觉得非常有必要将自己所学的东西进行整理,因为每每当我知道我应该是如何去实现功能的时候,有许多细节问题我总是会遗漏,因此我也萌生了写一系列博客来描述自己学习的路线,让我的an ...

  6. Android开发自学笔记(Android Studio)—4.1布局组件

    一.引言 Android的界面是有布局和组件协同完成的,布局好比是建筑里的框架,而组件则相当于建筑里的砖瓦.组件按照布局的要求依次排列,就组成了用户所看见的界面.在Android4.0之前,我们通常说 ...

  7. Android开发自学笔记(Android Studio1.3.1)—2.开始第一个Android应用

    一.前言      使用Android Studio开发Android应用是一件非常简单的事情,因为它会帮你自动完成很多工作.本篇我们主要完成一个单击按钮在文本框显示当前时间的简单应用,借此来演示一下 ...

  8. Windows下Qt5搭建Android开发环境笔记

    Windows很大的特点是配置使用几乎都可以图形化进行,和Linux比起来在很多时候配置环境也要方便很多.所以,搭建Qt for Andorid也是十分简单的.需要以下工具: 1.最方便的Qt官方包, ...

  9. Android开发问题笔记

    1.Toolbar问题:最低版本15,必须使用support,才能使用Toolbar,Toobar是5.0引入的 2.BottomTab:这个用TabLayout解决了 3.后端API最好采用一个成熟 ...

随机推荐

  1. JS五种绑定彻底弄懂this,默认绑定、隐式绑定、显式绑定、new绑定、箭头函数绑定详解(转载)

    目录 壹 ❀ 引 贰 ❀ this默认绑定 叁 ❀ this隐式绑定 1.隐式绑定 2.隐式丢失 肆 ❀ this显式绑定 伍 ❀ new绑定 陆 ❀ this绑定优先级 柒 ❀ 箭头函数的this ...

  2. PicGo 图床配置【工具篇】

    Github图床(舍弃) step1 下载PicGo 下载链接: https://github.com/Molunerfinn/picgo/releases step2 新建仓库作为上传图片的目标地址 ...

  3. 前端坑多:使用js模拟按键输入的踩坑记录

    坑 一开始在Google搜索了一番,找到了用jQuery的方案,代码量很少,看起来很美好很不错,结果,根本没用-- 我反复试了这几个版本: var e = $.Event('keyup') e.key ...

  4. LZZY高级语言程序设计之输入秒数并用时钟的方式表达

    import java.util.Scanner;public class MQ5 { public static void main(String[] args) { Scanner sc = ne ...

  5. POJ2635(数论+欧拉筛+大数除法)

    题目链接:https://vjudge.net/problem/POJ-2635 题意:给定一个由两个质数积的大数M和一个数L,问大数M的其中较小的质数是否小于L. 题解:因为大数M已经超过long ...

  6. POj1860(floyd+正权回路)

    题目传送门 题意:有多种汇币,汇币之间可以交换,这需要手续费,当你用100A币交换B币时,A到B的汇率是29.75,手续费是0.39,那么你可以得到(100 - 0.39) * 29.75 = 296 ...

  7. python学习9 函数的基础知识

    1.函数的定义 def  func(): 2.函数的调用 func() 3.函数的返回值 #1.没有返回值 # (1)不写return # (2)只写return后面的代码不在继续执行,返回空,代表结 ...

  8. APIView里如何获取HTTP里的数据

    request.data.get()  获取post方法表单里的数据 request.post.get()  获取post方法表单里的数据 request.GET.get()  获取URL里的数据 r ...

  9. OpenCV图像处理中“找圆技术”的使用

    一.为什么"找圆"     圆是基本图形的一种,更为重要的是,自然情况下采集的图像,很少大量存在"圆":但凡存在的,大都是人工的,那么就必然代表特定的意义,从而 ...

  10. JVM学习笔记(二):JVM基本结构

    1 来源 来源:<Java虚拟机 JVM故障诊断与性能优化>--葛一鸣 章节:第二章 本文是第二章的一些笔记整理. 2 JVM基本参数-Xmx java命令的一般形式如下: java [- ...