android 渠道打包工具,Android渠道打包技术小结
導讀
本文對比了渠道4種渠道打包方式:
與iOS的單一渠道(AppStore)不同,Android平臺在國內的渠道多入牛毛。以我們的App為例,就有27個普通渠道(應用寶,百度,360這種)和更多的推廣專用渠道。我們打包技術也經過了若干次的改進。
1.利用Gradle Product Favor打包
android?{
productFlavors?{
base?{
manifestPlaceholders?=?[?CHANNEL:”0"]
}
yingyongbao?{
manifestPlaceholders?=?[?CHANNEL:"1"]
}
baidu?{
manifestPlaceholders?=?[?CHANNEL:"2"]
}
}
}
AndroidManifest.xml
android:name="CHANNEL"
android:value="${CHANNEL}”/>
原理很簡單,gradle編譯的時候,會根據這個配置,把manifest里對應的metadata占位符替換成指定的值。然后Android這邊在運行期再去取出來就是:
publicstaticString?getChannel(Context?context)?{
String?channel?=?"";
PackageManager?pm?=?sContext.getPackageManager();
try?{
ApplicationInfo?ai?=?pm.getApplicationInfo(
context.getPackageName(),
PackageManager.GET_META_DATA);
String?value?=?ai.metaData.getString("CHANNEL");
if?(value?!=?null)?{
channel?=?value;
}
}?catch?(Exception?e)?{
//?忽略找不到包信息的異常
}
returnchannel;
}
這個辦法,缺點很明顯,每打一個渠道包都會完整得執行一遍apk的編譯打包流程,非常慢。近30個包要打一個多小時…優點就是不依賴其他工具,gradle自己就能搞定。
2.替換Assets資源打包
assets用于存放一些資源。不同與res,assets里的資源編譯時原樣保留,不需要生成什么resouce id之類的東西。因此,我們可以通過替換assets里的文件打出不同的渠道包,而不用每次都重新編譯。
我們知道apk本質上就是個zip文件,那么我們就可以通過解壓縮->替換文件->壓縮的辦法來搞定:
這里給出一份Python3的實現
#?解壓縮
src_file_path?=?'原始apk文件路徑'
extract_dir?=?'解壓的目標目錄路徑'
os.makedirs(extract_dir,?exist_ok=True)
os.system(UNZIP_PATH?+?'?-o?-d?%s?%s'%?(extract_dir,?src_file_path))
#?刪除簽名信息
shutil.rmtree(os.path.join(extract_dir,'META-INF'))
#?寫入渠道文件assets/channel.conf
channel_file_path?=?os.path.join(extract_dir,'assets','channel.conf')withopen(channel_file_path,?mode='w')asf:
f.write(channel)??#?寫入渠道號寫進去
os.chdir(extract_dir)
output_file_name?=?'輸出文件名稱'
output_file_path?=?'輸出文件路徑'
output_file_path_tmp?=?os.path.join(output_dir,?output_file_name?+'_tmp.apk')
#?壓縮
os.system(ZIP_PATH?+?'?-r?%s?*'%?output_file_path)
os.rename(output_file_path,?output_file_path_tmp)
#?重新簽名
#?jarsigner?-sigalg?MD5withRSA?-digestalg?SHA1?-keystore?your_keystore_path
#?-storepass?your_storepass?-signedjar?your_signed_apk,?your_unsigned_apk,?your_alias
signer_params?=?'?-verbose?-sigalg?MD5withRSA?-digestalg?SHA1'+?\
'?-keystore?%s?-storepass?%s?%s?%s?-sigFile?CERT'%?\
(
sign,?#?簽名文件路徑
store_pass,?#?存儲密碼
output_file_path_tmp,
alias?#?別名
)
os.system(JAR_SIGNER_PATH?+?signer_params)
#?Zip對齊
os.system(ZIP_ALIGN_PATH?+?'?-v?4?%s?%s'%?(output_file_path_tmp,?output_file_path))
os.remove(output_file_path_tmp)
在這里,幾個PATH分別表示zip、unzip、jarsigner和zipalign這幾個可執行文件的路徑。
簽名是apk的一個重要機制,它給apk里的每一個文件(META-INF目錄下的除外)計算一個hash值,記錄在META-INF下的若干文件里。Zip對齊能夠優化運行時Android讀取資源的效率,這一步雖然不是必須的,但還是推薦做一下。
采用這個方法,我們不需要再編譯Java代碼,速度有極大地提升。大約每10秒就能打一個包。
同時給出讀取渠道號的實現代碼:
publicstaticString?getChannel(Context?context)?{
String?channel?=?"";
InputStream?is=null;
try?{
is=?context.getAssets().open("channel.conf");
byte[]?buffer?=?new?byte[100];
intl?=is.read(buffer);
channel?=?new?String(buffer,?0,?l);
}?catch?(IOException?e)?{
//?如果讀不到,那么取缺省值
}?finally?{
if?(is!=null)?{
try?{
is.close();
}?catch?(Exception?ignored)?{
}
}
}
returnchannel;
}
順便說一下,還可以用aapt這個工具來替代zip&unzip來實現文件替換:
#?替換assets/channel.conf
os.chdir(base_dir)
os.system(AAPT_PATH?+?'?remove?%s?assets/channel.conf'%?output_file_path_tmp)
os.system(AAPT_PATH?+?'?add?%s?assets/channel.conf'%?output_file_path_tmp)
3.美團給出的一種方案
剛才上文提到META-INF目錄對簽名機制是豁免的,往這里面放東西就可以免去重簽名這一步,美團技術團隊就是這么做的。
import?zipfile
zipped?=?zipfile.ZipFile(your_apk,?'a',?zipfile.ZIP_DEFLATED)
empty_channel_file?=?"META-INF/mtchannel_{channel}".format(channel=your_channel)
zipped.write(your_empty_file,?empty_channel_file)
給META-INFO目錄加入一個名為“mtchannel_渠道號”的空文件,在Java這邊查找到這個文件,取得文件名即可:
publicstaticString?getChannel(Context?context)?{
ApplicationInfo?appinfo?=?context.getApplicationInfo();
String?sourceDir?=?appinfo.sourceDir;
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("mtchannel"))?{
ret?=?entryName;
break;
}
}
}?catch?(IOException?e)?{
e.printStackTrace();
}?finally?{
if?(zipfile?!=?null)?{
try?{
zipfile.close();
}?catch?(IOException?e)?{
e.printStackTrace();
}
}
}
String[]?split?=?ret.split("_");
if?(split?!=?null&&?split.length?>=?2)?{
returnret.substring(split[0].length()?+?1);
}?else{
return"";
}
}
這個方法省去了重簽名這一步,速度提升也很大。他們的描述是“900多個渠道不到一分鐘就能打完”,也就是不到0.06s一個包。
4.利用Zip文件comment的終極方案
另外給出了一個終極方案:我們知道Zip文件末尾有一塊區域,可以用來存放文件的comment。改動這個區域,絲毫不會影響Zip文件的內容。
打包的代碼很簡單:
shutil.copyfile(src_file_path,?output_file_path)
withzipfile.ZipFile(output_file_path,?mode='a')aszipFile:
zipFile.comment?=?bytes(channel,?encoding=‘utf8')
這個方法比前一個方法的區別在于,它不會修改Apk的內容,也就不必重新打包,速度又有提升!
按文檔中的說法,這個方法1s內可以打300多個包,也就是說單個包的時間小于10毫秒!
讀取的代碼稍微復雜一些。
Java 7的ZipFile類,有getComment方法,可以輕易地讀取comment值。然而這個方法只在Android 4.4以及更高版本才可用,我們就需要多花點時間把這段邏輯移植過來。所幸這里的邏輯不復雜,我們查看源碼,可以看到主要邏輯都在ZipFile的一個私有方法readCentralDir里,一小部分讀取二進制數據的邏輯在libcore.io.HeapBufferIterator,全部搬過來,整理一下就搞定了:
publicstaticString?getChannel(Context?context)?{
String?packagePath?=?context.getPackageCodePath();
RandomAccessFile?raf?=?null;
String?channel?=?"";
try?{
raf?=?new?RandomAccessFile(packagePath,?"r");
channel?=?readChannel(raf);
}?catch?(IOException?e)?{
//?ignore
}?finally?{
if?(raf?!=?null)?{
try?{
raf.close();
}?catch?(IOException?e)?{
//?ignore
}
}
}
returnchannel;}privatestaticfinal?long?LOCSIG?=?0x4034b50;privatestaticfinal?long?ENDSIG?=?0x6054b50;privatestaticfinalintENDHDR?=?22;privatestaticshort?peekShort(byte[]?src,intoffset)?{
return(short)?((src[offset?+?1]?<
//?Scan?back,?looking?fortheEndOfCentral?Directory?field.?If?the?zip?file?doesn't
//?have?an?overall?comment?(unrelated?toanyper-entry?comments),?we'll?hit?the?EOCD
//?onthefirsttry.
//?Noneedtosynchronize?raf?here--?we?only?do?this?when?we?first?open?the?zip?file.
long?scanOffset?=?raf.length()?-?ENDHDR;
if?(scanOffset?
throw?new?ZipException("File?too?short?to?be?a?zip?file:?"+?raf.length());
}
raf.seek(0);
final?intheaderMagic?=Integer.reverseBytes(raf.readInt());
if?(headerMagic?==?ENDSIG)?{
throw?new?ZipException("Empty?zip?archive?not?supported");
}
if?(headerMagic?!=?LOCSIG)?{
throw?new?ZipException("Not?a?zip?archive");
}
long?stopOffset?=?scanOffset?-?65536;
if?(stopOffset?
stopOffset?=?0;
}
while?(true)?{
raf.seek(scanOffset);
if?(Integer.reverseBytes(raf.readInt())?==?ENDSIG)?{
break;
}
scanOffset--;
if?(scanOffset?
throw?new?ZipException("End?Of?Central?Directory?signature?not?found");
}
}
//?ReadtheEndOfCentral?Directory.?ENDHDR?includes?the?signature?bytes,
//?which?we've?already?read.
byte[]?eocd?=?new?byte[ENDHDR?-?4];
raf.readFully(eocd);
//?Pull?outthe?information?we?need.
intposition?=?0;
intdiskNumber?=?peekShort(eocd,?position)?&?0xffff;
position?+=?2;
intdiskWithCentralDir?=?peekShort(eocd,?position)?&?0xffff;
position?+=?2;
intnumEntries?=?peekShort(eocd,?position)?&?0xffff;
position?+=?2;
inttotalNumEntries?=?peekShort(eocd,?position)?&?0xffff;
position?+=?2;
position?+=?4;?//?IgnorecentralDirSize.
//?long?centralDirOffset?=?((long)?peekInt(eocd,?position))?&?0xffffffffL;
position?+=?4;
intcommentLength?=?peekShort(eocd,?position)?&?0xffff;
position?+=?2;
if?(numEntries?!=?totalNumEntries?||?diskNumber?!=?0?||?diskWithCentralDir?!=?0)?{
throw?new?ZipException("Spanned?archives?not?supported");
}
String?comment?=?"";
if?(commentLength?>?0)?{
byte[]?commentBytes?=?new?byte[commentLength];
raf.readFully(commentBytes);
comment?=?new?String(commentBytes,?0,?commentBytes.length,?Charset.forName("UTF-8"));
}
returncomment;
}
需要注意的是,Android 7.0加入了APK Signature Scheme v2技術。在Android Plugin for Gradle 2.2,這一技術是缺省啟用的,這會導致第三、第四兩種方法打出的包在Android 7.0下面校驗失敗。解決方法有二,一是把Gradle版本改低,二是在signingConfigs/release下面加上配置v2SigningEnabled false。詳細說明見谷歌的文檔
總結
用表格說話
【編輯推薦】
【責任編輯:枯木 TEL:(010)68476606】
點贊 0
總結
以上是生活随笔為你收集整理的android 渠道打包工具,Android渠道打包技术小结的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python整数预测_时间序列预测全攻略
- 下一篇: php 删除文件夹及文件夹,php删除一