tft每日頭條

 > 科技

 > 手機java編譯器教學

手機java編譯器教學

科技 更新时间:2025-01-12 01:40:38

  Class文件結構 我之前寫了一篇關于class文件重要性的,并且從宏觀角度解釋了下class文件的構成,文章直通車(www.juejin.im/post/684490…)

  這篇我們就深入的了解一下class文件的各項内容,先看看字節碼的樣子。

  手機java編譯器教學(軟妹手把手教你javap反編譯分解代碼)(1)

  手機java編譯器教學(軟妹手把手教你javap反編譯分解代碼)(2)

  以下對字節碼的分析,就以這個簡單的例子為主。所有的字節碼都是cafe babe開頭,Java一直給咖啡代言,可說是咖啡的忠實粉絲了,就像我愛大幂幂一樣,撒花~

  手機java編譯器教學(軟妹手把手教你javap反編譯分解代碼)(3)

  class文件可真是個小機靈鬼,正是class文件 JVM組合,各種語言編寫的代碼隻要能編譯成JVM可以 正确識别的class文件,就可以運行在JVM上面,才使得JAVA語言乃至所有可以運行在JVM上的語言實現了平台無關性,JVM更是可以向語言無關性發展,class文件的使命就是教JVM怎麼運行,運行什麼。

  class 文件是一組以8位字節為基礎單位的十六進制流,中間沒有任何分隔符,細細品這句話。正是因為class文件是流式的,中間沒有任何分隔符所以class文件裡面的數據項在順序和數量上面是嚴格限定的,每個字節的含義,長度,先後順序,都不允許改變,因為JVM靠的就是上面所說的長度,先後順序等這些信息來翻譯class文件,哪些内容是一組信息哪些符号是另外一組信息,清楚了這一點,我們再來看class文件的設計就會更加的清晰啦~。

  class文件采用類似于C語言結構體的僞結構體來存儲數據,class文件是包含了虛拟機指令,符号表以及其他輔助信息這三大内容,還是這張表,包含了任意class文件的所有内容。

  手機java編譯器教學(軟妹手把手教你javap反編譯分解代碼)(4)

  簡單介紹一下class文件結構的這張表内容:

  兩種數據類型:無符号數和表

  無符号數屬于基本數據類型(Java類中也有基本數據類型),以 u1,u2,u4,u8這種來代表1個字節,2個字節,4個字節,8個字節的無符号數,可以用來描述數字,索引引用,數量值或者字符串值;表就跟Java類中的對象引用類型一樣,對象屬性可以是基本數據類型(對應U1,U2無符号數),也可以是其他的對象(對應其他的表),Java工程項目中參數實體通常以_Param結尾(class文件的表都習慣以“_info”結尾);上圖中的順序,就是Class文件嚴格要求的順序;各個計數器主要是用來描述表裡面數據個數,例如方法計數器的值是methods_count,代表方法表method——info裡面有“methods_count”個方法; JAVA代碼千變萬化,然而所有的内容卻都歸納在了區區一張表裡面?弄不懂這張表,誓不當程序員!!!但願不會啪啪打臉。

  手機java編譯器教學(軟妹手把手教你javap反編譯分解代碼)(5)

  Class魔數和版本 每個Class文件的頭4個字節成為魔數(Magic Number),它唯一的作用是确定這個文件是否為一個能被虛拟機接受的Class文件。值為:0xCAFEBABE(咖啡寶貝)

  手機java編譯器教學(軟妹手把手教你javap反編譯分解代碼)(6)

  緊接魔數的4個字節是Class文件的版本号: 第5-6字節是次版本号(Minor Version), 第7-8字節是主版本号(Major Version)

  手機java編譯器教學(軟妹手把手教你javap反編譯分解代碼)(7)

  J2SE 8 = 52 (0x34 hex) J2SE 7 = 51 (0x33 hex) J2SE 6.0 = 50 (0x32 hex) J2SE 5.0 = 49 (0x31 hex) JDK 1.4 = 48 (0x30 hex) JDK 1.3 = 47 (0x2F hex) JDK 1.2 = 46 (0x2E hex) JDK 1.1 = 45 (0x2D hex) 複制代碼

  這是十六進制分别對應的JDK版本号,十六進制的34換算成十進制是52,對應jdk1.8,由于本人用的是JDK1.8所以此處是34。高版本的JDK能向下兼容低版本的class文件,但不能運行比他高版本的 class文件。

  手機java編譯器教學(軟妹手把手教你javap反編譯分解代碼)(8)

  常量池 常量池代表Class文件中的倉庫資源,緊接着主次版本号之後就是常量池入口,由于常量池中常量的數據 是不固定的,所以在常量池的入口放置了一項u2類型的數據,代表常量池容量計數值,從1開始,字節碼裡面是0x002d(即十進制的45個,代表有44項常量,索引值範圍1~44,第0項空了出來,這樣做目的在于滿足後面某些指向常量池的索引值的數據在特定情況下需要表示 不引用任何一個常量池項目 的目的)。

  常量池主要存放兩大類常量:字面量;符号引用。

  字面量接近Java語言層面的常量概念,如文本字符串、聲名為final的常量值等;符号引用包含三類常量:類和接口的全限定名 org.springframework.....Bean字段的名稱和描述符 private/public/protected方法的名稱和描述符 private/public/protected 手機java編譯器教學(軟妹手把手教你javap反編譯分解代碼)(9)

  u1類型表示的标志tag(1~18)代表當前這個常量屬于哪種常量類型,如10代表了類中方法的符号引用,回到我們的字節碼截圖裡面,他的标志位是0x0a,對應到表中就是10即此類型的常量代表一個類中方法的符号引用。

  看圖哇事,這玩意繁瑣又多,且都是_info結尾,每一項都有自己的結構,主要是字面量,和字段,類,接口方法的符号引用,誰都往裡參合了一腳啊這是。

  手機java編譯器教學(軟妹手把手教你javap反編譯分解代碼)(10)

  标志位為10的 CONSTANT_Methodref_info 的結構

  類型 名稱 數量 u1 tag 1 u2 name_index 1 u2 name_index 1 複制代碼

  name_index就是圖中的index,是一個索引值代表了這個類或者接口的全限定名,字節碼中name_index都占2個u,的值分别是0x0009(十進制值為9),0x001d(十進制值為29),根據表可知分别是指向聲明方法的類描述符以及指向名稱及類型描述符的索引;

  然後字節碼是0x09,查表得知此9代表字段的符号引用Fieldref,結構和CONSTANT_Methodref_info一樣,依次推算可得到所有的44個常量的内容以及索引。

  這裡借助javap看看其他的情況,javap -verbose TestJVM

  Classfile /Users/zengzhiqin/Desktop/daima/leetcode/out/production/leetcode/TestJVM.class Last modified 2020-9-20; size 731 bytes MD5 checksum 73a774d54f51805cb2319a2133c47c04 Compiled from TestJVM.java public class TestJVM minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #9.#29 // java/lang/Object.init:()V #2 = Fieldref #5.#30 // TestJVM.a:I #3 = Fieldref #5.#31 // TestJVM.b:I #4 = Fieldref #32.#33 // java/lang/System.out:Ljava/io/PrintStream; #5 = Class #34 // TestJVM #6 = Methodref #5.#29 // TestJVM.init:()V #7 = Methodref #5.#35 // TestJVM.multi:()I #8 = Methodref #36.#37 // java/io/PrintStream.println:(I)V #9 = Class #38 // java/lang/Object #10 = Utf8 a #11 = Utf8 I #12 = Utf8 b #13 = Utf8 #14 = Utf8 ()V #15 = Utf8 Code #16 = Utf8 LineNumberTable #17 = Utf8 LocalVariableTable #18 = Utf8 this #19 = Utf8 LTestJVM; #20 = Utf8 add #21 = Utf8 ()I #22 = Utf8 multi #23 = Utf8 main #24 = Utf8 ([Ljava/lang/String;)V #25 = Utf8 args #26 = Utf8 [Ljava/lang/String; #27 = Utf8 SourceFile #28 = Utf8 TestJVM.java #29 = NameAndType #13:#14 // init:()V #30 = NameAndType #10:#11 // a:I #31 = NameAndType #12:#11 // b:I #32 = Class #39 // java/lang/System #33 = NameAndType #40:#41 // out:Ljava/io/PrintStream; #34 = Utf8 TestJVM #35 = NameAndType #22:#21 // multi:()I #36 = Class #42 // java/io/PrintStream #37 = NameAndType #43:#44 // println:(I)V #38 = Utf8 java/lang/Object #39 = Utf8 java/lang/System #40 = Utf8 out #41 = Utf8 Ljava/io/PrintStream; #42 = Utf8 java/io/PrintStream #43 = Utf8 println #44 = Utf8 (I)V 複制代碼

  對照一下可知,前面兩個常量和我們算到的結果一緻,我們看到圖中出現了很多I,V,《init》,LineNumberTable等非人類能理解在代碼裡面也從未出現過的東西,這些都會被後面要說到的字段表,方法表,屬性表引用到,用來描述一些不可名狀的東西,不方便用固定字節表示的内容,例如方法的返回值是什麼,有幾個參數,每個參數類型是啥等等,也就是這些不确定的東西需要常量表的符号引用進行表達。

  添加一個方法時,常量池中會增加4個常量;同理,添加字段也是如此,添加的内容有:

  CONSTANT_Methodref_info 方法的符号引用方法符号引用指向的CONSTANT_NameAndType_info 方法的部分符号引用方法的名稱方法的描述符訪問标志 緊接着常量池之後的兩個字節代表訪問标志(access_flags),用于識别一些類或者接口層次的 訪問信息,包括:這個Class是類還是接口、是否為public類型、是否為abstract類型、類是否聲 名為final等。标志位及其含義如下表:

  手機java編譯器教學(軟妹手把手教你javap反編譯分解代碼)(11)

  TestJVM這個類僅僅被public修飾了,因此其他的标志都為假,最終access_flags應為 0x0001|0x0020=0x0021,字節碼中值内容确實是這個。

  手機java編譯器教學(軟妹手把手教你javap反編譯分解代碼)(12)

  類索引、父類索引與接口索引集合 訪問标志之後順序排列類索引(this)、父類索引(super)、接口索引集合(interfaces)。 Class文件由這三項來确定這個類的集成關系。

  手機java編譯器教學(軟妹手把手教你javap反編譯分解代碼)(13)

  類索引和父類索引 引用2個u2類型的索引值表示,他們各自指向一個類型為CONSTANT_Class_info 的類描述符常量,通過CONSTANT_Class_info類型的常量中的索引值找到定義在CONSTANT_Utf8_info類型的常量中的全限定名字符串,從而找到類。

  類索引和父類索引都是u2類型的數據。 手機java編譯器教學(軟妹手把手教你javap反編譯分解代碼)(14)

  JAVAP裡面看到的這兩個索引,分别是此類繼承自Object基類,就無别的繼承關系了。

  手機java編譯器教學(軟妹手把手教你javap反編譯分解代碼)(15)

  接口索引集合入口第一項是u2類型的接口計數器(interfaces_count)表示索引表的容量(即實現了幾個接口)。如果該類沒有實現任何接口,則計數器值為0,後面的接口索引表不再占用任何字節,0x0000因為此類沒有實現任何接口。字段表集合 看到這裡已經很累了吧,我寫的都累了,安利給讀者們一首小阿七的歌《不謂俠》,很好聽啊~

  接口索引集合後邊的是字段計數器:用于标識有多少個字段,接着就是字段表集合。 字段表(field_info)用于描述接口或者類中聲明的變量。

  字段包括類級變量以及實例級變量。可以包括的信息有:

  字段的作用域(public、private、protected修飾符)實例變量還是類變量(static修飾符)可變性(final)并發可見性(volatile)可否被序列化(transient)字段數據類型(基本類型,對象,數組)字段名稱 各個修飾符都是布爾值,要麼有要麼沒有,這個可以使用标志位表示;但字段叫什麼名字、字段被定義成什麼類型,都是無法固定的,所以隻能引用常量池中的常量來描述。由字段的這些内容信息,抽象得到如下的字段表結構:

  手機java編譯器教學(軟妹手把手教你javap反編譯分解代碼)(16)

  name_index和descriptor_index都是對常量池的引用,分别代表着字段的簡單名稱以及字段和方法的描述符,關于全限定名、簡單名稱及描述符的區别:

  全限定名 ai/yunxi/vm/TestClasss類的全限定名, 僅僅是把類中的“.”替換成了“/”

  簡單名稱 沒有類型和參數修飾的方法或者字段名稱 如:add()和int m簡單名稱就是:add、m

  描述符 用來描述字段的數據類型、方法的參數列表(數量、類型及順序)和返回值

  字段訪問标志 手機java編譯器教學(軟妹手把手教你javap反編譯分解代碼)(17)

  字節碼中0x0002代表字段為private,字節碼代表如下:

  u2 0x0002 - 第一個跟着的是fields_count,這個類隻有2個字段表數據 以下是字段表内容: u2 0x0002 private為真,其他為假 u2 0x000a 字段名稱name_index,由上面JAVAP常量表可知#10為a u2 0x000b 字段描述符descriptor_index,由上面JAVAP常量表可知#11指 向常量池字符串I,這個描述符标識字符含義 标識基本類型int u2 0x0000 attribute_count 屬性表集合無屬性,為0表示沒有額外描述的信息 attribute_info 上面無内容,不占字節 複制代碼

  描述符标識字符含義,上面的标識基本類型為I,即對應的下面表的基本類型int

  手機java編譯器教學(軟妹手把手教你javap反編譯分解代碼)(18)

  由上面這些信息可以推導源代碼定義的字段為 private int a;和源碼相符。

  方法表集合 懂了字段表之後,方法表結構幾乎和字段表結構是一模一樣的,通過訪問标志、名稱索引、描述符索引可清楚的表達方法的定義。除了一些标志位不同,畢竟有些修飾符可以修飾方法不能修飾字段,有些修飾符可以修飾字段但是方法沒有,内容如下對比字段表标志有添加有删減有相同:

  手機java編譯器教學(軟妹手把手教你javap反編譯分解代碼)(19)

  重載(Overload)一個方法:

  要與原方法具有相同的簡單名稱要與原方法有不同的特征簽名(特征簽名就是一個方法中各個參數在常量池中字段符号的引用集合,因為返回值不在特征簽名裡面,所以返回值不同作為重載條件) 手機java編譯器教學(軟妹手把手教你javap反編譯分解代碼)(20)

  u2 0x0004 - 第一個跟着的是方法數量,這個類有4個方法表數據,分别是 add(),multi(),main()和構造器方法 以下是第一個方法表内容: u2 0x0001 public為真,其他為假 u2 0x000d 方法簡單名稱name_index,由上面JAVAP常量表可知#14為()v,v由描述 符含義可知是特殊類型void,()代表無參數,即構造函數 u2 0x000e 方法描述符descriptor_index,由上面JAVAP常量表可知#15指 Code,Code之後再講 u2 0x0001 attribute_count,屬性表集合有一項屬性用于存儲一些額外信息 attribute_info 0x000f 由JAVAP看到的指令,指向#15,即對應常量”Code“,說明此屬性是方法的 字節碼描述 複制代碼

  手機java編譯器教學(軟妹手把手教你javap反編譯分解代碼)(21)

  第一個方法:

  手機java編譯器教學(軟妹手把手教你javap反編譯分解代碼)(22)

  屬性表集合 講了大半年,還隻是講了字段,方法頭這些内容可以通過訪問标志,名稱索引,方法描述符來表達清楚,這些都是些元數據,那麼方法體上哪去了呢?這就要屬性表出山啦!

  有眼力見的朋友可能已經講字段表和方法表的時候,就發現了屬性表的蹤影,用來描述某些場景專有信息的,與上面講到的其他的數據項目不同的是,其他數據項目要求嚴格的順序,長度和内容,屬性表的限制是放養狀态,不要求各個屬性表具有嚴格的順序,隻要不與已有的屬性名重複,任何人實現的編譯器都可以向屬性表中寫入自己定義的屬性信息,JVM會運行時會忽略掉他不認識的。

  Java程序方法體中的代碼經過Javac編譯處理後,最終變為字節碼指令存儲在Code屬性中,Code屬性出現在方法表的屬性集合之中。但并非所有方法表都有Code屬性,例如抽象類或接口。

  code屬性表結構如圖:

  手機java編譯器教學(軟妹手把手教你javap反編譯分解代碼)(23)

  attribute_name_index指向CONSTANT_Utf8_info 類型常量的值固定為“Code”attribute_length标識屬性值的總長度max_stack代表了操作數據(Operand Stacks)深度的最大值max_locals代表了局部變量所表示的存儲空間 單位:Slotcode_length和code是用來存儲Java源程序編譯後産生的字節碼指令,codelength代表字節碼長度,code是用于存儲字節碼指令的一系列字節流。字節碼指令,每個指令字節碼代表的指令含義,是否需參數,是u1類型的單字節,取值範圍是0x00~0xFF,即0~255,一共可以表達256條指令,目前JVM規範已經定義了約200條指令了。 屬性有很多的,JAVA虛拟機規範預定義了21項,我們平時能看到的都有

  手機java編譯器教學(軟妹手把手教你javap反編譯分解代碼)(24)

  還是javap -verbose TestJVM 将所有剩下的指令展示出來,可以看到方法的描述和調用

  常量表前面已經貼出了 { public int b; descriptor: I flags: ACC_PUBLIC public TestJVM(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object.init:()V 4: aload_0 5: iconst_3 6: putfield #2 // Field a:I 9: aload_0 10: iconst_4 11: putfield #3 // Field b:I 14: return LineNumberTable: line 5: 0 line 6: 4 line 7: 9 LocalVariableTable: Start Length Slot Name Signature 0 15 0 this LTestJVM; public int add(); descriptor: ()I flags: ACC_PUBLIC Code: stack=2, locals=1, args_size=1 0: aload_0 1: getfield #2 // Field a:I 4: aload_0 5: getfield #3 // Field b:I 8: iadd 9: ireturn LineNumberTable: line 10: 0 LocalVariableTable: Start Length Slot Name Signature 0 10 0 this LTestJVM; public int multi(); descriptor: ()I flags: ACC_PUBLIC Code: stack=2, locals=1, args_size=1 0: aload_0 1: getfield #2 // Field a:I 4: aload_0 5: getfield #3 // Field b:I 8: imul 9: ireturn LineNumberTable: line 14: 0 LocalVariableTable: Start Length Slot Name Signature 0 10 0 this LTestJVM; public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=1, args_size=1 0: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream; 3: new #5 // class TestJVM 6: dup 7: invokespecial #6 // Method init:()V 10: invokevirtual #7 // Method multi:()I 13: invokevirtual #8 // Method java/io/PrintStream.println:(I)V 16: return LineNumberTable: line 18: 0 line 20: 16 LocalVariableTable: Start Length Slot Name Signature 0 17 0 args [Ljava/lang/String; } SourceFile: TestJVM.java 複制代碼

  可以看到一共四個方法,和我們之前看到字節碼推論到的數目一樣,args_size都為1,但是無論是實例構造器,還是add(),multi()方法都沒有參數,這個的原因是:在任何的實例方法我們知道可以通過this.method()來進行調用,通過this來訪問到此方法所屬對象,他的實現就是通過javac編譯器編譯的時候把對this關鍵字的訪問變成對一個普通方法參數的訪問,然後在虛拟機調用實例方法時候自動傳入此參數,因此在實例方法的局部變量表裡面至少會存在一個指向當前對象實例的局部變量,局部變量表也會預留第一個slot位來存放對象實例的引用,其他的方法參數自然靠邊站從1開始計算了。

  字節碼分析,從上面的方法屬性表位置開始:

  手機java編譯器教學(軟妹手把手教你javap反編譯分解代碼)(25)

  attribute_name_index是一項指向CONSTANT_UTF8_INFO的常量索引,常量值固定為Code,代表了該屬性的屬性名稱。

  屬性表之異常表 看一段包含異常語法的簡單代碼:

  /** * @author by zengzhiqin * 2020-09-13 */ public class TestException { public int inc() { int x; try { x = 1; return x; } catch (Exception e) { x = 2; return x; } finally { x= 3; } } } 複制代碼

  再看其内容(字節碼0~4行做的就是将證書1賦值給變量x,并且将x的值複制一份副本到最後一個本地變量表的slot中,這個slot裡面的值在ireturn指令執行前将會被重讀到操作棧頂,作為方法返回值使用,這個slot用returnValue表示):

  0: iconst_0 //常量0壓入操作數棧 1: istore_2 //彈出操作數棧棧頂元素,保存到局部變量表第2個位置 2: iload_0 //第0個變量壓入操作數棧頂 3: iload_1 //第1個變量壓入操作數棧頂 4: iadd //操作數棧中的前兩個int相加,并将結果壓入操作數棧頂 5: istore_2 //彈出操作數棧棧頂元素,保存到局部變量表第2個位置 6: iload_2 //加載局部變量表的第2個變量到操作數棧頂 7: ireturn //返回 8:aload //從局部變量表的相應位置裝載一個對象引用到操作數棧的棧頂 複制代碼

  上面是一些需要用的的指令的相關解釋

  zengzhiqin@cengzhiqindeMacBook-Pro  ~/Desktop/daima/leetcode/src  javap -c TestException Compiled from TestException.java public class TestException { public TestException(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object.init:()V 4: return public int inc(); Code: 0: iconst_1 // try中x=1,1壓入操作數棧 1: istore_1 //将1從操作數棧存儲到局部變量表第一個位置,x=1 2: iload_1 // 加載局部變量表第一個位置元素到操作數棧頂 3: istore_2 // 彈出操作數棧頂元素1,保存到局部變量表第2個位置 4: iconst_3 // finally塊中x=3,将3壓入操作數棧 5: istore_1 // 彈出棧頂元素3,将其保存到局部變量表第1個位置 6: iload_2 // 将變量表第2個位置值1放到棧頂,準備給ireturn返回 7: ireturn // 正常情況下返回 1 正确吻合~ 8: astore_2 // 給catch中定義的 Exception e 賦值,存儲在slot 2中 9: iconst_2 // catch 中 x=2,2壓入操作數棧 10: istore_1 // 彈出棧頂的2,保存到slot 1 11: iload_1 // 局部變量表第1個位置的2壓入棧頂 12: istore_3 // 彈出棧頂元素2保存到局部變量第3個位置 13: iconst_3 // finally中x=3,将3壓入操作數棧 14: istore_1 // 将3放到局部變量表第1個位置,準備給ireturn返回 15: iload_3 // 加載局部變量表第3個位置的值2到棧頂 16: ireturn // 返回棧頂元素2 catch異常返回2 正确,吻合 17: astore 4 // 如果出現了不屬于java.lang.Exception及其子類異常走到這裡 19: iconst_3 // finally塊中x=3,将3壓入操作數棧 20: istore_1 // 将3存儲到局部變量表第1個位置 21: aload 4 // 将異常引用放在棧頂,并且抛出 23: athrow // 抛出異常 Exception table: from to target type 0 4 8 Class java/lang/Exception 0 4 17 any 8 13 17 any 17 19 17 any } 複制代碼

  這裡可初步推測,Java虛拟機執行字節碼是基于棧的體系結構,執行過程可以看我上一篇的講解~懶得貼鍊接了。

  異常的執行過程,finally 代碼塊會在所有正常及異常的路徑上都複制一份,在這段字節碼中,iconst_3 就是對應着 finally 代碼塊,共三份,所以即便在 try 或者 catch 代碼塊中有 return 語句,最終還是會會執行 finally 代碼塊中的内容,這段代碼毫無疑問是返回1,如果在finally裡面加上return X,那麼就是返回3了,這個return什麼值的原因是這樣來滴!!!

  我們可以看到異常表,歸納出異常表結構:

  手機java編譯器教學(軟妹手把手教你javap反編譯分解代碼)(26)

  字節碼0-4行所做的操作數就是将整數1賦值給變量x如果這時沒有出現異常,則會繼續走到第5-7行如果出現了異常,PC寄存器指針轉到第8行如果0-4行出現任何異常,則跳轉17行如果8-13行出現任何異常,則跳轉17行如果17-19行出現任何異常,則跳轉17行 可知,異常表實際上是JAVA代碼的一部分,編譯器使用異常表而不是簡單命令來實現JAVA異常以及finally處理機制的。

  異常是平時最常用的,其他的屬性大家有興趣再去深入了解亦可,寫到這裡本可愛是真的很累,而你百分之八十的幾率是直接跳着看到我這句話的,路過的小哥哥們随手點個贊吧,

  作者:阿甘的馬路鍊接:htt

更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!

查看全部

相关科技资讯推荐

热门科技资讯推荐

网友关注

Copyright 2023-2025 - www.tftnews.com All Rights Reserved