专栏首页MelonTeam专栏Android旁门左道之动态替换应用程序

Android旁门左道之动态替换应用程序

导语: 本文讲述如何通过替换应用程序类的方法,可以协助开发调试甚至应用于项目中。

作者: yarkeyzhang  2017.8.31

一,引子

继上一篇文章( Android旁门左道之动态替换系统View类 )中我们讨论的,动态替换布局中的View,从而实现不需要修改xml布局文件的情况下控制View对象的创建。同事表示因吹斯听,思路轻奇;后来发现这个功能也可以应用于某些开发场景,比如日迹业务接入手Q基础拍摄框架,不需要修改到框架代码以及布局文件,通过动态替换View方案便可以实现业务特殊功能;以及用于定位并规避一些系统View类的Bug。然而自始至终我们一直局限在View的层次,有没有办法实现动态替换任意类?我们来继续讨论这个因吹斯听的话题吧!

二,安卓平台机制

Android App进程通过应用程序唯一的包名(package name)可以获取到Apk包的信息(apk路径),然后通过dalvik.system.PathClassLoader来加载对应的应用程序类。而用户自定义的Application派生类(比如MyAppApplication)是一个应用程序中第一个被加载的用户类,我们查看它的ClassLoader如下:

|- dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.xxx.xxx-1/base.apk"], nativeLibraryDirectories=[/data/app/com.xxx.xxx-1/lib/arm, /vendor/lib, /system/lib]]]
|- java.lang.BootClassLoader

这里的BootClassLoader是PathClassLoader的“parent”。BootClassLoader可以加载各种基础的类(比如:List,String,Activity),PathClassLoader则完成从Apk中加载用户类。加载顺序:先BootClassLoader尝试加载,如果找不到类则由PathClassLoader加载。ClassA类加载ClassB类,默认使用ClassA类的ClassLoader。(包括new关键字,以及Class.forName(String)等等)

三,修改应用程序ClassLoader

我们的想法很简单,想要替换类,也就是能够控制类的加载,那么需要能够自行创建应用程序的ClassLoader。我们一旦成功地修改了应用程序ClassLoader,那么便可以动态控制用户类的加载。比如动态修改某个Activity(比如MyMainActivity)。只需要重写ClassLoader中的public Class> loadClass(String className)方法。比如:(示意代码)

public class MyClassLoader extends ClassLoader {
    @Override
    public Class> loadClass(String className) throws ClassNotFoundException {
        if (className.equals("com.xxx.xxx.OldActivity")) {
            className = “com.xxx.xxx.NewActivity”;
        }
        return super.loadClass(className);
    }
}

如何修改应用程序ClassLoader?

1,插件框架方案:

有了解过插件框架原理的同学想必已经明白,比较彻底的做法是通过Android单进程多Application实例的特性:让假的FakeApplication先启动进程,然后构建一个NewClassLoader加载真正的MyAppApplication类。这样一来,我们整个App的用户代码都会被NewClassLoader加载,而不是默认的PathClassLoader。在NewClassLoader的实现中做手脚,我们可以动态替换类。插件框架的改动会比较大,我们不想把事情搞太大,看看能否在应用内自身完成替换。对插件框架有兴趣的我们可以私下一起讨论。

2,应用自身替换

应用自身替换,也就是需要在Application类以及启动之后开始做手脚。

private static boolean hookPackageClassLoader(Context context, ClassLoader appClassLoaderNew) {
    try {
        Field packageInfoField = Class.forName("android.app.ContextImpl").getDeclaredField("mPackageInfo");
        packageInfoField.setAccessible(true);
        Object loadedApkObject = packageInfoField.get(context);
        Class> LoadedApkClass = Class.forName("android.app.LoadedApk");
        Method getClassLoaderMethod = LoadedApkClass.getDeclaredMethod("getClassLoader");
        ClassLoader appClassLoaderOld = (ClassLoader) getClassLoaderMethod.invoke(loadedApkObject);
        Field appClassLoaderField = LoadedApkClass.getDeclaredField("mClassLoader");
        appClassLoaderField.setAccessible(true);
        appClassLoaderField.set(loadedApkObject, appClassLoaderNew);
        return true;
    } catch (Throwable ignored) {
    }
    return false;
}

public class MyAppApplication extends Application {
    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        hookPackageClassLoader(base, new NewClassLoader());
    }
}

替换一个应用程序的ClassLoader核心代码如上所示。加上了以上的代码之后,启动一下我们的首页MyMainActivity。

public class MyMainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.e("MyMainActivity", "getClass().getClassLoader() : " + getClass().getClassLoader());
        Log.e("MyMainActivity", "getClassLoader() : " + getClassLoader());
        Log.e("MyMainActivity", "MyView classLoader : " + MyView.class.getClassLoader());
    }
}

非常成功!我们发现MyMainActivity.class,以及context.getClassLoader都会是我们自定义的NewClassLoader。而且!如上第三句日志,自定义MyView类,它的ClassLoader也是NewClassLoader,也证明了ClassLoader的“传承”。自此,MyMainActivity类中加载的类都会经过NewClassLoader,于是我们可以控制它们的加载。

但是!这里比插件框架方案迟了一步。细心的你发现了问题,也就是说无论如何,我们的MyAppApplication类需要先被系统加载起来,它的ClassLoader是系统创建的PathClassLoader,而不是我们想要的NewClassLoader。根据以上我们说的“传承”,那么MyAppApplication类中创建出来的对象,都会跟随MyAppApplication类的ClassLoader。这些对象之后创建的对象,也会是如此!这条线我们无法控制。

四,类加载器终极修改

待续,记得再来看我 … ( ⊙ o ⊙ )

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 深度学习入门实战(一)

    导语:现在人工智能是个大热点,而人工智能离不开机器学习,机器学习中深度学习又是比较热门的方向,本系列文章就从实战出发,介绍下如何使用MXnet进行深度学习~...

    MelonTeam
  • 使用Anko Layouts来开发Android ( 翻译)

    导语: Kotlin现在已成为Android的另一官方语言。JetBrains针对Android开发者也推出了一些有用的库和工具。Anko Layouts是使用...

    MelonTeam
  • Kotlin 初体验: 用 Kotlin 写命令行工具

    导语 :可喜可贺, kotlin 在今年的 google I/O 大会上, 成为 google android 平台的新一门官方语言, 偶尔有了个写工具的机会试...

    MelonTeam
  • 你知道这种开发模式能更好的帮你排错吗?

    很多时候我们在开发一个项目的时候写着写着sql语句报错了?(这里多指使用框架开发,当然也有原声sql语句),之后有时候会扎耳挠腮,看来看去都感觉自己的sql语句...

    思梦php
  • 初级.NET程序员,你必须知道的EF知识和经验

    注意:以下内容如果没有特别申明,默认使用的EF6.0版本,code first模式。 推荐MiniProfiler插件 工欲善其事,必先利其器。 我们使用EF和...

    逸鹏
  • 50 行代码教你爬取猫眼电影 TOP100 榜所有信息

    来源:程序人生 ID:coder_life 今天,手把手教你入门 Python 爬虫,爬取猫眼电影 TOP100 榜信息。 ? 作者 | 丁彦军 对于 Py...

    小小科
  • IOS UIResponder 触屏

    class ViewController:UIViewController { override func viewDidLoad() { super.vi...

    用户5760343
  • Yoshua Bengio首次中国演讲:深度学习通往人类水平AI的挑战

    11 月 7 日,Yoshua Bengio 受邀来到北京参加第二十届「二十一世纪的计算」国际学术研讨会。会上以及随后受邀前往清华时,他给出了题为「深度学习通往...

    机器之心
  • 「技术架构」5分钟把前端应用程序部署到NGINX

    Nginx是一个流行的web服务器,用于提供web应用程序的静态资源(客户端源)。我将解释如何将Nginx设置为静态内容资源web服务器,以及如何将它配置为Li...

    首席架构师智库
  • 大数据项目之_15_帮助文档_NTP 配置时间服务器+Linux 集群服务群起脚本+CentOS6.8 升级到 python 到 2.7

      当集群中各个节点的时间不同步,误差超过某个范围时,会导致一些集群的服务无法正常进行,这时我们应该想办法做一个定时同步集群所有节点时间的任务。

    黑泽君

扫码关注云+社区

领取腾讯云代金券

玩转腾讯云 有奖征文活动