直奔主题,在模块化开发中,模块间的数据交流大多数同学会采用以接口作为通信协议的方式。需要面对的问题有以下几点:

总结下C++中模块(Dll)对外暴露接口的方式:

之前看了微信Android模块化架构重构实践&version=12020810&nettype=WIFI&fontScale=100&pass_ticket=cGLqdHvj3DBPU1Hz4dfJbSYEsO6rmz3OlNS006uAcSDzHeRgHYdg%2Fb3OD8VI4Ec7)很受启发,对其中的”.api化”比较感兴趣,下面为自己的实现,和微信的效果有些许区别,但是能实现基本功能。

1.建立一个模块

升级到最新的Android Studio 3.0
PreView后,会提示我们升级Gradle版本,然后就挂掉了。IDE报如下错误:

  • 接口由谁来维护?这个问题简单,由提供服务的模块来维护。

  • 接口怎么暴露?打成jar包,发布到maven。

  • 接口在哪里维护?现在可以参考的方案有三种:一.
    所有相关模块的接口统一在一个模块中维护;二.
    各个模块的接口分别在自建一个新的模块中维护,通过命名规则一一对应;三.
    像微信的.api方案,使用特殊的规则混杂在各自的模块中。

(1)导出API函数的方式
这种方式是Windows中调用DLL接口的最基本方式,GDI32.dll,
User32.dll都是用这种方式对外暴露系统API的。
这种方式的优点是导出函数没有语言限制,什么语言都能调用;
缺点是这种方式是面向过程的,外部如果要支持多实例等不是很方便,另外它要求的回调函数(callback)只能是普通C函数,C++中我们通常用类静态成员函数,很不方便。
当然,我们通过封装其实也可以让这种方式支持多实例,通过一个抽象句柄HComponent,
比如支持导出函数HComponent CreateInstance(); VOID
DeleteInstance(HComponent
h);然后内部的其他导出函数的第一个参数都是实例句柄,类似INT
SendMessage(HComponent h, …), 用这种方式可以模拟出面向对象的效果。
另外如果用动态加载(LoadLibrary,
GetProcAddress)的方式调用它的导出函数,即使导出函数内部实现修改了,外部程序也不用重新编译,仍然可用。
导出函数方式一个比较优秀的例子是GDI+的实现,整个GdiPlus.dll对外提供的都是普通导出函数,但是它却可以方便的给面向对象的语言使用,因为一方面它用Handle的方式在DLL内部封装了对象,另一方面它在DLL外围又用C++类的方式封装了头文件直接提供给用户,
所以C++程序可以直接以面向对象的方式调用。

原理

  1. 找到所有需要暴露的接口文件
  2. 把他们打成jar
  3. 发布到本地maven仓库
  4. 其他module引用jar包使用

eg.Market.js

Error:(1, 0) The android gradle plugin version 3.0.0-alpha1 is too
old, please update to the latest version.
To override this check from the command line please set the
ANDROID_DAILY_OVERRIDE environment variable to
“8d256f619ba96afd1273947e8b8bebea4cb2fd05”
[Upgrade plugin to version 2.3.2 and sync project]

如果接着第一个问题,方案一好像就有点难确定接口对应的来源模块。方案二会出现接口模块成倍增加,极易出现一个模块只含一个接口类的现象。方案三需要自定义相关插件,在创建接口时会有点不便,不够灵活。

(2)导出类方式
导出类的方式就是把整个C++类对外导出, MFC42.dll就是这种方式。
这种方式的优点是直接面向对象。
缺点是只能给C++用,而且最好编译器都要一致,另外DLL一变动,
外部程序需要重新编译, 而且外部程序可以通过头文件看到你类的内部实现,
所以这种方式是最不建议使用的方式。

实现

  1. 约定需要暴露的接口文件规则:
    • 因为打进jar包的是class文件,所以按照微信的.api后缀的做法就不能在我这里实现了,因为.api编译后还是.class(除非自己javac,这也是一种解决方案),所以我就在文件夹上进行标注,约定在"_apis_"文件夹下的java/kotlin文件为需要暴露的接口文件。
    • 这样带来了一个好处就是兼容了kotlin文件,我们可以暴露使用kotlin写的接口
  2. 发布到本地maven仓库

    • 在需要暴露接口的modulebuild.gradle中添加:

         apply plugin: 'maven-publish'
         task sourceJar(type: Jar) {
             //java编译后的class文件
             from fileTree(dir: "${project.projectDir.absolutePath}/build/intermediates/classes/debug", include: '**/_apis_/**/*.class')
             //kotlin编译后的class文件
             from fileTree(dir: "${project.projectDir.absolutePath}/build/tmp/kapt3/incrementalData/debug", include: '**/_apis_/**/*.class')
         }
      
         publishing {
             publications {
                 bar(MavenPublication) {
                     groupId 'your.group.id'
                     artifactId project.name //使用当前module name作为jar的name
                     version '1.0'
                     artifact(sourceJar)
                 }
             }
             repositories {
                 maven {
                     //发布到本项目的:  根目录/repo/
                     url "${rootProject.projectDir.absolutePath}/repo"
                 }
             }
         }
      
    • Rebuild Project 后 找到gradle project如下图:

      图片 1

      image.png

    • 依下图找到publish task,双击666

      图片 2

      image.png

    • 上述2步可以简洁化处理:在当前项目的terminal中执行

       ./gradlew clean
       ./gradlew assembleDebug
       ./gradlew publish
      

//—————————————————————————————————

百度大部分答案都是手动修改gradle文件至可用版本,但是我们之前编译明明是通过的,为什么会出现这种情况呢?笔者最后在stackOverFlow上找到了答案:https://stackoverflow.com/questions/44301207/android-plugin-is-too-old-2-4-0-alpha7/44302290\#44302290

MIS登场!!!接下来先介绍mis的简单使用以及背后的原理。

(3)COM方式
COM方式实际上导出了几个固定函数(DllGetClassObject, DllCanUnloadNow,
DllRegisterServer, DllUnregisterServer),
然后以这几个函数为入口,调用组件内部‘实现的接口。
COM方式综合了上面2种方法的所有优点,没有语言限制,面向对象,多实例,只能看到接口,动态升级等。
当然COM因为其复杂性和对注册表的依赖,很多时候我们在封装模块时不愿意严格按照COM标准来实现,但是我们可以按照COM思想来提供接口。
比如我们可以让我们模块只提供一个导出函数CreateFactory,
然后外部可以调用该接口来创建工厂,最后通过工厂创建出各种类型的对象,这些对象实现了某些接口,外部只需要这些接口的头文件即可调用对象的方法。
现在越来越多的组件以这种方式对外提供接口,比如D2D对外的导出接口就是D2D1CreateFactory,
然后就可以通过该工厂来创建其他的对象,比如pD2DFactory->CreateHwndRenderTarget(…),最后可以直接调用对象实现的接口:pRenderTarget->DrawRectangle(D2D1::RectF(100.f,
100.f, 500.f, 500.f), pBlackBrush);

使用

  • 在项目根目录的build.gradle中添加:

    allprojects {
        repositories {
                maven { url "${rootProject.projectDir.absolutePath}/repo" }
        }
    }
    
  • 在需要使用接口的modulebuild.gradle中添加依赖

    provide 'you.group.id:module_name:1.0' //module_name为暴露接口的那个module的name
    

function createMark(marketName){

There is a 40 day limit for beta/alpha versions and after that, Android Studio forces you to update to the latest version.

But it doesn't exist (the newest is the alpha with AS 3.0) so a possible solution is setting that environment variable, executing in the command line (in MAC):

launchctl setenv ANDROID_DAILY_OVERRIDE 8d256f619ba96afd1273947e8b8bebea4cb2fd05

And relaunch Android Studio

模块接口服务(Module Interface Service)

当然,上面几种DLL对外暴露接口的方式本质上没有区别,都是利用PE文件的导出节来导出数据和函数,但是根据它们使用方式的不同,对外部模块来说还是有很大的区别,我们的推荐次序依次是:COM方式->导出API函数方式->导出类方式。

this.marketName = marketName;

原来beta/alpha版本有40天限制,在此之后Android
Studio就会强制我们升级新版本。但是这个所谓的新版本在引用库中并不存在(最新版是配合Android
Studio
3.0使用的alpha版),解决方法是更新一下环境变量ANDROID_DAILY_OVERRIDE,设置为Android
Studio中提示的值。

MIS是从微信的.api方案演变而来,主要解决的问题是如何在一个模块内维护其对外暴露的接口,而不是把接口和接口实现分离到两个不同的模块。

console.info(‘you create a market which name is
‘+this.marketName+’!!!’);

在MAC中我们可以在terminal中输入如下命令:
launchctl setenv ANDROID_DAILY_OVERRIDE xxx(Android
Studio报错信息中提示的值)

图片 3mis工程结构

function getMarketName (){

Windows修改环境变量原理是一样的,在此就不再赘述。

在根项目的build.gradle中添加mis插件的classpath

return this.marketName;

PS:更新完成后记得重启Android Studio

buildscript { dependencies { ... classpath 'com.eastwood.tools.plugins:mis:1.3.5' }}

}

在模块的build.gradle中添加mis插件

exports.createMark = createMark;

...apply plugin: 'mis'

exports.getMarketName = getMarketName;

Gradle Sync后,在java同级目录创建mis文件夹

//—————————————————————————————————

图片 4mis目录

eg. Main.js

直接在mis文件夹下,创建对应的包名、接口类和数据Model。并在java文件夹下实现接口服务。

//—————————————————————————————————

图片 3mis接口服务

var mark = require(‘./Market.js’);

mis { publications { main { groupId 'com.eastwood.demo' artifactId 'library-sdk' // version '1.0.0-SNAPSHOT' dependencies { compileOnly 'com.google.code.gson:gson:2.8.1' } } } ...}

mark.createMark(‘dapaer`s market’);

  • main指的是src/main/java中的main,除了main之外,其值还可以为
    build types和product
    flavors对应的值,即对应目录下的mis。比如与src/debug/java对应的src/debug/mis

  • groupIdartifactIdversion对应的是Maven的GAV。初次配置时不设置version,发布至maven时设置version

  • dependencies中可声明该mis编译和运行时需用到的第三方库,仅支持compileOnlyimplementation

console.info(‘mark`s name is ‘ + mark.getMarketName());

//—————————————————————————————————

mis { publications { main { groupId 'com.eastwood.demo' artifactId 'library-sdk' version '1.0.0-SNAPSHOT' ... } } repositories { maven { url "http://***" credentials { username '***' password '***' } } } ...}

通过exports去暴露模块的成员或方法(暴露公开的api)

  • 发布时需设置version

  • 发布时内部用到的插件是maven-publish,其中repositories相关设置请查阅#
    Maven Publish Plugin

有两种方式

Gradle Sync后,打开Gradle Tasks
View,选择publishMis[…]PublicationToMavenRepository执行发布任务。

1.module.exports.xxx = 模块里面的某个接口

图片 6上传Task

2.exports.xxx = 模块里面的某个接口

其中publishMis[…]PublicationToMavenLocal
是发布至本地maven。如果使用本地maven,请将mavenLocal()添加至根项目的build.gradle中,比如:

这两者的区别

allprojects { repositories { google() jcenter() mavenLocal() }}

1.exports 是module.exports的引用指向同一个内存地址

不会。虽然mis目录下的类能被java目录下的类直接引用,但不会参与编译,真正参与编译的是该mis目录生成的jar包,其位于当前工程.gradle/mis下。在当前工程Sync&Build的时候,mis插件会对这些配置了publication的mis目录进行编译打包生成jar包,并且依赖该jar包。

2.require需要返回的是module.exports而不是exports,如果此时exports仍为module.exprots的引用则没有关系,如果不是则会报错。

mis目录下的类之所以能被java目录下的类直接引用,是因为mis目录被设置为sourceSets
aidl的src目录,而Android Studio对sourceSets aidl的src目录有特别支持。

           –》不是的情况:eg.exports = function(){xxx}或者exports =
{}//一个新的对象,则会将原来的引用关系破裂,此时若是暴露module.exports =
function(){xxx}或者module.exports =
{}//一个新对象,不会报错,因为返回的仍然是require需要的。

不设置publicationversion。通过misPublication声明依赖,比如:

通过require去加载模块

dependencies { ... implementation misPublication('com.eastwood.demo:library-sdk')}

eg. var market = require(‘./Market.js’)//.代表当前目录下

misPublication运行机理是会自动在当前工程.gradle/mis下查找是否有对应的mis提供的jar包。如果有,就使用对应的mis提供的jar包;如果没有且指定了version,就使用maven上的jar包。

这段代码表达用require加载Market.js这个模块,返回了module.exports暴露出来的所有方法到market这个对象上

接口被发布到maven后,其.gradle/mis下的jar包会被删除,接口所在的模块根据publication中设置的GAV使用maven上的jar包。如果其他模块通过misPublication声明对其依赖,比如:

补充

dependencies { ... implementation misPublication('com.eastwood.demo:library-sdk') // 或 implementation misPublication('com.eastwood.demo:library-sdk:1.0.0-SNAPSHOT')}

所谓的引用是指指向同一个内存地址

不管misPublication中是否设置了的version,都会使用maven上的jar包,其版本同接口所在的模块publication中的GAV。

eg. var personA = {Name:’TOM’};

mis目录下类发生实质性的修改后(生成不同的jar包),在当前工程Sync&Build的时,会在.gradle/mis下的重新生成jar包,接口所在的模块不管publication中是否设置version,都使用.gradle/mis下的jar包。如果其他模块通过misPublication声明对其依赖,不管misPublication中是否设置的version,都会使用.gradle/mis下的jar包。

      var personB = personA;

初次发布时,请检查对应的publication是否已经设置的version,以及是否添加相关repositories

     此时personB是personA的引用

MIS已经上传至Github,欢迎star交流。QQ
1056453754

     如果此时将personB的属性Name
修改personA同样也会修改因为他们指向同一个内存地址,若此时将personB =
{Name:’Candy’},则此时personA不会被改变,因为已将personB重新赋值,personB不再是personA的引用了,这里可以解释上的exports和module.exports的关系。

最后运行

图片 7

发表评论

电子邮件地址不会被公开。 必填项已用*标注

网站地图xml地图