> For the complete documentation index, see [llms.txt](https://xfl03.gitbook.io/coremodtutor/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://xfl03.gitbook.io/coremodtutor/5/5.6.md).

# 融合

`mix in`本就有`融合`之意。

## 注入类中的`this`

现在我们来设想一下这种情况：你需要在注入类中访问一个方法，但是这个方法的其中一个参数的类型恰好是目标类的类型。这样描述可能有些抽象，我们就拿[Hyperium](https://github.com/HyperiumClient/Hyperium)举个例子。\
在Hyperium中有一个[`HyperiumLayerCape`](https://github.com/HyperiumClient/Hyperium/blob/ab4bf494f84107e05da74adbc114050acb53dab0/src/main/java/cc/hyperium/mixinsimp/renderer/HyperiumLayerCape.java)类，它的构造方法需要传入一个`LayerCape`类的实例，这样就需要在`LayerCape`初始化的时候注入一段代码用于初始化`HyperiumLayerCape`：

```java
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`类型，所以编译时会报错，解决方案也很简单——做两次强制类型转换就行了：

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

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

## 访问目标类的父类成员

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

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

```java
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, ""));
    }
}
```

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

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

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

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

## 向目标类中添加类成员

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

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

```java
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;
    }
}
```

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

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

你会得到一个异常：

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

因为Mixin会把所有配置文件中符合要求的注入类都[添加到](https://github.com/SpongePowered/Mixin/blob/2c72246fe0b67c145db5510e66c8873df18b5a9f/src/launchwrapper/java/org/spongepowered/asm/service/mojang/LaunchClassLoaderUtil.java#L148)`LaunchClassLoader`的`invalidClasses`中去，阻止外部访问注入类以至于造成不必要的麻烦。\
*虽然用反射可以运行，没有出现问题，但是这样不便于维护，而且运行效率有所损失。*

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

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

```java
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;
    }
}
```

这样，外部就能正常访问，并不会出问题：

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

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

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

比如有这么一个需求：8月14号是[OptiFine](https://optifine.net/home)作者[sp614x](https://github.com/sp614x)的生日，现在要求每年这个时候在主菜单界面`GuiMainMenu`的[闪烁标语](https://minecraft-zh.gamepedia.com/index.php?title=%E9%97%AA%E7%83%81%E6%A0%87%E8%AF%AD\&variant=zh-cn)`splashText`显示为`Happy birthday, sp614x!`。\
现在我们尝试一下： 1. 用上面一小节的方法：

```java
    package com.example.mixins;

    public interface IMixinGuiMainMenu {
        void setSplashText(String text);
    }
```

```java
    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;
        }
    }
```

```java
    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早期版本下的一个方案。
```

1. Mixin在0.6版本新增了两个注解用于应对以上这个状况：
   * `@Accessor`（[文档](http://jenkins.liteloader.com/job/Mixin/javadoc/org/spongepowered/asm/mixin/gen/Accessor.html)）\
     标记一个**抽象**方法，用于访问非公有字段。\
     方法签名需要遵循以下规则：
     * `get`开头：用于获取一个字段，此方法的返回类型必须和该字段的类型相同，且方法不能有参数。
     * `is`开头：用于获取一个`boolean`类型的字段，且方法不能有参数。
     * `set`开头：用于修改一个字段的值，方法返回类型必须是`void`，有且仅能有一个参数，参数类型和字段类型必须一致。

       方法名剩余部分必须与目标字段的一致，且首字母须大写。\
       如果指定了`value`属性的值，该属性的值需要与目标字段的名称完全一致，这样，方法名可以随意取，但是方法参数与返回类型依然需要遵循上述规则。
   * `@Invoker`（[文档](http://jenkins.liteloader.com/job/Mixin/javadoc/org/spongepowered/asm/mixin/gen/Invoker.html)）\
     标记一个**抽象**方法，用于访问非公有方法。\
     方法签名需要遵循以下规则：

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

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

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

     ```java
     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代码做不出来而已）。\
某些大佬对下面这个例子一定非常熟悉：

```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`成员，情况就变得不一样了：

```java
package com.example.mixins;

public interface IMixinMinecraft {
    float getLimitFramerate();
    String getTitle();
}
```

```java
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`（[文档](http://jenkins.liteloader.com/job/Mixin/javadoc/org/spongepowered/asm/mixin/Implements.html)） &#x20;

  这个注解用于标记一个注入类，用于存储的`@Interface`注解。
* `@Interface`（[文档](http://jenkins.liteloader.com/job/Mixin/javadoc/org/spongepowered/asm/mixin/Interface.html)） &#x20;
  * `iface`: 指定要实现的接口
  * `prefix`: 指定一个特征前缀，必须以美元符号 `$` 结尾 &#x20;

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

```java
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`属性就是为此而来：

```java
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"; }
}
```
