彻底搞懂虚拟机这一块,看这一篇就够了


前言

作为豁然开朗篇的最终篇,本文要讲解的是虚拟机这块,因为在之前讲解内存与线程的时候,一直都会牵涉到虚拟机和指令集这块,所以,为了让大家再豁然开朗多一次,本文会从解虚拟机和dex指令以及klass模型等这些地方来带大家深入了解安卓的虚拟机的。

当然如果对豁然开朗篇之前的文章感兴趣可以直接去点击下面阅读:
豁然开朗篇:安卓开发中关于内存那些事
豁然开朗篇:安卓开发中关于线程那些事(上篇)
豁然开朗篇:安卓开发中关于线程那些事(下篇)


提示:以下是本篇文章正文内容

一、虚拟机

虚拟机(JVM)的作用是将Java源码编译成字节码文件.class文件,并且运行这些字节码文件,为什么可以运行呢,因为它可以将这些字节码解读(翻译)成不同平台(系统)对应的机器码,从而能运行。而Dalvik和Art是安卓中使用的虚拟机。

1.Jvm虚拟机与Android虚拟机的区别

一个Java文件通过JVM虚拟机调用javac编译成.class文件,然后虚拟机运行该字节码文件时,就是将里面的字节码翻译成机器指令供硬件去运行。而在安卓体系里,一个Java文件通过虚拟机调用javac编译成.class文件后,会用dex工具去将该.class文件编译成.dex文件,然后这些.dex文件又会被打包成apk文件,当安装apk文件时,安卓虚拟机就会运行里面的.dex文件,将.dex里面的字节码翻译成机器指令供硬件去运行:

机器指令就是机器码,一种CPU的可读指令,所以它是CPU用来调度各硬件的指令;字节码比机器码要抽象多一层,由一序列操作码代码/数据组成的二进制文件,在jvm解释器使用JIT编译器编译成本机机器代码后才供硬件运行。

而汇编代码是一种宏语言,相当于机器码的一种人类可读形式。它经过转译最终也会成为机器码,所以它和字节码是同一级别的,汇编语言可以简单理解为机器语言的助记符,便于人们理解和记忆。

可能会有人疑问为什么jvm不直接对java文件编译成机器指令?因为jvm是跨平台的。无论在哪种操作系统上执行,都可以转成对应的机器语言(这是字节码转机器指令的过程),不仅如此,因为jvm只认识class字节码文件,所以其他语言只需要编译成class文件就可以使用jvm来编译执行了。这仅仅是从跨平台的优点来讲,把所写的源代码编译成class文件后按照规划区分不同的部分,这样也有利于虚拟机高效方便在内存里管理对象与运行程序。

2.Art虚拟机与Dalvik虚拟机的区别

安卓5.0之前的虚拟机是Dalvik虚拟机,使用的是JIT编译,即每次运行程序,都实时地进行将部分dex字节码编译成机器指令供手机去运行,这样整个apk包占用系统内存会很小,但因为每次运行都需要编译,所以CPU的消耗则变大。
5.0之后安卓虚拟机就变成ART虚拟机,使用AOT编译,即在应用安装期间,就将全部dex文件编译成机器指令存储在手机上。这样手机运行app时就可以直接运行这些机器指令,不需要像以前Dalvik虚拟机那样还要再去编译,这样就可以使得整个app运行过程速度要快很多。当然与此同时app可能会比以前要占用的内存要大,但现在的安卓手机的内存已经发展的越来越大了,所以这个缺点是可以忽略不计的。

3.class文件与dex文件的区别

我们都知道一个class文件就是一个类文件,有多少个类则生成多少个文件,而一个dex文件是包含了很多文件,如图所示:

这个dex文件是解压apk文件后得到的,查看classes.dex里面是包含很多文件的,所以,对比class文件来说,使用dex方式可以减少文件数,从而减少很多冗余数据,减少占用的内存大小。

class文件和dex文件的结构也是不一样:

可以看到,dex文件比class文件里的数据不一样,class文件里更多表示的是一个类里的每个结构部分,而dex文件的结构不一样,里面细划分为不同部分结构的集合。从一个jar包和一个apk包对比来看,jar包里会有很多class文件,因为一个类对应一个class文件,而一个apk文件就一个或者几个dex文件就可以表示所有我们写的java文件了:

class文件有header、常量池以及其他结构的数据,而每个class文件里的这些结构数据,到了被编译dex文件时,都被分类成每个不同类型的文件集合里,然后这些文件集合就相当于dex文件里的各个结构属性文件(header、string_ids、method_ids等),所以这也是为什么说一个dex文件就包含了多个class的原因了。

所以其实dex这种压缩性好(指令密集能节省内存空间)的特性是适合于移动端这种内存小的系统,而像PC这种内存大的则更适合使用class文件(指令短小所以能快速执行)。

4.栈跟寄存器的概念

栈是在内存里划分的一块连续空间,从上到下依次排下去,然后数据先进后出,每次当一个数据要入栈时,栈就会向上腾出一个空间,让该数据入栈,下一个数据入栈时,也是如此操作,而当栈底的数据出栈时,实则栈的一个类似于指针的标志位变量会往下调,让它存储为栈底的那个数据的值,这样就相当于把原先那个栈顶的数据弹出栈了,而只剩底部的数据,这样一直上调下调(load和store)来控制变量入栈和出栈。

而寄存器是相当于CPU里的内存,而它是有固定的内存地址的,所以数据在它那里存取是非常快的,距离CPU又近,所以直接根据它的内存地址便可取到数据。

寄存器的指令长度较长,但一个指令能存储的数据也多,但也容易丢失,但总体来看,可以用数量更少的指令去完成操作。而栈的指令则跟寄存器的情况相反,指令短小,携带数据也变少,但能快速执行。

最后还要补充的一点就是Android虚拟机是基于寄存器的虚拟机,而jvm虚拟机是基于虚拟栈的虚拟机。(寄存器和虚拟机栈的更多详情可以去观阅我的豁然开朗篇:安卓开发中关于内存那些事 以及豁然开朗篇:安卓开发中关于线程那些事(上篇))。所以dex文件里的指令跟class文件里的指令相比,dex指令要长而且数量要少,这也是dex文件与class文件的区别。

5.程序执行的原理

当一个java文件被编译成.class文件后,直接用记事本打开它,是一堆乱码,因为此时它里面都是一些字节码,所以要使用javap工具去打开它,javap会去对它进行翻译,即编译成机器指令。比如这段代码:

public class Person {public static void run() {int a = 1;int b = 2;int c = a*b;}
}

我们把这个java文件编译成class文件后,再用javap工具对它编译成机器指令:

可以看到输入命令后输出了一堆指令集,这些指令便是class文件里面的乱码经过编译后变成的机器指令了,其中源码中我们写的Person类的run()方法对应的指令便是这段:

public static void run();descriptor: ()Vflags: ACC_PUBLIC, ACC_STATICCode:stack=2, locals=3, args_size=00: iconst_11: istore_02: iconst_23: istore_14: iload_05: iload_16: imul7: istore_28: return....

0-8行的指令集可以描述整个run方法是怎样执行的:
1)iconst_1表示把常量1加载到操作数栈
2)istore_0表示把操作数栈的1存储到局部变量表的第0个位置上
3)iconst_2表示把常量2加载到操作数栈里
4)istore_1表示把操作数栈的2存储到局部变量表的第1个位置上
5)iload_0表示加载局部变量第0个变量的值压入到操作数栈
6)iload_1表示加载局部变量第1个变量的值压入到操作数栈
7)imul表示操作数栈中的前两个变量相乘,并将结果压入操作数栈顶
8)istore_2表示把操作数栈的2(imul指令的结果)存储到局部变量表的第2个位置上
9)return表示返回空(因为return后面没有数据,表示返回空)

纵观下来,其实这些指令并不难懂,如果对这块感兴趣的可自行去搜索学习。以上这个过程其实我在豁然开朗篇:安卓开发中关于内存那些事 已经讲过,不过当时并未讲它们的指令,只从内存的结构去分析一个方法的运行,因此现在再重新以指令的角度去阐释一个方法的运行,再结合之前的内存结构知识去理解,相信大家对于虚拟机的认识又上升了一个台阶了。

而同样的代码在安卓系统下,则是会被编译成arm机器指令。当java文件编译成.class文件后,然后安卓虚拟机使用dx.bat工具把.class文件编译成.dex文件:

输入上面的命令,然后成功将Person类的run()方法编程后的dex指令输出到Person.dex.txt文件里,我们来查看它:

Person.run:()V:
regs: 0003; ins: 0000; outs: 0000...0000: const/4 v0, #int 1 // #1...0001: const/4 v1, #int 2 // #2...0002: mul-int v2, v0, v1...0004: return-void...

已经把一些不需要关心的指令给忽略掉,可以明显看到,这四行指令就是整个run方法执行的过程:

1)const/4 v0, #int 1表示把int类型的常量1(const/4:位数为4)存入到v0寄存器中
2)const/4 v1, #int 2表示把int类型的常量2(const/4:位数为4)存入到v1寄存器中
3)mul-int v2, v0, v1表示把v0寄存器上的值跟v1寄存器的值相乘的结果存入到v2寄存器中
4)return-void表示返回空值

其实arm机器指令也是很好理解,对应的指令是什么意思可以网上去搜索,这里就不细讲了。

通过以上的class文件编译后的机器指令跟dex文件编译后的arm指令对比之后,可以发现,同样的代码(java代码),JVM生成的机器指令要9行,而安卓虚拟机生成的则4行,不过你也可以看到,JVM的每条指令简短,而安卓虚拟机的每条指令则较长。而且安卓虚拟机的每条指令占用的大小比JVM的每条指令要大。从语义上来讲,arm指令要比jvm的机器指令要好理解。

二、Klass

在豁然开朗篇:安卓开发中关于线程那些事(上篇)中讲锁的对象的内存结构时提到过klass,在程序里对Person进行断点:

通过断点信息可以看到person对象的klass_:

klass其实是person对象在内存里的映射,它里面包含着一个对象里各种信息的集合:

所以对于虚拟机的视角来说,它看到的对象其实是klass,klass才是真正的一个类模板,在内存中存储着该类的各种信息以及它们自己所处的地址。

1.起源

在一个方法里构造对象:

public class TestActivity extends AppCompatActivity {@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_test);Person person = new Person();person.run();}
}

从上面分析指令的时候可以知道,类里所写的方法其实就是一堆指令集,既然如此,也是要有内存空间来存储这些指令的,而这些指令集就是存在方法区里。首先方法区里的onCreate方法指令集执行,然后开辟虚拟机栈,在栈帧里入栈onCreate()方法,此时局部变量表里就存储person变量,然后随着 Person person = new Person()的执行,堆区里构造出new Person()对象,最后让person引用指向new Person()对象:

Person person = new Person()这句代码我们分成两部分来看,一部分是Person person,另一部分则是new Person()。Person person是个引用,可以理解为指针,用来存储该对象的内存地址的。当new Person()执行完后,就在堆里开辟了该对象,然后把实例数据填充进去,填充的自然是变量和方法表地址等,而这些数据是从方法区里的类信息和方法表里加载进去的,最后把那个指针指向该对象,所以这时通过“person.run()、person.age”方式访问对象和调用对象方法时,通过person引用找到Person()对象,然后在Person()对象里找到age变量,又因为对象头里有方法表地址,所以顺着该地址找到方法区里的方发表,然后根据对象名以及相关信息可以找到ArtMethod表里关于run方法的地址(指针),虚拟机就开始执行该方法的指令集:

整个过程清晰明了,关键的地方在于类信息的属性以及方法表里关于这个类的方法的地址等这些信息要加载给我们的对象里,这样我们调用对象的属性和方法才能调用到,那这个加载是怎样进行的,其实就是把klass里关于这个类的信息的属性以及这个类的方法的地址等这些信息加载给java层的Object的过程,下面来分析这个通信过程。

2.FindClass过程

使用过反射来创建class对象都知道这个方法:

Class pClass1 = Class.forName(className);

跟踪它里面的源码可以知道它是native方法:

/** Called after security checks have been made. */@FastNativestatic native Class<?> classForName(String className, boolean shouldInitialize,ClassLoader classLoader) throws ClassNotFoundException;

如果使用过jni开发过,应该也知道它里面也有一个可以构建class对象的函数:

extern "C" JNIEXPORT jstring JNICALL
Java_com_pingred_myapplication3_MainActivity_stringFromJNI(JNIEnv* env,jobject /* this */) {...env->FindClass("com/pingred/myapplication3/Person")...
}

无论是new Person(),还是使用Class.forName(“Person”),亦或是在jni里调用env->FindClass(“Person”),这三种方式去构建Person对象,最终都是会调用到在目录\android-8.0.0_r1\art\runtime下的类链接器class_linker.cc的FindClass()方法来构建对象的:

mirror::Class* ClassLinker::FindClass(Thread* self,const char* descriptor,Handle<mirror::ClassLoader> class_loader) {DCHECK_NE(*descriptor, '\0') << "descriptor is empty string";DCHECK(self != nullptr);self->AssertNoPendingException();self->PoisonObjectPointers();  // For DefineClass, CreateArrayClass, etc...if (descriptor[1] == '\0') {// only the descriptors of primitive types should be 1 character long, also avoid class lookup// for primitive classes that aren't backed by dex files.return FindPrimitiveClass(descriptor[0]);}const size_t hash = ComputeModifiedUtf8Hash(descriptor);// Find the class in the loaded classes table.ObjPtr<mirror::Class> klass = LookupClass(self, descriptor, hash, class_loader.Get());if (klass != nullptr) {return EnsureResolved(self, descriptor, klass);}...

代码很长,先来一点点来分析,首先就留意到ObjPtr类型的klass(这就证明了对于虚拟机底层来说一个类对象是klass,而java层来说则是class对象),然后调用了LookupClass()函数构建出klass,根据注释知道从一个类表里去查找这个class对象,然后赋值给klass变量,那很明显这个类表是一个缓存的作用,然后如果类表里查不到,则往下的逻辑应该是去构造新的klass了,接着往下看是否是如此:

mirror::Class* ClassLinker::FindClass(Thread* self,const char* descriptor,Handle<mirror::ClassLoader> class_loader) {...const size_t hash = ComputeModifiedUtf8Hash(descriptor);// Find the class in the loaded classes table.ObjPtr<mirror::Class> klass = LookupClass(self, descriptor, hash, class_loader.Get());if (klass != nullptr) {return EnsureResolved(self, descriptor, klass);}// Class is not yet loaded.if (descriptor[0] != '[' && class_loader == nullptr) {// Non-array class and the boot class loader, search the boot class path.ClassPathEntry pair = FindInClassPath(descriptor, hash, boot_class_path_);if (pair.second != nullptr) {return DefineClass(self,descriptor,hash,ScopedNullHandle<mirror::ClassLoader>(),*pair.first,*pair.second);}...}...

可以看到后面果然是调用了DefineClass()函数构建新的klass,来看看这个函数的详情:

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));} else if (strcmp(descriptor, "Ldalvik/system/ClassExt;") == 0) {klass.Assign(GetClassRoot(kDalvikSystemClassExt));}}...

同样代码很长,先来看开始的部分,可以看到,构建新的klass对象前先清空,把klass的内存空间置为0,再接着看下面的代码:

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) {...// 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;}...

可以看到调用了LoadClass函数,这个函数传的参数有new_dex_file、new_class_def以及klass,很明显这个函数其实就是把dex文件里的各种关于这个类的数据都加载到klass对象里:

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

里面先调用了dex_file.GetClassData()函数把dex文件里关于该类的信息数据放进class_data里,然后再次调用LoadClassMembers()函数,把dex_file, class_data, klass都传进去:

void ClassLinker::LoadClassMembers(Thread* self,const DexFile& dex_file,const uint8_t* class_data,Handle<mirror::Class> klass) {{...// Load instance fields.LengthPrefixedArray<ArtField>* ifields = AllocArtFieldArray(self,allocator,it.NumInstanceFields());...// Set the field arrays.klass->SetSFieldsPtr(sfields);DCHECK_EQ(klass->NumStaticFields(), num_sfields);klass->SetIFieldsPtr(ifields);DCHECK_EQ(klass->NumInstanceFields(), num_ifields);// Load methods.bool has_oat_class = false;const OatFile::OatClass oat_class =(Runtime::Current()->IsStarted() && !Runtime::Current()->IsAotCompiler())? OatFile::FindOatClass(dex_file, klass->GetDexClassDefIndex(), &has_oat_class): OatFile::OatClass::Invalid();const OatFile::OatClass* oat_class_ptr = has_oat_class ? &oat_class : nullptr;klass->SetMethodsPtr(AllocArtMethodArray(self, allocator, it.NumDirectMethods() + it.NumVirtualMethods()),it.NumDirectMethods(),it.NumVirtualMethods());...}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++;}...

可以看到该函数里都是各种for循环遍历dex文件里的集合,然后调用klass->SetSFieldsPtr()等这些klass的函数,把从dex文件里存储的关于该类的变量集合、方法表地址等数据加载进去klass里,其中可以看到ifields变量表这样的集合,对应上文中dex文件结构图的field_ids。然后method方法则通过klass->GetVirtualMethodUnchecked()构建之后,再调用LoadMethod()把方法指令集和它对应的klass类、访问权限和以及执行地址等信息设置到ArtMethod里:

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();...}

所以平时使用反射invoke获取Method方法底层实质上就是会从ArtMethod表里根据所传的字符串来遍历然后找到方法,这样会比较费时,而通过对象.方法名的方式调用方法,因为事先对象已经跟klass关联了,也就不用遍历ArtMethod表就能直接通过方法指令集的执行地址像指针那样直接定位到这个方法,所以就比反射的方式高效。

来看看art_method.h文件就可以知道ArtMethod是长怎样的:

class ArtMethod FINAL {public:// May cause thread suspension due to GetClassFromTypeIdx calling ResolveType this caused a large// number of bugs at call sites.mirror::Class* GetReturnType(bool resolve) REQUIRES_SHARED(Locks::mutator_lock_);mirror::ClassLoader* GetClassLoader() REQUIRES_SHARED(Locks::mutator_lock_);...// Offset to the CodeItem.uint32_t dex_code_item_offset_;// Index into method_ids of the dex file associated with this method.uint32_t dex_method_index_;/* End of dex file fields. */// Entry within a dispatch table for this method. For static/direct methods the index is into// the declaringClass.directMethods, for virtual methods the vtable and for interface methods the// ifTable.uint16_t method_index_;// The hotness we measure for this method. Managed by the interpreter. Not atomic, as we allow// missing increments: if the method is hot, we will see it eventually.uint16_t hotness_count_;// Fake padding field gets inserted here.// Must be the last fields in the method.struct PtrSizedFields {// Short cuts to declaring_class_->dex_cache_ member for fast compiled code access.ArtMethod** dex_cache_resolved_methods_;// Pointer to JNI function registered to this method, or a function to resolve the JNI function,// or the profiling data for non-native methods, or an ImtConflictTable, or the// single-implementation of an abstract/interface method.void* data_;// Method dispatch from quick compiled code invokes this pointer which may cause bridging into// the interpreter.void* entry_point_from_quick_compiled_code_;} ptr_sized_fields_;...

后面还有属性和方法就省略掉了,可以看到方法的类、访问权限和执行地址等信息,entry_point_from_quick_compiled_code_指针就是存储执行指令的地址,有了它就可以在内存中定位到方法指令集。现在我们就可以知道在java源文件里我们声明类的方法,在虚拟机底层里的表现就是ArtMethod类,所以也可以说在java里定义的方法它的大小并不是由多少写了多少行代码来决定的,而是由这个ArtMethod类的大小决定的。

总结

豁然开朗篇暂时就告一段落,写该系列的初衷除了让大家明白自己所写的代码在内存里是怎样呈现之外,还想激发大家对安卓系统的兴趣,因为你也看到我们一深入虚拟机底层的时候,它里面都是c/c++文件,以及暗含各种内存、操作系统等这些基础知识,所以该系列作为研究安卓系统源码前的热身学习就再好不过了。


豁然开朗篇:安卓开发中关于虚拟机那些事相关推荐

  1. 豁然开朗篇:安卓开发中关于线程那些事(下篇)

    彻底搞懂线程这一块,看这一篇就够了 前言 本系列详细讲解并发的知识,从基础到底层,让大家彻底搞懂线程和锁的原理,当然里面会涉及到一些内存结构的知识,所以如果为了更好地阅读效果,也可以先去看以下这两篇: ...

  2. 安卓开发中遇到的奇奇怪怪的问题(三)

    本文已授权微信公众号:鸿洋(hongyangAndroid)原创首发. 距离上一篇 安卓开发中遇到的奇奇怪怪的问题(二)又过了半年了,转眼也到年底了,是时候拿出点干货了.这篇算是本年度个人印象最深的几 ...

  3. 安卓开发中的USB转串口通讯

    安卓开发中的USB转串口通讯 本文使用GitHub上开源的"hoho.android.usbserial"USB串口库.该库基于"Android USB Host API ...

  4. 安卓开发中非常炫的效果集合

    安卓开发中非常炫的效果集合 这几天开发的时候,想做一些好看而且酷炫的特效,于是又开始从网上收集各种特效资源.下面给大家一些我喜欢的把,附代码,喜欢的看源代码,然后加到自己项目去把!! 一个开源项目网站 ...

  5. 安卓开发中,release安装包安装后,打开app后再按home键,再次点击程序图标app再次重新启动的解决办法

    安卓开发中,release安装包安装后,打开app后再按home键,再次点击程序图标app再次重新启动的解决办法 在开发中我们一般都是直接AS上的安装(Run)按钮,直接安装到真机或模拟器上进行测试, ...

  6. Android安卓开发中图片缩放讲解

    安卓开发中应用到图片的处理时候,我们通常会怎么缩放操作呢,来看下面的两种做法: 方法1:按固定比例进行缩放 在开发一些软件,如新闻客户端,很多时候要显示图片的缩略图,由于手机屏幕限制,一般情况下,我们 ...

  7. Android Studio安卓开发中使用json来作为网络数据传输格式

    如果你是在安卓开发中并且使用android studio,要使用json来作为数据传输的格式,那么下面是我的一些经验. 一开始我在android studio中导入那6个包,那6个包找了非常久,因为放 ...

  8. iOS开发UI篇—IOS开发中Xcode的一些使用技巧

    iOS开发UI篇-IOS开发中Xcode的一些使用技巧 一.快捷键的使用 经常用到的快捷键如下: 新建 shift + cmd + n     新建项目 cmd + n             新建文 ...

  9. 安卓开发中的重力感应传感器

    2019独角兽企业重金招聘Python工程师标准>>> 安卓开发中拥有多种传感器,google提供了11种传感器供应用层使用:加速度.磁力.方向.陀螺仪.光线.压力(返回当前压强). ...

最新文章

  1. Vue2.0使用vue-cli脚手架搭建
  2. R语言explore包进行探索性数据分析实战(EDA、exploratory data analysis):基于iris数据集
  3. 可伸缩性架构常用技术——之数据切分
  4. VS2005下开发PPC2003和WM50编译器一些设置
  5. C语言通过用户输入将八进制转为二进制(附完整源码)
  6. java面试总结之一
  7. Verilg 2001相对于Verilog 1995的改进(Z) (内含 乘方 运算符** )
  8. Shell编程之if语法练习(LNMP)全过程
  9. shell的if-else的基本用法
  10. 据说学编程的计算这题不超1分钟!
  11. asp.net的10个提升性能或扩展性的秘密(二)
  12. java string是final_关于java:String和Final
  13. qq批量提取群成员_学会这个QQ营销技巧,助你一天引流200+
  14. 大厂面试必备之设计模式:漫画适配器模式
  15. b站up粉丝数量及变化爬取,并保存成txt文件
  16. vCenter Server 相关介绍
  17. 程序员的遮羞布:这个需求技术上无法实现
  18. vue : 无法加载文件 C:\Users\xxx\AppData\Roaming\npm\vue.ps1,因为在此系统上禁止运行脚本。有关详细信息,请参阅 https:/go.
  19. 【学习笔记】传说中的马尔可夫决策过程(MDP)和贝尔曼方程(Bellman Equation)
  20. 验证手机号码 (包含166和199)

热门文章

  1. 【技术分享】历经16年猪八戒网如何成功实现双活流量架构
  2. 苹果6如何截屏_苹果升级iOS14,轻点背面能开启截屏功能,真是太方便了
  3. UE4-密室逃脱小游戏学习-3 开门
  4. Java中自定义注解的使用
  5. 2022年知识产权司法保护状况发布,中创算力:尊重知识产权,共建知识产权强国!
  6. 揭秘LOL背后的IT基础架构丨微服务生态系统
  7. java判断日文_判断字符串是否含有日文
  8. Unity3d 性能优化篇
  9. 第二节 红帽认证培训 部署虚拟环境安装LInux系统+新手必须掌握的Linux命令(讲到2.3)
  10. 将json文件里面的数据写入数据库