mix in
本就有融合
之意。
现在我们来设想一下这种情况:你需要在注入类中访问一个方法,但是这个方法的其中一个参数的类型恰好是目标类的类型。这样描述可能有些抽象,我们就拿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
类型的,但是编译器认为的this
是MixinLayerCape
类型,所以编译时会报错,解决方案也很简单——做两次强制类型转换就行了:
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会把所有配置文件中符合要求的注入类都添加到LaunchClassLoader
的invalidClasses
中去,阻止外部访问注入类以至于造成不必要的麻烦。
虽然用反射可以运行,没有出现问题,但是这样不便于维护,而且运行效率有所损失。
正确的做法是:创建一个接口,并在注入类中实现它,外部类访问时强制类型转换到对应接口即可:
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!
。
现在我们尝试一下:
-
用上面一小节的方法:
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早期版本下的一个方案。
-
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"; }
}