Android-APK加固-简单版

Proguard的使用与配置

介绍

Proguard是一个代码优化和混淆工具。
能够提供对Java类文件的压缩、优化、混淆,和预校验。压缩的步骤是检测并移除未使用的类、字段、方法和属性。优化的步骤是分析和优化方法的字节码。混淆的步骤是使用短的毫无意义的名称重命名剩余的类、字段和方法。压缩、优化、混淆使得代码更小,更高效。

开启proguard

minifyEnabled true

buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
        debug {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }

常用配置

	-keep 指定类和类成员(变量和方法)不被混淆。
	(保护了类名)
	-keep class com.lk.proxy.guard.test.Bug
	
	(保护了 public static void的没有参数的函数 不被混淆)
	-keep class com.lk.proxy.guard.test.Bug{
   		public static void *();
	}
	(保护com.dongnao.proxy.guard.test.Bug类里面所有 不被混淆)
	-keep class com.lk.proxy.guard.test.Bug{
   		*;
	}
	
-keepclassmembers 指定类成员不被混淆(就是-keep的缩小版,不管类名了)。
	-keepclassmembers
	class com.lk.proxy.guard.test.Bug
	(都被混淆了)
-keepclasseswithmembers 指定类和类成员不被混淆,前提是指定的类成员存在。
	-keepclasseswithmembers class 	com.lk.proxy.guard.test.Bug
	(保护类名,但是没指定成员,所以函数名被混淆)
	-keepclasseswithmembers class   	com.lk.proxy.guard.test.Bug{
		native <methods>;	//这个类里面的native方法不被混淆
	}

加固

大体思路

在这里插入图片描述
思路

1、编写一个Tools工具(java项目)负责把APK解压出来的所有dex文件抽出来进行加密(注意这不是在运行时做的);
2、加密后再和APK解压出来的其他文件一起打包得到一个全新的APK;
里面需要创建一个ProxyApplication(代理Application),让这个代理去和Android的系统对接,这个代理需要负责解密再交给Android系统去对接执行;确保程序能正常运行;

和Android系统对接的这个过程,就是插桩…
这个插桩听着是不是很熟悉,Tinker热修复里面的dex是进行插桩修复…

源码(浅析)

我们再来看看这个系统的源码 - -!我们只看关键部分,我这里是6.0的源码;其他版本的源码,需要自己去做适配;

打印出来的 ClassLoader 是:dalvik.system.PathClassLoader

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Log.i("Kai",getClassLoader().toString());
    }
}

dalvik.system.PathClassLoader

跟进来,发现PathClassLoader这个类里面基本没干什么事,就是继承了BaseDexClassLoader,和两个构造方法;所以我们得到BaseDexClassLoader这个类里面去看看;

package dalvik.system;

public class PathClassLoader extends BaseDexClassLoader {

    public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }

    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
        super(dexPath, null, librarySearchPath, parent);
    }
}

dalvik.system.BaseDexClassLoader

成员属性 DexPathList 类:看这个名字,就应该知道这个是存放dex文件路径的集合类
findClass方法:从dexpathList类中的findClass方法获取到类的字节码;

package dalvik.system;

......
/**
 * Base class for common functionality between various dex-based
 * {@link ClassLoader} implementations.
 */
public class BaseDexClassLoader extends ClassLoader {
	//看这个名字,就应该知道这个是存放dex文件路径的集合类
    private final DexPathList pathList;

	public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
        super(parent);
        //初始化这个 存放dex文件路径的集合类
        this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory);
    }
	//查找类的方法
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        //从dexpathList类中的findClass方法获取到类的字节码
        Class c = pathList.findClass(name, suppressedExceptions);
        if (c == null) {
            ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
            for (Throwable t : suppressedExceptions) {
                cnfe.addSuppressed(t);
            }
            throw cnfe;
        }
        return c;
    }

    /**
     * @hide
     */
    public void addDexPath(String dexPath) {
        pathList.addDexPath(dexPath, null /*optimizedDirectory*/);
    }
    ......
    ......
}

dalvik.system.DexPathList

看到这个类里面的findClass方法,我们也就能明白:dex文件都是存放在dexElements这个数组里面;
dexElements的初始化时通过 makePathElements方法;
注意:6.0这个里面还是通过makePatrhElements方法初始化这个数组,但是到了7.0的时候就是用makeDexElements这个和方法初始化这个数组了,所以如果需要反射系统层面的东西,需要做好适配…

package dalvik.system;
......
/*package*/ final class DexPathList {
/**
     * Finds the named class in one of the dex files pointed at by
     * this instance. This will find the one in the earliest listed
     * path element. If the class is found but has not yet been
     * defined, then this method will define it in the defining
     * context that this instance was constructed with.
     *
     * @param name of class to find
     * @param suppressed exceptions encountered whilst finding the class
     * @return the named class or {@code null} if the class is not
     * found in any of the dex files
     */
	......
	//存放了 Dex文件的数组
	private Element[] dexElements;
	......
	public DexPathList(ClassLoader definingContext, String dexPath,
            String librarySearchPath, File optimizedDirectory) {
		......
		// dexElements 数组的初始化 makePathElements方法
		// save dexPath for BaseDexClassLoader
        this.dexElements = makePathElements(splitDexPath(dexPath), optimizedDirectory,
                                            suppressedExceptions);
		......
	}
	......
	 /**
     * Makes an array of dex/resource path elements, one per element of
     * the given array.
     */
    private static Element[] makePathElements(List<File> files, File optimizedDirectory,
                                              List<IOException> suppressedExceptions) {
        List<Element> elements = new ArrayList<>();
        /*
         * Open all files and load the (direct or contained) dex files
         * up front.
         */
        for (File file : files) {
            File zip = null;
            File dir = new File("");
            DexFile dex = null;
            String path = file.getPath();
            String name = file.getName();

            if (path.contains(zipSeparator)) {
                String split[] = path.split(zipSeparator, 2);
                zip = new File(split[0]);
                dir = new File(split[1]);
            } else if (file.isDirectory()) {
                // We support directories for looking up resources and native libraries.
                // Looking up resources in directories is useful for running libcore tests.
                elements.add(new Element(file, true, null, null));
            } else if (file.isFile()) {
                if (name.endsWith(DEX_SUFFIX)) {
                    // Raw dex file (not inside a zip/jar).
                    try {
                        dex = loadDexFile(file, optimizedDirectory);
                    } catch (IOException ex) {
                        System.logE("Unable to load dex file: " + file, ex);
                    }
                } else {
                    zip = file;

                    try {
                        dex = loadDexFile(file, optimizedDirectory);
                    } catch (IOException suppressed) {
                        /*
                         * IOException might get thrown "legitimately" by the DexFile constructor if
                         * the zip file turns out to be resource-only (that is, no classes.dex file
                         * in it).
                         * Let dex == null and hang on to the exception to add to the tea-leaves for
                         * when findClass returns null.
                         */
                        suppressedExceptions.add(suppressed);
                    }
                }
            } else {
                System.logW("ClassLoader referenced unknown path: " + file);
            }

            if ((zip != null) || (dex != null)) {
                elements.add(new Element(dir, false, zip, dex));
            }
        }

        return elements.toArray(new Element[elements.size()]);
    }
	......
    public Class findClass(String name, List<Throwable> suppressed) {
    	//dex文件都是存放在 dexElements这个数组里面
        for (Element element : dexElements) {
        	//获取到dex文件  DexFile类
            DexFile dex = element.dexFile;

            if (dex != null) {
            	//通过dex文件  DexFile类的loadClassBinaryName方法获取到Class字节码
                Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
                if (clazz != null) {
                    return clazz;
                }
            }
        }
        if (dexElementsSuppressedExceptions != null) {
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        }
        return null;
    }
}

思路

通过上面的源码,和前面写的Tinker热修复的插桩原理,我们可以了解到:

1、程序的ClassLoader类加载器是一个PathClassLoader;
2、这个ClassLoader继承了BaseDexClassLoader这个父类;
3、这个父类中的findClass方法中,通过pathList.findClass获取Class字节码,而pathList是DexPathList对象;
4、跟进DexPathList类的findClass方法中,发现遍历的是一个Element数组—dexElements,数组里面的元素通过.dexFile获取到DexFile对象,也就是可以理解成Dex文件;
5、这个dex文件其实就是程序的dex文件,也就是我们上面说的交给系统的dex文件;

思路

我们只需要反射到这个dexElements数组,把我们解密后dex文件与这个dexElements数组合并到一起,然后再赋值给系统;
而dexElements的初始化是通过makePathElements方法获取的;
可以通过反射makePathElements方法来获取到dexElements对象
也可以直接反射dexElements变量获取到dexElements对象(这种方式的获取,可以去看看我的Tinker热修复这一篇)
Android-Tinker热修复原理

撸码

我们从解密开始看~~

解密

我们上面说过解密是需要用代理ProxyApplication来做解密,并且把解密后的dex文件交给Android系统去加载;

我们需要新建一个Android Library;这个是用来代理、解密、和系统对接操作时用的;

工具类-AES(解密时用)

package com.example.proxy_core;


import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;


public class AES {

    //16字节
    public static final String DEFAULT_PWD = "abcdefghijklmnop";
    //填充方式
    private static final String algorithmStr = "AES/ECB/PKCS5Padding";
    private static Cipher encryptCipher;
    private static Cipher decryptCipher;

    public static void init(String password) {
        try {
            // 生成一个实现指定转换的 Cipher 对象。
            encryptCipher = Cipher.getInstance(algorithmStr);
            decryptCipher = Cipher.getInstance(algorithmStr);// algorithmStr
            byte[] keyStr = password.getBytes();
            SecretKeySpec key = new SecretKeySpec(keyStr, "AES");
            encryptCipher.init(Cipher.ENCRYPT_MODE, key);
            decryptCipher.init(Cipher.DECRYPT_MODE, key);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (NoSuchPaddingException e) {
            e.printStackTrace();
        } catch (InvalidKeyException e) {
            e.printStackTrace();
        }
    }

    public static byte[] encrypt(byte[] content) {
        try {
            byte[] result = encryptCipher.doFinal(content);
            return result;
        } catch (IllegalBlockSizeException e) {
            e.printStackTrace();
        } catch (BadPaddingException e) {
            e.printStackTrace();
        }
        return null;
    }

    public static byte[] decrypt(byte[] content) {
        try {
            byte[] result = decryptCipher.doFinal(content);
            return result;
        } catch (IllegalBlockSizeException e) {
            e.printStackTrace();
        } catch (BadPaddingException e) {
            e.printStackTrace();
        }
        return null;
    }
}

工具类-Zip(压缩、解压)

也可以自己去用第三方的来压缩和解压

package com.example.proxy_core;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.zip.CRC32;
import java.util.zip.CheckedOutputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;


public class Zip {

    private static void deleteFile(File file){
        if (file.isDirectory()){
            File[] files = file.listFiles();
            for (File f: files) {
                deleteFile(f);
            }
        }else{
            file.delete();
        }
    }

    /**
     * 解压zip文件至dir目录
     * @param zip
     * @param dir
     */
    public static void unZip(File zip, File dir) {
        try {
            deleteFile(dir);
            ZipFile zipFile = new ZipFile(zip);
            //zip文件中每一个条目
            Enumeration<? extends ZipEntry> entries = zipFile.entries();
            //遍历
            while (entries.hasMoreElements()) {
                ZipEntry zipEntry = entries.nextElement();
                //zip中 文件/目录名
                String name = zipEntry.getName();
                //原来的签名文件 不需要了
                if (name.equals("META-INF/CERT.RSA") || name.equals("META-INF/CERT.SF") || name
                        .equals("META-INF/MANIFEST.MF")) {
                    continue;
                }
                //空目录不管
                if (!zipEntry.isDirectory()) {
                    File file = new File(dir, name);
                    //创建目录
                    if (!file.getParentFile().exists()) {
                        file.getParentFile().mkdirs();
                    }
                    //写文件
                    FileOutputStream fos = new FileOutputStream(file);
                    InputStream is = zipFile.getInputStream(zipEntry);
                    byte[] buffer = new byte[2048];
                    int len;
                    while ((len = is.read(buffer)) != -1) {
                        fos.write(buffer, 0, len);
                    }
                    is.close();
                    fos.close();
                }
            }
            zipFile.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 压缩目录为zip
     * @param dir 待压缩目录
     * @param zip 输出的zip文件
     * @throws Exception
     */
    public static void zip(File dir, File zip) throws Exception {
        zip.delete();
        // 对输出文件做CRC32校验
        CheckedOutputStream cos = new CheckedOutputStream(new FileOutputStream(
                zip), new CRC32());
        ZipOutputStream zos = new ZipOutputStream(cos);
        //压缩
        compress(dir, zos, "");
        zos.flush();
        zos.close();
    }

    /**
     * 添加目录/文件 至zip中
     * @param srcFile 需要添加的目录/文件
     * @param zos   zip输出流
     * @param basePath  递归子目录时的完整目录 如 lib/x86
     * @throws Exception
     */
    private static void compress(File srcFile, ZipOutputStream zos,
                                 String basePath) throws Exception {
        if (srcFile.isDirectory()) {
            File[] files = srcFile.listFiles();
            for (File file : files) {
                // zip 递归添加目录中的文件
                compress(file, zos, basePath + srcFile.getName() + "/");
            }
        } else {
            compressFile(srcFile, zos, basePath);
        }
    }

    private static void compressFile(File file, ZipOutputStream zos, String dir)
            throws Exception {
        // temp/lib/x86/libdn_ssl.so
        String fullName = dir + file.getName();
        // 需要去掉temp
        String[] fileNames = fullName.split("/");
        //正确的文件目录名 (去掉了temp)
        StringBuffer sb = new StringBuffer();
        if (fileNames.length > 1){
            for (int i = 1;i<fileNames.length;++i){
                sb.append("/");
                sb.append(fileNames[i]);
            }
        }else{
            sb.append("/");
        }
        //添加一个zip条目
        ZipEntry entry = new ZipEntry(sb.substring(1));
        zos.putNextEntry(entry);
        //读取条目输出到zip中
        FileInputStream fis = new FileInputStream(file);
        int len;
        byte data[] = new byte[2048];
        while ((len = fis.read(data, 0, 2048)) != -1) {
            zos.write(data, 0, len);
        }
        fis.close();
        zos.closeEntry();
    }

}

工具类-Utils(反射操作)

package com.example.proxy_core;

import java.io.File;
import java.io.RandomAccessFile;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;


public class Utils {

    /**
     * 读取文件
     * @param file
     * @return
     * @throws Exception
     */
    public static byte[] getBytes(File file) throws Exception {
        RandomAccessFile r = new RandomAccessFile(file, "r");
        byte[] buffer = new byte[(int) r.length()];
        r.readFully(buffer);
        r.close();
        return buffer;
    }

    /**
     * 反射获得 指定对象(当前-》父类-》父类...)中的 成员属性
     * @param instance
     * @param name
     * @return
     * @throws NoSuchFieldException
     */
    public static Field findField(Object instance, String name) throws NoSuchFieldException {
        Class clazz = instance.getClass();
        //反射获得
        while (clazz != null) {
            try {
                Field field = clazz.getDeclaredField(name);
                //如果无法访问 设置为可访问
                if (!field.isAccessible()) {
                    field.setAccessible(true);
                }
                return field;
            } catch (NoSuchFieldException e) {
                //如果找不到往父类找
                clazz = clazz.getSuperclass();
            }
        }
        throw new NoSuchFieldException("Field " + name + " not found in " + instance.getClass());
    }


    /**
     * 反射获得 指定对象(当前-》父类-》父类...)中的 函数
     * @param instance
     * @param name
     * @param parameterTypes
     * @return
     * @throws NoSuchMethodException
     */
    public static Method findMethod(Object instance, String name, Class... parameterTypes)
            throws NoSuchMethodException {
        Class clazz = instance.getClass();
        while (clazz != null) {
            try {
                Method method = clazz.getDeclaredMethod(name, parameterTypes);
                if (!method.isAccessible()) {
                    method.setAccessible(true);
                }
                return method;
            } catch (NoSuchMethodException e) {
                //如果找不到往父类找
                clazz = clazz.getSuperclass();
            }
        }
        throw new NoSuchMethodException("Method " + name + " with parameters " + Arrays.asList
                (parameterTypes) + " not found in " + instance.getClass());
    }
}

解密开始

上面的准备工作,工具类准备好了后,准备开始;

先用meta-data标签,把最终需要加载的MyApplication全类名、解密后的dex目录都定义出来;
是需要解密后把项目的MyApplication运行出来;
这两个meta-data标签的数据,需要等会儿反射拿到;

		<!--真实的Application的全名-->
        <meta-data android:name="app_name" android:value="com.lk.reinforce_demo.MyApplication"/>
        <!--用于dex后的目录名_版本号-->
        <meta-data android:name="app_version" android:value="\dexDir_1.0"/>

attachBaseContext方法中,获取到主项目的meta-data标签里面的值(终需要加载的MyApplication全类名、dex解密后需要存放的目录)

 /**
     * ActivityThread创建Application之后调用的第一个方法
     * 可以在这个方法中进行解密,同时把dex交给android去加载
     */
    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        //获取定义的的metadata
        getMetaData();
	}
	
	/**
     * 读取 meta-data标签的值
     */
    private void getMetaData() {
        try{
            //获取 ApplicationInfo
            `applicationInfo = getPackageManager().getApplicationInfo(
                    getPackageName(), PackageManager.GET_META_DATA);
            Bundle metaData=applicationInfo.metaData;
            if(null!=metaData){
                //获取到清单文件里面的 meta-data 标签里面的数据
                if(metaData.containsKey("app_name")){
                    //获取到的是 最终需要加载的MyApplication全类名
                    app_name=metaData.getString("app_name");
                }
                if(metaData.containsKey("app_version")){
                    app_version=metaData.getString("app_version");
                }
            }

        }catch(Exception e){
            e.printStackTrace();
        }
    }

把apk解压后;
classes.dex 是存放我们这个代理 ProxyApplication的,所以我们加密的时候也不会把是这个classes.dex进行加密,所以这里不需要解密;
解密后再写入文件中,然后将文件存入到集合,后面方便我们将所有的dex文件给到系统去对接;

//得到当前加密了的APK文件
        File apkFile=new File(getApplicationInfo().sourceDir);

        //把apk解压   app_name+"_"+app_version目录中的内容 别的应用需要boot权限才能用
        File versionDir = getDir(app_name+"_"+app_version,MODE_PRIVATE);
        //创建两个目录
        //这个目录用来存放 apk解压后,除了dex以外的文件
        File appDir=new File(versionDir,"app");
        //这个目录用来存放dex文件
        File dexDir=new File(appDir,"dexDir");


        //得到我们需要加载的Dex文件
        List<File> dexFiles=new ArrayList<>();
        //进行解密(最好做MD5文件校验)
        if(!dexDir.exists() || dexDir.list().length==0){
            //把apk解压到appDir
            Zip.unZip(apkFile,appDir);
            //获取目录下所有的文件
            File[] files=appDir.listFiles();
            for (File file : files) {
                String name=file.getName();
                //classes.dex 是存放我们这个代理 ProxyApplication的
                // 所以我们加密的时候也不会把是这个classes.dex进行加密,所以这里不需要解密
                if(name.endsWith(".dex") && !TextUtils.equals(name,"classes.dex")){
                    try{
                        AES.init(AES.DEFAULT_PWD);
                        //读取文件内容
                        byte[] bytes=Utils.getBytes(file);
                        //解密
                        byte[] decrypt=AES.decrypt(bytes);
                        //写到指定的目录
                        FileOutputStream fos=new FileOutputStream(file);
                        fos.write(decrypt);
                        fos.flush();
                        fos.close();
                        dexFiles.add(file);

                    }catch (Exception e){
                        e.printStackTrace();
                    }
                }
            }
        }else{
            for (File file : dexDir.listFiles()) {
                dexFiles.add(file);
            }
        }

将我们解密后的dex文件集合,与系统对接;
也就是反射将dex文件存放到系统的dexElements这个数组中;

//将我们解密后的dex文件集合,与系统对接
private void loadDex(List<File> dexFiles, File versionDir) throws Exception{
        //1.获取pathlist
        Field pathListField = Utils.findField(getClassLoader(), "pathList");
        Object pathList = pathListField.get(getClassLoader());
        //2.获取数组dexElements
        Field dexElementsField=Utils.findField(pathList,"dexElements");
        Object[] dexElements=(Object[])dexElementsField.get(pathList);
        //3.反射到初始化dexElements的方法
        Method makeDexElements=Utils.findMethod(pathList,"makePathElements",List.class,File.class,List.class);

        ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
        Object[] addElements=(Object[])makeDexElements.invoke(pathList,dexFiles,versionDir,suppressedExceptions);

        //合并数组
        Object[] newElements= (Object[])Array.newInstance(dexElements.getClass().getComponentType(),dexElements.length+addElements.length);
        System.arraycopy(dexElements,0,newElements,0,dexElements.length);
        System.arraycopy(addElements,0,newElements,dexElements.length,addElements.length);

        //替换classloader中的element数组
        dexElementsField.set(pathList,newElements);
    }

一定要注意的点:这个主项目的application用到的是 代理的ProxyApplication,不然是没法运行到代理去进行解密dex、和系统对接的!!!!!!

<application
        android:name="com.example.proxy_core.ProxyApplication"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">

加密

这里用java项目来做加密操作,因为是编译操作的加密,所以这里用到一个java项目来做的;
这里有时候执行命令的时候process.exitValue()会不等于0,建议先把指令打印出来,再执行命令通不过的地方去用cmd执行~~
加密的步骤:
1、将Android Library生成的aar文件解压到自己定义的目录中;
2、解压之后会有个classes.jar,将这个jar用dx命令,生成dex文件,classes.dex
3、解压我们主的APK文件到临时目录,并将解压后的所有的.dex后缀的文件,读取字节,然后进行加密,再变成新的文件;
4、然后将第二步生成的classes.dex文件拷入到这个临时目录中,然后压缩成apk,这个时候的apk就是没有签名对齐的apk,里面除了classes.dex文件没有加密以外,其余的dex文件都是加密的;
5、然后对新生成的apk,进行签名和对齐;

package com.example.proxy_tools;

import java.io.File;
import java.io.FileOutputStream;
import java.io.FilenameFilter;

public class Main {
    public static void main(String[] args) throws Exception {
        /**
         * 1.制作只包含解密代码的dex文件
         */
        File aarFile=new File("proxy_core/build/outputs/aar/proxy_core-debug.aar");
        File aarTemp=new File("proxy_tools/temp");
        Zip.unZip(aarFile,aarTemp);
        File classesJar=new File(aarTemp,"classes.jar");
        File classesDex=new File(aarTemp,"classes.dex");
		 String dxStr = "cmd /c dx --dex --output "+classesDex.getAbsolutePath()+" "+classesJar.getAbsolutePath();   
	     System.out.println("dxStr::"+dxStr);
	     //dx --dex --output F:\AndroidTools\AndroidObj\dn\Lsn_11_Demo2\proxy_tools\temp\classes.dex F:\AndroidTools\AndroidObj\dn\Lsn_11_Demo2\proxy_tools\temp\classes.jar
        //dx --dex --output out.dex in.jar
        Process process=Runtime.getRuntime().exec(dxStr);
        process.waitFor();
        if(process.exitValue()!=0){
            throw new RuntimeException("dex error");
        }

        /**
         * 2.加密APK中所有的dex文件
         */
        File apkFile=new File("app/build/outputs/apk/debug/app-debug.apk");
        File apkTemp=new File("app/build/outputs/apk/debug/temp");
        Zip.unZip(apkFile,apkTemp);
        //只要dex文件拿出来加密
        File[] dexFiles=apkTemp.listFiles(new FilenameFilter() {
            @Override
            public boolean accept(File file, String s) {
                return s.endsWith(".dex");
            }
        });
        //AES加密了
        AES.init(AES.DEFAULT_PWD);
        for (File dexFile : dexFiles) {
            byte[] bytes = Utils.getBytes(dexFile);
            byte[] encrypt = AES.encrypt(bytes);
            FileOutputStream fos=new FileOutputStream(new File(apkTemp,
                    "secret-"+dexFile.getName()));
            fos.write(encrypt);
            fos.flush();
            fos.close();
            dexFile.delete();
        }

        /**
         * 3.把dex放入apk解压目录,重新压成apk文件
         */
        classesDex.renameTo(new File(apkTemp,"classes.dex"));
        File unSignedApk=new File("app/build/outputs/apk/debug/app-unsigned.apk");
        Zip.zip(apkTemp,unSignedApk);


        /**
         * 4.对齐和签名
         */
//        zipalign -v -p 4 my-app-unsigned.apk my-app-unsigned-aligned.apk
        File alignedApk=new File("app/build/outputs/apk/debug/app-unsigned-aligned.apk");
        // 可以用cmd执行命令:zipalign -v -p 4 F:\AndroidTools\AndroidObj\dn\Lsn_11_Demo2\app\build\outputs\apk\debug\app-unsigned.apk F:\AndroidTools\AndroidObj\dn\Lsn_11_Demo2\app\build\outputs\apk\debug\app-unsigned-aligned.apk
        process=Runtime.getRuntime().exec("cmd /c zipalign -v -p 4 "+unSignedApk.getAbsolutePath()
                        +" "+alignedApk.getAbsolutePath());
        process.waitFor();
        if(process.exitValue()!=0){
            throw new RuntimeException("dex error");
        }


//        apksigner sign --ks my-release-key.jks --out my-app-release.apk my-app-unsigned-aligned.apk
//        apksigner sign  --ks jks文件地址 --ks-key-alias 别名 --ks-pass pass:jsk密码 --key-pass pass:别名密码 --out  out.apk in.apk
        File signedApk=new File("app/build/outputs/apk/debug/app-signed-aligned.apk");
        File jks=new File("proxy_tools/proxy2.jks");
        //可以用cmd执行命令:apksigner sign --ks F:\AndroidTools\AndroidObj\dn\Lsn_11_Demo2\proxy_tools\proxy2.jks --ks-key-alias jett --ks-pass pass:123456 --key-pass pass:123456 --out F:\AndroidTools\AndroidObj\dn\Lsn_11_Demo2\app\build\outputs\apk\debug\app-signed-aligned.apk F:\AndroidTools\AndroidObj\dn\Lsn_11_Demo2\app\build\outputs\apk\debug\app-unsigned-aligned.apk
        process=Runtime.getRuntime().exec("cmd /c apksigner sign --ks "+jks.getAbsolutePath()
                            +" --ks-key-alias kaizi --ks-pass pass:123456 --key-pass pass:123456 --out "
                                +signedApk.getAbsolutePath()+" "+alignedApk.getAbsolutePath());
        process.waitFor();
        if(process.exitValue()!=0){
            throw new RuntimeException("dex error");
        }
        System.out.println("执行成功");

    }
}

截图

1、解压aar后的存放目录解压aar后的存放目录
2、将这个jar用dx命令,生成dex文件,classes.dex
在这里插入图片描述
3、解压我们主的APK文件到临时目录,并将解压后的所有的.dex后缀的文件,读取字节,然后进行加密,再变成新的文件;这里的classes.dex是有第四步的:将第二步生成的classes.dex文件拷入到这个临时目录中了
在这里插入图片描述
后续的压缩apk,签名对齐的操作生成的apk,最终app-signed-aligned.apk这个apk是最后需要的apk
在这里插入图片描述
缺点:第一次启动的时候,会慢一点,想要后续不响应速度,我们就需要自己去做个md5的文件效验,看看文件是否已经解密过了~

后续的替换MyApplication的操作,童鞋们可以自己去研究下~~

辛苦各位童鞋观看到最后,如果博客中有不对的地方望指出,大神勿喷,谢谢~~

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值