资讯专栏INFORMATION COLUMN

android基于mvp的热修复方案构思,不使用第三方

WrBug / 3273人阅读

摘要:也是较容易出现问题的一个模块,对层实现热修复,对项目的稳定性是十分有利的。需求在不修改层和层的基础上,实现对层的热修复。通过不停的更换所加载的文件,但调用方法一致,来达到热修复的目的。综上,本方案应该是可以在项目中实际应用的一个热修复方案。

本文为作者原创,转载请注明出处
由来

现在开发android项目大部分都已经由mvc转移到了mvp,关于mvp是什么大致也不必多说了,无非三个层:

m:model层,一般封装对数据的操作,增删改查,接口访问等等。

v:view层,也就是视图层,视图层不主动做什么,只是根据某些事件作出对应的视图展示。

p:Presenter层,也就是逻辑层。

图解(之前在别的博文看到的,觉得比较好就直接拿来用了):

在mvp模式中,model层和view层不再有直接交互,而把相应的工作交给了“中间人”Presenter层来处理。
因此在我们的项目中,Presenter层可以说是一个改动比较频繁,逻辑比较复杂的一个模块。也是较容易出现问题的一个模块,对Presenter层实现热修复,对项目的稳定性是十分有利的。
总结一下:

项目背景:mvp的项目。

需求:在不修改model层和view层的基础上,实现对Presenter层的热修复。

构想

核心思想就是Presenter层只写接口,然后使用java的classloader机制加载Presenter层的实现类来产生对象然后赋值给接口指针调用。通过不停的更换classloader所加载的文件,但调用方法一致,来达到热修复的目的。
下面是我画的一个整体的结构图。

尝试实现

既然大体思路都有了,那么咱们就来尝试一下能不能行得通吧。做一个案例,功能非常简单,界面上一个按钮,点击按钮吐司一个字符串,这个字符串通过。

1.创建项目

除了一路next之外我这里想说的实际上是项目module结构:

app里面主要写项目的相关界面。

motorlib里面主要写presenter接口和热修复、classloadler等相关的代码,另外model层也可以在里面写,其实它们可以写在app里面,但这样组件化的话,更有利于解耦。

motorhot这里就是写presenter的具体实现了,另外model层也可以在里面写,那样的话对于后台接口返回的数据格式变更等也可以进行修复了。

再设置一下这三个module之间的引用关系。

.appbuild.gradle

dependencies {
    ...
    //只关心接口,不关心实现,因此只引用motorlib
    implementation project(":motorlib")
}

.motorhotbuild.gradle

dependencies {
    ...
    //需要集成motorlib定义的接口,因此需要引用
    implementation project(":motorlib")
}

.motorlibbuild.gradle

dependencies {
  ...
  //因为只定义接口,所以都不需要引用
}
2.persenter接口

在motorlib中创建一个java接口,AppParsenter,里面只有一个方法:

public interface AppParsenter {
    String getStr();
}
3.实现接口persenter接口

在motorlib中创建一个java类,实现AppParsenter:

public class AppParsenterImpl implements AppParsenter {
    @Override
    public String getStr() {
        return "hello word !";
    }
}
4.热修复文件打包

在android studio中的右侧,打开Gradle一栏,然后点击(也可以直接运行gradlew命令gradlew motorhot:assembleRelease):


打包后的jar包文件存放在.motorhotuildintermediatesundles eleaseclasses.jar

拿到打包好的classes.jar,然后使用android sdk提供的dx.bat将jar包转换为dex:

CD ..androidsdkuild-tools20.0.0
dx --dex --output=..hotfix.dex ..classes.jar

这样,用于热修复的.dex文件就打包完成了。

5.ObjectFactory

因为dex的加载离不开DexClassLoader,因此我在这里先对DexClassLoader进行了一下封装:

public class Motor {
    private Context context;
    private MotorListener listener;
    private static volatile Motor motor;
    private DexClassLoader mClassLoader;

    private Motor() {
    }

    public static Motor get() {
        if (motor == null) {
            synchronized (Motor.class) {
                if (motor == null) {
                    motor = new Motor();
                }
            }
        }
        return motor;
    }

    public static void init(Context context, MotorListener listener) {
        get();
        motor.context = context;
        motor.listener = listener;
        //加载dex
        motor.initClassLoader();
        listener.initFnish();
        //todo 网络检查dex更新
    }

    private void initClassLoader() {
        String dexDir = context.getCacheDir().getAbsolutePath() + "/dex/";
        File dir = new File(dexDir);
        if (!dir.exists()) {
            dir.mkdirs();
        }
        File dexFile = new File(dir, "mydex.dex");
        if (!(dexFile.exists() && dexFile.isFile() && dexFile.length() > 0)) {
            copyFileFromAssets(context, "mydex.dex", dexFile.getAbsolutePath());
        }
        mClassLoader = new DexClassLoader(
                dexFile.getAbsolutePath(), context.getFilesDir().getAbsolutePath()
                , null, context.getClassLoader());

    }

    public boolean copyFileFromAssets(Context context, String assetName, String path) {
        boolean bRet = false;
        try {
            InputStream is = context.getAssets().open(assetName);

            File file = new File(path);
            file.createNewFile();
            FileOutputStream fos = new FileOutputStream(file);
            byte[] temp = new byte[64];
            int i = 0;
            while ((i = is.read(temp)) > 0) {
                fos.write(temp, 0, i);
            }
            fos.close();
            is.close();
            bRet = true;
        } catch (IOException e) {
            e.printStackTrace();
        }

        return bRet;
    }

    public DexClassLoader getClassLoader() {
        return mClassLoader;
    }

    public void setmClassLoader(DexClassLoader mClassLoader) {
        this.mClassLoader = mClassLoader;
    }

    public Context getContext() {
        return context;
    }

    public void setContext(Context context) {
        this.context = context;
    }

    public interface MotorListener {
        void initFnish();

        void initError(Throwable throwable);
    }
}

单例,首先从assets中把.dex文件拷贝到android的沙盒目录(android加载dex的时候有限制,必须是在沙盒目录中才能加载),然后构建好了mClassLoader就完成了。

方便起见,热修复文件就不从网络下载了,因此直接把打包好的.dex拷贝到项目的assets文件夹中。

在motorlib中创建ObjectFactory,用来通过classloader生产对象:

public class ObjectFactory {
    public static Object make(String type) {
        try {
            Class ap = (Class) Motor.get()
                    .getClassLoader().loadClass("com.example.motordex.AppParsenterImpl");
            AppParsenter o = ap.newInstance();
            return o;
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }
        return null;
    }
}

到了这里就可以注意到了,反射构建对象的时候ap.newInstance();没法传参数,因此对于Parsenter的实现类中,必须有一个无参的构造方法。

6.运行

app中新建一个activity,设置一个按钮然后添加点击事件:
layout/activity_main.xml

com.example.miqt.dexmvppdemo.MainActivity

public class MainActivity extends AppCompatActivity {
    AppParsenter ap;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Motor.init(this, new Motor.MotorListener() {
            @Override
            public void initFnish() {
                ap = (AppParsenter) ObjectFactory.make(MainActivity.class.getName());
            }
            @Override
            public void initError(Throwable throwable) {
            }
        });

    }

    public void getStr(View view) {
        String str = ap.getStr();
        Toast.makeText(this, str, Toast.LENGTH_SHORT).show();
    }
}

运行结果:

修改motorhot中的AppParsenterImpl,模拟修复了一个bug:

public class AppParsenterImpl implements AppParsenter {
    @Override
    public String getStr() {
        return "fix a bug!";
    }
}

重复4步骤,打包运行:

总结

事实证明这种构想完全可以实现,并且可以在不使用其他框架,达到热修复的目的,并且不仅仅是presenter层,在其他的层应用这种方式,也是可以的。

但实际上从上面的实践,也可以发现一些问题:

dex文件线上下载或拷贝过程中如果损坏,则可能引起程序崩溃。不过我们可以使用对dex文件取hash码然后对比下载后的文件,如果不一致则证明出错,重新下载拷贝。解决这个问题。

dex暴露在用户手机沙盒目录中,而android的沙盒目录在root之后是可以直接访问的,因此有可能被人拿到dex反编译,修改逻辑搞破坏。并且因为反射的影响,motorhot中的类文件是不可以进行混淆的,因为混淆之后就会报找不到类的异常。不过关于这个也是有解决办法的,我想到的那就是对热修复文件加密,在loader之前再在内存中解密。这样别人没法知道加密规则,也就没法解密了。

双亲委托机制,在classloader加载外部dex之前会先检查本地是否已经存在同名的类,如果有则优先加载本地已经存在的类,因此在实际使用中我们最好还要禁用双亲委托机制。

综上,本方案应该是可以在项目中实际应用的一个热修复方案。

完整代码:https://github.com/miqt/MVPHo...

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/76756.html

相关文章

  • Android 进阶

    摘要:理解内存模型对多线程编程无疑是有好处的。干货高级动画高级动画进阶,矢量动画。 这是最好的Android相关原创知识体系(100+篇) 知识体系从2016年开始构建,所有的文章都是围绕着这个知识体系来写,目前共收入了100多篇原创文章,其中有一部分未收入的文章在我的新书《Android进阶之光》中。最重要的是,这个知识体系仍旧在成长中。 Android 下拉刷新库,这一个就够了! 新鲜出...

    DoINsiSt 评论0 收藏0
  • 在 2016 年学 Android 是一种什么样的体验?

    摘要:当然,目前看来,的势头是盖过的。平台的插件化框架也是存在多种方案,各有优劣。常见的携程的,的,的以及等。另外,插件化也是解决问题的一大利器。 在 2016 年学 Android 是一种什么样的体验? @author ASCE1885的 Github 简书 微博 CSDN 知乎本文由于潜在的商业目的,不开放全文转载许可,谢谢! showImg(/img/remote/146000000...

    MonoLog 评论0 收藏0

发表评论

0条评论

WrBug

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<