Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android 多渠道打包

Android 多渠道打包

編輯:關於Android編程

配置AndroidMainfest.xml

以友盟渠道為例,渠道信息一般都是寫在 AndroidManifest.xml文件中,代碼大約如下:

如果不使用多渠道打包方法,那就需要我們手動一個一個去修改value中的值,xiaomi,360,qq,wandoujia等等。
使用多渠道打包的方式,就需要把上面的value配置成下面的方式:

其中${UMENG_CHANNEL_VALUE}中的值就是你在gradle中自定義配置的值。

在build.gradle中配置productFlavors

productFlavors {
     wandoujia {
          manifestPlaceholders = [UMENG_CHANNEL_VALUE: "wandoujia"]
     }
     xiaomi{
          manifestPlaceholders = [UMENG_CHANNEL_VALUE: "xiaomi"]
     }
     qq {
          manifestPlaceholders = [UMENG_CHANNEL_VALUE: "qq"]
     }
     _360 {
          manifestPlaceholders = [UMENG_CHANNEL_VALUE: "360"]
     }
}

其中[UMENG_CHANNEL_VALUE: “wandoujia”]就是對應${UMENG_CHANNEL_VALUE}的值。
我們可以發現,按照上面的方式寫,比較繁瑣,其實還有更簡潔的方式去寫,方法如下:

android { 
    productFlavors {
        wandoujia{}
        xiaomi{}
        qq{}
        _360 {}
    } 
    productFlavors.all { 
        flavor -> flavor.manifestPlaceholders = [UMENG_CHANNEL_VALUE: name] 
        }
}

其中name的值對相對應各個productFlavors的選項值,這樣就達到自動替換渠道值的目的了。
這樣生成apk時,選擇相應的Flavors來生成指定渠道的包就可以了,而且生成的apk會自動幫你加上相應渠道的後綴,非常方便和直觀。大家可以自己反編譯驗證。

配置簽名信息

//簽名
signingConfigs{
     appsign{
          storeFile file("keystore路徑")
          storePassword "***"
          keyAlias "***"
          keyPassword "***"
     }
}
buildTypes {
        release {
            runProguard false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            signingConfig signingConfigs.appsign
        }
}

注意:signingConfig signingConfigs.appsign:這段代碼不可少,作用是打包的時候連帶簽名信息一起打進去APK。否則在安裝生成的APK的時候會出現下面這個錯誤信息:
install_parse_failed_no_certificates
這裡寫圖片描述

修改導出APK的名稱

我們可以根據渠道自定義apk的名稱

android {
    applicationVariants.all { variant ->
        variant.outputs.each { output ->
            output.outputFile = new File(
                    output.outputFile.parent,
                    "xxxx(apk的名字)-${variant.buildType.name}-${defaultConfig.versionName}-${variant.productFlavors[0].name}.apk".toLowerCase())
        }
    }
}

最後打包完成之後,apk文件就會生成在項目的build\outputs\apk下。

配置Gradle的環境變量

(一)Windows平台下配置Gradle:

我們可以使用CMD命令,進入到項目所在的目錄,直接輸入命令:

gradle assembleRelease
就開始打包了,如果渠道很多的話,時間可能會很長。或者,當然Android Studio中的下方底欄中有個命令行工具Terminal,你也可以直接打開,輸入上面的命令:

gradle assembleRelease

用CMD進入到項目所在目錄執行,或者用AS中自帶的命令行工具Terminal其實性質都是一樣的。
注意:如果沒有對gradle配置的話,可能輸入上面的命令,會提示“不是內部或者外部命令”,不要著急,我們只需要找到gradle的目錄,把它配置到電腦中的環境變量中去即可。
配置方式如下:

1)先找到gralde的根目錄,在系統變量裡添加兩個環境變量:

變量名為:GRADLE_HOME,變量值就為gradle的根目錄;
所以變量值為:D:\android\android-studio-ide-143.2739321-windows\android-studio\gradle\gradle-2.10

2)還有一個在系統變量裡PATH裡面添加gradle的bin目錄
D:\android\android-studio-ide-143.2739321-windows\android-studio\gradle\gradle-2.10\bin
這樣就配置完了,,執行以下這個命令:gradle assembleRelease。

(二)Linux平台下配置Gradle:
1)配置profile

$ sudo vim /etc/profile

在文件末尾添加:

export GRADLE_HOME=/XX/XXX/gradle-2.10
export PATH=$GRADLE_HOME/bin:$PATH

2)重啟
重啟機器,然後就可以運行 gradle

$ sudo reboot
$ gradle 

運行完Gradle會出現如圖所示的信息:

這裡寫圖片描述

如果不配置Gradle會出現的問題:

1)<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwPjxpbWcgYWx0PQ=="這裡寫圖片描述" src="/uploadfile/Collfiles/20160623/20160623091506516.png" title="\" />
2)

這裡寫圖片描述

如果配置完Gradle仍然出現上圖的錯誤,則需要刪除工程下面的.gradle和gradle文件,然後重新導入工程即可。其他的操作和Windows平台下一致

打包

當然Android Studio中的下方底欄中有個命令行工具Terminal,你也可以直接打開,輸入上面的命令:

gradle assembleRelease

備注:Gradle方式打包的缺點是如果需要渠道包特別多的時候,則會非常的慢。耗費大量的時間。另外使用Gradle還可以適配不同的渠道包(如果有需求參考第二章)。

參考文檔:

http://mp.weixin.qq.com/s?__biz=MjM5NDkxMTgyNw==&mid=2653057549&idx=1&sn=456fa138f2fd307a3ff94eddc5ff2e73&scene=21

Gradle適配多渠道包(來自美團技術分享(原文))

概述

隨著渠道越來越多,不同渠道對應用的要求也不盡相同。例如,有的渠道要求美團客戶端的應用名為美團,有的渠道要求應用名為美團團購。又比如,有些渠道要求應用不能使用第三方統計工具(如flurry)。總之,每次打包都需要對這些渠道進行適配。

之前的做法是為每個需要適配的渠道創建一個Git分支,發版時再切換到相應的分支,並合並主分支的代碼。適配的渠道比較少的話這種方式還可以接受,如果分支比較多,對開發人員來說簡直就是噩夢。還好,自從有了Gradle flavor,一切都變得簡單了。本文假定讀者使用過Gradle,如果還不了解建議先閱讀相關文檔。

flavor的配置

先來看build.gradle文件中的一段代碼:

android {
    ....

    productFlavors {
        flavor1 {
            minSdkVersion 14
        }
    }
}

上例定義了一個flavor:flavor1,並指定了應用的minSdkVersion為14(當然還可以配置更多的屬性,具體可參考相關文檔)。與此同時,Gradle還會為該flavor關聯對應的sourceSet,默認位置為src/目錄,對應到本例就是src/flavor1。

接下來,要做的就是根據具體的需求在build.gradle文件中配置flavor,並添加必要的代碼和資源文件。以flavor1為例,運行gradle assembleFlavor1命令既可生成所需的適配包。下面主要介紹美團團購Android客戶端的一些適配案例。

案例

使用不同的包名

使用不同的包名,美團團購Android客戶端之前有兩個版本:手機版(com.meituan.group)和hd版(com.meituan.group.hd),兩個版本使用了不同的代碼。目前hd版對應的代碼已不再維護,希望能直接使用手機版的代碼。解決該問題可以有多種方法,不過使用flavor相對比較簡單,示例如下:

productFlavors {
    hd {
        applicationId "com.meituan.group.hd"
    }
}

上面的代碼添加了一個名為hd的flavor,並指定了應用的包名為com.meituan.group.hd,運行gradle assembleHd命令即可生成hd適配包

控制是否自動更新

美團團購Android客戶端在啟動時會默認檢查客戶端是否有更新,如果有更新就會提示用戶下載。但是有些渠道和應用市場不允許這種默認行為,所以在適配這些渠道時需要禁止自動更新功能。

解決的思路是提供一個配置字段,應用啟動的時候檢查該字段的值以決定是否開啟自動更新功能。使用flavor可以完美的解決這類問題。

Gradle會在generateSources階段為flavor生成一個BuildConfig.java文件。BuildConfig類默認提供了一些常量字段,比如應用的版本名(VERSION_NAME),應用的包名(PACKAGE_NAME)等。更強大的是,開發者還可以添加自定義的一些字段。下面的示例假設wandoujia市場默認禁止自動更新功能:

android {
    defaultConfig {
        buildConfigField "boolean", "AUTO_UPDATES", "true"
    }

    productFlavors {
        wandoujia {
            buildConfigField "boolean", "AUTO_UPDATES", "false"
        }        
    }

}

上面的代碼會在BuildConfig類中生成AUTO_UPDATES布爾常量,默認值為true,在使用wandoujia flavor時,該值會被設置成false。接下來就可以在代碼中使用AUTO_UPDATES常量來判斷是否開啟自動更新功能了。最後,運行gradle assembleWandoujia命令即可生成默認不開啟自動升級功能的渠道包,是不是很簡單。

使用不同的應用名

最常見的一類適配是修改應用的資源。例如,美團團購Android客戶端的應用名是美團,但有的渠道需要把應用名修改為美團團購;還有,客戶端經常會和一些應用分發市場合作,需要在應用的啟動界面中加上第三方市場的Logo,類似這類適配形式還有很多。
Gradle在構建應用時,會優先使用flavor所屬dataSet中的同名資源。所以,解決思路就是在flavor的dataSet中添加同名的字符串資源,以覆蓋默認的資源。下面以適配wandoujia渠道的應用名為美團團購為例進行介紹。

首先,在build.gradle配置文件中添加如下flavor:

android {
    productFlavors {
        wandoujia { 
        }
    }
}

上面的配置會默認src/wandoujia目錄為wandoujia flavor的dataSet。

接下來,在src目錄內創建wandoujia目錄,並添加如下應用名字符串資源(src/wandoujia/res/values/appname.xml):


    美團團購

默認的應用名字符串資源如下(src/main/res/values/strings.xml):


    美團

最後,運行gradle assembleWandoujia命令即可生成應用名為美團團購的應用了。

使用第三方SDK

某些渠道會要求客戶端嵌入第三方SDK來滿足特定的適配需求。比如360應用市場要求美團團購Android客戶端的精品應用模塊使用他們提供的SDK。問題的難點在於如何只為特定的渠道添加SDK,其他渠道不引入該SDK。使用flavor可以很好的解決這個問題,下面以為qihu360 flavor引入com.qihoo360.union.sdk:union:1.0 SDK為例進行說明:

android {
    productFlavors {
        qihu360 {
        }
    }
}
...
dependencies {
    provided 'com.qihoo360.union.sdk:union:1.0'
    qihu360Compile 'com.qihoo360.union.sdk:union:1.0'
}

上例添加了名為qihu360的flavor,並且指定編譯和運行時都依賴com.qihoo360.union.sdk:union:1.0。而其他渠道只是在構建的時候依賴該SDK,打包的時候並不會添加它。

接下來,需要在代碼中使用反射技術判斷應用程序是否添加了該SDK,從而決定是否要顯示360 SDK提供的精品應用。部分代碼如下:

class MyActivity extends Activity {
    private boolean useQihuSdk;

    @override
    public void onCreate(Bundle savedInstanceState) {
        try {
            Class.forName("com.qihoo360.union.sdk.UnionManager");
            useQihuSdk = true;
        } catch (ClassNotFoundException ignored) {

        }
    }
}

最後,運行gradle assembleQihu360命令即可生成包含360精品應用模塊的渠道包了。

 

Android的META_INF多渠道打包(來自美團技術分享)

原理介紹

美團高效的多渠道打包方案是把一個Android應用程序包當作一個zip文件包進行解壓,然後發現在簽名生成的目錄下添加一個空文件,空文件用渠道名來命名,而且不需要重新簽名。這種方式不需要重新簽名,編譯等步驟,使得這種方法非常高效。
如果能直接修改apk的渠道號,而不需要再重新簽名能節省不少打包的時間。幸運的是我們找到了這種方法。直接解壓apk,解壓後的根目錄會有一個META-INF目錄,如下圖所示:

這裡寫圖片描述

如果在META-INF目錄內添加空文件,可以不用重新簽名應用。因此,通過為不同渠道的應用添加不同的空文件,可以唯一標識一個渠道。

下面的python代碼用來給apk添加空的渠道文件,渠道名的前綴為cztchannel_:

用python腳本向apk文件中添加空渠道文件

假定目錄是:/home/XXX/XXXX/multibuildtool
1)配置python環境
Windows下需要配置python開發環境。Linux默認有python開發環境
2)配置渠道列表
將渠道包列表文件channel.txt放在上述目錄裡面。這個是一個樣例:

samsungapps
hiapk
anzhi
360cn
xiaomi
myapp
91com
gfan
appchina
nduoa
3gcn
mumayi
10086com
wostore
189store
lenovomm
hicloud
meizu
baidu
googleplay
wandou

3)編寫腳本
將multiChannelBuildTool.py也放在這個目錄下面。這個是python代碼:

#coding=utf-8
#!/usr/bin/python
import zipfile
import shutil
import os

# 空文件 便於寫入此空文件到apk包中作為channel文件(指定特定目錄文件)
src_empty_file = '/home/XXX/XXXX/XXXX/czt.txt'
# 創建一個空文件(不存在則創建)
f = open(src_empty_file, 'w') 
f.close()

# 獲取當前目錄中所有的apk源包
src_apks = []
# python3 : os.listdir()即可,這裡使用兼容Python2的os.listdir('.')
directs = '/home/XXXX/XXXX/XXXXX/'
for file in os.listdir('/XXXX/XXXX/XXXX/XXXX'):
    #######打印出拼接的字符串的結果
    print os.path.join(directs,file)    
    if os.path.isfile(os.path.join(directs,file)):
        extension = os.path.splitext(file)[1][1:]
    print extension
####不加上not in 的條件判斷會出現如果沒有後綴的文件依舊會執行append方法
        if extension in 'apk' and extension not in "":
            src_apks.append(file)
        print "apk"
print len(src_apks)


# 獲取渠道列表
channel_file = '/XXXX/XXXX/XXXX/XXXX/channel.txt'
f = open(channel_file)
lines = f.readlines()
f.close()

for src_apk in src_apks:
    # file name (with extension)
    print src_apk
    src_apk_file_name = os.path.basename(src_apk)
    # 分割文件名與後綴
    temp_list = os.path.splitext(src_apk_file_name)
    # name without extension   Apk的文件名稱
    src_apk_name = temp_list[0]
    # 後綴名,包含.   例如: ".apk "
    src_apk_extension = temp_list[1]

    # 創建生成目錄,與文件名相關
    output_dir = 'output_' + src_apk_name + '/'
    # 目錄不存在則創建
    if not os.path.exists(output_dir):
        os.mkdir(output_dir)

    # 遍歷渠道號並創建對應渠道號的apk文件
    for line in lines:
        # 獲取當前渠道號,因為從渠道文件中獲得帶有\n,所有strip一下
        target_channel = line.strip()
        # 拼接對應渠道號的apk
        target_apk = output_dir + src_apk_name + "-" + target_channel + src_apk_extension  
        # 拷貝建立新apk
        shutil.copy(src_apk,  target_apk)
        # zip獲取新建立的apk文件
        zipped = zipfile.ZipFile(target_apk, 'a', zipfile.ZIP_DEFLATED)
        # 初始化渠道信息
        empty_channel_file = "META-INF/cztchannel_{channel}".format(channel = target_channel)
        # 寫入渠道信息
        zipped.write(src_empty_file, empty_channel_file)
        # 關閉zip流
    print target_channel
        zipped.close()

復制簽好名的包,運行腳本

選擇一個之前打好的APK,也放在同一個目錄下面。然後執行python腳本。
命令如下:python /home/dq/桌面/multibuildtool/multiChannelBuildTool.py.
執行完Python腳本以後會在這個目錄下面,生成output_(APK名稱)的文件夾,裡面有相關的APK文件
3.3用java代碼讀取渠道名,並動態設置渠道名
我們用腳本生成了文件之後,文件的名字是用渠道名來命名的,所以我們在啟動程序的時候,可以用java代碼動態讀取渠道名,並動態的去設置。
java代碼讀取渠道名的方法:

package XXXXXXX.utils;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.preference.PreferenceManager;
import android.text.TextUtils;
import java.io.IOException;
import java.util.Enumeration;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
/**
 * 獲取渠道包的類(如果使用了umneg的數據統計可以直接將結果用字setChannel()umeng的一個方法)
 * Createod by dengquan on 16-6-7.
 */
public class ChannelUtil
{
    private static final String CHANNEL_KEY = "cztchannel";
    private static final String CHANNEL_VERSION_KEY = "cztchannel_version";
    private static String mChannel;
    /**
     * 返回市場。  如果獲取失敗返回""
     * @param context
     * @return
     */
    public static String getChannel(Context context){
        return getChannel(context, "");
    }
    /**
     * 返回市場。  如果獲取失敗返回defaultChannel
     * @param context
     * @param defaultChannel
     * @return
     */
    public static String getChannel(Context context, String defaultChannel) {
        //內存中獲取
        if(!TextUtils.isEmpty(mChannel)){
            return mChannel;
        }
        //sp中獲取
        mChannel = getChannelBySharedPreferences(context);
        if(!TextUtils.isEmpty(mChannel)){
            return mChannel;
        }
        //從apk中獲取
        mChannel = getChannelFromApk(context, CHANNEL_KEY);
        if(!TextUtils.isEmpty(mChannel)){
            //保存sp中備用
            saveChannelBySharedPreferences(context, mChannel);
            return mChannel;
        }
        //全部獲取失敗
        return defaultChannel;
    }
    /**
     * 從apk中獲取版本信息
     * @param context
     * @param channelKey
     * @return
     */
    private static String getChannelFromApk(Context context, String channelKey) {
        //從apk包中獲取
        ApplicationInfo appinfo = context.getApplicationInfo();
        String sourceDir = appinfo.sourceDir;
        //默認放在meta-inf/裡, 所以需要再拼接一下
        String key = "META-INF/" + channelKey;
        String ret = "";
        ZipFile zipfile = null;
        try {
            zipfile = new ZipFile(sourceDir);
            Enumeration entries = zipfile.entries();
            while (entries.hasMoreElements()) {
                ZipEntry entry = ((ZipEntry) entries.nextElement());
                String entryName = entry.getName();
                if (entryName.startsWith(key)) {
                    ret = entryName;
                    break;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (zipfile != null) {
                try {
                    zipfile.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        String[] split = ret.split("_");
        String channel = "";
        if (split != null && split.length >= 2) {
            channel = ret.substring(split[0].length() + 1);
        }
        return channel;
    }
    /**
     * 本地保存channel & 對應版本號
     * @param context
     * @param channel
     */
    private static void saveChannelBySharedPreferences(Context context, String channel){
        SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
        SharedPreferences.Editor editor = sp.edit();
        editor.putString(CHANNEL_KEY, channel);
        editor.putInt(CHANNEL_VERSION_KEY, getVersionCode(context));
        editor.commit();
    }
    /**
     * 從sp中獲取channel
     * @param context
     * @return 為空表示獲取異常、sp中的值已經失效、sp中沒有此值
     */
    private static String getChannelBySharedPreferences(Context context){
        SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
        int currentVersionCode = getVersionCode(context);
        if(currentVersionCode == -1){
            //獲取錯誤
            return "";
        }
        int versionCodeSaved = sp.getInt(CHANNEL_VERSION_KEY, -1);
        if(versionCodeSaved == -1){
            //本地沒有存儲的channel對應的版本號
            //第一次使用  或者 原先存儲版本號異常
            return "";
        }
        if(currentVersionCode != versionCodeSaved){
            return "";
        }
        return sp.getString(CHANNEL_KEY, "");
    }
    /**
     * 從包信息中獲取版本號
     * @param context
     * @return
     */
    private static int getVersionCode(Context context){
        try{
            return context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionCode;
        }catch(PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        return -1;
    }
}

讀取到了渠道名,我們就可以動態的設置了,比如友盟渠道的動態設置方法是:AnalyticsConfig.setChannel(getChannel(Context context) );這樣就好了。這種方式每打一個渠道包只需復制一個apk,在META-INF中添加一個使用渠道號命名的空文件即可。這種打包方式速度非常快,據說900多個渠道不到一分鐘就能打完。我親測的是我用了10秒鐘打了32個渠道包,是不是很快。

友盟設置渠道包代碼:

private void initUmneg()
    {
        MobclickAgent.UMAnalyticsConfig config = new MobclickAgent.UMAnalyticsConfig(this, Constant.UMeng.APP_KEY,getChannel());

        MobclickAgent.startWithConfigure(config);
    }

相關問題參考這個Github的文檔介紹。

參考文檔:
http://mp.weixin.qq.com/s?__biz=MjM5NDkxMTgyNw==&mid=2653057569&idx=1&sn=0fa214999538a7ae8e5964d729377827

http://tech.meituan.com/mt-apk-packaging.html

https://github.com/GavinCT/AndroidMultiChannelBuildTool

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved