融合

mix in本就有融合之意。

注入类中的this

现在我们来设想一下这种情况:你需要在注入类中访问一个方法,但是这个方法的其中一个参数的类型恰好是目标类的类型。这样描述可能有些抽象,我们就拿Hyperium举个例子。
在Hyperium中有一个HyperiumLayerCape类,它的构造方法需要传入一个LayerCape类的实例,这样就需要在LayerCape初始化的时候注入一段代码用于初始化HyperiumLayerCape

package cc.hyperium.mixins.renderer;

import cc.hyperium.mixinsimp.renderer.HyperiumLayerCape;
import net.minecraft.client.renderer.entity.layers.LayerCape;
import org.spongepowered.asm.mixin.Mixin;

@Mixin(LayerCape.class)
public class MixinLayerCape {
    private HyperiumLayerCape hyperiumLayerCape = new HyperiumLayerCape(this);
}

但是这样会有一个问题:虽然实际运行时的this我们知道是LayerCape类型的,但是编译器认为的thisMixinLayerCape类型,所以编译时会报错,解决方案也很简单——做两次强制类型转换就行了:

    private HyperiumLayerCape hyperiumLayerCape = new HyperiumLayerCape((LayerCape) (Object) this);

不必担心强制类型转换带来的运行效率损耗,因为Mixin会在合并时智能地判断并除去这种不必要的强制类型转换。

访问目标类的父类成员

通过之前的教程我们知道,@Shadow注解可以标识在注入类中存在的原本存在于目标类中的成员。那么如果我们需要访问目标类的父类中的字段或者方法,该如何处理?

比如有一个需求:在主菜单GuiMainMenu中添加一个按钮。
我们知道存放按钮的字段是buttonList,但是这个字段并不存在于GuiMainMenu中,而是存在于它的父类GuiScreen中,所以我们并不能通过@Shadow来标记它。
但是解决方法也很简单:让MixinGuiMainMenu也继承GuiScreen就行了!

package com.example.mixins;

import net.minecraft.client.gui.GuiButton;
import net.minecraft.client.gui.GuiMainMenu;
import net.minecraft.client.gui.GuiScreen;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;

@Mixin(GuiMainMenu.class)
public abstract class MixinGuiMainMenu extends GuiScreen {
    // 如果继承的类没有无参构造方法并要求子类必须有显式的构造方法,
    // 那么就在注入类中随便写一个默认的构造方法就行了,
    // 不必担心注入类中的构造方法被合并到目标类中去
    private MixinGuiMainMenu() {
        super();
    }

    @Inject(method = "initGui", at = @At("RETURN"))
    private void inject_initGui(CallbackInfo ci) {
        this.buttonList.add(new GuiButton(114, 5, 14, ""));
    }
}

所以注入类访问目标类父类成员的方法是:让注入类也继承目标类的父类或接口。

类似的,如果目标类是泛型类,那么就让注入类使用相同的泛型就行了:

@Mixin(Render.class)
public abstract class MixinRender<T extends Entity>

如果泛型的有界类型是非公有的,那么只能对每个涉及到泛型的注入方法的相关参数使用@Corece注解。

向目标类中添加类成员

既然注入类能继承目标类原有的父类或接口,那么能不能让注入类继承目标类原本没有继承的类或接口,以实现在目标类中继承模组想让它继承的类或接口?
当然可以!这也是Mixin推荐的向目标类中添加类成员的方法之一。

可能有人会问,既然注入类最后会被合并到目标类中去,那么直接在注入类中添加成员不可以吗?
答案是:虽然可以,但有限制。
如果你仅仅是想在注入类内部访问,这么做完全没有问题:

package com.example.mixins;

import net.minecraft.client.Minecraft;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.Constant;
import org.spongepowered.asm.mixin.injection.ModifyConstant;

@Mixin(Minecraft.class)
public abstract class MixinMinecraft {
    private String title = "MyCustomTitle";

    @ModifyConstant(method = "createDisplay", constant = @Constant(stringValue = "Minecraft 1.12.2"))
    private String modifyConstant_createDisplay(String title) {
        return this.getTitle();
    }

    public String getTitle() {
        return this.title;
    }
}

但是,当外部想要访问的时候,就会出问题:

public void foo() {
    // 模仿之前的两次强制类型转换
    ((MixinMinecraft) (Object) Minecraft.getMinecraft()).getTitle();
}

你会得到一个异常:

java.lang.ClassNotFoundException: com.example.mixins.MixinMinecraft

因为Mixin会把所有配置文件中符合要求的注入类都添加到LaunchClassLoaderinvalidClasses中去,阻止外部访问注入类以至于造成不必要的麻烦。
虽然用反射可以运行,没有出现问题,但是这样不便于维护,而且运行效率有所损失。

正确的做法是:创建一个接口,并在注入类中实现它,外部类访问时强制类型转换到对应接口即可:

package com.example.mixins;
// 这个接口不需要@Mixin注解,也不需要添加到配置文件中
public interface IMixinMinecraft {
    String getTitle();
}
package com.example.mixins;

import net.minecraft.client.Minecraft;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.Constant;
import org.spongepowered.asm.mixin.injection.ModifyConstant;

@Mixin(Minecraft.class)
public abstract class MixinMinecraft implements IMixinMinecraft {
    private String title = "MyCustomTitle";

    @ModifyConstant(method = "createDisplay", constant = @Constant(stringValue = "Minecraft 1.12.2"))
    private String modifyConstant_createDisplay(String title) {
        return this.getTitle();
    }

    @Override
    public String getTitle() {
        return this.title;
    }
}

这样,外部就能正常访问,并不会出问题:

public void foo() {
    ((IMixinMinecraft) Minecraft.getMinecraft()).getTitle();
}

外部访问目标类非公有成员

在以往的代码中,如果想要访问Minecraft中类的某些非公有成员,一般有两种途径:反射与Access Transformer
对于在Forge下的模组,一般都会用FMLAT,然而Mixin不仅仅能在Forge环境下运行,所以Mixin有一套自己的方法。

比如有这么一个需求:8月14号是OptiFine作者sp614x的生日,现在要求每年这个时候在主菜单界面GuiMainMenu闪烁标语splashText显示为Happy birthday, sp614x!
现在我们尝试一下:

  1. 用上面一小节的方法:

     package com.example.mixins;
    
     public interface IMixinGuiMainMenu {
         void setSplashText(String text);
     }
    
     package com.example.mixins;
    
     import net.minecraft.client.gui.GuiMainMenu;
     import org.spongepowered.asm.mixin.Mixin;
     import org.spongepowered.asm.mixin.Shadow;
    
     @Mixin(GuiMainMenu.class)
     public abstract class MixinGuiMainMenu implements IMixinGuiMainMenu {
         @Shadow private String splashText;
    
         @Override
         public void setSplashText(String text) {
             this.splashText = text;
         }
     }
    
     package com.example.mixins;
    
     import java.time.MonthDay;
     import net.minecraft.client.Minecraft;
     import net.minecraft.client.gui.GuiMainMenu;
     import net.minecraft.client.renderer.EntityRenderer;
     import org.spongepowered.asm.mixin.Final;
     import org.spongepowered.asm.mixin.Mixin;
     import org.spongepowered.asm.mixin.Shadow;
     import org.spongepowered.asm.mixin.injection.At;
     import org.spongepowered.asm.mixin.injection.Inject;
     import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
    
     @Mixin(EntityRenderer.class) // 不要问我为什么注入的是EntityRenderer,你得去问sp614x
     public abstract class MixinEntityRenderer {
         @Shadow @Final private Minecraft mc;
    
         @Inject(method = "updateCameraAndRender", at = @At("HEAD"))
         private void inject_updateCameraAndRender(CallbackInfo ci) {
             if (this.mc.currentScreen instanceof GuiMainMenu && MonthDay.now().equals(MonthDay.of(8, 14))) {
                 ((IMixinGuiMainMenu) this.mc.currentScreen).setSplashText("Happy birthday, sp614x!");
             } // 这个就相当于外部去访问 GuiMainMenu 的私有成员 splashText
         }
     }
    

    上面这种方式是完全可行的,也是Mixin早期版本下的一个方案。

  2. Mixin在0.6版本新增了两个注解用于应对以上这个状况:

    • @Accessor文档
      标记一个抽象方法,用于访问非公有字段。
      方法签名需要遵循以下规则:

      • get开头:用于获取一个字段,此方法的返回类型必须和该字段的类型相同,且方法不能有参数。
      • is开头:用于获取一个boolean类型的字段,且方法不能有参数。
      • set开头:用于修改一个字段的值,方法返回类型必须是void,有且仅能有一个参数,参数类型和字段类型必须一致。

        方法名剩余部分必须与目标字段的一致,且首字母须大写。
        如果指定了value属性的值,该属性的值需要与目标字段的名称完全一致,这样,方法名可以随意取,但是方法参数与返回类型依然需要遵循上述规则。

    • @Invoker文档
      标记一个抽象方法,用于访问非公有方法。
      方法签名需要遵循以下规则:

      • call或者invoke开头,剩余部分需要和目标方法名一致,且首字母大写,参数和返回类型也需要和目标方法一致。

        当指定了value属性之后,方法名可以任意取,参数类型和恶返回类型仍然需要遵守上述规则。

      如果你想让外部类访问这些方法,那么这两个注解仅能出现在接口中,相关接口有且能有这两个Mixin注解,否则会被Mixin加入到invalidClasses中去,造成无法访问的问题。 现在利用这个注解,我们可以抛弃MixinGuiMainMenu了,对IMixinGuiMainMenu略加修改即可:

      package com.example.mixins;
      
      import net.minecraft.client.gui.GuiMainMenu;
      import org.spongepowered.asm.mixin.Mixin;
      import org.spongepowered.asm.mixin.gen.Accessor;
      
      @Mixin(GuiMainMenu.class) // 注意这里得有@Mixin注解,而且这个接口得写到配置文件中去
      public interface IMixinGuiMainMenu {
        @Accessor
        void setSplashText(String text);
      }
      

      我们并不需要自己手动去实现这个方法,Mixin会帮我们实现。

「重载」方法

我们都知道Java中可以重载方法,同一个类中可以存在方法名相同,但方法参数不同的两个方法。 JVM在运行依靠方法签名识别方法,方法签名包含方法的返回值,因此同一个类中允许出现方法名和方法参数都相同,返回值不同的方法(只不过用Java代码做不出来而已)。
某些大佬对下面这个例子一定非常熟悉:

public void ALLATORIxDEMO(String iIiIIIIiIi) {
    // ...
}

public String ALLATORIxDEMO(String iIiIIIIiIi) {
    // ...
}

这两个方法出现在同一个类中完全没有问题,因为他们的方法签名一个是ALLATORIxDEMO(Ljava/lang/String;)V,而另一个是ALLATORIxDEMO(Ljava/lang/String;)Ljava/lang/String;

Mixin也能让目标类实现这样的效果。 如果你添加的方法虽然在目标类中有同名同参数的,但是并没有写在注入类中,那么直接按照之前的方法添加就可以了。
但是如果注入类中有同名的@Shadow成员,情况就变得不一样了:

package com.example.mixins;

public interface IMixinMinecraft {
    float getLimitFramerate();
    String getTitle();
}
package com.example.mixins;

import net.minecraft.client.Minecraft;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;

@Mixin(Minecraft.class)
public abstract class MixinMinecraft implements IMixinMinecraft {
    @Shadow
    public abstract int getLimitFramerate();
    @Override
    public float getLimitFramerate() { return 5.0F; }
    @Override
    public String getTitle() { return "MyCustomTitle"; }
}

很明显,这样编译是不可能通过的,对此,Mixin的解决方案是:编译时用一个带前缀的方法名骗过编译器,在合并时把这个前缀去掉,因此Mixin提供了下面这个注解:

  • @Implements文档
    这个注解用于标记一个注入类,用于存储的@Interface注解。
  • @Interface文档
    • iface: 指定要实现的接口
    • prefix: 指定一个特征前缀,必须以美元符号 $ 结尾

利用这两个注解,我们就能隐式实现接口:

package com.example.mixins;

import net.minecraft.client.Minecraft;
import org.spongepowered.asm.mixin.Implements;
import org.spongepowered.asm.mixin.Interface;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;

@Mixin(Minecraft.class)
@Implements(@Interface(iface = IMixinMinecraft.class, prefix = "example$"))
public abstract class MixinMinecraft {
    @Shadow
    public abstract int getLimitFramerate();
    public float example$getLimitFramerate() { return 5.0F; }
    public String getTitle() { return "MyCustomTitle"; }
    // 并不是所有继承指定接口的方法都需要加前缀
}

或者换个思路,反过来,我们也可以修改@Shadow注解标记的方法名,@Shadow注解下的prefix属性就是为此而来:

package com.example.mixins;

import net.minecraft.client.Minecraft;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;

@Mixin(Minecraft.class)
public abstract class MixinMinecraft implements IMixinMinecraft {
    @Shadow(prefix = "example$")
    public abstract int example$getLimitFramerate();
    @Override
    public float getLimitFramerate() { return 5.0F; }
    @Override
    public String getTitle() { return "MyCustomTitle"; }
}

results matching ""

    No results matching ""