利用ASM实现的轻量级跨Module依赖注入框架

2,589 阅读5分钟

为什么需要依赖注入

一些大型项目往往会有多个module,随着module越来越复杂,module间的依赖关系会变得难以维护,一不小心就可能造成循环依赖,导致项目编译不过。

典型的循环依赖

有一个Module-A,里面有一个class A,在Module B有一个class B,如果Module A需要用到class B,同时Module-B又需要用到class A时,就必须得改变代码结构了。如果直接在gradle里写

// module A build.gradle
implementation project(":Module-B")

// module B build.gradle
implementation project(":Module-A")

在构建时就会看到循环依赖的错误输出。

为什么不能循环依赖? 在构建前Gradle会计算依赖图,相当于对任务进行拓扑排序,而拓扑排序是不允许图中有环的,上面的AB显然就构成了一个图中的环,构建无法继续。

没有依赖注入的解决方案

Module-AModule-B中抽出一个Module-C,用来装class Aclass B。形成了这样的依赖关系。

这看起来是一个好的办法,对于比较简单的比如工具类的处理是不错的。但是如果class A本身又依赖于很多Module-A中的文件,是不是也得把以来的文件也一起抽出来呢?另一个问题是,这样做显然违反了分Module的初衷,把本该各自属于Module-AModule-B的功能合到了同一个Module。

依赖注入的解决方案

class Aclass B需要的对外提供的功能抽象出接口interface Ainterface B放在Module-C中,其实现放在原来module中,通过依赖注入创建A和B的实例。任何module要使用A、B的功能,只需要依赖Module-C即可。形成如下依赖关系:

其中,API Factory就集中管理实例的创建和查询,通过他获取到接口的实例。

框架使用

项目地址:github.com/SirLYC/AppI…

添加依赖

在根目录的build.gradle添加

buildscript {
    repositories {
        //...
        jcenter()
    }
    dependencies {
        // ...
        classpath "com.lyc:app-inject-plugin:latest.release"
    }
}

在输出apk文件的module中添加gradle插件:

apply plugin: 'com.android.application'
// 必须在application插件apply后引入
apply plugin: "com.lyc.app-inject-plugin"

最后在需要获取接口实例的module添加上这个依赖即可:

dependencies {
    // provide Annotations and AppInject API
    implementation "com.lyc:app-inject:latest.release"
}

当然,一个一个添加嫌麻烦的话,可以直接在一个基础module使用api引入:

dependencies {
    // provide Annotations and AppInject API
    api "com.lyc:app-inject:latest.release"
}

创建接口

使用@InjectApi标记需要依赖注入的接口。

@InjectApi
public interface ISingleApi {
    String logMsg();
}

oneToMany 参数表示这个接口是否可以有多个实现,oneToMany不同,获取接口实例的方式不同。

@InjectApi(oneToMany = true)
public interface IOneToManyApi {
    String logMsg();
}

在任意module创建接口的实现

实现使用@InjectApiImpl标记,同时需要指出实现的父接口是哪一个,默认情况是调用这个类的空构造方法构造实例(一个接口只会构建一次实例)。

@InjectApiImpl(api = ISingleApi.class)
public class SingleApiImpl implements ISingleApi {
    @Override
    public String logMsg() {
        return "I'm SingleApiImpl!";
    }
}

对于oneToMany=true的接口,允许有多个实现

// 第一个实现
@InjectApiImpl(api = IOneToManyApi.class)
public class OneToManyApiImpl1 implements IOneToManyApi {
    @Override
    public String logMsg() {
        return "I'm OneToManyApiImpl1!";
    }
}
// 第二个实现
@InjectApiImpl(api = IOneToManyApi.class)
public class OneToManyApiImpl2 implements IOneToManyApi {
    @Override
    public String logMsg() {
        return "I'm OneToManyApiImpl2!";
    }
}

这个是用kotlin实现的例子,其中createMethod是实例的创建方法,这个改成了GET_INSTANCE,会调用类的静态方法getInstance构建,所以在用kotlin实现时要添加@JvmStatic注解,否则会找不到方法无法创建这个实例。

// 这是用kotlin实现的例子
// class直接传::classs
@InjectApiImpl(api = IOneToManyApi::class, createMethod = CreateMethod.GET_INSTANCE)
class OneToManyApiImplKt private constructor() : IOneToManyApi {

    companion object {

        private val instance = OneToManyApiImplKt()

        // important!
        @JvmStatic
        fun getInstance(): IOneToManyApi {
            return instance
        }
    }

    override fun logMsg(): String {
        return "I'm OneToManyApiImplKt, created by getInstance()!"
    }
}

下面这个是使用Java实现的创建方法为getInstance的实例:

@InjectApiImpl(api = IGetInstanceApi.class, createMethod = CreateMethod.GET_INSTANCE)
public class GetInstanceApiImpl implements IGetInstanceApi {
    private static GetInstanceApiImpl instance = new GetInstanceApiImpl();

    private GetInstanceApiImpl() {
    }

`   // 会用这个方法去获取实例
    public static IGetInstanceApi getInstance() {
        return instance;
    }

    @Override
    public String logMsg() {
        return "I'm GetInstanceApiImpl, created by getInstance()!";
    }
}

在任意module获取接口实例

在有com.lyc:app-inject这个依赖的任意module,通过以下方法就可以获取到实例:

// oneToMany = false
ISingleApi singleApi = AppInject.getInstance().getSingleApi(ISingleApi.class);
Log.d(TAG, singleApi.logMsg());

// oneToMany = true
for (IOneToManyApi oneToManyApi : AppInject.getInstance().getOneToManyApiList(IOneToManyApi.class)) {
    Log.d(TAG, oneToManyApi.logMsg());
}

可以看到,调用哪个方法获取实例,由前文提到的oneToMany相关,取决于是否允许接口有多个实现。

使用kotlin进一步封装

利用kotlin的泛型特化,可以进一步做封装(在sample中有):

inline fun <reified T> getSingleApi(): T? {
    return AppInject.getInstance().getSingleApi(T::class.java)
}

inline fun <reified T> getOneToManyApiList(): List<T> {
    return AppInject.getInstance().getOneToManyApiList(T::class.java)
}

使用就变得更加简洁:

val testApi = getSingleApi<ITestApi>()
// or
val testApi:ITestApi? = getSingleApi()

框架原理

框架的实现有一些背景知识,这里就不展开说:

  • APK构建流程
  • 自定义Gradle插件
  • 字节码(稍微了解即可,因为有插件可以生产字节码)
  • ASM操作字节码

了解了上面的背景知识后,框架的结构就很简单了。首先有两个插桩方法:

// 保存单实现接口的信息
private Map<Class<?>, Implementation> singleApiClassMap = new HashMap<>();
// 保存多实现接口的信息
private Map<Class<?>, List<Implementation>> oneToManyApiClassMap = new HashMap<>();

// 插桩方法
private void initSingleApiMap() {
}

// 插桩方法
private void initOneToManyApiMap() {
    List<Implementation> list;
}

在构建的transform流程里,扫描所有类(必须在输出apk的module中引入插件才可以扫描到app中用到的所有类),收集接口和实现的信息,再把把这些信息插入到上述代码两个map对应的字节码插到两个对应的插桩方法即可。所以在了解了上述背景知识后实现起来并不是太难,只是有可能遇到一些坑...

其他

这个框架的灵感其实是来源于我之前实习的团队。最近自己在写毕设的时候才发现模块多了后,模块之间的依赖不好处理,于是就想起了实习团队中用到的类似的模块解耦的方式,当时还没怎么注意这个框架,自己摸索着去实现这样一个框架才发现需要这么多的背景知识...还得继续加油啊!

再次贴一下项目地址:github.com/SirLYC/AppI…

里面有sample可以clone下来,按照README的指引来运行。如果有什么问题欢迎留言或者邮件联系!