Android 多渠道打包方式详解

   <p>面试的时候,如果面试官突然问到:你们渠道包是怎么打的?如果你说是用gradle一个一个编译的,然后他很鄙视的说这个效率太低啦,你们写过什么脚本自己打渠道包没?你肯定心里想,卧槽,这么狂炫吊炸天,自己写脚本打包?!其实这个根本也不是太难啦!!今天就来聊聊多渠道打包的原理以及如何自己DIY多渠道打包的工具!</p>    <h3><strong>渠道包出现</strong></h3>    <p>当一个产品到发版的时候,我们搞Android的就会面临一个超级尴尬的问题:国内这么多的渠道,渠道统计是必须做滴,那么十多个主要渠道再加无限量的地推渠道包就成了一个巨坑了!这一块耗费的时间是一个无底洞啊!!!</p>    <h3><strong>方式一览</strong></h3>    <p>这里一共会介绍三种渠道包的实现方式,分别是:</p>    <p>1、使用gradle配置直接编译出不同的渠道包。</p>    <p>2、通过反编译修改对应的渠道号。</p>    <p>3、META-INF里面新加一个文件。</p>    <h3><strong>Gradle方式</strong></h3>    <p>不管是用友盟统计还是其他什么的,首先肯定都是要有一些准备工作的,由于本人就比较了解友盟的,所以就用友盟统计来举例啦!</p>    <p>友盟统计提供了两种渠道统计策略,其实就是一个自动挡的一个手动挡的。</p>    <pre>  <code class="language-java"><meta-data          android:name="UMENG_APPKEY"          android:value="xxxxxxxx"/>      <meta-data          android:name="UMENG_CHANNEL"          android:value="${GRADLE_CHANNEL_VALUE}"/></code></pre>    <p>在对应的build.gradle里面配置对应的信息:</p>    <pre>  <code class="language-java">productFlavors.all { flavor ->      flavor.manifestPlaceholders = [GRADLE_CHANNEL_VALUE: name]  }  productFlavors {      dev {      }      baidu {          minSdkVersion 18          applicationId "com.test.michat"      }  }</code></pre>    <p>如果手动去设置对应的渠道号的话,就在程序入口处调用以下方法:</p>    <pre>  <code class="language-java">MobclickAgent. startWithConfigure(UMAnalyticsConfig config)    UMAnalyticsConfig(Context context, String appkey, String channelId)    UMAnalyticsConfig(Context context, String appkey, String channelId, EScenarioType eType)    UMAnalyticsConfig(Context context, String appkey, String channelId, EScenarioType eType,Boolean isCrashEnable)</code></pre>    <p>那么怎么获取到对应的渠道号呢?!这个方法在之后的所有方式中都要使用滴,其实不管是哪种方式,最后都会调用这个方法去读相关数据的!!</p>    <pre>  <code class="language-java">private String getChannel(Context context) {      try {          PackageManager pm = context.getPackageManager();          ApplicationInfo appInfo = pm.getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA);          return appInfo.metaData.getString("CHANNEL_VALUE");      } catch (PackageManager.NameNotFoundException ignored) {      }      return "";    }</code></pre>    <p style="text-align:center"><img src="https://simg.open-open.com/show/103deca13cc896ef2bfc524790c27688.png"></p>    <p style="text-align:center">buildVariants.png</p>    <p>当然你也可以使用命令行:gradlew assemble 组装出所有的渠道包!!</p>    <h3><strong>反编译方式</strong></h3>    <p>gradle方式用着也挺不错的,为什么还要去搞什么反编译这么麻烦的东西呢?因为它有一个很大的问题,那就是每一个包都是要去编译打包的! 这是相当的耗时!time is 加班啊!谁也不想加班打渠道包咯!! 反编译的方式就是节省了每个渠道包都去编译的时间,而是编译好一个渠道包之后就使用该渠道包,通过反编译动态修改 AndroidManifest.xml 里面的信息,然后再重新打包签名!</p>    <p>说到反编译,那么这里就不得不提大名鼎鼎的 apktool.jar 了!纳尼,你说你从未听说过?!没事儿,以前没有听过,现在会用了就行了!!</p>    <p>然后总结一下接下来的一系列套路:</p>    <p>解包->修改相关参数->打包->签名->Zipalign优化</p>    <ul>     <li> <p>1、解包</p> <pre>  <code class="language-java">apktool d your_original_apk build</code></pre> <p>你没有看错,就是这样的!因为我们是站在巨人的肩膀上工作的嘛,所以好多工作就不同自己搞了!</p> <p>执行以上命令之后,如果不出什么意外,你就会得到一个文件夹:</p> </li>    </ul>    <p style="text-align:center"><img src="https://simg.open-open.com/show/43012cf38c4ed58c8bf0ee1654dbc3aa.png"></p>    <p style="text-align:center">反编译.png</p>    <p>相关代码:</p>    <pre>  <code class="language-java">try {          brut.apktool.Main.main(new String[]{"d", "-f", apkFilePath, "-o", outPath});          return true;      } catch (Exception e) {          e.printStackTrace();          callback("解包失败 !!!!!\r\n" + e.getMessage());      }</code></pre>    <ul>     <li> <p>2、修改对应的参数</p> <p>打开对应的 AndroidManifest.xml ,你没有看错,什么都在里面,直接修改就好了!等等,xml解析你不会?!没有关系,这里有dom4j.jar给你使用啦!!</p> </li>    </ul>    <p>修改反编译之后的AndroidManifest文件相关代码</p>    <pre>  <code class="language-java">try {          File androidManifestFile = new File(appFolderName + File.separator + "AndroidManifest.xml");          Document document = new SAXReader().read(androidManifestFile);//使用dom4j的sax解析          Element element = document.getRootElement().element("application");          List<Element> list = element.elements("meta-data");//获取到所有的“meta-data”          List<MetaData> metaData = manifest.getMetaData();          boolean isUpdate = false;          for (MetaData data : metaData) {              String name = data.getName();              String value = data.getValue();              callback(" meta-data name='" + name + "' value='" + value + "'");              for (Element s : list) {                  Attribute attribute = s.attribute("name");                  //更新相关渠道号                   if ( "UMENG_CHANNEL".equals(name)&&"UMENG_CHANNEL".equals(attribute.getValue())) {//更换相关的渠道号                      s.attribute("value").setValue(value);                      isUpdate = true;                      callback("更新1 AndroidManifest.xml meta-data name='" + attribute.getValue() + "' value='" + value + "'");                     break;                   }                }          }          if(isUpdate){//更新后重新写入              XMLWriter writer = new XMLWriter(new FileOutputStream(androidManifestFile));              writer.write(document);              writer.close();              callback("更新 AndroidManifest.xml 完成 ~ ");          }      } catch (Exception e) {          e.printStackTrace();          return false;      }</code></pre>    <ul>     <li> <p>3、打包</p> <pre>  <code class="language-java">apktool b build your_unsigned_apk</code></pre> </li>    </ul>    <p>还是这么简单:</p>    <pre>  <code class="language-java">try {          brut.apktool.Main.main(new String[]{"b", buildApkFolderPath, "-o", buildApkOutPath});          return true;      } catch (Exception e) {          e.printStackTrace();          callback("打包失败 !!!!!\r\n" + e.getMessage());      }</code></pre>    <ul>     <li> <p>4、签名</p> <pre>  <code class="language-java">jarsigner -sigalg MD5withRSA -digestalg SHA1 -keystore your_keystore_path -storepass your_storepass -signedjar your_signed_apk, your_unsigned_apk, your_alias</code></pre> <p>这个是jdk里面直接提供了的,只要你的环境变量配置好了的,就没有什么问题啦!</p> </li>    </ul>    <p>重新签名相关代码</p>    <pre>  <code class="language-java">executeCommand("jarsigner", "-verbose", "-sigalg", "SHA1withRSA", "-digestalg", "SHA1", "-keystore", keystoreFilePath, apkFilePath, alias, "-storepass", password);      /**   * 执行命令   *   * @param command 命令   */  private synchronized boolean executeCommand(String... command) {      Process process = null;      BufferedReader reader = null;      try {          ProcessBuilder builder = new ProcessBuilder();          builder.command(command);          builder.redirectErrorStream(true);          process = builder.start();          reader = new BufferedReader(new InputStreamReader(process.getInputStream(),"UTF-8"));          String line;          while ((line = reader.readLine()) != null) {              callback(line);              if (line.contains("Exception") || line.contains("Unable to open")) {                  return false;              }          }          return true;      } catch (IOException e) {          e.printStackTrace();          callback(e.getMessage());      } finally {          close(reader);          if (process != null) {              process.destroy();          }      }      return false;  }</code></pre>    <ul>     <li>5、Zipalign优化</li>    </ul>    <p><img src="https://simg.open-open.com/show/e10694da53977eaa5ec45c86b019dafd.png"></p>    <p style="text-align:center">Zipalign.png</p>    <p>如图所示,sdk/build-tools里面每个版本都是有这个东西的,加到环境变量中就好了!!!</p>    <p>zipalign 优化处理相关代码</p>    <pre>  <code class="language-java">* 需要安装并Android SDK并配置环境变量Build Tools路径   * 优化apk文件,这个需要Android Build Tools 中的zipalign程序文件   *   * @param apkFilePath 要优化的apk文件路径   * @param outFilePath 优化后的apk存放文件路径   */  public boolean zipalign(String apkFilePath, String outFilePath) {      return executeCommand("zipalign", "-f", "-v", "4", apkFilePath, outFilePath);  }</code></pre>    <h3><strong>美团方式</strong></h3>    <p>上面说的反编译要各种解包,打包,签名,相对也比较繁琐,然后我们可以发现,apk其实都是一个压缩包,我们直接在这个压缩包里添加对应的文件作为渠道号标记是不是又能省去上面繁琐的步奏呢?!打开一个APK文件之后你会看到 META-INF 这个文件夹!</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/bdb0f8005db315100b46165d09268a4e.png"></p>    <p style="text-align:center">apk压缩包.png</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/2b4adfd2426e6b22de20bb901c13fd13.png"></p>    <p style="text-align:center">META-INF.png</p>    <p>美团的方式就是在这里面直接再添加一个文件,然后通过这个文件的名称来指定对应的渠道号!</p>    <p>话不多说,直接上代码!!</p>    <pre>  <code class="language-java">public static void addUmengChannel(String filepath, String channel) {      String channel_title = "umengchannel_";      if(filepath.substring(filepath.lastIndexOf(".") + 1).toLowerCase().equals("apk")) {          String path2 = "";          if(filepath.lastIndexOf(File.separator) >= 0) {              path2 = filepath.substring(0, filepath.lastIndexOf(File.separator) + 1);//得到父路径          }            if(path2.length() != 0) {              File s = new File(filepath);//原始的apk              File t = new File(filepath.substring(0, filepath.lastIndexOf(".")) + "_" + channel + ".apk");//目标apk              if(!t.exists()) {//不存在就创建                  try {                      t.createNewFile();                  } catch (IOException var12) {                      var12.printStackTrace();                  }              }                Utils.fileChannelCopy(s, t);//拷贝原始apk到目标apk              File addFile = new File(path2 + channel_title + channel);//需要添加的渠道文件               if(!addFile.exists()) {                  try {                      addFile.createNewFile();                  } catch (IOException var11) {                      var11.printStackTrace();                  }              }                try {                  Utils.addFileToExistingZip(t, addFile);//将新加的渠道文件添加到目标apk文件中                  addFile.delete();              } catch (IOException var10) {                  var10.printStackTrace();              }            }      }  }       public static void addFileToExistingZip(File zipFile, File file) throws IOException {          File tempFile = File.createTempFile(zipFile.getName(), (String)null);          tempFile.delete();          boolean renameOk = zipFile.renameTo(tempFile);//拷贝          if(!renameOk) {              throw new RuntimeException("could not rename the file " + zipFile.getAbsolutePath() + " to " + tempFile.getAbsolutePath());          } else {              byte[] buf = new byte[1024];              ZipInputStream zin = new ZipInputStream(new FileInputStream(tempFile));              ZipOutputStream out = new ZipOutputStream(new FileOutputStream(zipFile));                for(ZipEntry entry = zin.getNextEntry(); entry != null; entry = zin.getNextEntry()) {                  String in = entry.getName();                  if(in.contains("umengchannel")) {//如果有重复的就不复制回去了!                      continue;                  }                    out.putNextEntry(new ZipEntry(in));                    int len1;                  while((len1 = zin.read(buf)) > 0) {                      out.write(buf, 0, len1);                  }              }                zin.close();              FileInputStream in1 = new FileInputStream(file);              out.putNextEntry(new ZipEntry("META-INF/" + file.getName()));//创建对应的渠道文件                int len2;              while((len2 = in1.read(buf)) > 0) {                  out.write(buf, 0, len2);              }                out.closeEntry();              in1.close();              out.close();              tempFile.delete();          }      }</code></pre>    <p style="text-align:center"><img src="https://simg.open-open.com/show/70bbe590cccd9169953897e71659ed4c.png"></p>    <p style="text-align:center"> </p>    <p style="text-align:center">渠道包完成.png</p>    <p>最后送上读取相关的方法:</p>    <pre>  <code class="language-java">public static String 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();                      //这里需要替换成你的那个key                  if (entryName.startsWith(YOUR_CHNANNEL_NAME)) {                      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) {              return ret.substring(split[0].length() + 1);            } else {              return "";          }      }</code></pre>    <p>当然你肯定要手动设置了,这个没法直接在清单文件中去配置了!!</p>    <h3>小结</h3>    <p>主要的方式就是这三种了!可以说一个比一个快,一个比一个的定制也要高,效率提高了,灵活性似乎就会下降的,至于到底使用哪种方式,还是根据实际情况灵活选择吧,反正到现在,这些方案都是很成熟的,没有什么坑!一不小心又说了几句废话啊!</p>    <h3> </h3>    <p> </p>    <p>来自:http://www.jianshu.com/p/f3f930fd4f6a</p>    <p> </p>