Android 多渠道打包提速

之前打包我们项目中还是用的Studio中的 build.gradle 里 配置 『productFlavors』,

以便使用Gradle构建Apk时,动态的替换manifest中的相应配置,来达到多渠道打包的目的

这样打包比较慢,今天看了网上流行的Python打包,原理如下:

> 使用Python或其它方式(如纯java),解压一个已经签名的apk(如果未签名,脚本中也可以动态签名)

> 在解压后的目录META-INF中,通过脚本代码动态创建一个规则命名的渠道空文件

> 将这空文件压入到新复制出的apk的META-INF目录下

> app中,动态读取 渠道文件名 的字符串,并使用


apk用的是java那一套签名,放在META-INF文件夹里的文件原则上是不参与签名的。

如果Google修改了apk的签名规则,这一套可能就不适用了。


这里贴一份纯Java实现上面提到的功能代码(出自:https://github.com/xiaohongmaosimida/apkSignAndAddChannels/blob/master/ChangeAPKChannel.java):

package com.stone;

import java.awt.BorderLayout;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.TransferHandler;

import com.umeng.analytics.MobclickAgent;

import net.lingala.zip4j.model.ZipParameters;

@SuppressWarnings("all")
class ChangeAPKChannel extends TransferHandler {

	private JTextArea textarea;
	private JButton button;
	private static final byte[] BUFFER = new byte[4096 * 1024];
	private static String keystorePath;
	private static String storepass; //这里放签名文件的密码
	private static String keypass;//这里放签名文件的密码
	private static String alias;//这里放签名文件的别名
	

	public ChangeAPKChannel(JTextArea filePathList, JButton button) {
		this.textarea = filePathList;
		this.button = button;
		
//		MobclickAgent.startWithConfigure(new com.umeng.analytics.MobclickAgent.UMAnalyticsConfig(application, "appkey", "channelId"));
	}

	public boolean importData(JComponent c, Transferable t) {
		try {
			List files = (List) t.getTransferData(DataFlavor.javaFileListFlavor);
			// FileReader reader = new FileReader((File)files.get(0));
			// textarea.read(reader, null);

			Iterator iterator = files.iterator();
			while (iterator.hasNext()) {
				File f = (File) iterator.next();
				if (f.isFile()) {
					textarea.setText(f.getAbsolutePath());
				} else {
					textarea.setText("不是标准文件");
				}
			}

			// reader.close();
			return true;
		} catch (UnsupportedFlavorException ufe) {
			ufe.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
		return false;
	}

	public boolean canImport(JComponent c, DataFlavor[] flavors) {
		for (int i = 0; i < flavors.length; i++) {
			if (DataFlavor.javaFileListFlavor.equals(flavors[i])) {
				return true;
			}
		}
		return false;
	}

	public static void main(String[] args) {
		keystorePath = "stone.keystore";
		storepass = "482935"; //这里放签名文件的密码
		keypass = "482935";//这里放签名文件的密码
		alias = "stone";//这里放签名文件的别名

		
		final JTextArea textarea = new JTextArea(10, 30);
		JButton button = new JButton("添加渠道");
		textarea.setText("拖动要添加渠道的apk到这个窗口里面 ");
		button.addActionListener(new ActionListener() {
			@Override
			@SuppressWarnings("empty-statement")
			public void actionPerformed(ActionEvent e) {
				String apk_path = "";
				if (textarea.getText().equals("不是标准文件")) {
					JOptionPane.showMessageDialog(null, "路径出错");
				} else {
					apk_path = textarea.getText().trim();
					if (!apk_path.substring(apk_path.length() - 3, apk_path.length()).equals("apk")) {
						JOptionPane.showMessageDialog(null, "不是apk文件哦");
						return;
					}
					try {
						ZipFile apkZipFile = new ZipFile(apk_path);
						File apkFile;
						Enumeration entries = apkZipFile.entries();
						String fileName;
						int i = 0;
						while (entries.hasMoreElements()) {
							fileName = entries.nextElement().getName().trim();
							if (fileName.contains("META-INF")) {
								i = 1;
								if (fileName.contains("META-INF/channel_")) {
									fileName = fileName.substring(fileName.indexOf("channel_") + 13, fileName.length());
									JOptionPane.showMessageDialog(null, "已有渠道:" + fileName);
									return;
								}
							}
						}
						String path = new File(".").getCanonicalPath();
						File file = new File(path);
						File[] listFiles = file.listFiles();
						System.out.println(apk_path);
						String apkName = apk_path.substring(apk_path.lastIndexOf("/") + 1,
								apk_path.lastIndexOf(".apk"));
						if (i == 0) {// 签名
							// JOptionPane.showMessageDialog(null,
							// "此包还没有签名,签名之后才能打渠道包哦");
							new Thread(new Runnable() {
								@Override
								public void run() {
									textarea.setText("正在签名...");
								}

							}).start();
							Runtime runtime = Runtime.getRuntime();
							Process process = null;
							BufferedReader br = null;
							createFiles(path + "/out_" + apkName + "/");
							String newApkPath = path + "/out_" + apkName + "/sign_nochannel_" + apkName + ".apk";
							for (File ff : listFiles) {
								if (ff.getName().toString().trim().contains(".keystore")) {
									keystorePath = ff.getPath().toString().trim();
									break;
								}
							}
							if (keystorePath == null || keystorePath.equals("")) {
								JOptionPane.showMessageDialog(null, "请把签名文件放在打包工具相同路径下");
								textarea.setText("请把签名文件放在打包工具相同路径下");
								return;
							}
							String cmd = "jarsigner -digestalg SHA1 -sigalg MD5withRSA -verbose -keystore "
									+ keystorePath + " -storepass " + storepass + " -signedjar " + newApkPath + " "
									+ apk_path + " " + alias + " -keypass " + keypass;

							System.out.println(cmd);
							process = runtime.exec(cmd);
							// 两个线程输出log
							new Thread(new StreamDrainer(process.getInputStream())).start();
							new Thread(new StreamDrainer(process.getErrorStream())).start();
							int exitValue = process.waitFor();
							System.out.println("返回值:" + exitValue);
							process.destroy();
							if (exitValue == 0) {
								JOptionPane.showMessageDialog(null, "签名完成,正在打包...");
								textarea.setText("已签名,正在打包...");
								// return;
							} else if (exitValue == 1) {
								JOptionPane.showMessageDialog(null, "签名错误,请联系@李朋");
								textarea.setText("签名错误,请联系@李朋");
							}
							apkFile = new File(newApkPath);
						} else {
							apkFile = new File(apk_path);
							textarea.setText("已签名,正在打包...");
							// JOptionPane.showMessageDialog(null, "已签名");
						}

						// JOptionPane.showMessageDialog(null, "此包还没有添加渠道信息");
						// 能走到这一步说明没有渠道,开始签渠道包

						// 获取info文件
						File infoFile = getFileFormParent(file, "info");
						// 获取info文件里面的包含channel_的渠道文件
						ArrayList channelFiles = getFilesFormParent(infoFile, "channels");

						// 遍历channel文件
						for (File channelFile : channelFiles) {
							// 创建一个放签名包的文件夹
							String channelFileName = channelFile.getName().toString().trim();
							channelFileName = channelFileName.substring(0, channelFileName.length() - 4);
							String channelAPKFolderName = path + "/out_" + apkName + "/" + channelFileName + "/";
							createFiles(channelAPKFolderName);
							// 读取渠道txt里的渠道数据
							InputStreamReader read = new InputStreamReader(new FileInputStream(channelFile), "UTF-8");// 考虑到编码格式
							BufferedReader bufferedReader = new BufferedReader(read);
							String lineTxt = null;
							ArrayList lines = new ArrayList();
							// 将渠道读取到字符串list里来
							while ((lineTxt = bufferedReader.readLine()) != null) {
								if (!lineTxt.equals("")) {
									lines.add(lineTxt.trim());
								}
							}
							// 关闭
							read.close();
							bufferedReader.close();
							// 遍历渠道信息
							String newAPKPath;
							for (String channel : lines) {
								System.out.println("下面打:" + channel + "的渠道");
								// 复制apk到文件夹下

								newAPKPath = channelAPKFolderName + channel + ".apk";
								System.out.println("签名包路径:" + newAPKPath);
								File newAPKFile = createFile(newAPKPath);
								fileChannelCopy(apkFile, newAPKFile);
								// 修改渠道创建一个渠道名的文件
								net.lingala.zip4j.core.ZipFile newAPKZipFile;

								newAPKZipFile = new net.lingala.zip4j.core.ZipFile(newAPKPath);

								File METAFile = new File(path + "/info/META-INF/");
								deleteFiles(METAFile);
								System.out.println(METAFile);
								createFiles(path + "/info/META-INF/");
								System.out.println(path + "/info/META-INF/channel_" + channel);
								createFile(path + "/info/META-INF/channel_" + channel);
								ZipParameters parameters = new ZipParameters();

								newAPKZipFile.addFolder(METAFile, parameters);
							}
						}
						JOptionPane.showMessageDialog(null, "添加渠道信息完成");
					} catch (Exception ex) {
						ex.printStackTrace();
						Logger.getLogger(ChangeAPKChannel.class.getName()).log(Level.SEVERE, null, ex);
					}
				}

			}
		});
		textarea.setTransferHandler(new ChangeAPKChannel(textarea, button));

		JFrame f = new JFrame("<签名>拖动要添加渠道信息的apk到这个窗口里面 ");
		f.getContentPane().add(new JScrollPane(textarea), BorderLayout.CENTER);
		f.getContentPane().add(new JScrollPane(button), BorderLayout.SOUTH);
		f.pack();
		f.setLocationRelativeTo(null);
		f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		f.setBounds(400, 300, 400, 500);
		f.setVisible(true);
	}

	// 从父文件夹获取等于此名字的子文件
	private static File getFileFormParent(File file, String name) {
		File[] f = file.listFiles();
		for (File ff : f) {
			if (ff.getName().toString().equals(name)) {
				return ff;
			}
		}
		return null;
	}

	// 从父文件夹获取包含此名字的子文件集合
	private static ArrayList getFilesFormParent(File file, String name) {
		File[] f = file.listFiles();
		ArrayList paths = new ArrayList();
		String fileName;
		for (File ff : f) {
			fileName = ff.getName().toString();
			if (fileName.contains(name)) {
				paths.add(ff);
			}
		}
		return paths;
	}

	// 创建一个文件夹,绝对路径
	private static File createFiles(File file) {
		// if (!file.exists()) {
		file.mkdirs();
		// }
		return file;
	}

	// 创建一个文件夹,绝对路径
	private static File createFiles(String path) {
		File file = new File(path);
		return createFiles(file);
	}

	// 删除一个文件夹,绝对路径
	private static boolean deleteFiles(File file) {
		if (file.exists()) {
			if (file.isDirectory()) {
				File[] listFiles = file.listFiles();
				for (File fffff : listFiles) {
					deleteFiles(fffff);
				}
				file.delete();
			} else {
				file.delete();
			}
		}
		return true;
	}

	// 创建一个文件,绝对路径
	private static File createFile(String path) {
		File file = new File(path);
		if (!file.exists()) {
			try {
				file.createNewFile();
			} catch (IOException ex) {
				Logger.getLogger(ChangeAPKChannel.class.getName()).log(Level.SEVERE, null, ex);
			}
		}
		return file;
	}

	public static void fileChannelCopy(File s, File t) {
		FileInputStream fi = null;
		FileOutputStream fo = null;
		FileChannel in = null;
		FileChannel out = null;

		try {
			fi = new FileInputStream(s);
			fo = new FileOutputStream(t);
			in = fi.getChannel();// 得到对应的文件通道
			out = fo.getChannel();// 得到对应的文件通道
			in.transferTo(0, in.size(), out);// 连接两个通道,并且从in通道读取,然后写入out通道
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			try {
				fi.close();
				in.close();
				fo.close();
				out.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}

}

class StreamDrainer implements Runnable {

	private InputStream ins;

	public StreamDrainer(InputStream ins) {
		this.ins = ins;
	}

	public void run() {
		try {
			BufferedReader reader = new BufferedReader(new InputStreamReader(ins));
			String line = null;
			while ((line = reader.readLine()) != null) {
				System.out.println(line);
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

}

上面最终会压入一个METAFile,在某些市场上传时,可能还要求apk zip aligned

附上一段linux/mac的shell脚本:

if [ -d "release" ]
then
    if [ -d "release/zipaligned" ]
    then
        rm -rf "release/zipaligned"
    fi
    mkdir "release/zipaligned"
    for file in ./release/*
    do
        if test -f $file
        then
            echo "${file##*/}"
            /Users/stone/Documents/sdk/build-tools/25.0.2/zipalign -v 4 $file release/zipaligned/${file##*/}        
        fi
    done
else
    echo "No release folder, can not go on zipalign"
fi

参考链接中,有windows的bat脚本,我这就不贴了


上面呢,也仅是在apk根据目录的META-INF目录建立了一个名为 channel_xxx 的空文件,

而还需要在代码中进行读取,读取的工具类(该类放在安卓项目中):

package base.utils;

import java.io.IOException;
import java.util.Enumeration;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager.NameNotFoundException;
import android.preference.PreferenceManager;
import android.text.TextUtils;

public class ChannelUtil {

	private static final String CHANNEL_KEY = "channel_key";
	private static final String READ_CHANNEL_KEY = "channel_";
	private static final String CHANNEL_VERSION_KEY = "channel_version_key";
	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, READ_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);
		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(NameNotFoundException e) {
			e.printStackTrace();
		}
		return -1;
	}
}

这样通过代码就能拿到渠道名了

String channel = ChannelUtil.getChannel(this, "360");

比如友盟统计中设置渠道(6.0版本后),以下摘自友盟统计开发指南:

如果希望在代码中配置Appkey、Channel、Token(Dplus)等信息,请在程序入口处调用如下方法:

MobclickAgent. startWithConfigure(UMAnalyticsConfig config)

UMAnalyticsConfig初始化参数类,提供多参数构造方式:

UMAnalyticsConfig(Context context, String appkey, String channelId)


好了,这样就可以了。如上,我们只需要有Java环境就可以打包了

当然,参考链接中,还有python脚本实现渠道名注入的相关指导

在此,感谢各路大大分享的精神~~


可能看第1部分关于注入渠道名的 一些代码有点累,这里放个demo地址:

https://github.com/aa86799/ApkChannelBuild


关于studio flavors打包方式,另见我的: 

Android 多渠道打包:使用Gradle和Android Studio


参考:

Android多渠道打包之Python打包 ---- http://www.jianshu.com/p/52a3c3187dcc

Android批量打包提速 - 1分钟900个市场不是梦 ------ http://www.cnblogs.com/ct2011/p/4152323.html




你可能感兴趣的:(Android,Studio,Android)