您的位置:首页 > 移动开发 > Android开发

Android Dex 热补丁更新

2016-06-16 15:57 537 查看
前言:

当一个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 = nul
4000
l;

        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)等具体细节,每一个展开说都很有内涵,本文不再细述。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息