介绍本文比较了渠道的四种渠道打包方式:不同于iOS的单一渠道(AppStore),Android平台在国内有很多渠道。以我们的App为例,共有27个常用渠道(影宝、百度、360)和更多的专用渠道进行推广。我们的封装技术也经历了数次改进。1.使用GradleProductFavor打包android{productFlavors{base{manifestPlaceholders=[CHANNEL:”0”]}yingyongbao{manifestPlaceholders=[CHANNEL:”1”]}baidu{manifestPlaceholders=[CHANNEL:”2”]}}}AndroidManifest.xml原理很简单。编译gradle时,会将manifest中对应的元数据占位符替换为指定的值。然后在运行时在Android端取出来:Stringvalue=ai.metaData.getString("CHANNEL");if(value!=null){channel=value;}}catch(Exceptione){//忽略找不到包信息的异常}returnchannel;}这个方法缺点很明显,每次创建通道包时,apk的编译打包过程都会被完整执行,速度很慢。制作近30个包,耗时一个多小时。。。好处是不需要依赖其他工具,gradle自己搞定。2.替换Assets资源打包assets来存放一些资源。与res不同的是,assets中的资源在编译时保持原样,不需要生成资源id之类的。因此,我们可以通过替换assets中的文件来创建不同的频道包,而不必每次都重新编译。我们知道apk本质上是一个zip文件,所以我们可以通过解压->替换文件->压缩来搞定:这里是Python3的实现#unzipsrc_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.confchannel_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-sigalgMD5withRSA-digestalgSHA1-keystoreyour_keystore_path#-storepassyour_storepass-signedjaryour_signed_apk,your_unsigned_apk,your_aliassigner_params='-verbose-sigalgMD5withRSA-digestalgSHA1'+\'-keystore%s-storepass%s%s%s-sigFileCERT'%\(sign,#签名文件路径store_pass,#store密码输出ut_file_path_tmp,alias#alias)os.system(JAR_SIGNER_PATH+signer_params)#Zip对齐os.system(ZIP_ALIGN_PATH+'-v4%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秒可以制作一个数据包。同时给出读取通道号的实现代码:publicstaticStringgetChannel(Contextcontext){Stringchannel="";InputStreamis=null;try{is=context.getAssets().open("channel.conf");byte[]buffer=newbyte[100];intl=is.read(buffer);channel=newString(buffer,0,l);}catch(IOExceptione){//如果读取不到,则取默认值}finally{if(is!=null){try{is.close();}catch(Exceptionignored){}}}returnchannel;}对了,也可以使用aapt工具替换zip&unzip实现文件替换:#Replaceassets/channel.confos.chdir(base_dir)os.system(AAPT_PATH+'remove%sassets/channel.conf'%output_file_path_tmp)os.system(AAPT_PATH+'add%sassets/channel.conf'%output_file_path_tmp)3.给出的解决方案美团刚刚在上面提到了META-INF目录是免签名机制的。把东西放进去可以避免重新签名的步骤。美团技术团队就是这么做的。importzipfilezipped=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_channelnumber”的空文件,在Java端找到这个文件,得到文件名:publicstaticStringgetChannel(Contextcontext){ApplicationInfoappinfo=context.getApplicationInfo();StringsourceDir=appinfo.sourceDir;Stringret="";ZipFilezipfile=null;try{zipfile=newZipFile(sourceDir);Enumeration>entries=zipfile.entries();while(entries.hasMoreElements()){ZipEntryentry=((ZipEntry)entries.nextElement());StringentryName=entry.getName();if(entryName.startsWith("mtchannel")){ret=entryName;break;}}}catch(IOExceptione){e.printStackTrace();}最后{if(zipfile!=null){try{zipfile.close();}catch(IOExceptione){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文件注释的终极解决方案给出另一种终极解决方案:我们知道在Zip文件的末尾有一个区域,可以用来存放文件的注释。更改此区域根本不会影响Zip文件的内容。打包后的代码很简单:shutil.copyfile(src_file_path,output_file_path)和zipfile.ZipFile(output_file_path,mode='a')aszipFile:zipFile.comment=bytes(channel,encoding='utf8')这种方法和之前的方法是因为不会修改Apk的内容,所以不需要重新打包,速度提高了!根据文档,这种方法可以在1秒内打印300多个包,也就是说单个包的时间不到10毫秒!Read抓取的代码稍微复杂一些。Java7的ZipFile类有一个getComment方法,可以方便的读取注释值。但是这个方法只有Android4.4及以上版本才有,所以我们需要花更多的时间来移植这个逻辑。幸运的是,这里的逻辑并不复杂。我们查看源码可以看到,主要逻辑在ZipFile的一个私有方法readCentralDir中,一小部分读取二进制数据的逻辑在libcore.io.HeapBufferIterator中。把它们都搬过来,把它们整理好。向上:publicstaticStringgetChannel(Contextcontext){StringpackagePath=context.getPackageCodePath();RandomAccessFileraf=null;Stringchannel="";try{raf=newRandomAccessFile(packagePath,"r");channel=readChannel(raf);}catch(IOExceptione){//ignore}finally{if(raf!=null){try{raf.close();}catch(IOExceptione){//ignore}}}returnchannel;}privatestaticfinallongLOCSIG=0x4034b50;privatestaticfinallongENDSIG=0x6054b50;privatestaticfinalintENDHDR=22;privatestaticshortpeekShort(byte[]src,intoffset){return(short)((src[offset+1]<<8)|(src[offset]&0xff));}privatestaticStringreadChannel(RandomAccessFileraf)throwsIOException{//Scanback,lookingfortheEndOfCentralDirectoryfield.Ifthezipfiledoesn't//有一个整体评论(与任何条目评论无关),我们将在第一次尝试时点击EOCD。//Noneedtosynchronizerafhere--weonlydothiswhenwefirststopenthezipfile.longscanOffset=raf.length()-ENDHDR;if(scanOffset<0){thrownewZipException("Filetooshorttobeazipfile:"+raf.length());}raf.seek(0);finalintheaderMagic=Integer.reverseBytes(raf.readInt());if(headerMagic==ENDSIG){thrownewZipException("Emptyziparchivenotsupported");}if(headerMagic!=LOCSIG){thrownewZipException("Notaziparchive");}longstopOffset=scanOffset-65536;if(stopOffset<0){stopOffset=0;}while(true){raf.seek(scanOffset);if(Integer.reverseBytes(raf.readInt())==ENDSIG){break;}scanOffset--;if(scanOffset0){byte[]commentBytes=newbyte[commentLength];raf.readFully(commentBytes);comment=newString(commentBytes,0,commentBytes.length,Charset.forName("UTF-8"));}returncomment;}需要注意的是,Android7.0在AndroidPluginforGradle2.2中加入了APKSignatureSchemev2技术。该技术默认开启,在Android7.0下会导致第三种和第四种方法输出包验证失败。解决方案有两种,一种是降低Gradle版本,另一种是在signingConfigs/release下添加v2SigningEnabledfalse。详情请看谷歌的文档摘要以表格形式发言