一、简介

之前公司的一个项目需要用到Gradle插件来修改编译后的class文件,今天有时间就拿出来整理一下,学习一下Gradle插件的编写还是一件十分有意义的事。

二、Gradle插件类型

  • 一种是直接在项目中的gradle文件里编写,这种方式的缺点是无法复用插件代码,在其他项目中还得复制一遍代码(或者说说复制一遍文件)

  • 另一种是在独立的项目里编写插件,然后发布到中央仓库,之后直接引用就可以了,优点就是可复用。

今天我们主要来讲解下第二种。

三、Gradle插件

Gradle插件是使用Groovy进行开发的,而Groovy其实是可以兼容Java的。Android Studio其实除了开发Android App外,完全可以胜任开发Gradle插件这一工作。

在此之前我们先来了解下 Gradle插件Gradle 的关系:

Gradle插件 版本在项目根目录下的 build.gradle 中,如下:

dependencies {classpath 'com.android.tools.build:gradle:2.3.0'
}

而每个Gradle插件版本号又对应有一个或一些 Gradle发行版本(一般是限定一个最低版本),也就是我们常见的类似gradle-3.3-all.zip这种东西,如下:

distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip

如果这两个版本对应不上了,那你的工程构建的时候就会报错。

具体的对应关系如下:

插件版本 所需的 Gradle 版本
1.0.0 - 1.1.3 2.2.1 - 2.3
1.2.0 - 1.3.1 2.2.1 - 2.9
1.5.0 2.2.1 - 2.13
2.0.0 - 2.1.2 2.10 - 2.13
2.1.3 - 2.2.3 2.14.1+
2.3.0+ 3.3+
3.0.0+ 4.1+
3.1.0+ 4.4+
3.2.0 - 3.2.1 4.6+
3.3.0 - 3.3.3 4.10.1+
3.4.0 - 3.4.3 5.1.1+
3.5.0 - 3.5.4 5.4.1+
3.6.0 - 3.6.4 5.6.4+
4.0.0+ 6.1.1+
4.1.0+ 6.5+

详情请见:Android Gradle 插件版本说明

下面来讲讲具体如何开发。

1、创建插件步骤

第一步:新建一个Android工程

第二步:在该工程中新建一个Android Module项目,类型选择Android Library

第三步:将Module里的内容删除,只保留build.gradle文件和src/main目录,同时移除build.gradle文件里的内容

第四步:建立Gradle插件目录

由于gradle是基于groovy,因此,我们开发的gradle插件相当于一个groovy项目。所以需要在main目录下新建groovy目录,这时候groovy文件夹会被Android识别为groovy源码目录。除了在main目录下新建groovy目录外,你还要在main目录下新建resources目录,同理resources目录会被自动识别为资源文件夹。在groovy目录下新建项目包名,就像Java包名那样。resources目录下新建文件夹META-INFMETA-INF文件夹下新建gradle-plugins文件夹。这样,就完成了gradle 插件的项目的整体搭建。目前项目的结构是这样的:

第五步:修改build.gradle文件

内容如下:

apply plugin: 'groovy'
apply plugin: 'maven'dependencies{// gradle sdkcompile gradleApi()// groovy sdkcompile localGroovy()compile 'com.android.tools.build:gradle:1.5.0'
}repositories{mavenCentral()
}

第六步:在com.davisplugins包名下通过new -> file ->创建PluginImpl.groovy文件

内容如下:

package com.davispluginsimport com.android.build.gradle.AppExtension
import org.gradle.api.Plugin
import org.gradle.api.Projectpublic class PluginImpl implements Plugin<Project>{void apply(Project project){System.out.println("========================");System.out.println("hello gradle plugin!");System.out.println("========================");}
}

第七步:定义插件名称

resources/META-INF/gradle-plugins目录下新建一个properties文件,注意该文件的命名就是你使用插件的名字,这里命名为davis.properties,那么你在其他build.gradle文件中使用自定义的插件时候则需写成:

apply plugin: 'davis'

davis.properties文件内容:

implementation-class=com.davisplugins.PluginImpl

注意包名需要替换为你自己的包名。

现在你的目录结构如下:

2、插件发布

前面我们已经自定义好了插件,接下来就是要打包到Maven库里面去了,你可以选择打包到本地,或者是远程服务器中。

(1)打包到本地Maven仓库

在我们自定义Module目录下的build.gradle添加如下代码:

uploadArchives {repositories {mavenDeployer {pom.groupId = 'com.davisplugins'pom.artifactId = 'davis'pom.version = 1.0// maven本地仓库的目录repository(url: uri('../DavisPlugin'))}}
}

这时候,右侧的gradle Toolbar就会在module下多出一个task

点击uploadArchives这个Task,就会在项目下多出一个DavisPlugin目录,里面存着这个gradle插件。

(2)发布到远程Jcenter仓库

内容更新中…

3、插件的使用

我们来看下,发布到本地maven仓库的插件如何使用,在项目根目录下的gradle.build的文件中加入:

buildscript {repositories {// maven插件目录maven{url uri('DavisPlugin')}jcenter()}dependencies {classpath 'com.android.tools.build:gradle:2.1.0'// 使用自定义插件classpath 'com.davisplugins:davis:1.0'}
}allprojects {repositories {jcenter()}
}task clean(type: Delete) {delete rootProject.buildDir
}

app目录下的build.gradle文件中加入:

apply plugin: 'davis'

然后我们就可以使用该插件了,执行一次打包命令看看会发生啥吧!

在打包之前输出了我们打印的日志信息。

4、最佳实践

(1)修改编译后的class文件

我们回到如何修改class文件,首先我们得知道什么时候编译完成,并且我们要赶在class文件被转化为dex文件之前去修改。从1.5.0-beta1开始,android的gradle插件引入了com.android.build.api.transform.Transform,可以点击 http://tools.android.com/tech-docs/new-build-system/transform-api 查看相关内容。Transform每次都是将一个输入进行处理,然后将处理结果输出,而输出的结果将会作为另一个Transform的输入,过程如下:

注意:输出地址不是由你任意指定的。而是根据输入的内容、作用范围等由TransformOutputProvider生成,比如,你要获取输出路径:

String dest = outputProvider.getContentLocation(directoryInput.name,directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)

Transform是一个抽象类,我们先自定义一个Transform,如下:

package com.davispluginsimport com.android.build.api.transform.*
import com.android.build.gradle.internal.pipeline.TransformManager
import org.apache.commons.codec.digest.DigestUtils
import org.apache.commons.io.FileUtilspublic class InsertTransform extends Transform {//设置我们自定义的Transform对应的Task名称@OverrideString getName() {return "DavisPlugin"}//指定输入的类型,通过这里设定,可以指定我们要处理的文件类型//这样确保其他类型的文件不会传入@OverrideSet<QualifiedContent.ContentType> getInputTypes() {return TransformManager.CONTENT_CLASS}//指定Transfrom的作用范围@OverrideSet<QualifiedContent.Scope> getScopes() {return TransformManager.SCOPE_FULL_PROJECT}@Overrideboolean isIncremental() {return false}@Overridevoid transform(Context context, Collection<TransformInput> inputs,Collection<TransformInput> referencedInputs,TransformOutputProvider outputProvider,boolean isIncremental) throws IOException,TransformException, InterruptedException {}
}

看到函数transform,我们还没有具体实现这个函数。这个函数就是具体如何处理输入和输出。可以运行一下看看,注意,这里的运行时直接编译执行我们的apk,而不是像之前那样直接rebuild,因为rebuild并没有执行到编译这一步。由于我们没有实现transform这个函数,导致没有输出!使得整个过程中断了!最终导致apk运行时找不到MainActivity,所以会报错。接下来我们去实现以下这个函数,我们啥也不干,就是把输入内容写入到作为输出内容,不做任何处理:

 @Overridevoid transform(Context context, Collection<TransformInput> inputs,Collection<TransformInput> referencedInputs,TransformOutputProvider outputProvider,boolean isIncremental) throws IOException, TransformException, InterruptedException {// Transform的inputs有两种类型,一种是目录,一种是jar包,要分开遍历inputs.each { TransformInput input ->//对类型为“文件夹”的input进行遍历input.directoryInputs.each { DirectoryInput directoryInput ->//文件夹里面包含的是我们手写的类以及R.class、BuildConfig.class以及R$XXX.class等// 获取output目录def dest = outputProvider.getContentLocation(directoryInput.name,directoryInput.contentTypes, directoryInput.scopes,Format.DIRECTORY)//这里执行字节码的注入,不操作字节码的话也要将输入路径拷贝到输出路径FileUtils.copyDirectory(directoryInput.file, dest)}//对类型为jar文件的input进行遍历input.jarInputs.each { JarInput jarInput ->//jar文件一般是第三方依赖库jar文件// 重命名输出文件(同目录copyFile会冲突)def jarName = jarInput.namedef md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())if (jarName.endsWith(".jar")) {jarName = jarName.substring(0, jarName.length() - 4)}//生成输出路径 + md5Namedef dest = outputProvider.getContentLocation(jarName + md5Name,jarInput.contentTypes, jarInput.scopes, Format.JAR)//这里执行字节码的注入,不操作字节码的话也要将输入路径拷贝到输出路径FileUtils.copyFile(jarInput.file, dest)}}}

注意input的类型,分为“文件夹”和“jar文件”,”文件夹”里面的就是我们写的类对应的class文件,jar文件一般为第三方库。此时,能成功运行,但是这里我们没有注入任何代码。

Transform类我们实现了,那么如何调用的呢?调用方式如下:

public class PluginImpl implements Plugin<Project>{void apply(Project project){def android = project.extensions.findByType(AppExtension);android.registerTransform(new InsertTransform())}
}

(2)监控每一个Task任务执行

在我们的工程目录中我们可以看到还有一个TaskListener.groovy类,内容如下:

package com.davispluginsimport org.gradle.BuildListener
import org.gradle.BuildResult
import org.gradle.api.Task
import org.gradle.api.execution.TaskExecutionListener
import org.gradle.api.initialization.Settings
import org.gradle.api.invocation.Gradle
import org.gradle.api.tasks.TaskStatepublic class TaskListener implements TaskExecutionListener, BuildListener {private static final String TAG = "[DAVIS] ";/*** 此类可以监控每一个task的执行开始和结束,以及工程build的情况*/public TaskListener(){}@Overridevoid beforeExecute(Task task) {println(TAG + "task before : " + task.getName())}/*** 比如,我们要在packageRelease这个task任务执行完后,做一些操作,* 我们就可以在此方法中判断* @param task* @param taskState*/@Overridevoid afterExecute(Task task, TaskState taskState) {println(TAG + "task after : " + task.getName())if(task.getName().equals("packageRelease")){//做自己的任务}}@Overridevoid buildFinished(BuildResult result) {//项目build完成之后,会调用此方法println(TAG + "build finished.")}@Overridevoid buildStarted(Gradle gradle) {println(TAG + "build started.")}@Overridevoid projectsEvaluated(Gradle gradle) {println(TAG + "project evaluated.")}@Overridevoid projectsLoaded(Gradle gradle) {println(TAG + "project loaded.")}@Overridevoid settingsEvaluated(Settings settings) {println(TAG + "setting evaluated.")}
}

调用方式:

public class PluginImpl implements Plugin<Project>{void apply(Project project){project.gradle.addListener(new TaskListener())}
}

这个类是做啥用的呢,此类可以用来监控每一个Task任务的执行情况,比如我们在打apk包的过程中,其实就是调用了一连串的Task任务。下面是我们在未使用插件的情况下打一个release包过程中Gradle Console输出的日志:

Executing tasks: [:app:assembleRelease]Configuration on demand is an incubating feature.
Incremental java compilation is an incubating feature.
:app:preBuild UP-TO-DATE
:app:preReleaseBuild UP-TO-DATE
:app:checkReleaseManifest
:app:prepareReleaseDependencies
:app:compileReleaseAidl UP-TO-DATE
:app:compileReleaseRenderscript UP-TO-DATE
:app:generateReleaseBuildConfig UP-TO-DATE
:app:mergeReleaseShaders UP-TO-DATE
:app:compileReleaseShaders UP-TO-DATE
:app:generateReleaseAssets UP-TO-DATE
:app:mergeReleaseAssets UP-TO-DATE
:app:generateReleaseResValues UP-TO-DATE
:app:generateReleaseResources UP-TO-DATE
:app:mergeReleaseResources UP-TO-DATE
:app:processReleaseManifest UP-TO-DATE
:app:processReleaseResources UP-TO-DATE
:app:generateReleaseSources UP-TO-DATE
:app:incrementalReleaseJavaCompilationSafeguard UP-TO-DATE
:app:compileReleaseJavaWithJavac UP-TO-DATE
:app:compileReleaseNdk UP-TO-DATE
:app:compileReleaseSources UP-TO-DATE
:app:lintVitalRelease
:app:prePackageMarkerForRelease
:app:transformClassesWithDexForRelease
To run dex in process, the Gradle daemon needs a larger heap.
It currently has approximately 1365 MB.
For faster builds, increase the maximum heap size for the Gradle daemon to more than 2048 MB.
To do this set org.gradle.jvmargs=-Xmx2048M in the project gradle.properties.
For more information see https://docs.gradle.org/current/userguide/build_environment.html
:app:mergeReleaseJniLibFolders UP-TO-DATE
:app:transformNative_libsWithMergeJniLibsForRelease UP-TO-DATE
:app:processReleaseJavaRes UP-TO-DATE
:app:transformResourcesWithMergeJavaResForRelease UP-TO-DATE
:app:validateExternalOverrideSigning
:app:packageRelease UP-TO-DATE
:app:zipalignRelease UP-TO-DATE
:app:assembleReleaseBUILD SUCCESSFULTotal time: 5.557 secs

那么我们使用了该插件之后输出的日志是怎样的那,如下:

Executing tasks: [:app:assembleRelease]Configuration on demand is an incubating feature.
Incremental java compilation is an incubating feature.
[DAVIS] project evaluated.
:app:preBuild
[DAVIS] task before : preBuild
:app:preBuild UP-TO-DATE
[DAVIS] task after : preBuild
:app:preReleaseBuild
[DAVIS] task before : preReleaseBuild
:app:preReleaseBuild UP-TO-DATE
[DAVIS] task after : preReleaseBuild
:app:checkReleaseManifest
[DAVIS] task before : checkReleaseManifest
[DAVIS] task after : checkReleaseManifest
:app:prepareReleaseDependencies
[DAVIS] task before : prepareReleaseDependencies
[DAVIS] task after : prepareReleaseDependencies
:app:compileReleaseAidl
[DAVIS] task before : compileReleaseAidl
:app:compileReleaseAidl UP-TO-DATE
[DAVIS] task after : compileReleaseAidl
:app:compileReleaseRenderscript
[DAVIS] task before : compileReleaseRenderscript
:app:compileReleaseRenderscript UP-TO-DATE
[DAVIS] task after : compileReleaseRenderscript
:app:generateReleaseBuildConfig
[DAVIS] task before : generateReleaseBuildConfig
:app:generateReleaseBuildConfig UP-TO-DATE
[DAVIS] task after : generateReleaseBuildConfig
:app:mergeReleaseShaders
[DAVIS] task before : mergeReleaseShaders
:app:mergeReleaseShaders UP-TO-DATE
[DAVIS] task after : mergeReleaseShaders
:app:compileReleaseShaders
[DAVIS] task before : compileReleaseShaders
:app:compileReleaseShaders UP-TO-DATE
[DAVIS] task after : compileReleaseShaders
:app:generateReleaseAssets
[DAVIS] task before : generateReleaseAssets
:app:generateReleaseAssets UP-TO-DATE
[DAVIS] task after : generateReleaseAssets
:app:mergeReleaseAssets
[DAVIS] task before : mergeReleaseAssets
:app:mergeReleaseAssets UP-TO-DATE
[DAVIS] task after : mergeReleaseAssets
:app:generateReleaseResValues
[DAVIS] task before : generateReleaseResValues
:app:generateReleaseResValues UP-TO-DATE
[DAVIS] task after : generateReleaseResValues
:app:generateReleaseResources
[DAVIS] task before : generateReleaseResources
:app:generateReleaseResources UP-TO-DATE
[DAVIS] task after : generateReleaseResources
:app:mergeReleaseResources
[DAVIS] task before : mergeReleaseResources
:app:mergeReleaseResources UP-TO-DATE
[DAVIS] task after : mergeReleaseResources
:app:processReleaseManifest
[DAVIS] task before : processReleaseManifest
:app:processReleaseManifest UP-TO-DATE
[DAVIS] task after : processReleaseManifest
:app:processReleaseResources
[DAVIS] task before : processReleaseResources
:app:processReleaseResources UP-TO-DATE
[DAVIS] task after : processReleaseResources
:app:generateReleaseSources
[DAVIS] task before : generateReleaseSources
:app:generateReleaseSources UP-TO-DATE
[DAVIS] task after : generateReleaseSources
:app:incrementalReleaseJavaCompilationSafeguard
[DAVIS] task before : incrementalReleaseJavaCompilationSafeguard
:app:incrementalReleaseJavaCompilationSafeguard UP-TO-DATE
[DAVIS] task after : incrementalReleaseJavaCompilationSafeguard
:app:compileReleaseJavaWithJavac
[DAVIS] task before : compileReleaseJavaWithJavac
:app:compileReleaseJavaWithJavac UP-TO-DATE
[DAVIS] task after : compileReleaseJavaWithJavac
:app:compileReleaseNdk
[DAVIS] task before : compileReleaseNdk
:app:compileReleaseNdk UP-TO-DATE
[DAVIS] task after : compileReleaseNdk
:app:compileReleaseSources
[DAVIS] task before : compileReleaseSources
:app:compileReleaseSources UP-TO-DATE
[DAVIS] task after : compileReleaseSources
:app:lintVitalRelease
[DAVIS] task before : lintVitalRelease
[DAVIS] task after : lintVitalRelease
:app:prePackageMarkerForRelease
[DAVIS] task before : prePackageMarkerForRelease
[DAVIS] task after : prePackageMarkerForRelease
:app:transformClassesWithDavisPluginForRelease
[DAVIS] task before : transformClassesWithDavisPluginForRelease
:app:transformClassesWithDavisPluginForRelease UP-TO-DATE
[DAVIS] task after : transformClassesWithDavisPluginForRelease
:app:transformClassesWithDexForRelease
[DAVIS] task before : transformClassesWithDexForRelease
To run dex in process, the Gradle daemon needs a larger heap.
It currently has approximately 1365 MB.
For faster builds, increase the maximum heap size for the Gradle daemon to more than 2048 MB.
To do this set org.gradle.jvmargs=-Xmx2048M in the project gradle.properties.
For more information see https://docs.gradle.org/current/userguide/build_environment.html
[DAVIS] task after : transformClassesWithDexForRelease
:app:mergeReleaseJniLibFolders
[DAVIS] task before : mergeReleaseJniLibFolders
:app:mergeReleaseJniLibFolders UP-TO-DATE
[DAVIS] task after : mergeReleaseJniLibFolders
:app:transformNative_libsWithMergeJniLibsForRelease
[DAVIS] task before : transformNative_libsWithMergeJniLibsForRelease
:app:transformNative_libsWithMergeJniLibsForRelease UP-TO-DATE
[DAVIS] task after : transformNative_libsWithMergeJniLibsForRelease
:app:processReleaseJavaRes
[DAVIS] task before : processReleaseJavaRes
:app:processReleaseJavaRes UP-TO-DATE
[DAVIS] task after : processReleaseJavaRes
:app:transformResourcesWithMergeJavaResForRelease
[DAVIS] task before : transformResourcesWithMergeJavaResForRelease
:app:transformResourcesWithMergeJavaResForRelease UP-TO-DATE
[DAVIS] task after : transformResourcesWithMergeJavaResForRelease
:app:validateExternalOverrideSigning
[DAVIS] task before : validateExternalOverrideSigning
[DAVIS] task after : validateExternalOverrideSigning
:app:packageRelease
[DAVIS] task before : packageRelease
:app:packageRelease UP-TO-DATE
[DAVIS] task after : packageRelease
:app:zipalignRelease
[DAVIS] task before : zipalignRelease
:app:zipalignRelease UP-TO-DATE
[DAVIS] task after : zipalignRelease
:app:assembleRelease
[DAVIS] task before : assembleRelease
[DAVIS] task after : assembleReleaseBUILD SUCCESSFULTotal time: 3.6 secs
[DAVIS] build finished.

从上面的日志我们可以看出,我们可以在项目打包前、某个Task任务执行前或执行后以及整个项目打包完成后来做自己想做的事了。

GitHub源码地址:https://github.com/881205wzs/GradlePluginDemo
源码下载地址:https://download.csdn.net/download/wangzhongshun/11010210

Android Studio 自定义Gradle Plugin相关推荐

  1. 自定义Gradle Plugin

    1. 前言 自定义Gradle Plugin使用的是Groovy语言,和Java很像,很容易理解,不做过多的语言介绍. 2. Gradle Plugin的位置 直接在构建文件build.gradle中 ...

  2. 利用 Android Studio 和 Gradle 打包多版本APK

    视频汇总首页:http://edu.51cto.com/lecturer/index/user_id-4626073.html ==================================== ...

  3. AS 自定义 Gradle plugin 插件 案例 MD

    Markdown版本笔记 我的GitHub首页 我的博客 我的微信 我的邮箱 MyAndroidBlogs baiqiantao baiqiantao bqt20094 baiqiantao@sina ...

  4. android studio try again,完美解决Android Studio在gradle上的各种问题

    原标题:完美解决Android Studio在gradle上的各种问题 题记: 看到很多人都来读这篇文章,说明很多人都有遇到这个问题,文章质量不是很高,感觉我自己都有些看不懂了,因此来更新一下,希望可 ...

  5. Android studio 和 gradle插件版本对应关系(最新 2022年)

    官网链接:Android Gradle 插件版本说明  |  Android 开发者  |  Android Developers 由于网络的问题,在更新了Android studio版本之后,不能同 ...

  6. 拥抱 Android Studio 之二:Android Studio 与 Gradle 深入

    2019独角兽企业重金招聘Python工程师标准>>> 关于学习方式 曾经跟朋友讨论过我们所接受过的大学工科教育,都是一上来先学基础理论,最后再来一个金工实习.一开始不知道为什么而学 ...

  7. android+自定义皮肤,android studio自定义更换皮肤详细图文教程

    android studio这款app程序开发软件内也内置了多种皮肤主题,程序开发人员如果感觉一种皮肤太过单调乏味,可以选择使用软件内的其他皮肤风格,软件默认的皮肤是IntelliJ,还有黑色的Dra ...

  8. Android Studio自定义视图无法预览

    Android Studio自定义视图没有办法预览 我想大家应该都和我一样,如果看到布局的编码的时候如果右边能够非常直观地显示出对应的视图,心里会非常舒心,像官方提供的tools命名空间就是为了这个目 ...

  9. mac下Android studio配置gradle的路径

    2019独角兽企业重金招聘Python工程师标准>>> 最详细的mac下Android studio配置gradle的路径 转载于:https://my.oschina.net/sh ...

最新文章

  1. manacher算法----O(n)最长回文串
  2. python 判断中文标点符号_Python入门编程题库27--生成随机密码
  3. Python os.path路径模块中的操作方法总结
  4. 子查询dinstinct放哪_第四关 复杂查询
  5. 随机森林做特征重要性排序和特征选择
  6. jzoj3379-查询【主席树】
  7. Redis缓存,你真的懂了吗
  8. python绘制社会关系网络图_Python networkx 网络图绘制
  9. gradle 构建测试
  10. cenos安装erlang
  11. 拓端tecdat|R语言无监督学习:PCA主成分分析可视化
  12. redis安装与指标监控
  13. PHPWord通过docx模板替换标签,最终生成pdf文件
  14. 【福利】更新电脑游戏安装合集,百款大型单机游戏,百度网盘临时会员领取方法...
  15. 怼天怼地的马斯克道歉了?
  16. MySQL基础篇(上)
  17. cmd 新增dns_cmd修改DNS,以及DNS大全
  18. 【微机原理】数字电路器件—门 与门 或门 非门电路及实例
  19. Excel-VBA:“银行家舍入” 与“国际标准的四舍五入”
  20. push后再git status出现Your branch is ahead of ‘xxx‘ by 1 commit.

热门文章

  1. ENVI计算公式(一)
  2. 【转】win32窗口的大小,居中,拖动
  3. CSDN改版,找不到各种入口,链接放下面
  4. SharePoint安全 - SharePoint网站常用页面URL索引
  5. java 无法执行export 命令_模块中的export、import以及复合模式的使用方法
  6. 【Python CheckiO 题解】The Warriors
  7. 完美解决 bash: hexo: command not found
  8. 项目总结2:ionic3开发跨平台App如何设置和替换应用图标及启动图
  9. 莆田学院计算机科学与技术分数,莆田学院录取分数线2021是多少分(附历年录取分数线)...
  10. oracle导出建表主键,oracle主键自动生成 配合hibernate的生成策略详解