# 融合

`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`中去，阻止外部访问注入类以至于造成不必要的麻烦。\
\&#xNAN;*虽然用反射可以运行，没有出现问题，但是这样不便于维护，而且运行效率有所损失。*

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

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


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://xfl03.gitbook.io/coremodtutor/5/5.6.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
