一、注意事项

  • 不能热更新 Manifest 清单文件中的内容

二、相关配置

  • enableProxyApplication = true 情况下的配置

    执行 Gradle -> :app -> Tasks -> build -> assemableNone 后,会在 /app/build/bakApk/baseApkDir/none 目录下生成 APK

    检查 APK 的 manifest 文件,ApplicationName 会被替换,并生成几个额外的 meta-data,其中包括 TINKER_ID 和 TINKER_PATCH_APPLICATION

    • TINKER_ID = noneRelease_base-1.54
    • TINKER_PATCH_APPLICATION = xxx.xxx.CustomApplication

三、打包过程

  • 生成基包
    1. 修改 TinkerId 为 base-version
    2. 生成包 Gradle Projects -> :app -> Tasks -> build -> assembleRelease
    3. 获取 APK,在 /app/build/bakApk/baseApkDir/flavors/app-flavor-release.apk
  • 生成补丁包
    1. 修改代码
    2. 修改 TinkerId 为 patch-version-index
    3. 设置 baseApkDir 路径为基包的名称
    4. 生成基包 Gradle Projects -> :app -> Tasks -> tinker-support -> buildAllFlavorsTinkerPatchRelease
    5. 获取 APK,在 /app/build/output/patch/flavors/release/patch_signed_7zip.apk
  • 其它配置
    • buildAllFlavorsDir 构建多渠道补丁时使用
    • isProtectedApp 是否启用加固模式,默认为false
    • enableProxyApplication 是否开启反射Application模式

四、遇到的 BUG

  • 证书
    Execution failed for task ':app:tinkerPatchNoneRelease'.
    > Could not resolve all dependencies for configuration ':app:sevenZipToolsLocator'.
       > Could not download SevenZip-osx-x86_64.exe (com.tencent.mm:SevenZip:1.1.10)
          > Could not get resource 'https://jcenter.bintray.com/com/tencent/mm/SevenZip/1.1.10/SevenZip-1.1.10-osx-x86_64.exe'.
             > Could not GET 'https://jcenter.bintray.com/com/tencent/mm/SevenZip/1.1.10/SevenZip-1.1.10-osx-x86_64.exe'.
                > peer not authenticated
    

    原因:电脑上没有 SevenZip 签名软件

    解决:设置 path 路径

    sevenZip {
        zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
        path = "/usr/local/bin/7za"
    }
    
  • BuglyFileProvider 文件冲突,应用无法安装
    Failed to install xxx.apk: Failure
    [INSTALL_FAILED_CONFLICTING_PROVIDER: Package couldn't be installed in /data/app/com.xxx: 
    Can't install because provider name com.tencent.bugly.beta.fileProvider 
    (in package com.xxx.xxx) is already used by com.xxx.bbb]
    

    原因:android:authorities 不能与已安装应用相同导致。

    在配置清单文件时没有添加 com.tencent.bugly.beta.fileProvider,导致自动生成了一个,并且 android:authorities=”com.tencent.bugly.beta.fileProvider”。

    解决:加上 BuglyFileProvider,代码如下:

    <provider
        android:name="com.tencent.bugly.beta.utils.BuglyFileProvider"
        android:authorities="${applicationId}.fileProvider"
        android:exported="false"
        android:grantUriPermissions="true"
        tools:replace="name,authorities,exported,grantUriPermissions">
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/provider_paths"
            tools:replace="name,resource"/>
    </provider>
    

五、使用 Python 编写脚本提取相关信息(不重要)

因为同时要为好多个应用添加 Bugly 的热更新,而且有四个渠道的安装包,手动将每个文件复制出来并重命名非常的繁琐。借此写一个脚本,顺便练练手(之前只看过文档):

import os
import shutil
import time

BASE_PROJECT_BUILD_DIR = "/Users/ionesmile/Documents/iOnesmileDocs/WorkSpace/Snaillove/ColorLampChangda/app/build"

BAK_PATH = BASE_PROJECT_BUILD_DIR + "/bakApk/"

baseApkDir = "app-0206-18-36-11"

FLAVORS_ALIAS_MAP = {"self360": "360", "selfBaidu": "baidu"}


def getApkVersionCode(baseApk):
    return "1.38"


def getProjectName():
    return "i-lamp"


def copyBaseApkRenameToPath(outPath):
    outPath = os.path.join(outPath, "base")
    if not os.path.exists(outPath):
        os.makedirs(outPath)
    for flavorApk in os.listdir(BAK_PATH + baseApkDir):
        if os.path.isdir(os.path.join(BAK_PATH + baseApkDir, flavorApk)):
            baseApk = os.path.join(BAK_PATH + baseApkDir, flavorApk + "/app-"+flavorApk+"-release.apk")
            print "baseApk", baseApk
            if os.path.exists(baseApk):
                if not FLAVORS_ALIAS_MAP.get(flavorApk, None) is None:
                    flavorApk = FLAVORS_ALIAS_MAP.get(flavorApk, None)
                out_file = os.path.join(outPath, "android_"+getProjectName()+"_v"+getApkVersionCode(baseApk)+"_"+getDateYyyyMMdd()+"_"+flavorApk+".apk")
                print "out_file", out_file
                shutil.copyfile(baseApk, out_file)


def copyBaseFileRenameToPath(outPath):
    outPath = os.path.join(outPath, "baseFile")
    if not os.path.exists(outPath):
        os.makedirs(outPath)

    for flavorApk in os.listdir(BAK_PATH + baseApkDir):
        if os.path.isdir(os.path.join(BAK_PATH + baseApkDir, flavorApk)):
            baseApk = os.path.join(BAK_PATH + baseApkDir, flavorApk)

            out_flavor_path = os.path.join(outPath, flavorApk)
            if not os.path.exists(out_flavor_path):
                os.makedirs(out_flavor_path)

            for child_file in os.listdir(baseApk):
                item_file = os.path.join(baseApk, child_file)
                if os.path.isfile(item_file) and not item_file.endswith(".apk"):
                    out_file = os.path.join(out_flavor_path, child_file)
                    print "item_file", item_file
                    print "out_file", out_file
                    print ""
                    shutil.copyfile(item_file, out_file)



def copyPatchApkRenameToPath(outPath):
    outPath = os.path.join(outPath, "patch")
    if not os.path.exists(outPath):
        os.makedirs(outPath)

    patchPath = os.path.join(BASE_PROJECT_BUILD_DIR, "outputs/patch")
    for flavorApk in os.listdir(patchPath):
        if os.path.isdir(os.path.join(patchPath, flavorApk)):
            baseApk = os.path.join(patchPath, flavorApk + "/release/patch_signed_7zip.apk")
            print "baseApk", baseApk
            if os.path.exists(baseApk):
                if not FLAVORS_ALIAS_MAP.get(flavorApk, None) is None:
                    flavorApk = FLAVORS_ALIAS_MAP.get(flavorApk, None)
                out_file = os.path.join(outPath, flavorApk + "_patch_"+getApkVersionCode(baseApk)+"_7zip.apk")
                print "out_file", out_file
                shutil.copyfile(baseApk, out_file)


def getDateYyyyMMdd():
    return time.strftime("%Y%m%d", time.localtime(time.time()))


# 复制基包和 R 等文件到指定的目录
# copyBaseApkRenameToPath("/Users/ionesmile/Desktop/iLamp")
# copyBaseFileRenameToPath("/Users/ionesmile/Desktop/iLamp")

# 提取补丁包到指定目录
copyPatchApkRenameToPath("/Users/ionesmile/Desktop/iLamp")

因为上传不同的市场,需要内置不同的更新包,而 360 和 百度 的更新包在上传时会有冲突,所以需要根据情况来编译对应的更新包。

build.gradle 中 Flavor 编译,并设置输出文件名代码如下:

apply plugin: 'com.android.application'

android {

    productFlavors {
        none { }
        self { }
        self360 { }
        selfBaidu { }
    }

    // 设置输出文件名称(不必须)
    android.applicationVariants.all { variant ->
        variant.outputs.each { output ->
            def flavorAliasMap = ['self360':'360', 'selfBaidu':'baidu']
            String flavorType = flavorAliasMap.get(productFlavors[0].name) == null ? productFlavors[0].name : flavorAliasMap.get(productFlavors[0].name)
            String fileName = "android_ilight_pro_v${defaultConfig.versionName}_${new Date().format("yyyyMMdd")}_${flavorType}.apk"
            output.outputFile = new File(output.outputFile.parent, fileName)
        }
    }
}

dependencies {
    ...

    // 使用 flavorCompile 依赖不同库
    selfCompile(name: 'updateSelfLibrary_20171221', ext: 'aar')
    self360Compile(name: 'update360Library_20171221', ext: 'aar')
    selfBaiduCompile(name: 'updateBaiduLibrary_20171221', ext: 'aar')
}

repositories {
    flatDir { dirs 'libs' }

    jcenter()
}

build.gradle 中签名配置代码如下(不必须):

apply plugin: 'com.android.application'

android {

    // 配置签名包
    signingConfigs {
        config {
            keyAlias 'xxx'
            keyPassword 'xxxxxx'
            storeFile file('/Users/ionesmile/Documents/iOnesmileDocs/WorkDoc/keystore')
            storePassword 'xxxxxx'
        }
    }

    buildTypes {
        release {
            signingConfig signingConfigs.config
        }
    }
}

目前代码中使用反射的方式来出发检测版本,如下:

/** 动态导入不同的更新库,通过反射方式检查更新 **/
public final static void checkVersionUpdate(Activity activity) {
    final String[] updateClassArr = new String[]{
            "com.snaillove.common.update.DynamicUpdateSelf",
            "com.snaillove.common.update.DynamicUpdateBaidu",
            "com.snaillove.common.update.DynamicUpdate360"
    };
    for (String clazzName : updateClassArr) {
        try {
            Class cls = Class.forName(clazzName);
            Method setMethod = cls.getDeclaredMethod("exec", Activity.class);
            setMethod.invoke(cls.newInstance(), activity);
        } catch (Exception e) {
        }
    }
}

一、接入流程

其实没啥好说的,直接看 快速上手文档SDK API 的使用说明



接入中一些小小的总结:

  1. 下载官网支付 Demo,熟悉调用流程

  2. 按照上面 Training 文档,将 Demo 中的支付相关代码移植到自己的项目中

    • 添加 IInAppBillingService.aidl 文件
    • 设置 com.android.vending.BILLING 权限
    • 调用 IabHelper 对象,完成初始化操作(其中 base64EncodedPublicKey 需要到 控制台 拿取)
  3. 打包当前版本,并上传到控制台
    • 进入控制台: http://play.google.com/apps/publish
    • 在当前应用下【版本管理】 -> 【应用版本】,在 【Aplha 版】 -> 【管理 Alpha 版】 -> 【修改版本】 中上传当前项目打包的安装包
    • 注意接下来测试要和已上传安装包的签名文件、版本号保持一致
    • 在 【设置】 -> 【开发者账号】 -> 【账号详情】 -> 【许可测试】 下添加测试人员账号
  4. 在当前应用下 【商品发布】 -> 【应用内商品】 中添加商品

  5. 查询商品详情 mHelper.queryInventoryAsync

  6. 购买商品 mHelper.launchPurchaseFlow

  7. 消耗商品 mHelper.consumeAsync

二、测试环境

  1. 支持 Google Play 的手机(比如 Nexus,国内很多手机不支持需要 root)
  2. Google 开发者账号和测试账号
  3. 支持双币的信用卡(测试账号付款的时候要用,虽然实际不扣费)
  4. 梯子

三、常见错误

  1. 无法购买您要买的商品

    • 当前Google Play帐号不是测试帐号
    • 当前商品未在后台配置
  2. 此版本的应用为配置为通过Google Play结算。有关详情,请访问帮助中心。
    • 检查下打包所用的签名与上传Google Play后台的签名是否一致
    • 检查版本号与上传的版本号是否一致

前一段时间因为打 AAR 包折腾了一整天,不得不怀疑我对 Gradle 的认识。虽然在此之前确实能解决一些 Gradle 打包依赖的冲突或错误,但并没有系统的去学习。

一、Gradle 是什么

Gradle依赖管理 + 构建工具。它继承了 Ant 的灵活和 Maven 的生命周期管理,它最后被 google 作为了 Android 御用管理工具。它最大的区别是不用 XML 作为配置文件格式,采用了DSL格式,使得脚本更加简洁。

  • Ant 是最早的构建工具,基于 idea,好象是2000年有的,当时是最流行 java 构建工具,不过它的 XML 脚本编写格式让 XML 文件特别大。对工程构建过程中的过程控制特别好。

  • Maven 它是用来给 Ant 补坑的,Maven 第一次支持了从网络上下载的功能,仍然采用 xml 作为配置文件格式,它的问题是不能很好的相同库文件的版本冲突。Maven 专注的是依赖管理,构建神马的并不擅长。

  • 构建工具 是什么

    单个源码文件,你可以很轻松地 javac、gcc。然而项目结构复杂的时候,从源代码到实际产出的生成物之间需要经过一些列的转换操作,比如说编译、打包。而这一整个完整的过剩叫做“构建”。

  • Maven 的主要功能主要分为5点,分别是依赖管理系统、多模块构建、一致的项目结构、一致的构建模型和插件机制。

二、Android 是如何打包的

将一堆源码生成一个 APK 的过程就是打包,Gradle 作为一个构建平台已经有了很好的基础,到具体的打包应用步骤就由 Android Gradle Plugin 完成。即我们在项目下 build.gradle 的配置:

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

另外在项目子工程中,app、XXXlibrary 内的 build.gradle 文件使用 apply plugin 来指定具体使用的插件,如:

apply plugin: 'com.android.application'
apply plugin: 'com.android.library'

关于该插件打包时更多的配置,请参见(如:buildTypes、productFlavors、signingConfigs、ProGuard):
https://developer.android.com/studio/build/index.html

三、什么是 AAR 文件

AAR 文件本身是一个 zip 文件,在您构建相关应用模块时,库模块将先编译到 AAR 文件中,然后再添加到应用模块中。为了避免常用资源 ID 的资源冲突,请使用在模块(或在所有项目模块)中具有唯一性的前缀或其他一致的命名方案。解压后可以看到如下目录:

  • aapt
  • aidl
  • AndroidManifest.xml
  • assets
  • classes.jar
  • jni
  • libs
  • R.txt
  • res

四、依赖冲突的解决办法

在集成多个库工程时,出现了如下异常:

java.lang.NoSuchMethodError: android.support.v4.app.ActivityCompat.startActivity

很明显是 support 包冲突的问题,查看 gradle.build 文件,在 dependencies 中有如下警告信息:

All com.android.support libraries must use the exact same version specification (mixing versions can lead to runtime crashes).
Found versions 25.2.0, 24.0.0. Examples include com.android.support:animated-vector-drawable:25.2.0 and com.android.support:mediarouter-v7:24.0.0

提示存在多个版本,可能会导致运行时错误,需要使用同一个版本。解决办法:

  • 通过 ./gradlew dependenciesgradle dependencies 查看依赖树

  • 可以使用 exclude 关键字来排除单个的库工程,如:

    compile('com.yanzhenjie:recyclerview-swipe:1.0.3') {
        exclude group: 'com.android.support', module: 'recyclerview-v7'
    }
    
  • 可以使用全局的方式替换,在根目录的 build.gradle 文件中,代码如下:
    subprojects {
        project.configurations.all {
            resolutionStrategy.eachDependency { details ->
                if (details.requested.group == 'com.android.support'
                        && details.requested.name.contains('appcompat-v7') ) {
                    details.useVersion "24.2.0"
                }
            }
        }
    }
    

    并且在项目中可见的地方,修改为相同的版本号,并 sync project gradle

参考:
the-exact-same-version-specification
the-exact-same-version/42582204#42582204

五、Android Studio 如何快速运行程序

运行速度严重影响了开发效率,虽然换 MBP 后比以前的运行速度提高了三倍,但还是不够满意。提高速度主要有如下几个方法:

  1. 提高编译内存,在工程目录下的 gradle.properties 文件中新增如下代码:
    org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
    org.gradle.parallel=true
    org.gradle.daemon=true
    
  2. 在 app 子工程目录下的 build.gradle 中配置下改成增量编译和调整 minSdkVersion
    dexOptions {
        incremental true
    }
    
    productFlavors {
        dev {
            // dev utilizes minSDKVersion = 21
            // to allow the Android gradle plugin
            // to pre-dex each module and produce an APK that can be tested on
            // Android Lollipop without time consuming dex merging processes.
            minSdkVersion 21
        }
        prod {
            // The actual minSdkVersion for the application.
            minSdkVersion 15
        }
    }
    
  3. 使用插件 Freeline

参考文档

上面是我再查找资料中做的一些总结,内容不全面和一些不连贯地方。如要更详细的了解请见如下链接:

由于公司还有一些老项目,不得不再重装一下 Eclipse,中间遇到了不少坑,把流程记录一下。

一、下载安装

  1. 下载 Eclipse

    路径: https://www.eclipse.org/downloads/packages/eclipse-android-developers/neonm6

  2. 下载 SDK

    最开始我下载的是最新的 SDK 版本,但是发现 Android SDK Manager 无法打开。最后尝试使用一个老一点的版本,如:android-sdk_r24.4.1-macosx

  3. 配置 SDK

    在 Eclipse -> Preference -> Android -> SDK Location 中选择下载的版本,并点击 Apply 按钮。

    打开 Android SDK Manager,主要需要下载的有如下几个:

    • Tools
      • Android SDK tools
      • Android SDK Build-tools
    • Android X.X.X (API XX)
      • SDK Platform

二、遇到的异常

  1. 运行时 Unable to build,错误信息:
    Failed to load C:\Program Files (x86)\Android\android-sdk\build-tools\26.0.0-preview\lib\dx.jar   
    Unable to build: the file dx.jar was not loaded from the SDK folder
    

    原因
    build-tools 工具的版本太高出现的问题

    解决
    换一个低版本的 build-tools 工具,如 24.0.0。最简单的方式是在目录下只留下一个,删除其它。

  2. 打签名包时,弹出对话框

    Conversion to Dalvik format failed with error 1
    

    解决

    1. 关闭自动 BuildProject -> Build Automatically
    2. clean 项目 Project -> Clean
    3. 手动 Build,右键点击项目,然后 Build Project
    4. 然后再 Export 项目,报错消失了!

    参考:http://blog.csdn.net/myzlhh/article/details/52279443

  3. 打签名包时,弹出对话框

    No DEX file found.
    

    原因
    eclipse打包的时候,bin路径下的dex文件没有生成,导致无法打包。

    解决

    • 方案一:在打包之前 Clean 并 run 一下项目
    • 方案二:eclipse -> preferences -> android -> build 下,Skip packaging and dexing until export to launch 选项去掉勾选。

    参考:http://blog.csdn.net/henrychow_2015/article/details/60954473