写在前头
- 本文编辑于2016年10月2日,请注意技术的时效性
- 请带着批判的态度阅读
- 项目工程地址:
读完本文的收获
本文将假设你是入门安卓不久的新手,引导你获取版本规范、代码规范、签名生成以及手动发版的流程。
丑陋的目录
开发环境
工具 | 版本 |
---|---|
Window 10 | |
2.2 | |
2.14.1 | |
1.8.0 | |
2.9.0.windows.1 | |
2.2.0.0 | |
2.5.2 |
1. 项目初始化
1.1 项目构建
项目的构建过程还是很简单的,打开我们的Android Studio,如图1.1.1所示:
先去设置代理,有助于翻墙,点选Configure,在下拉菜单中选择Settings,在弹出的设置界面,进行如图1.1.2所示的配置(作者使用的是,代理端口为1080,请根据具体的VPN自行配置):
1.1.2 代理配置
设置完毕后,返回主界面,选择Start a new Android Studio project选项卡,填写相应的数据信息,如图1.1.3所示:
1.1.3 创建一个新项目
OK这个名字是Dota里面全能骑士的简称,不过无所谓了,继续下一步是设置最小支持版本,如图1.1.4所示:
为什么要设置4.0以上呢,因为4.0以上的机器已经占据了97.4%,现在除非是一些大厂,其他很多应用已经不支持2.3以下了。后续的操作,因为不影响开发,一路next即可,最后完成时,会弹出代理设置,也点选OK即可,如图1.1.5所示。
1.1.5 代理设置
1.2 版本规范
1.2.1 生成.gitignore文件做完这些操作以后,创建第一次提交,如图1.2.2所示:
1.2.2 代码提交
其后的分支管理规则可以根据以下制定:
- master分支,稳定发布的分支,代表最后一次稳定的全量发版分支。
- develop分支,表示开发分支,用于代码合并、回滚,代码审查。
- release分支,发布分支,往往伴随tag标签一起标注。
- 其他分支,可以用版本区分,主要用于开发。
作为技术主管,只要管控好master,develop,release分支即可。
2. 开发之前
2.1 生成签名
第一步,进入Android Studio项目界面,Build->Generate Signed APK... 如图2.1.1所示:
第二步,填写相应的信息,如图2.1.2所示:
2.1.2 填写信息
需要注意的是,一般需要生成两个签名,一个用于debug,一个用于release;组内成员使用debug版本,release签名由技术主管之类的管控。
2.2 代码规范
2.2.1 查找插件在弹出的对话框中,搜索CheckStyle,并点击Install安装,如图2.2.2所示
2.2.2 安装插件
安装完毕后,按照操作提示,需要重启AS,如图2.2.3所示:
2.2.3 重启AS
<br />
接下来,添加规则,打开设置界面,进行设置,如图2.2.4所示:
2.2.4 设置代码规范
设置完成后,就可以在工程中使用了,有什么好处呢?举个例子来说,如图2.2.5所示:
2.2.5 一个例子
不规范的地方会出现高亮提示,同时鼠标放上去会有提示语,你也可以通过CheckStyle窗口来查看,在代码区域,右键,点击Check Current File,如图2.2.6所示:
2.2.6 Check Current File
那么在CheckStyle选项卡中就能看到关联信息了,如图2.2.7所示:
2.2.7 CheckStyle选项卡
然后还要设置CodeStyle,在设置页面,如图2.2.8所示:
2.2.8 CodeStyle
导入之前的CheckStyle.xml到CodeStyle,如图2.2.9所示:
2.2.9导入CodeStyle
最后,我们需要设置文件头,在设置界面操作,如图2.2.10所示:
2.2.10 设置文件头
别忘了,将关联的资料备份到工程中,如图2.2.11所示:
2.2.11 备份资料
2.3 Gradle配置
Gradle中配置有以下需要注意的事项:
- 将全局变量、密钥等抽取出来
- 区分debug版本与release版本,使其可以共存在同一个手机上
- 代码、资源开启混淆
这一部分工作,主要参考了以下链接:
参见详细代码如下:
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.2.0'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
// 配置全局变量
ext {
compileSdkVersion = 24
buildToolsVersion = "24.0.0"
minSdkVersion = 14
targetSdkVersion = 24
versionCode = 1
versionName = "1.0"
}
apply plugin: 'com.android.application'
android {
compileSdkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
defaultConfig {
applicationId "com.example.whr.ok"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode rootProject.ext.versionCode
versionName rootProject.ext.versionName
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
// 设置应用名称,注意删除res目录下的app_name
resValue("string","app_name","OK")
}
// lint 检查出错后 不停止构建任务
lintOptions {
abortOnError false
}
signingConfigs {
sign {
// 默认local.properties不加入版本控制中
Properties properties = new Properties()
properties.load(project.rootProject.file('local.properties').newDataInputStream())
def keystore = properties.getProperty("KEY_STORE")
def storepasswd = properties.getProperty("KEY_STORE_PASSWORD")
def alias = properties.getProperty("KEY_ALIAS")
def aliaspasswd = properties.getProperty("KEY_ALIAS_PASSWORD")
storeFile file(keystore)
storePassword(storepasswd)
keyAlias alias
keyPassword aliaspasswd
}
}
buildTypes {
debug {
// 读取设置的签名文件
signingConfig signingConfigs.sign
// 不进行代码优化
minifyEnabled false
// 不进行zip对齐
zipAlignEnabled false
// 为包名添加后缀,使调试与正式包可以并存
applicationIdSuffix ".debug"
// 设置应用名称,注意删除res目录下的app_name
resValue("string","app_name","Test_" + new Date().format("MM-dd HH:mm"))
// 在调试包中禁用lint
project.gradle.startParameter.excludedTaskNames.add("lint")
}
release {
// 读取设置的签名文件
signingConfig signingConfigs.sign
minifyEnabled true
shrinkResources true
proguardFiles 'proguard-rules.pro'
}
android.applicationVariants.all { variant ->
variant.assemble.doLast {
String outname = "";
variant.productFlavors.each { productFlavor ->
println("productFlavor:" + productFlavor.name);
outname += productFlavor.name;
}
println("buildType:" + variant.buildType.name);
// 是正式版本才进行资源混淆
if(variant.buildType.name != 'release') {
return;
}
variant.outputs.each { output ->
// 注释以下一行可以取消自动资源混淆的方法
runResourceGuard(output.outputFile, outname);
}
}
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile 'com.android.support:appcompat-v7:24.2.1'
testCompile 'junit:junit:4.12'
}
/** 执行资源混淆的方法*/
void runResourceGuard(File outFile, String outName) {
String finalOutDir = outFile.getParentFile().getAbsolutePath();
String name = getFileNameWithoutExt(outFile.getName());
if(outName == null || "".equals(outName)) {
outName = name;
}
String outDirPath = "build/outputs/resguard/" + outName;
println("输出目录:" + outDirPath);
File outDir = file(outDirPath);
if(outDir.exists()) {
delete(outDir.getAbsolutePath());
}
outDir.mkdirs();
//读取签名信息
String storeFile = android.signingConfigs.sign.storeFile.getAbsolutePath();
String storePassword = android.signingConfigs.sign.storePassword;
String keyAlias = android.signingConfigs.sign.keyAlias;
String keyPassword = android.signingConfigs.sign.keyPassword;
println("签名:" + storeFile + " " + storePassword + " " + keyAlias + " " + keyPassword);
String commandExt = isWindowsOS() ? ".exe" : "";
String zipalignPath = android.getSdkDirectory().getAbsolutePath() + "/build-tools/" + android.buildToolsVersion + "/zipalign" + commandExt;
println("zipalignPath=" + zipalignPath);
//执行资源混淆方法
def exit = javaexec {
main = "-jar"
args = [
"../resources/AndResGuard/resourcesproguard.jar",
outFile.getAbsolutePath(),
"-config", "../resources/AndResGuard/config.xml",
"-out", outDirPath,
"-7zip", "../resources/command/7za" + commandExt,
"-zipalign", zipalignPath,
"-mapping", "../resources/AndResGuard/resource_mapping.txt",
"-signature", storeFile, storePassword, keyPassword, keyAlias
].toList()
}
println("运行结果:" + exit);
if(exit.exitValue != 0) {
return;
}
//将混淆后apk包替换原来的apk包
name += "_signed_aligned.apk";
File outApkFile = new File(outDir, name);
outFile.delete();
if(!outApkFile.exists()) {
println("资源混淆文件失败!删除原文件!");
} else {
//自定义输出的文件名,如 demo_100_1.0.0.apk
String outputname = "OK_" + android.defaultConfig.versionCode + "_" + android.defaultConfig.versionName;
String tempExt = "";
int count = 0;
while(true) {
File temp = new File(finalOutDir, outputname + tempExt + ".apk");
if(!temp.exists()) {
break;
}
tempExt = "(" + (++count) + ")";
}
outputname += tempExt + ".apk";
println("最终文件名为:" + outputname);
copy {
from(outDirPath) {
include name
}
into finalOutDir
rename(name, outputname)
}
}
}
/**
* <p>获取文件名,不带扩展名</p>
* 如path=/sdcard/image.jpg --> image
*
* @param filePath
* @return
*/
String getFileNameWithoutExt(String filePath) {
if (filePath == null || "".equals(filePath)) {
return null;
}
int last = filePath.lastIndexOf("/");
int index = filePath.lastIndexOf(".");
if (last == -1 && index == -1) {
return filePath;
} else if (index > last) {
return filePath.substring(last + 1, index);
} else {
return filePath.substring(last);
}
}
/**
* @return true---是Windows操作系统
*/
boolean isWindowsOS(){
boolean isWindowsOS = false;
String osName = System.getProperty("os.name");
println("os.name=" + osName);
if(osName == null || "".equals(osName)) {
return false;
}
if(osName.toLowerCase().indexOf("windows") > -1){
isWindowsOS = true;
}
return isWindowsOS;
}
至于local.properties文件,它需要添加如下代码:
KEY_STORE=../resources/debug.jks
KEY_STORE_PASSWORD=123456
KEY_ALIAS=debug
KEY_ALIAS_PASSWORD=123456
2.4 混淆配置
这一部分工作,主要参考了以下链接:
##############################################通用配置##############################################
## 常规不用混淆的类
-keep public class * extends android.app.Fragment
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Application
-keep public class * extends android.app.Service
-keep public class * extends android.content.BroadcastReceiver
-keep public class * extends android.preference.Preference
-keep public class * extends android.content.ContentProvider
# 保留继承的
-keep public class * extends android.support.v4.**
-keep public class * extends android.support.v7.**
-keep public class * extends android.support.annotation.**
-dontwarn android.support.v4.**
# 保留support下的所有类及其内部类
-keep class android.support.** {*;}
# 不做预校验,preverify是proguard的四个步骤之一,Android不需要preverify,去掉这一步能够加快混淆速度。
-dontpreverify
# 保留Annotation不混淆
-keepattributes *Annotation*,InnerClasses
# 避免混淆泛型
-keepattributes Signature
# 抛出异常时保留代码行号
-keepattributes SourceFile,LineNumberTable
# 保留本地native方法不被混淆
-keepclasseswithmembernames class * {
native <methods>;
}
# 保留在Activity中的方法参数是view的方法,
# 这样以来我们在layout中写的onClick就不会被影响
-keepclassmembers class * extends android.app.Activity{
public void *(android.view.View);
}
# 保留枚举类不被混淆
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
# 保留Parcelable序列化类不被混淆
-keep class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator *;
}
# 保留Serializable序列化的类不被混淆
-keepclassmembers class * implements java.io.Serializable {
static final long serialVersionUID;
private static final java.io.ObjectStreamField[] serialPersistentFields;
!static !transient <fields>;
!private <fields>;
!private <methods>;
private void writeObject(java.io.ObjectOutputStream);
private void readObject(java.io.ObjectInputStream);
java.lang.Object writeReplace();
java.lang.Object readResolve();
}
# webView处理,项目中没有使用到webView忽略即可
-keepclassmembers class fqcn.of.javascript.interface.for.webview {
public *;
}
-keepclassmembers class * extends android.webkit.webViewClient {
public void *(android.webkit.WebView, java.lang.String, android.graphics.Bitmap);
public boolean *(android.webkit.WebView, java.lang.String);
}
-keepclassmembers class * extends android.webkit.webViewClient {
public void *(android.webkit.webView, jav.lang.String);
}
# 去除调试的日志
-assumenosideeffects class android.util.Log{ public static *** d(...); public static *** i(...); public static *** e(...); public static *** v(...); public static *** w(...);}
#################################################################################################
3. 手动发版
3.1 生成签名APK
先点击Build->Generate Signed APK..., 如图3.1.1所示:
3.1.1 点击生成签名APK
填写关联信息,如图3.1.2所示:
3.1.2 填写关联信息
选择release选项卡,点击Finish即可,如图3.1.3所示:
3.1.3 确认生成