当前位置: 首页 > news >正文

做聚划算网站做app网站的软件有哪些内容

做聚划算网站,做app网站的软件有哪些内容,建设三类人员报考网站,2017年做网站好难Dex文件格式、指令码 一个Class文件对应一个Java源码文件#xff0c;而一个Dex文件可对应多个Java源码文件。开发者开发一个Java模块#xff08;不管是Jar包还是Apk#xff09;时#xff1a; 在PC平台上#xff0c;该模块包含的每一个Java源码文件都会对应生成一个同文件…Dex文件格式、指令码 一个Class文件对应一个Java源码文件而一个Dex文件可对应多个Java源码文件。开发者开发一个Java模块不管是Jar包还是Apk时 在PC平台上该模块包含的每一个Java源码文件都会对应生成一个同文件名不包含后缀的.class文件。这些文件最终打包到一个压缩包即Jar包中。 而在Android平台上这些Java源码文件的内容最终会编译、合并到一个名为classes.dex的文件中。不过从编译过程来看Java源文件其实会先编译成多个.class文件然后再由相关工具将它们合并到Jar包或Apk包中的classes.dex文件中。 Dex文件的这种做法有什么好处呢笔者至少能想出如下两个优点 虽然Class文件通过索引方式能减少字符串等信息的冗余度但是多个Class文件之间可能还是有重复字符串等信息。而classes.dex由于包含了多个Class文件的内容所以可以进一步去除其中的重复信息。 如果一个Class文件依赖另外一个Class文件则虚拟机在处理的时候需要读取另外一个Class文件的内容这可能会导致 CPU 和存储设备进行更多的 I/O 操作。而classes.dex由于一个文件就包含了所有的信息相对而言会减少 I/O 操作的次数。 字节序 Java平台上字节序采用的是 Big Endian。所以Class文件的内容也采用Big Endian字 节序来组织其内容。而 Android 平台上的Dex文件默认的字节序是Little Endian这可能是因为ARM CPU也包括X86 CPU采用的也是Little endian字节序的原因吧。 Dex 文件格式的概貌 图3-3所示为Dex文件格式的概貌。其各个成员解释如下。 首先是Dex文件头很重要类型为header_item。string_ids数组元素类型为string_id_item它存储和字符串相关的信息。type_ids数组元素类型为type_id_item。存储类型相关的信息由TypeDescriptor 描述。field_ids数组元素类型为field_id_item存储成员变量 信息包括变量名、类型等。method_ids数组元素类型为method_id_item存储成员函数信息包括函数名、参数和返回值类型等。class_defs数组元素类型为class_def_item存储类的信息。dataDex文件重要的数据内容都存在data区域里。一些数据结构会通过如xx_off这样的成员变量指向文件的某个位置从该位置开始存储了对应数据结构的内容而xx_off的位置一般落在data区域里。link_data理论上是预留区域没有特别的作用。 和Class文件格式比起来Dex文件格式的特点如下: 有一个文件头这个文件头对正确解析整个Dex文件至关重要。有几个xxx_ids数组包括 string_ids字符串相关、type_ids数据类型相关、proto_ids主要功能就是用于描述成员函数的参数、返回值类型同时包含 ShortyDescriptor信息、field_ids成员域相关和method_ids成员函数相关。data区域存储了绝大部分的内容而data区域的解析又依赖于header和相关的数据项。 Dex 指令码介绍 Dex 指令码的条数和 Class 指令码差不多都不超过 255 条但是 Dex 文件中存储函数内容的insns数组位于code_item结构体里却比Class文件中存储函数内容的code 数组位于 Code 属性中解析起来要有难度。其中一个原因是 Android 虚拟机在执行指令码的时候不需要操作数栈所有参数要么和 Class 指令码一样直接跟在指令码后面要么就存储在寄存器中。对于参数位于寄存器中的指令指令码就需要携带一些信息来表示该指令执行时需要操作哪些寄存器。此外虽然官方文档详细介绍了所有Dex指令码的格式和含义但是它采用了一种特别的语法来描述它们所以初学者读官方文档时会感觉比较难懂。 insns_size 和 insns 数组指令码数组的长度和指令码的内容。Dex 文件格式中 JVM 指令码长度为 2 个字节而 Class 文件中 JVM 指令码长度为 1 个字节。 由图3-9可知 Dex指令码的长度还是1个字节所以指令码的个数不会超过255条。但是和Class指令码不同的是Dex指令码与第一个参数混在一起构成了一个双字节元素存储在insns内。在这个双字节中低8位才是指令码高8位是参数。笔者称这种双字节元素为 参数操作码组合。 参数操作码组合 后的下一个ushort双字节元素可以是新一组的 参数操作码组合也可以是 纯参数组合。 参数组合的格式也有要求不同的字符代表不同的参数参数的比特位长度又是由字符的个数决定。比如AA表示一个参数这个参数占8位而其中每一个A都代表4位比特长。 提示 关于图3-9中的参数格式根据官方文档下面几点内容需要读者了解。 1不同的字符代表不同的参数比如A、B、C代表三个不同的参数。 2参数的长度由对应字符的个数决定1个字符占据4个比特。比如A表示一个占4比特的参数AA代表一个占8比特的参数AAAA代表一个16比特长的参数。 3代表一个特殊的参数该参数取值为0。比如ØØ表示这样一个参数这个参数长度为8位每位的取值都是0。 在图 6-57 中 右边是 C/C 源码编译成目标机器码后该目标机器码编译运行时只和目标的操作系统和相关库有关。 而 Dex/Java 字节码虽然也编译成目标机器码但是它的编译和运行不仅仅依赖操作系统还依赖具体的虚拟机实现。比如Dex字节码编译成机器码的话就依赖ART虚拟机。 这个道理很容易理解但是也很容易被忽视。很多人以为 Java 字节码编译成机器码后就能和那些 C/C 编译得到的机器码一样无所羁绊地直接在OS上运行了殊不知在Java字节码编译为机器码的过程中虚拟机会添加一些必要和特殊的指令使得得到的机器码在运行过程中实际上离不开虚拟机的管控。这里不妨举一个例子加以说明。 Java虚拟机的垃圾回收器做对象标记前往往会设置一个标志表示自己要做对象标记了。其他线程运行时要经常检查这个标志发现这个标志为true时这些线程就得等待好让垃圾回收器能安全地做对象标记。否则的话垃圾回收器一边做对象标记其他线程同时又去创建对象或更改对象间的引用关系这将导致对象标记不准确影响垃圾回收。 显然程序员在代码中是不会主动加上这个检查标记的动作。实际上这是由编译器来主动完成的。它会在两个地方添加标记检查指令一个是在 Entry 基本块里另一个是在 loopheader 基本块里。 Java 字节码编译得到的机器码是离不开虚拟机的它的编译也依赖与具体的虚拟机实现。 Android 在 Dalvik 时代采用的是 Java 虚拟机技术中较为成熟的 Just-In-Time即时编译 简写为 JIT编译方案JIT 会将热点 Java 函数的字节码转换成机器码这样可提升虚拟机的运行速度。而 Android 虚拟机换为 ART 后Google 最初却非常激进地抛弃了 JIT转而采用了 Ahead-Of-Time预编译简写为 AOT编译方案。AOT 导致系统在安装应用程序之时就会尝试将 APK 中大部分 Java 函数的字节码转换为机器码其尽一切可能提升虚拟机运行速度的努力用心良苦。但 AOT 却带来了应用程序安装时间过长编译生成的 oat 文件过大等一系列较为影响用户体验的副作用。为此Android 在 7.0Nougat中对 ART 虚拟机进行了改造综合使用了 JIT、AOT 编译方案解决了纯 AOT 的弊端同时还达到了预期目标。 ELF 文件 和.class及.dex文件对应.oat文件是 Android ART 虚拟机上的“可执行文件”。虽然 Android 官方没有明确解释oat表示什么意思但通过相关源码和一些工具我们发现它其实是一种经 Android 定制的 ELF 文件。ELF 文件是 oat 文件的基础其难度较大本章先来学习ELF。 概述 ELF 是 Executable and Linkable Format 的缩写它是 Unix包括Linux这样的类Unix 平台上最通用的二进制文件格式。那些使用 Native 语言比如 C/C 开发的程序员几乎每天都会和 ELF 文件打交道比如 C/C 文件编译后得到的.o或.obj文件就是 ELF 文件。动态库.so文件是 ELF 文件。.o文件和.so文件链接后得到的二进制可执行文件也是 ELF 文件。 提示 .oat是一种定制化的 ELF 文件所以 EFL 文件是 oat 文件的基础但是 oat 文件包含的内容和 art 虚拟机密切相关。 传统Java虚拟机的可执行文件是.class文件Dalvik虚拟机的可执行文件是.dex文件而ART虚拟机的可执行文件是.oat文件。 ELF文件格式介绍 如前述内容可知ELF 是 Executable and Linkable Format 的缩写。其名称中的 “Executable”和“Linkable”表明ELF文件有两种重要的特性。 Executable可执行。ELF文件将参与程序的执行Execution工作。包括二进制程序的运行以及动态库.so文件的加载。Linkable可链接。ELF文件是编译链接工作的重要参与者。下面来看ELF文件格式的内容如图4-1所示。 图4-1表明我们从不同角度View来观察ELF的话将会看到不同的信息。 Linking View链接视图它是从编译链接的角度来观察一个ELF文件应该包含什么内容。Execution View执行视图它是从执行的角度可执行文件或动态库文件来观察一个ELF文件应该包含什么信息。 介绍几个关键的.text和.bss等section。 .text section用于存储程序的指令。简单点说程序的机器指令就放在这个section中。根据规范.text section的 sh_type 为 SHT_PROGBITS取值为1意为 Program Bits即完全由应用程序自己决定程序的机器指令当然是由程序自己决定的sh_flags 为 SHF_ALLOC当ELF文件加载到内存时表示该Section会分配内存和 SHF_EXECINSTR表示该Section包含可执行的机器指令。.bss sectionbss 是 block storage segment 的缩写。ELF规范中.bss section包含了一块内存区域这块区域在ELF文件被加载到进程空间时会由系统创建并设置这块内存的内容为 0。注意.bss section 在 ELF 文件里不占据任何文件的空间所以其 sh_type 为 SHF_NOBITS取值为8它只是在ELF加载到内存的时候会分配一块由 sh_size 指定大小的内存。.bss 的sh_flags 取值必须为 SHF_ALLOC 和 SHF_WRITE表示该区域的内存是可写的。同时 因为该区域要初始化为0所以要求该区域内存可写。什么样的数据应该属于 .bss section 呢如果读者在 main.c 中定义一个全局的int a 0之后生成的 main.o 就包含有效的 .bss section了。.data section.data 和 .bss 类似但是它包含的数据不会初始化为0。这种情况下就需要在文件中包含对应的信息了。所以 .data 的 sh_type 为 SHF_PROGBITS但 sh_flags 和 .bss 一样。读者可以尝试在 main.c 中定义一个比如char c f这样的变量就能看到 .data section 的变化了。.rodata section包含只读数据的信息比如 main.c 中 printf 里的字符串就属于这一类。 它的 sh_flags 只能为 SHF_ALLOC。 虚拟机的创建和启动 在 Android 系统中Java 虚拟机是借由大名鼎鼎的 Zygote 进程来创建的。Zygote 是 Java 世界的创造者即 Android 中所有 Java 进程都由 Zygote 进程 fork 而来而 Zygote 进程自己又是 Linux 系统上的 init 进程通过解析配置脚本来启动的。假设目标设备为32位CPU架构zygote 进程对应的配置脚本文件是system/core/rootdir/init.zygote32.rc该文件描述了init该如何启动zygote进程如图7-1所示。 接着来看 AndroidRuntime 的 start 函数。 上述代码中和启动ART虚拟机密切相关的两个重要函数如下所示。 Jnilnvocation的Init函数它将加载ART虚拟机的核心动态库。AndroidRuntime的startVm函数在ART虚拟机对应的核心动态库加载到zyogte进程后该函数将启动ART虚拟机。 JniInvocation Init 函数介绍 先来看JniInvocation的Init函数代码如下所示。 由上述代码可知我们将从 libart.so 里将取出并保存三个函数的函数指针 这三个函数的代码位于 java_vm_ext.cc 中。第二个函数JNI_CreateJavaVM用于创建Java虚拟机所以它是最关键的。 AndroidRuntime startVm 函数介绍 接着来看AndroidRuntime的startVm函数代码如下所示。 如上述代码中的注释所言 JNI_CreateJavaVM 函数并非是Jnilnovcation Init从 libart.so获取的那个JNI_CreateJavaVM函数。相反它是直接在AndroidRuntime.cpp中定义的其代码如下所示。 辗转多次终于和 libart.so关联上了。马上来看libart.so中的这个JNI_CreateJavaVM函数代码如下所示。 在上述libart.so的JNI_CreateJavaVM代码中我们见到了ART虚拟机的化身Runtime即 ART虚拟机在代码中是由Runtime类来表示的。其中 Runtime::Create 将创建一个Runtime对象。Runtime::Start 函数将启动这个Runtime对象也就是启动Java虚拟机。 先来看 Runtime 对象的创建。 VM 和 Runtime: 虚拟机一词的英文为Virtual Machine简写为VM。Runtime则是另外一个在虚拟机 技术领域常用于表示虚拟机的单词。Runtime也被翻译为运行时在本书中笔者使用虚拟机来表示它。在ART虚拟机Native层代码中Runtime是一个类。而JDK源码里也有一个Runtime 类位于java.lang包下。这个Java Runtime类提供了一些针对整个虚拟机层面而言的API比如exit退出虚拟机、gc触发垃圾回收、load加载动态库等。 内存映射 MemMap是一个辅助工具类它封装了和内存映射memory map有关的操作。 MemMap使用mmap、msync、mprotect等系统调用来完成具体的内存映射、设置内存读写权限等相关操作。它可创建基于文件的内存映射以及匿名内存映射。mmap 系统调用的返回值只是一个代表地址的指针而MemMap则提供了更多的成员变量来辅助我们更好地使用mmap得到的这块映射内存。比如每一个MemMap对象都有一个名称。另外对于非x86_64的64位平台如果要想映射内存到进程的低2G空间地址的话即想在非x86_64的64位平台上使用mmap的MAP_32BIT标志MemMap需要做一些特殊处理。 与线程同步相关的辅助类 ART 提供了 Mutex、ReadWriteMutex、ConditionVariable 等辅助类来实现互斥锁、条件变量等常用的同步操作。它们定义于mutex.h中不同平台的实现略有不同。另外ART 还借助一种称之为 Lock Hierarchies 的方法来解决线程同步时经常出现的因为使用锁的顺序不一样导致死锁的问题即线程应该按相同的顺序抢占互斥锁比如先锁住互斥锁A接着再锁住互斥锁B否则极易出现死锁的情况。在 Lock Hierachies 体系下互斥锁可以设置一个优先级如果某个资源需要多个锁来保护的话只有先拿到高优先级的锁之后才能去抢占低优先级的锁。如果顺序反了运行时可以采取报错或程序退出的方式来处理。注意LockHierachies 只是提供了解决死锁问题的思路读者可结合 ART 代码以及下文的资料 http://www.drdobbs.com/parallel/use-lock-hierarchies-to-avoid-deadlock/204801163来加深对它的认识。 OAT文件 OatFileManager 介绍 OatFileManager用于管理虚拟机加载的oat文件。dex字节码编译成机器码后相关内容会存储在一个以.oat为后缀名的文件里。我们先来简单认识 OAT 文件的格式。 OAT 文件格式简介 图7-3所示为OAT文件的部分内容。 图7-3展示了OAT文件的部分内容及格式。 一个OAT文件包含一个OatHeader头结构。注意这个OatHeader信息并不存储在OAT文件的头部。OAT文件其实是一个ELF格式的文件相关信息存储在ELF对应的段中。Oat文件是怎么来的呢 它是对jar包或apk包中的dex项名为classes.dex、 classes2.dex、classes3.dex等其实就是dex文件打包到jar或apk里了。以后我们统称它们为dex文件而不必理会它们是单独的.dex文件还是jar或apk包中的一项进行编译处理后得到的该过程借助dex2oat来完成。jar或apk中可包含多个dex项即所谓的multidex每一个jar或apk中的所有dex文件在oat文件中对应都有一个OatDexFile项OatDexFile项存储了一些信息比如它所对应的dex文件的路径、dex文件的校验以及其他各种信息在oat文件中的位置offset等。OatDexFile区域之后的是DexFile区域。在生成oat文件时jar或apk 中 classes.dex 的如果有多个话则包含classes2.dex、classes3.dex内容会完整地拷贝到Oat文件中对应的DexFile区域。简单点说OAT文件里的一个DexFile项包含一个.dex文件的全部内容。通过在OAT文件中包含dex文件的内容ART虚拟机只需要加载OAT文件即可获取相关信息而不需要单独再打开dex文件了。当然这种做法也使得 OAT 文件尺寸较大。现在回过头来看OatDexFile。每一个OatDexFile 对应一个DexFile项。OatDexFile中有一个dex_file_offset 成员用于指明与之对应的DexFile 在 OAT 文件里的偏移量。当然OatDexFile还有其他类似的成员以offset_做后缀用于指明其他信息在OAT文件里的偏移量。 最后笔者简单说一下关于boot oat文件里所包含的系统基础类。在 frameworks/base下有一个preloaded-classes文件其内容是希望加载到 zygote 进程里的类名按照JNI格式定义这些类包含在不同的 boot oat 文件里。图7-5展示了其中的部分内容。 图7-5中 “…” 号是由笔者添加的省略号。由于zygote是Java世界的第一个进程其他APP进程包括 system server 进程均由zygote进程fork而来。所以 这些加载到 zygote 进程里的类也叫预加载类即所谓的 preloaded classes。根据 linux 进程 fork 的机制其他 APP 进程从 zygote fork 后将继承得到这些预加载的类。 信号处理和 SignalAction 介绍 信号处理 Linux系统中一个进程可以接收来自操作系统或其他进程发送的信号Signal。简单点说信号就是事件event代表某个事件发生了而接收进程可以对这些事件进行有针对性的处理。 Linux 系统支持POSIX中的标准和实时两大类信号。ART只处理标准信号。信号由信号 ID一个正整数来唯一标示。每一个信号都对应有一个信号处理方法。信号处理方法是指当某个进程接收到一个信号时该如何处理它。进程可以为某些信号设置特定的处理方法。如果不设置的话操作系统将使用预先规定好的办法来处理这些信号也就是所谓的默认处理。一个进程可以阻塞某些信号block signal。阻塞的意思是指这些信号只要发生的话还是会由操作系统投递到目标进程的信号队列中只不过OS不会通知进程进行处理而已。这些被阻塞的信号pending signal将存储在目标进程的信号队列中一旦进程解除它们的阻塞OS就会通知进程进行处理。 如果想要为某个信号设置信号处理结构体的话需要使用系统调用sigaction其定义如下。 SignalAction 类介绍 现在来看看ART基于上述内容所提供的封装类 SignalAction。由上述代码可知我们可以为一个SignalAction对象 直接设置一个特殊的信号处理函数。该步骤借助 SetSpecialHandler 函数来完成。设置一个信号处理结构体。该步骤借助SetAction来完成。 使用第一种方法的话必须调用 SetSpecialSignalHandlerFn函数。 FaultManager介绍 FaultManager的初始化 FaultManager的初始化步骤中涉及FaultManager的构造函数以及Init函数。先来看 FaultManager 的构造函数代码如下所示。 接着来看Init函数代码如下所示。 线程 Attach 函数介绍 接着来看Attach的代码如下所示。 Attach 内部又包含三个关键函数先来看第一个即Thread的构造函数。 Thread 构造函数 Thread的构造函数并不复杂主要是完成对某些成员变量的初始化来看代码。 Init函数介绍 Thread Init的代码如下所示。 InitStackHwm 本函数用于设置线程的线程栈。我们先回顾下一个线程的栈空间是怎么设置的。在Android平台上我们可通过调用pthread_create来创建一个线程来看看pthread_create的函 数重点考察其中对线程栈的处理代码如下所示。 这里再次特别说明栈只有一个出入口即栈顶而栈底是不动的。线程的栈空间由 allocate_thread 分配。 通过上面pthread_create的代码我们可知 Android 平台上线程栈的创建过程如下。 mmap得到一块内存其返回值为该内存的低地址stack_base。设置该内存从低地址开始的某段区域由guard_size为不可访问。得到该内存段的高地址将其作为线程栈的栈底位置传递给 clone 系统调用。 总结上述内容可知 在ART虚拟机中每一个Thread 对象代表一个线程。每个线程在InitCpu中都会将代表自己的Thread对象的地址设置GDT中并且将关联的GDT表项的索引保存到FS寄存器里。这么做的目的是当这个线程执行 generatedcode的时候如果需要调用quick entrypoints 等虚拟机提供的函数时均可借助图7-8所示的流程进行跳转。而要达到这个目的所必需的前提条件是FS的内容会随着线程切换而做相应的切换。因为FS只有一个而线程A的Thread对象和线程B的Thread对象不会是同一个所以FS的内容应该随着线程的切换而相应进行调整。好在这部分工作由操作系统来完成。 Thread FinishSetup 接着来看Thread FinishSetup函数代码如下所示 Thread CreatePeer 研究代码之前笔者先简单介绍下和 Java Thread 有关的一些背景知识。 我们知道在 Java 世界里线程的概念包装在 Thread 类里。创建一个 Thread 实例并start它则会启动一个操作系统概念中的线程。 通过上面的描述可知一个 Java Thread实例是需要和操作系统中的某个线程关联到一起的。光有一个JavaThread实例而没有操作系统里对应的线程来支持它那这个Thread 对象充其量也就是一块内存罢了。 在Java Thread类中有一个名为 nativePeer的成员变量这个变量就是该Thread实例所关联的操作系统的线程。当然出于管理需要 nativePeer并不会直接对应到操作系统里线程ID这样的信息而是根据不同虚拟机的实现被设置成不同的信息。 下面的CreatePeer的功能包括两个部分 创建一个 Java Thread 实例。 把调用线程操作系统意义的线程即此处的 art Thread 对象关联到上述 Java Thread实例的 nativePeer 成员。 简单点说ART 虚拟机执行到这个地方的时候代表主线程的操作系统线程已经创建好了但 Java 层里的主线程 Thread 示例还未准备好。而这个准备工作就由CreatePeer 来完成。 上述代码执行后我们总结相关信息于图8-3。 在图8-3中 Java Thread 的nativePeer成员括号中的为该成员的数据类型指向一个 ART Thread 对象。ART Thread 对象中的 tlsPtr.opeer 和 tlsPtr.jpeer 都指向同一个 Java Thread 实例。opeer 的类型为mirror Objectjpeer的类型为jobject。以后我们将看到这两个成员变量 只是使用场景不同。 ThreadList 和 ThreadState Java 虚拟机中往往运行了多个 Java 线程。为了方便管理ART 设计了一个 ThreadList 类来统一管理这些 Java 线程。这里请读者注意 每一个Java线程都对应为ART虚拟机中的一个Thread对象。Native 线程可通过JavaVM::AttachCurrentThread 接口将自己变成一个 Java 线程。而这 就会创建对应的一个 Thread 对象。 Heap HeapBitmap 相关类 当我们使用new或malloc等内存分配方法创建一个对象时得到的是该对象所在内存的地址即指针。指针本身的长度根据CPU架构的不同导致是32位长或者是64位长。如果创建1万个对象的话那么这一万个对象的指针本身所占据的内存空间就很可观了。如何减少指针本身所占据的内存空间呢ART采用的办法很简单就是将对象的指针转换成一个位图里的索引位图里的每一位指向一个唯一的指针。来看图7-12的示例。 在图7-12中 中间框是一个有n个比特位的位图如果按字节计算则该位图长度为 n8 字节长。这个位图本身是一块内存由基地址pbitmap表示。其上下还有两个方框代表两块连续的内存起始地址分别是pbase1和pbase2。先来看pbase1对应的内存块。这块内存中存储的是指针p0指向对象0object0p1指向对象1object1。p0和p1本身占据的内存长度为 sizeof(指针)字节。显然如果有很多个对象的话内存块1会占用不小的空间。优化的办法很简单就是将 p0、p1 的值借助位图索引来计算。比如第x个对象的地址就是 pbase1 x * sizeof(指针)。除了可以保存对象的指针外还可以用位图存储更大块的空间。比如pbase2对应的内存块其内部又可细分为以4KB为单位的空间。那么第y个4K内存空间的起始位置就是 pbase2 y * 4KB。不管是pbase1还是pbase2所对应的内存如果我们想知道第x个对象是否存在的话该怎么处理呢答案很简单设置中间那个位图框中第x个索引位的值即可。如果第x个索引位的值为1则表明第x个对象存在比如 pbase1 x * sizeof(指针)处的内存被占用了否则表示该对象不存在即 pbase1 x * sizeof(指针)处的内存空间空闲。 art文件 .art 文件格式介绍 一个包含 classes.dex 项的 jar 或 apk 文件经由 dex2oat 进行编译处理后实际上会生成两个结果文件一个是.oat文件另外一个是.art文件。图7-15简单展示了这个过程。 图7-15中当用 dex2oat 对一个 jar 包或 apk 进行编译处理后其输出文件包含两个文件。 一个是 .oat 文件。 值得再次指出的是jar 或 apk 中的 classes.dex 内容将被完整拷贝到 oat 文件里。另外一个文件是.art文件。它就是 ART 虚拟机代码里常提到的 Image 文件。art 文件的格式在官方文档中没有介绍相关资料也很少。所以学习art文件格式相对会困难一些。art文件和oat 文件密切相关。 根据art文件的来源比如它是从哪个jar包或 apk包编译得来的Image分为boot镜像boot image和 app镜像app image。 来源于某个 apk 的 art 文件称为 App 镜像。来自 Android 系统里 /system/framework 下那些核心 jar 包的 art 文件统称为 boot 镜像。 这些核心 jar 包包含了 Android 系统最基础和很重要的类。注意系统核心 jar 包有多个比如core-oj.jaroj 是 open jdk 的简称。jdk所包含的类几乎都在其中、framework.jar、org.apache.http.legacy.jar、okhttp.jar等。由于这些核心类在 ART 虚拟机启动时就必须加载所以称它们为 boot 镜像文件。 为什么叫 Image 笔者在很长一段时间内都非常困惑为什么 art 文件会被称为 Image。随着研究的深入笔者对这个问题有了一个较为粗浅的认识。首先art 文件加载到虚拟机里都是通过 mmap 的方式来完成的加载到内存里的位置在 art 文件的 ImageHeader 结构体中有描述。其次art 文件的内容布局是有严格组织的这些内容将加载到内存里的不同的位置。最后这些信息从文件中映射到内存后可以直接转换成对应的对象。就好像我们事先将对象的信息存储到文件中后续只不过再将其从文件中还原出来一样。 另外一般而言针对核心库的编译都会生成 boot.art 镜像文件而针对 app 的编译则通过 dex2oat 相关选项来控制是否生成对应的 art 文件。 就本章而言art 文件结构中的 ImageHeader 最为关键图7-16展示了它的部分信息。 图7-16展示了art文件格式的部分内容它分为左中右三个部分。 左边是art文件的组成结构图中只绘制了位于文件头部的关键数据结构 ImageHeader。右边是ImageHeader结构体的各个成员变量。magic_数组存储的是art文件格式的魔幻数取,值为[a, r, t, \n]version_数组为art文件格式的版本号取值为[0, 2, 9, \0]。imagebegin表示该art文件期望自己被映射到内存的什么位置image_size_则表示映射多大空间到内存。ImageHeader中sections_是一个非常重要的成员它是一个数组数组大小固定为kSectionCount取值为9数组成员的数据类型为ImageSection。art 文件中包含9个section每个section存储了不同的信息。ImageSection就是用来描述 一个section在内存里什么位置基于image_begin_的偏移量以及该section有多大。 storage_mode_表示文件内容除ImageHeader外是否为压缩存储。中间是art文件加载到内存里的情况。image_begin_是这块内存的起始位置。特别注意ImageHeader的内容被包括在sections_[kSectionObjects]中取值为0即该section从 image_begin_开始。另外image_size_只覆盖到 sections_[kSectionImageBitmap-1] 而 sections_的最后一个元素 sections_[kSectionImageBitmap]则从image_size_之后某 个按页大小对齐的位置处开始。结合上文对HeapBitmap的介绍读者可知道sections_[kSectionImageBitmap]应该是一个位图空间。 JNI JavaVMExt 和 JNIEnvExt 本节讨论JNI中最常见的两个类JavaVM和JNIEnv。根据笔者在《深入理解Android卷1》一书中对JNI知识的介绍可知 JavaVM 在 JNI 层中表示 Java 虚拟机。它的作用有点像 Runtime。只不过 JNI 作为一种规范它必须设定一个统一的结构即此处的JavaVM。不同的虚拟机实现里真实的虚拟机对象可以完全不一样比如 art 虚拟机中的 Runtime 才是当之无愧的虚拟机。另外一个 Java 进程只有一个 JavaVM 实例在 ART 虚拟机中JavaVM 实际代表的是 JavaVMExt 类。JNIEnv 代表 JNI 环境每一个需要和 Java 交互不管是Java层进入Native层还是 Native层进入Java层的线程都有一个独立的 JNIEnv 对象。 同理JNIEnv 是 JNI 规范里指定的数据结构不同虚拟机有不同的实现。在 ART 虚拟机中JNIEnv 实际代表的是 JNIEnvExt 类。 JavaVM 是跟着进程走的JNIEnv 是跟着线程走的。 JavaVMExt 现在来看 JavaVMExt 对象的创建先回顾它在 Runtime Init 中的代码。 在图7-19中 JavaVM 是一个结构体当然在C中结构体也是一种类的类型。当定义了CPLUSPLUS宏时按C来编译JavaVM 还有一个类型别名即 JavaVM。所以 JavaVM的真实数据类型是_JavaVM。JNIInvokeInterface也是结构体。其中JNIInvokeInterface的 AttachCurrentThread、 GetEnv等成员变量的数据类型都是函数指针为方便书写图7-19中没有展示它们的参数。JavaVM结构体的第一个成员变量指向一个JNIInvokeInterface对象。JavaVMExt是一个类它从 JavaVM中派生。 提示 JNI 或 runtime 模块里往往通过一个JavaVM *类型的指针来引用一个JavaVM对象。通过上面的介绍可知ART中JavaVM对象的真正数据类型是JavaVMExt。 JNIEnvExt JNIEnvExt 的思路和 JavaVMExt 类似我们直接来看代码。 JNINativeInterface 和上节中提到的 JNIInvokeInterface 有些类似都是包含了很多函数指针的结构体。 现在来看JNIEnvExt它是JNIEnv的派生类。其创建是通过Create 函数来完成的。 我们重点了解下gJniNativeInterface的内容。 总结 了解上述JavaVMExt和JNIEnvExt代码后外界如果通过它们的基类JavaVM和JNIEnv 来操作JNI相关接口时我们就可以很方便地找到真实的函数实现在哪了。笔者总结如下 操作JavaVM相关接口时其实现在java_vm_ext.cc文件的JIT类中。如果需要检查JNI的话则先通过check_jni.cc的CheckJIT类对应函数处理。最终还是会调用 JIT 类的相关函数。操作 JNIEnv 相关接口时其实现在jni_internal.cc的JNI类中。同理如果需要检查JNI的话也通过check_jni.cc的CheckJNI类对应函数先处理。 JNI里相关的数据结构和API都定义在头文件jni.h中来看其中的内容。 总结上述的代码可知 Java中基础数据类型在JNI层中都对应为native层中的某种基础数据类型。Java中引用类型在JNI层中对应为_jobject 注意带下划线及派生类。但是JNI的使用者只能通过_jobject和它的派生类的指针类型即 JNI 使用者只能使用jobject、jclass、jstring 等不带下划线的数据类型来间接引用这些对象的实例。不过鉴于上述代码明确地将_jobject定义为一个没有任何成员变量和成员函数的类。可想而知jobject、jclass 等的作用和 void*差不多。Java类中的成员变量或成员函数在JNI中也有对应的类型。对比_jobject读者可发现_jfieldID 和 _jmethodID甚至都没有实际的定义。不过由于JNI使用者只能通过指针类型jfieldID和jmethodID都是指针来操作所以编译不会报错。不过这也说明jfieldID、jmethodID 的作用和 void* 一样 那么这些“void*”背后到底是谁 Java虚拟机规范笔者参考的是《Java VirtualMachine Specification》第 7 版把这个问题的答案留给了各种虚拟机的实现。那么ART 虚拟机是如何处理的呢 先来看另外一组 ART 里常用的辅助类。 ScopedObjectAccess 等辅助类 图8-1所示为ScopedObjectAccess辅助类家族。 在图8-1所示类家族中 ValueObject是一个没有任何成员也不允许编译器自动创建构造函数的类。ScopedObjectAccessAlreadyRunnable是关键类。它包含三个重要成员变量Self_指向当前调用线程的线程对象类型为Threadenv_指向当前线程的JNIEnvExt对象而vm_则指向代表虚拟机的JavaVMExt对象。 我们只要看一下 ScopedObjectAccessAlreadyRunnable的代码上节遗留下的问题将迎刃而解。 上述代码非常清晰得展示了jfieldID、jmethodID以及jobject在ART虚拟机实现里所对应的具体数据类型。 jfieldID其实就是 ArtField *。jmethodID 其实就是 ArtMethod *。jobject指向一个 mirror Object 对象但其具体是什么需要再由mirror Object*向下转换为指定的类型。 最后我们来看一个使用AddLocalReference的代码段代码如下所示。 常用JNI函数介绍 FindClass FindClass是JNIEnv中的API用于查找指定类名的类信息。由7.7.2节的介绍可知该函数的真正实现不考虑checkJni的情况位于jni_internal.cc中代码如下所示。 RegisterNativeMethods RegisterNativeMethods 用于将native层的函数与Java层中标记为native的函数关联起来 该函数是每一个JNI库Linux平台上以so文件的方式提供使用前必须调用的。 上面代码内容比较简单不过有一个小地方需要解释即 fast jni模式。 从函数调用Java层进入JNI层时虚拟机会将执行线程的状态从Runnable转换为Native。如果JNI层里又调用Java层相关函数时执行线程的状态又得从Native转为Runnable。线程状态的切换会浪费一点执行时间。所以对于某些特别强调执行速度的JNI函数可以设置为fast jni模式。这种模式下执行这个native函数时将不会进行状态切换即执行线程的状态始终为Runnable。当然这种模式的使用对GC有一些影响所以最好在那些本身执行时间短又不会阻塞的情况下使用。另外这种模式目前在ART虚拟机内部很多 java native 函数有使用。为了和其他native函数进行区分使用fast jni模式的函数的签名信息字符串必须以 “!感叹号开头。 LocalRef、GlobalRef 和 WeakGlobalRef 相关函数 JNI层的代码虽然是用native语言C或C开发的但Java中和GC相关的一些特性在JNI中依然有所体现。 JNI层中创建的jobject对象默认是局部引用Local Reference当函数从JNI层返回后Local reference的对象很可能被回收。 所以不能在JNI层中永久保存一个LocalReference的对象。有时候JNI层确实需要长期保存一个jobject对象。但如上条规则所言JNI函数返回后相关的jobject对象都可能被回收。该如何保存一个需要长期使用的 jobject 对象呢答案很简单就是将这个Local Reference 对象转换成 Global Reference全局引用 对象。 而全局引用对象不会被GC回收而是需要使用者主动释放它。当然为了减少内存占用进程能持有的全局引用对象的总个数有所限制。如果觉得 Global Reference 对象用起来不方便比如需要主动释放它们则可将局部引用对象变成所谓的弱全局引用对象。弱全局引用对象有可能被回收所以使用前需要调用JNIEnv提供的IsSameObject函数将一个弱引用对象与nullptr进行比较。 下面是JNI提供的操作这三种引用类型的三组API。 我们重点研究NewGlobalRef的代码如下所示。 结合JavaVMExt的AddGlobalRef 代码可知 一个Java进程中只有一个JavaVMExt对象代表虚拟机本身。一个JavaVMExt对象中有一个globals_成员变量这个变量是一个容器可存储进程中所创建的全局引用对象。每个全局引用对象添加到globals_容器后都会得到一个IndirectRef值。这个值的类型虽然是指针类型void *但它的值和要保存的mirror Object对象的地址以及IndirectReferenceTable内部对元素管理的方法有关。外界需通过 IndirectReferenceTable的Get 函数将一个IndirectRef值还原为对应的mirror Object对象。 上述代码是全局引用对象的创建它是借助JavaVMExt对象的AddGlobalRef来完成的。与之相似 如果是创建局部引用对象的话将会使用JNIEnvExt对象的AddLocalRef函数来完成。每一个JNIEnvExt对象都包含一个locals_成员变量用于存储在这个JNIEnvExt环境里创建的局部引用对象。 JavaVM 和 JNIEnv 如上文所述JNI 是帮助 Java 层和 Native 层交互的接口。JNI 中有两个关键数据结构。 JavaVM它代表Java虚拟机。每一个 Java 进程有一个全局唯一的 JavaVM 对象。JNIEnv它是JNI运行环境的含义。每一个 Java 线程都有一个 JNIEnv 对象。Java线程在执行 JNI 相关操作时都需要利用该线程对应的 JNIEnv 对象。 JavaVM 和 JNIEnv 是jni.h里定义的数据结构里边包含的都是函数指针成员变量。所以这两个数据结构有些类似 Java 中的 interface。不同虚拟机实现都会从它们派生出实际的实现类。在 ART 虚拟机中JavaVM 和 JNIEnv 创建的代码如下所示。 再来看 ART 中JNIEnv的创建 JNI 中引用型对象的管理 我们先回顾一下Native层和Java层里对象的创建和销毁的过程。 以C为例Native层中要创建一个对象的话需使用new操作符以先分配内存然后构造对象。如果不再使用这个对象则需要通过delete操作符先析构这个对象然后回收该对象所占的内存。Java层中也通过new操作来构造一个对象。如果后续不再使用它则可以显式地设置持有这个对象的变量的值为null也可以不做这一步而交由垃圾回收来扫描和标记该对象是否有被引用。该对象所占的内存则在垃圾回收过程中被收回。 JNI层作为Java层和Native层之间相交互的中间层它兼具Native层和Java层的某些特性尤其在对引用对象的创建和回收上。 和C里的new操作符可以创建一个对象类似JNI层可以利用JNI NewObject等函数创建一个Java意义的对象引用型对象。这个被New出来的对象是Local型的引用对象。JNI层可通过DeleteLocalRef释放Local型的引用对象等同于Java层中设置持有这 个对象的变量的值为null。如果不调用DeleteLocalRef的话根据JNI规范Local 型对象在JNI函数返回后也会由虚拟机根据垃圾回收的逻辑进行标记和回收。除了Local型对象外JNI层借助JNI Global相关函数可以将一个Local型引用对象转换成一个Global型对象。而Global型对象的回收只能先由程序显式地调用Global相关函数进行删除然后虚拟机才能借助垃圾回收机制回收它们。 Mirror Object、ArtField、ArtMethod 关键类介绍 ClassLinker 中涉及常多的关键类认识它们将极大帮助后续的代码理解。先来看Mirror Object家族。 Mirror Object 家族 ART源码文件夹中有一个子文件夹叫mirror。这个mirror子文件夹下代码所定义的类都位于mirror命名空间中。mirror的中文含义是镜子那么这面镜子里外都是什么呢 原来在ART虚拟机的实现中Java 的某些类在虚拟机层也有对应的 C 类比如图7-20所示的 Mirror Object类家族图谱。 图7-20展示了Mirror Object家族中几个主要的类。其中 Object对应Java的Object类Class对应Java的Class类。以此类推DexCache、String、 Throwable、StackTraceElement 等与同名Java类相对应。Array对应Java Array类。对基础数据类型的数组比 如int[]long[] 这样的Java类则对应图中的PrimitiveArrayint以及PrimitiveArraylong。图7-20中的 PointArray 则可与 Java层中的IntArray或LongArray对应。对于其他类型的数组则可用ObjectArrayT模板类来描述。 注意IfTable在Java 层中没有对应类。 ArtFieldArtMethod 等 我们知道Java 源码中的 class 可以包含成员变量和成员函数当class 经过 dex2oat 编译转 换后一个类的成员变量和成员函数的信息将转换为对应的C类即 ArtField 和 ArtMethod如图7-23所示。 图7-23展示了ArtField和ArtMethod类它们用于描述类的成员变量和成员函数的信息。其中 declaring_class_成员变量指向声明该成员的类是谁。access_flags_成员变量描述该成员的访问权限比如是public还是private等。ArtField的 field_dex_idx_为该成员在dex文件中field_ids数组里的索引。field_ids数组的元素的类型可由field_id_item 来描述。 同理ArtMethod的几个成员变量也和dex文件格式密切相关如dex_code_itemoffset_为该函数对应字节码在 dex文件里的偏移量dex_method_idx_为该成员在dex文 件中 method_ids数组里的索引该数组的元素的数据类型为method_id_item。 来看下ArtField的GetName函数如果了解Dex文件格式的话这段代码几乎没有难度。 在图8-6中 DexCache、PointArray、IfTable和Class都是mirror Object家族的。这里要特别注意 IfTable它在Java层中没有对应类。LengthPrefixedArray是模板数组容器类其数组元素的个数以及每个元素的大小即SizeOf的值在创建之初就必须确定。使用过程中不允许修改总的元素个数。该类的实现相当简单笔者不拟介绍它。ArtField和ArtMethod 在ART虚拟机代码中分别用于描述一个类的成员变量和成员函数。 图8-7展示了四个关键信息的数据组织结构首先是代表类的基本信息的class_def结构体其中的关键内容如下所示。 class_idx实际上是一个索引值通过它可找到代表类类名的字符串。在某些书籍的术语中它们也叫符号引用Symbol Reference。与之类似superclass_idx 代表该类的父类的类名。interfaces_off它指向的数据结构由 type_list表示。type_list里包含一个type_item数 组。该数组的每一个成员对应描述了该类实现的一个接口类的类名通过type_item的type_idx可找到类名。class_data_off它指向的数据结构由 class_data_item表示里边包含了这个类的成员变量和成员函数的信息。 接着看 class_data_item结构体。其中 direct_methods数组和virtual_methods数组代表该类所定义的方法以及它继承或实现 的方法。根据dex文件格式的说明direct_methods包含该类中所有static、private 函数以及构造函数而 virtual_methods包含该类中除static、final以及构造函数之外的函数并且不包括从父类继承的函数如果本类没有重载它的话。static_fields和instance_fields 代表该类的静态成员以及非静态成员。 最后是代表类成员的encoded_field和 encoded_method结构体。其中 field_idx_diff是索引值的偏移量通过它能找到这个成员变量的变量名数据类型以及它所在类的类名。method_idx_diff和field_idx_diff类似通过它能找到这个成员函数的函数名、函数签 名信息由参数类型和返回值类型组成以及它所在类的类名。encoded_method中的 code_off指向该成员方法对应的dex指令码内容。 初识 ArtField 和 ArtMethod 接下来先介绍 ArtField 和 ArtMethod 这两个分别代表类的成员变量和成员方法的数据结构。 如上所述一个 ArtField 对象代表类中的一个成员变量。比如一个 Java 类 A 中有一个名为 a 的 long 型变量。那么在 ART 虚拟机中就有一个 ArtField 对象表示这个 a。不过请读者务必注意 a 这个变量需要的用来存储一个 long 型数据在Java中long型数据占据8个字节的空间在哪里上面展示的 ArtField 的成员变量也没有看出来哪里有地方存储这 8 个字节。是的一个 ArtField 对象仅仅是代表一个 Java 类的成员变量但它自己并不提供空间来存储这个Java 成员变量的内容。 提示下文介绍Class LinkFields时我们将看到这个Java成员变量所需的存储空间在什么地方。 接着来看 ArtMethod 的成员变量。 一个 ArtMethod 代表一个 Java 类中的成员方法。对一个方法而言也就是一个函数它的入口函数地址是最核心的信息。所以ArtMethod 通过成员 ptr_size_fields_ 结构体里相关变量直接就能存储这个信息。 初识 Class 接着来看Class类先关注它的成员变量。 上述Class的成员变量较多如下几个成员变量尤其值得读者关注。我们先介绍它们的情况下文将详细分析它们的来历和作用。 iftable_保存了该类所直接实现或间接实现的接口信息。直接实现是指该类自己implements的某个接口。间接实现是指它的继承关系树上有某个祖父类implements了某个接口。另外一条接口信息包含两个部分第一部分是接口类所对应的Class对象第二部分则是该接口类中的接口方法。 vtable_和iftable类似它保存了该类所有直接定义或间接定义的virtual方法信息。比如Object类中有耳熟能详的wait、notify、toString等的11个virtual方法。所以 任意一个派生类除interface类之外中都将包含这11个方法。 methods_methods只包含本类直接定义的direct方法、virtual方法和那些拷贝过来 的诸如Miranda这样的方法下文将介绍它。一般而言vtable包含的内容要远多于methods_。 embedded_imtable_、embedded_vtable_和fields_为隐含成员变量。其中前两个变量只在能实例化的类中才存在。实例化是指该类在Java层的对应类可以通过new来创建一个对象。举个反例基础数据类、抽象类、接口类就属于不能实例化的类。 接下来我们介绍三个小知识点。 Interface default method 从Java 1.8开始interface接口类中可以定义接口函数的默认实现了其英文描述为Javainterface default method。来看一段示例代码。 注意以上所说的 interface defaultstatic method 等只在Java 1.8上支持。 Miranda Methods 接着来认识Miranda methods。这是什么东西呢原来Miranda方法和美国的Miranda rights中文译为米兰达权利或米兰达规则有关。米兰达规则中说如果你负担不起请律师的费用的话法院将为你提供一个律师。放到Java世界中来米兰达规则则变成了如果有个类没有定义某个函数的话编译器将为你提供这个函数。为什么需要Miranda方法呢这和JavaVM早期版本中的一个缺陷有关。 提示 关于这个自动生成的Miranda方法资料上说是由编译器生成但并没有说是否在编译得到的class文件中能看到它。在Android平台上.dex文件中并不会包含Miranda方法。而是在ART虚拟机为MirandaAbstract类设置虚拟函数表时将拷贝来自Miranda- Interface 接口的inInterface到自己的虚拟函数表下文介绍LinkClass时将见到这和 在代码中主动为MirandaAbstract 类声明inInterface 函数是一样的效果。 Marker Interface 一般而言Interface中会定义相关功能函数的然后由实现类来实现。不过Java库中也存在一类没有提供任何功能函数的接口类。这些接口类大家想必还很熟悉比如下面列出的两个非常常见的接口类。 Cloneable和Serializable 接口类中就没有定义任何函数。这样的接口也叫 Marker Interface即起标记作用的接口。它只是说明实现者支持 Cloneable 或 Serializable而实际的 Clone 或 Serialize 功能则是由其他函数来完成比如下面的代码。 从 Object clone 函数可知Marker Interface 确实只是个标记罢了。 另外读者会好奇为什么ART不遗余力地要把类的所有virtual方法都组织到VTable中呢要知道这可是LinkMethods 中所调用的LinkVirtualMethods 函数的一个很主要的工作。这个问题我们可以反过来问如果Class中没有保存这个VTable会出现什么情况举个例子 假设我们要调用类A的wait方法也就是Object 11个virtual方法中的某个wait方法。搜索类A的methods数组读者还记得它吗它保存了本类明确定义的所有方法其中是没有wait方法。是的因为类A不会直接定义这个方法所以在类A中找不到它。 那么我们就该沿着类A的派生关系或实现关系一路向上搜索它们的methods数组了。显然这个过程非常耗时难以接受。 最后回顾上文对ArtMethod成员变量的介绍可知它有一个名为methodindex的成员变量该参数非常重要此处先简单总结它的取值如下 如果这个ArtMethod对应的是一个static或direct 函数则 methodindex是指向定义它的类的methods中的索引。 如果这个ArtMethod是virtual函数则methodindex是指向它的VTable中的索引。注 意可能多个类的VTable都包含该ArtMethod对象比如Object的那11个方法所以要保证这个methodindex在不同VTable中都有相同的值这也是LinkMethods中那三个函数比较复杂的原因。 如上所述Class的大小除了包含sizeofClass之外还包括IMTable、VTable如果该类 是可实例化的话所需空间以及静态变量的空间。如果要想知道一个Class中存储用于存储静态变量的位置时可利用下面这个函数获取。 LinkFields 代码介绍 先来看ART虚拟机实现中一个Java类以及这个类的实例分别需要多大的内存。如图8-13所示 图8-13展示了一个Java Class类对象以及这个类对应实例所需的内存大小。 左边是Java Class类对象所需内存大小。它由三部分组成首先是sizeofClass。然后是如果有的话IMTable和VTable所需空间最后是该类静态变量所需空间。注意引用类型排在最前面然后是longdouble 类型、intfloat类型、shortchar类型最后 是byteboolean类型变量所需空间。 右边是某个Java类对应实例对象所需空间。它包含两部分首先是父类对象的大小紧接其后的是非静态成员变量所需空间。内存布局与静态成员变量在Class中的一样。 提示 在OOP中我们经常会提及的一个知识点是类的成员函数和静态成员变量是类属性的即它们归属于类的财产。而类中定义的非静态成员变量则属于该类对应实例对象的。这个知识点在图8-13所示的Java Class和Java Object的内存布局中得到了印证。 接着我们再回顾ArtField的一个重要成员变量它的含义现在就可以解释清楚了
http://www.fuzeviewer.com/news/54780/

相关文章:

  • 邢台公司做网站wix做网站步骤
  • 英文版企业网站布局设计虚拟主机搭建
  • 2025年评价高的GEO优化推广市场表现榜
  • 做红酒的网站有哪些随州抖音seo收费标准
  • 计算机网站开发专业海南新闻在线观看
  • 重庆怎么站seo专门做淘宝客网站
  • vs和sql做购物网站免费wap自助建站系统
  • 网站建设 主要内容wordpress收费注册
  • 社交网站开发语言公司做网站需要提供什么条件
  • 有没有转门做乐器演奏的网站wordpress中logo大小
  • 小型网站设计及建设论文文献秦皇岛网站建设
  • asp网站后台模板泉州市网站设计企业
  • 网站定制开发一般多久龙之向导外贸网站 网络服务
  • 1688企业网站建设软装潢.企业网站建设
  • vps主机访问网站自学网络运营要多久
  • 服务器怎样建设网站新注册公司核名步骤
  • 政务网站建设情况汇报集约化网站建设
  • 网站推广目的云主机网站面板
  • 电子毕业设计网站建设互联网保险现状
  • 枣庄做网站建设找哪家网站seo优化很好徐州百度网络点赞
  • 深圳做专业网站html5做音乐网站
  • 可以看任何网站的浏览器rest api 做网站
  • 网站建设qq群高端网站设计制作的
  • 包头网站建设公司哪家好网址搜索域名查询
  • 网络规划设计师题库网站优化策略分析
  • 唐山企业做网站开源商城网站
  • 用vs做网站原型html网站建设实例教程
  • 沈阳专业做网站开发公司贵州网站定制
  • 网站开发语言总结官网天下
  • 网盟官方网站深圳福田区