有一个需求,就是很多时候,我们的应用为了推广,都会鼓励用户邀请好友,然后给别人奖励,这个邀请的链接,可以动态的生成,但是,下载的apk里面,没法保存邀请人的信息,就没法确定到底是谁邀请的,所以呢,就有了这个需求。
1、三种实现方式
我找了一早上的资料,目前大概有3种方法:
- 邀请的时候,要求用户直接在网页上面注册,这样,就可以把是谁邀请的,直接保存在数据库中,这种方法适合大多数应用,但是很少的一部分,不行用户注册的,就不好弄了;
- 用户注册的时候,手动的填写邀请码,这种方式,对于很懒的用户,都懒得写,而且邀请码一般还不会太短,很麻烦的一个事情;
- 服务器根据邀请的链接,动态的打包apk,把邀请的信息直接写入apk,用户安装以后,就可以从apk里面读取出来,使用起来很方便,全过程用户无感知,可以说是最好的一种方式。当然,也有缺点,那就是签发apk的密钥库必须放在服务器上面,要保证安全,一次解压,打包,签名,花费的时间,还是比较长的,大概下几十秒。
2、重新打包的两种实现方式
前面两种都很简单就可以实现,下面我来说一下第三种实现的方式。
第三种方法,写入信息,我是直接写入到assets文件夹里面的,因为这个文件夹本来是保存静态资源的,而且不会编译,正合我意,哈哈。在android里面,直接读取就可以了:
//读取一个资源文件
InputStream open = getResources().getAssets().open("ad.conf");
String text=in2String(open);
//转换成字符串的函数
public String in2String(InputStream in){
ByteArrayOutputStream byteOut=new ByteArrayOutputStream();
byte[] b=new byte[1024];
int len;
try {
while ((len=in.read(b))!=-1){
byteOut.write(b,0,len);
}
return byteOut.toString("UTF-8");
} catch (IOException e) {
e.printStackTrace();
}
return "";
}
第三种方法,其实也有2种实现的方式:
- 第一种是我看到网上大多数人使用的方式,就是使用apktool来解压,打包,这种方式不是很好,如果是使用360、百度等加固了的apk,会失败,打包的时候,会提示找不到资源;
- 因为apk其实就是一个zip压缩包,所以,直接使用unzip,zip(Liunx下可以直接调用命令)就可以了,推荐使用这种方法实现。
2.1、使用apktool来实现
我是在ubuntu 14.4 LTS+ JDK1.8下面通过测试的,因为一般服务器上都是部署在Liunx上,所以,最好使用Liunx来开发。
需要的工具:
- java–这个不说了
- apktool–点击这里去下载 我使用的是2.2.1
- jarsigner java自带的前面工具,一般在{jdk}/bin下面
详细的实现如下:
import java.io.*;
/**
* 本类是演示使用apktool在动态的解压,打包,签名一个apk,用来向apk写入一些信息,比如推广的时候
* 使用本类需要在执行的机器上面配置java,apktool。
* Author:Mr.Jie
* DateTime:16-11-6 上午11:02
*/
public class Main {
public static void main(String[] args) {
Process process = null;
//记录处理开始的时间
long start = System.currentTimeMillis();
//为了方便使用我把所有需要配置的信息,全部写在下面了(所有配置均不能包含空格)
//java的位置,如果你有环境变量,可以直接写java
String javaPath="java";
//apktool路径
String apktoolPath = "apktool_2.2.1.jar";
//需要解压的apk文件路径
String apkPath = "my.apk";
//保存解压以后文件夹的路径
String unPackagePath = "myapp/";
//重新打包以后,apk存放的路径
String rePackagePath = "repackage.apk";
//签名以后apk的
String signedApkPath = "signedApk.apk";
//jarsigner的路径,一般在jdk的bin目录,用于对apk签名
String jarsignerPath = "jarsigner";
//用于签名的密钥库的路径
String keyPath = "testkey.jks";
//密钥库完整性的口令
String keyStorepass = "123456";
//使用密钥库里面密钥的别名
String keyAlias = "测试签名";
//密钥的口令
String keypass = "123456";
//1----解压apk
try {
//解压apk文件包
String unPackageCmd = javaPath+" -jar " + apktoolPath + " d -f -o " + unPackagePath + " " + apkPath;
System.out.println("1.正在执行解压:" + unPackageCmd);
process = Runtime.getRuntime().exec(unPackageCmd);
if (process.waitFor() != 0) {
System.out.println("解压失败,过程终止");
return;
}
System.out.println("解压成功");
} catch (Exception e) {
e.printStackTrace();
}
//2----内容修改,这里是在assets文件夹里面写入信息
OutputStreamWriter osw = null;
try {
String targetFilePath = unPackagePath + "assets";
File wfile = new File(targetFilePath);
if (!wfile.exists()) {
wfile.mkdirs();
}
wfile = new File(wfile, "ad.conf");
String content = "这里是解压以后,新写入的内容";
System.out.println("2.正在执行写入:" + content);
osw = new OutputStreamWriter(new FileOutputStream(wfile));
osw.write(content, 0, content.length());
osw.flush();
System.out.println("写入文件新内容成功");
} catch (Exception e) {
e.printStackTrace();
System.out.println("写入文件信息出错,过程终止");
return;
} finally {
try {
osw.close();
} catch (Exception e) {
e.printStackTrace();
}
}
//3----重新打包
try {
String packageCmd =javaPath+" -jar " + apktoolPath + " b -o " + rePackagePath + " " + unPackagePath;
System.out.println("3.正在重新打包apk:" + packageCmd);
process = Runtime.getRuntime().exec(packageCmd);
if (process.waitFor() != 0) {
System.out.println("打包失败,过程终止");
return;
}
System.out.println("打包成功");
} catch (Exception e) {
e.printStackTrace();
}
//4----对apk签名
try {
String signCmd = jarsignerPath + " -keystore " + keyPath + " -storepass " + keyStorepass + " -signedjar " + signedApkPath + " " + rePackagePath + " -keypass " + keypass + " " + keyAlias;
System.out.println("4.正在签名apk:" + signCmd);
process = Runtime.getRuntime().exec(signCmd);
if (process.waitFor() != 0) {
System.out.println("签名失败。。。");
return;
}
System.out.println("签名成功");
} catch (Exception e) {
e.printStackTrace();
}
//5----删除临时文件
try {
System.out.println("5.正在删除临时文件");
//删除生成的没有前面的apk
delDir(rePackagePath);
//删除解压的目录
delDir(unPackagePath);
} catch (Exception e) {
e.printStackTrace();
System.out.println("删除临时文件出错:"+e.getMessage());
}
float end = (System.currentTimeMillis() - start) / 1000f;
System.out.println("解压,打包,签名apk完成,一共耗时:" + end + "S");
}
/**
* 用来删除一个文件夹或者文件(写的有点麻烦,主要是删除文件夹,如果在Liunx下面,可以直接勇敢的调用rm命令,为了兼容windows,我就直接用java实现了)
* @param filePath 文件夹路径
*/
private static void delDir(String filePath) {
File file = new File(filePath);
//不存在就返回
if (!file.exists()) return;
if (file.isFile()) {
//是文件就直接删除
file.delete();
return;
} else {
//是文件夹就需要迭代的删除
File[] files = file.listFiles();
int len = files.length;
for (int i = 0; i < len; i++) {
if (files[i].isFile()) {
//是文件就直接删除
files[i].delete();
} else {
//是文件夹就递归调用
delDir(files[i].getPath());
}
}
file.delete();
}
}
}
到这里就完全实现了,跑一个看看:
1.正在执行解压:java -jar apktool_2.2.1.jar d -f -o myapp/ my.apk
解压成功
2.正在执行写入:这里是解压以后,新写入的内容
写入文件新内容成功
3.正在重新打包apk:java -jar apktool_2.2.1.jar b -o repackage.apk myapp/
打包成功
4.正在签名apk:jarsigner -keystore testkey.jks -storepass 123456 -signedjar signedApk.apk repackage.apk -keypass 123456 测试签名
签名成功
5.正在删除临时文件
解压,打包,签名apk完成,一共耗时:20.174S
Process finished with exit code 0
2.2、使用zip,unzip来实现
这种方法,如果在Liunx下直接调用命令,就很简单了,我这个也是在Ubuntu 14.04 LTS下面测试通过的。
过程和上面差不多,直接解压,写信息,打包的方式,不过要把原来的签名信息删除掉,不然安装的时候,会提示签名错误。
全部实现如下,是直接调用Linux命令实现的:
import java.io.*;
/**
* Author:Mr.Jie
* DateTime:16-11-6 下午2:30
*/
public class ZipApk {
public static void main(String[] args) {
Process process = null;
//记录处理开始的时间
long start = System.currentTimeMillis();
//为了方便使用我把所有需要配置的信息,全部写在下面了(所有配置均不能包含空格)
//java的位置,如果你有环境变量,可以直接写java
//需要解压的apk文件路径
String apkPath = "my.apk";
//保存解压以后文件夹的路径
String unPackagePath = "myapp/";
//重新打包以后,apk存放的路径
String rePackagePath = "repackage.apk";
//签名以后apk的
String signedApkPath = "signedApk.apk";
//jarsigner的路径,一般在jdk的bin目录,用于对apk签名
String jarsignerPath = "jarsigner";
//用于签名的密钥库的路径
String keyPath = "testkey.jks";
//密钥库完整性的口令
String keyStorepass = "123456";
//使用密钥库里面密钥的别名
String keyAlias = "测试签名";
//密钥的口令
String keypass = "123456";
//1----解压apk
try {
//解压apk文件包
String unPackageCmd = "unzip -o " + apkPath + " -d " + unPackagePath;
System.out.println("1.正在执行解压:" + unPackageCmd);
process = Runtime.getRuntime().exec(unPackageCmd);
if (process.waitFor() != 0) {
System.out.println(in2String( process.getInputStream()));
System.out.println("解压失败,过程终止");
return;
}
System.out.println("解压成功");
} catch (Exception e) {
e.printStackTrace();
}
//2----写入信息
OutputStreamWriter osw = null;
try {
String targetFilePath = unPackagePath + "assets";
File wfile = new File(targetFilePath);
if (!wfile.exists()) {
wfile.mkdirs();
}
wfile = new File(wfile, "ad.conf");
String content = "这里是解压以后,新写入的内容";
System.out.println("2.正在执行写入:" + content);
osw = new OutputStreamWriter(new FileOutputStream(wfile));
osw.write(content, 0, content.length());
osw.flush();
System.out.println("写入文件新内容成功");
} catch (Exception e) {
e.printStackTrace();
System.out.println("写入文件信息出错,过程终止");
return;
} finally {
try {
osw.close();
} catch (Exception e) {
e.printStackTrace();
}
}
//3----删除原来的签名,不然最后打包出来的应用安装的时候,会提示签名错误
String s=unPackagePath+"META-INF";
System.out.println("3.删除原来的签名等信息:"+s);
delDir(s);
System.out.println("删除成功");
//4----重新打包
try {
String packageCmd ="zip -r "+rePackagePath+" .";
System.out.println("3.正在重新打包apk:" + packageCmd);
process = Runtime.getRuntime().exec(packageCmd,null,new File(unPackagePath));
if (process.waitFor() != 0) {
System.out.println(in2String( process.getInputStream()));
System.out.println("打包失败,过程终止");
return;
}
System.out.println("打包成功");
} catch (Exception e) {
e.printStackTrace();
}
//5----打包以后目录不对,移动到指定的位置
try {
String moveCmd ="mv "+rePackagePath+" ..";
System.out.println("3.正在移动重新打包apk:" + moveCmd);
process = Runtime.getRuntime().exec(moveCmd,null,new File(unPackagePath));
if (process.waitFor() != 0) {
System.out.println(in2String( process.getInputStream()));
System.out.println("移动失败,过程终止");
return;
}
System.out.println("移动成功");
} catch (Exception e) {
e.printStackTrace();
}
//6----对apk签名
try {
String signCmd = jarsignerPath + " -keystore " + keyPath + " -storepass " + keyStorepass + " -signedjar " + signedApkPath + " " + rePackagePath + " -keypass " + keypass + " " + keyAlias;
System.out.println("4.正在签名apk:" + signCmd);
process = Runtime.getRuntime().exec(signCmd);
if (process.waitFor() != 0) {
System.out.println(in2String( process.getInputStream()));
System.out.println("签名失败。。。");
return;
}
System.out.println("签名成功");
} catch (Exception e) {
e.printStackTrace();
}
//7----删除临时文件
try {
System.out.println("5.正在删除临时文件");
//删除生成的没有签名的apk
delDir(rePackagePath);
//删除解压的目录
delDir(unPackagePath);
} catch (Exception e) {
e.printStackTrace();
System.out.println("删除临时文件出错:"+e.getMessage());
}
float end = (System.currentTimeMillis() - start) / 1000f;
System.out.println("解压,打包,签名apk完成,一共耗时:" + end + "S");
}
//转换成字符串的函数,用来读取出错信息
public static String in2String(InputStream in){
ByteArrayOutputStream byteOut=new ByteArrayOutputStream();
byte[] b=new byte[1024];
int len;
try {
while ((len=in.read(b))!=-1){
byteOut.write(b,0,len);
}
return byteOut.toString("UTF-8");
} catch (IOException e) {
e.printStackTrace();
}
return "";
}
/**
* 用来删除一个文件夹或者文件,用来删除临时文件
* @param filePath 文件夹路径
*/
private static void delDir(String filePath) {
File file = new File(filePath);
//不存在就返回
if (!file.exists()) return;
if (file.isFile()) {
//是文件就直接删除
file.delete();
return;
} else {
//是文件夹就需要迭代的删除
File[] files = file.listFiles();
int len = files.length;
for (int i = 0; i < len; i++) {
if (files[i].isFile()) {
//是文件就直接删除
files[i].delete();
} else {
//是文件夹就递归调用
delDir(files[i].getPath());
}
}
file.delete();
}
}
}
最后的结果:
1.正在执行解压:unzip -o my.apk -d myapp/
解压成功
2.正在执行写入:这里是解压以后,新写入的内容sadfsdafsfs
sfsdfsdfsdfsdf
写入文件新内容成功
3.删除原来的签名等信息:myapp/META-INF
删除成功
3.正在重新打包apk:zip -r repackage.apk .
打包成功
3.正在移动重新打包apk:mv repackage.apk ..
移动成功
4.正在签名apk:jarsigner -keystore testkey.jks -storepass 123456 -signedjar signedApk.apk repackage.apk -keypass 123456 测试签名
签名成功
5.正在删除临时文件
解压,打包,签名apk完成,一共耗时:1.652S
Process finished with exit code 0
3、总结
用两种方法都可以实现,这个都是亲测通过了,但是推荐第二种;
第一种调用时间很长,一个demo的apk处理过程大概要20多秒,很大的apk估计时间还要长一些,第二种因为是直接调用Linux的命令,也只是仅仅解压,打包,签名,非常快,大概1s多,而且还主要是花费在签名上面;
第二种,理论上是可以支持任何apk的,应该可以支持加固以后的应用,把加固以后的apk拿来,然后用上面的程序,应该可以直接跑。不过我没测试,也不敢保证;
另外,提示一下,网上很多人写的博客,不知道是版本不一样,还是怎麽的,很多参数都不对,需要自己看一个文档,这里是个坑。
原创文章,转载请注明本博客地址:http://blog.csdn.net/jie11447416/article/details/50489957