創(chuàng)建和發(fā)布Android開發(fā)庫的終極指南
我經(jīng)常被 android 開發(fā)社區(qū)中如此多而且好用的第三方庫所震驚。有很長的一段時(shí)間,我想貢獻(xiàn)一些東西,但我不知道如何去做。在瀏覽了其他很多關(guān)于如何發(fā)布一個(gè) android 開發(fā)庫的文章后,我仍然發(fā)現(xiàn)缺失了一些細(xì)節(jié),而且,所有的信息都是在不同的地方。所以,我將完整的走過這個(gè)過程,向大家展示我的做法。
對(duì)新手來說,我推薦使用 Android Studio 來創(chuàng)建所有的 Android 項(xiàng)目,Android Studio官方使用 Gradle 構(gòu)建系統(tǒng)。請(qǐng)確保你下載了 Android Studio 的最新版。
相關(guān)術(shù)語介紹
在我們開始之前,還有一些術(shù)語,需要熟悉下。
項(xiàng)目(Project) — 在 Android Studio 中,一個(gè) 項(xiàng)目 就是一個(gè)完整的 Android app。Android Studio 項(xiàng)目包含了一個(gè)或多個(gè)模塊。 在 Android Studio 中,一個(gè) 項(xiàng)目 類似于在 Eclipse 的一個(gè)工作區(qū)間( workspace )。
模塊( Module) – 一個(gè) 模塊 是 app 中的一個(gè)組件,它可以單獨(dú)的進(jìn)行構(gòu)建、測試和調(diào)試。模塊包含了 app 的源代碼和資源文件。在 Android Studio 中,一個(gè) 模塊 類似于在 Eclipse 的一個(gè)項(xiàng)目。
AAR – ‘aar’ 套件是 Android 開發(fā)庫項(xiàng)目的二進(jìn)制的分發(fā)形式。(AAR 格式)開發(fā)庫項(xiàng)目的主要產(chǎn)出就是 .aar 包(意思是 Android 壓縮包)。它是由編譯后的代碼(如 jar 文件或者 .so 文件)和資源文件(如 manifest 文件、res 文件、asset 文件)組合而成的。
Maven 中央倉庫 – 由 Maven 社區(qū)提供的倉庫。它包含了很多我們常用的開發(fā)庫。 Search Maven 網(wǎng)站可用來瀏覽 maven 中央倉庫的內(nèi)容。Gradle, Please 網(wǎng)站是另一個(gè)可用來搜索中央倉庫的工具。如果你在項(xiàng)目配置文件的倉庫配置部分添加了 jCenter() ,那么 Gradle 將使用 jCenter 倉庫( jCenter 的說明)。Maven 中央倉庫也經(jīng)常被稱作 Maven 中心或者中央倉庫。
Sonatype — Sonatype的開源軟件倉庫托管(OSSRH)服務(wù)是項(xiàng)目作者和貢獻(xiàn)者們發(fā)布他們的組件到中央倉庫的主要途徑。它是 Sonatype Nexus Professional 組織利用 Nexus Staging Suite 工具,對(duì)開源項(xiàng)目提供部署托管服務(wù),該服務(wù)主要用來處理部署和驗(yàn)證操作,也提供同步操作將內(nèi)容通過網(wǎng)絡(luò)投遞到中央倉庫。
GPG – GNU 隱私保護(hù)組織 (也稱為 GPG 或者 GnuPG),這個(gè) GNU 項(xiàng)目是一個(gè)加密軟件,遵循 OpenPGP (RFC4880)標(biāo)準(zhǔn),是 PGP 的免費(fèi)替代品。使用 GPG 你可以加密(解密)包含敏感數(shù)據(jù)的文件,比如那些由健康保險(xiǎn)攜帶和責(zé)任法案 (HIPAA) 制定的受保護(hù)的隱私和安全方面的電子健康信息。想了解 GPG 的更多信息,請(qǐng)?jiān)L問 GNU Privacy Guard website。
準(zhǔn)備好你的 Android 開發(fā)庫
我將使用我的 Trestle 開發(fā)庫作例子來講解。在你的項(xiàng)目中,需要修改一些地方,來準(zhǔn)備作為一個(gè)開發(fā)庫發(fā)布到 Maven 中央倉庫中。
- 將開發(fā)庫的核心代碼和示例代碼區(qū)分開來。在我的項(xiàng)目中,我將他們分成 library 和 sample 兩個(gè)模塊。請(qǐng)仔細(xì)閱讀關(guān)于創(chuàng)建一個(gè)開發(fā)庫模塊的技巧。你也可能需要重命名你的模塊。
- 在 sample 模塊的 build.gradle 文件中,請(qǐng)確保包含以下內(nèi)容:
- apply plugin: 'com.android.application'
- dependencies {
- compile project(':library')
- }
- 在 library 模塊的 build.gradle 文件中,請(qǐng)確保包含以下內(nèi)容:
- apply plugin: 'com.android.library'
- apply from: 'maven-push.gradle'
- 在 library 模塊中,增加 gradle.properties 文件,請(qǐng)確保在該文件中包含以下內(nèi)容:
- POM_NAME=ProjectName
- POM_ARTIFACT_ID=projectname
- POM_PACKAGING=aar
- 在 library 模塊中,增加 maven-push.gradle 文件,請(qǐng)確保在該文件中包含以下內(nèi)容:
- /*
- * Copyright 2013 Chris Banes
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- apply plugin: 'maven'
- apply plugin: 'signing'
- def isReleaseBuild() {
- return VERSION_NAME.contains("SNAPSHOT") == false
- }
- def getReleaseRepositoryUrl() {
- return hasProperty('RELEASE_REPOSITORY_URL') ? RELEASE_REPOSITORY_URL
- : "https://oss.sonatype.org/service/local/staging/deploy/maven2/"
- }
- def getSnapshotRepositoryUrl() {
- return hasProperty('SNAPSHOT_REPOSITORY_URL') ? SNAPSHOT_REPOSITORY_URL
- : "https://oss.sonatype.org/content/repositories/snapshots/"
- }
- def getRepositoryUsername() {
- return hasProperty('NEXUS_USERNAME') ? NEXUS_USERNAME : ""
- }
- def getRepositoryPassword() {
- return hasProperty('NEXUS_PASSWORD') ? NEXUS_PASSWORD : ""
- }
- afterEvaluate { project ->
- uploadArchives {
- repositories {
- mavenDeployer {
- beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) }
- pom.groupId = GROUP
- pom.artifactId = POM_ARTIFACT_ID
- pom.version = VERSION_NAME
- repository(url: getReleaseRepositoryUrl()) {
- authentication(userName: getRepositoryUsername(), password: getRepositoryPassword())
- }
- snapshotRepository(url: getSnapshotRepositoryUrl()) {
- authentication(userName: getRepositoryUsername(), password: getRepositoryPassword())
- }
- pom.project {
- name POM_NAME
- packaging POM_PACKAGING
- description POM_DESCRIPTION
- url POM_URL
- scm {
- url POM_SCM_URL
- connection POM_SCM_CONNECTION
- developerConnection POM_SCM_DEV_CONNECTION
- }
- licenses {
- license {
- name POM_LICENCE_NAME
- url POM_LICENCE_URL
- distribution POM_LICENCE_DIST
- }
- }
- developers {
- developer {
- id POM_DEVELOPER_ID
- name POM_DEVELOPER_NAME
- }
- }
- }
- }
- }
- }
- signing {
- required { isReleaseBuild() && gradle.taskGraph.hasTask("uploadArchives") }
- sign configurations.archives
- }
- //task androidJavadocs(type: Javadoc) {
- //source = android.sourceSets.main.allJava
- //}
- //task androidJavadocsJar(type: Jar, dependsOn: androidJavadocs) {
- //classifier = 'javadoc'
- //from androidJavadocs.destinationDir
- //}
- task androidSourcesJar(type: Jar) {
- classifier = 'sources'
- from android.sourceSets.main.java.sourceFiles
- }
- artifacts {
- archives androidSourcesJar
- }
- }
- 修改在項(xiàng)目根目錄的 .gitignore 文件
- # [Android] ========================
- # Built application files
- *.apk
- *.ap_
- # Files for the Dalvik VM
- *.dex
- # Java class files
- *.class
- # Generated files
- bin/
- gen/
- # Gradle files
- .gradle/
- build/
- # Local configuration file (sdk path, etc)
- local.properties
- # Proguard folder generated by Eclipse
- proguard/
- # Log Files
- *.log
- ## Directory-based project format:
- .idea/
- ## File-based project format:
- *.ipr
- *.iws
- ## Plugin-specific files:
- # IntelliJ
- out/
- # mpeltonen/sbt-idea plugin
- .idea_modules/
- # JIRA plugin
- atlassian-ide-plugin.xml
- # Crashlytics plugin (for Android Studio and IntelliJ)
- com_crashlytics_export_strings.xml
- # [Maven] ========================
- target/
- pom.xml.tag
- pom.xml.releaseBackup
- pom.xml.versionsBackup
- pom.xml.next
- release.properties
- # [Gradle-Android] ========================
- # Ignore Gradle GUI config
- gradle-app.setting
- # Gradle Signing
- signing.properties
- trestle.keystore
- # Mobile Tools for Java (J2ME)
- .mtj.tmp/
- # Package Files #
- *.jar
- *.war
- *.ear
- # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
- hs_err_pid*
- # Misc
- /.idea/workspace.xml
- .DS_Store
- /captures
- **/*.iml
- *.class
- 修改在項(xiàng)目根目錄的 settings.gradle 文件
- include ':sample', ':library'
- 修改在項(xiàng)目根目錄的 gradle.properties 文件
- # Project-wide Gradle settings.
- # IDE (e.g. Android Studio) users:
- # Gradle settings configured through the IDE *will override*
- # any settings specified in this file.
- # For more details on how to configure your build environment visit
- # http://www.gradle.org/docs/current/userguide/build_environment.html
- # Specifies the JVM arguments used for the daemon process.
- # The setting is particularly useful for tweaking memory settings.
- # Default value: -Xmx10248m -XX:MaxPermSize=256m
- # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
- # When configured, Gradle will run in incubating parallel mode.
- # This option should only be used with decoupled projects. More details, visit
- # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
- # org.gradle.parallel=true
- VERSION_NAME=0.0.1
- VERSION_CODE=1
- GROUP=com.github.github_username
- POM_DESCRIPTION=A library that does X, Y, and Z
- POM_URL=https://github.com/github_username/ProjectName
- POM_SCM_URL=https://github.com/github_username/ProjectName
- POM_SCM_CONNECTION=scm:git@github.com:github_username/ProjectName.git
- POM_SCM_DEV_CONNECTION=scm:git@github.com:github_username/ProjectName.git
- POM_LICENCE_NAME=The Apache Software License, Version 2.0
- POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt
- POM_LICENCE_DIST=repo
- POM_DEVELOPER_ID=github_username
- POM_DEVELOPER_NAME=GitHub FullName
- 增加 README.md 文件,向其他開發(fā)者介紹你的開發(fā)庫以及如何使用它。如果你想在你的 README.md 文件中增加些截圖,我極力推薦一款叫做Screenr的app。
安裝 GPG
如果你的機(jī)器上還沒安裝 GPG,你需要下載安裝它。如果你是 MacOSX 系統(tǒng),安裝手冊(cè)在這里。
如果你從未使用過 GPG – 首先,請(qǐng)創(chuàng)建 GPG 密鑰:
- $ gpg --gen-key
在你創(chuàng)建 GPG 密鑰的時(shí)候,如果你不確定該如何回答問題, 這篇指南(Creating an encryption key)可以幫上忙。
接下來,找到你的密鑰 ID:
- $ gpg --list-keys
第一行像是 pub XXXXX/YYYYYYYY <日期>的。切記,’YYYYYYYY’ 部分,就是你的密鑰 ID。
現(xiàn)在,發(fā)布你的密鑰:
- $ gpg --keyserver hkp://keyserver.ubuntu.com --send-keys YYYYYYYY
- $ gpg --keyserver hkp://pgp.mit.edu --send-keys YYYYYYYY
你當(dāng)然也可以使用其他密鑰服務(wù)器,你也可以通過如下命令確認(rèn)你的密鑰是否已經(jīng)發(fā)布:
- $ gpg --keyserver hkp://pgp.mit.edu --search-keys johndoe@example.com # 使用你的郵箱
為了使你的開發(fā)庫在 Gradle, Please網(wǎng)站上列出(也為了其他人方便的引用你的開發(fā)庫),請(qǐng)上傳你的項(xiàng)目到 Maven Central。 最簡單的上傳項(xiàng)目的方法是使用 Sonatype。
Sonatype
- 在Sonatype 創(chuàng)建一個(gè) JIRA 帳號(hào)。
- 登錄成功后,創(chuàng)建一個(gè) new issue。
我為我的 Trestle 項(xiàng)目創(chuàng)建了一個(gè) GitHub 倉庫。所以我在 new issue 上填寫的字段大概如此:
Group Id : com.github.<github_username>
Project URL : https://github.com/<github_username>/<project_name>
SCM url : https://github.com/<github_username>/<project_name>.git
Username : <sonatype_username>
Already Synced to Central : No
注意:我在你需要填寫的字段上增加了括號(hào)作為占位符。你需要將它們替換成合適的值。
當(dāng)你準(zhǔn)備提交 issue 的時(shí)候,issue 的細(xì)節(jié)應(yīng)該要和上面的截圖差不多。當(dāng)你提交完成后,Sonatype 將用 2 個(gè)工作日來處理你的 issue。接著,你將收到一份確認(rèn)郵件,告知你的配置已經(jīng)準(zhǔn)備好了,你可以發(fā)布你的開源庫了。
不要部署你的開源庫,直到你接收到一封表明你的票據(jù)已經(jīng) OK 了的郵件。 對(duì)新項(xiàng)目來說,一個(gè)通病就是過早的部署。這將會(huì)誤使你的構(gòu)件(artifacts)變成一個(gè)人人都能獲得的倉庫。
最后,如果你的組件已經(jīng)在中央倉庫中了,請(qǐng)?jiān)谀愕钠睋?jù)中添加以下信息,并參考這篇文章,如何遷移到 OSSRH。
修改你本機(jī)上的 ~/.gradle/gradle.properties 文件,包含以下內(nèi)容:
- NEXUS_USERNAME=sonatype_username
- NEXUS_PASSWORD=sonatype_password
- signing.keyId=gpg_key_id
- signing.password=gpg_password
- signing.secretKeyRingFile=/Users/username/.gnupg/secring.gpg
- org.gradle.daemon=true
當(dāng)你發(fā)布開發(fā)庫的時(shí)候,身份認(rèn)證信息已經(jīng)在 gradle.properties 文件中提供了。請(qǐng)確保提供了正確的 nexus 用戶名和密碼(也就是 Sonatype 的用戶名和密碼),否則你將得到未授權(quán)的 401 錯(cuò)誤。
注意:如果你之前已經(jīng)發(fā)布過一個(gè)開發(fā)庫,那么你不需要在 JIRA(Sonatype) 上創(chuàng)建一個(gè)新的 issue。每個(gè)頂級(jí)的 groupId 對(duì)應(yīng)一個(gè) JIRA issue。你應(yīng)該已經(jīng)有了部署任何新構(gòu)件到你的 groupId 或者下屬 group 應(yīng)需要的所有權(quán)限。中央倉庫的同步操作遵循從上到下的過程,所以任何下屬組的發(fā)布版本都將會(huì)自動(dòng)同步。當(dāng)你發(fā)布新組件,你不需要告訴 Sonatype,因?yàn)楫?dāng)你進(jìn)行倉庫同步工作的時(shí)候,沒有什么需要 Sonatype 去配置或者檢查的, Sonatype 僅僅會(huì)在第一次進(jìn)行同步操作的時(shí)候,發(fā)布一條 twitter。
發(fā)布
一旦你準(zhǔn)備發(fā)布你的開發(fā)庫,在 Android Studio 中, 打開右側(cè)的 Gradle 視圖,在 Tasks > upload 下,點(diǎn)擊 uploadArchives,將會(huì)上傳你的開發(fā)庫到 Sonatype Staging Repositories。
在 Sonatype Staging Repositories 網(wǎng)頁上,登陸你的 Sonatype 賬號(hào),查找你的 “staging” 開發(fā)庫,它應(yīng)該在列表的最后,選中它,并按下 “關(guān)閉” 按鈕。關(guān)閉一個(gè)開發(fā)庫實(shí)際上意味著你準(zhǔn)備發(fā)布它。如果關(guān)閉操作一切順利 – 你應(yīng)該會(huì)看到一個(gè)激活了的 ‘發(fā)布’ 按鈕。你可能需要刷新下頁面。請(qǐng)按下發(fā)布按鈕。請(qǐng)閱讀關(guān)于使用 Nexus 管理倉庫的幫助文檔。如果這是你發(fā)布的第一個(gè)開發(fā)庫,返回到 JIRA,在 JIRA 上發(fā)表一條你已經(jīng)改進(jìn)了你的開發(fā)庫的評(píng)論,如果不是第一個(gè),就沒必要告訴 Sonatype 你改進(jìn)了你的開發(fā)庫。然后,你應(yīng)該會(huì)收到來自 Sonatype 的一條回復(fù)信息,信息上說你的開發(fā)庫在 10 分鐘內(nèi)能準(zhǔn)備好,將會(huì)在接下來的幾個(gè)小時(shí)同步到 Maven 中央倉庫。幾個(gè)小時(shí)之后,它將展示在 Gradle, Please 網(wǎng)站上。
使用你的開發(fā)庫
對(duì)其他開發(fā)者來說,想要使用你的開發(fā)庫,他們需要在 Android 項(xiàng)目的 build.gradle 文件中添加一條依賴, 如下所示:
- apply plugin: 'android'
- dependencies {
- compile 'com.github.lawloretienne:trestle:0.0.3'
- }
特別感謝
非常感謝 Chris Banes,Jonathan Le,Serge Zaitsev 以及其他發(fā)表博客的人們,你們的文章幫助我得以走過這個(gè)復(fù)雜的過程。
我的開發(fā)庫
QuickReturn — https://github.com/lawloretienne/QuickReturn
Trestle — https://github.com/lawloretienne/Trestle
ImageGallery — https://github.com/lawloretienne/ImageGallery