Android运行时ART加载类和方法的过程分析

发表于 5年以前  | 总阅读数:3520 次

在前一篇文章中,我们通过分析OAT文件的加载过程,认识了OAT文件的格式,其中包含了原始的DEX文件。既然ART运行时执行的都是翻译DEX字节码后得到的本地机器指令了,为什么还需要在OAT文件中包含DEX文件,并且将它加载到内存去呢?这是因为ART运行时提供了Java虚拟机接口,而要实现Java虚拟机接口不得不依赖于DEX文件。本文就通过分析ART运行时加载类及其方法的过程来理解DEX文件的作用。

《Android系统源代码情景分析》一书正在进击的程序员网(http://0xcc0xcd.com)中连载,点击进入!

在前面Android运行时ART加载OAT文件的过程分析这篇文章的最后,我们简单总结了ART运行时查找类方法的本地机器指令的过程,如图1所示:

图1 ART运行时查找类方法的本地机器指令的过程

为了方便描述,我们将DEX文件中描述的类和方法称为DEX类(Dex Class)和DEX方法(Dex Method),而将在OAT文件中描述的类和方法称为OAT类(Oat Class)和OAT方法(Oat Method)。接下来我们还会看到,ART运行时在内部又会使用另外两个不同的术语来描述类和方法,其中将类描述为Class,而将类方法描述为ArtMethod。

在图1中,为了找到一个类方法的本地机器指令,我们需要执行以下的操作:

1. 在DEX文件中找到目标DEX类的编号,并且以这个编号为索引,在OAT文件中找到对应的OAT类。

2. 在DEX文件中找到目标DEX方法的编号,并且以这个编号为索引,在上一步找到的OAT类中找到对应的OAT方法。

3. 使用上一步找到的OAT方法的成员变量begin_和codeoffset,计算出该方法对应的本地机器指令。

通过前面Android运行时ART简要介绍和学习计划一文的学习,我们可以知道,ART运行时的入口是com.android.internal.os.ZygoteInit类的静态成员函数main,如下所示:

void AndroidRuntime::start(const char* className, const char* options)    
    {    
        ......    

        /* start the virtual machine */    
        JniInvocation jni_invocation;    
        jni_invocation.Init(NULL);    
        JNIEnv* env;    
        if (startVm(&mJavaVM, &env) != 0) {    
            return;    
        }    

        ......    

        /*  
         * Start VM.  This thread becomes the main thread of the VM, and will  
         * not return until the VM exits.  
         */    
        char* slashClassName = toSlashClassName(className);    
        jclass startClass = env->FindClass(slashClassName);    
        if (startClass == NULL) {    
            ALOGE("JavaVM unable to locate class '%s'\n", slashClassName);    
            /* keep going */    
        } else {    
            jmethodID startMeth = env->GetStaticMethodID(startClass, "main",    
                                                     "([Ljava/lang/String;)V");    
            if (startMeth == NULL) {    
                ALOGE("JavaVM unable to find main() in '%s'\n", className);    
                /* keep going */    
            } else {    
                env->CallStaticVoidMethod(startClass, startMeth, strArray);    
                ......    
            }    
        }    

        ......    
    }    

这个函数定义在文件frameworks/base/core/jni/AndroidRuntime.cpp中。

在AndroidRuntime类的成员函数start中,首先是通过调用函数startVm创建了一个Java虚拟机mJavaVM及其JNI接口env。这个Java虚拟机实际上就是ART运行时。在接下来的描述中,我们将不区分ART虚拟机和ART运行时,并且认为它们表达的是同一个概念。获得了ART虚拟机的JNI接口之后,就可以通过它提供的函数FindClass和GetStaticMethodID来加载com.android.internal.os.ZygoteInit类及其静态成员函数main。于是,最后就可以再通过JNI接口提供的函数CallStaticVoidMethod来调用com.android.internal.os.ZygoteInit类的静态成员函数main,以及进行到ART虚拟机里面去运行。

接下来,我们就通过分析JNI接口FindClass和GetStaticMethodID的实现,以便理解ART运行时是如何查找到指定的类和方法的。在接下来的一篇文章中,我们再分析ART运行时是如何通过JNI接口CallStaticVoidMethod来执行指定类方法的本地机器指令的。

在分析JNI接口FindClass和GetStaticMethodID的实现之前,我们先要讲清楚JNI接口是如何创建的。从前面Android运行时ART加载OAT文件的过程分析一文可以知道,与ART虚拟机主线程关联的JNI接口是在函数JNI_CreateJavaVM中创建的,如下所示:

extern "C" jint JNI_CreateJavaVM(JavaVM** p_vm, JNIEnv** p_env, void* vm_args) {  
      ......

      *p_env = Thread::Current()->GetJniEnv();  
      ......

      return JNI_OK;  
    }

这个函数定义在文件art/runtime/jni_internal.cc中。

调用Thread类的静态成员函数Current获得的是用来描述当前线程(即ART虚拟机的主线程)的一个Thread对象,再通过调用这个Thread对象的成员函数GetJniEnv就获得一个JNI接口,并且保存在输出参数p_env中。

Thread类的成员函数GetJniEnv的实现如下所示:

class PACKED(4) Thread {
     public:
       ......

       // JNI methods
       JNIEnvExt* GetJniEnv() const {
         return jni_env_;
       }

       ......

     private:
       ......

      // Every thread may have an associated JNI environment
      JNIEnvExt* jni_env_;

      ......
    };  

这个函数定义在文件art/runtime/thread.h中。

Thread类的成员函数GetJniEnv返回的是成员变量jni_env_指向的一个JNIEnvExt对象。

JNIEnvExt类是从JNIEnv类继承下来的,如下所示:

struct JNIEnvExt : public JNIEnv {
      ......
    };

这个类定义在文件art/runtime/jni_internal.h。

JNIEnv类定义了JNI接口,如下所示:

typedef _JNIEnv JNIEnv;
    ......

    struct _JNIEnv {
        /* do not rename this; it does not seem to be entirely opaque */
        const struct JNINativeInterface* functions;
        ......

        jint GetVersion()
        { return functions->GetVersion(this); }

        ......
    };

这个类定义在文件libnativehelper/include/nativehelper/jni.h中。

在JNIEnv类中,最重要的就是成员变量functions了,它指向的是一个类型为JNINativeInterface的JNI函数表。所有的JNI接口调用都是通过这个JNI函数表来实现的。例如,用来获得版本号的JNI接口GetVersion就是通过调用JNI函数表中的GetVersion函数来实现的。

那么,上述的JNI函数表是如何创建的呢?通过JNIEnvExt类的构造函数可以知道答案,如下所示:

JNIEnvExt::JNIEnvExt(Thread* self, JavaVMExt* vm)
        : ...... {
      functions = unchecked_functions = &gJniNativeInterface;
      ......
    }

这个函数定义在文件art/runtime/jni_internal.cc中。

JNIEnvExt类的构造函数将父类JNIEnv的成员变量functions初始化为全局变量gJniNativeInterface。也就是说,JNI函数表实际是由全局变量gJniNativeInterface来描述的。

全局变量gJniNativeInterface的定义如下所示:

const JNINativeInterface gJniNativeInterface = {
      NULL,  // reserved0.
      NULL,  // reserved1.
      NULL,  // reserved2.
      NULL,  // reserved3.
      JNI::GetVersion,
      ......
      JNI::FindClass,
      ......
      JNI::GetStaticMethodID,
      ......
      JNI::CallStaticVoidMethod,
      ......
    };

这个全局变量定义在文件art/runtime/jni_internal.cc中。

从这里可以看出,JNI函数表实际上是由JNI类的静态成员函数组成的。例如,JNI函数GetVersion是由JNI类的静态成员函数GetVersion来实现的。理解了这一点之后,我们就轻松地知道同接下来我们要分析的JNI接口FindClass和GetStaticMethodID分别是由JNI类的静态成员函数FindClass和GetStaticMethodID来实现的。事实上,如果读者看过Dalvik虚拟机的启动过程分析这篇文章,那么对上述的JNI接口定义是一目了然的。

JNI类的静态成员函数FindClass的实现如下所示:

class JNI {
     public:
      ......

      static jclass FindClass(JNIEnv* env, const char* name) {
        CHECK_NON_NULL_ARGUMENT(FindClass, name);
        Runtime* runtime = Runtime::Current();
        ClassLinker* class_linker = runtime->GetClassLinker();
        std::string descriptor(NormalizeJniClassDescriptor(name));
        ScopedObjectAccess soa(env);
        Class* c = NULL;
        if (runtime->IsStarted()) {
          ClassLoader* cl = GetClassLoader(soa);
          c = class_linker->FindClass(descriptor.c_str(), cl);
        } else {
          c = class_linker->FindSystemClass(descriptor.c_str());
        }
        return soa.AddLocalReference<jclass>(c);
      }

      ......
    };

这个函数定义在文件art/runtime/jni_internal.cc中。

在ART虚拟机进程中,存在着一个Runtime单例,用来描述ART运行时。通过调用Runtime类的静态成员函数Current可以获得上述Runtime单例。获得了这个单例之后,就可以调用它的成员函数GetClassLinker来获得一个ClassLinker对象。从前面Android运行时ART加载OAT文件的过程分析一文可以知道。上述ClassLinker对象是在创建ART虚拟机的过程中创建的,用来加载类以及链接类方法。

JNI类的静态成员函数FindClass首先是判断ART运行时是否已经启动起来。如果已经启动,那么就通过调用函数GetClassLoader来获得当前线程所关联的ClassLoader,并且以此为参数,调用前面获得的ClassLinker对象的成员函数FindClass来加载由参数name指定的类。一般来说,当前线程所关联的ClassLoader就是当前正在执行的类方法所关联的ClassLoader,即用来加载当前正在执行的类的ClassLoader。如果ART虚拟机还没有开始执行类方法,就像我们现在这个场景,那么当前线程所关联的ClassLoader实际上就系统类加载器,即SystemClassLoader。

如果ART运行时还没有启动,那么这时候只可以加载系统类。这个通过前面获得的ClassLinker对象的成员函数FindSystemClass来实现的。在我们这个场景中,ART运行时已经启动,因此,接下来我们就继续分析ClassLinker类的成员函数FindClass的实现。

ClassLinker类的成员函数FindClass的实现如下所示:

mirror::Class* ClassLinker::FindClass(const char* descriptor, mirror::ClassLoader* class_loader) {
      ......
      Thread* self = Thread::Current();
      ......

      // Find the class in the loaded classes table.
      mirror::Class* klass = LookupClass(descriptor, class_loader);
      if (klass != NULL) {
        return EnsureResolved(self, klass);
      }
      // Class is not yet loaded.
      if (descriptor[0] == '[') {
        ......
      } else if (class_loader == NULL) {
        DexFile::ClassPathEntry pair = DexFile::FindInClassPath(descriptor, boot_class_path_);
        if (pair.second != NULL) {
          return DefineClass(descriptor, NULL, *pair.first, *pair.second);
        }
      } else if (Runtime::Current()->UseCompileTimeClassPath()) {
        ......
      } else {
        ScopedObjectAccessUnchecked soa(self->GetJniEnv());
        ScopedLocalRef<jobject> class_loader_object(soa.Env(),
                                                    soa.AddLocalReference<jobject>(class_loader));
        std::string class_name_string(DescriptorToDot(descriptor));
        ScopedLocalRef<jobject> result(soa.Env(), NULL);
        {
          ScopedThreadStateChange tsc(self, kNative);
          ScopedLocalRef<jobject> class_name_object(soa.Env(),
                                                    soa.Env()->NewStringUTF(class_name_string.c_str()));
          if (class_name_object.get() == NULL) {
            return NULL;
          }
          CHECK(class_loader_object.get() != NULL);
          result.reset(soa.Env()->CallObjectMethod(class_loader_object.get(),
                                                   WellKnownClasses::java_lang_ClassLoader_loadClass,
                                                   class_name_object.get()));
        }
        if (soa.Self()->IsExceptionPending()) {
          // If the ClassLoader threw, pass that exception up.
          return NULL;
        } else if (result.get() == NULL) {
          // broken loader - throw NPE to be compatible with Dalvik
          ThrowNullPointerException(NULL, StringPrintf("ClassLoader.loadClass returned null for %s",
                                                       class_name_string.c_str()).c_str());
          return NULL;
        } else {
          // success, return mirror::Class*
          return soa.Decode<mirror::Class*>(result.get());
        }
      }

      ThrowNoClassDefFoundError("Class %s not found", PrintableString(descriptor).c_str());
      return NULL;
    }

这个函数定义在文件art/runtime/class_linker.cc中。

参数descriptor指向的是要加载的类的签名,而参数class_loader指向的是一个类加载器,我们假设它的值不为空,并且指向系统类加载器。

ClassLinker类的成员函数FindClass首先是调用另外一个成员函数LookupClass来检查参数descriptor指定的类是否已经被加载过。如果是的话,那么ClassLinker类的成员函数LookupClass就会返回一个对应的Class对象,这个Class对象接着就会返回给调用者,表示加载已经完成。

如果参数descriptor指定的类还没有被加载过,这时候主要就是要看参数class_loader的值了。如果参数class_loader的值等于NULL,那么就需要调用DexFile类的静态FindInClassPath来在系统启动类路径寻找对应的类。一旦寻找到,那么就会获得包含目标类的DEX文件,因此接下来就调用ClassLinker类的另外一个成员函数DefineClass从获得的DEX文件中加载参数descriptor指定的类了。

如果参数class_loader的值不等于NULL,也就是说ClassLinker类的成员函数FindClass的调用者指定了类加载器,那么就通过该类加载器来加载参数descriptor指定的类。每一个类加载器在Java层都对应有一个java.lang.ClassLoader对象。通过调用这个java.lang.ClassLoader类的成员函数loadClass即可加载指定的类。在我们这个场景中,上述的java.lang.ClassLoader类是一个系统类加载器,它负责加载系统类。而我们当前要加载的类为com.android.internal.os.ZygoteInit,它属于一个系统类。

系统类加载器在加载系统类实际上也是通过JNI方法调用ClassLinker类的成员函数FindClass来实现的。只不过这时候传进来的参数class_loader是一个NULL值。这样,ClassLinker类的成员函数FindClass就会在系统启动类路径中寻找参数descriptor指定的类可以在哪一个DEX文件加载,这是通过调用DexFile类的静态成员函数FindInClassPath来实现的。

所谓的系统启动类路径,其实就是一系列指定的由系统提供的DEX文件,这些DEX文件保存在ClassLinker类的成员变量boot_class_path_描述的一个向量中。那么问题就来了,这些DEX文件是怎么来的呢?我们知道,在ART运行时中,我们使用的是OAT文件。如果看过前面Android运行时ART加载OAT文件的过程分析这篇文章,就会很容易知道,OAT文件里面包含有DEX文件。而且ART运行时在启动的时候,会加载一个名称为system@framework@boot.art@classes.oat的OAT文件。这个OAT文件包含有多个DEX文件,每一个DEX文件都是一个系统启动类路径,它们会被添加到ClassLinker类的成员变量boot_class_path_描述的向量中去。

这里调用DexFile类的静态成员函数FindInClassPath,实际要完成的工作就是从ClassLinker类的成员变量boot_class_path_描述的一系列的DEX文件中检查哪一个DEX文件包含有参数descriptor指定的类。这可以通过解析DEX文件来实现,关于DEX文件的格式,可以参考官方文档:http://source.android.com/tech/dalvik/index.html

知道了参数descriptor指定的类定义在哪一个DEX文件之后,就可以通过ClassLinker类的另外一个成员函数DefineClass来从中加载它了。接下来,我们就继续分析ClassLinker类的成员函数DefineClass的实现,如下所示:

mirror::Class* ClassLinker::DefineClass(const char* descriptor,
                                            mirror::ClassLoader* class_loader,
                                            const DexFile& dex_file,
                                            const DexFile::ClassDef& dex_class_def) {
      Thread* self = Thread::Current();
      SirtRef<mirror::Class> klass(self, NULL);
      // 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.reset(GetClassRoot(kJavaLangObject));
        } else if (strcmp(descriptor, "Ljava/lang/Class;") == 0) {
          klass.reset(GetClassRoot(kJavaLangClass));
        } else if (strcmp(descriptor, "Ljava/lang/String;") == 0) {
          klass.reset(GetClassRoot(kJavaLangString));
        } else if (strcmp(descriptor, "Ljava/lang/DexCache;") == 0) {
          klass.reset(GetClassRoot(kJavaLangDexCache));
        } else if (strcmp(descriptor, "Ljava/lang/reflect/ArtField;") == 0) {
          klass.reset(GetClassRoot(kJavaLangReflectArtField));
        } else if (strcmp(descriptor, "Ljava/lang/reflect/ArtMethod;") == 0) {
          klass.reset(GetClassRoot(kJavaLangReflectArtMethod));
        } else {
          klass.reset(AllocClass(self, SizeOfClass(dex_file, dex_class_def)));
        }
      } else {
        klass.reset(AllocClass(self, SizeOfClass(dex_file, dex_class_def)));
      }
      ......
      LoadClass(dex_file, dex_class_def, klass, class_loader);
      ......
      {
        // Add the newly loaded class to the loaded classes table.
        mirror::Class* existing = InsertClass(descriptor, klass.get(), Hash(descriptor));
        if (existing != NULL) {
          // We failed to insert because we raced with another thread. Calling EnsureResolved may cause
          // this thread to block.
          return EnsureResolved(self, existing);
        }
      }
      ......
      if (!LinkClass(klass, NULL, self)) {
        // Linking failed.
        klass->SetStatus(mirror::Class::kStatusError, self);
        return NULL;
      }
      ......
      return klass.get();
    }

这个函数定义在文件art/runtime/class_linker.cc中。

ClassLinker类有一个类型为bool的成员变量initdone,用来表示ClassLinker是否已经初始化完成。ClassLinker在创建的时候,有一个初始化过程,用来创建一些内部类。这些内部类要么是手动创建的,要么是从Image空间获得的。关于ART虚拟机的Image空间,我们在后面分析ART垃圾收集机制的文章中再详细分析。

调用ClassLinker类的成员函数DefineClass的时候,如果ClassLinker正处于初始化过程,即其成员变量init_done_的值等于false,并且参数descriptor描述的是特定的内部类,那么就将本地变量klass指向它们,其余情况则会通过成员函数AllocClass为其分配存储空间,以便后面通过成员函数LoadClass进行初始化。

ClassLinker类的成员函数LoadClass用来从指定的DEX文件中加载指定的类。指定的类从DEX文件中加载完成后,需要通过另外一个成员函数InsertClass添加到ClassLinker的已加载类列表中去。如果指定的类之前已经加载过,即调用成员函数InsertClass得到的返回值不等于空,那么就说明有另外的一个线程也正在加载指定的类。这时候就需要调用成员函数EnsureResolved来保证(等待)该类已经加载并且解析完成。另一方面,如果没有其它线程加载指定的类,那么当前线程从指定的DEX文件加载完成指定的类后,还需要调用成员函数LinkClass来对加载后的类进行解析。最后,一个类型为Class的对象就可以返回给调用者了,用来表示一个已经加载和解析完成的类。

接下来,我们主要分析ClassLinker类的成员函数LoadClass的实现,以便可以了解类的加载过程。

ClassLinker类的成员函数LoadClass的实现如下所示:

void ClassLinker::LoadClass(const DexFile& dex_file,
                                const DexFile::ClassDef& dex_class_def,
                                SirtRef<mirror::Class>& klass,
                                mirror::ClassLoader* class_loader) {
      ......

      klass->SetClassLoader(class_loader);
      ......

      klass->SetDexClassDefIndex(dex_file.GetIndexForClassDef(dex_class_def));
      .....

      // Load fields fields.
      const byte* class_data = dex_file.GetClassData(dex_class_def);
      ......
      ClassDataItemIterator it(dex_file, class_data);
      Thread* self = Thread::Current();
      if (it.NumStaticFields() != 0) {
        mirror::ObjectArray<mirror::ArtField>* statics = AllocArtFieldArray(self, it.NumStaticFields());
        ......
        klass->SetSFields(statics);
      }
      if (it.NumInstanceFields() != 0) {
        mirror::ObjectArray<mirror::ArtField>* fields =
            AllocArtFieldArray(self, it.NumInstanceFields());
        ......
        klass->SetIFields(fields);
      }
      for (size_t i = 0; it.HasNextStaticField(); i++, it.Next()) {
        SirtRef<mirror::ArtField> sfield(self, AllocArtField(self));
        ......
        klass->SetStaticField(i, sfield.get());
        LoadField(dex_file, it, klass, sfield);
      }
      for (size_t i = 0; it.HasNextInstanceField(); i++, it.Next()) {
        SirtRef<mirror::ArtField> ifield(self, AllocArtField(self));
        ......
        klass->SetInstanceField(i, ifield.get());
        LoadField(dex_file, it, klass, ifield);
      }

      UniquePtr<const OatFile::OatClass> oat_class;
      if (Runtime::Current()->IsStarted() && !Runtime::Current()->UseCompileTimeClassPath()) {
        oat_class.reset(GetOatClass(dex_file, klass->GetDexClassDefIndex()));
      }

      // Load methods.
      if (it.NumDirectMethods() != 0) {
        // TODO: append direct methods to class object
        mirror::ObjectArray<mirror::ArtMethod>* directs =
             AllocArtMethodArray(self, it.NumDirectMethods());
        ......
        klass->SetDirectMethods(directs);
      }
      if (it.NumVirtualMethods() != 0) {
        // TODO: append direct methods to class object
        mirror::ObjectArray<mirror::ArtMethod>* virtuals =
            AllocArtMethodArray(self, it.NumVirtualMethods());
        ......
        klass->SetVirtualMethods(virtuals);
      }
      size_t class_def_method_index = 0;
      for (size_t i = 0; it.HasNextDirectMethod(); i++, it.Next()) {
        SirtRef<mirror::ArtMethod> method(self, LoadMethod(self, dex_file, it, klass));
        ......
        klass->SetDirectMethod(i, method.get());
        if (oat_class.get() != NULL) {
          LinkCode(method, oat_class.get(), class_def_method_index);
        }
        method->SetMethodIndex(class_def_method_index);
        class_def_method_index++;
      }
      for (size_t i = 0; it.HasNextVirtualMethod(); i++, it.Next()) {
        SirtRef<mirror::ArtMethod> method(self, LoadMethod(self, dex_file, it, klass));
        ......
        klass->SetVirtualMethod(i, method.get());
        ......
        if (oat_class.get() != NULL) {
          LinkCode(method, oat_class.get(), class_def_method_index);
        }
        class_def_method_index++;
      }
      ......
    }

这个函数定义在文件art/runtime/class_linker.cc中。

我们首先要明确一下各个参数的含义:

dex_file: 类型为DexFile,描述要加载的类所在的DEX文件。

dex_class_def: 类型为ClassDef,描述要加载的类在DEX文件里面的信息。

klass: 类型为Class,描述加载完成的类。

class_loader: 类型为ClassLoader,描述所使用的类加载器。

总的来说,ClassLinker类的成员函数LoadClass的任务就是要用dex_file、dex_class_def、class_loader三个参数包含的相关信息设置到参数klass描述的Class对象去,以便可以得到一个完整的已加载类信息。

ClassLinker类的成员函数LoadClass主要完成的工作如下所示:

1. 将参数class_loader描述的ClassLoader设置到klass描述的Class对象中去,即给每一个已加载类关联一个类加载器。

2. 通过DexFile类的成员函数GetIndexForClassDef获得正在加载的类在DEX文件中的类索引号,并且设置到klass描述的Class对象中去。这个类索引号是一个很重要的信息,因为我们需要通过类索引号在相应的OAT文件找到一个OatClass结构体。有了这个OatClass结构体之后,我们才可以找到类方法对应的本地机器指令。具体可以参考前面图1和Android运行时ART加载OAT文件的过程分析一文。

3. 从参数dex_file描述的DEX文件中获得正在加载的类的静态成员变量和实例成员变量个数,并且为每一个静态成员变量和实例成员变量都分配一个ArtField对象,接着通过ClassLinker类的成员函数LoadField对这些ArtField对象进行初始化。初始好得到的ArtField对象全部保存在klass描述的Class对象中。

4. 调用ClassLinker类的成员函数GetOatClass,从相应的OAT文件中找到与正在加载的类对应的一个OatClass结构体oat_class。这需要利用到上面提到的DEX类索引号,这是因为DEX类和OAT类根据索引号存在一一对应关系。这一点可以参考图1和Android运行时ART加载OAT文件的过程分析一文。

  1. 从参数dex_file描述的DEX文件中获得正在加载的类的直接成员函数和虚拟成员函数个数,并且为每一个直接成员函数和虚拟成员函数都分配一个ArtMethod对象,接着通过ClassLinker类的成员函数LoadMethod对这些ArtMethod对象进行初始化。初始好得到的ArtMethod对象全部保存在klass描述的Class对象中。

  2. 每一个直接成员函数和虚拟成员函数都对应有一个函数索引号。根据这个函数索引号可以在第4步得到的OatClass结构体中找到对应的本地机器指令,具体可以参考前面图1和Android运行时ART加载OAT文件的过程分析一文。所有与这些成员函数关联的本地机器指令信息通过全局函数LinkCode设置到klass描述的Class对象中。

总结来说,参数klass描述的Class对象包含了一系列的ArtField对象和ArtMethod对象,其中,ArtField对象用来描述成员变量信息,而ArtMethod用来描述成员函数信息。

接下来,我们继续分析全局函数LinkCode的实现,以便可以了解如何在一个OAT文件中找到一个DEX类方法的本地机器指令。

函数LinkCode的实现如下所示:

static void LinkCode(SirtRef<mirror::ArtMethod>& method, const OatFile::OatClass* oat_class,
                         uint32_t method_index)
        SHARED_LOCKS_REQUIRED(Locks::mutator_lock_) {
      // Method shouldn't have already been linked.
      DCHECK(method->GetEntryPointFromCompiledCode() == NULL);
      // 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(method_index);
      oat_method.LinkMethod(method.get());

      // Install entry point from interpreter.
      Runtime* runtime = Runtime::Current();
      bool enter_interpreter = NeedsInterpreter(method.get(), method->GetEntryPointFromCompiledCode());
      if (enter_interpreter) {
        method->SetEntryPointFromInterpreter(interpreter::artInterpreterToInterpreterBridge);
      } else {
        method->SetEntryPointFromInterpreter(artInterpreterToCompiledCodeBridge);
      }

      if (method->IsAbstract()) {
        method->SetEntryPointFromCompiledCode(GetCompiledCodeToInterpreterBridge());
        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->SetEntryPointFromCompiledCode(GetResolutionTrampoline(runtime->GetClassLinker()));
      } else if (enter_interpreter) {
        // Set entry point from compiled code if there's no code or in interpreter only mode.
        method->SetEntryPointFromCompiledCode(GetCompiledCodeToInterpreterBridge());
      }

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

      // Allow instrumentation its chance to hijack code.
      runtime->GetInstrumentation()->UpdateMethodsCode(method.get(),
                                                       method->GetEntryPointFromCompiledCode());
    }

这个函数定义在文件art/runtime/class_linker.cc中。

参数method表示要设置本地机器指令的类方法,参数oat_class表示类方法method在OAT文件中对应的OatClass结构体,参数method_index表示类方法method的索引号。

通过参数method_index描述的索引号可以在oat_class表示的OatClass结构体中找到一个OatMethod结构体oat_method。这个OatMethod结构描述了类方法method的本地机器指令相关信息,通过调用它的成员函数LinkMethod可以将这些信息设置到参数method描述的ArtMethod对象中去。如下所示:

const void* OatFile::OatMethod::GetCode() const {
      return GetOatPointer<const void*>(code_offset_);
    }
    ......
    void OatFile::OatMethod::LinkMethod(mirror::ArtMethod* method) const {
      CHECK(method != NULL);
      method->SetEntryPointFromCompiledCode(GetCode());
      method->SetFrameSizeInBytes(frame_size_in_bytes_);
      method->SetCoreSpillMask(core_spill_mask_);
      method->SetFpSpillMask(fp_spill_mask_);
      method->SetMappingTable(GetMappingTable());
      method->SetVmapTable(GetVmapTable());
      method->SetNativeGcMap(GetNativeGcMap());  // Used by native methods in work around JNI mode.
    }

这个函数定义在文件art/runtime/oat_file.cc中。

其中,最重要的就是通过OatMethod类的成员函数GetCode获得OatMethod结构体中的code_offset_字段,并且通过调用ArtMethod类的成员函数SetEntryPointFromCompiledCode设置到参数method描述的ArtMethod对象中去。OatMethod结构体中的code_offset_字段指向的是一个本地机器指令函数,这个本地机器指令函数正是通过翻译参数method描述的类方法的DEX字节码得到的。

回到函数LinkCode中,它接着调用另外一个全局函数NeedsInterpreter检查参数method描述的类方法是否需要通过解释器执行,它的实现如下所示:

// Returns true if the method must run with interpreter, false otherwise.
    static bool NeedsInterpreter(const mirror::ArtMethod* method, const void* code) {
      if (code == NULL) {
        // No code: need interpreter.
        return true;
      }
      ......
      // If interpreter mode is enabled, every method (except native and proxy) must
      // be run with interpreter.
      return Runtime::Current()->GetInstrumentation()->InterpretOnly() &&
             !method->IsNative() && !method->IsProxyMethod();
    }

这个函数定义在文件art/runtime/class_linker.cc中。

在以下两种情况下,一个类方法需要通过解释器来执行:

1. 没有对应的本地机器指令,即参数code的值等于NULL。

2. ART虚拟机运行在解释模式中,并且类方法不是JNI方法,并且也不是代理方法。

调用Runtime类的静态成员函数Current获得的是描述ART运行时的一个Runtime对象。调用这个Runtime对象的成员函数GetInstrumentation获得的是一个Instrumentation对象。这个Instrumentation对象是用来调试ART运行时的,通过调用它的成员函数InterpretOnly可以知道ART虚拟机是否运行在解释模式中。

因为JNI方法是没有对应的DEX字节码的,因此即使ART虚拟机运行在解释模式中,JNI方法也不能通过解释器来执行。至于代理方法,由于是动态生成的(没有对应的DEX字节码),因此即使ART虚拟机运行在解释模式中,它们也不通过解释器来执行(这一点猜测的,还没有确认)。

回到函数LinkCode中,如果调用函数NeedsInterpreter得到的返回值enter_interpreter等于true,那么就意味着参数method描述的类方法需要通过解释器来执行,这时候就将函数artInterpreterToInterpreterBridge设置为解释器执行该类方法的入口点。否则的话,就将另外一个函数artInterpreterToCompiledCodeBridge设置为解释器执行该类方法的入口点。

为什么我们需要为类方法设置解释器入口点呢?根据前面的分析可以知道,在ART虚拟机中,并不是所有的类方法都是有对应的本地机器指令的,并且即使一个类方法有对应的本地机器指令,当ART虚拟机以解释模式运行时,它也需要通过解释器来执行。当以解释器执行的类方法在执行的过程中调用了其它的类方法时,解释器就需要进一步知道被调用的类方法是应用以解释方式执行,还是本地机器指令方法执行。为了能够进行统一处理,就给每一个类方法都设置一个解释器入口点。需要通过解释执行的类方法的解释器入口点函数是artInterpreterToInterpreterBridge,它会继续通过解释器来执行该类方法。需要通过本地机器指令执行的类方法的解释器入口点函数是artInterpreterToCompiledCodeBridge,它会间接地调用该类方法的本地机器指令。

函数LinkCode继续往下执行,判断参数method描述的类方法是否是一个抽象方法。抽象方法声明类中是没有实现的,必须要由子类实现。因此抽象方法在声明类中是没有对应的本地机器指令的,它们必须要通过解释器来执行。不过,为了能够进行统一处理,我们仍然假装抽象方法有对应的本地机器指令函数,只不过这个本地机器指令函数被设置为GetCompiledCodeToInterpreterBridge。当函数GetCompiledCodeToInterpreterBridge被调用时,就会自动进入到解释器中去。

对于非抽象方法,函数LinkCode还要继续往下处理。到这里有一点是需要注意的,前面通过调用OatMethod类的成员函数LinkMethod,我们已经设置好参数method描述的类方法的本地机器指令了。但是,在以下两种情况下,我们需要进行调整:

1. 当参数method描述的类方法是一个非类静态初始化函数(class initializer)的静态方法时,我们不能直接执行翻译其DEX字节码得到的本地机器指令。这是因为类静态方法可以在不创建类对象的前提下执行。这意味着一个类静态方法在执行的时候,对应的类可能还没有初始化好。这时候我们就需要先将对应的类初始化好,再执行相应的静态方法。为了能够做到这一点。我们就先调用GetResolutionTrampoline函数得到一个Tampoline函数,接着将这个Trampoline函数作为静态方法的本地机器指令。这样如果类静态方法在对应的类初始化前被调用,就会触发上述的Trampoline函数被执行。而当上述Trampoline函数执行时,它们先初始化好对应的类,再调用原来的类静态方法对应的本地机器指令。按照代码中的注释,当一个类初始化完成之后,就可以调用函数ClassLinker::FixupStaticTrampolines来修复该类的静态成员函数的本地机器指令,也是通过翻译DEX字节码得到的本地机器指令。这里需要注意的是,为什么类静态初始化函数不需要按照其它的类静态方法一样设置Tampoline函数呢?这是因为类静态初始化函数是一定保证是在类初始化过程中执行的。

  1. 当参数method描述的类方法需要通过解释器执行时,那么当该类方法执行时,就不能执行它的本地机器指令,因此我们就先调用GetCompiledCodeToInterpreterBridge函数获得一个桥接函数,并且将这个桥接函数假装为类方法的本地机器指令。一旦该桥接函数被执行,它就会入到解释器去执行类方法。通过这种方式,我们就可以以统一的方法来调用解释执行和本地机器指令执行的类方法。

函数LinkCode接下来继续判断参数method描述的类方法是否是一个JNI方法。如果是的话,那么就调用ArtMethod类的成员函数UnregisterNative来初始化它的JNI方法调用接口。ArtMethod类的成员函数UnregisterNative的实现如下所示:

void ArtMethod::UnregisterNative(Thread* self) {
      CHECK(IsNative()) << PrettyMethod(this);
      // restore stub to lookup native pointer via dlsym
      RegisterNative(self, GetJniDlsymLookupStub());
    }

这个函数定义在文件runtime/mirror/art_method.cc中。

ArtMethod类的成员函数UnregisterNative实际上就是将一个JNI方法的初始化入口设置为通过调用函数GetJniDlsymLookupStub获得的一个Stub。这个Stub的作用是,当一个JNI方法被调用时,如果还没有显示地注册有Native函数,那么它就会自动从已加载的SO文件查找是否存在一个对应的Native函数。如果存在的话,就将它注册为JNI方法的Native函数,并且执行它。这就是隐式的JNI方法注册。

回到函数LinkCode,它最后调用Instrumentation类的成员函数UpdateMethodsCode检查是否要进一步修改参数method描述的类方法的本地机器指令入口,它的实现如下所示:

void Instrumentation::UpdateMethodsCode(mirror::ArtMethod* method, const void* code) const {
      if (LIKELY(!instrumentation_stubs_installed_)) {
        method->SetEntryPointFromCompiledCode(code);
      } else {
        if (!interpreter_stubs_installed_ || method->IsNative()) {
          method->SetEntryPointFromCompiledCode(GetQuickInstrumentationEntryPoint());
        } else {
          method->SetEntryPointFromCompiledCode(GetCompiledCodeToInterpreterBridge());
        }
      }
    }

这个函数定义在文件art/runtime/instrumentation.cc中。

Instrumentation类是用来调用ART运行时的。例如,当我们需要监控类方法的调用时,就可以往Instrumentation注册一些Listener。这样当类方法调用时,这些注册的Listener就会得到回调。当Instrumentation注册有相应的Listener时,它的成员变量instrumentation_stubs_installed_的值就会等于true。

因此,当Instrumentation类的成员变量instrumentation_stubs_installed_的值等于true时,我们需要使用一个监控函数来替换掉类方法原来的本地机器指令。这样当类方法被调用时,监控函数就获得控制权,它可以在调用原来的本地机器指令前后,向注册的Listener发出通知。

对于JNI方法,我们通过调用函数GetQuickInstrumentationEntryPoint获得的函数作为其监控函数;而对其它的类方法,我们通过调用函数GetCompiledCodeToInterpreterBridge获得的函数作为其监控函数。

另一方面,如果没有Listener注册到Instrumentation中,即它的成员变量instrumentation_stubs_installed_的值等于false,那么Instrumentation类的成员函UpdateMethodsCode就会使用参数code描述的本地机器指令作为参数method描述的类方法的本地机器指令入口。参数code描述的本地机器指一般就是翻译类方法的DEX字节码得到的本地机器指令了。实际上是相当于没有修改类方法的本地机器指令入口。

这样,一个类的加载过程就完成了。加载完成后,得到的是一个Class对象。这个Class对象关联有一系列的ArtField对象和ArtMethod对象。其中,ArtField对象描述的是成员变量,而ArtMethod对象描述的是成员函数。对于每一个ArtMethod对象,它都有一个解释器入口点和一个本地机器指令入口点。这样,无论一个类方法是通过解释器执行,还是直接以本地机器指令执行,我们都可以以统一的方式来进行调用。同时,理解了上述的类加载过程后,我们就可以知道,我们在Native层通过JNI接口FindClass查找或者加载类时,得到的一个不透明的jclass值,实际上指向的是一个Class对象。

有了类加载过程的知识后,接下来我们再继续分析类方法的查找过程,也就是分析JNI接口GetStaticMethodID的实现。按照前面的分析,JNI接口GetStaticMethodID是由JNI类的静态成员函数GetStaticMethodID实现的。因此,接下来我们就开始分析JNI类的静态成员函数GetStaticMethodID的实现。

JNI类的静态成员函数GetStaticMethodID的实现如下所示:

class JNI {
     public:
      ......

      static jmethodID GetStaticMethodID(JNIEnv* env, jclass java_class, const char* name,
                                         const char* sig) {
        CHECK_NON_NULL_ARGUMENT(GetStaticMethodID, java_class);
        CHECK_NON_NULL_ARGUMENT(GetStaticMethodID, name);
        CHECK_NON_NULL_ARGUMENT(GetStaticMethodID, sig);
        ScopedObjectAccess soa(env);
        return FindMethodID(soa, java_class, name, sig, true);
      }

      ......
    };

这个函数定义在文件art/runtime/jni_internal.cc中。

参数name和sig描述的分别是要查找的类方法的名称和签名,而参数java_class的是对应的类。参数java_class的类型是jclass,从前面类加载过程的分析可以知道,它实际上指向的是一个Class对象。

JNI类的静态成员函数GetStaticMethodID通过调用一个全局函数FindMethodID来查找指定的类,后者的实现如下所示:

static jmethodID FindMethodID(ScopedObjectAccess& soa, jclass jni_class,
                                  const char* name, const char* sig, bool is_static)
        SHARED_LOCKS_REQUIRED(Locks::mutator_lock_) {
      Class* c = soa.Decode<Class*>(jni_class);
      if (!Runtime::Current()->GetClassLinker()->EnsureInitialized(c, true, true)) {
        return NULL;
      }

      ArtMethod* method = NULL;
      if (is_static) {
        method = c->FindDirectMethod(name, sig);
      } else {
        method = c->FindVirtualMethod(name, sig);
        if (method == NULL) {
          // No virtual method matching the signature.  Search declared
          // private methods and constructors.
          method = c->FindDeclaredDirectMethod(name, sig);
        }
      }

      if (method == NULL || method->IsStatic() != is_static) {
        ThrowNoSuchMethodError(soa, c, name, sig, is_static ? "static" : "non-static");
        return NULL;
      }

      return soa.EncodeMethod(method);
    }

这个函数定义在文件art/runtime/jni_internal.cc。

函数FindMethodID的执行过程如下所示:

1. 将参数jni_class的值转换为一个Class指针c,因此就可以得到一个Class对象,并且通过ClassLinker类的成员函数EnsureInitialized确保该Class对象描述的类已经初始化。

2. Class对象c描述的类在加载的过程中,经过解析已经关联上一系列的成员函数。这些成员函数可以分为两类:Direct和Virtual。Direct类的成员函数包括所有的静态成员函数、私有成员函数和构造函数,而Virtual则包括所有的虚成员函数。因此:

2.1. 当参数is_static的值等于true时,那么就表示要查找的是静态成员函数,这时候就在Class对象c描述的类的关联的Direct成员函数列表中查找参数name和sig对应的成员函数。这是通过调用Class类的成员函数FindDirectMethod来实现的。

2.2. 当参数is_static的值不等于true时,那么就表示要查找的是虚拟成员函数或者非静态的Direct成员函数,这时候先在Class对象c描述的类的关联的Virtual成员函数列表中查找参数name和sig对应的成员函数。这是通过调用Class类的成员函数FindVirtualMethod来实现的。如果找不到对应的虚拟成员函数,那么再在Class对象c描述的类的关联的Direct成员函数列表中查找参数name和sig对应的成员函数。

3. 经过前面的查找过程,如果都不能在Class对象c描述的类中找到与参数name和sig对应的成员函数,那么就抛出一个NoSuchMethodError异常。否则的话,就将查找得到的ArtMethod对象封装成一个jmethodID值返回给调用者。

也就是说,我们通过调用JNI接口GetStaticMethodID获得的不透明jmethodID值指向的实际上是一个ArtMethod对象。得益于前面的类加载过程,当我们获得了一个ArtMethod对象之后,就可以轻松地得到它的本地机器指令入口,进而对它进行执行。

这样,我们就分析完成类方法的查找过程了。在接下来的一篇文章中,我们将继续分析类方法的本地机器指令的调用过程。通过对类方法的本地机器指令的调用过程的理解,可以进一步理解ART虚拟机的运行原理。敬请关注!更多信息也可以关注老罗的新浪微博:http://weibo.com/shengyangluo

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 目录