【Android 安全】Android 应用 APK 加固总结 ( 加固原理 | 应用加固完整的实现方案 | 源码资源 )



Android 安全 专栏进行总结 ;





一、 APK 加固原理





1、 Android 应用反编译


Android 应用反编译 :



2、 ProGuard 混淆


ProGuard 混淆 :



3、 多 dex 加载原理


多 dex 加载原理 :



4、 代理 Application 开发




5、Java 工具开发


Java 工具开发 :



6、Application 替换


Application 替换 :





二、 应用加固完整的实现方案





1、 代理 Application


( 1 ) ProxyApplication

package kim.hsl.multipledex;

import android.app.Application;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

public class ProxyApplication extends Application {
    public static final String TAG = "ProxyApplication";

    /**
     * 应用真实的 Application 全类名
     */
    String app_name;

    /**
     * DEX 解密之后的目录名称
     */
    String app_version;

    /**
     * 在 Application 在 ActivityThread 中被创建之后,
     * 第一个调用的方法是 attachBaseContext 函数.
     * 该函数是 Application 中最先执行的函数.
     */
    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        try {
            Log.i(TAG, "attachBaseContext");

            /*
                在该 Application 中主要进行两个操作 :
                1 . 解密并加载多个 DEX 文件
                2 . 将真实的 Application 替换成应用的主 Application
             */


            /*
                I . 解密与加载多 DEX 文件
                    先进行解密, 然后再加载解密之后的 DEX 文件

                    1. 先获取当前的 APK 文件
                    2. 然后解压该 APK 文件
             */

            // 获取当前的 APK 文件, 下面的 getApplicationInfo().sourceDir 就是本应用 APK 安装文件的全路径
            File apkFile = new File(getApplicationInfo().sourceDir);

            // 获取在 app Module 下的 AndroidManifest.xml 中配置的元数据,
            // 应用真实的 Application 全类名
            // 解密后的 dex 文件存放目录
            ApplicationInfo applicationInfo = null;

            applicationInfo = getPackageManager().getApplicationInfo(
                    getPackageName(),
                    PackageManager.GET_META_DATA
            );

            Bundle metaData = applicationInfo.metaData;
            if (metaData != null) {
                // 检查是否存在 app_name 元数据
                if (metaData.containsKey("app_name")) {
                    app_name = metaData.getString("app_name").toString();
                }
                // 检查是否存在 app_version 元数据
                if (metaData.containsKey("app_version")) {
                    app_version = metaData.getString("app_version").toString();
                }
            }

            // 创建用户的私有目录 , 将 apk 文件解压到该目录中
            File privateDir = getDir(app_name + "_" + app_version, MODE_PRIVATE);

            Log.i(TAG, "attachBaseContext 创建用户的私有目录 : " + privateDir.getAbsolutePath());

            // 在上述目录下创建 app 目录
            // 创建该目录的目的是存放解压后的 apk 文件的
            File appDir = new File(privateDir, "app");

            // app 中存放的是解压后的所有的 apk 文件
            // app 下创建 dexDir 目录 , 将所有的 dex 目录移动到该 dexDir 目录中
            // dexDir 目录存放应用的所有 dex 文件
            // 这些 dex 文件都需要进行解密
            File dexDir = new File(appDir, "dexDir");

            // 遍历解压后的 apk 文件 , 将需要加载的 dex 放入如下集合中
            ArrayList<File> dexFiles = new ArrayList<File>();

            // 如果该 dexDir 不存在 , 或者该目录为空 , 并进行 MD5 文件校验
            if (!dexDir.exists() || dexDir.list().length == 0) {
                // 将 apk 中的文件解压到了 appDir 目录
                ZipUtils.unZipApk(apkFile, appDir);


                // 获取 appDir 目录下的所有文件
                File[] files = appDir.listFiles();

                Log.i(TAG, "attachBaseContext appDir 目录路径 : " + appDir.getAbsolutePath());
                Log.i(TAG, "attachBaseContext appDir 目录内容 : " + files);

                // 遍历文件名称集合
                for (int i = 0; i < files.length; i++) {
                    File file = files[i];

                    Log.i(TAG, "attachBaseContext 遍历 " + i + " . " + file);

                    // 如果文件后缀是 .dex , 并且不是 主 dex 文件 classes.dex
                    // 符合上述两个条件的 dex 文件放入到 dexDir 中
                    if (file.getName().endsWith(".dex") &&
                            !TextUtils.equals(file.getName(), "classes.dex")) {
                        // 筛选出来的 dex 文件都是需要解密的
                        // 解密需要使用 OpenSSL 进行解密

                        // 获取该文件的二进制 Byte 数据
                        // 这些 Byte 数组就是加密后的 dex 数据
                        byte[] bytes = OpenSSL.getBytes(file);

                        // 解密该二进制数据, 并替换原来的加密 dex, 直接覆盖原来的文件即可
                        OpenSSL.decrypt(bytes, file.getAbsolutePath());

                        // 将解密完毕的 dex 文件放在需要加载的 dex 集合中
                        dexFiles.add(file);

                        // 拷贝到 dexDir 中

                        Log.i(TAG, "attachBaseContext 解密完成 被解密文件是 : " + file);

                    }// 判定是否是需要解密的 dex 文件
                }// 遍历 apk 解压后的文件

            } else {
                // 已经解密完成, 此时不需要解密, 直接获取 dexDir 中的文件即可
                for (File file : dexDir.listFiles()) {
                    dexFiles.add(file);
                }
            }

            Log.i(TAG, "attachBaseContext 解密完成 dexFiles : " + dexFiles);

            for (int i = 0; i < dexFiles.size(); i++) {
                Log.i(TAG, i + " . " + dexFiles.get(i).getAbsolutePath());
            }

            // 截止到此处 , 已经拿到了解密完毕 , 需要加载的 dex 文件
            // 加载自己解密的 dex 文件
            loadDex(dexFiles, privateDir);

            Log.i(TAG, "attachBaseContext 完成");
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 加载 dex 文件集合
     * 这些 dex 文件已经解密
     * 参考博客 : https://hanshuliang.blog.csdn.net/article/details/109608605
     * <p>
     * 创建自己的 Element[] dexElements 数组
     * ( libcore/dalvik/src/main/java/dalvik/system/DexPathList.java )
     * 然后将 系统加载的 Element[] dexElements 数组 与 我们自己的 Element[] dexElements 数组进行合并操作
     */
    void loadDex(ArrayList<File> dexFiles, File optimizedDirectory)
            throws
            IllegalAccessException,
            InvocationTargetException,
            NoSuchFieldException,
            NoSuchMethodException {
        Log.i(TAG, "loadDex");
        /*
            需要执行的步骤
            1 . 获得系统 DexPathList 中的 Element[] dexElements 数组
                ( libcore/dalvik/src/main/java/dalvik/system/DexPathList.java )
            2 . 在本应用中创建 Element[] dexElements 数组 , 用于存放解密后的 dex 文件
            3 . 将 系统加载的 Element[] dexElements 数组
                与 我们自己的 Element[] dexElements 数组进行合并操作
            4 . 替换 ClassLoader 加载过程中的 Element[] dexElements 数组 ( 封装在 DexPathList 中 )
         */


        /*
            1 . 获得系统 DexPathList 中的 Element[] dexElements 数组

            第一阶段 : 在 Context 中调用 getClassLoader() 方法 , 可以拿到 PathClassLoader ;

            第二阶段 : 从 PathClassLoader 父类 BaseDexClassLoader 中找到 DexPathList ;

            第三阶段 : 获取封装在 DexPathList 类中的 Element[] dexElements 数组 ;

            上述的 DexPathList 对象 是 BaseDexClassLoader 的私有成员
            Element[] dexElements 数组 也是 DexPathList 的私有成员
            因此只能使用反射获取 Element[] dexElements 数组
         */

        // 阶段一二 : 调用 getClassLoader() 方法可以获取 PathClassLoader 对象
        // 从 PathClassLoader 对象中获取 private final DexPathList pathList 成员
        Field pathListField = ReflexUtils.reflexField(getClassLoader(), "pathList");
        // 获取 classLoader 对象对应的 DexPathList pathList 成员
        Object pathList = pathListField.get(getClassLoader());

        //阶段三 : 获取封装在 DexPathList 类中的 Element[] dexElements 数组
        Field dexElementsField = ReflexUtils.reflexField(pathList, "dexElements");
        // 获取 pathList 对象对应的 Element[] dexElements 数组成员
        Object[] dexElements = (Object[]) dexElementsField.get(pathList);



        /*
            2 . 在本应用中创建 Element[] dexElements 数组 , 用于存放解密后的 dex 文件
                不同的 Android 版本中 , 创建 Element[] dexElements 数组的方法不同 , 这里需要做兼容

         */
        Method makeDexElements;
        Object[] addElements = null;

        if (Build.VERSION.SDK_INT <=
                Build.VERSION_CODES.M) { // 5.0, 5.1  makeDexElements

            // 反射 5.0, 5.1, 6.0 版本的 DexPathList 中的 makeDexElements 方法
            makeDexElements = ReflexUtils.reflexMethod(
                    pathList, "makeDexElements",
                    ArrayList.class, File.class, ArrayList.class);
            ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
            addElements = (Object[]) makeDexElements.invoke(pathList, dexFiles,
                    optimizedDirectory,
                    suppressedExceptions);

        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {   // 7.0 以上版本 makePathElements

            // 反射 7.0 以上版本的 DexPathList 中的 makeDexElements 方法
            makeDexElements = ReflexUtils.reflexMethod(pathList, "makePathElements",
                    List.class, File.class, List.class);
            ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
            addElements = (Object[]) makeDexElements.invoke(pathList, dexFiles,
                    optimizedDirectory,
                    suppressedExceptions);

        }

        /*
            3 . 将 系统加载的 Element[] dexElements 数组
                与 我们自己的 Element[] dexElements 数组进行合并操作

            首先创建数组 , 数组类型与 dexElements 数组类型相同
            将 dexElements 数组中的元素拷贝到 newElements 前半部分, 拷贝元素个数是 dexElements.size
            将 addElements 数组中的元素拷贝到 newElements 后半部分, 拷贝元素个数是 dexElements.size
         */
        Object[] newElements = (Object[]) Array.newInstance(
                dexElements.getClass().getComponentType(),
                dexElements.length + addElements.length);

        // 将 dexElements 数组中的元素拷贝到 newElements 前半部分, 拷贝元素个数是 dexElements.size
        System.arraycopy(dexElements, 0, newElements, 0, dexElements.length);

        // 将 addElements 数组中的元素拷贝到 newElements 后半部分, 拷贝元素个数是 dexElements.size
        System.arraycopy(addElements, 0, newElements, dexElements.length, addElements.length);


        /*
            4 . 替换 ClassLoader 加载过程中的 Element[] dexElements 数组 ( 封装在 DexPathList 中 )
         */
        dexElementsField.set(pathList, newElements);

        Log.i(TAG, "loadDex 完成");

    }


    @Override
    public void onCreate() {
        super.onCreate();

        // 如果之前没有替换过 , 执行 Application 替换操作
        // 说明没有调用到 createPackageContext 方法
        // 该 createPackageContext 方法只有在创建 ContentProvider 时才调用到
        // 如果没有调用到 , 说明 AndroidManifest.xml 中没有配置 ContentProvider
        // 此时需要在此处进行 Application 替换
        if (delegate == null){
            applicationExchange();
        }

    }

    @Override
    public String getPackageName() {
        if(TextUtils.isEmpty(app_name)){
            // 如果 AndroidManifest.xml 中配置的 Application 全类名为空
            // 那么 不做任何操作
        }else{
            // 如果 AndroidManifest.xml 中配置的 Application 全类名不为空
            // 为了使 ActivityThread 的 installProvider 方法
            // 无法命中如下两个分支
            // 分支一 : context.getPackageName().equals(ai.packageName)
            // 分支二 : mInitialApplication.getPackageName().equals(ai.packageName)
            // 设置该方法返回值为空 , 上述两个分支就无法命中
            return "";
        }

        return super.getPackageName();
    }

    @Override
    public Context createPackageContext(String packageName, int flags)
            throws PackageManager.NameNotFoundException {
        if(TextUtils.isEmpty(app_name)){
            // 如果 AndroidManifest.xml 中配置的 Application 全类名为空
            // 说明没有进行 dex 加密操作 , 返回父类方法执行即可
            return super.createPackageContext(packageName, flags);
        }else{
            // 只有在创建 ContentProvider 时才调用到该 createPackageContext 方法 ,
            // 如果没有调用到该方法 , 说明该应用中没有配置 ContentProvider ;
            // 该方法不一定会调用到
            // 先进行 Application 替换
            applicationExchange();
            // Application 替换完成之后 , 再继续向下执行创建 ContentProvider
            return delegate;
        }
    }

    /**
     * 调用 applicationExchange 替换 Application
     * 该成员就是替换后的 Application
     */
    private Application delegate;

    /**
     * Application 替换主方法
     */
    private void applicationExchange(){
        try {

        /*
            在此处进行 Application 替换
         */

            // 先判断是否有配置 Application ,
            // 那么在 Manifest.xml 中的 meta-data 元数据 app_name 不为空
            // 如果开发者没有自定义 Application , 没有配置元数据 , 直接退出
            if (TextUtils.isEmpty(app_name)) {
                return;
            }

            // 获取上下文对象 , 保存下来 , 之后要使用
            Context baseContext = getBaseContext();

            // 通过反射获取 Application , 系统也是进行的反射操作
            Class<?> delegateClass = Class.forName(app_name);

            // 创建用户真实配置的 Application
            delegate = (Application) delegateClass.newInstance();

            // 调用 Application 的 attach 函数
            // 该函数无法直接调用 , 也需要通过反射调用
            // 这里先通过反射获取 Application 的 attach 函数
            Method attach = Application.class.getDeclaredMethod("attach", Context.class);
            // attach 方法是私有的 , 设置 attach 方法允许访问
            attach.setAccessible(true);

            // 获取上下文对象 ,
            // 该 Context 是通过调用 Application 的 attachBaseContext 方法传入的 ContextImpl
            // 将该上下文对象传入 Application 的 attach 方法中
            attach.invoke(delegate, baseContext);


            /*
                参考 : https://hanshuliang.blog.csdn.net/article/details/111569017 博客
                查询应该替换哪些对象中的哪些成员

                截止到此处, Application 创建完毕 , 下面开始逐个替换下面的 Application

                ① ContextImpl 的 private Context mOuterContext
                    成员是 kim.hsl.multipledex.ProxyApplication 对象 ;

                ② ActivityThread 中的 ArrayList<Application> mAllApplications
                    集合中添加了 kim.hsl.multipledex.ProxyApplication 对象 ;

                ③ LoadedApk 中的 mApplication 成员是 kim.hsl.multipledex.ProxyApplication 对象 ;

                ④ ActivityThread 中的 Application mInitialApplication
                    成员是 kim.hsl.multipledex.ProxyApplication 对象 ;
             */

            // I . 替换 ① ContextImpl 的 private Context mOuterContext
            //  成员是 kim.hsl.multipledex.ProxyApplication 对象
            Class<?> contextImplClass = Class.forName("android.app.ContextImpl");
            // 获取 ContextImpl 中的 mOuterContext 成员
            Field mOuterContextField = contextImplClass.getDeclaredField("mOuterContext");
            // mOuterContext 成员是私有的 , 设置可访问性
            mOuterContextField.setAccessible(true);
            // ContextImpl 就是应用的 Context , 直接通过 getBaseContext() 获取即可
            mOuterContextField.set(baseContext, delegate);




            // II . 替换 ④ ActivityThread 中的 Application mInitialApplication
            //                    成员是 kim.hsl.multipledex.ProxyApplication 对象 ;
            Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
            // 获取 ActivityThread 中的 mInitialApplication 成员
            Field mInitialApplicationField =
                    activityThreadClass.getDeclaredField("mInitialApplication");
            // mInitialApplication 成员是私有的 , 设置可访问性
            mInitialApplicationField.setAccessible(true);

            // 从 ContextImpl 对象中获取其 ActivityThread mMainThread 成员变量
            Field mMainThreadField = contextImplClass.getDeclaredField("mMainThread");
            mMainThreadField.setAccessible(true);
            // ContextImpl 就是本应用的上下文对象 , 调用 getBaseContext 方法获得
            Object mMainThread = mMainThreadField.get(baseContext);

            // ContextImpl 就是应用的 Context , 直接通过 getBaseContext() 获取即可
            mInitialApplicationField.set(mMainThread, delegate);



            // III . 替换 ② ActivityThread 中的 ArrayList<Application> mAllApplications
            //                    集合中添加了 kim.hsl.multipledex.ProxyApplication 对象 ;

            // 获取 ActivityThread 中的 mAllApplications 成员
            Field mAllApplicationsField =
                    activityThreadClass.getDeclaredField("mAllApplications");
            // mAllApplications 成员是私有的 , 设置可访问性
            mAllApplicationsField.setAccessible(true);

            // 获取 ActivityThread 中的 ArrayList<Application> mAllApplications 队列
            ArrayList<Application> mAllApplications =
                    (ArrayList<Application>) mAllApplicationsField.get(mMainThread);
            // 将真实的 Application 添加到上述队列中
            mAllApplications.add(delegate);



            // IV . 替换 ③ LoadedApk 中的 mApplication
            //          成员是 kim.hsl.multipledex.ProxyApplication 对象

            // 1. 先获取 LoadedApk 对象
            // LoadedApk 是 ContextImpl 中的 LoadedApk mPackageInfo 成员变量
            // 从 ContextImpl 对象中获取其 LoadedApk mPackageInfo 成员变量
            Field mPackageInfoField = contextImplClass.getDeclaredField("mPackageInfo");
            mPackageInfoField.setAccessible(true);
            // ContextImpl 就是本应用的上下文对象 , 调用 getBaseContext 方法获得
            Object mPackageInfo = mPackageInfoField.get(baseContext);

            // 2. 获取 LoadedApk 对象中的 mApplication 成员
            Class<?> loadedApkClass = Class.forName("android.app.LoadedApk");
            // 获取 ActivityThread 中的 mInitialApplication 成员
            Field mApplicationField =
                    loadedApkClass.getDeclaredField("mApplication");
            // LoadedApk 中的 mApplication 成员是私有的 , 设置可访问性
            mApplicationField.setAccessible(true);

            // 3. 将 Application 设置给 LoadedApk 中的 mApplication 成员
            mApplicationField.set(mPackageInfo, delegate);


            // V . 下一步操作替换替换 ApplicationInfo 中的 className , 该操作不是必须的 , 不替换也不会报错
            // 在应用中可能需要操作获取应用的相关信息 , 如果希望获取准确的信息 , 需要替换 ApplicationInfo
            // ApplicationInfo 在 LoadedApk 中

            Field mApplicationInfoField = loadedApkClass.getDeclaredField("mApplicationInfo");
            // 设置该字段可访问
            mApplicationInfoField.setAccessible(true);

            // mPackageInfo 就是 LoadedApk 对象
            // mApplicationInfo 就是从 LoadedApk 对象中获得的 mApplicationInfo 字段
            ApplicationInfo mApplicationInfo = (ApplicationInfo) mApplicationInfoField.get(mPackageInfo);

            // 设置 ApplicationInfo 中的 className 字段值
            mApplicationInfo.className = app_name;


            // 再次调用 onCreate 方法
            delegate.onCreate();

        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (NoSuchFieldException exception) {
            exception.printStackTrace();
        }
    }
}

( 2 ) OpenSSL 解码 Kotlin 类

package kim.hsl.multipledex;

import android.util.Log;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;

public class OpenSSL {
    static {
        System.loadLibrary("openssl");
    }

    /**
     * 从文件中读取 Byte 数组
     * @param file
     * @return
     * @throws Exception
     */
    public static byte[] getBytes(File file) throws Exception {
        try {
            // 创建随机读取文件
            RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r");
            // 获取文件字节数 , 创建保存文件数据的缓冲区
            byte[] buffer = new byte[(int) randomAccessFile.length()];
            // 读取整个文件数据
            randomAccessFile.readFully(buffer);
            // 关闭文件
            randomAccessFile.close();
            return buffer;
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 调用 OpenSSL 解密 dex 文件
     * @param data
     * @param path
     */
    public static native void decrypt(byte[] data, String path);
}

( 3 ) 反射工具类

package kim.hsl.multipledex;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;

public class ReflexUtils {

    /**
     * 通过反射方法获取 instance 类中的 memberName 名称的成员
     * @param instance 成员所在对象
     * @param memberName 成员变量名称
     * @return 返回 Field 类型成员
     * @throws NoSuchFieldException
     */
    public static Field reflexField(Object instance, String memberName) throws NoSuchFieldException {

        // 获取字节码类
        Class clazz = instance.getClass();

        // 循环通过反射获取
        // 可能存在通过反射没有找到成员的情况 , 此时查找其父类是否有该成员
        // 循环次数就是其父类层级个数
        while (clazz != null) {
            try {
                // 获取成员
                Field memberField = clazz.getDeclaredField(memberName);

                // 如果不是 public , 无法访问 , 设置可访问
                if (!memberField.isAccessible()) {
                    memberField.setAccessible(true);
                }
                return memberField;
            } catch (NoSuchFieldException exception){
                // 如果找不到, 就到父类中查找
                clazz = clazz.getSuperclass();
            }
        }

        // 如果没有拿到成员 , 则直接中断程序 , 加载无法进行下去
        throw new NoSuchFieldException("没有在 " + clazz.getName() + " 类中找到 " + memberName +  "成员");
    }

    /**
     * 通过反射方法获取 instance 类中的 参数为 parameterTypes , 名称为 methodName 的成员方法
     * @param instance 成员方法所在对象
     * @param methodName 成员方法名称
     * @param parameterTypes 成员方法参数
     * @return
     * @throws NoSuchMethodException
     */
    public static Method reflexMethod(Object instance, String methodName, Class... parameterTypes)
            throws NoSuchMethodException {

        // 获取字节码类
        Class clazz = instance.getClass();

        // 循环通过反射获取
        // 可能存在通过反射没有找到成员方法的情况 , 此时查找其父类是否有该成员方法
        // 循环次数就是其父类层级个数
        while (clazz != null) {
            try {
                // 获取成员方法
                Method method = clazz.getDeclaredMethod(methodName, parameterTypes);

                // 如果不是 public , 无法访问 , 设置可访问
                if (!method.isAccessible()) {
                    method.setAccessible(true);
                }
                return method;
            } catch (NoSuchMethodException e) {
                // 如果找不到, 就到父类中查找
                clazz = clazz.getSuperclass();
            }
        }

        // 如果没有拿到成员 , 则直接中断程序 , 加载无法进行下去
        throw new NoSuchMethodException("没有在 " + clazz.getName() + " 类中找到 " + methodName +  "成员方法");
    }

}

( 4 ) 压缩解压缩工具类

package kim.hsl.multipledex;

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 ZipUtils {

    /**
     * 删除文件, 如果有目录, 则递归删除
     */
    private static void deleteFile(File file){
        if (file.isDirectory()){
            File[] files = file.listFiles();
            for (File f: files) {
                deleteFile(f);
            }
        }else{
            file.delete();
        }
    }

    /**
     * 解压文件
     * @param zip 被解压的压缩包文件
     * @param dir 解压后的文件存放目录
     */
    public static void unZipApk(File zip, File dir) {
        try {
            // 如果存放文件目录存在, 删除该目录
            deleteFile(dir);
            // 获取 zip 压缩包文件
            ZipFile zipFile = new ZipFile(zip);
            // 获取 zip 压缩包中每一个文件条目
            Enumeration<? extends ZipEntry> entries = zipFile.entries();
            // 遍历压缩包中的文件
            while (entries.hasMoreElements()) {
                ZipEntry zipEntry = entries.nextElement();
                // zip 压缩包中的文件名称 或 目录名称
                String name = zipEntry.getName();
                // 如果 apk 压缩包中含有以下文件 , 这些文件是 V1 签名文件保存目录 , 不需要解压 , 跳过即可
                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();
                }
            }

            // 关闭 zip 文件
            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();
    }

}

( 5 ) OpenSSL 解码 C++ 代码

需要交叉编译 OpenSSL 得到 libcrypto.a 静态库 , 在应用中使用该静态库进行解码操作 ;

#include <jni.h>
#include <stdio.h>
#include <android/log.h>
#include <malloc.h>
#include <string.h>
#include <openssl/evp.h>
#include "logging_macros.h"

JNIEXPORT void JNICALL
Java_kim_hsl_multipledex_OpenSSL_decrypt(JNIEnv *env, jobject instance, jbyteArray data, jstring path) {

    // 将 Java Byte 数组转为 C 数组
    jbyte *src = (*env)->GetByteArrayElements(env, data, NULL);
    // 将 Java String 字符串转为 C char* 字符串
    const char *filePath = (*env)->GetStringUTFChars(env, path, 0);
    // 获取 Java Byte 数组长度
    int srcLen = (*env)->GetArrayLength(env, data);

    /*
     * 下面的代码是从 OpenSSL 源码跟目录下 demos/evp/aesccm.c 中拷贝并修改
     */

    // 加密解密的上下文
    EVP_CIPHER_CTX *ctx;
    int outlen;
    // 创建加密解密上下文
    ctx = EVP_CIPHER_CTX_new();

    /* Select cipher 配置上下文解码参数
     * 配置加密模式 :
     * Java 中的加密算法类型 "AES/ECB/PKCS5Padding" , 使用 ecb 模式
     * EVP_aes_192_ecb() 配置 ecb 模式
     * AES 有五种加密模式 : CBC、ECB、CTR、OCF、CFB
     * 配置密钥 :
     * Java 中定义的密钥是 "kimhslmultiplede"
     */
    EVP_DecryptInit_ex(ctx, EVP_aes_128_ecb(), NULL, "kimhslmultiplede", NULL);


    // 申请解密输出数据内存, 申请内存长度与密文长度一样即可
    // AES 加密密文比明文要长
    uint8_t *out = malloc(srcLen);
    // 将申请的内存设置为 0
    memset(out, 0, srcLen);

    // 记录解密总长度
    int totalLen = 0;

    /*
     * 解密操作
     * int EVP_DecryptUpdate(EVP_CIPHER_CTX *ctx, unsigned char *out,
                                 int *outl, const unsigned char *in, int inl);
     * 解密 inl 长度的 in , 解密为 outl 长度的 out
     * 解密的输入数据是 src, 长度为 srcLen 字节, 注意该长度是 int 类型
     * 解密的输出数据是 out, 长度为 srcLen 字节, 注意该长度是 int* 指针类型
     */
    EVP_DecryptUpdate(ctx, out, &outlen, src, srcLen);
    totalLen += outlen; //更新总长度

    /*
     * int EVP_DecryptFinal_ex(EVP_CIPHER_CTX *ctx, unsigned char *outm,
                                   int *outl);
     * 解密时, 每次解密 16 字节, 如果超过了 16 字节 , 就会剩余一部分无法解密,
     * 之前的 out 指针已经解密了 outlen 长度, 此时接着后续解密, 指针需要进行改变 out + outlen
     * 此时需要调用该函数 , 解密剩余内容
     */
    EVP_DecryptFinal_ex(ctx, out + outlen, &outlen);
    totalLen += outlen; //更新总长度, 此时 totalLen 就是总长度

    // 解密完成, 释放上下文对象
    EVP_CIPHER_CTX_free(ctx);

    // 将解密出的明文, 写出到给定的 Java 文件中
    FILE *file = fopen(filePath, "wb");
    // 写出 out 指针指向的数据 , 写出个数 totalLen * 1 , 写出到 file 文件中
    fwrite(out, totalLen, 1, file);
    // 关闭文件
    fclose(file);
    // 释放解密出的密文内存
    free(out);

    // 释放 Java 引用
    (*env)->ReleaseByteArrayElements(env, data, src, 0);
    (*env)->ReleaseStringUTFChars(env, path, filePath);
}

( 6 ) OpenSSL 静态库涉及的 CMakeLists.txt 配置

cmake_minimum_required(VERSION 3.4.1)

# 配置编译选项, 编译类型 动态库, C++ 源码为 native-lib.c
add_library(
        openssl
        SHARED
        native-lib.c)

find_library(
        log-lib
        log)

# 设置 openssl 函数库的静态库地址 方式一 报错
set(LIB_DIR ${CMAKE_SOURCE_DIR}/lib/${ANDROID_ABI})
add_library(crypto STATIC IMPORTED)

# 预编译 openssl 静态库
set_target_properties(
        crypto
        PROPERTIES
        IMPORTED_LOCATION
        ${LIB_DIR}/libcrypto.a)
# 指定头文件
include_directories(${CMAKE_SOURCE_DIR}/include)
# 方式一配置完毕


# 设置 openssl 函数库的静态库地址 方式二

# 指定 openssl 头文件查找目录
#           CMAKE_SOURCE_DIR 指的是当前的文件地址
#include_directories(${CMAKE_SOURCE_DIR}/include)

# 指定 openssl 静态库
# CMAKE_CXX_FLAGS 表示会将 C++ 的参数传给编译器
# CMAKE_C_FLAGS 表示会将 C 参数传给编译器

# 参数设置 : 传递 CMAKE_CXX_FLAGS C+= 参数给编译器时 , 在 该参数后面指定库的路径
#   CMAKE_SOURCE_DIR 指的是当前的文件地址
#   -L 参数指定动态库的查找路径
#set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -L${CMAKE_SOURCE_DIR}/lib/armeabi-v7a")

#message("CMake octopus ${CMAKE_SOURCE_DIR} , ${ANDROID_ABI}, {CMAKE_SOURCE_DIR}/lib/${ANDROID_ABI}")

# 链接动态库
target_link_libraries(
        openssl
        crypto
        android
        ${log-lib})


2、 Java 工具


( 1 ) Java 工具主函数入口类

package kim.hsl.multiple_dex_tools

import java.io.*
import java.util.zip.*

/**
 * 此处配置 SDK 根目录绝对路径
 * D:/001_Programs/001_Android/002_Sdk/Sdk/
 * Y:/001_DevelopTools/002_Android_SDK/
 */
val sdkDirectory = "Y:/001_DevelopTools/002_Android_SDK/"

@ExperimentalStdlibApi
fun main() {
    /*
        1 . 生成 dex 文件 , 该 dex 文件中只包含解密 其它 dex 的功能

        编译工程
        会生成 Android 依赖库的 aar 文件
        生成目录是 module/build/outputs/aar/ 目录下

        前提是需要在 菜单栏 / File / Setting / Build, Execution, Deployment / Compiler
        设置界面中 , 勾选 Compile independent modules in parallel (may require larger )

        将 D:\002_Project\002_Android_Learn\DexEncryption\multiple-dex-core\build\outputs\aar
        路径下的 multiple-dex-core-debug.aar 文件后缀修改为 .zip
        解压上述文件
        拿到 classes.jar 文件即可 ;
     */

    // 获取 multiple-dex-core-debug.aar 文件对象
    var aarFile = File("multiple-dex-core/build/outputs/aar/multiple-dex-core-debug.aar")

    // 解压上述 multiple-dex-core-debug.aar 文件到 aarUnzip 目录中
    // 创建解压目录
    var aarUnzip = File("multiple-dex-tools/aarUnzip")
    // 解压操作
    unZip(aarFile, aarUnzip)

    // 拿到 multiple-dex-core-debug.aar 中解压出来的 classes.jar 文件
    var classesJarFile = File(aarUnzip, "classes.jar")

    // 创建转换后的 dex 目的文件, 下面会开始创建该 dex 文件
    var classesDexFile = File(aarUnzip, "classes.dex")

    // 打印要执行的命令
    println("cmd /c ${sdkDirectory}build-tools/30.0.2/dx.bat --dex --output ${classesDexFile.absolutePath} ${classesJarFile.absolutePath}")

    /*
        将 jar 包变成 dex 文件
        使用 dx 工具命令

        注意 : Windows 命令行命令之前需要加上 "cmd /c " 信息 , Linux 与 MAC 命令行不用添加
     */
    var process = Runtime.getRuntime().exec("cmd /c ${sdkDirectory}build-tools/30.0.2/dx.bat --dex --output ${classesDexFile.absolutePath} ${classesJarFile.absolutePath}")
    // 等待上述命令执行完毕
    process.waitFor()

    // 执行结果提示
    if(process.exitValue() == 0){
        println("生成 dex 操作 , 执行成功");
    } else {
        println("生成 dex 操作 , 执行失败");
    }


    /*
        2 . 加密 apk 中的 dex 文件
     */

    // 解压 apk 文件 , 获取所有的 dex 文件

    // 被解压的 apk 文件
    var apkFile = File("app/build/outputs/apk/debug/app-debug.apk")
    // 解压的目标文件夹
    var apkUnZipFile = File("app/build/outputs/apk/debug/unZipFile")

    // 解压文件
    unZip(apkFile, apkUnZipFile)

    // 从被解压的 apk 文件中找到所有的 dex 文件, 小项目只有 1 个, 大项目可能有多个
    // 使用文件过滤器获取后缀是 .dex 的文件
    var dexFiles : Array<File> = apkUnZipFile.listFiles({ file: File, s: String ->
        s.endsWith(".dex")
    })

    // 加密找到的 dex 文件
    var aes = AES(AES.DEFAULT_PWD)
    // 遍历 dex 文件
    for(dexFile: File in dexFiles){
        // 读取文件数据
        var bytes = getBytes(dexFile)
        // 加密文件数据
        var encryptedBytes = aes.encrypt(bytes)

        // 将加密后的数据写出到指定目录
        var outputFile = File(apkUnZipFile, "secret-${dexFile.name}")
        // 创建对应输出流
        var fileOutputStream = FileOutputStream(outputFile)

        // 将加密后的 dex 文件写出, 然后刷写 , 关闭该输出流
        fileOutputStream.write(encryptedBytes)
        fileOutputStream.flush()
        fileOutputStream.close()

        // 删除原来的文件
        dexFile.delete()
    }


    /*
        3 . 将代理 Application 中的 classes.dex 解压到上述
            app/build/outputs/apk/debug/unZipFile 目录中
     */
    // 拷贝文件到 app/build/outputs/apk/debug/unZipFile 目录中
    classesDexFile.renameTo(File(apkUnZipFile, "classes.dex"))

    // 压缩打包 , 该压缩包是未签名的压缩包
    var unSignedApk = File("app/build/outputs/apk/debug/app-unsigned.apk")
    // 压缩打包操作
    zip(apkUnZipFile, unSignedApk)


    /*
        4 . 对齐操作
     */
    // 对齐操作的输出结果, 将 app-unsigned.apk 对齐, 对齐后的文件输出到 app-unsigned-aligned.apk 中
    var unSignedAlignApk = File("app/build/outputs/apk/debug/app-unsigned-aligned.apk")

    // 打印要执行的命令
    println("cmd /c ${sdkDirectory}build-tools/30.0.2/zipalign -f 4 ${unSignedApk.absolutePath} ${unSignedAlignApk.absolutePath}")

    /*
        将 app-unsigned.apk 对齐
        使用 zipalign 工具命令

        注意 : Windows 命令行命令之前需要加上 "cmd /c " 信息 , Linux 与 MAC 命令行不用添加
     */
    process = Runtime.getRuntime().exec("cmd /c ${sdkDirectory}build-tools/30.0.2/zipalign -f 4 ${unSignedApk.absolutePath} ${unSignedAlignApk.absolutePath}")
    // 等待上述命令执行完毕
    process.waitFor()

    // 执行结果提示
    if(process.exitValue() == 0){
        println("对齐操作 执行成功");
    } else {
        println("对齐操作 执行失败");
    }



    /*
        5 . 签名操作
     */
    // 签名 apk 输出结果, 将 app-unsigned-aligned.apk 签名, 签名后的文件输出到 app-signed-aligned.apk 中
    var signedAlignApk = File("app/build/outputs/apk/debug/app-signed-aligned.apk")

    // 获取签名 jks 文件
    var jksFile = File("dex.jks")

    // 打印要执行的命令
    println("cmd /c ${sdkDirectory}build-tools/30.0.2/apksigner sign --ks ${jksFile.absolutePath} --ks-key-alias Key0 --ks-pass pass:000000 --key-pass pass:000000 --out ${signedAlignApk.absolutePath} ${unSignedAlignApk.absolutePath}")

    /*
        将 app-unsigned.apk 对齐
        使用 zipalign 工具命令

        注意 : Windows 命令行命令之前需要加上 "cmd /c " 信息 , Linux 与 MAC 命令行不用添加
     */
    process = Runtime.getRuntime().exec("cmd /c ${sdkDirectory}build-tools/30.0.2/apksigner sign --ks ${jksFile.absolutePath} --ks-key-alias Key0 --ks-pass pass:000000 --key-pass pass:000000 --out ${signedAlignApk.absolutePath} ${unSignedAlignApk.absolutePath}")

    // 打印错误日志
    var br = BufferedReader(InputStreamReader(process.errorStream))
    while ( true ){
        var line = br.readLine()
        if(line == null){
            break
        }else{
            println(line)
        }
    }
    br.close()

    // 等待上述命令执行完毕
    process.waitFor()

    // 执行结果提示
    if(process.exitValue() == 0){
        println("签名操作 执行成功");
    } else {
        println("签名操作 执行失败");
    }




}

/**
 * 删除文件, 如果有目录, 则递归删除
 */
private fun deleteFile(file: File) {
    if (file.isDirectory) {
        val files = file.listFiles()
        for (f in files) {
            deleteFile(f)
        }
    } else {
        file.delete()
    }
}

/**
 * 解压文件
 * @param zip 被解压的压缩包文件
 * @param dir 解压后的文件存放目录
 */
fun unZip(zip: File, dir: File) {
    try {
        // 如果存放文件目录存在, 删除该目录
        deleteFile(dir)
        // 获取 zip 压缩包文件
        val zipFile = ZipFile(zip)
        // 获取 zip 压缩包中每一个文件条目
        val entries = zipFile.entries()
        // 遍历压缩包中的文件
        while (entries.hasMoreElements()) {
            val zipEntry = entries.nextElement()
            // zip 压缩包中的文件名称 或 目录名称
            val name = zipEntry.name
            // 如果 apk 压缩包中含有以下文件 , 这些文件是 V1 签名文件保存目录 , 不需要解压 , 跳过即可
            if (name == "META-INF/CERT.RSA" || name == "META-INF/CERT.SF" || (name
                            == "META-INF/MANIFEST.MF")
            ) {
                continue
            }
            // 如果该文件条目 , 不是目录 , 说明就是文件
            if (!zipEntry.isDirectory) {
                val file = File(dir, name)
                // 创建目录
                if (!file.parentFile.exists()) {
                    file.parentFile.mkdirs()
                }
                // 向刚才创建的目录中写出文件
                val fileOutputStream = FileOutputStream(file)
                val inputStream = zipFile.getInputStream(zipEntry)
                val buffer = ByteArray(1024)
                var len: Int
                while (inputStream.read(buffer).also { len = it } != -1) {
                    fileOutputStream.write(buffer, 0, len)
                }
                inputStream.close()
                fileOutputStream.close()
            }
        }

        // 关闭 zip 文件
        zipFile.close()
    } catch (e: Exception) {
        e.printStackTrace()
    }
}

fun zip(dir: File, zip: File) {
    // 如果目标压缩包存在 , 删除该压缩包
    zip.delete()

    // 对输出文件做 CRC32 校验
    val cos = CheckedOutputStream(FileOutputStream(
            zip), CRC32())
    val zos = ZipOutputStream(cos)

    // 压缩文件
    compress(dir, zos, "")
    zos.flush()
    zos.close()
}

private fun compress(srcFile: File, zos: ZipOutputStream, basePath: String) {
    if (srcFile.isDirectory) {
        val files = srcFile.listFiles()
        for (file in files) {
            // zip 递归添加目录中的文件
            compress(file, zos, basePath + srcFile.name + "/")
        }
    } else {
        compressFile(srcFile, zos, basePath)
    }
}


private fun compressFile(file: File, zos: ZipOutputStream, dir: String) {
    // 拼接完整的文件路径名称
    val fullName = dir + file.name

    // app/build/outputs/apk/debug/unZipFile 路径
    val fileNames = fullName.split("/").toTypedArray()

    // 正确的文件目录名
    val sb = StringBuffer()
    if (fileNames.size > 1) {
        for (i in 1 until fileNames.size) {
            sb.append("/")
            sb.append(fileNames[i])
        }
    } else {
        sb.append("/")
    }

    // 添加 zip 条目
    val entry = ZipEntry(sb.substring(1))
    zos.putNextEntry(entry)

    // 读取 zip 条目输出到文件中
    val fis = FileInputStream(file)
    var len: Int
    val data = ByteArray(2048)
    while (fis.read(data, 0, 2048).also { len = it } != -1) {
        zos.write(data, 0, len)
    }
    fis.close()
    zos.closeEntry()
}


/**
 * 读取文件到数组中
 */
fun getBytes(file: File): ByteArray {
    // 创建随机方位文件对象
    val randomAccessFile = RandomAccessFile(file, "r")
    // 获取文件大小 , 并创建同样大小的数据组
    val buffer = ByteArray(randomAccessFile.length().toInt())
    // 读取真个文件到数组中
    randomAccessFile.readFully(buffer)
    // 关闭文件
    randomAccessFile.close()
    return buffer
}

( 2 ) dex 加密工具类

package kim.hsl.multiple_dex_tools

import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.RandomAccessFile
import java.util.zip.*
import javax.crypto.Cipher
import javax.crypto.spec.SecretKeySpec

class AES {

    // Kotlin 类中的静态变量
    companion object{
        /**
         * 加密密钥, 16 字节
         */
        val DEFAULT_PWD = "kimhslmultiplede"
    }


    /**
     * 加密解密算法类型
     */
    val algorithm = "AES/ECB/PKCS5Padding"

    /**
     * 加密算法, 目前本应用中只需要加密, 不需要解密
     */
    lateinit var encryptCipher: Cipher;

    /**
     * 解密算法
     */
    lateinit var decryptCipher: Cipher;

    @ExperimentalStdlibApi
    constructor(pwd: String){
        // 初始化加密算法
        encryptCipher = Cipher.getInstance(algorithm)
        // 初始化解密算法
        decryptCipher = Cipher.getInstance(algorithm)

        // 将密钥字符串转为字节数组
        var keyByte = pwd.toByteArray()
        // 创建密钥
        val key = SecretKeySpec(keyByte, "AES")

        // 设置算法类型, 及密钥
        encryptCipher.init(Cipher.ENCRYPT_MODE, key);
        // 设置算法类型, 及密钥
        decryptCipher.init(Cipher.DECRYPT_MODE, key);
    }

    /**
     * 加密操作
     */
    fun encrypt(contet: ByteArray) : ByteArray{
        var result : ByteArray = encryptCipher.doFinal(contet)
        return  result
    }

    /**
     * 解密操作
     */
    fun decrypt(contet: ByteArray) : ByteArray{
        var result : ByteArray = decryptCipher.doFinal(contet)
        return  result
    }

}


3、 主应用程序


( 1 ) AndroidManifest.xml 清单文件

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="kim.hsl.dex">

    <application
        android:name="kim.hsl.multipledex.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">

        <!-- app_name 值是该应用的 Application 的真实全类名
            真实 Application : kim.hsl.dex.MyApplication
            代理 Application : kim.hsl.multipledex.ProxyApplication -->
        <meta-data android:name="app_name" android:value="kim.hsl.dex.MyApplication"/>
        <!-- DEX 解密之后的目录名称版本号 , 完整目录名称为 :
                kim.hsl.dex.MyApplication_1.0 -->
        <meta-data android:name="app_version" android:value="\1.0"/>

        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <service android:name=".MyService" />

        <receiver android:name=".MyBroadCastReciver">
            <intent-filter>
                <action android:name="kim.hsl.dex.broadcast" />
            </intent-filter>
        </receiver>

        <provider
            android:exported="true"
            android:name=".MyProvider"
            android:authorities="kim.hsl.dex.myprovider" />
    </application>

</manifest>

( 2 ) Application 主类

package kim.hsl.dex;

import android.app.Application;
import android.util.Log;

public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        /*
            验证 Application 是否替换成功
            打印 Application , ApplicationContext , ApplicationInfo
         */
        Log.i("octopus.MyApplication", "Application : " + this);
        Log.i("octopus.MyApplication", "ApplicationContext : " + getApplicationContext());
        Log.i("octopus.MyApplication", "ApplicationInfo.className : " + getApplicationInfo().className);
    }
}





三、 源码资源



DEX 加密源码资源 :

val sdkDirectory = "Y:/001_DevelopTools/002_Android_SDK/"

在这里插入图片描述

  • 5
    点赞
  • 60
    收藏
    觉得还不错? 一键收藏
  • 8
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值