Android的类加载浅析

Posted xuguoli_beyondboy

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android的类加载浅析相关的知识,希望对你有一定的参考价值。

类加载流程

在周志明写的<<深入理解java虚拟机的一本书中>>已经详细地介绍java加载类过程,在HotSpot虚拟机实现中是通过双亲委派机制来加载类的,那么android又如何实现呢?在android系统中,有两种常见的加载器实现:DexClassLoaderPathClassLoader,其都继承了BaseDexClassLoader,而BaseDexClassLoader也继承了java的ClassLoader,那么这上面两种实现有什么差异呢?
其差异可根据官方文档解释:
DexClassLoader:A class loader that loads classes from .jar and .apk files containing a classes.dex entry. This can be used to execute code not installed as part of an application.
PathClassLoader:Provides a simple ClassLoader implementation that operates on a list of files and directories in the local file system, but does not attempt to load classes from the network. Android uses this class for its system class loader and for its application class loader(s).
通过查看这两个源码发现,发现当前类的代码非常短,而具体细节实现都交给了BaseDexClassLoader,既然这样,我就从BaseDexClassLoader源码中去看其如何实现:

/**
 * Base class for common functionality between various dex-based
 * @link ClassLoader implementations.
 */
public class BaseDexClassLoader extends ClassLoader 
    private final DexPathList pathList;

    /**
     * Constructs an instance.
     *
     * @param dexPath the list of jar/apk files containing classes and
     * resources, delimited by @code File.pathSeparator, which
     * defaults to @code ":" on Android
     * @param optimizedDirectory directory where optimized dex files
     * should be written; may be @code null
     * @param librarySearchPath the list of directories containing native
     * libraries, delimited by @code File.pathSeparator; may be
     * @code null
     * @param parent the parent class loader
     */
    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String librarySearchPath, ClassLoader parent) 
        super(parent);
        this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory);
    

    @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;
    

    /**
     * @hide
     */
    public void addDexPath(String dexPath) 
        pathList.addDexPath(dexPath, null /*optimizedDirectory*/);
    

    @Override
    protected URL findResource(String name) 
        return pathList.findResource(name);
    

    @Override
    protected Enumeration<URL> findResources(String name) 
        return pathList.findResources(name);
    

    @Override
    public String findLibrary(String name) 
        return pathList.findLibrary(name);
    

    /**
     * Returns package information for the given package.
     * Unfortunately, instances of this class don't really have this
     * information, and as a non-secure @code ClassLoader, it isn't
     * even required to, according to the spec. Yet, we want to
     * provide it, in order to make all those hopeful callers of
     * @code myClass.getPackage().getName() happy. Thus we construct
     * a @code Package object the first time it is being requested
     * and fill most of the fields with dummy values. The @code
     * Package object is then put into the @code ClassLoader's
     * package cache, so we see the same one next time. We don't
     * create @code Package objects for @code null arguments or
     * for the default package.
     *
     * <p>There is a limited chance that we end up with multiple
     * @code Package objects representing the same package: It can
     * happen when when a package is scattered across different JAR
     * files which were loaded by different @code ClassLoader
     * instances. This is rather unlikely, and given that this whole
     * thing is more or less a workaround, probably not worth the
     * effort to address.
     *
     * @param name the name of the class
     * @return the package information for the class, or @code null
     * if there is no package information available for it
     */
    @Override
    protected synchronized Package getPackage(String name) 
        if (name != null && !name.isEmpty()) 
            Package pack = super.getPackage(name);

            if (pack == null) 
                pack = definePackage(name, "Unknown", "0.0", "Unknown",
                        "Unknown", "0.0", "Unknown", null);
            

            return pack;
        

        return null;
    

    /**
     * @hide
     */
    public String getLdLibraryPath() 
        StringBuilder result = new StringBuilder();
        for (File directory : pathList.getNativeLibraryDirectories()) 
            if (result.length() > 0) 
                result.append(':');
            
            result.append(directory);
        

        return result.toString();
    

    @Override public String toString() 
        return getClass().getName() + "[" + pathList + "]";
    

原来BaseDexClassLoader只暴露一些信息接口,而具体实现交给了ClassLoaderDexPathList这两个类,但从ClassLoader来看,其加载机制的实现也遵循双亲委派机制,对于一个android类的加载过程,我们最关注就是类加载以及类查找两个实现方法,其中类的加载方法实现在ClassLoader中,核心代码:

 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) 
                long t0 = System.nanoTime();
                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.
                    long t1 = System.nanoTime();
                    //调用当前加载器去加载,不过findclass()如何加载还要看具体子类的实现
                    c = findClass(name);
                    // this is the defining class loader; record the stats
                
            
            return c;
    

这里至始至终还不知道当前的应用类如何被加载,不过这里还有一个关键方法还没有跟踪剖析就是findClass(),从上面代码中,我们知道安卓有个BaseDexClassLoader加载器父类,它里面却通过了DexPathList类的findClass来实现加载和查询一个应用类的过程,因此需要进一步剖析DexPathListfindClass(),其分析如下:
/libcore/dalvik/src/main/java/dalvik/system/DexPathList.java:

    /**
     * Finds the named class in one of the dex files pointed at by
     * this instance. This will find the one in the earliest listed
     * path element. If the class is found but has not yet been
     * defined, then this method will define it in the defining
     * context that this instance was constructed with.
     *
     * @param name of class to find
     * @param suppressed exceptions encountered whilst finding the class
     * @return the named class or @code null if the class is not
     * found in any of the dex files
     */
    public Class findClass(String name, List<Throwable> suppressed) 
    //遍历dex优化文件
        for (Element element : dexElements) 
            DexFile dex = element.dexFile;
            if (dex != null) 
            //转交给DexFile类去处理类的加载过程
                Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
                if (clazz != null) 
                    return clazz;
                
            
        
        if (dexElementsSuppressedExceptions != null) 
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        
        return null;
    

/libcore/dalvik/src/main/java/dalvik/system/DexFile.java

  /**
     * See @link #loadClass(String, ClassLoader).
     *
     * This takes a "binary" class name to better match ClassLoader semantics.
     *
     * @hide
     */
    public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) 
        return defineClass(name, loader, mCookie, this, suppressed);
    

    private static Class defineClass(String name, ClassLoader loader, Object cookie,
                                     DexFile dexFile, List<Throwable> suppressed) 
        Class result = null;
        try 
        //调用jni层DexFile类的defineClassNative()去处理类的加载
            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

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 = dex_file->FindClassDef(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)));
      class_linker->RegisterDexFile(*dex_file, class_loader.Get());
  //处理加载类
      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

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);

  // Load the class from the dex file.
  if (UNLIKELY(!init_done_)) 
    // finish up init of hand crafted class_roots_
    if (strcmp(descriptor, "Ljava/lang/Object;") == 0) 
      klass.Assign(GetClassRoot(kJavaLangObject));
     else if (strcmp(descriptor, "Ljava/lang/Class;") == 0) 
      klass.Assign(GetClassRoot(kJavaLangClass));
     else if (strcmp(descriptor, "Ljava/lang/String;") == 0) 
      klass.Assign(GetClassRoot(kJavaLangString));
     else if (strcmp(descriptor, "Ljava/lang/ref/Reference;") == 0) 
      klass.Assign(GetClassRoot(kJavaLangRefReference));
     else if (strcmp(descriptor, "Ljava/lang/DexCache;") == 0) 
      klass.Assign(GetClassRoot(kJavaLangDexCache));
    
   
  if (klass.Get() == nullptr) 
    // Allocate a class with the status of not ready.
    // Interface object should get the right size here. Regular class will
    // figure out the right size later and be replaced with one of the right
    // size when the class becomes resolved.    
    klass.Assign(AllocClass(self, SizeOfClassWithoutEmbeddedTables(dex_file, dex_class_def)));
  
  if (UNLIKELY(klass.Get() == nullptr)) 
    self->AssertPendingOOMException();
    return nullptr;
  
  mirror::DexCache* dex_cache = RegisterDexFile(dex_file, class_loader.Get());
  if (dex_cache == nullptr) 
    self->AssertPendingOOMException();
    return nullptr;
  
  klass->SetDexCache(dex_cache);
  SetupClass(dex_file, dex_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());

  // Add the newly loaded class to the loaded classes table.
  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, dex_file, dex_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::kStatusError, self);
    
    return nullptr;
  

  // Finish loading (if necessary) by finding parents
  CHECK(!klass->IsLoaded());
  if (!LoadSuperAndInterfaces(klass, dex_file)) 
    // Loading failed.
    if (!klass->IsErroneous()) 
      mirror::Class::SetStatus(klass, mirror::Class::kStatusError, self);
    
    return nullptr;
  
  CHECK(klass->IsLoaded());
  // 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::kStatusError, self);
    
    return nullptr;
  
  self->AssertNoPendingException();
  CHECK(h_new_class.Get() != nullptr) << descriptor;
  CHECK(h_new_class->IsResolved()) << 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.
   */
  Dbg::PostClassPrepare(h_new_class.Get());

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

  return h_new_class.Get();

//加载类
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
  
  bool has_oat_class = false;
  if (Runtime::Current()->IsStarted() && !Runtime::Current()->IsAotCompiler()) 
    OatFile::OatClass oat_class = FindOatClass(dex_file, klass->GetDexClassDefIndex(),
                                               &has_oat_class);
    if (has_oat_class) 
      LoadClassMembers(self, dex_file, class_data, klass, &oat_class);
    
  
  if (!has_oat_class) 
    LoadClassMembers(self, dex_file, class_data, klass, nullptr);
  

原来加载类是由class_linker去具体实现了,在android中,一般通过getClassLoader()获取系统加载器,其系统加载器创建是在ClassLoader中创建,核心代码:

 /**
     * Encapsulates the set of parallel capable loader types.
     */
    private static ClassLoader createSystemClassLoader() 
        String classPath = System.getProperty("java.class.path", ".");
        String librarySearchPath = System.getProperty("java.library.path", "");
          return new PathClassLoader(classPath, librarySearchPath, BootClassLoader.getInstance());
    
    //通过ClassLoader静态内部类创建系统加载器
    static private class SystemClassLoader 
        public static ClassLoader loader = ClassLoader.createSystemClassLoader();
    
    @CallerSensitive
    public static ClassLoader getSystemClassLoader() 
        return SystemClassLoader.loader;
    

从上面知道,android获取到的系统类加载器是PathClassLoader,而加载类方法最终实现是在class_linker

加载寄主APK的类

现在我来做个实验就是如何用DexClassLoader去加载寄主APK的类,这里暂时不考虑资源和组件的加载,因为想要解决这些还需要考虑资源如何加载,由于这篇主要是围绕着类加载展开,因此就不去涉及资源的加载,核心代码如下:

//定义一个接口让寄住和宿主APK共享,以解决无法直接通过引用地方式直接调用方法
  public interface IDynamic 
    String getTip();
    Drawable getDrawable(Resources res, @DrawableRes int id);
    String getString(Resources res);
    public void startPluginActivity(Context context,Class<?> cls);
    public void startPluginActivity(Context context);


//寄主APK对其接口的实现
public class DynamicClass1 implements IDynamic 

    public DynamicClass1() 
       // Log.i(DynamicClass1.class.getName(),"加载动态类");
    
    public String getTip()
        return "插件动态加载";
    

    @Override
    public Drawable getDrawable(Resources resources, @DrawableRes int i) 
        return null;
    

    @Override
    public String getString(Resources resources) 
        return resources.getString(R.string.test);
    

    @Override
    public void startPluginActivity(Context context, Class<?> Cls) 
        Intent   intent = new Intent(context,Cls);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        context.startActivity(intent);
    

    @Override
    public void startPluginActivity(Context context) 
        Intent   intent = new Intent(context,DynamicActivity.class);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        context.startActivity(intent);
    

    @Override
    public String toString() 
        return "插件";
    


//宿主APK从Assets拷贝dex到dirpath路径
    private void copyFileToDexDir(Context context, String path, String dirpath, String apkName) 
        InputStream inputStream;
        BufferedInputStream buffer = null;
        File file;
        FileOutputStream out = null;
        try 
            inputStream = context.getAssets().open(path);
            buffer = new BufferedInputStream(inputStream);
            byte[] content = new byte[1024];
            file = new File(dirpath, apkName);
            out = new FileOutputStream(file);
            int length = -1;
            while ((length = buffer.read(content)) != -1) 
                out.write(content, 0, length);
            
         catch (Exception e) 
            e.printStackTrace();
         finally 
            StreamUtil.close(buffer);
            StreamUtil.close(out);
        
    

//宿主APK加载寄主APK的DynamicClass1类
    private void dnyLoadPluCls() 
        String path = "plugindemo1-debug.apk";
        String dir = getFilesDir().getAbsolutePath();
        copyFileToDexDir(this, path, dir, path);
        String packName = "com.scau.beyondboy.plugindemo1";
        String apkPath = dir + File.separator + path;
        //获取当前应用dex输出目录
        String dexOutputDir = getApplicationInfo().dataDir;
        String libPath = getApplicationInfo().nativeLibraryDir;
        loadDynRes(apkPath);
        DexClassLoader classLoader = new DexClassLoader(apkPath, dexOutputDir, libPath, this.getClassLoader());
        try 
            Class<?> clazz = classLoader.loadClass(packName + ".DynamicClass1");
            IDynamic object = (IDynamic) clazz.newInstance();
            String showTip = object.getTip();
            Toast.makeText(this, showTip, Toast.LENGTH_SHORT).show();
         catch (Exception e) 
            e.printStackTrace();
        
    

在这次实验中,发现有个细节的地方,就是寄主APK和宿主APK依赖动态加载接口时,配置需要如下配置(才能兼容Android各个版本):

//宿主APK依赖配置
 compile files('libs/plugininterface.jar')
//寄主APK依赖配置
provided files('libs/plugininterface.jar')

运行效果如图:

Demo地址:https://github.com/scau-beyondboy/ClassLoadDemo

以上是关于Android的类加载浅析的主要内容,如果未能解决你的问题,请参考以下文章

Android ClassLoader浅析

浅析ClassLoader

Activity插件化解决方案

插件化开发详解

android studio for android learning (二十七) UI控件动态加载机制浅析

浅析类加载