赞
踩
自己就之前的面试经历,以及其他比较常见的安卓开发面试的问题做的一些总结
byte short int long float double char boolean
分别占1、2、4、8 、4、8、2、1字节,位数为其八倍。
要满足自反性,传递性,对称性,非空性,一致性。
自动装箱就是将基本类型转换为包装器类,自动拆箱则相反,是将包装器类转换为基本类型。两者都是由编译器完成的。
抽象类和其子类是 是(is) 的关系。
接口和实现其的类是 有(has) 的关系。
抽象类就相当于一个模板,模板中有子类可以公用的部分,也有需要子类自行实现的部分,是为模板式设计;
而接口是对行为的抽象,它只定义一组行为规范,每个实现类都要实现所有规范,是辐射式设计;
一个类有父类的时候只能选择接口,因为Java不支持多重继承。
标准注解:@Override,@SuppressWarnings,@Deprecated
元注解:
@Target : 说明了注解修饰的对象范围
@Retention :定义了该注解被保留的级别
@Documented:表明该注解应该被javadoc工具记录
@Inherited :允许子类继承父类中的注解
自定义注解:由元注解编写的其他注解
异常公有父类为Throwable,分为错误(Error)和异常(Exception)两类。
Error类层次结构描述了Java运行时系统的内部错误和资源耗尽错误,如OOMError,StackOverflowError。
Exception分为两种:运行时异常(RuntimeException)和其他异常。前者属于由编程错误导致的异常,如:数组越界异常(ArrayOutOfIndexException),空指针异常(NullPointerException),强制类型转换异常(CasrClassException)。后者属于程序本身没有问题,但由于像IO错误这类问题导致的异常属于其他异常(IOException),如:文件不存在异常(FileNotFoundException)。
根据编译器是否能检测出异常,分为检查型异常和非检查型异常。
Error和RuntimeException又被称为非检查型异常,所有其他的异常(IOException)被称为检查型异常。
Java中的引用类型有强引用,软引用,弱引用,虚引用。
强引用:在把一个对象赋给一个引用变量时,这个引用变量就是一个强引用。有强引用的对象一定是可达性状态,不会被垃圾回收。因此,强引用是内存泄漏的主要原因。
软引用:用来描述一些有用但并不是必需的对象,内存不足时该对象会被回收。如果之后还不足,则抛出OOM。
弱引用:也用来描述非必需对象的,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。
虚引用:和前面的软引用、弱引用不同,它并不影响对象的生命周期。如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来和引用队列搭配进行跟踪对象的垃圾回收状态。
泛型即广泛的类型,指类型不确定的类型。
无论何时定义一个泛型类型,都会自动提供个相应的原始类型(类型变量会被擦除(erased), 并替换为其限定类型(或者,对于无限定的变量则替换为Object)。
为了兼容以前的代码,Java将原有不支持泛型的类型扩展为支持泛型。如非泛型的写法,编译成的虚拟机汇编码块是A,而之后的泛型写法,只是在A的前面,后面“插入”了其它的汇编码,而并不会破坏A这个整体。这才算是既把非泛型“扩展为泛型”,又兼容了非泛型。
面向对象编程是以面向对象的思维编程,它将问题抽象为对象与对象之间的问题并予以解决,而面向过程编程没有对象的概念,将目标功能的实现分为多个步骤。程序依据步骤的过程一步步执行,最终实现程序功能。
首先,String类不可修改指的是其值不可修改,其引用是可以改变的。而其值设为不可修改是因为value数组是私有字段,并且没有提供更改器方法。
为了使相同内容的字符串可以共享字符串常量池中已有的字符串。如果可变,那一个修改后,其他指向它的字符串变量就全都改变了。不安全。
如果是改变其引用,直接赋以其他对象即可,如果是改变对象的值,即value数组,需要使用反射机制(运行时获取类型信息的机制),得到String对象中的value数组后,设为Accessible,进行修改。
public static void main(String[] args) throws Exception {
// TODO Auto-generated method stub
String str = "不可变的字符串";
System.out.println(str.hashCode()+":"+str); //改变前的hash值
Field f = str.getClass().getDeclaredField("value"); //获取value属性
f.setAccessible(true); //设置其可以被访问(private)
f.set(str, new char[] { '改', '变', '后', '的', '值' }); //改变其值
System.out.println(str.hashCode()+":"+str); //改变后的hash值
}
————————————————
版权声明:本文为CSDN博主「片刻清夏」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/zjq_1314520/article/details/73430885
StringBuilder性能较高,但不具备线程安全性,StringBuffer用sychronized具备了线程安全,支持并发操作,但性能较低。
String常见的创建方式有两种, String s1 = “Java” 和 String s2 = new String(“Java”)的方式,两者在JVM的存储区域截然不同,在JDK 1.8中,s1会先去字符串常量池中找字符串"Java” ,如果有相同的字符则直接返回常量句柄,如果没有此字符串则会先在常量池中创建此字符串,然后再返回常量句柄;而变量s2是直接在堆上创建一个变量 ,如果调用intern方法才会把此字符串保存到常量池中
如下代码所示:
String s1 = new String("南街");
String s2 = s1.intern();
String s3 = "南街";
System.out.println(s1 == s2); // false
System.out.println(s2 == s3); // true
Hashcode是由hash函数计算得出的值,它代表着该对象在Hash表中的位置,其的存在主要是为了查找的快捷性,即HashCode是用来在散列存储结构中确定对象的存储地址的。
由于相同的对象在哈希表中的位置也应该是相同的,所以equals和hashcode必须相容,即如果两对象经equals方法后返回为true,它们的hashcode也应该是相同的。而Object下定义的Hashcode方法是根据对象的存储位置生成hashcode的,如果不进行修改,两对象的存储位置不同,两个对象的hashcode就不相同,这就违反了前面提到的equals和hashcode必须相容的原则。
分为Collection(集合)和Map(映射)两种,前者包括:List,Queue,Set,后者包括Map
Vector在多线程环境下,通过频繁加锁和释放锁的操作,保证线程安全,这也导致了Vector的读写效率整体上比ArrayList低。
List默认初始容量为10,在数组列表满了的情况下继续添加元素时扩容,扩容为原来的1.5倍。
Map默认初始容量为16,在实际容量超过阀值(最大容量×负载因子)时扩容,扩容为原来的2倍。
申请容量为5k的数组,如果使用默认容量会反复扩容,带来性能损耗。
不可以,获得迭代器后不能对原集合进行不是由迭代器发起的结构性修改,否则会导致expectedModCount变量与modCount不一致,抛出ConcurrentModificationException异常。
底层是数组链表。
解决碰撞冲突的方法有两种,一种是开放寻址法,包括线性探测法,二次探测法,双重哈希法等。另一种是链表法。Hashmap采用的就是这种,直接将对象缀在链表末尾。
这是均衡了时间和空间损耗算出来的值,因为当扩容因子设置比较大的时候,相当于扩容的门槛就变高了,发生扩容的频率变低了,但此时发生Hash冲突的几率就会提升,当冲突的元素过多的时候,无论是链表还是链表变成的红黑树都会增加查找成本(hash 冲突增加,链表长度变长)。而扩容因子过小的时候,会频繁触发扩容,占用的空间变大,比如重新计算Hash等,使得性能变差。
因为扩容的本质不是简单的增加容量,而是申请一块新的存储空间,将原来的数据复制过去。
因为在使用是2的幂的数字的时候,Length-1的值是所有二进制位全为1,这种情况下,index的结果等同于HashCode后几位的值。只要输入的HashCode本身分布均匀,Hash算法的结果就是均匀的。这是为了实现均匀分布。
深入解释可以参考这篇文章:HashMap中hash(Object key)原理,为什么(hashcode >>> 16)
链表长度大于8时转为红黑树
头插改尾插
先判断hash值,后通过equals方法。
进程是系统分配资源的最小单位,而线程是程序执行的最小单位。
继承Thread类:新建一个类,实现run方法,创建实例,调用start方法
实现Runnable接口:实现Runnable接口,构造线程实例时将实现了Runnable接口的实例传入,调用start方法
通过ExecutorService和Callable< Class>实现有返回值的线程:用于收集各个线程的执行返回结果并将结果汇总起来
基于线程池:创建线程池,调用execute方法,传入实现Runnable接口的实例
原子性:指一个操作是不可中断的,要么全部执行成功要么全部执行失败
有序性:即程序执行的顺序按照代码的先后顺序执行
可见性:指线程对一个变量的修改对于其他线程而言是可见的
新建状态:线程经new创建后,处于新建状态,此时为线程分配内存,并初始化其成员变量的值。
就绪状态:调用线程的start方法后,线程处于就绪状态,此时JVM完成了方法调用栈和程序计数器的创建,等待该线程的调度和运行。
运行状态:就绪状态的线程在得到时间片后,执行run方法 的线程执行体时,处于运行状态。
阻塞状态:运行中的线程主动或被动放弃时间片暂停运行时,线程转入阻塞状态。阻塞状态有三种,等待阻塞,同步阻塞和其他阻塞。
等待阻塞:调用wait方法后,JVM将线程放入等待队列,线程转入等待阻塞。
同步阻塞:运行状态的线程获取对象锁失败时,JVM会将其放入锁池中,此时线程转为阻塞状态。
其他阻塞:运行状态的线程在执行sleep、join方法或发出IO请求时,JVM会将该线程转完其他阻塞状态。
死亡状态:线程有三种方式进入死亡状态,正常结束,异常退出,被手动结束(stop方法)
① 调用new方法新建一个线程(此时处于新建状态)
② 调用start方法启动一个线程(此时处于就绪状态)
③ 处于就绪状态的线程等待线程获取时间片,获取后转入运行状态执行run方法
④ 正在运行的线程在调用了yield方法或失去CPU时,会再次进入就绪状态
⑤ 正在运行的线程在执行了sleep方法,发生IO阻塞、等待同步锁、等待通知时,会挂起,进入阻塞状态,进入锁池
⑥ 阻塞状态的线程由于出现sleep时间已到、IO方法返回、获得同步锁、收到通知等情况,就会进入就绪状态,等待时间片,获得时间片后转入运行状态
⑦ 处于运行状态的线程,在调用run方法完成或发生异常导致退出时,进入死亡状态
上下文切换指的是线程的状态保存及再加载。
interrupt():将调用该方法的对象所表示的线程标记一个停止标记,并不是真的停止该线程。
interrupted():获取当前线程的中断状态,并且会清除线程的状态标记。是一个是静态方法。
isInterrupted():获取调用该方法的对象所表示的线程的中断状态,不会清除线程的状态标记。是一个实例方法。
方法一:调用interrupt方法,通知线程应该中断了;
这有两种情况:
A.如果线程处于被阻塞状态,那么线程将立即退出被阻塞状态,并抛出了一个InterruptedException异常。
B.如果线程处于正常活动状态,那么会将该线程的中断标志设置为true。被设置中断标志的线程将正常运行,不受影响。
方法二:使用volatile boolean类型变量控制;
二者都是使操作系统立刻重新进行一次CPU竞争。区别在于调用sleep方法后线程进入的是阻塞状态,而调用yield方法进入的是就绪状态。
a线程进入阻塞状态。因为sleep方法是静态方法。在哪里出现,哪里就sleep
对象锁是独占排他锁,一个线程获得后,其他需要获得该对象锁的线程只能等待该线程释放对象锁。
死锁:线程间由于互相拥有对方需要的请求的资源,导致所有线程被阻塞。
前提:互斥,请求与保持,不可剥夺,环路等待。
解决方法:改变请求资源的顺序。
当在一个同步块中需要获得另一个对象锁时容易发生死锁。
显式锁是lock,内置锁是sychronized。
公平锁指的是线程排队获取的锁,先来先得;而非公平锁是允许“插队”的,当一个线程请求锁时恰好另一个线程释放了这个锁,它将跳过前面已经在排队等待的线程,直接获取这个锁,如果没有的话则进入队列中等待。
公平锁由于挂起和恢复存在一定的开销,因此性能不如非公平锁,所以 ReentrantLock 和 synchronized 默认都是非公平锁的实现方式。
(在一个锁释放之后,其他的线程会需要重新来获取锁。其中经历了持有锁的线程释放锁,其他线程从挂起恢复到RUNNABLE状态,其他线程请求锁,获得锁,线程执行,这一系列步骤。如果这个时候,存在一个线程直接请求锁,可能就避开挂起到恢复RUNNABLE状态的这段消耗,所以性能更优化)
获取不到锁就马上进入阻塞状态的锁,我们称之为重量级锁。
反之则为轻量级锁。轻量级锁适合用在那种,很少出现多个线程竞争一个锁的情况。
悲观锁:总是假设最坏的情况,悲观锁认为被它保护的数据是极其不安全的,每时每刻都有可能变动,一个事务拿到悲观锁后(可以理解为一个用户),其他任何事务都不能对该数据进行修改,只能等待锁被释放才可以执行。
传统的关系型数据库里就用到了很多这种锁机制,比如行锁,表锁,读锁,写锁等,都是在做操作之前先上锁。Java中 synchronized 和 ReentrantLock 等独占锁就是悲观锁思想的体现。
乐观锁:总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。
悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)=
乐观锁一般会使用版本号机制或CAS算法实现。
;版本号机制
一般是在数据表中加一个数据版本号 version 字段,表示数据被修改的次数,当数据被修改时,version 值会加1。当线程 A 要更新数据值时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version值为当前数据库中的 version 值相等时才更新,否则重试更新操作,直到更新成功。
CAS 算法
CAS 指令是硬件支持的操作: Compare And Swap(比较与交换),是一种著名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS 算法涉及到三个操作数
需要读写的内存地址 V
进行比较的值 A
拟写入的新值 B
当且仅当 V 的值等于 A 时,CAS 通过原子方式用新值 B 来更新 V 的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。
因为CAS在进行操作的时候,总是需要比较新的操作数和旧的操作数,如果相同则更新。但是如果新的操作数经过两次修改之后仍为原来的值,那么就出现了ABA问题(该操作数经历了A→B→A)。解决问题的方法就是增加一个版本号,不仅仅通过检查值的变化来确定是否更新。
对于基本类型的值来说,这种把数字改变了在改回原来的值是没有太大影响的,但如果是对于引用类型的话,就会产生很大的影响了。
解决的方法:把多个共享变量合并成一个共享变量。AtomicReference类来保证引用对象之间的原子性。
共享锁又称为读锁,简称 S 锁。共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。如ReentranReadWriteLock中的读锁。
独占锁又称为写锁,简称 X 锁。排他锁就是不能与其他锁并存,如果一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,只有获取排他锁的事务可以对数据行读取和修。如ReentranLock。
自旋锁就是,如果此时拿不到锁,它不马上进入阻塞状态,而是等待一段时间,看看这段时间有没其他人把这锁给释放了。类似于线程做空循环,如果循环一定的次数还拿不到锁,它才会进入阻塞的状态。
而能够根据线程最近获得锁的状态来调整循环次数的自旋锁,我们称之为自适应自旋锁。
优点是可以减少上下文切换,尤其是当占用锁的时间很短或锁竞争不激烈时,性能能够得到很大提升,因为此时自旋的CPU耗时明显短于线程阻塞、挂起、再唤醒时的两次上下文切换所用的时间。
缺点是如果持有锁的线程占用锁时间过长或锁的竞争过于激烈时,线程在自旋过程中会长时间获取不到锁资源,造成CPU的浪费。
因此,当占用锁的时间很短或锁竞争不激烈时,适合用自旋锁。
一个锁根据是否允许线程可以重复获得已获得的锁,分为不可重入锁和可重入锁。
可重入锁有一个持有技术来跟踪对lock方法的嵌套调用。线程每使用一次lock后都要调用unlock来释放锁。由于这个特性,被一个锁保护的代码可以调用另一个使用相同锁的方法。
它的原理是有一个引用计数,0表示未被线程获取,每次lock后+1,unlock-1,当重新为0时才被完全释放。
Java中ReentrantLock和sychronized都是可重入锁。
分段锁不是一种锁,而是一种思想。它将数据分段,并在每个分段上单独加锁,把锁进一步细粒度化,以提高并发效率。ConcurrentHashMap就是使用分段锁的思想实现的。
这几种锁的级别从低到高依次是:无锁、偏向锁、轻量级锁、重量级锁。这几个状态会随着竞争情况逐渐升级,但要注意的是除了偏向锁可以恢复到无锁状态以外,只允许锁升级不允许降级,比如由偏向锁升级成轻量级锁之后,不能再降级为偏向锁。
synchronized是独占式悲观锁,是通过JVM 层面实现的,synchronized 只允许同一时刻只有一个线程操作资源。在Java中每个对象都隐式包含一个monitor (监视器)对象,加锁的过程其实就是竞争monitor的过程,当线程进入字节码monitorenter指令之后,线程将持有monitor对象,执行monitorexit时释放monitor对象,当其他线程没有拿到monitor对象时,则需要阻塞等待获取该对象。
ReentrantLock是Lock的默认实现方式之一,它是基于AQS (Abstract Queued Synchronizer,队列同步器)实现的,它默认是通过非公平锁实现的,在它的内部有一个state的状态字段用于表示锁是否被占用,如果是0则表示锁未被占用,此时线程就可以把state改为1,并成功获得锁,其他未获得锁的线程只能去排队等待获取锁资源。
synchronized和ReentrantLock都具备互斥性和不可见性。但在JDK 1.6以前synchronized的性能低于ReentrantLock, JDK 1.6之后synchronized(锁膨胀)的性能略低于ReentrantLock,它的区别如下:
锁升级就是从偏向锁到轻量级锁再到重量级锁升级的过程,也称之为锁膨胀。
synchronized(类名.class)是类锁,是用来锁类的,类锁的作用就是使持有者可以同步地调用静态方法。当synchronized指定修饰静态方法或者class对象的时候,拿到的就是类锁,类锁是所有对象共同争抢一把。
synchronized(this)对象锁,是用来锁对象的,synchronized修饰非静态方法或者this的时候拿到的就是对象锁,对象锁是每个对象各有一把的,即同一个类如果有两个对象,锁住一个对象的方法后还可以调用另一个的该方法。
在线程执行时,首先会从主存中read变量值,再load到工作内存中的副本中,然后再传给处理器执行,执行完毕后再给工作内存中的副本赋值,随后工作内存再把值传回给主存,主存中的值才更新。
可见性与有序性。
为了性能优化,编译器和处理器会进行指令重排序;如果在本线程内观察,所有的操作都是有序的;如果在一个线程观察另一个线程,所有的操作都是无序的(这就有可能发生问题)。
volatile只能用于修饰成员变量和静态变量,且需要放在数据类型关键字之前。
volatile 和 final不能同时修饰一个变量。volatile 是保证变量被写时其结果其他线程可见,而 final 已经让该变量不能被再次写了
Java就是利用volatile来提供可见性的。当一个变量被volatile修饰时,那么对它的写操作会立刻刷新到主存,强制缓存和主存同步,当其它线程需要读取该变量时,会发现缓存失效,然后去主存中读取新的值,由此保证了变量的可见性。
此外,通过synchronized和Lock也能够保证可见性,线程在释放锁之前,会把共享变量值都刷回主存,但是synchronized和Lock的开销都更大。
在Java内存模型中,允许编译器和处理器对指令进行重排序,重排序的结果不会影响到单线程的执行,但不能保证多线程并发执行时不受影响。
而volatile可以禁止指令重排序,所以说其是可以保证有序性的。
synchronized主要特性:可见性;原子性;有序性;
修饰范围:可以是变量,可以方法。静态和非静态在锁的范围上会有区别。
volatile主要特性:有序性,可见性
修饰范围:只能是变量;
线程池是为了避免线程频繁的创建和销毁带来的性能消耗,而建立的一种池化技术,它是把已创建的线程放入“池”中,当有任务来临时就可以重用已有的线程,无需等待创建的过程,这样就可以有效提高程序的响应速度。
通过线程池复用线程有以下几点优点:
减少资源创建 => 减少内存开销,创建线程占用内存
降低系统开销 => 创建线程需要时间,会延迟处理的请求
提高稳定性 => 避免无限创建线程引起的OOM
JVM先根据用用户的参数创建一定数量的可运行的线程任务,并将其放入队列中,在线程创建后启动这些任务,如果线程数量超过了最大线程数量,则超出数量的线程排队等候,在有任务执行完毕后,线程池调度器会发现有可用的线程,进而从阻塞队列中取出任务并执行。
线程池名称 | 说明 | 用途 |
---|---|---|
newCachedThreadPool | 缓存线程池,创建新线程时如果有可重用的线程,则重用之,否则重新创建一个新线程并将其添加到线程池中 | 适合执行时间较短,或者大量时间都在阻塞的任务 |
newFixedThreadPool | 固定数量线程池,活动状态的线程数量大于等于核心线程池的数量时,则将新提交的任务加入阻塞队列,直至有可用的线程资源 | 为了得到最优的运行速度,并发线程数等于处理器内核数 |
newScheduledThreadPool | 定时调度线程池,可设置在给定的延迟时间后或定期执行某线程任务 | |
newSingleThreadExecutor | 单一线程池保证永远只有一个可用的线程,该线程停止或发生异常时,启动一个新的线程来代替该线程继续执行任务 | 用于性能分析,单线程池替换其他线程池,就能测量不使用并发的情况下应用的运行速度会慢多少 |
newWorkingStealingPool | 工作密取线程池,执行是无序的,哪个线程抢到任务,就由它执行 |
其实Executors在底层还是通过ThreadPoolExecutor创建的线程池,不同点在于Executors通过传给ThreadPoolExecutor其设置的默认参数创建线程池,而直接使用ThreadPoolExecutor创建,我们可以传入我们实际需要的参数。
execute和submit都属于线程池的方法,
execute只能提交Runnable类型的任务,而submit既能提交Runnable类型任务也能提交Callable类型任务。
execute会直接抛出任务执行时的异常,submit会吃掉异常,可通过Future的get方法将任务执行时的异常重新抛出。
execute所属顶层接口是Executor,submit所属顶层接口是ExecutorService,实现类ThreadPoolExecutor重写了execute方法,抽象类AbstractExecutorService重写了submit方法。
execute和submit的区别与联系
shutdown()和shutdownNow()
JVM的内存区域分为线程私有区和线程共享区。
线程私有区包括程序计数器,虚拟机栈,本地方法栈,线程共享区包括堆和方法区。
名称 | 描述 | 异常状态 | 线程私/公有 |
---|---|---|---|
程序计数器 | 当前线程所执行的字节码的行号指示器,用来保证上下文切换正常。 | 它是唯一没有OOM的区域 | 线程私有 |
虚拟机栈 | Java方法执行的线程内存模型,每个方法执行的时候虚拟机栈都会同步创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息 | 线程请求栈深度超过虚拟机允许的深度是抛出StackOverflowError;当栈无法申请到足够内存时抛出OOM | 线程私有 |
本地方法栈 | 与虚拟机栈类似,区别在于虚拟机栈为Java方法服务,本地方法栈为本地方法服务 | 与虚拟机栈相同 | 线程私有 |
堆 | 存放对象实例 | 堆中没有内存完成实例分配且无法扩展时抛出OOM | 线程公有 |
方法区 | 存储被虚拟机加载的类型信息,常量,静态变量,即时编译器后的代码缓存等数据。 | 方法区无法满足新的内存分配需求时抛出OOM | 线程公有 |
直接内存(堆外内存) | 它并不是JVM运行时数据区的一部分,但在并发编程中被频繁使用。 | 当无法满足内存需要时同样会抛出OOM | ---- |
Java6和6之前,常量池是存放在方法区(永久代)中的。
Java7,将常量池是存放到了堆中。
Java8之后,取消了整个永久代区域,取而代之的是元空间。运行时常量池和静态常量池存放在元空间中,而字符串常量池依然存放在堆中。
分为新生代和老年代。
新生代存放新生成的对象,特点是对象数量多但生命周期短,默认占1/3的堆空间。
老年代存放大对象和生命周期长的对象,默认占2/3的堆空间。
引用计数器和可达性分析。
不一定。可达性分析算法中判定为不可达的对象暂时处于“缓刑”阶段,要真正宣告一个对象的死亡至少要经历两次标记过程:
如果可达性分析判定为不可达,将会被第一次标记。
之后根据对象是否有必要执行finalize方法进行一次筛选,假如没有finalize方法或finalize方法已经被虚拟机调用过,那么虚拟机将这两种情况都视为“没有必要执行”。有必要执行finalize方法的对象会被放置在一个F-Queue中,并在稍后由一条虚拟机自动建立的、低调度优先级的Finalizer线程去执行(虚拟机承诺触发,但不保证等待其运行结束)它们的finalize方法。如果对象在finalize方法中成功与引用链上任何一个对象建立关联,即可避免被回收的命运,否则将被第二次标记,随后被回收。
如果某对象的finalize方法执行缓慢甚至发生死循环,很可能导致F-Queue阻塞,甚至使整个内存回收子系统崩溃。
根据如何判断对象消亡的角度分为直接垃圾收集和间接垃圾收集。(JVM中并未涉及使用引用技术式的直接垃圾收集)
算法名称 | 实现方法 | 特点 |
---|---|---|
标记清除算法 | 标记出所有需要回收的对象,并在清除阶段清除标记的对象并释放其内存 | 会引起内存碎片化的问题 |
标记复制算法 | 将内存分为两块大小相等的内存区域1和区域2,新生成的对象都在区域1中,对区域1进行标记清除,之后将仍然存活的对象复制到区域2中,最后直接清理区域1并释放内存 | 内存清理效率高且易于实现,但存在大量内存浪费,同时面对大量长时间存活的对象时来回复制会影响系统的运行效率 |
标记整理算法 | 标记阶段于标记清除算法相同,标记完成后将存活的对象移到内存的另一端,然后清除该端的对象并释放内存 | “stop the world” |
分代收集算法 | 对以上算法的综合,JVM根据对象的不同类型将内存分为了新生代和老年代,对新生代使用标记复制算法,对老年代使用标记清除算法 | 根据对象类型使用不同的算法算法 |
JVM将新生代进一步分为Eden区(8/10)和两块Servivor区(各1/10)。
JVM内存中的对象主要被分配到新生代的Eden区和ServivorFrom区,对于大对象将被直接分配到老年代。如果没有足够内存,会发生发生MinorGC。
MinorGC发生前,先扫描老年代最大可用的连续存储空间是否大于新生代所有对象的总空间,如果大于,则发生MinoGC。将在Eden区和ServivorFrom区中存活的对象复制到ServivorTo区(如果ServivorTo区内存不足,会发生分配担保,将其放入老年代,大对象和年龄达到要求的对象也将被移入老年代),同时将这些对象的年龄加一,然后清空Eden区和ServivorFrom区的所有对象,之后再将ServivorTo区和ServivorFrom区互换,即原来的ServivorFrom区成为下一次MinorGC的ServivorTo区。
为了满足分配担保。
二者都不会发生GC,不同在于元空间没有使用虚拟机的内存,而是直接使用操作系统的本地内存。因此元空间大小不受JVM内存限制,只和操作系统的内存有关。
启动类加载器:负责加载Java_HOME/lib目录中的类库
扩展类加载器:负责加载Java_HOME/lib/ext目录中的类库
应用程序类加载器:负责加载用户路径(classpath)上的类库
此外,我们还可以通过继承java.lang.ClassLoader实现自定义加载器
分清是发生的内存泄漏还是内存溢出。
内存溢出 out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。
内存泄露 memory leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。
memory leak会最终会导致out of memory!
情况 | 具体情况 | 解决方法 |
---|---|---|
单例和工具类造成的内存泄漏 | 单例对象持有Activity的context时,activity销毁时候本该被内存回收,却无法回收,这就造成了内存泄漏 | 将持有的context对象改为全局的ApplicationContext引用 |
内部类造成的内存泄漏 | 非静态内部类会隐式持有外部类的引用。如果Activity该销毁了而handler里面还有任务未执行完毕,就会造成内存泄漏 | 将其改为Handler持有activity的弱引用 |
数据库,文件流等使用完未及时关闭 | 对于使用了BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等资源的代码,如果在Activity销毁时未关闭或者注销,这些资源将不会被回收,造成内存泄漏 | 在Activity销毁时及时关闭或者注销 |
监听器没有注销造成的内存泄漏 | 在Android程序里面存在很多需要register与unregister的监听器,如果未unregister则引起内存泄漏。 | 我们需要确保及时unregister监听器 |
Activity、Service、BroadcastReceiver、ContentProvider
onCreate、onStart、onResume、onPause、onStop、onDestroy、onRestart
standard、singleTop、singleTask、singleInstance
A:onPause,B:onCreate、onStart、onResume,A:onStop
B:onPause,A:onRestart、onStart、onResume,B:onStop、onDestroy
数据持久化:保存在文件中,SF中,数据库中
保存在Bundle中(Intent的底层就是Bundle)onSaveInstanceState(outState: Bundle) outState.put…并在onCreate中判断saveinstance是否为空,非空则提取数据并恢复
A中使用startActivityForResult跳转B,B中用Intent返回数据给A
onCreate、onStart、onResume,
系统内存不足时
Activity包括一个PhoneWindow实现的Window,PhoneWindow中有一个DecorView,DecorView又由TitleView和ContentView组成。
一个Task中的Activity可以来自不同的进程,同一个进程的Activity也可能不在一个Task中
安卓开发中开启服务的方式有两种,一种是onStartCommand直接开启服务,这种服务开启之后如果不stopservice关闭服务的话,它会在后台一直运行,影响性能,消耗内存。还有一种就是通过bindservice的方法开启服务,这种方法就是绑定服务,绑定之后会随着activity的关闭而销毁。
在绑定服务的时候可以写一个内部类继承binder,然后再调用的时候可以写一个内部类实现serviceconnection接口,在onServiceConnected的方法中会返回一个binder的代理人对象,这个代理人对象和绑定服务的binder对象是同一个对象
stop&unbind
有,系统创建的。
如果需要自己的looper需要开启子线程创建
代码中动态注册,注册文件中静态注册
标准广播和有序广播
Android中广播的基本原理,具体实现流程要点粗略概括如下:
不可以,有可能会发生ANR。
系统可能由于上个事件未处理结束而没有处理该事件(5秒钟之内没有响应输入的事件,比如按键、屏幕触摸等)
该事件在一定时间内未处理完毕(广播接收器在10秒内没有执行完毕)
而如果在主线程中进行耗时操作,就有可能触发ANR。
不可靠,Receiver只在onReceive方法执行时是激活状态,只要onReceive一返回,Receiver就不再是激活状态了。由于activity可能会被用户退出,Broadcast Receiver的生命周期本身就很短,可能出现的情况是: 在子线程还没有结束的情况下,Activity已经被用户退出了,或者BroadcastReceiver已经结束了。在Activity已经退出、BroadcastReceiver已经结束的情况下,此时它们所在的进程就变成了空进程(没有任何活动组件的进程),系统需要内存时可能会优先终止该进程。如果宿主进程被终止,那么该进程内的所有子线程也会被中止,这样就可能导致子线程无法执行完成.。
开启一个Service
通过ExecutorService和Callable< Class>实现有返回值的线程:用于收集各个线程的执行返回结果并将结果汇总起来
ContentProvider,即内容提供器,主要用于在不同的应用程序之间实现数据共享的功能,它提供了一套完整机制,允许一个程序访问另一个程序的数据,同时还能保证被访问数据的安全性。
不同于文件存储和SharedPerferences存储中的两种全局可读写模式,ContentProvider通过选择哪一部分数据进行共享,从而保证我们程序中的隐私数据不会有泄漏的风险。
依靠内容URI进行增删改查操作:
内容URI可清楚表达我们想要访问哪个程序中的哪张表的数据,其由两部分组成,authority和path。前者用于区分不同程序,一般用应用包名命名,后者则区分同一程序中不同的表。
其标准格式如下:
content://authoriy/path content://com.example.app.provider/table1
访问com.example.app.provider下名为table1的表中的数据
content://com.example.app.provider/table1/1
访问com.example.app.provider下名为table1的表中id为1的数据
URIMatcher解析匹配本地数据
文件存储,SharedPerferences,SQLite
文件存储不对存储内容进行任何格式化处理,所有数据都是原封不动地保存到文件中的,因而适合一些文本数据或二进制数据;
SharedPreferences使用键值对的方式存储数据,适用于保存一些简单的数据和键值对,通常用来存储一些简单的配置信息;
SQlite适合存储大量复杂的关系型数据
SQlite是一款轻量级的关系型数据库,运算速度非常快,占用资源很少,因而特别适合在移动设备上使用。SQLite不仅支持标准的SQL语法,还遵循了数据库的ACID事务,比一般数据库要简单,无需设置用户名密码即可使用
Android消息处理机制由Looper MessageQueue Handler组成
Looper的loop方法将MessageQueue中的Message取出,交由Handler处理。
子线程默认是没有Looper的,如果需要使用Handler就必须为线程创建Looper
Handler的主要作用是将一个任务切换到某个指定的线程中去执行,如切换到主线程中更新UI。
Handler创建时会采用当前线程的Looper来构建内部的消息循环系统,如果当前线程没有Looper则会报错。Handler创建完毕后,这个时候其内部的Looper以及MessageQueue就可以和Handler一起协同工作了,然后通过Handler的post方法将一个Runnable投递到Handler内部的Looper去处理,也可以通过Handler的send方法发送一个消息,这个消息同样会在Looper中去处理。当Handler的send方法被调用时,它会调用MessageQueue的enqueueMessage方法将这个消息放入消息队列中,然后Looper发现有新消息到来时,就会处理这个消息,最终消息中的Runnable和Handler中的handlerMessage方法就会被调用。
注意,Looper是运行在创建Handler所在的线程中的,这样一来Handler的业务逻辑就被切换到创建Handler所在的线程中去执行了。
首先,检查Message的callback是否为Null,部位null就通过handleCallback处理消息。其次检查mCallback是否为null,不为null就调用mCallback的handleMessage方法来处理消息。最后,调用Handler的handleMessage方法来处理消息。
尽管叫消息队列,但它内部存储结构并不是真正的队列,而是采用单链表的数据结构来存储消息列表。
通过Looper.prepare()即可为当前线程创建一个Looper,接着通过Looper.loop()开启消息循环。
通过quit和quitSafely两种方法退出,区别在于前者会直接退出Looper,而后者是设定一个退出标记,然后把消息队列中的已有消息处理完毕后才安全地退出。
在子线程中,如果为其手动创建了looper,那么在所有事情完成以后应该调用quit方法来终止消息循环,否则这个子线程就会一直处于等待状态,而如果退出Looper以后,这个线程就会立刻终止。
loop方法是一个死循环,唯一退出循环的方式是MessageQueue的next方法返回了null(消息队列被标记为退出状态时,next方法就会返回null)。也就是说,Looper必须退出,否则loop方法就会无限循环下去。
ActivityThread的main方法主要就是做消息循环,一旦退出消息循环,那么你的应用也就退出了。
而因为Android 的是由事件驱动的,looper.loop() 不断地接收事件、处理事件,每一个点击触摸或者说Activity的生命周期都是运行在 Looper.loop() 的控制之下,如果它停止了,应用也就停止了。只能是某一个消息或者说对消息的处理阻塞了 Looper.loop(),而不是 Looper.loop() 阻塞它。
https://www.zhihu.com/question/34652589
主线程Looper从消息队列读取消息,当读完所有消息时,主线程阻塞。子线程往消息队列发送消息,并且往管道文件写数据,主线程即被唤醒,从管道文件读取数据,主线程被唤醒只是为了读取消息,当消息读取完毕,再次睡眠。因此loop的循环并不会对CPU性能有过多的消耗。
ThreadLocal是一个线程内部的数据存储类,通过它可以在指定的线程中存储数据,数据存储之后,只有在指定线程中可以获取到存储的数据,对于其他线程来说则无法获取到数据
当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候,就可以考虑采用ThreadLocal。
ThreadLocal是一个泛型类,其数据存储在ThreadLocal.Value中。localvalues内部有一个数组table,其值就存放在这个table数组中。
Window是一个抽象类,表示一个窗口的概念,具体实现是PhoneWindow
应用Window:对应一个Activity,层级范围为1-99
子Window:不能单独存在,需要附属在特定的父Window之中,比如常见的一些Dialog就是一个子Window,层级范围为1000-1999
系统Window:需要声明权限才能创建的Window,比如Toast和系统状态栏,层级范围为2000-2999
Window是一个抽象的概念,每一个Window都对应着一个View和一个ViewRootImpl,Window和View通过ViewRootImpl来建立联系,因此Window并不是实际存在的,它是以View的形式存在。
View的工作流程主要是指measure,layout,draw这三大流程,,即测量、布局、绘制。其中measure确定View的测量宽和高,layout确定View的最终宽高和四个顶点的位置,draw则将View绘制到屏幕上。
MeasureSpec参与measure过程,与父容器的MeasureSpec共同决定View的尺寸规格。
在测量过程中,系统会将View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec,然后根据这个measurespec来测量出View的测量宽高。
MeasureSpec代表一个32位的int值,高2位代表SpecMode,低30位代表SpecSize。
UNSPECIFIED:父容器对View没有任何限制,要多大给多大,常用于系统内部表示一种测量的状态。
EXACTLY:父容器已经检测出View所需要的精确大小,此时View的最终大小就是SpecSize所指定的值。它对应于LayoutParams中的match_parent和具体的数值这两种模式。
AT_MOST:父容器指定了一个可用大小,View的大小不能超过这个最大值。它对应于LayoutParams中的wrap_content。
外部内部滑动方向不一致、外部内部滑动方向不一致、嵌套
外部拦截,内部拦截
LinearLayout:线性布局,在某一方向上一次排列内部视图
RelativeLayout:相对布局,默认是FrameLayout,可以取一个控件作为参考控件,以此安排该控件的位置
FrameLayout:帧布局,默认叠放在左上角
ConstraintLayout:约束布局,利用可视化操作进行布局
MaterialCardView:卡片式布局,在帧布局的基础上额外提供了圆角和阴影等效果
DrawerLayout:抽屉式布局,即滑动菜单,内含两个控件,第一个为主界面,第二个为菜单界面
CoordinatorLayout:协调器布局,加强版帧布局,普通情况下与帧布局效果相同,可以监听其索引子控件的各种事件,并自动帮助我们做出最为合理的相应。
线性布局的局限性在于只能针对一个方向上布局视图,所以适用于所有控件width或height属性为match_parent的情况,此时不需要考虑另一个方向上的布局情况。
而相对布局就弥补了线性布局的这个短板,它通过相对定位可以让内部视图出现在任意位置,适用于比较复杂的布局情况。
用来与include标签搭配进行布局嵌套。
另外需要注意的是:
< merge />只可以作为xml FrameLayout的根节点.
当需要扩充的xml layout本身是由merge作为根节点的话,需要将被导入的xml layout置于 viewGroup中,同时需要设置attachToRoot为True.
总之,标签在UI的结构优化中起着非常重要的作用,它可以删减多余的层级,优化UI。多用于替换FrameLayout或者当一个布局包含另一个时,标签消除视图层次结构中多余的视图组。例如你的主布局文件是垂直布局,引入了一个垂直布局的include,这是如果include布局使用的LinearLayout就没意义了,使用的话反而减慢你的UI表现。这时可以使用标签优化。
xml中Merge标签使用
Android布局优化之merge标签
参照这两篇博客
RecyclerView和ListView的区别、RecyclerView优化
RecyclerView 和 ListView 性能和效果区别
RemoteViews提供了一组基础操作,支持跨进程更新它的界面。
主要用在通知栏和桌面小部件
因为Android的UI控件不是线程安全的,如果多线程并发访问可能会导致UI控件处于不可预期的状态。
View动画:通过渐进对对象做图像变换,从而产生动画效果。
帧动画:通过顺序播放一系列图像从而产生动画效果。
属性动画:根据传递的属性的初始值(如果没有提供初始值则需要提供get方法)和最终值,通过多次调用属性的set方法,从而产生动画效果。
补间动画只是改变了View的显示效果而已,并不会真正的改变View的属性。而属性动画可以改变View的显示效果和属性。举个例子:例如屏幕左上角有一个Button按钮,使用补间动画将其移动到右下角,此刻你去点击右下角的Button,它是绝对不会响应点击事件的,因此其作用区域依然还在左上角。只不过是补间动画将其绘制在右下角而已,而属性动画则不会。
插值器的作用是根据时间的流逝百分比,计算当前属性值变化的百分比。
估值器的作用是根据当前属性改变的百分比来计算改变后的属性值。
Bundle、文件共享、Messenger、AIDL、ContentProvider、Socket
Binder机制是 Android系统中进程间通讯(IPC)的一种方式,Android中ContentProvider、Intent、aidl都是基于Binder。
(1)获得ServiceManager的对象引用
(2)向duServiceManager注册新的Service
(3)在Client中通过ServiceManager获得Service对象引用
(3)在Client中发送请求,由Service返回结果。
1、只需要进行一次数据拷贝,性能上仅次于共享内存
2、基于C/S架构,职责明确,架构清晰,稳定性较好
3、为每个App分配UID,UID可用来识别进程身份,安全性较好
Android中线程分主线程和子线程,主线程即ActivityThread,主要处理和界面相关的事情,而子线程则往往执行耗时操作。
AsyncTask是一种轻量级的异步任务类,它可以在线程池中执行后台任务,然后把执行的进度和最终结果传递给主线程并在主线程中更新UI
HandlerThread继承了Thread,它是一种可以使用Handler的Thread。
普通Thread主要用于在run方法中执行一个耗时任务,而HandlerThread在内部创建了消息队列,外界需要通过Handler的消息方式来通知HandlerThread执行一个具体的任务。
IntentService是一种特殊的Service,它继承了Service并且它是一个抽象类,因此必须创建它的子类才能使用IntentService。IntentService可用于执行后台耗时的任务,当任务执行后它会自动停止,同时由于IntentService是服务的原因,导致其优先级比单纯的线程高很多,所以IntentService比较适合执行一些高优先级的后台任务,因为其优先级高,不容易被系统杀死。
危险权限和普通权限,普通权限只需要在注册文件中声明即可,危险权限不仅需要在注册文件中声明,还需要向用户申请权限许可。
api 指令
完全等同于compile指令
implement指令
这个指令的特点就是,对于使用了该命令编译的依赖,对该项目有依赖的项目将无法访问到使用该命令编译的依赖中的任何程序,也就是将该依赖隐藏在内部,而不对外部公开
用api指令编译,Glide依赖对app Module 是可见的
用implement指令编译依赖对app Module 是不可见的
android gradle tools 3.X 中依赖,implement、api 指令
①Android 5.0:使用一种新的Material Design设计风格
②Android 6.0:引入了运行时权限
③Android 7.0:引入了多窗口模式
④Android 8.0:引入了通知渠道,画中画模式
⑤Android 9.0:适配全面屏,引入全面屏手势
⑥Android 10.0:引入了黑暗模式
⑦Android 11.0:引入了一次性权限,屏幕录制工具。
借助Queue实现
模式名称 | 描述 |
---|---|
观察者模式 | 让对象能够在状态改变时被通知,如LiveData |
单例模式 | 确保有且只有一个对象 |
装饰模式 | 包装一个对象,以提供新的行为 |
适配器模式 | 封装对象,并提供不同的接口 |
状态模式 | 封装了基于状态的行为,并使用委托在行为直接切换 |
迭代器模式 | 在对象的集合之中游走,而不暴露集合的实现 |
外观模式 | 简化一群类的接口 |
策略模式 | 封装可以互换的行为,并使用委托来决定要使用哪一个 |
代理模式 | 包装对象,以控制对此对象的访问 |
工厂方法模式 | 由子类决定要创建的具体类是哪一个 |
抽象工厂模式 | 允许客户创建对象的家族,而无需指定他们的具体类 |
模板方法模式 | 由子类决定如何实现一个算法中的步骤 |
组合模式 | 客户用一致的方式处理单个对象和对象集合 |
命令模式 | 封装请求成为对象 |
懒汉模式:需要时才会去创建
public Class Singleton{
private static Singleton instance = null;
private Singleton(){}
//通过sychronized关键字保证线程安全
public static synchronized Singleton getInstance(){
if( instance == null ){instance = new Singleton(); }
return instance;
}
}
饿汉模式:类加载时就创建了实例
public Class Singleton{
//通过在静态初始化器中创建单件保证线程安全
private static Singleton instance = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return instance;
}
}
双重校验锁:首先检查实例是否已经创建,如果尚未创建,“才”进行同步。
public class Singleton{ //通过vilatiel关键字保证可见性 private volatile static Singleton uniqueInstance; private Singleton(){} public static synchronized Singleton getInstance(){ //检查实例,如果不存在则进入同步块 if( instance == null ){ synchronized(Singleton.class){ //再检查一次,如果仍为null,才创建实例 if(instance == null){ instance = new Singleton(); } } } return uniqueInstance; } }
三种模式的比较:
懒汉模式 | 饿汉模式 | 双重检验锁 | |
---|---|---|---|
优点 | 实现简单,能够避免内存浪费 | 实现起来也比较简单 | 可大大减少getInstance方法的时间耗费 |
缺点 | 由于使用了sychronized关键字性能代价较高 | 只适用于总是创建并使用单件实例或创建运行负担不重的情况 | 实现较为复杂,且仅适用于java5+ |
代理是一种常用的设计模式,其目的就是为其他对象提供一个代理以控制对某个对象的访问。
静态代理类:由程序员创建或由特定工具自动生成源代码,再对其编译。在程序运行前,代理类的.class文件就已经存在了。
动态代理类:程序在运行期间动态构建代理对象和动态调用代理方法的一种机制。
静态代理通常只代理一个类,动态代理是代理一个接口下的多个实现类。
静态代理事先知道要代理的是什么,而动态代理不知道要代理什么东西,只有在运行时才知道。
Retrofit框架就使用了动态代理。
应用层,传输层,网络层,网络接口层
应用层:FTP,SNMP,DNS
传输层:TCP, UDP
网络层:IP ,ARP
网络接口层:FDDI,ATM
Tcp通过校验和,重传控制,序号标识,滑动窗口、确认应答实现可靠传输。如丢包时的重发控制,还可以对次序乱掉的分包进行顺序控制。
三次握手的本质是确认通信双方收发数据的能力
四次挥手的目的是关闭一个连接
要考虑丢包的问题,如果第四次挥手的报文丢失,服务端没收到确认ack报文就会重发第三次挥手的报文,这样报文一去一回最长时间就是2MSL,所以需要等这么长时间来确认服务端确实已经收到了。
HTTP是以明文方式传输的报文,不安全,而HTTPS在HTTP的基础上加入了SSL协议,它采用对称加密方式,而在发送其公共密钥时采用的则是公钥加密方式(即非对称加密)。
对称加密过程和解密过程使用的同一个密钥,加密过程相当于用原文+密钥可以传输出密文,同时解密过程用密文-密钥可以推导出原文。
而非对称加密采用了两个密钥,一般使用公钥进行加密,使用私钥进行解密。
如输入:15, 7,12, 6,14, 13,9, 11
输出:5
11, 12, 13, 14,15
public static int longest(int[] nums){ if (nums==null) return 0; if (nums.length==0 || nums.length==1) return nums.length; Arrays.sort(nums); int left = 0; int right = 0; int max = 0; int start = 0; int end = 0; for (int i = 1; i <nums.length ; i++) { if (nums[i] == nums[i-1]+1){ right++; }else { if (right-left > max){ max = right-left; start = left; end = right; } left = i; right = i; } } if (right-left > max){ max = right-left; start = left; end = right; } int[] res = Arrays.copyOfRange(nums,start,end+1); System.out.println(Arrays.toString(res)); return max+1; }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。