融合

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就行了!

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

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

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

向目标类中添加类成员

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

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

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

你会得到一个异常:

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

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

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

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

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

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

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

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

      • get开头:用于获取一个字段,此方法的返回类型必须和该字段的类型相同,且方法不能有参数。

      • is开头:用于获取一个boolean类型的字段,且方法不能有参数。

      • set开头:用于修改一个字段的值,方法返回类型必须是void,有且仅能有一个参数,参数类型和字段类型必须一致。

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

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

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

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

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

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

「重载」方法

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

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

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

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

  • @Implements文档

    这个注解用于标记一个注入类,用于存储的@Interface注解。

  • @Interface文档

    • iface: 指定要实现的接口

    • prefix: 指定一个特征前缀,必须以美元符号 $ 结尾

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

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

Last updated

Was this helpful?