編輯:關於Android編程
以友盟渠道為例,渠道信息一般都是寫在 AndroidManifest.xml文件中,代碼大約如下:
如果不使用多渠道打包方法,那就需要我們手動一個一個去修改value中的值,xiaomi,360,qq,wandoujia等等。
使用多渠道打包的方式,就需要把上面的value配置成下面的方式:
其中${UMENG_CHANNEL_VALUE}中的值就是你在gradle中自定義配置的值。
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的名稱
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下。
(一)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
隨著渠道越來越多,不同渠道對應用的要求也不盡相同。例如,有的渠道要求美團客戶端的應用名為美團,有的渠道要求應用名為美團團購。又比如,有些渠道要求應用不能使用第三方統計工具(如flurry)。總之,每次打包都需要對這些渠道進行適配。
之前的做法是為每個需要適配的渠道創建一個Git分支,發版時再切換到相應的分支,並合並主分支的代碼。適配的渠道比較少的話這種方式還可以接受,如果分支比較多,對開發人員來說簡直就是噩夢。還好,自從有了Gradle flavor,一切都變得簡單了。本文假定讀者使用過Gradle,如果還不了解建議先閱讀相關文檔。
先來看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來滿足特定的適配需求。比如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應用程序包當作一個zip文件包進行解壓,然後發現在簽名生成的目錄下添加一個空文件,空文件用渠道名來命名,而且不需要重新簽名。這種方式不需要重新簽名,編譯等步驟,使得這種方法非常高效。
如果能直接修改apk的渠道號,而不需要再重新簽名能節省不少打包的時間。幸運的是我們找到了這種方法。直接解壓apk,解壓後的根目錄會有一個META-INF目錄,如下圖所示:
如果在META-INF目錄內添加空文件,可以不用重新簽名應用。因此,通過為不同渠道的應用添加不同的空文件,可以唯一標識一個渠道。
下面的python代碼用來給apk添加空的渠道文件,渠道名的前綴為cztchannel_:
假定目錄是:/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);
}
參考文檔:
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
有時候,看到一些界面上的色彩,心情可能會很舒暢,有時候,看到一些其他色彩,就覺得很討厭,不爽,看到android L Palette 從圖片中提取篩選出來的顏色,覺得都挺
ContextMenu介紹: 如果一個View注冊了上下文菜單,那麼當長按該View時便會彈出一個浮動菜單,來供選擇下一步操作。 實現這個功能需要調用setOnCrea
Android Studio的代碼自動檢測的錯誤提示方式感覺有點奇葩,和Eclipse差別很大,Eclipse檢測到某個資源文件找不到或者錯誤,都會在Project中對應
在Android中,線程內部或者線程之間進行信息交互時經常會使用消息,這些基礎的東西如果我們熟悉其內部的原理,將會使我們容易、更好地架構系統,避免一些低級的錯誤。每一個A