Android开发经验谈Gradle

Gradle学习笔记(一) 自定义插件上传APK文件

2018-11-17  本文已影响8人  houtengzhi

工作中打包好的APK文件经常需要签名才能安装,公司没有提供签名APK所需的私钥,每次签名都需要先上传到签名服务器上,再下载签名后的APK文件,这样手动操作很繁琐,遂萌生出写个一键签名的脚本,这里考虑用gradle脚本来实现,在APK打包好之后可以自动上传至服务器签名然后自动下载。

签名服务器接口分析

编写gradle插件需要groovy,与java是兼容的,这样我们可以直接用OkHttp框架更方便地实现各种网络请求。由于没有现成的签名服务API接口,只能自己通过fiddler抓包来一步步分析请求参数。

首先是模拟登录

在签名服务器登录页面输入用户名和密码,观察抓包的情况。可以看到请求头以及表单信息


请求头信息 表单信息

有了这些参数,接下来我们就可以手动模拟登录。

FormBody formBody = new FormBody.Builder()
                .add("return", "index.php")
                .add("username", username)
                .add("password", password)
                .build();
Request request = new Request.Builder()
                .post(formBody)
                .url(url)
                .addHeader("User-Agent", USER_AGENT)
                .build();
Response response = mOkHttpClient.newCall(request).execute();

模拟登录成功后就可以获取到包含登录信息的Cookie,后面的上传APK文件请求中需要携带这个Cookie。OkHttp3提供了cookieJar的方法来实现Cookie的缓存。

OkHttpClient.Builder builder = new OkHttpClient.Builder()
builder.connectTimeout(10, TimeUnit.SECONDS)
           .readTimeout(20, TimeUnit.SECONDS)
           .writeTimeout(20, TimeUnit.SECONDS)
           .cookieJar(new CookieJar() {

           //使用Map缓存Cookie
           private final Map<String, List<Cookie>> cookieStore = new HashMap<>()

            @Override
            void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
                cookieStore.put(url.host(), cookies)
            }

            @Override
            List<Cookie> loadForRequest(HttpUrl url) {
                List<Cookie> cookies = cookieStore.get(url.host())
                return cookies != null ? cookies : new ArrayList<Cookie>()
            }
        })
mOkHttpClient = builder.build();
模拟网页上传文件

浏览器是用Multipart/form-data上传文件的,下图是抓包网页上传文件的请求头和请求体信息。请求头中包含Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryFFpuNGUcr8mOaedw这一行,其中multipart/form-data是表单方式上传文件,----WebKitFormBoundaryFFpuNGUcr8mOaedw是随机生成的分隔符boundary,在下面详细的请求体数据中可以看到这个。

请求体中包含了两处数据,一个是自定义名称APC_UPLOAD_PROGRESS以及它的值,再就是名称为APK文件二进制数据。

用OkHttp模拟相同的请求信息,调用APIsetType(MultipartBody.FORM)设置请求头Content-Type,就不需要我们手动设置boundary了。此外OkHttp提供了addFormDataPart的API可以方便的构造表单数据。

MediaType mediaType = MediaType.parse("application/vnd.android.package-archive");
        RequestBody requestBody = new MultipartBody.Builder()
                .setType(MultipartBody.FORM)
                .addFormDataPart("APC_UPLOAD_PROGRESS", String.valueOf(key))
                .addFormDataPart("upfile", file.getName(), RequestBody.create(mediaType, file))
                .build();
        Request request = new Request.Builder()
                .url(url)
                .post(requestBody)
                .addHeader("User-Agent", USER_AGENT)
                .addHeader("Connection", "keep-alive")
                .build();
        Response response = mOkHttpClient.newCall(request).execute()
下载文件

后面根据返回信息生成的url就可以下载签名后的APK文件了。

Request request = new Request.Builder()
                .url(url)
                .addHeader("User-Agent", USER_AGENT)
                .addHeader("Accept-Encoding", "identity")   // 服务器下发的的资源不要做gzip, 否则没有content-length,报ProtocolException
                .build();

        Response response = mOkHttpClient.newCall(request).execute()

gradle插件

关于gradle插件的编写已有很多不错的文章,大家可以参考下面罗列出来的。

配置参数类

外部配置参数

class ConfigExt {
    String username
    String password
    String loginUrl
    String baseSignUrl
    String signType
    String suffix
    boolean debugged = false
    boolean autoSign = true
    boolean deleteSourceFile = true
}

嵌套配置参数,可以设置每个buildType是否签名

class VariantConfigExt {
    boolean sign
}
创建SignPlugin

这里根据variant生成不同的签名task,通过设置签名tasksignXXX和打包taskassembleXXX不同的依赖关系以实现自动或手动签名。

class SignPlugin implements Plugin<Project> {
    private Project mProject = null

    @Override
    void apply(Project project) {
        this.mProject = project
        GLog.i("apply")

        project.extensions.create('remoteSign', ConfigExt)

        ConfigExt mConfig = project.remoteSign

        // 根据不同的buildType创建VariantConfigExt
        if (project.android.hasProperty("buildTypes")) {
            project.android.buildTypes.all { type ->
                project.remoteSign.extensions.create("${type.name}", VariantConfigExt)
            }
            // release默认自动签名
            mConfig.release.sign = true
        }

        if (project.android.hasProperty("applicationVariants")) {
            project.android.applicationVariants.all { variant ->

                Task assembleTask

                DefaultTask signTask = createSignTask(variant)
                GLog.debug = (mConfig != null && mConfig.debugged)

                variant.outputs.each { output ->

                    assembleTask = output.assemble
                    GLog.d("    config: " + (mConfig == null ? "null" : mConfig.toString()))
                    GLog.d("    assembleTask: " + assembleTask.name)
                    signTask.assembleTaskName = assembleTask.name

                    signTask.dependsOn assembleTask
                    if (project.remoteSign != null && project.remoteSign.autoSign) {
                        def buildType = variant.buildType.name
                        boolean hasBuildTypePro = mConfig.hasProperty(buildType)
                        boolean sign = true
                        if (hasBuildTypePro) {
                            sign = mConfig.getProperty(buildType).properties.get("sign").asBoolean()
                            GLog.d(buildType + ": " + mConfig.getProperty(buildType).toString())
                        }

                        if (sign) {
                            assembleTask.doLast { tk ->
                                if (!tk.state.failure) {
                                    signTask.execute()
                                }
                            }
                        }
                    }

                }


            }
        }


    }

    private DefaultTask createSignTask(Object variant) {
        String variantName = variant.name.capitalize()
        GLog.i("create sign${variantName} task")
        DefaultTask signTask = mProject.task("sign${variantName}", type: RemoteSignTask)
        signTask.group = 'signature'
        signTask.description = 'Upload apk to sign server'
        return signTask
    }
创建DoSignTask

如果在gradle中修改过输出的apk名称,在配置阶段(在SignPlugin的apply方法中)就查找文件名会是修改之前的名称,所以这里我们在task执行阶段再根据前面生成的assembleTask名称查找对应生成的apk文件名,

class DoSignTask extends DefaultTask {
String assembleTaskName
...
...
private ApkInfo getApkInfo(Project project) {
        glogd("getApkInfo:")
        ApkInfo apkInfo = new ApkInfo()

        project.android.applicationVariants.all { variant ->

            variant.outputs.each { output ->
                GLog.d("    assembleTask: ${output.assemble.name}")
                if (output.assemble.name == assembleTaskName) {
                    File apkFile = output.outputFile
                    apkInfo.apkFile = apkFile
                    apkInfo.apkName = apkFile.name
                    apkInfo.parentDir = apkFile.parent
                    GLog.d("    output: ${apkFile.name}")
                }
            }
        }
        return apkInfo
    }
...
...
}
发布插件到maven私服

在项目根目录下新建一个uploadToMaven.gradle文件,还在开发调试阶段可以发布SNAPSHOT版本 (理解Maven中的SNAPSHOT版本和正式版本)。

apply plugin: 'maven'

def mavenReleaseUrl = 'http://xx.xx.x.xx/nexus/content/repositories/gradle-plugin/'
def mavenSnapshotUrl = 'http://xx.xx.x.xx/nexus/content/repositories/android-snapshots/'

uploadArchives {
    configuration = configurations.archives
    repositories {
        mavenDeployer {
            repository(url: uri(mavenReleaseUrl)) {
                authentication(userName: MAVEN_USERNAME, password: MAVEN_PASSWORD)
            }

            snapshotRepository(url: uri(mavenSnapshotUrl)) {
                authentication(userName: MAVEN_USERNAME, password: MAVEN_PASSWORD)
            }
        }
    }
}

在module的build.gradle文件中添加

group='com.ahgx.gradleplugin'
version='1.1.0'
//version='1.0.0-SNAPSHOT'

//apply from: '../uploadToLocalRepo.gradle'
apply from: '../uploadToMaven.gradle'
集成使用

在项目根目录下的build.gradle文件中添加

buildscript {
    repositories {
        maven {
            url uri('http://xx.xx.x.xx/nexus/content/groups/public/')
        }
    }
    dependencies {
        classpath 'com.ahgx.gradleplugin:sign-plugin:1.1.0'
    }
}

在mudule下的build.gradle文件中添加

apply plugin: 'com.ahgx.sign-plugin'

可配置参数有

remoteSign {
    username 'xxxxx'                                  //用户名,必填
    password 'xxxxx'                                     //密码,必填
    loginUrl 'http://xx.xx.x.xx/mantis/login.php'         //登录地址,必填
    baseSignUrl 'http://xx.xx.x.xx/mantis'                //地址,必填
    signType 'app'                                       //签名类型(app, apk, sys),必填

    debugged false                                       //调试模式,默认false
    autoSign true                                       //打包APK之后自动签名,默认true
    suffix '-signed'                                    //签名后文件命名后缀,默认签名服务器自动添加
    deleteSourceFile true                              //是否删除原APK文件,默认true

    release {
        sign true    //是否自动签名,默认为true
    }

    debug {
        sign false   //是否自动签名,默认为false
    }

最后在AndroidStudio右侧的gradle面板上可以看到signature分组下的签名task,双击执行即可手动签名。


参考文章

上一篇下一篇

猜你喜欢

热点阅读