Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

第三方代码修改神器——reflect in JAVA #205

Open
soapgu opened this issue Jun 26, 2023 · 0 comments
Open

第三方代码修改神器——reflect in JAVA #205

soapgu opened this issue Jun 26, 2023 · 0 comments
Labels
JAVA This doesn't seem right 安卓 安卓

Comments

@soapgu
Copy link
Owner

soapgu commented Jun 26, 2023

  • 前言

一般作为一名开发来说,接触最多的代码是系统级别的SDK代码,接下来就是形形色色的“第三方组件”。
虽然系统提供的SDK已经非常强大,但是也不可能把方方面面的功能全部包圆了,提供各种特定功能的第三方依赖就有庞大的市场了。
大致结构图大概是这样
图片

但是做为开发也时时刻刻和bug在做“斗争”,一般来说第三方引起的bug是一件比较棘手的事情。

  • 几种主要的解决手段

  1. 升级到最新稳定版本
    这是一个最常见解决方案。有的开源组件社区和团队非常活跃,feature和bug修复的效率高得惊人,只要动手update一下就解决所有烦恼了,简直和躺赢差不多。直接改配置就搞定了
  • 优点:操作简便
  • 缺点:对于有些比较陈旧的项目,升级第三方组件会带来不确定性风险,也可能SDK版本过低造成升级失败
  1. 从开源社区的issues里面或者StackOverflow找到相关问题成因,写一些代码work around的方式规避。
  • 优点:代码改动不大,代码改动还是在用户代码这侧。
  • 缺点:优点同时也是缺点,由于改动不在组件层,那么很可能无法根本解决问题,有部分无法规避的bug就没法修复
  1. 直接动手下载开源源码,自己操刀修改bug。自行编译替换原始组件。
    这部分的修改就直接到了组件代码这块了,打一个比分,方案2更加就像是保守治疗,而方案3就像是直接开刀,直面解决问题了。
  • 优点:可以相对比较彻底解决问题,可以向原作者pull request,贡献开源社区也是比较有价值感的

  • 缺点:需要对组件有比较深的理解,需要花比较大的精力和代价。同时有些编译环境和工具构建起来还比较复杂,不太容易搞定。后期如果第三方组件升级的跟进,如果没有被社区接受推送到主线也是一个麻烦事情

  • 开源组件新改法——reflect

那有没有把代码改动是应用在应用端,又能把bug给修掉那,听上去像量子力学啊。
图片

还真多有!
接下来说的方法就是reflect,他介于方法2和方法3之间。

  • 灵感来源

作为一个曾经一个“电脑爱好者”,相信都接触过一款叫“金山游侠”这款神器工具
图片
这款工具没有能力去修改游戏的源代码,但是就是有把你角色的HP改到9999的神奇魔法。
reflect的原理其实和金山游侠是一样的!

  • 实践演示

举一个🌰
比如这个AndroidPdfViewerPdf的阅读组件,有一个翻页跳转的bug

指望它升级不用想了。
图片
这辈子都是不会升级的!

然后再看看issues列表,看看有啥可以参考的。
I use horizontal viewpage, when Defaultpage is not 0,pdf can not be displayed in the center,and It's centered when touches it.
图片
虽然这个解决方案的代码不是完全正确,但是大致思路是对的。
但是显然work around是不行的,看上去只能方案3,修改源码。

试试看reflect吧。

  1. 派生类继承

一般来说除了一些及其重要的系统类,很少有第三方组件的类会写成final来防止被派生。
第一步就是写一个派生类来“私有化”

public class PDFPlusView extends PDFView {

    /**
     * Construct the initial view
     *
     * @param context context
     * @param set AttributeSet
     */
    public PDFPlusView(Context context, AttributeSet set) {
        super(context, set);
    }
}

这是界面声明部分

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".fragments.PdfViewerFragment">

    <com.space365.meetingpad.control.PDFPlusView
        android:id="@+id/pdfView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/black"
        tools:layout_editor_absoluteX="-397dp"
        tools:layout_editor_absoluteY="95dp" />
</androidx.constraintlayout.widget.ConstraintLayout>

写法上和自定义控件一摸一样的

  1. override方法
    我们知道多态继承可以把方法给换掉
@Override
    public void jumpTo(int page, boolean withAnimation) {
       //自定义逻辑
    }

先看下组件的源码,因为大部分的代码都是“有用”的,我需要把他抄过来

public void jumpTo(int page, boolean withAnimation) {
        if (pdfFile == null) {
            return;
        }

        page = pdfFile.determineValidPageNumberFrom(page);
        float offset = page == 0 ? 0 : -pdfFile.getPageOffset(page, zoom);
        if (swipeVertical) {
            if (withAnimation) {
                animationManager.startYAnimation(currentYOffset, offset);
            } else {
                moveTo(currentXOffset, offset);
            }
        } else {
            if (withAnimation) {
                animationManager.startXAnimation(currentXOffset, offset);
            } else {
                moveTo(offset, currentYOffset);
            }
        }
        showPage(page);
    }

如果这个组件里面的变量是public或者protect的话,那我就能直接抄了,但是这都是私有变量,我怎么访问。
不要慌,绝招来了!

  1. 通过反射获取私有变量的Field以及Method

可以通过getDeclaredField方法获取Field,可以通过getDeclaredMethod获取Method,反射的精髓就是只要Name 字符串匹配就行。

冷知识:getField()和getDeclaredField()有什么区别?

getDeclaredField
在Java中,getField()和getDeclaredField()都是用于获取类的属性(成员变量)的方法,但它们之间存在一些差异:

访问权限:getField()只能获取public修饰的属性,而getDeclaredField()可以获取所有访问权限修饰符的属性,包括public、protected、private和默认访问权限。

范围不同:getField()只能获取当前类及其父类中public修饰的属性,而getDeclaredField()可以获取当前类中所有的属性,包括public、protected、private和默认访问权限修饰符的属性,但不包括父类中的属性。

异常处理方式不同:getField()在找不到属性时会抛出NoSuchFieldException异常,而getDeclaredField()会抛出NullPointerException异常。

这里我们要拿的是私有变量所以使用getDeclaredField。getDeclaredMethod同理可得

  1. 通过setAccessible(true)完成开挂
    默认情况下,Java还是用面向对象,封装的原则来对变量进行限制访问,比如private/protect都是限制访问的关键字,但是当我们设置setAccessible(boolean flag)以后。是AccessibleObject类中的一个方法,修改默认值,如此会屏蔽Java语言的(运行时)访问检查,使得对象的私有成员可以访问,而不报错。
    当然任何东西都有两面性,取消限制确实爽但是也是危险的。

这里被ChatGPT警告了:需要注意的是,通过反射获取父类的私有属性可能会破坏类的封装性,因此应该谨慎使用。同时,如果在安卓应用程序中使用反射获取私有属性,需要注意安卓平台可能对反射机制做出的限制。

先当耳边风好啦~~。

  1. 通过Field.get(Object)和Method.Invoke方法调用
    获取变量的Field反射对象后,可以通过get方法获取变量。
    方法可以通过invoke方法来调用

  2. 如何访问并操作package-private类(内部类)

package-private在设计之初就是给package内部使用,考虑安全性,外部是无法访问的。但是为了能够修改bug,我必须要突破这层结界才行。
虽然我不能import这个package-private class,但是反射是“无所不能”的,不是啥东西是一串字符串是不能搞定的!
使用Class.forName(“{类全名}”)就可以获取到Class类型,再通过getDeclaredField方法和getDeclaredMethod方法完成进一步的操作。
虽然我无法把类型声明出来,但是可以委屈下,包装成Object。

  1. 组合测试
    接下来从理论基础上已经没有障碍了,通过一段测试代码来验证一下
public void testForReflect(){
        try {
            Field pdfFileField = PDFView.class.getDeclaredField("pdfFile");
            pdfFileField.setAccessible(true);
            Object pdfFile = pdfFileField.get(this);
            Method getCountMethod = Class.forName("com.github.barteksc.pdfviewer.PdfFile").getDeclaredMethod("getPagesCount");
            getCountMethod.setAccessible(true);
            Object pageCountValue = getCountMethod.invoke( pdfFile );
            if( pageCountValue != null ){
                int pageCount = (int)pageCountValue;
                Logger.i("TestForReflect success,page count %d",pageCount);
            }
        }
        catch (Exception exception){
            Logger.e( "TestForReflect error",exception );
        }
    }

这里pdfFile是一个私有变量,PdfFile是一个内部类,我们试试看调用pageCount方法,看看能不能拿到页数。
测试通过,那可以继续来

  1. 通过反射方式复制源码

竟然失败了!发现是NoSuchMethodException,获取反射方法失败了,但是明明源代码里面有为什么找不到。
后来发现一个“规律”,带参数的方法就不成功。原来是我用法错误,Class<?>... parameterTypes是一个重要参数,传function的name是不够的,还要传入参数类型列表。万一有重载那,看来还是api考虑得仔细。
排除问题后,成功复刻了源代码的反射版本。

@Override
    public void jumpTo(int page, boolean withAnimation) {
        try {
            Object pdfFile = this.getReflectField("pdfFile");
            if (pdfFile == null) {
                return;
            }

            Class<?> pdfFileCls = Class.forName("com.github.barteksc.pdfviewer.PdfFile");
            Object pageValue = getReflectMethod(pdfFileCls,"determineValidPageNumberFrom",int.class)
                    .invoke(pdfFile, page);
            if( pageValue != null ){
                page = (int)pageValue;
            }

            float zoom = (float)getReflectField("zoom");
            Object pageOffsetValue = getReflectMethod( pdfFileCls, "getPageOffset",int.class,float.class )
                    .invoke( pdfFile,page,zoom );
            float pageOffset =  pageOffsetValue == null ? 0 : (float)pageOffsetValue;
            float offset = page == 0 ? 0 : -pageOffset;
            boolean swipeVertical = (boolean)getReflectField("swipeVertical");


            Object animationManager = getReflectField("animationManager");
            float currentYOffset = (float)getReflectField("currentYOffset");
            float currentXOffset = (float)getReflectField("currentXOffset");

            Class<?> animationManagerCls = Class.forName("com.github.barteksc.pdfviewer.AnimationManager");
            Method startXAnimationMethod = this.getReflectMethod( animationManagerCls,"startXAnimation",float.class,float.class );
            Method startYAnimationMethod = this.getReflectMethod( animationManagerCls,"startYAnimation",float.class,float.class );
            if (swipeVertical) {
                if (withAnimation) {
                    //animationManager.startYAnimation(currentYOffset, offset);
                    startXAnimationMethod.invoke( animationManager,currentYOffset,offset);
                } else {
                    moveTo(currentXOffset, offset);
                }
            } else {
                if (withAnimation) {
                    //animationManager.startXAnimation(currentXOffset, offset);
                    startYAnimationMethod.invoke( animationManager, currentXOffset,offset);
                } else {
                    moveTo(offset, currentYOffset);
                }
            }
            this.getReflectMethod(PDFView.class,"showPage",int.class)
                    .invoke( this,page );
        }
        catch (Exception exception){
            Logger.e( "jumpTo error",exception );
        }
    }


    private Object getReflectField(String fieldName) throws IllegalAccessException, NoSuchFieldException {
        Field field = PDFView.class.getDeclaredField(fieldName);
        field.setAccessible(true);
        return field.get(this);
    }

    private Method getReflectMethod( Class<?> targetClass,String method, Class<?>... parameterTypes ) throws NoSuchMethodException {
        Method methodValue = targetClass.getDeclaredMethod(method,parameterTypes);
        methodValue.setAccessible(true);
        return methodValue;
    }
  1. 修复bug
    修复代码部分并不多。
Object pageSpacingValue = this.getReflectMethod(pdfFileCls,"getPageSpacing",int.class,float.class)
                        .invoke( pdfFile,page,zoom );
                if( pageSpacingValue != null ){
                    offset += (float)pageSpacingValue / 2f;
                }

只要加入留空部分的偏移量,那么文档就能在正中正常呈现了,完美修复。

  1. 回issue区去得瑟一下
    装逼这种事不做是不行的。
    I use horizontal viewpage, when Defaultpage is not 0,pdf can not be displayed in the center,and It's centered when touches it. DImuthuUpe/AndroidPdfViewer#674 (comment)
  • 总结&参考资料

反射虽然代码美观程度不高,但是超强的穿透式的访问性,具有强大的特殊用户。
另外写一些模版类代码也有应用。C#的反射功能基本和Java差不多。

@soapgu soapgu added JAVA This doesn't seem right 安卓 安卓 labels Jun 26, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
JAVA This doesn't seem right 安卓 安卓
Projects
None yet
Development

No branches or pull requests

1 participant