Android类加载流程分析

作者:luoyesiqiu 时间:2023-05-30 11:13:44 

背景

由于前前前阵子写了个壳,得去了解类的加载流程,当时记了一些潦草的笔记。这几天把这些东西简单梳理了一下,本文分析的代码基于Android8.1.0源码。

流程分析

从loadClass开始,我们来看下Android中类加载的流程

/libcore/ojluni/src/main/java/java/lang/ClassLoader.java::loadClass

loadClass流程如下:

Android类加载流程分析

protected Class<?> loadClass(String name, boolean resolve)
   throws ClassNotFoundException
{
       // First, check if the class has already been loaded
       Class<?> c = findLoadedClass(name);
       if (c == null) {
           try {
               if (parent != null) {
                   c = parent.loadClass(name, false);
               } else {
                   c = findBootstrapClassOrNull(name);
               }
           } catch (ClassNotFoundException e) {
               // ClassNotFoundException thrown if class not found
               // from the non-null parent class loader
           }

if (c == null) {
               // If still not found, then invoke findClass in order
               // to find the class.
               c = findClass(name);
           }
       }
       return c;
}

/libcore/ojluni/src/main/java/java/lang/ClassLoader.java::findClass

protected Class<?> findClass(String name) throws ClassNotFoundException {
       throw new ClassNotFoundException(name);
   }

ClassLoader类的findClass是没有实际查找代码的,所以调用findClass其实是调用其实现类的findClass函数,例如:BaseDexClassLoader

/libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java::findClass

每个BaseDexClassLoader都持有一个DexPathList,BaseDexClassLoader的findClass类调用了DexPathList的findClass。

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
   List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
   Class c = pathList.findClass(name, suppressedExceptions);
   if (c == null) {
       ClassNotFoundException cnfe = new ClassNotFoundException(
               "Didn't find class \"" + name + "\" on path: " + pathList);
       for (Throwable t : suppressedExceptions) {
           cnfe.addSuppressed(t);
       }
       throw cnfe;
   }
   return c;
}

/libcore/dalvik/src/main/java/dalvik/system/DexPathList.java::findClass

遍历所有dexElements,并调用Element类的findClass。

public Class<?> findClass(String name, List<Throwable> suppressed) {
   for (Element element : dexElements) {
       Class<?> clazz = element.findClass(name, definingContext, suppressed);
       if (clazz != null) {
           return clazz;
       }
   }

if (dexElementsSuppressedExceptions != null) {
       suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
   }
   return null;
}

题外话,dexElements对象其实是DexPathList$Element类的数组,用于存储已加载的dex或者jar的信息。

/libcore/dalvik/src/main/java/dalvik/system/DexPathList$Element::findClass

Element的findClass,又去调用DexFile类的loadClassBinaryName,可以理解为在单独的dex或者jar对象中加载类

public Class<?> findClass(String name, ClassLoader definingContext,
               List<Throwable> suppressed) {
           return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed)
                   : null;
       }

libcore\dalvik\src\main\java\dalvik\system\DexFile.java::loadClassBinaryName

去调用defineClass函数

public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
       return defineClass(name, loader, mCookie, this, suppressed);
   }

libcore\dalvik\src\main\java\dalvik\system\DexFile.java::defineClass

调用defineClassNative,准备进入Native层

private static Class defineClass(String name, ClassLoader loader, Object cookie,
                                DexFile dexFile, List<Throwable> suppressed) {
   Class result = null;
   try {
       result = defineClassNative(name, loader, cookie, dexFile);
   } catch (NoClassDefFoundError e) {
       if (suppressed != null) {
           suppressed.add(e);
       }
   } catch (ClassNotFoundException e) {
       if (suppressed != null) {
           suppressed.add(e);
       }
   }
   return result;
}

art\runtime\native\dalvik_system_DexFile.cc::DexFile_defineClassNative

检查dex是否加载,类名是否合理,并遍历DexFile对象,查找Dex文件中的类的定义,找到就去调用ClassLinker::DefineClass函数。

static jclass DexFile_defineClassNative(JNIEnv* env,
                                       jclass,
                                       jstring javaName,
                                       jobject javaLoader,
                                       jobject cookie,
                                       jobject dexFile) {
 std::vector<const DexFile*> dex_files;
 const OatFile* oat_file;
 if (!ConvertJavaArrayToDexFiles(env, cookie, /*out*/ dex_files, /*out*/ oat_file)) {
   VLOG(class_linker) << "Failed to find dex_file";
   DCHECK(env->ExceptionCheck());
   return nullptr;
 }

ScopedUtfChars class_name(env, javaName);
 if (class_name.c_str() == nullptr) {
   VLOG(class_linker) << "Failed to find class_name";
   return nullptr;
 }
 const std::string descriptor(DotToDescriptor(class_name.c_str()));
 const size_t hash(ComputeModifiedUtf8Hash(descriptor.c_str()));
 for (auto& dex_file : dex_files) {
   const DexFile::ClassDef* dex_class_def =
       OatDexFile::FindClassDef(*dex_file, descriptor.c_str(), hash);
   if (dex_class_def != nullptr) {
     ScopedObjectAccess soa(env);
     ClassLinker* class_linker = Runtime::Current()->GetClassLinker();
     StackHandleScope<1> hs(soa.Self());
     Handle<mirror::ClassLoader> class_loader(
         hs.NewHandle(soa.Decode<mirror::ClassLoader>(javaLoader)));
     ObjPtr<mirror::DexCache> dex_cache =
         class_linker->RegisterDexFile(*dex_file, class_loader.Get());
     if (dex_cache == nullptr) {
       // OOME or InternalError (dexFile already registered with a different class loader).
       soa.Self()->AssertPendingException();
       return nullptr;
     }
     ObjPtr<mirror::Class> result = class_linker->DefineClass(soa.Self(),
                                                              descriptor.c_str(),
                                                              hash,
                                                              class_loader,
                                                              *dex_file,
                                                              *dex_class_def);
     // Add the used dex file. This only required for the DexFile.loadClass API since normal
     // class loaders already keep their dex files live.
     class_linker->InsertDexFileInToClassLoader(soa.Decode<mirror::Object>(dexFile),
                                                class_loader.Get());
     if (result != nullptr) {
       VLOG(class_linker) << "DexFile_defineClassNative returning " << result
                          << " for " << class_name.c_str();
       return soa.AddLocalReference<jclass>(result);
     }
   }
 }
 VLOG(class_linker) << "Failed to find dex_class_def " << class_name.c_str();
 return nullptr;
}

art\runtime\class_linker.cc::DefineClass

DefineClass这个函数做了许多工作,相当于底层类加载逻辑的分发器,整体逻辑如下图:

Android类加载流程分析

mirror::Class* ClassLinker::DefineClass(Thread* self,
                                       const char* descriptor,
                                       size_t hash,
                                       Handle<mirror::ClassLoader> class_loader,
                                       const DexFile& dex_file,
                                       const DexFile::ClassDef& dex_class_def) {

StackHandleScope<3> hs(self);
 auto klass = hs.NewHandle<mirror::Class>(nullptr);

......

// Get the real dex file. This will return the input if there aren't any callbacks or they do
 // nothing.
 DexFile const* new_dex_file = nullptr;
 DexFile::ClassDef const* new_class_def = nullptr;
 // TODO We should ideally figure out some way to move this after we get a lock on the klass so it
 // will only be called once.
 Runtime::Current()->GetRuntimeCallbacks()->ClassPreDefine(descriptor,
                                                           klass,
                                                           class_loader,
                                                           dex_file,
                                                           dex_class_def,
                                                           &new_dex_file,
                                                           &new_class_def);
 // Check to see if an exception happened during runtime callbacks. Return if so.
 if (self->IsExceptionPending()) {
   return nullptr;
 }
 ObjPtr<mirror::DexCache> dex_cache = RegisterDexFile(*new_dex_file, class_loader.Get());
 if (dex_cache == nullptr) {
   self->AssertPendingException();
   return nullptr;
 }
 klass->SetDexCache(dex_cache);
 SetupClass(*new_dex_file, *new_class_def, klass, class_loader.Get());

// Mark the string class by setting its access flag.
 if (UNLIKELY(!init_done_)) {
   if (strcmp(descriptor, "Ljava/lang/String;") == 0) {
     klass->SetStringClass();
   }
 }

ObjectLock<mirror::Class> lock(self, klass);
 klass->SetClinitThreadId(self->GetTid());
 // Make sure we have a valid empty iftable even if there are errors.
 klass->SetIfTable(GetClassRoot(kJavaLangObject)->GetIfTable());

// Add the newly loaded class to the loaded classes table.
 ObjPtr<mirror::Class> existing = InsertClass(descriptor, klass.Get(), hash);
 if (existing != nullptr) {
   // We failed to insert because we raced with another thread. Calling EnsureResolved may cause
   // this thread to block.
   return EnsureResolved(self, descriptor, existing);
 }

// Load the fields and other things after we are inserted in the table. This is so that we don't
 // end up allocating unfree-able linear alloc resources and then lose the race condition. The
 // other reason is that the field roots are only visited from the class table. So we need to be
 // inserted before we allocate / fill in these fields.
 LoadClass(self, *new_dex_file, *new_class_def, klass);
 if (self->IsExceptionPending()) {
   VLOG(class_linker) << self->GetException()->Dump();
   // An exception occured during load, set status to erroneous while holding klass' lock in case
   // notification is necessary.
   if (!klass->IsErroneous()) {
     mirror::Class::SetStatus(klass, mirror::Class::kStatusErrorUnresolved, self);
   }
   return nullptr;
 }

// Finish loading (if necessary) by finding parents
 CHECK(!klass->IsLoaded());
 if (!LoadSuperAndInterfaces(klass, *new_dex_file)) {
   // Loading failed.
   if (!klass->IsErroneous()) {
     mirror::Class::SetStatus(klass, mirror::Class::kStatusErrorUnresolved, self);
   }
   return nullptr;
 }
 CHECK(klass->IsLoaded());

// At this point the class is loaded. Publish a ClassLoad event.
 // Note: this may be a temporary class. It is a listener's responsibility to handle this.
 Runtime::Current()->GetRuntimeCallbacks()->ClassLoad(klass);

// Link the class (if necessary)
 CHECK(!klass->IsResolved());
 // TODO: Use fast jobjects?
 auto interfaces = hs.NewHandle<mirror::ObjectArray<mirror::Class>>(nullptr);

MutableHandle<mirror::Class> h_new_class = hs.NewHandle<mirror::Class>(nullptr);
 if (!LinkClass(self, descriptor, klass, interfaces, &h_new_class)) {
   // Linking failed.
   if (!klass->IsErroneous()) {
     mirror::Class::SetStatus(klass, mirror::Class::kStatusErrorUnresolved, self);
   }
   return nullptr;
 }
 self->AssertNoPendingException();
 CHECK(h_new_class != nullptr) << descriptor;
 CHECK(h_new_class->IsResolved() && !h_new_class->IsErroneousResolved()) << descriptor;

// Instrumentation may have updated entrypoints for all methods of all
 // classes. However it could not update methods of this class while we
 // were loading it. Now the class is resolved, we can update entrypoints
 // as required by instrumentation.
 if (Runtime::Current()->GetInstrumentation()->AreExitStubsInstalled()) {
   // We must be in the kRunnable state to prevent instrumentation from
   // suspending all threads to update entrypoints while we are doing it
   // for this class.
   DCHECK_EQ(self->GetState(), kRunnable);
   Runtime::Current()->GetInstrumentation()->InstallStubsForClass(h_new_class.Get());
 }

/*
  * We send CLASS_PREPARE events to the debugger from here.  The
  * definition of "preparation" is creating the static fields for a
  * class and initializing them to the standard default values, but not
  * executing any code (that comes later, during "initialization").
  *
  * We did the static preparation in LinkClass.
  *
  * The class has been prepared and resolved but possibly not yet verified
  * at this point.
  */
 Runtime::Current()->GetRuntimeCallbacks()->ClassPrepare(klass, h_new_class);

// Notify native debugger of the new class and its layout.
 jit::Jit::NewTypeLoadedIfUsingJit(h_new_class.Get());

return h_new_class.Get();
}

art\runtime\class_linker.cc::SetupClass

SetupClass设置类的一些基本字段信息。

void ClassLinker::SetupClass(const DexFile& dex_file,
                            const DexFile::ClassDef& dex_class_def,
                            Handle<mirror::Class> klass,
                            ObjPtr<mirror::ClassLoader> class_loader) {
 CHECK(klass != nullptr);
 CHECK(klass->GetDexCache() != nullptr);
 CHECK_EQ(mirror::Class::kStatusNotReady, klass->GetStatus());
 const char* descriptor = dex_file.GetClassDescriptor(dex_class_def);
 CHECK(descriptor != nullptr);

klass->SetClass(GetClassRoot(kJavaLangClass));
 uint32_t access_flags = dex_class_def.GetJavaAccessFlags();
 CHECK_EQ(access_flags & ~kAccJavaFlagsMask, 0U);
 klass->SetAccessFlags(access_flags);
 klass->SetClassLoader(class_loader);
 DCHECK_EQ(klass->GetPrimitiveType(), Primitive::kPrimNot);
 mirror::Class::SetStatus(klass, mirror::Class::kStatusIdx, nullptr);

klass->SetDexClassDefIndex(dex_file.GetIndexForClassDef(dex_class_def));
 klass->SetDexTypeIndex(dex_class_def.class_idx_);
}

延申:mirror命名空间下的类是底层对Java层类的映射,比如:mirror::Class类就是对java.lang.Class类的映射,SetAccessFlags就是对Class类的accessFlags字段赋值。

art\runtime\class_linker.cc::InsertClass

InsertClass函数判断类是否在列表中:

  • 如果在列表中,则直接返回;

  • 如果没有,则添加到列表。

mirror::Class* ClassLinker::InsertClass(const char* descriptor, ObjPtr<mirror::Class> klass, size_t hash) {
 if (VLOG_IS_ON(class_linker)) {
   ObjPtr<mirror::DexCache> dex_cache = klass->GetDexCache();
   std::string source;
   if (dex_cache != nullptr) {
     source += " from ";
     source += dex_cache->GetLocation()->ToModifiedUtf8();
   }
   LOG(INFO) << "Loaded class " << descriptor << source;
 }
 {
   WriterMutexLock mu(Thread::Current(), *Locks::classlinker_classes_lock_);
   ObjPtr<mirror::ClassLoader> const class_loader = klass->GetClassLoader();
   ClassTable* const class_table = InsertClassTableForClassLoader(class_loader);
   ObjPtr<mirror::Class> existing = class_table->Lookup(descriptor, hash);
   if (existing != nullptr) {
     return existing.Ptr();
   }
   VerifyObject(klass);
   class_table->InsertWithHash(klass, hash);
   if (class_loader != nullptr) {
     // This is necessary because we need to have the card dirtied for remembered sets.
     Runtime::Current()->GetHeap()->WriteBarrierEveryFieldOf(class_loader);
   }
   if (log_new_roots_) {
     new_class_roots_.push_back(GcRoot<mirror::Class>(klass));
   }
 }
 if (kIsDebugBuild) {
   // Test that copied methods correctly can find their holder.
   for (ArtMethod& method : klass->GetCopiedMethods(image_pointer_size_)) {
     CHECK_EQ(GetHoldingClassOfCopiedMethod(&method), klass);
   }
 }
 return nullptr;
}

art\runtime\class_linker.cc::LoadClass

LoadClass函数获取了dex文件中的classData部分,然后去调用LoadClassMembers

void ClassLinker::LoadClass(Thread* self,
                           const DexFile& dex_file,
                           const DexFile::ClassDef& dex_class_def,
                           Handle<mirror::Class> klass) {
 const uint8_t* class_data = dex_file.GetClassData(dex_class_def);
 if (class_data == nullptr) {
   return;  // no fields or methods - for example a marker interface
 }
 LoadClassMembers(self, dex_file, class_data, klass);
}

art\runtime\class_linker.cc::LoadClassMembers

LoadClassMembers函数主要逻辑是遍历类中的所有字段和函数,然后分别调用LoadField,LoadMethod和LinkCode

void ClassLinker::LoadClassMembers(Thread* self,
                                  const DexFile& dex_file,
                                  const uint8_t* class_data,
                                  Handle<mirror::Class> klass){
   ......

LinearAlloc* const allocator = GetAllocatorForClassLoader(klass->GetClassLoader());
   ClassDataItemIterator it(dex_file, class_data);
   LengthPrefixedArray<ArtField>* sfields = AllocArtFieldArray(self,
                                                               allocator,
                                                               it.NumStaticFields());
   size_t num_sfields = 0;
   uint32_t last_field_idx = 0u;
   for (; it.HasNextStaticField(); it.Next()) {
     uint32_t field_idx = it.GetMemberIndex();
     DCHECK_GE(field_idx, last_field_idx);  // Ordering enforced by DexFileVerifier.
     if (num_sfields == 0 || LIKELY(field_idx > last_field_idx)) {
       DCHECK_LT(num_sfields, it.NumStaticFields());
       LoadField(it, klass, &sfields->At(num_sfields));
       ++num_sfields;
       last_field_idx = field_idx;
     }
   }

// Load instance fields.
   LengthPrefixedArray<ArtField>* ifields = AllocArtFieldArray(self,
                                                               allocator,
                                                               it.NumInstanceFields());
   size_t num_ifields = 0u;
   last_field_idx = 0u;
   for (; it.HasNextInstanceField(); it.Next()) {
     uint32_t field_idx = it.GetMemberIndex();
     DCHECK_GE(field_idx, last_field_idx);  // Ordering enforced by DexFileVerifier.
     if (num_ifields == 0 || LIKELY(field_idx > last_field_idx)) {
       DCHECK_LT(num_ifields, it.NumInstanceFields());
       LoadField(it, klass, &ifields->At(num_ifields));
       ++num_ifields;
       last_field_idx = field_idx;
     }
   }

......

size_t class_def_method_index = 0;
   uint32_t last_dex_method_index = DexFile::kDexNoIndex;
   size_t last_class_def_method_index = 0;
   for (size_t i = 0; it.HasNextDirectMethod(); i++, it.Next()) {
     ArtMethod* method = klass->GetDirectMethodUnchecked(i, image_pointer_size_);
     LoadMethod(dex_file, it, klass, method);
     LinkCode(this, method, oat_class_ptr, class_def_method_index);
     uint32_t it_method_index = it.GetMemberIndex();
     if (last_dex_method_index == it_method_index) {
       // duplicate case
       method->SetMethodIndex(last_class_def_method_index);
     } else {
       method->SetMethodIndex(class_def_method_index);
       last_dex_method_index = it_method_index;
       last_class_def_method_index = class_def_method_index;
     }
     class_def_method_index++;
   }
   for (size_t i = 0; it.HasNextVirtualMethod(); i++, it.Next()) {
     ArtMethod* method = klass->GetVirtualMethodUnchecked(i, image_pointer_size_);
     LoadMethod(dex_file, it, klass, method);
     DCHECK_EQ(class_def_method_index, it.NumDirectMethods() + i);
     LinkCode(this, method, oat_class_ptr, class_def_method_index);
     class_def_method_index++;
   }
   ......
}

art\runtime\class_linker.cc::LoadField

LoadField设置ArtField结构中字段的一些值

void ClassLinker::LoadField(const ClassDataItemIterator& it,
                           Handle<mirror::Class> klass,
                           ArtField* dst) {
 const uint32_t field_idx = it.GetMemberIndex();
 dst->SetDexFieldIndex(field_idx);
 dst->SetDeclaringClass(klass.Get());
 dst->SetAccessFlags(it.GetFieldAccessFlags());
}

art\runtime\class_linker.cc::LoadMethod

LoadMethod函数主要做设置ArtMethod结构的一些属性,比如函数的MethodIdx,CodeItem在dex文件中的偏移,函数的AccessFlag等。

void ClassLinker::LoadMethod(const DexFile& dex_file,
                            const ClassDataItemIterator& it,
                            Handle<mirror::Class> klass,
                            ArtMethod* dst){
   uint32_t dex_method_idx = it.GetMemberIndex();
   const DexFile::MethodId& method_id = dex_file.GetMethodId(dex_method_idx);
   const char* method_name = dex_file.StringDataByIdx(method_id.name_idx_);

ScopedAssertNoThreadSuspension ants("LoadMethod");
   dst->SetDexMethodIndex(dex_method_idx);
   dst->SetDeclaringClass(klass.Get());
   dst->SetCodeItemOffset(it.GetMethodCodeItemOffset());

dst->SetDexCacheResolvedMethods(klass->GetDexCache()->GetResolvedMethods(), image_pointer_size_);

uint32_t access_flags = it.GetMethodAccessFlags();

......

dst->SetAccessFlags(access_flags);
}

延申:ArtMethod是存储Java函数在虚拟机内相关信息的结构,它不同于mirror命名空间下的Method类,ArtMethod在Java层没有类与之直接映射。

art\runtime\class_linker.cc::LinkCode

LinkCode函数主要功能是判断代码是否编译从而为函数设置入口代码。

static void LinkCode(ClassLinker* class_linker,
                    ArtMethod* method,
                    const OatFile::OatClass* oat_class,
                    uint32_t class_def_method_index){
 Runtime* const runtime = Runtime::Current();
 if (runtime->IsAotCompiler()) {
   // The following code only applies to a non-compiler runtime.
   return;
 }
 // Method shouldn't have already been linked.
 DCHECK(method->GetEntryPointFromQuickCompiledCode() == nullptr);
 if (oat_class != nullptr) {
   // Every kind of method should at least get an invoke stub from the oat_method.
   // non-abstract methods also get their code pointers.
   const OatFile::OatMethod oat_method = oat_class->GetOatMethod(class_def_method_index);
   oat_method.LinkMethod(method);
 }

// Install entry point from interpreter.
 const void* quick_code = method->GetEntryPointFromQuickCompiledCode();
 bool enter_interpreter = class_linker->ShouldUseInterpreterEntrypoint(method, quick_code);

if (!method->IsInvokable()) {
   EnsureThrowsInvocationError(class_linker, method);
   return;
 }

if (method->IsStatic() && !method->IsConstructor()) {
   // For static methods excluding the class initializer, install the trampoline.
   // It will be replaced by the proper entry point by ClassLinker::FixupStaticTrampolines
   // after initializing class (see ClassLinker::InitializeClass method).
   method->SetEntryPointFromQuickCompiledCode(GetQuickResolutionStub());
 } else if (quick_code == nullptr && method->IsNative()) {
   method->SetEntryPointFromQuickCompiledCode(GetQuickGenericJniStub());
 } else if (enter_interpreter) {
   // Set entry point from compiled code if there's no code or in interpreter only mode.
   method->SetEntryPointFromQuickCompiledCode(GetQuickToInterpreterBridge());
 }

if (method->IsNative()) {
   // Unregistering restores the dlsym lookup stub.
   method->UnregisterNative();

if (enter_interpreter || quick_code == nullptr) {
     // We have a native method here without code. Then it should have either the generic JNI
     // trampoline as entrypoint (non-static), or the resolution trampoline (static).
     // TODO: this doesn't handle all the cases where trampolines may be installed.
     const void* entry_point = method->GetEntryPointFromQuickCompiledCode();
     DCHECK(class_linker->IsQuickGenericJniStub(entry_point) ||
            class_linker->IsQuickResolutionStub(entry_point));
   }
 }
}

来源:https://www.cnblogs.com/luoyesiqiu/p/classload.html

标签:Android,类,加载
0
投稿

猜你喜欢

  • C#实现json格式转换成对象并更换key的方法

    2021-12-23 00:09:39
  • SpringBoot整合Shiro两种方式(总结)

    2021-09-08 21:28:24
  • c# Linq查询详解

    2023-05-23 20:43:50
  • Spring Bean生命周期之BeanDefinition的合并过程详解

    2023-11-29 02:50:35
  • Java字符判断的小例子

    2023-08-26 17:29:49
  • java文件重命名(文件批量重命名)实例程序代码分享

    2023-07-20 06:45:06
  • Android ImageView绘制圆角效果

    2023-11-22 22:59:15
  • Android App中实现可以双击放大和缩小图片功能的实例

    2023-04-01 16:41:17
  • Java运行时数据区概述详解

    2023-10-08 07:00:10
  • Android时间对话框TimePickerDialog详解

    2022-08-23 05:50:06
  • Handler实现倒计时功能

    2021-06-01 15:24:31
  • java9中gc log参数迁移

    2022-06-28 03:21:01
  • Java数据结构及算法实例:冒泡排序 Bubble Sort

    2022-10-17 08:39:45
  • Android App中ViewPager与Fragment结合的一些问题解决

    2023-08-02 22:22:43
  • C# Memcached缓存用法实例详解

    2023-06-29 07:27:33
  • java guava主要功能介绍及使用心得总结

    2023-07-30 11:01:54
  • Android使用线程获取网络图片的方法

    2023-05-28 22:29:28
  • Java数据结构之链表相关知识总结

    2023-11-02 00:29:28
  • C#实现图片加相框的方法

    2022-06-02 13:52:28
  • C++多重继承二义性原理实例解析

    2021-10-08 20:18:33
  • asp之家 软件编程 m.aspxhome.com