Android Gradle Plugin 源码解析(上)

简介: 一、源码依赖本文基于:android gradle plugin版本:com.android.tools.build:gradle:2.

一、源码依赖

本文基于:

android gradle plugin版本:

com.android.tools.build:gradle:2.3.0

gradle 版本:4.1

Gradle源码总共30个G,为简单起见,方便大家看源码,此处通过gradle依赖的形式来查看源码,依赖源码姿势:

创建一个新工程,app 项目目录中删除所有文件,仅留下gradle文件,依赖

apply plugin: 'java'
sourceCompatibility = 1.8
dependencies {
 compile gradleApi()
 compile 'com.android.tools.build:gradle:2.3.0'
}

将跟目录下的gradle文件,删除掉gradle依赖

buildscript {
 repositories {
 google()
 jcenter()
 }
 dependencies {
// compile 'com.android.tools.build:gradle:2.3.0'
 }
}

然后rebuild一下,就可以在External Libraries中查看到android gradle的源码已经依赖了

二、Android Gradle Plugin简介

我们知道Android gradle plugin是用来构建Android工程的gradle插件,在Android gradle 插件中,可以看到app工程和library工程所依赖的plugin是不一样的

// app 工程
apply plugin: 'com.android.application'
// library 工程
apply plugin: 'com.android.library'

而对应填写andorid块中所填写的配置也不同,这就是区分Application和Library的插件的extension块

分别为:

app工程 -> AppPlugin -> AppExtension
librar工程 -> LibraryPlugin -> LibraryExtension

对应的是AppPlugin和AppExtension,这两个插件构建的流程大抵是相同的,只是各自插件生成的任务不同,接下来我们着重分析Application插件是如何构建我们的Android应用的

三、AppPlugin的构建流程

我们先看下app工程中gradle的文件格式

apply plugin: 'com.android.application'
android {
 compileSdkVersion 25
 buildToolsVersion '26.0.2'
 defaultConfig {
 applicationId "com.zengshaoyi.gradledemo"
 minSdkVersion 15
 targetSdkVersion 25
 versionCode project.ext.versionCode
 versionName project.ext.versionName
 testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
 }
 buildTypes {
 release {
 minifyEnabled false
 proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
 }
 }
 lintOptions {
 abortOnError false
 }
}

跟踪apply方法,其实是进入到

AppPlugin的apply的方法,我们可以看到内部实现是直接调用父类BasePlugin的apply方法

protected void apply(@NonNull Project project) {
 checkPluginVersion();
 this.project = project;
 ExecutionConfigurationUtil.setThreadPoolSize(project);
 checkPathForErrors();
 checkModulesForErrors();
 ProfilerInitializer.init(project);
 threadRecorder = ThreadRecorder.get();
 ProcessProfileWriter.getProject(project.getPath())
 .setAndroidPluginVersion(Version.ANDROID_GRADLE_PLUGIN_VERSION)
 .setAndroidPlugin(getAnalyticsPluginType())
 .setPluginGeneration(GradleBuildProject.PluginGeneration.FIRST);
 threadRecorder.record(
 ExecutionType.BASE_PLUGIN_PROJECT_CONFIGURE,
 project.getPath(),
 null,
 this::configureProject);
 threadRecorder.record(
 ExecutionType.BASE_PLUGIN_PROJECT_BASE_EXTENSION_CREATION,
 project.getPath(),
 null,
 this::configureExtension);
 threadRecorder.record(
 ExecutionType.BASE_PLUGIN_PROJECT_TASKS_CREATION,
 project.getPath(),
 null,
 this::createTasks);
 // Apply additional plugins
 for (String plugin : AndroidGradleOptions.getAdditionalPlugins(project)) {
 project.apply(ImmutableMap.of("plugin", plugin));
 }
 }

threadRecoirder.recode()是记录最后一个参数的路径和执行的时间点,前面做了一些必要性的信息检测之前,其实主要做了以下几件事情:

// 配置项目,设置构建回调
this::configureProject
// 配置Extension
this::configureExtension
// 创建任务
this::createTasks

::是java 8引入的特性,详情可以查看java8特性 ,这里就是方法的调用

configureProject

直接来看源码

private void configureProject() {
 extraModelInfo = new ExtraModelInfo(project);
 checkGradleVersion();
 AndroidGradleOptions.validate(project);
 // Android SDK处理类
 sdkHandler = new SdkHandler(project, getLogger());
 // 设置项目评估阶段回调
 project.afterEvaluate(p -> {
 // TODO: Read flag from extension.
 if (!p.getGradle().getStartParameter().isOffline()
 && AndroidGradleOptions.getUseSdkDownload(p)) {
 // 相关配置依赖的下载处理 
 SdkLibData sdkLibData =
 SdkLibData.download(getDownloader(), getSettingsController());
 dependencyManager.setSdkLibData(sdkLibData);
 sdkHandler.setSdkLibData(sdkLibData);
 }
 });
 // 创建AndroidBuilder
 androidBuilder = new AndroidBuilder(
 project == project.getRootProject() ? project.getName() : project.getPath(),
 creator,
 new GradleProcessExecutor(project),
 new GradleJavaProcessExecutor(project),
 extraModelInfo,
 getLogger(),
 isVerbose());
 // dataBinding的相关处理
 dataBindingBuilder = new DataBindingBuilder();
 dataBindingBuilder.setPrintMachineReadableOutput(
 extraModelInfo.getErrorFormatMode() ==
 ExtraModelInfo.ErrorFormatMode.MACHINE_PARSABLE);
 // Apply the Java and Jacoco plugins.
 project.getPlugins().apply(JavaBasePlugin.class);
 project.getPlugins().apply(JacocoPlugin.class);
 // 给assemble任务添加描述
 project.getTasks()
 .getByName("assemble")
 .setDescription(
 "Assembles all variants of all applications and secondary packages.");
 ...

可以看到 configureProject 方法中在 project.afterEvaluate 设置了回调,当项目评估结束时,根据项目配置情况,设置 dependece 依赖;创建了 AndroidBuilder 对象,这个对象是用来合并manifest 和创建 dex 等作用,后面在创建任务的过程中会使用到,结下来继续看 configureProject 的源码

 // call back on execution. This is called after the whole build is done (not
 // after the current project is done).
 // This is will be called for each (android) projects though, so this should support
 // being called 2+ times.
 // 设置构建回调
 project.getGradle()
 .addBuildListener(
 new BuildListener() {
 private final LibraryCache libraryCache = LibraryCache.getCache();
 @Override
 public void buildStarted(Gradle gradle) {}
 @Override
 public void settingsEvaluated(Settings settings) {}
 @Override
 public void projectsLoaded(Gradle gradle) {}
 @Override
 public void projectsEvaluated(Gradle gradle) {}
 @Override
 public void buildFinished(BuildResult buildResult) {
 ExecutorSingleton.shutdown();
 sdkHandler.unload();
 threadRecorder.record(
 ExecutionType.BASE_PLUGIN_BUILD_FINISHED,
 project.getPath(),
 null,
 () -> {
 // 当任务执行完成时,清楚dex缓存
 PreDexCache.getCache()
 .clear(
 FileUtils.join(
 project.getRootProject()
 .getBuildDir(),
 FD_INTERMEDIATES,
 "dex-cache",
 "cache.xml"),
 getLogger());
 JackConversionCache.getCache()
 .clear(
 FileUtils.join(
 project.getRootProject()
 .getBuildDir(),
 FD_INTERMEDIATES,
 "jack-cache",
 "cache.xml"),
 getLogger());
 libraryCache.unload();
 Main.clearInternTables();
 });
 }
 });
 // 设置创建有向图任务回调
 project.getGradle()
 .getTaskGraph()
 .addTaskExecutionGraphListener(
 taskGraph -> {
 for (Task task : taskGraph.getAllTasks()) {
 // TransformTask是class编译成dex的重要任务
 if (task instanceof TransformTask) {
 Transform transform = ((TransformTask) task).getTransform();
 if (transform instanceof DexTransform) {
 PreDexCache.getCache()
 .load(
 FileUtils.join(
 project.getRootProject()
 .getBuildDir(),
 FD_INTERMEDIATES,
 "dex-cache",
 "cache.xml"));
 break;
 } else if (transform instanceof JackPreDexTransform) {
 JackConversionCache.getCache()
 .load(
 FileUtils.join(
 project.getRootProject()
 .getBuildDir(),
 FD_INTERMEDIATES,
 "jack-cache",
 "cache.xml"));
 break;
 }
 }
 }
 });

这里在添加了 BuildListener,在 buildFinished 的时候清楚了dex缓存,而在任务有向图创建的回调中,判断是否是 DexTransfrom,从而从缓存中加载dex。

总结一下 configureProject 做的事情,主要是进行版本有效性的判断,创建了 AndroidBuilder 对象,并设置了构建流程的回调来处理依赖和dex的加载和缓存清理。

configureExtension

这个阶段就是配置 extension 的阶段,就是创建我们 android 块中的可配置的对象

private void configureExtension() {
 final NamedDomainObjectContainer<BuildType> buildTypeContainer =
 project.container(
 BuildType.class,
 new BuildTypeFactory(instantiator, project, project.getLogger()));
 final NamedDomainObjectContainer<ProductFlavor> productFlavorContainer =
 project.container(
 ProductFlavor.class,
 new ProductFlavorFactory(
 instantiator, project, project.getLogger(), extraModelInfo));
 final NamedDomainObjectContainer<SigningConfig> signingConfigContainer =
 project.container(SigningConfig.class, new SigningConfigFactory(instantiator));
 extension =
 createExtension(
 project,
 instantiator,
 androidBuilder,
 sdkHandler,
 buildTypeContainer,
 productFlavorContainer,
 signingConfigContainer,
 extraModelInfo);
 ...

首先创建了 BuildType、ProductFlavor、SigningConfig 三个类型的Container,接着传入到了createExtension方法中,点入查看是个抽象的方法,各自的实现在子类中,这里也就是我们的AppPlugin 中

@NonNull
 @Override
 protected BaseExtension createExtension(
 @NonNull Project project,
 @NonNull Instantiator instantiator,
 @NonNull AndroidBuilder androidBuilder,
 @NonNull SdkHandler sdkHandler,
 @NonNull NamedDomainObjectContainer<BuildType> buildTypeContainer,
 @NonNull NamedDomainObjectContainer<ProductFlavor> productFlavorContainer,
 @NonNull NamedDomainObjectContainer<SigningConfig> signingConfigContainer,
 @NonNull ExtraModelInfo extraModelInfo) {
 return project.getExtensions()
 .create(
 "android",
 AppExtension.class,
 project,
 instantiator,
 androidBuilder,
 sdkHandler,
 buildTypeContainer,
 productFlavorContainer,
 signingConfigContainer,
 extraModelInfo);
 }

这里也就是可以看到我们android块配置是如何来的了,对应的Extension也确实是AppExtension,继续查看 configureExtension 的源码

 dependencyManager = new DependencyManager(
 project,
 extraModelInfo,
 sdkHandler);
 ndkHandler = new NdkHandler(
 project.getRootDir(),
 null, /* compileSkdVersion, this will be set in afterEvaluate */
 "gcc",
 "" /*toolchainVersion*/);
 taskManager =
 createTaskManager(
 project,
 androidBuilder,
 dataBindingBuilder,
 extension,
 sdkHandler,
 ndkHandler,
 dependencyManager,
 registry,
 threadRecorder);
 variantFactory = createVariantFactory(instantiator, androidBuilder, extension);
 variantManager =
 new VariantManager(
 project,
 androidBuilder,
 extension,
 variantFactory,
 taskManager,
 instantiator,
 threadRecorder);
 // Register a builder for the custom tooling model
 ModelBuilder modelBuilder = new ModelBuilder(
 androidBuilder,
 variantManager,
 taskManager,
 extension,
 extraModelInfo,
 ndkHandler,
 new NativeLibraryFactoryImpl(ndkHandler),
 getProjectType(),
 AndroidProject.GENERATION_ORIGINAL);
 registry.register(modelBuilder);
 // Register a builder for the native tooling model
 NativeModelBuilder nativeModelBuilder = new NativeModelBuilder(variantManager);
 registry.register(nativeModelBuilder);

这一部分主要是创建一些管理类,其中 createTaskManager、createVariantFactory 都是抽象方法,对应的实现类

createTaskManager
AppPlugin -> ApplicationTaskManager
LibraryPlugin -> LibraryTaskManager
createVariantFactory
AppPlugin -> ApplicationVariantFactory
LibraryPlugin -> LibraryVariantFactory

这里简单介绍一下 TaskManager 就是创建具体任务的管理类,app 工程和库 library 工程所需的构建任务是不同的,后面我们会介绍 app 工程创建的构建任务;VariantFactory 就是我们常说的构建变体的工厂类,主要是生成Variant(构建变体)的对象。我们回到 createExtension 的源码中

 // map the whenObjectAdded callbacks on the containers.
 signingConfigContainer.whenObjectAdded(variantManager::addSigningConfig);
 buildTypeContainer.whenObjectAdded(
 buildType -> {
 SigningConfig signingConfig =
 signingConfigContainer.findByName(BuilderConstants.DEBUG);
 buildType.init(signingConfig);
 variantManager.addBuildType(buildType);
 });
 productFlavorContainer.whenObjectAdded(variantManager::addProductFlavor);
 ...
 // create default Objects, signingConfig first as its used by the BuildTypes.
 variantFactory.createDefaultComponents(
 buildTypeContainer, productFlavorContainer, signingConfigContainer);

这一部分做得事情,配置了 BuildTypeContainer、ProductFlavorContainer、SigningConfigContainer 这三个配置项的 whenObjectAdded 的回调,每个配置的添加都会加入到 variantManager 中;创建默认配置,下面是 ApplicationVariantFactory 的 createDefaultComponents 代码

 @Override
 public void createDefaultComponents(
 @NonNull NamedDomainObjectContainer<BuildType> buildTypes,
 @NonNull NamedDomainObjectContainer<ProductFlavor> productFlavors,
 @NonNull NamedDomainObjectContainer<SigningConfig> signingConfigs) {
 // must create signing config first so that build type 'debug' can be initialized
 // with the debug signing config.
 signingConfigs.create(DEBUG);
 buildTypes.create(DEBUG);
 buildTypes.create(RELEASE);
 }

总结一下 configureExtension 方法的作用,主要是创建 Android 插件的扩展对象,对配置项 BuildType、ProductFlavor、SigningConfig 做了统一的创建和回调处理, 创建taskManager、variantFactory、variantManager。

createTasks

private void createTasks() {
 threadRecorder.record(
 ExecutionType.TASK_MANAGER_CREATE_TASKS,
 project.getPath(),
 null,
 () -> // 在项目评估之前创建任务 
 taskManager.createTasksBeforeEvaluate(
 new TaskContainerAdaptor(project.getTasks())));
 project.afterEvaluate(
 project ->
 threadRecorder.record(
 ExecutionType.BASE_PLUGIN_CREATE_ANDROID_TASKS,
 project.getPath(),
 null,
 // 在项目评估完成之后创建 androidTask
 () -> createAndroidTasks(false)));
 }

这里主要是分两块,一个是在 beforeEvaluate 创建任务;一个是在 afterEvaluate 创建任务。这里的区别是 AndroidTask 是依赖配置项的配置才能生成相应任务,所以是需要在 afterEvaluate 之后创建,如果对项目评估回调不理解的话,可以查阅Project文档。beforeEvaluate 创建的任务跟我们编译没有太大关系,我们重点查看一下 afterEvaluate 创建的任务 createAndroidTasks

 @VisibleForTesting
 final void createAndroidTasks(boolean force) {
 ...
 threadRecorder.record(
 ExecutionType.VARIANT_MANAGER_CREATE_ANDROID_TASKS,
 project.getPath(),
 null,
 () -> {
 // 创建AndroidTasks
 variantManager.createAndroidTasks();
 ApiObjectFactory apiObjectFactory =
 new ApiObjectFactory(
 androidBuilder, extension, variantFactory, instantiator);
 for (BaseVariantData variantData : variantManager.getVariantDataList()) {
 apiObjectFactory.create(variantData);
 }
 });
 ...
 }

我们主要看下variantManager的createAndroidTasks的方法

 /**
 * Variant/Task creation entry point.
 *
 * Not used by gradle-experimental.
 */
 public void createAndroidTasks() {
 variantFactory.validateModel(this);
 variantFactory.preVariantWork(project);
 final TaskFactory tasks = new TaskContainerAdaptor(project.getTasks());
 if (variantDataList.isEmpty()) {
 recorder.record(
 ExecutionType.VARIANT_MANAGER_CREATE_VARIANTS,
 project.getPath(),
 null /*variantName*/,
 this::populateVariantDataList);
 }
 // Create top level test tasks.
 recorder.record(
 ExecutionType.VARIANT_MANAGER_CREATE_TESTS_TASKS,
 project.getPath(),
 null /*variantName*/,
 () -> taskManager.createTopLevelTestTasks(tasks, !productFlavors.isEmpty()));
 for (final BaseVariantData<? extends BaseVariantOutputData> variantData : variantDataList) {
 recorder.record(
 ExecutionType.VARIANT_MANAGER_CREATE_TASKS_FOR_VARIANT,
 project.getPath(),
 variantData.getName(),
 () -> createTasksForVariantData(tasks, variantData));
 }
 taskManager.createReportTasks(tasks, variantDataList);
 }

首先判断 variantDataList 是否是空,如果是空的就会进入到 populateVariantDataList 方法中

/**
 * Create all variants.
 */
 public void populateVariantDataList() {
 if (productFlavors.isEmpty()) {
 createVariantDataForProductFlavors(Collections.emptyList());
 } else {
 List<String> flavorDimensionList = extension.getFlavorDimensionList();
 // Create iterable to get GradleProductFlavor from ProductFlavorData.
 Iterable<CoreProductFlavor> flavorDsl =
 Iterables.transform(
 productFlavors.values(),
 ProductFlavorData::getProductFlavor);
 // Get a list of all combinations of product flavors.
 List<ProductFlavorCombo<CoreProductFlavor>> flavorComboList =
 ProductFlavorCombo.createCombinations(
 flavorDimensionList,
 flavorDsl);
 for (ProductFlavorCombo<CoreProductFlavor> flavorCombo : flavorComboList) {
 //noinspection unchecked
 createVariantDataForProductFlavors(
 (List<ProductFlavor>) (List) flavorCombo.getFlavorList());
 }
 }
 }

从方法注释可以看到,这个方法主要的作用就是创建所有的 variants,试想一下该段代码会做哪些事情,是否是解析 buildType、productFlavor 配置?

创建构建变体(BuildVariant)

继续观察上面的代码,可以看到无论是否有配置productFlavor 子项,都会进入到 createVariantDataForProductFlavors 方法。如果有配置的话,通过获取配置的 flavorDimension 和 productFlavor 数组,调用 ProductFlavorCombo.createCombinations 组合出最后的产品风味数组 flavorComboList ,最后通过遍历调用 createVariantDataForProductFlavors 方法

 /**
 * Creates VariantData for a specified list of product flavor.
 *
 * This will create VariantData for all build types of the given flavors.
 *
 * @param productFlavorList the flavor(s) to build.
 */
 private void createVariantDataForProductFlavors(
 @NonNull List<ProductFlavor> productFlavorList) {
 ...
 for (BuildTypeData buildTypeData : buildTypes.values()) {
 boolean ignore = false;
 ...
 if (!ignore) {
 BaseVariantData<?> variantData = createVariantData(
 buildTypeData.getBuildType(),
 productFlavorList);
 variantDataList.add(variantData);
 ...
 }
 }
 ...
}

看上述代码,通过 creatVariantData 方法,将 buildType 和 productFlavor 的作为参数传入,创建了 variantData,并且加入到了 variantDataList 集合中,这里我们就是将所有的构建变体集合到了 variantDataList 中。

接着我们返回继续看 createAndroidTasks 方法

 /**
 * Variant/Task creation entry point.
 *
 * Not used by gradle-experimental.
 */
 public void createAndroidTasks() {
 ...
 for (final BaseVariantData<? extends BaseVariantOutputData> variantData : variantDataList) {
 recorder.record(
 ExecutionType.VARIANT_MANAGER_CREATE_TASKS_FOR_VARIANT,
 project.getPath(),
 variantData.getName(),
 () -> createTasksForVariantData(tasks, variantData));
 }
 ...
 }

通过上面拿到的variantDataList,遍历该集合来创建任务

 /**
 * Create tasks for the specified variantData.
 */
 public void createTasksForVariantData(
 final TaskFactory tasks,
 final BaseVariantData<? extends BaseVariantOutputData> variantData) {
 final BuildTypeData buildTypeData = buildTypes.get(
 variantData.getVariantConfiguration().getBuildType().getName());
 if (buildTypeData.getAssembleTask() == null) {
 // 创建assemble + buildType任务
 buildTypeData.setAssembleTask(taskManager.createAssembleTask(tasks, buildTypeData));
 }
 // Add dependency of assemble task on assemble build type task.
 tasks.named("assemble", new Action<Task>() {
 @Override
 public void execute(Task task) {
 assert buildTypeData.getAssembleTask() != null;
 // 将 assemble 任务依赖于我们的 assemble + buildType 任务
 task.dependsOn(buildTypeData.getAssembleTask().getName());
 }
 });
 VariantType variantType = variantData.getType();
 // 根据 variantData 创建 assemble + flavor + buildType 任务
 createAssembleTaskForVariantData(tasks, variantData);
 if (variantType.isForTesting()) {
 ...
 } else {
 // 根据 variantData 创建一系列任务
 taskManager.createTasksForVariantData(tasks, variantData);
 }
 }

首先会先根据 buildType 信息创建 assemble + buildType 的任务,可以看下taskManager. createAssembleTask里的代码

 @NonNull
 public AndroidTask<DefaultTask> createAssembleTask(
 @NonNull TaskFactory tasks,
 @NonNull VariantDimensionData dimensionData) {
 final String sourceSetName =
 StringHelper.capitalize(dimensionData.getSourceSet().getName());
 return androidTasks.create(
 tasks,
 // 设置任务名字为 assembleXXX
 "assemble" + sourceSetName,
 assembleTask -> {
 // 设置描述和任务组
 assembleTask.setDescription("Assembles all " + sourceSetName + " builds.");
 assembleTask.setGroup(BasePlugin.BUILD_GROUP);
 });
 }

创建完任务之后,将assemble任务依赖于我们的assembleXXX任务,随后调用 createAssembleTaskForVariantData 方法,此方法是创建 assemble + flavor + buildType 任务,流程多了 productFlavor 任务的创建,这里就不赘述了。后面会执 createTasksForVariantData,这个方法就是根据 variant 生成一系列 Android 构建所需任务(后面会详细介绍),回到 createAndroidTasks 方法中

threadRecorder.record(
 ExecutionType.VARIANT_MANAGER_CREATE_ANDROID_TASKS,
 project.getPath(),
 null,
 () -> {
 variantManager.createAndroidTasks();
 ApiObjectFactory apiObjectFactory =
 new ApiObjectFactory(
 androidBuilder, extension, variantFactory, instantiator);
 for (BaseVariantData variantData : variantManager.getVariantDataList()) {
 // 创建variantApi,添加到extensions中
 apiObjectFactory.create(variantData);
 }
 });

最后就遍历 variantDataList 通过 ApiObjectFactory 创建 variantApi,添加到 extensions 中。至此,我们就已经将配置的构建变种任务已经添加到我们的任务列表中,并形成了相关依赖。
    一篇文太长,还有一半下一章发出来。

最后

感谢你到这里,喜欢的话请帮忙点个赞让更多需要的人看到哦。更多Android进阶技术,面试资料整理分享,职业生涯规划,产品,思维,行业观察,谈天说地。可以加Android架构师群;701740775。

相关文章
|
16天前
|
XML Java Android开发
Android实现自定义进度条(源码+解析)
Android实现自定义进度条(源码+解析)
49 1
|
16天前
|
Java Android开发
Android反编译查看源码
Android反编译查看源码
21 0
|
1月前
|
编译器 开发工具 Android开发
Android 12 新特性深度解析
【2月更文挑战第15天】 随着移动操作系统的不断进化,Android 12带来了一系列创新功能与性能提升。本文将深入剖析Android 12的核心新特性,包括隐私仪表盘、通知管理、设备控制以及性能优化等方面,为开发者和用户提供全面的更新指南。
|
1月前
|
定位技术 API 数据库
基于Android的在线移动电子导航系统的研究与实现(论文+源码)_kaic
基于Android的在线移动电子导航系统的研究与实现(论文+源码)_kaic
|
1月前
|
搜索推荐 测试技术 定位技术
基于Android的自助导游系统的设计与实现(论文+源码)_kaic
基于Android的自助导游系统的设计与实现(论文+源码)_kaic
|
1月前
|
Java 关系型数据库 应用服务中间件
基于Android的人事管理系统设计与实现(论文+源码)_kaic
基于Android的人事管理系统设计与实现(论文+源码)_kaic
|
1月前
|
设计模式 测试技术 数据库
基于Android的食堂点餐APP的设计与实现(论文+源码)_kaic
基于Android的食堂点餐APP的设计与实现(论文+源码)_kaic
|
2月前
|
小程序 JavaScript Java
android电子班牌人脸识别系统源码
智慧校园云平台全套源码包含:电子班牌管理系统、成绩管理系统、考勤人脸刷卡管理系统、综合素养评价系统、请假管理系统、电子班牌发布系统、校务管理系统、小程序移动端、教师后台管理系统、SaaS运营云平台。
34 1
|
2月前
|
小程序 Java 数据挖掘
Java校园智慧管理云平台源码 小程序+android电子班牌系统
智慧校园技术架构 ❀后端:Java ❀框架:springboot ❀前端页面:vue +element-ui ❀小程序:小程序原生开发 ❀电子班牌:Java Android
34 0
|
6月前
|
SQL 人工智能 移动开发
Android Studio插件版本与Gradle 版本对应关系
Android Studio插件版本与Gradle 版本对应关系
Android Studio插件版本与Gradle 版本对应关系

推荐镜像

更多