Android中实现Dex和apk动态加载

35 篇文章 1 订阅

Android中实现Dex和apk动态加载

一. 动态加载原理


   android中可实现dex动态加载功能, 通过反射机制,替换系统内部的类加载器,使用自定义的类加载器,可实现动态加载最新的class, 此方法主要用途:
    (1). 用于替换调系统内部实现内实现class; 
    (2).  实现bug热修复,服务器下发一个dex文件,客户端重启进程事加载新的class,从而声不知鬼不觉的"偷偷"修复bug.

二.  Dex类动态加载


   下面一使用自定义加载assets中的jar文件加以说明.
  把org.apache.tools.zip.jar放到 android项目的 assets目录下;

  (1) 加载自定义的dex jar,生成自定义的DexClassLoader
      step1: 先把jar文件从assets中拷贝到data/data中方便使用, 生成mDexPath目录
      step2: 预先建立优化后的dex输出目录optimizedDexOutputPath, 必要字段,必须是绝对路径,目录存在即可.
      step3: 生成dex类加载器
      @Param  mDexPath: dex文件源目录
      @Param  librarySearchPath: 表示一类库的目录,通常在dex加载中传null即可, 在apk加载中传: mContext.getApplicationInfo().dataDir + "/lib";
      @Param  parent 表示父类加载器, 通常为mContext.getClassLoader(), apk加载时为 mContext.getClassLoader().getParent()
      stDexClassLoader cl = new DexClassLoader(mDexPath, optimizedDexOutputPath, null, mContext.getClassLoader());

   (2) 使用反射机制替换mContext.mPackageInfo中的mClassLoader字段,把他换成自定义的StubClassloader,在其中重写loadClass(String className) throws ClassNotFoundException:
    @Override
    public Class<?> loadClass(String className) throws ClassNotFoundException {

        String realClassName = className;
        ClassLoader stubloader = AssetsDexClassLoader.getClassLoader();

        if (AssetsDexClassLoader.needCustomLoader(realClassName)&& null != stubloader) {
            //加载自定义的ClassLoader中的类,从而实现hook或者劫持系统内部的class
            Log.d(mTAG, "StubClassloader loading own ClassName: " + className + " from stubloader: " + stubloader);
            return stubloader.loadClass(realClassName);
        } else {
            return super.loadClass(RealClassName);
        }
    }

  (3). 使用动态加载方式加载jar中的类
   (1)  把java目录中的类拷贝到 package com.hulk.android.util 中, 可以随意定义其他把哦哦名.
   (2)  在自定义Application.attachBaseContext(Context context)中调用初始化:
    @Override
    protected void attachBaseContext(Context context) {
        //动态加载Assets中的jar包
        AssetsDexClassLoader.init(context);
    }

  (4). 直接使用ZipUtils中的线程函数, 或者自己写相关压缩解压功能函数.
   dex加载的详细源代码请参考详细代码:  https://download.csdn.net/download/zhanghao_Hulk/16294655

三. apk动态加载


     manifest中需要提前声明apk中的四大组件, 而且必须运行在子进程;

eg:  com.hulk.android.util:browser
     1. 把asstes中的apk拷贝到data/data的file目录下 (下面核心代码中的mApkDirPath为子apk的apk文件路径(通常在/data/data/.....目录中));
     2. DexClassLoader类加载器的基础实现与dex加载差不多,只是需要注意DexClassLoader的partent为 mContext.getClassLoader().getParent()
        DexClassLoader cl = new DexClassLoader(mDexPath,
                optimizedDexOutputPath.getAbsolutePath(), mContext.getApplicationInfo().dataDir + "/lib",
                mContext.getClassLoader().getParent());

     3. apk中的资源替换
     启动子进程的activity等组件时, 子进程中的资源默认加载主应用的, 必须把apk中的相关资源和class替换掉里面进程中, 核心函数代码:
     private void setCustomFields() {
        Object mPackageInfo = null;
        Application application = null;
        
        try {

             // 使用反射获取mContext的mPackageInfo对象
            mPackageInfo = getField(mContext, "mPackageInfo");
            Log.d(TAG, "reflect mPackageInfo Value: " + mPackageInfo);

            // 使用反射替换mPackageInfo中的mClassLoader字段 
            setField(mPackageInfo, "mClassLoader", mClassLoader);
            Log.i(TAG, "setCustomFields: Reflected to set mClassLoader: " + mClassLoader);

            if (mPluginInfo == null) {
                return;
            }

            //资源替换

             //mApkDirPath为子apk的apk文件路径(通常在/data/data/.....目录中)

            String apkPath = mApkDirPath.getAbsolutePath() + "/" + mPluginInfo.mApkFileName;

            // 使用反射替换mPackageInfo的mResDir
            setField(mPackageInfo, "mResDir", apkPath);  

            // 使用反射替换mPackageInfo的mResources:

             //先置空,然后再通过getResources拿到解析后的资源

            //原因:进程中牧人的mResources为主应用的资源, 有缓存机制,

           //先设置为null,才能重新解析mResDir目录下的资源文件
            setField(mPackageInfo, "mResources", null); 
            Object mActivityThread = getField(mPackageInfo, "mActivityThread");

            Resources resources = null;
            if (Build.VERSION.SDK_INT < 26) {
                Method getResources = mPackageInfo.getClass().getDeclaredMethod(
                        "getResources", mActivityThread.getClass());
                resources = (Resources) getResources.invoke( mPackageInfo, mActivityThread);
            } else {
                Method getResources = mPackageInfo.getClass().getDeclaredMethod("getResources");
                resources = (Resources) getResources.invoke(mPackageInfo);
            }

           // resources为子apk的资源, 通过反射在设置为进程的资源字段

            setField(mApplication.getBaseContext(), "mResources", resources);
            Log.i(TAG, "setCustomFields: Reflected to set mResources: " + resources);

            // application:

            //生成目标application,替换进程中原来的application,执行目标application周期函数
            try {

                //mPluginInfo.mAppliactionName为Application的全路径, new出一个对象
                application = (Application)Class.forName(mPluginInfo.mAppliactionName, true, mContext.getClassLoader()).newInstance();

                //反射替换mPackageInfo中的mApplication字段为实际的application对象
                setField(mPackageInfo, "mApplication", application);
                setField(mApplication.getBaseContext(), "mOuterContext", application);

                setField(mActivityThread, "mInitialApplication", application);

                //把mActivityThread中的"mAllApplications"全部替换为目标application

                ArrayList<Application> mAllApplications = (ArrayList<Application>) getField(
                        mActivityThread, "mAllApplications");

                for (int i = 0; i < mAllApplications.size(); i++) {

                     //mApplication为主应用的application
                    if (mAllApplications.get(i) == mApplication) {
                        mAllApplications.set(i, application);
                    }
                }
                Log.i(TAG, "Finished to init application: " + mPluginInfo.mAppliactionName);
            } catch (Exception e) {
                e.printStackTrace();
                Log.w(TAG, "setCustomFields: Init application error: " + e, e);
            } finally {

                //执行目标application的生命周期函数
                Method attach = Application.class.getDeclaredMethod("attach",
                        Context.class);
                attach.setAccessible(true);
                if(application!=null){
                    Log.w(TAG, "Finally, Reflect to call attach() and onCreate() for " + application );
                    attach.invoke(application, mApplication.getBaseContext());
                    application.onCreate();
                } else {
                    Log.e(TAG, "Finally, the application is null" );
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
            Log.e(TAG, "setCustomFields error: " + e, e);
        }
    }
   以上为apk动态加载的核心代码, 感兴趣的码友请留言. apk加载相对麻烦一下,详细代码 

​​​​​​​https://download.csdn.net/download/zhanghao_Hulk/20272210

4.  备注说明

(1)  缺陷:上述apk动态加载为简单的动态加载, 所有子apk的四大组件必须在宿主中声明,也就是把子应用的manifest的四大组件拷贝到宿主manifest中, 使用不是很方便,子apk的manifest有修改,就得拷贝变化部分.

优点: 但是不存在兼容问题. 现在的gradle打包可实现manifest自动合并,具体实现就该各位大神自行脑补喽.

(2) 其他动态加载框架(360的replugin), 那样在宿主中对四大组件进行"打桩", 先声明坑位, 也需要打包时进行代码注入; 这种实现相对比较复杂,存在兼容新问题, 需要考虑的东西比较多.

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值