java 反编译器源码分析

简介

由于工作需要反编译分析 java 源码,于是需要反编译器做些改动,所以就有了这篇文章。
这次要分析的反编译器是 Femflower,是著名 IDE Idea 的反编译器。源码也是从 Idea 开源部分抠出来的。
[Github](https://github.com/JetBrains/intellij-community/tree/master/plugins/java-decompiler/engine)

数据结构

CodeConstants

package org.jetbrains.java.decompiler.code;
代码段,即字节码中的关键字,是所有关键字的父类型,例如 ALOAD,NEW,RET 等。
其保存了所有字节码中关键字对应的 Constant,便于词法解析对应。

Instruction

package org.jetbrains.java.decompiler.code;

指令,CodeConstants 的子类型,对应字节码中的指令,同上例如 ALOAD,NEW,RET。众多指令的父类:

这些 Instructions 将被保存在一个 Class[] 中。方法 ConstantsUtil. getInstructionInstance 从字节码数据中获取指令对应的 Instruction 类型。
private static Instruction getInstructionInstance(int opcode, int bytecode_version) {
  try {
    Instruction instr;

    if ((opcode >= CodeConstants.opc_ifeq &&
         opcode <= CodeConstants.opc_if_acmpne) ||
        opcode == CodeConstants.opc_ifnull ||
        opcode == CodeConstants.opc_ifnonnull) {
      instr = new IfInstruction();
    }
    else {

      Class cl = opcodeClasses[opcode];

      if (opcode == CodeConstants.opc_invokedynamic && bytecode_version < CodeConstants.BYTECODE_JAVA_7) {
        cl = null; // instruction unused in Java 6 and before
      }

      if (cl == null) {
        instr = new Instruction();
      }
      else {
        instr = (Instruction)cl.newInstance();
      }
    }

    instr.opcode = opcode;
    return instr;
  }
  catch (Exception ex) {
    return null;
  }
}

InstructionSequence

指令片段,其实就是一段字节码的 Instruction 集合。

IMatchable

package org.jetbrains.java.decompiler.struct.match;

可匹配接口,在字节码中,除了上面的所说的指令一类的关键字,那些类似定义的类名,变量名,方法名被称为 Matchable 字段,可结构化的字段。
Matchable 主要有两个实现,一是 Statement 分支结构,对应方法,Field,Var 的等声明,二是 Exprent 表达式,类似等于,Field 引用,New Object,if,等等。简单的说 Matchable 类似于指令后面的操作数,即 ALOAD {Mathable}。
从字节码中匹配对应的 IMatchable 类型如下:
public IMatchable findObject(MatchNode matchNode, int index)

位于 Imatchbale 接口内。

Statement

package org.jetbrains.java.decompiler.modules.decompiler.stats;
分支结构,类似于 if,Switch,Synchronize,Try Catch等含有分支的结构。

Exprent

package org.jetbrains.java.decompiler.modules.decompiler.exps;
表达式,类似方法调用,变量引用,常量,赋值,New,return 等等都是表达式。

Struct

Struct 是比较重要的,描述了 Java 里面几个比较重要的结构,类型,方法体,Field。除此之外还有一个 Context,上下文,和文件的路径相关。

CosntantPool

常量池,常量池中除了保存了常量之外,还有 Field,Method 的一些关键信息,类成员的名称,Modifers 信息都需要到常量池中读取。

ControlFlowGraph

流程控制图,CFG,程序中有关流程控制例如,循环,if 判断等等,在类似于汇编的字节码中,程序顺序执行,流程控制也是以顺序执行 + 跳转的方式实现。也就是说在字节码中流程控制是一种扁平结构的代码段,而在 java 源码中是类似于图的立体分支结构。
那么将字节码中的流程控制代码段转化为 java 代码的大致过程就是:

InstructionSequence 代码段 —(构建图)—> ControlFlowGraph —> Statement 分支语句。。。

/**
 * 将方法结构体 中的流程控制代码段 转化为分支语句
 * @param mt
 * @param md
 * @param varProc
 * @return
 * @throws IOException
   */
public static RootStatement codeToJava(StructMethod mt, MethodDescriptor md, VarProcessor varProc) throws IOException {
  StructClass cl = mt.getClassStruct();

  boolean isInitializer = CodeConstants.CLINIT_NAME.equals(mt.getName()); // for now static initializer only

  mt.expandData();
  //获取方法体的指令片段
  InstructionSequence seq = mt.getInstructionSequence();
  //控制流程图
  ControlFlowGraph graph = new ControlFlowGraph(seq);
  //移除死循环的流程块
  DeadCodeHelper.removeDeadBlocks(graph);
  //内联方法程序跳转处理
  graph.inlineJsr(mt);

  // TODO: move to the start, before jsr inlining
  DeadCodeHelper.connectDummyExitBlock(graph);

  DeadCodeHelper.removeGotos(graph);

  ExceptionDeobfuscator.removeCircularRanges(graph);

  ExceptionDeobfuscator.restorePopRanges(graph);

  if (DecompilerContext.getOption(IFernflowerPreferences.REMOVE_EMPTY_RANGES)) {
    ExceptionDeobfuscator.removeEmptyRanges(graph);
  }

  if (DecompilerContext.getOption(IFernflowerPreferences.NO_EXCEPTIONS_RETURN)) {
    // special case: single return instruction outside of a protected range
    DeadCodeHelper.incorporateValueReturns(graph);
  }

  //      ExceptionDeobfuscator.restorePopRanges(graph);
  ExceptionDeobfuscator.insertEmptyExceptionHandlerBlocks(graph);

  DeadCodeHelper.mergeBasicBlocks(graph);

  DecompilerContext.getCounterContainer().setCounter(CounterContainer.VAR_COUNTER, mt.getLocalVariables());

  if (ExceptionDeobfuscator.hasObfuscatedExceptions(graph)) {
    DecompilerContext.getLogger().writeMessage("Heavily obfuscated exception ranges found!", IFernflowerLogger.Severity.WARN);
  }

  //流程控制快 -> 代码块
  RootStatement root = DomHelper.parseGraph(graph);

  //处理 finally 块
  FinallyProcessor fProc = new FinallyProcessor(md, varProc);
  while (fProc.iterateGraph(mt, root, graph)) {
    root = DomHelper.parseGraph(graph);
  }

  // remove synchronized exception handler
  // not until now because of comparison between synchronized statements in the finally cycle
  DomHelper.removeSynchronizedHandler(root);

  //      LabelHelper.lowContinueLabels(root, new HashSet<StatEdge>());

  SequenceHelper.condenseSequences(root);

  ClearStructHelper.clearStatements(root);

  //表达式处理
  ExprProcessor proc = new ExprProcessor(md, varProc);
  proc.processStatement(root, cl);

  SequenceHelper.condenseSequences(root);

  //参数栈 v1 v2 v3 什么的
  while (true) {
    StackVarsProcessor stackProc = new StackVarsProcessor();
    stackProc.simplifyStackVars(root, mt, cl);

    varProc.setVarVersions(root);

    if (!new PPandMMHelper().findPPandMM(root)) {
      break;
    }
  }

  while (true) {
    LabelHelper.cleanUpEdges(root);

    while (true) {
      MergeHelper.enhanceLoops(root);

      if (LoopExtractHelper.extractLoops(root)) {
        continue;
      }

      if (!IfHelper.mergeAllIfs(root)) {
        break;
      }
    }

    if (DecompilerContext.getOption(IFernflowerPreferences.IDEA_NOT_NULL_ANNOTATION)) {
      if (IdeaNotNullHelper.removeHardcodedChecks(root, mt)) {
        SequenceHelper.condenseSequences(root);

        StackVarsProcessor stackProc = new StackVarsProcessor();
        stackProc.simplifyStackVars(root, mt, cl);

        varProc.setVarVersions(root);
      }
    }

    LabelHelper.identifyLabels(root);

    if (InlineSingleBlockHelper.inlineSingleBlocks(root)) {
      continue;
    }

    // initializer may have at most one return point, so no transformation of method exits permitted
    if (isInitializer || !ExitHelper.condenseExits(root)) {
      break;
    }

    // FIXME: !!
    //       if(!EliminateLoopsHelper.eliminateLoops(root)) {
    //          break;
    //       }
  }

  ExitHelper.removeRedundantReturns(root);

  SecondaryFunctionsHelper.identifySecondaryFunctions(root, varProc);

  varProc.setVarDefinitions(root);

  // must be the last invocation, because it makes the statement structure inconsistent
  // FIXME: new edge type needed
  LabelHelper.replaceContinueWithBreak(root);

  mt.releaseResources();

  return root;
}

BytecodeMappingTracer

BytecodeMappingTracer 是一个用于跟踪所写 java code 行数的追踪器,每当 TextBuffer写过一行时,tracer 内部的行数都被手动 +1,本来行数是不包括 Class 体上面的 package 语句和 import 语句的。由于需要自行添加 headlines 变量。

TextBuffer

TextBuffer 其实没什么好讲的,java code 的字符串最后都被塞到了这里,类似于 StringBuffer,不过加上了一些函数。
需要注意的是对于表达式来说,表达式是一个树形的集合,将表达式树写成 java code 就是对树进行遍历,而每个表达式即树节点都是一个单独的 TextBuffer,最后由 append 拼接。所以想知道某个表达式在一行中具体的位置是比较困难的。 
拿 ExitExprent 即 return 表达式来说:
@Override
public TextBuffer toJava(int indent, BytecodeMappingTracer tracer) {
  tracer.addMapping(bytecode);

  if (exitType == EXIT_RETURN) {
    TextBuffer buffer = new TextBuffer("return");

    if (retType.type != CodeConstants.TYPE_VOID) {
      buffer.append(' ');
      ExprProcessor.getCastedExprent(value, retType, buffer, indent, false, tracer);
    }
…………………………..
    return buffer;
  }

过程

遍历 .class 文件

  1. Femflower.decmpileContext
    //开始反编译
public void decompileContext() {
  if (DecompilerContext.getOption(IFernflowerPreferences.RENAME_ENTITIES)) {
    new IdentifierConverter().rename(structContext);
  }

  //从 Class 节点开始反编译
  classesProcessor = new ClassesProcessor(structContext);
  classesProcessor.visitor = visitor;

  DecompilerContext.setClassProcessor(classesProcessor);
  DecompilerContext.setStructContext(structContext);

  structContext.saveContext();
}
  1. StructContext.saveContext
public void saveContext() {
  for (ContextUnit unit : units.values()) {
    if (unit.isOwn()) {
      unit.save();
    }
  }
}
  1. ContextUnit.save
/**
 * 输入分发
 */
public void save() {
  switch (type) {
    //文件夹
    case TYPE_FOLDER:
      // create folder
      resultSaver.saveFolder(filename);

      // non-class files 无内容的文件,直接拷贝
      for (String[] pair : otherEntries) {
        resultSaver.copyFile(pair[0], filename, pair[1]);
      }

      // classes 类文件,需要解析
      for (int i = 0; i < classes.size(); i++) {
        StructClass cl = classes.get(i);
        String entryName = decompiledData.getClassEntryName(cl, classEntries.get(i));
        if (entryName != null) {
          //反编译这个类 得到 java String
          String content = decompiledData.getClassContent(cl);
          if (content != null) {
            int[] mapping = null;
            if (DecompilerContext.getOption(IFernflowerPreferences.BYTECODE_SOURCE_MAPPING)) {
              mapping = DecompilerContext.getBytecodeSourceMapper().getOriginalLinesMapping();
            }
            //保存到本地
            resultSaver.saveClassFile(filename, cl.qualifiedName, entryName, content, mapping);
          }
        }
      }

      break;
    //jar 文件同下 zip 文件
    case TYPE_JAR:
    case TYPE_ZIP:
      // create archive file
      resultSaver.saveFolder(archivePath);
      resultSaver.createArchive(archivePath, filename, manifest);

      // directory entries 生成文件夹结构
      for (String dirEntry : dirEntries) {
        resultSaver.saveDirEntry(archivePath, filename, dirEntry);
      }

      // non-class entries 无内容文件 copy
      for (String[] pair : otherEntries) {
        if (type != TYPE_JAR || !JarFile.MANIFEST_NAME.equalsIgnoreCase(pair[1])) {
          resultSaver.copyEntry(pair[0], archivePath, filename, pair[1]);
        }
      }

      // classes 类文件
      for (int i = 0; i < classes.size(); i++) {
        StructClass cl = classes.get(i);
        String entryName = decompiledData.getClassEntryName(cl, classEntries.get(i));
        if (entryName != null) {
          //反编译
          String content = decompiledData.getClassContent(cl);
          //保存
          resultSaver.saveClassEntry(archivePath, filename, cl.qualifiedName, entryName, content);
        }
      }
      //关闭 zip 文件
      resultSaver.closeArchive(archivePath, filename);
  }
}

反编译一个类

对类基础结构的解析

  1. 入口
/**
   * 反编译类文件
    * @param cl
   * @return
   */
@Override
public String getClassContent(StructClass cl) {
  try {
    TextBuffer buffer = new TextBuffer(ClassesProcessor.AVERAGE_CLASS_SIZE);
    buffer.append(DecompilerContext.getProperty(IFernflowerPreferences.BANNER).toString());

    classesProcessor.writeClass(cl, buffer);
    return buffer.toString();
  }
  catch (Throwable ex) {
    DecompilerContext.getLogger().writeMessage("Class " + cl.qualifiedName + " couldn't be fully decompiled.", ex);
    return null;
  }
}
  1. ClassesProcessor.writeClass
    writeClass 函数逻辑比较清晰,先写 package xxxxx, 然后是 import xxxxxx, 最后是类结构。
/**
   * 写类文件
    * @param cl
   * @param buffer
   * @throws IOException
   */
public void writeClass(StructClass cl, TextBuffer buffer) throws IOException {
  //class 树,包含父类,内部类外部类等等
  ClassNode root = mapRootClasses.get(cl.qualifiedName);
  if (root.type != ClassNode.CLASS_ROOT) {
    return;
  }

  DecompilerContext.getLogger().startReadingClass(cl.qualifiedName);
  try {
    //import 语句的集合
    ImportCollector importCollector = new ImportCollector(root);
    DecompilerContext.setImportCollector(importCollector);
    DecompilerContext.setCounterContainer(new CounterContainer());
    DecompilerContext.setBytecodeSourceMapper(new BytecodeSourceMapper());

    new LambdaProcessor().processClass(root);

    // add simple class names to implicit import
    addClassnameToImport(root, importCollector);

    // build wrappers for all nested classes (that's where actual processing takes place) 实际拼装类代码的地方
    initWrappers(root, null);

    // build 内部类
    new NestedClassProcessor().processClass(root, root);
    // build 内部类 meber 引用
    new NestedMemberAccess().propagateMemberAccess(root);

    // 写 package 语句
    int index = cl.qualifiedName.lastIndexOf("/");
    if (index >= 0) {
      String packageName = cl.qualifiedName.substring(0, index).replace('/', '.');

      buffer.append("package ");
      buffer.append(packageName);
      buffer.append(";");
      buffer.appendLineSeparator();
      buffer.appendLineSeparator();
    }
    // 写 import 语句
    int import_lines_written = importCollector.writeImports(buffer);
    if (import_lines_written > 0) {
      buffer.appendLineSeparator();
    }

    int offsetLines = buffer.countLines();

    TextBuffer classBuffer = new TextBuffer(AVERAGE_CLASS_SIZE);
    // 重点,解析 class 结构
    new ClassWriter(visitor).classToJava(root, classBuffer, 0, null, offsetLines);

    buffer.append(classBuffer);

    if (DecompilerContext.getOption(IFernflowerPreferences.BYTECODE_SOURCE_MAPPING)) {
      BytecodeSourceMapper mapper = DecompilerContext.getBytecodeSourceMapper();
      mapper.addTotalOffset(offsetLines);
      if (DecompilerContext.getOption(IFernflowerPreferences.DUMP_ORIGINAL_LINES)) {
        buffer.dumpOriginalLineNumbers(mapper.getOriginalLinesMapping());
      }
      if (DecompilerContext.getOption(IFernflowerPreferences.UNIT_TEST_MODE)) {
        buffer.appendLineSeparator();
        mapper.dumpMapping(buffer, true);
      }
    }
  }
  finally {
    destroyWrappers(root);
    DecompilerContext.getLogger().endReadingClass();
  }
}
  1. initWrappers
    ClassWrapper 对比 ClassNode 这个已知的 Class 数据结构多了 Class 内部,Method 外部的 Expert 表达式集合,以及 MethodWrapper 集合,而 MethodWrapper 与 MethodNode 的区别可以类推。
private final VBStyleCollection<Exprent, String> staticFieldInitializers
private final VBStyleCollection<Exprent, String> dynamicFieldInitializers
private final VBStyleCollection<MethodWrapper, String> methods

可以看到主要是 Field 定义右边的初始化表达式。

/**
   *
   * @param node 进入是 root class
   * @throws IOException
   */
private static void initWrappers(ClassNode node, ClassNode root) throws IOException {
  if (node.type == ClassNode.CLASS_LAMBDA) {
    return;
  }

  ClassWrapper wrapper = new ClassWrapper(node.classStruct);

  // 手动添加根类型
  if (root == null) {
      wrapper.rootClass = node.classStruct;
      root = node;
  } else {
      wrapper.rootClass = root.classStruct;
  }

  //结构解析入口
  wrapper.init();

  node.wrapper = wrapper;
  // 递归解析内部类
  for (ClassNode nd : node.nested) {
    initWrappers(nd, root);
  }
}
  1. ClassWrapper.init
/**
   * 从字节码中解析类结构
   * @throws IOException
   */
public void init() throws IOException {
  DecompilerContext.setProperty(DecompilerContext.CURRENT_CLASS, classStruct);
  DecompilerContext.setProperty(DecompilerContext.CURRENT_CLASS_WRAPPER, this);
  DecompilerContext.getLogger().startClass(classStruct.qualifiedName);

  // collect field names
  Set<String> setFieldNames = new HashSet<>();
  for (StructField fd : classStruct.getFields()) {
    setFieldNames.add(fd.getName());
  }

  int maxSec = Integer.parseInt(DecompilerContext.getProperty(IFernflowerPreferences.MAX_PROCESSING_METHOD).toString());
  boolean testMode = DecompilerContext.getOption(IFernflowerPreferences.UNIT_TEST_MODE);

  // 拼装方法
  for (StructMethod mt : classStruct.getMethods()) {
    DecompilerContext.getLogger().startMethod(mt.getName() + " " + mt.getDescriptor());

    // 参数名列表
    VarNamesCollector vc = new VarNamesCollector();
    DecompilerContext.setVarNamesCollector(vc);

    // 引用计数器
    CounterContainer counter = new CounterContainer();
    DecompilerContext.setCounterContainer(counter);
    // 方法描述, 根据方法全限定名
    MethodDescriptor md = MethodDescriptor.parseDescriptor(mt.getDescriptor());
    VarProcessor varProc = new VarProcessor(mt, md);
    DecompilerContext.setProperty(DecompilerContext.CURRENT_VAR_PROCESSOR, varProc);

    RootStatement root = null;

    boolean isError = false;

    try {
      if (mt.containsCode()) {
        if (maxSec == 0 || testMode) {
          root = MethodProcessorRunnable.codeToJava(mt, md, varProc);
        }
        else {
          MethodProcessorRunnable mtProc = new MethodProcessorRunnable(mt, md, varProc, DecompilerContext.getCurrentContext());

          Thread mtThread = new Thread(mtProc, "Java decompiler");
          //看门狗,当处理方法超过指定时间时,则认为处理失败超时,需要强行杀死线程.
          long stopAt = System.currentTimeMillis() + maxSec * 1000;

          mtThread.start();

          while (!mtProc.isFinished()) {
            try {
              synchronized (mtProc.lock) {
                // 看门狗每 0.2s 检查一次时间
                mtProc.lock.wait(200);
              }
            }
            catch (InterruptedException e) {
              killThread(mtThread);
              throw e;
            }
            //同上
            if (System.currentTimeMillis() >= stopAt) {
              String message = "Processing time limit exceeded for method " + mt.getName() + ", execution interrupted.";
              DecompilerContext.getLogger().writeMessage(message, IFernflowerLogger.Severity.ERROR);
              killThread(mtThread);
              isError = true;
              break;
            }
          }

          if (!isError) {
            root = mtProc.getResult();
          }
        }
      }
      else {
        boolean thisVar = !mt.hasModifier(CodeConstants.ACC_STATIC);

        int paramCount = 0;
        if (thisVar) {
          varProc.getThisVars().put(new VarVersionPair(0, 0), classStruct.qualifiedName);
          paramCount = 1;
        }
        paramCount += md.params.length;

        int varIndex = 0;
        for (int i = 0; i < paramCount; i++) {
          varProc.setVarName(new VarVersionPair(varIndex, 0), vc.getFreeName(varIndex));

          if (thisVar) {
            if (i == 0) {
              varIndex++;
            }
            else {
              varIndex += md.params[i - 1].stackSize;
            }
          }
          else {
            varIndex += md.params[i].stackSize;
          }
        }
      }
    }
    catch (Throwable ex) {
      DecompilerContext.getLogger().writeMessage("Method " + mt.getName() + " " + mt.getDescriptor() + " couldn't be decompiled.", ex);
      isError = true;
    }

    MethodWrapper methodWrapper = new MethodWrapper(root, varProc, mt, counter);
    methodWrapper.decompiledWithErrors = isError;

    methods.addWithKey(methodWrapper, InterpreterUtil.makeUniqueKey(mt.getName(), mt.getDescriptor()));

    // rename vars so that no one has the same name as a field
    varProc.refreshVarNames(new VarNamesCollector(setFieldNames));

    // if debug information present and should be used
    if (DecompilerContext.getOption(IFernflowerPreferences.USE_DEBUG_VAR_NAMES)) {
      StructLocalVariableTableAttribute attr = mt.getLocalVariableAttr();
      if (attr != null) {
        // only param names here
        varProc.setDebugVarNames(attr.getMapParamNames());

        // the rest is here
        methodWrapper.getOrBuildGraph().iterateExprents(exprent -> {
          List<Exprent> lst = exprent.getAllExprents(true);
          lst.add(exprent);
          lst.stream()
            .filter(e -> e.type == Exprent.EXPRENT_VAR)
            .forEach(e -> {
              VarExprent varExprent = (VarExprent)e;
              String name = varExprent.getDebugName(mt);
              if (name != null) {
                varProc.setVarName(varExprent.getVarVersionPair(), name);
              }
            });
          return 0;
        });
      }
    }

    DecompilerContext.getLogger().endMethod();
  }

  DecompilerContext.getLogger().endClass();
}

核心:ClassWritter

ClassWritter 主要解析了上面所说的类的最主要的直接成员,嵌套的内部类,Field,和 Method。
有三个主要方法:1.classTojava:解析类结构本身,2.fieldTojava:解析 Field 3.methodTojava:解析 Method。

classTojava
classToJava 是 ClassWrite 类中其他函数的入口。其最后一个参数 headLines 是自行添加的。

/**
   *
   * @param node
   * @param buffer
   * @param indent 缩进 在 Class 块中就是 {}
   * @param tracer
   * @param headLines 头部行数
   */
public void classToJava(ClassNode node, TextBuffer buffer, int indent, BytecodeMappingTracer tracer, int headLines) {
  ClassNode outerNode = (ClassNode)DecompilerContext.getProperty(DecompilerContext.CURRENT_CLASS_NODE);
  DecompilerContext.setProperty(DecompilerContext.CURRENT_CLASS_NODE, node);

  int startLine = tracer != null ? tracer.getCurrentSourceLine() : 0;
  BytecodeMappingTracer dummy_tracer = new BytecodeMappingTracer(startLine, headLines);
  dummy_tracer.visitor = visitor;
  dummy_tracer.srcPath = node.getWrapper().rootClass.qualifiedName + ".java";
  dummy_tracer.classKey = node.classStruct.qualifiedName;
  dummy_tracer.methodKey = "";

  try {

      Type type = new Type();
      type.setName(node.simpleName);
      type.setKey(node.classStruct.qualifiedName);
      type.setFullName(node.classStruct.qualifiedName.replaceAll("/", "."));
      type.setPosition(new Position());
    // last minute processing
    invokeProcessors(node);

    ClassWrapper wrapper = node.getWrapper();
    StructClass cl = wrapper.getClassStruct();

    DecompilerContext.getLogger().startWriteClass(cl.qualifiedName);

    // write class definition
    // 写方法描述 这里插入 TypeDefineNode
    int start_class_def = buffer.length();
    writeClassDefinition(node, buffer, indent, type);
    type.getPosition().line = startLine + headLines;

    boolean hasContent = false;
    boolean enumFields = false;

    dummy_tracer.incrementCurrentSourceLine(buffer.countLines(start_class_def));

    // 写 Fields
    for (StructField fd : cl.getFields()) {
      // 是否是隐藏 field 如 this$0
      boolean hide = fd.isSynthetic() && DecompilerContext.getOption(IFernflowerPreferences.REMOVE_SYNTHETIC) ||
                     wrapper.getHiddenMembers().contains(InterpreterUtil.makeUniqueKey(fd.getName(), fd.getDescriptor()));
      if (hide) continue;
      // enum field
      boolean isEnum = fd.hasModifier(CodeConstants.ACC_ENUM) && DecompilerContext.getOption(IFernflowerPreferences.DECOMPILE_ENUM);
      if (isEnum) {
        if (enumFields) {
          buffer.append(',').appendLineSeparator();
          dummy_tracer.incrementCurrentSourceLine();
        }
        enumFields = true;
      }
      else if (enumFields) {
        buffer.append(';');
        buffer.appendLineSeparator();
        buffer.appendLineSeparator();
        dummy_tracer.incrementCurrentSourceLine(2);
        enumFields = false;
      }
      // 写 Fields
      fieldToJava(wrapper, cl, fd, buffer, indent + 1, dummy_tracer); // FIXME: insert real tracer

      hasContent = true;
    }

    if (enumFields) {
      buffer.append(';').appendLineSeparator();
      dummy_tracer.incrementCurrentSourceLine();
    }

    // FIXME: fields don't matter at the moment
    startLine += buffer.countLines(start_class_def);

    // methods
    for (StructMethod mt : cl.getMethods()) {
      boolean hide = mt.isSynthetic() && DecompilerContext.getOption(IFernflowerPreferences.REMOVE_SYNTHETIC) ||
                     mt.hasModifier(CodeConstants.ACC_BRIDGE) && DecompilerContext.getOption(IFernflowerPreferences.REMOVE_BRIDGE) ||
                     wrapper.getHiddenMembers().contains(InterpreterUtil.makeUniqueKey(mt.getName(), mt.getDescriptor()));
      if (hide) continue;

      int position = buffer.length();
      int storedLine = startLine;
      if (hasContent) {
        buffer.appendLineSeparator();
        startLine++;
      }
      BytecodeMappingTracer method_tracer = new BytecodeMappingTracer(startLine, headLines);
      method_tracer.visitor = visitor;
      method_tracer.srcPath = node.getWrapper().rootClass.qualifiedName + ".java";
      method_tracer.classKey = node.classStruct.qualifiedName;
      method_tracer.methodKey = "." + mt.getName() + mt.getDescriptor();

      boolean methodSkipped = !methodToJava(node, mt, buffer, indent + 1, method_tracer);
      if (!methodSkipped) {
        hasContent = true;
        addTracer(cl, mt, method_tracer);
        startLine = method_tracer.getCurrentSourceLine();
      }
      else {
        buffer.setLength(position);
        startLine = storedLine;
      }
    }

    // member classes 内部类
    for (ClassNode inner : node.nested) {
      if (inner.type == ClassNode.CLASS_MEMBER) {
        StructClass innerCl = inner.classStruct;
        boolean isSynthetic = (inner.access & CodeConstants.ACC_SYNTHETIC) != 0 || innerCl.isSynthetic() || inner.namelessConstructorStub;
        boolean hide = isSynthetic && DecompilerContext.getOption(IFernflowerPreferences.REMOVE_SYNTHETIC) ||
                       wrapper.getHiddenMembers().contains(innerCl.qualifiedName);
        if (hide) continue;

        if (hasContent) {
          buffer.appendLineSeparator();
          startLine++;
        }
        BytecodeMappingTracer class_tracer = new BytecodeMappingTracer(startLine, headLines);
        class_tracer.visitor = visitor;
        class_tracer.srcPath = node.getWrapper().rootClass.qualifiedName + ".java";
        class_tracer.classKey = inner.classStruct.qualifiedName;
        class_tracer.methodKey = "";
        // 递归调用
        classToJava(inner, buffer, indent + 1, class_tracer, headLines);
        startLine = buffer.countLines();

        hasContent = true;
      }
    }

    buffer.appendIndent(indent).append('}');

    if (node.type != ClassNode.CLASS_ANONYMOUS) {
      buffer.appendLineSeparator();
    }

    if (visitor != null) {
        visitor.typeDefine(type);
    }



  }
  finally {
    DecompilerContext.setProperty(DecompilerContext.CURRENT_CLASS_NODE, outerNode);
  }

  DecompilerContext.getLogger().endWriteClass();
}

classToField
写 Field ,需要注意的是 Field 的初始化即 = 右边的写则代理给了 Exprent.toJava 方法。

//写 Field 重点 ReferenceNode 切入点
private void fieldToJava(ClassWrapper wrapper, StructClass cl, StructField fd, TextBuffer buffer, int indent, BytecodeMappingTracer tracer) {

  Field field = new Field();

  int start = buffer.length();
  boolean isInterface = cl.hasModifier(CodeConstants.ACC_INTERFACE);
  boolean isDeprecated = fd.getAttributes().containsKey("Deprecated");
  boolean isEnum = fd.hasModifier(CodeConstants.ACC_ENUM) && DecompilerContext.getOption(IFernflowerPreferences.DECOMPILE_ENUM);

  field.setName(fd.getName());
  field.setInterface(isInterface);
  field.setEnum(isEnum);
  field.setKey(cl.qualifiedName + "." + fd.getName());
  field.setFullName(cl.qualifiedName.replaceAll("/", ".") + "." + fd.getName());
  field.setMemberClass(tracer.classKey);

  //写废弃 Annotation
  if (isDeprecated) {
    appendDeprecation(buffer, indent);
  }

  if (interceptor != null) {
    String oldName = interceptor.getOldName(cl.qualifiedName + " " + fd.getName() + " " + fd.getDescriptor());
    appendRenameComment(buffer, oldName, MType.FIELD, indent);
  }

  //匿名字段 多为编译器生成字段
  if (fd.isSynthetic()) {
    appendComment(buffer, "synthetic field", indent);
  }

  //写注解 插入点
  appendAnnotations(buffer, indent, fd, TypeAnnotation.FIELD);


  buffer.appendIndent(indent);

  if (!isEnum) {
    appendModifiers(buffer, fd.getAccessFlags(), FIELD_ALLOWED, isInterface, FIELD_EXCLUDED);
  }

  VarType fieldType = new VarType(fd.getDescriptor(), false);

  field.setTypeKey(fieldType.value);

  // field 全限定名
  GenericFieldDescriptor descriptor = null;
  if (DecompilerContext.getOption(IFernflowerPreferences.DECOMPILE_GENERIC_SIGNATURES)) {
    StructGenericSignatureAttribute attr = (StructGenericSignatureAttribute)fd.getAttributes().getWithKey("Signature");
    if (attr != null) {
      descriptor = GenericMain.parseFieldSignature(attr.getSignature());
    }
  }

  if (!isEnum) {
    Position typePosition = new Position();
    typePosition.line = tracer.getLinesInJavaSource();
    field.setTypePosition(typePosition);
    if (descriptor != null) {
      //由 field 全限定名获取 field 类型的名称
      typePosition.start = buffer.length();
      String typeKey = GenericMain.getGenericCastTypeName(descriptor.type);
      buffer.append(typeKey);
      typePosition.end = buffer.length();
    }
    else {
      typePosition.start = buffer.length();
      String typeKey = ExprProcessor.getCastTypeName(fieldType);
      buffer.append(typeKey);
      typePosition.end = buffer.length();
    }
    buffer.append(' ');
  }
  int charStart = buffer.length();
  //重点 写 field 的名字
  buffer.append(fd.getName());

  int charEnd = buffer.length();
  int line = tracer.getLinesInJavaSource();
  //行号增加
  tracer.incrementCurrentSourceLine(buffer.countLines(start));

  Position position = new Position();
  position.start = charStart;
  position.end = charEnd;
  position.line = line;
  position.src = wrapper.rootClass.qualifiedName + ".java";

  field.setPosition(position);

  //写初始化表达式 int a = {初始化表达式};
  Exprent initializer;
  if (fd.hasModifier(CodeConstants.ACC_STATIC)) {
    field.setStatic(true);
    initializer = wrapper.getStaticFieldInitializers().getWithKey(InterpreterUtil.makeUniqueKey(fd.getName(), fd.getDescriptor()));
  }
  else {
    initializer = wrapper.getDynamicFieldInitializers().getWithKey(InterpreterUtil.makeUniqueKey(fd.getName(), fd.getDescriptor()));
  }
  if (initializer != null) {
    if (isEnum && initializer.type == Exprent.EXPRENT_NEW) {
      NewExprent nexpr = (NewExprent)initializer;
      nexpr.setEnumConst(true);
      //写操作交由具体的表达式结构体代理
      buffer.append(nexpr.toJava(indent, tracer));
    }
    else {
      buffer.append(" = ");
      // FIXME: special case field initializer. Can map to more than one method (constructor) and bytecode intruction.
      buffer.append(initializer.toJava(indent, tracer));
    }
  }
  else if (fd.hasModifier(CodeConstants.ACC_FINAL) && fd.hasModifier(CodeConstants.ACC_STATIC)) {
    StructConstantValueAttribute attr =
      (StructConstantValueAttribute)fd.getAttributes().getWithKey(StructGeneralAttribute.ATTRIBUTE_CONSTANT_VALUE);
    if (attr != null) {
      PrimitiveConstant constant = cl.getPool().getPrimitiveConstant(attr.getIndex());
      buffer.append(" = ");
      buffer.append(new ConstExprent(fieldType, constant.value, null).toJava(indent, tracer));
    }
  }

  //结束
  if (!isEnum) {
    buffer.append(";").appendLineSeparator();
    tracer.incrementCurrentSourceLine();
  }
  if (visitor != null) {
      visitor.fieldDefine(field);
  }
}

methodToJava
同样的,Method 内部也有很多表达式,同样被代理给了 Exprent.toJava

/**
   * 写 method
   * @param node
   * @param mt
   * @param buffer
   * @param indent
   * @param tracer
   * @return
   */
private boolean methodToJava(ClassNode node, StructMethod mt, TextBuffer buffer, int indent, BytecodeMappingTracer tracer) {

  Method method = new Method();
  method.setName(mt.getName());
  method.setPosition(new Position());
  method.getPosition().src = node.getWrapper().rootClass.qualifiedName + ".java";

  ClassWrapper wrapper = node.getWrapper();
  StructClass cl = wrapper.getClassStruct();
  MethodWrapper methodWrapper = wrapper.getMethodWrapper(mt.getName(), mt.getDescriptor());

  method.setDefineClass(cl.qualifiedName);
  method.setKey(cl.qualifiedName + "." + mt.getName() + mt.getDescriptor());
  method.setFullName(cl.qualifiedName.replaceAll("/", ".") + "." + mt.getName());

  boolean hideMethod = false;
  int start_index_method = buffer.length();

  MethodWrapper outerWrapper = (MethodWrapper)DecompilerContext.getProperty(DecompilerContext.CURRENT_METHOD_WRAPPER);
  DecompilerContext.setProperty(DecompilerContext.CURRENT_METHOD_WRAPPER, methodWrapper);

  try {
    boolean isInterface = cl.hasModifier(CodeConstants.ACC_INTERFACE);
    boolean isAnnotation = cl.hasModifier(CodeConstants.ACC_ANNOTATION);
    boolean isEnum = cl.hasModifier(CodeConstants.ACC_ENUM) && DecompilerContext.getOption(IFernflowerPreferences.DECOMPILE_ENUM);
    boolean isDeprecated = mt.getAttributes().containsKey("Deprecated");
    boolean clinit = false, init = false, dinit = false;

    MethodDescriptor md = MethodDescriptor.parseDescriptor(mt.getDescriptor());

    method.setRetType(md.ret.value);
    if (md.params != null && md.params.length > 0) {
        List<String> parsKey = new ArrayList<>();
        for (VarType parType:md.params) {
            parsKey.add(parType.value);
        }
    }

    int flags = mt.getAccessFlags();
    method.setStatic(mt.hasModifier(CodeConstants.ACC_STATIC));
    if ((flags & CodeConstants.ACC_NATIVE) != 0) {
      flags &= ~CodeConstants.ACC_STRICT; // compiler bug: a strictfp class sets all methods to strictfp
      method.setStatic(true);
    }
    if (CodeConstants.CLINIT_NAME.equals(mt.getName())) {
      flags &= CodeConstants.ACC_STATIC; // ignore all modifiers except 'static' in a static initializer
      method.setStatic(true);
    }

    if (isDeprecated) {
      appendDeprecation(buffer, indent);
    }

    if (interceptor != null) {
      String oldName = interceptor.getOldName(cl.qualifiedName + " " + mt.getName() + " " + mt.getDescriptor());
      appendRenameComment(buffer, oldName, MType.METHOD, indent);
    }

    boolean isSynthetic = (flags & CodeConstants.ACC_SYNTHETIC) != 0 || mt.getAttributes().containsKey("Synthetic");
    boolean isBridge = (flags & CodeConstants.ACC_BRIDGE) != 0;
    if (isSynthetic) {
      appendComment(buffer, "synthetic method", indent);
    }
    if (isBridge) {
      appendComment(buffer, "bridge method", indent);
    }
    //写注解
    appendAnnotations(buffer, indent, mt, TypeAnnotation.METHOD_RETURN_TYPE);

    buffer.appendIndent(indent);

    appendModifiers(buffer, flags, METHOD_ALLOWED, isInterface, METHOD_EXCLUDED);

    method.setAbstract(mt.hasModifier(CodeConstants.ACC_ABSTRACT));
    method.setPublic(mt.hasModifier(CodeConstants.ACC_PUBLIC));
    method.setModifiers(flags);
    if (isInterface) {
        method.setAbstract(true);
    }


    if (isInterface && mt.containsCode()) {
      // 'default' modifier (Java 8)
      buffer.append("default ");
      method.setAbstract(false);
    }

    String name = mt.getName();
    if (CodeConstants.INIT_NAME.equals(name)) {
      if (node.type == ClassNode.CLASS_ANONYMOUS) {
        name = "";
        dinit = true;
      }
      else {
        name = node.simpleName;
        init = true;
      }
    }
    else if (CodeConstants.CLINIT_NAME.equals(name)) {
      name = "";
      clinit = true;
    }

    //方法名,方法描述
    GenericMethodDescriptor descriptor = null;
    if (DecompilerContext.getOption(IFernflowerPreferences.DECOMPILE_GENERIC_SIGNATURES)) {
      StructGenericSignatureAttribute attr = (StructGenericSignatureAttribute)mt.getAttributes().getWithKey("Signature");
      if (attr != null) {
        descriptor = GenericMain.parseMethodSignature(attr.getSignature());
        if (descriptor != null) {
          long actualParams = md.params.length;
          List<VarVersionPair> sigFields = methodWrapper.signatureFields;
          if (sigFields != null) {
            actualParams = sigFields.stream().filter(Objects::isNull).count();
          }
          else if (isEnum && init) actualParams -= 2;
          if (actualParams != descriptor.params.size()) {
            String message = "Inconsistent generic signature in method " + mt.getName() + " " + mt.getDescriptor() + " in " + cl.qualifiedName;
            DecompilerContext.getLogger().writeMessage(message, IFernflowerLogger.Severity.WARN);
            descriptor = null;
          }
        }
      }
    }

    boolean throwsExceptions = false;
    int paramCount = 0;

    if (!clinit && !dinit) {
      boolean thisVar = !mt.hasModifier(CodeConstants.ACC_STATIC);
      // 方法返回值
      if (descriptor != null && !descriptor.fparameters.isEmpty()) {
        appendTypeParameters(buffer, descriptor.fparameters, descriptor.fbounds);
        buffer.append(' ');
      }

      if (!init) {
        if (descriptor != null) {
          buffer.append(GenericMain.getGenericCastTypeName(descriptor.ret));
        }
        else {
          buffer.append(ExprProcessor.getCastTypeName(md.ret));
        }
        buffer.append(' ');
      }

      //append 方法名
      method.getPosition().start = buffer.length();
      buffer.append(toValidJavaIdentifier(name));
      method.getPosition().end = buffer.length();
      method.getPosition().line = tracer.getLinesInJavaSource();
      buffer.append('(');

      // parameters 方法参数
      List<VarVersionPair> signFields = methodWrapper.signatureFields;

      int lastVisibleParameterIndex = -1;
      for (int i = 0; i < md.params.length; i++) {
        if (signFields == null || signFields.get(i) == null) {
          lastVisibleParameterIndex = i;
        }
      }

      boolean firstParameter = true;
      int index = isEnum && init ? 3 : thisVar ? 1 : 0;
      boolean hasDescriptor = descriptor != null;
      int start = isEnum && init && !hasDescriptor ? 2 : 0;
      int params = hasDescriptor ? descriptor.params.size() : md.params.length;
      for (int i = start; i < params; i++) {
        if (hasDescriptor || (signFields == null || signFields.get(i) == null)) {
          if (!firstParameter) {
            buffer.append(", ");
          }
          // 参数上的注解
          appendParameterAnnotations(buffer, mt, paramCount);

          if (methodWrapper.varproc.getVarFinal(new VarVersionPair(index, 0)) == VarTypeProcessor.VAR_EXPLICIT_FINAL) {
            buffer.append("final ");
          }

          if (descriptor != null) {
            GenericType parameterType = descriptor.params.get(i);

            boolean isVarArg = (i == lastVisibleParameterIndex && mt.hasModifier(CodeConstants.ACC_VARARGS) && parameterType.arrayDim > 0);
            if (isVarArg) {
              parameterType = parameterType.decreaseArrayDim();
            }

            String typeName = GenericMain.getGenericCastTypeName(parameterType);
            if (ExprProcessor.UNDEFINED_TYPE_STRING.equals(typeName) &&
                DecompilerContext.getOption(IFernflowerPreferences.UNDEFINED_PARAM_TYPE_OBJECT)) {
              typeName = ExprProcessor.getCastTypeName(VarType.VARTYPE_OBJECT);
            }

            buffer.append(typeName);

            if (isVarArg) {
              buffer.append("...");
            }
          }
          else {
            VarType parameterType = md.params[i];

            boolean isVarArg = (i == lastVisibleParameterIndex && mt.hasModifier(CodeConstants.ACC_VARARGS) && parameterType.arrayDim > 0);
            if (isVarArg) {
              parameterType = parameterType.decreaseArrayDim();
            }

            String typeName = ExprProcessor.getCastTypeName(parameterType);
            if (ExprProcessor.UNDEFINED_TYPE_STRING.equals(typeName) &&
                DecompilerContext.getOption(IFernflowerPreferences.UNDEFINED_PARAM_TYPE_OBJECT)) {
              typeName = ExprProcessor.getCastTypeName(VarType.VARTYPE_OBJECT);
            }

            buffer.append(typeName);

            if (isVarArg) {
              buffer.append("...");
            }
          }

          buffer.append(' ');
          String parameterName = methodWrapper.varproc.getVarName(new VarVersionPair(index, 0));
          // 写参数名字
          buffer.append(parameterName == null ? "param" + index : parameterName); // null iff decompiled with errors

          firstParameter = false;
          paramCount++;
        }

        index += md.params[i].stackSize;
      }

      buffer.append(')');

      // 异常列表
      StructExceptionsAttribute attr = (StructExceptionsAttribute)mt.getAttributes().getWithKey("Exceptions");
      if ((descriptor != null && !descriptor.exceptions.isEmpty()) || attr != null) {
        throwsExceptions = true;
        buffer.append(" throws ");
        List<String> exceptions = new ArrayList<>();
        for (int i = 0; i < attr.getThrowsExceptions().size(); i++) {
          if (i > 0) {
            buffer.append(", ");
          }
          if (descriptor != null && !descriptor.exceptions.isEmpty()) {
            GenericType type = descriptor.exceptions.get(i);
            buffer.append(GenericMain.getGenericCastTypeName(type));
            exceptions.add(descriptor.exceptions.get(i).value);
          }
          else {
            VarType type = new VarType(attr.getExcClassname(i, cl.getPool()), true);
            buffer.append(ExprProcessor.getCastTypeName(type));
          }
        }
        method.setExceptions(exceptions);
      }
    }

    tracer.incrementCurrentSourceLine(buffer.countLines(start_index_method));

    if ((flags & (CodeConstants.ACC_ABSTRACT | CodeConstants.ACC_NATIVE)) != 0) { // native or abstract method (explicit or interface)
      if (isAnnotation) {
        StructAnnDefaultAttribute attr = (StructAnnDefaultAttribute)mt.getAttributes().getWithKey("AnnotationDefault");
        if (attr != null) {
          buffer.append(" default ");
          buffer.append(attr.getDefaultValue().toJava(0, BytecodeMappingTracer.DUMMY));
        }
      }

      buffer.append(';');
      buffer.appendLineSeparator();
      tracer.incrementCurrentSourceLine();
    }
    else {
      if (!clinit && !dinit) {
        buffer.append(' ');
      }

      // We do not have line information for method start, lets have it here for now
      buffer.append('{').appendLineSeparator();
      tracer.incrementCurrentSourceLine();

      // 写方法内部众多的表达式
      RootStatement root = wrapper.getMethodWrapper(mt.getName(), mt.getDescriptor()).root;

      if (root != null && !methodWrapper.decompiledWithErrors) { // check for existence
        try {
          TextBuffer code = root.toJava(indent + 1, tracer);

          hideMethod = (clinit || dinit || hideConstructor(wrapper, init, throwsExceptions, paramCount)) && code.length() == 0;

          buffer.append(code);
        }
        catch (Throwable ex) {
          DecompilerContext.getLogger().writeMessage("Method " + mt.getName() + " " + mt.getDescriptor() + " couldn't be written.", ex);
          methodWrapper.decompiledWithErrors = true;
        }
      }

      if (methodWrapper.decompiledWithErrors) {
        buffer.appendIndent(indent + 1);
        buffer.append("// $FF: Couldn't be decompiled");
        buffer.appendLineSeparator();
        tracer.incrementCurrentSourceLine();
      }

      if (root != null) {
        tracer.addMapping(root.getDummyExit().bytecode);
      }
      buffer.appendIndent(indent).append('}').appendLineSeparator();
      tracer.incrementCurrentSourceLine();
    }
  }
  finally {
    DecompilerContext.setProperty(DecompilerContext.CURRENT_METHOD_WRAPPER, outerWrapper);
  }

  if (visitor != null) {
      visitor.methodDefine(method);
  }

  // save total lines
  // TODO: optimize
  //tracer.setCurrentSourceLine(buffer.countLines(start_index_method));

  return !hideMethod;
}

表达式树以及流程控制

前面相当于 java code 的骨架,表达式相当于 java code 的内容了,表达式一般存在于 变量定义的 = 右边初始化字段,以及 Method 内部大量的表达式。
一组有关的表达式(一般是一行)是树形结构,上面说过将表达式树转换为 java code 即是树的遍历。遍历的同时调用 toJava 组装 TextBuffer
public TextBuffer toJava(int indent, BytecodeMappingTracer tracer) {
  throw new RuntimeException("not implemented");
}

可见是交给具体的表达式实现。
至于 Statement 流程控制语句或者说是分支语句则差不多。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
GRPC是一种高性能、开源、通用的RPC框架。它基于HTTP/2协议标准设计,支持多种编程语言。在Java中,GRPC提供了完整的Java API和工具链。 下面是GRPC Java源码分析的一些关键点: 1. 服务定义:GRPC使用Protocol Buffers(protobuf)作为服务定义语言。通过编写.proto文件,定义服务接口和数据结构。 2. 代码生成:使用protobuf编译器生成Java代码,包括服务接口和数据结构的类定义、序列化和反序列化方法等。 3. 服务实现:实现服务接口的方法,完成实际的业务逻辑。GRPC支持两种类型的服务:普通服务和流式服务。普通服务是一次请求-响应的模式,流式服务则可以支持客户端和服务端之间的多次交互。 4. 服务绑定:将服务实现绑定到GRPC服务器上。GRPC提供了一个ServerBuilder类来创建和配置GRPC服务器。 5. 客户端调用:使用GRPC提供的客户端Stub类,调用服务接口中的方法。GRPC客户端支持同步和异步两种调用方式,以及流式调用。 6. 传输协议:GRPC使用HTTP/2作为传输协议。HTTP/2提供了流控制、头部压缩、多路复用等特性,可以提高网络传输效率和性能。 7. 序列化和反序列化:GRPC使用protobuf作为序列化和反序列化的工具。protobuf提供了高效的二进制序列化和反序列化方法,可以减少数据传输量和网络延迟。 总之,GRPC Java源码分析需要理解上述关键点,并深入了解GRPC的底层实现原理和机制。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值