Android Dex 热补丁更新

前言:

当一个App发布之后,突然发现了一个严重bug需要进行紧急修复,这时候公司各方就会忙得焦头烂额:重新打包App、测试、向各个应用市场和渠道换包、提示用户升级、用户下载、覆盖安装。有时候仅仅是为了修改了一行代码,也要付出巨大的成本进行换包和重新发布。
这时候就提出一个问题:有没有办法以补丁的方式动态修复紧急Bug,不再需要重新发布App,不再需要用户重新下载,覆盖安装?
虽然Android系统并没有提供这个技术,但是很幸运的告诉大家,答案是:可以,Android可以使用热补丁动态修复技术来解决以上这些问题。

解决方案

简单的概括一下,就是把多个dex文件塞入到app的classloader之中,但是androiddex拆包方案中的类是没有重复的,如果classes.dex和classes1.dex中有重复的类,当用到这个重复的类的时候,系统会选择哪个类进行加载呢?

ClassLoader机制

一个ClassLoader可以包含多个dex文件,每个dex文件是一个Element,多个dex文件排列成一个有序的数组dexElements,当找类的时候,会按顺序遍历dex文件,然后从当前遍历的dex文件中找类,如果找类则返回,如果找不到从下一个dex文件继续查找。
理论上,如果在不同的dex中有相同的类存在,那么会优先选择排在前面的dex文件的类,如下图:

技术实现:

创建DexLoaderUtil 类,默认加载Asset中包含定义控件BeatView的Dex包,如果Sdcard 根目录下有Dex,则优先加载sdcard
public class DexLoaderUtil {
    private static final String TAG = "DexLoaderUtil";
    public static final String HOTLIB3_DEX_NAME = "HotLib3.dex";
    public static final String HOTLIB3_CLASS_NAME = "com.example.hotlib.BeatView";
    //public static final String HOTLIB3_CLASS_NAME_FIX = "com.example.hotlib.BugClass";
    private static final int BUF_SIZE = 8 * 1024;
    private static final String DEX_PATH = "sdcard/";


    public static String getDexPath(Context context, String dexName) {
        return new File(context.getDir("dex", Context.MODE_PRIVATE), dexName)
                .getAbsolutePath();
    }


    public static String getOptimizedDexPath(Context context) {
        return context.getDir("outdex", Context.MODE_PRIVATE).getAbsolutePath();
    }


    public static boolean copysdCardDex(Context context, String dexName) {
        boolean bRes = false;
        File dexInternalStoragePath = new File(context.getDir("dex",
                Context.MODE_PRIVATE), dexName);
        BufferedInputStream bis = null;
        OutputStream dexWriter = null;


        try {
            File file = new File(DEX_PATH + dexName);
            bis = new BufferedInputStream(new FileInputStream(file));
            // bis = new BufferedInputStream(context.getAssets().open(dexName));
            dexWriter = new BufferedOutputStream(new FileOutputStream(
                    dexInternalStoragePath));
            byte[] buf = new byte[BUF_SIZE];
            int len;
            while ((len = bis.read(buf, 0, BUF_SIZE)) > 0) {
                dexWriter.write(buf, 0, len);
            }
            dexWriter.close();
            bis.close();
            bRes = true;


        } catch (FileNotFoundException e) {
            Log.d("Qinghua", e.getMessage());
            e.printStackTrace();
        } catch (IOException e) {
            Log.d("Qinghua", e.getMessage());
            e.printStackTrace();
        }
        Log.d("Qinghua", DEX_PATH + dexName + " " + bRes);
        return bRes;
    }


    public static void copyDex(Context context, String dexName) {
        if (!copysdCardDex(context, dexName)) {
            File dexInternalStoragePath = new File(context.getDir("dex",
                    Context.MODE_PRIVATE), dexName);
            BufferedInputStream bis = null;
            OutputStream dexWriter = null;


            try {
                // File file = new File(DEX_PATH+dexName);
                // bis = new BufferedInputStream(new FileInputStream(file));
                bis = new BufferedInputStream(context.getAssets().open(dexName));
                dexWriter = new BufferedOutputStream(new FileOutputStream(
                        dexInternalStoragePath));
                byte[] buf = new byte[BUF_SIZE];
                int len;
                while ((len = bis.read(buf, 0, BUF_SIZE)) > 0) {
                    dexWriter.write(buf, 0, len);
                }
                dexWriter.close();
                bis.close();


            } catch (FileNotFoundException e) {
                Log.d("Qinghua", e.getMessage());
                e.printStackTrace();
            } catch (IOException e) {
                Log.d("Qinghua", e.getMessage());
                e.printStackTrace();
            }


        }
    }


    public static void loadAndCall(Context context, String dexName,
            String className) {
        final File dexInternalStoragePath = new File(context.getDir("dex",
                Context.MODE_PRIVATE), dexName);
        final File optimizedDexOutputPath = context.getDir("outdex",
                Context.MODE_PRIVATE);


        DexClassLoader cl = new DexClassLoader(
                dexInternalStoragePath.getAbsolutePath(),
                optimizedDexOutputPath.getAbsolutePath(), null,
                context.getClassLoader());
        call(cl, className);
    }


    public static void call(ClassLoader cl, String className, Object instance,
            String functionName) {
        // String str = "";
        Class myClasz = null;
        try {
            myClasz = cl.loadClass(className);
            myClasz.getDeclaredMethod(functionName).invoke(instance);
            // .toString();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        // return str;
    }


    public static Object callInstance(ClassLoader cl, String className,
            Context para) {
        Object str = null;
        Class myClasz = null;
        try {
            myClasz = cl.loadClass(className);
            // Object instance
            str = myClasz.getConstructor(Context.class).newInstance(para);
            
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }
        return str;
    }


    public static String call(ClassLoader cl, String className) {
        String str = "";
        Class myClasz = null;
        try {
            myClasz = cl.loadClass(className);
            Object instance = myClasz.getConstructor().newInstance();
            // str = myClasz.getDeclaredMethod("test1",
            // String.class).invoke(instance, "cissy").toString();
            str = myClasz.getDeclaredMethod("test1").invoke(instance)
                    .toString();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }
        return str;
    }


    public static synchronized Boolean injectAboveEqualApiLevel14(
            String dexPath, String defaultDexOptPath, String nativeLibPath,
            String dummyClassName) {
        Log.i(TAG, "--> injectAboveEqualApiLevel14");
        PathClassLoader pathClassLoader = (PathClassLoader) DexLoaderUtil.class
                .getClassLoader();
        DexClassLoader dexClassLoader = new DexClassLoader(dexPath,
                defaultDexOptPath, nativeLibPath, pathClassLoader);
        try {
            dexClassLoader.loadClass(dummyClassName);
            Object dexElements = combineArray(
                    getDexElements(getPathList(pathClassLoader)),
                    getDexElements(getPathList(dexClassLoader)));


            Object pathList = getPathList(pathClassLoader);
            setField(pathList, pathList.getClass(), "dexElements", dexElements);
        } catch (Throwable e) {
            e.printStackTrace();
            return false;
        }
        Log.i(TAG, "<-- injectAboveEqualApiLevel14 End.");
        return true;
    }


    private static Object getPathList(Object baseDexClassLoader)
            throws IllegalArgumentException, NoSuchFieldException,
            IllegalAccessException, ClassNotFoundException {
        return getField(baseDexClassLoader,
                Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
    }


    private static Object getDexElements(Object paramObject)
            throws IllegalArgumentException, NoSuchFieldException,
            IllegalAccessException {
        return getField(paramObject, paramObject.getClass(), "dexElements");
    }


    private static Object getField(Object obj, Class<?> cl, String field)
            throws NoSuchFieldException, IllegalArgumentException,
            IllegalAccessException {
        Field localField = cl.getDeclaredField(field);
        localField.setAccessible(true);
        return localField.get(obj);
    }


    private static void setField(Object obj, Class<?> cl, String field,
            Object value) throws NoSuchFieldException,
            IllegalArgumentException, IllegalAccessException {
        Field localField = cl.getDeclaredField(field);
        localField.setAccessible(true);
        localField.set(obj, value);
    }


    private static Object combineArray(Object arrayLhs, Object arrayRhs) {
        Class<?> localClass = arrayLhs.getClass().getComponentType();
        int i = Array.getLength(arrayLhs);
        int j = i + Array.getLength(arrayRhs);
        Object result = Array.newInstance(localClass, j);
        for (int k = 0; k < j; ++k) {
            if (k < i) {
                Array.set(result, k, Array.get(arrayLhs, k));
            } else {
                Array.set(result, k, Array.get(arrayRhs, k - i));
            }
        }
        return result;
    }
}

Activity Class 调用:

public class MainActivity extends ActionBarActivity {
    private Button dowork3;
    Context ctx;
    RelativeLayout rl;


    private updateDatabaseReceiver mUpdateDatabaseReceiver;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ctx = this;
        new innitDexThread().start();
        dowork3 = (Button) findViewById(R.id.work3);
        dowork3.setText("beatview start");
        dowork3.setOnClickListener(new OnClickListener() {


            @Override
            public void onClick(View arg0) {
                DexLoaderUtil.call(getClassLoader(),
                        DexLoaderUtil.HOTLIB3_CLASS_NAME, rl.getChildAt(0),
                        "start");
            }


        });
        rl = (RelativeLayout) findViewById(R.id.beatviewP);
    }


    Handler loadHnadler = new Handler() {


        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
            case 0: {
                Toast to = Toast.makeText(getApplicationContext(), String
                        .format("Init dex file:%s done!",
                                DexLoaderUtil.HOTLIB3_DEX_NAME),
                        Toast.LENGTH_SHORT);
                to.show();
                new accSouThreadBeatInit(ctx).start();
                break;
            }
            case 3: {
                Toast to = Toast.makeText(getApplicationContext(),
                        "init beatview done!", Toast.LENGTH_SHORT);
                to.show();
                rl.addView((View) msg.obj);
                break;
            }
            default:


            }
        }
    };


    public class accSouThreadBeatInit extends Thread {
        Context mctx;


        public accSouThreadBeatInit(Context mctxP) {
            this.mctx = mctxP;
        }


        @Override
        public void run() {
            super.run();
            Object obj = DexLoaderUtil.callInstance(getClassLoader(),
                    DexLoaderUtil.HOTLIB3_CLASS_NAME, mctx);


            Message msg = loadHnadler.obtainMessage();


            if (null == obj) {
                Log.e("Qinghua", "xxxxxxxxxxxxxxxxxxx");
            }
            msg.what = 3;
            msg.obj = obj;


            // msg.what = P.SEARCHCOMPLETE;
            loadHnadler.sendMessageDelayed(msg, 20);
        }
    }


    public class innitDexThread extends Thread {
        @Override
        public void run() {
            super.run();


            if (!GerSharePerferencesInfo.getDexUpdated(getApplicationContext())) {
                DexLoaderUtil.copyDex(MainActivity.this,
                        DexLoaderUtil.HOTLIB3_DEX_NAME);


                GerSharePerferencesInfo.setDexUpdated(getApplicationContext(),
                        true);
            }
            
            String HotLib3DexPath = DexLoaderUtil.getDexPath(MainActivity.this,
                    DexLoaderUtil.HOTLIB3_DEX_NAME);
            
            DexLoaderUtil.injectAboveEqualApiLevel14(HotLib3DexPath,
                    optimizedDexOutputPath, null,
                    DexLoaderUtil.HOTLIB3_CLASS_NAME);
            Message msg = loadHnadler.obtainMessage();
            msg.what = 0;
            loadHnadler.sendMessageDelayed(msg, 20);
        }
    }


}

效果:


补丁更新

现在考虑到BeatView显示每秒帧数高,CPU占用率大,修改BeatView Code修复这个bug,重新包Dex,并放在Sdcard要目录下,去做热补丁更新:


重新启动APP的效果:



结果:

BeatView帧数降低了,整个过程中没有重新安装APP 或做Update,在用户简单的重启APP过程中就修复了Bug。

结语:

现实项目中做Dex补丁更新,还需要了解相关的分包方案,网络推送,版本控制,参数设定(Google Tag Manager)等具体细节,每一个展开说都很有内涵,本文不再细述。









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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值