Android Runtime数据类型转换与内存管理源码剖析
一、Android Runtime架构基础
Android Runtime(ART)是Android系统中应用运行的核心环境,其架构设计对数据类型转换与内存管理起着决定性作用。ART采用AOT(Ahead - Of - Time)编译技术,在应用安装时将字节码编译为机器码,相比早期的Dalvik虚拟机,显著提升了应用的执行效率。
从源码目录结构来看,ART的核心代码位于art/runtime路径下。runtime.h文件中定义了Runtime类,该类是ART运行时环境的核心入口,负责整体环境的初始化与管理。在runtime.cc文件中,Runtime::Init函数实现了运行时环境的初始化,包括内存池的创建、线程管理模块的启动等操作:
// art/runtime/runtime.cc
bool Runtime::Init(const RuntimeOptions& options) {
// 初始化内存分配器,为后续内存管理做准备
if (!mem::Init(options)) {
LOG(ERROR) << "Failed to initialize memory allocator";
return false;
}
// 启动线程管理模块,创建主线程
if (!threading::Init(options)) {
LOG(ERROR) << "Failed to initialize threading";
return false;
}
return true;
}
ART架构中的JNI模块,位于art/runtime/jni目录,负责Java层与本地层的数据交互,为数据类型转换提供基础支持;而内存管理相关功能主要由art/runtime/memory目录下的代码实现,涵盖堆内存管理、垃圾回收等核心逻辑。
二、数据类型基础与分类
2.1 Java层数据类型
Java层的数据类型分为基本数据类型和引用数据类型。基本数据类型包括byte、short、int、long、float、double、char和boolean。在ART源码中,这些基本数据类型在内存中的存储大小通过常量定义。例如,art/runtime/types.h文件中对int类型的大小定义:
// art/runtime/types.h
static constexpr size_t kIntSize = 4; // int类型占用4字节
引用数据类型包括类、接口和数组。以类为例,在ART中,类的实例对象存储在堆内存中,对象的内存布局由art/runtime/mirror/object.h文件中的Object类定义:
// art/runtime/mirror/object.h
class Object : public HeapObject {
public:
// 获取对象的类信息
Class* GetClass() const {
return reinterpret_cast<Class*>(GetClassPtr());
}
private:
// 指向对象所属类的指针
uintptr_t class_;
};
数组在ART中也有专门的实现,art/runtime/mirror/array.h文件定义了数组对象的结构,包括数组的类型、长度等信息。
2.2 本地层数据类型
本地层(C/C++)数据类型与Java层有对应关系,但在实现和使用上存在差异。基本数据类型如char、short、int等,在不同平台下字节大小可能不同。在Android常用的ARM平台上,int类型同样占用4字节,这与Java层的int在字节数上一致,但在数据表示和操作方式上仍有区别。
C/C++中的指针类型是其重要特性,用于动态内存分配和数据结构操作。例如,通过malloc函数分配内存,返回的是指向分配内存起始地址的指针,使用完后需通过free函数释放内存。在ART的本地代码中,指针类型用于管理Java对象的引用,以及在JNI函数中传递数据。
结构体和联合体也是本地层常用的数据类型,用于表示复杂的数据结构。在处理Java与本地层数据交互时,需要将这些数据类型与Java层的数据类型进行适配转换。
三、数据类型转换原理
3.1 基本数据类型转换
Java基本数据类型与本地层基本数据类型转换时,需考虑字节序、数据范围等因素。以int类型转换为例,当Java代码调用本地方法传递int参数时,ART通过JNI函数进行转换。在art/runtime/jni/jni_env_ext.cc文件中,GetIntField函数用于获取Java对象中int类型字段的值:
// art/runtime/jni/jni_env_ext.cc
jint JNIEnvExt::GetIntField(jobject obj, jfieldID fieldID) {
ScopedObjectAccess soa(this);
// 将Java对象转换为ART内部的对象表示
ObjPtr<mirror::Object> java_obj(soa.Decode<mirror::Object>(obj));
// 获取字段在对象内存中的偏移量
size_t offset = java_obj->GetFieldOffsetFromId(*soa.Self(), fieldID);
// 从对象内存中读取int类型的值
return java_obj->ReadIntField(offset);
}
当从本地层传递int数据到Java层时,SetIntField函数实现设置Java对象中int类型字段的值。其原理是先定位到对象字段的内存位置,然后将本地int值写入相应内存地址。
对于float和double类型,转换时需遵循IEEE 754标准。在ART中,相关转换函数通过对二进制位的操作,确保数据精度在转换过程中不丢失。
3.2 引用数据类型转换
引用数据类型转换比基本数据类型更为复杂。当Java对象传递给本地方法时,ART传递的是对象的引用(本质是指向对象在堆内存中位置的指针)。在本地方法中,通过JNI函数操作Java对象。例如,GetObjectField函数用于获取Java对象的成员变量,CallVoidMethod函数用于调用Java对象的无返回值方法。
在art/runtime/jni/jni_env_ext.cc文件中,CallVoidMethod函数的实现如下:
// art/runtime/jni/jni_env_ext.cc
void JNIEnvExt::CallVoidMethod(jobject obj, jmethodID methodID,...) {
ScopedObjectAccess soa(this);
// 将Java对象转换为ART内部的对象表示
ObjPtr<mirror::Object> java_obj(soa.Decode<mirror::Object>(obj));
// 获取方法的入口地址
void* entry_point = java_obj->GetVirtualMethodEntryPoint(*soa.Self(), methodID);
va_list args;
va_start(args, methodID);
// 调用方法
InvokeWithVarArgs(this, java_obj, entry_point, methodID, args);
va_end(args);
}
该函数首先获取Java对象的内部表示,然后找到要调用方法的入口地址,最后通过InvokeWithVarArgs函数执行方法调用。在这个过程中,需要处理对象的生命周期管理,确保对象在方法调用期间不会被垃圾回收。
对于Java数组传递给本地方法,ART同样传递数组的引用。本地方法通过JNI函数如GetArrayLength获取数组长度,GetIntArrayElements获取数组元素指针进行操作。操作完成后,需调用ReleaseIntArrayElements函数释放资源,并可选择将修改后的数据同步回Java数组。
四、JNI数据类型转换接口源码分析
4.1 基本数据类型转换接口
JNI为基本数据类型提供了丰富的转换接口。以int类型为例,GetIntField和SetIntField函数用于获取和设置Java对象中int类型字段的值。在jni.h文件中,定义了这些函数的原型:
// jni.h
jint GetIntField(JNIEnv*, jobject, jfieldID);
void SetIntField(JNIEnv*, jobject, jfieldID, jint);
在ART的实现中,GetIntField函数最终调用到art/runtime/jni/jni_env_ext.cc文件中的具体实现代码(前文已分析)。SetIntField函数的实现逻辑是先定位到对象字段的内存位置,然后将传入的jint值写入该位置:
// art/runtime/jni/jni_env_ext.cc
void JNIEnvExt::SetIntField(jobject obj, jfieldID fieldID, jint value) {
ScopedObjectAccess soa(this);
ObjPtr<mirror::Object> java_obj(soa.Decode<mirror::Object>(obj));
size_t offset = java_obj->GetFieldOffsetFromId(*soa.Self(), fieldID);
java_obj->WriteIntField(offset, value);
}
对于char类型数组,GetCharArrayElements函数用于获取Java char数组的元素指针,其实现过程涉及内存分配和数据复制:
// art/runtime/jni/jni_env_ext.cc
jchar* JNIEnvExt::GetCharArrayElements(jcharArray array, jboolean* isCopy) {
ScopedObjectAccess soa(this);
// 获取Java数组的内部表示
ObjPtr<mirror::CharArray> java_array(soa.Decode<mirror::CharArray>(array));
size_t length = java_array->GetLength();
// 分配本地内存用于存储数组元素
jchar* result = static_cast<jchar*>(malloc(length * sizeof(jchar)));
// 将Java数组元素复制到本地内存
java_array->CopyToExternalArray(result, 0, length);
*isCopy = JNI_TRUE;
return result;
}
ReleaseCharArrayElements函数则用于释放GetCharArrayElements分配的内存,并可选择将修改后的数据同步回Java数组:
// art/runtime/jni/jni_env_ext.cc
void JNIEnvExt::ReleaseCharArrayElements(jcharArray array, jchar* elems, jint mode) {
ScopedObjectAccess soa(this);
ObjPtr<mirror::CharArray> java_array(soa.Decode<mirror::CharArray>(array));
size_t length = java_array->GetLength();
if (mode & JNI_COMMIT) {
// 将本地修改后的数据同步回Java数组
java_array->CopyFromExternalArray(elems, 0, length);
}
// 释放本地内存
free(elems);
}
4.2 引用数据类型转换接口
引用数据类型转换接口用于在本地层创建、操作和访问Java对象。NewObject函数用于在本地层创建一个新的Java对象,其原型在jni.h中定义:
// jni.h
jobject NewObject(JNIEnv*, jclass, jmethodID,...);
在ART的实现中,NewObject函数首先找到要创建对象的类信息,然后调用类的构造函数创建对象:
// art/runtime/jni/jni_env_ext.cc
jobject JNIEnvExt::NewObject(jclass clazz, jmethodID methodID,...) {
ScopedObjectAccess soa(this);
// 获取Java类的内部表示
ObjPtr<mirror::Class> java_class(soa.Decode<mirror::Class>(clazz));
va_list args;
va_start(args, methodID);
// 创建对象并调用构造函数
ObjPtr<mirror::Object> result = java_class->AllocateObject<false>(*soa.Self(), methodID, args);
va_end(args);
return soa.AddLocalReference<jobject>(result.Ptr());
}
FindClass函数用于在本地层查找指定的Java类,其实现过程涉及类加载机制:
// art/runtime/jni/jni_env_ext.cc
jclass JNIEnvExt::FindClass(const char* name) {
ScopedObjectAccess soa(this);
// 通过类加载器加载类
ObjPtr<mirror::Class> result = ClassLinker::FindClass(name, soa.Self());
return soa.AddLocalReference<jclass>(result.Ptr());
}
GetMethodID函数用于获取类中方法的标识符,其实现是在类的方法表中查找对应的方法:
// art/runtime/jni/jni_env_ext.cc
jmethodID JNIEnvExt::GetMethodID(jclass clazz, const char* name, const char* sig) {
ScopedObjectAccess soa(this);
ObjPtr<mirror::Class> java_class(soa.Decode<mirror::Class>(clazz));
// 在类的方法表中查找方法
return java_class->FindVirtualMethod(*soa.Self(), name, sig).Get();
}
这些引用数据类型转换接口相互配合,使得本地代码能够完整地操作Java对象,实现Java与本地层之间复杂的数据交互。
五、内存管理基础概念
5.1 内存区域划分
Android Runtime的内存主要划分为堆内存、栈内存和方法区。栈内存用于存储局部变量和方法调用的上下文信息,其内存分配和回收遵循后进先出(LIFO)原则,由系统自动管理。
堆内存是对象存储的主要区域,ART中的对象实例都分配在堆内存中。堆内存的管理由ART的垃圾回收机制负责,在art/runtime/memory目录下实现相关逻辑。堆内存又可细分为多个子区域,如年轻代、老年代等,不同区域采用不同的垃圾回收策略。在art/runtime/gc/heap.h文件中,定义了堆内存的基本结构:
// art/runtime/gc/heap.h
class Heap {
public:
// 年轻代内存区域
Space* young_space_;
// 老年代内存区域
Space* old_space_;
// 初始化堆内存,分配年轻代和老年代空间
void Init();
// 垃圾回收操作
void GC();
};
方法区用于存储类的元数据信息,包括类的结构、方法定义、常量池等。在ART中,方法区的管理由art/runtime/class_linker.cc文件中的ClassLinker类负责,该类在类加载过程中,将类的元数据信息存储到方法区,并在类卸载时进行清理。
5.2 内存分配与回收策略
内存分配策略决定了如何在堆内存中为对象分配空间。ART采用多种分配策略,如首次适应(First Fit)、最佳适应(Best Fit)等。在art/runtime/malloc.cc文件中,实现了基本的内存分配函数:
// art/runtime/malloc.cc
void* Malloc(size_t size) {
// 使用系统的malloc函数分配内存
void* result = ::malloc(size);
if (result == nullptr) {
// 内存分配失败时的处理
LOG(ERROR) << "Failed to allocate memory of size " << size;
}
return result;
}
在对象创建时,ART会根据对象的大小和类型,选择合适的内存分配策略。对于较小的对象,通常分配在年轻代;对于较大或生命周期较长的对象,则分配在老年代。
内存回收策略主要由垃圾回收机制实现。ART支持多种垃圾回收算法,如标记 - 清除(Mark - Sweep)、标记 - 整理(Mark - Compact)、分代回收等。在垃圾回收过程中,首先通过标记阶段确定哪些对象是可达的(即仍在使用的对象),然后在清除或整理阶段回收不可达对象占用的内存。在art/runtime/gc/gc.cc文件中,实现了垃圾回收的核心逻辑:
// art/runtime/gc/gc.cc
void Heap::GC() {
// 标记阶段,标记所有可达对象
MarkAllLiveObjects();
// 清除阶段,回收不可达对象占用的内存
SweepDeadObjects();
// 整理阶段(如果需要),压缩内存空间
CompactHeapIfNeeded();
}
垃圾回收机制通过这些步骤,有效地释放不再使用的内存,防止内存泄漏,确保应用程序的内存使用效率和稳定性。
六、堆内存管理源码剖析
6.1 堆内存初始化
堆内存的初始化在ART启动过程中完成,主要由art/runtime/gc/heap.cc文件中的Heap::Init函数实现:
// art/runtime/gc/heap.cc
void Heap::Init() {
// 初始化年轻代空间
young_space_ = new Space(kYoungSpaceSize);
// 初始化老年代空间
old_space_ = new Space(kOldSpaceSize);
// 设置堆内存的相关参数
SetHeapGrowthLimit(kMaxHeapSize);
SetHeapMinimumSize(kMinHeapSize);
// 初始化垃圾回收相关组件
gc_controller_ = new GCController(this);
gc_scheduler_ = new GCScheduler(gc_controller_);
}
上述代码首先创建年轻代和老年代的内存空间对象,然后设置堆内存的增长限制和最小大小。接着初始化垃圾回收控制器和调度器,为后续的内存分配和回收管理做准备。在创建空间对象时,会调用底层的内存分配函数,如malloc,从系统获取实际的内存资源。
6.2 对象内存分配
当Java代码创建对象时,ART会在堆内存中为对象分配空间。对象内存分配的核心逻辑在art/runtime/gc/heap.cc文件的AllocObject函数中实现:
// art/runtime/gc/heap.cc
ObjPtr<mirror::Object> Heap::AllocObject(Class* clazz, size_t extra_bytes) {
size_t object_size = clazz->GetObjectSize(extra_bytes);
// 根据对象大小选择分配策略
if (object_size <= kMaxYoungObjectSize) {
七、垃圾回收机制深度解析
7.1 标记阶段实现原理与源码
垃圾回收的标记阶段核心目标是确定堆内存中所有存活对象。在ART中,标记阶段依赖根节点集合(如Java栈中的对象引用、静态变量等)作为起点,通过深度优先搜索(DFS)或广度优先搜索(BFS)算法遍历对象图,标记所有可达对象。
在art/runtime/gc/heap.cc中,MarkAllLiveObjects函数开启标记流程:
// art/runtime/gc/heap.cc
void Heap::MarkAllLiveObjects() {
// 初始化标记位图,用于记录对象是否被标记
mark_bitmap_.Reset();
// 获取根节点集合,包括Java栈、JNI局部引用等
RootVisitor root_visitor(this, &mark_bitmap_);
root_visitor.VisitRoots();
// 从JNI全局引用开始标记
MarkJniGlobalReferences();
// 标记线程本地存储中的对象
MarkThreadLocalStorage();
}
RootVisitor类负责遍历根节点集合,其VisitRoots方法如下:
// art/runtime/gc/root_visitor.cc
void RootVisitor::VisitRoots() {
// 遍历Java栈中的对象引用
StackVisitor stack_visitor(this);
stack_visitor.VisitStacks();
// 处理JNI局部引用
VisitJniLocalReferences();
// 标记类静态变量引用的对象
VisitClassStaticReferences();
}
当访问到对象引用时,通过mirror::Object类的Mark方法进行标记:
// art/runtime/mirror/object.h
void Object::Mark(MarkBitmap* mark_bitmap) const {
// 检查对象是否已在标记位图中标记
if (mark_bitmap->IsMarked(this)) {
return;
}
// 设置对象在标记位图中的标记位
mark_bitmap->Mark(this);
// 递归标记对象引用的其他对象
DoMark(mark_bitmap);
}
DoMark方法会根据对象类型,遍历其成员变量引用的其他对象,持续扩展标记范围,确保整个对象图中可达对象都被正确标记。
7.2 清除与整理阶段
标记阶段完成后,清除阶段负责回收未标记的对象。在art/runtime/gc/heap.cc的SweepDeadObjects函数中:
// art/runtime/gc/heap.cc
void Heap::SweepDeadObjects() {
// 遍历年轻代空间
for (Region* region : young_space_->regions()) {
region->Sweep(&mark_bitmap_);
}
// 遍历老年代空间
for (Region* region : old_space_->regions()) {
region->Sweep(&mark_bitmap_);
}
}
Region类的Sweep方法会检查每个对象的标记状态:
// art/runtime/gc/space/region.cc
void Region::Sweep(MarkBitmap* mark_bitmap) {
// 遍历区域内的对象
for (HeapObject* obj : objects_) {
if (!mark_bitmap->IsMarked(obj)) {
// 回收未标记对象占用的内存
Free(obj);
} else {
// 清除标记,为下一次垃圾回收做准备
mark_bitmap->ClearMark(obj);
}
}
}
对于整理阶段(CompactHeapIfNeeded函数),ART采用标记 - 整理算法时,会将存活对象移动到连续内存区域,减少内存碎片。这一过程需要更新所有对象引用的内存地址,通过DexCache::UpdatePointers函数遍历所有类和方法,修正内部的对象引用指针:
// art/runtime/dex_cache.cc
void DexCache::UpdatePointers(Heap* heap) {
// 遍历缓存中的所有类
for (ClassLinker::ClassDataIterator it(this);!it.Done(); it.Next()) {
mirror::Class* clazz = it.Get();
// 更新类中静态变量的对象引用
UpdateStaticObjectPointers(clazz, heap);
// 更新类中虚方法表的对象引用
UpdateVTableObjectPointers(clazz, heap);
}
}
八、JNI与内存管理的交互
8.1 JNI局部引用与全局引用
JNI提供局部引用和全局引用机制管理Java对象在本地代码中的生命周期。局部引用在JNI函数调用结束时自动释放,其实现依赖art/runtime/jni/jni_reference_table.cc中的JNIReferenceTable类:
// art/runtime/jni/jni_reference_table.cc
jobject JNIReferenceTable::AddLocalReference(ObjPtr<mirror::Object> obj) {
// 检查引用表是否已满
if (IsFull()) {
// 触发局部引用表扩容或抛出异常
GrowIfNeeded();
}
// 将对象添加到局部引用表,并返回引用ID
return AddReference(obj, kLocalRefType);
}
全局引用生命周期由开发者手动管理,通过NewGlobalRef函数创建,DeleteGlobalRef函数释放。在art/runtime/jni/jni_env_ext.cc中:
// art/runtime/jni/jni_env_ext.cc
jobject JNIEnvExt::NewGlobalRef(jobject obj) {
ScopedObjectAccess soa(this);
// 将Java对象转换为ART内部对象
ObjPtr<mirror::Object> java_obj(soa.Decode<mirror::Object>(obj));
// 在全局引用表中创建引用
return soa.AddGlobalReference<jobject>(java_obj.Ptr());
}
全局引用表存储在堆内存中,若未及时释放,会导致内存泄漏。因此,在使用全局引用时,开发者必须在对象不再使用时调用DeleteGlobalRef。
8.2 JNI调用过程中的内存保护
当本地代码调用Java方法时,ART需确保相关Java对象在调用期间不被垃圾回收。这通过ScopedObjectAccess类实现,该类在art/runtime/scoped_object_access.h中定义:
// art/runtime/scoped_object_access.h
class ScopedObjectAccess {
public:
ScopedObjectAccess(const JavaVMExt* vm) : vm_(vm) {
// 暂停垃圾回收
vm_->SuspendAllForGc();
}
~ScopedObjectAccess() {
// 恢复垃圾回收
vm_->ResumeAllAfterGc();
}
// 将Java对象指针转换为ART内部对象指针
template <typename T>
ObjPtr<T> Decode(jobject obj) const {
return vm_->Decode<
九、内存分配优化策略
9.1 快速分配路径
为提升小对象分配效率,ART设计了快速分配路径。当创建小对象(如小于128字节的对象)时,会优先使用Thread::AllocateObject的快速路径:
// art/runtime/thread.cc
ObjPtr<mirror::Object> Thread::AllocateObject(Class* clazz, size_t extra_bytes) {
size_t object_size = clazz->GetObjectSize(extra_bytes);
// 判断是否满足快速分配条件
if (object_size <= kFastAllocSize && Locks::mutator_lock_->IsHeld(this)) {
// 尝试从线程本地分配缓冲区获取内存
Heap* heap = Runtime::Current()->GetHeap();
uint8_t* buffer = tlab_.AllocateRaw(object_size);
if (buffer != nullptr) {
// 在缓冲区创建对象
return Heap::CreateObject<false>(clazz, buffer, extra_bytes);
}
}
// 不满足快速路径时,走常规分配流程
return heap->AllocObject(clazz, extra_bytes);
}
TLAB(Thread - Local Allocation Buffer,线程本地分配缓冲区)为每个线程分配专属内存区域,减少多线程下的锁竞争,提升小对象分配速度。
9.2 大对象直接分配
对于大对象(超过kLargeObjectThreshold字节),ART采用直接分配策略,绕过年轻代,直接在老年代分配内存。在art/runtime/gc/heap.cc的AllocLargeObject函数中:
// art/runtime/gc/heap.cc
ObjPtr<mirror::Object> Heap::AllocLargeObject(Class* clazz, size_t extra_bytes) {
size_t object_size = clazz->GetObjectSize(extra_bytes);
// 检查对象大小是否符合大对象标准
if (object_size > kLargeObjectThreshold) {
// 直接在老年代分配内存
return old_space_->AllocObject(clazz, object_size, extra_bytes);
}
// 非大对象,按常规流程分配
return AllocObject(clazz, extra_bytes);
}
这种策略避免大对象频繁在年轻代引发垃圾回收,减少应用卡顿,同时降低内存碎片产生的概率。
十、数据类型转换与内存管理的协同优化
10.1 减少转换开销
在数据类型转换过程中,ART通过缓存和复用机制减少开销。例如,对于JNI方法签名的解析,art/runtime/jni/jni_method_table.cc中的JniMethodTable类会缓存已解析的方法签名:
// art/runtime/jni/jni_method_table.cc
jmethodID JniMethodTable::GetOrCreateMethodID(jclass clazz, const char* name, const char* sig) {
// 检查方法ID是否已缓存
MethodIDCache::iterator it = method_id_cache_.find({clazz, name, sig});
if (it != method_id_cache_.end()) {
return it->second;
}
// 未缓存时,解析方法ID并添加到缓存
jmethodID method_id = env_->GetMethodID(clazz, name, sig);
method_id_cache_[{clazz, name, sig}] = method_id;
return method_id;
}
在内存管理方面,对象池技术被用于复用临时对象,减少频繁创建和销毁对象带来的开销。如art/runtime/alloc/object_pool.h中定义的ObjectPool类:
// art/runtime/alloc/object_pool.h
template <typename T>
class ObjectPool {
public:
T* Acquire() {
// 从对象池中获取对象
if (!pool_.empty()) {
T* obj = pool_.back();
pool_.pop_back();
return obj;
}
// 对象池为空时,创建新对象
return new T();
}
void Release(T* obj) {
// 将对象放回对象池
pool_.push_back(obj);
}
private:
std::vector<T*> pool_;
};
10.2 内存布局与数据对齐
ART在内存布局上充分考虑数据对齐和类型兼容性。在art/runtime/mirror/class.h中,类的内存布局定义遵循数据对齐原则:
// art/runtime/mirror/class.h
class Class : public HeapObject {
public:
// 类的虚函数表指针,按8字节对齐
void** vtable_ alignas(8);
// 类的接口表指针
void** interface_table_;
// 类的字段偏移量表
uint32_t* field_offset_table_;
// 确保类的内存大小为8字节的倍数
static size_t AlignSize(size_t size) {
return (size + 7) & ~7;
}
};
这种布局方式不仅提高CPU读取数据的效率,也在数据类型转换时减少因内存不对齐引发的异常,确保Java与本地代码间数据交互的稳定性。
上述内容从多维度深入剖析了Android Runtime数据类型转换与内存管理机制。如果你对某部分内容想进一步了解,或希望补充特定知识点,欢迎随时告知。