Android之开发性能优化

来源:互联网 发布:单片机之呼吸灯 led 编辑:程序博客网 时间:2024/05/16 07:49

转载自:http://www.eoeandroid.com/thread-327340-1-1.html


       随着技术的发展,智能手机硬件配置越来越高,可是它和现在的PC相比,其运算能力,续航能力,存储空间等都还是受到很大的限制,同时用户对手机的体验要求远远高于PC的桌面应用程序。以上理由,足以需要开发人员更加专心去实现和优化你的代码了。选择合适的算法和数据结构永远是开发人员最先应该考虑的事情。同时,我们应该时刻牢记,写出高效代码的两条基本的原则:(1)不要做不必要的事;(2)不要分配不必要的内存。


我从去年开始接触Android开发,以下结合自己的一点项目经验,同时参考了Google的优化文档和网上的诸多技术大牛给出的意见,整理出这份文档。
2)避免创建不必要的对象
就像世界上没有免费的午餐,世界上也没有免费的对象。虽然gc为每个线程都建立了临时对象池,可以使创建对象的代价变得小一些,但是分配内存永远都比不分配内存的代价大。如果你在用户界面循环中分配对象内存,就会引发周期性的垃圾回收,用户就会觉得界面像打嗝一样一顿一顿的。所以,除非必要,应尽量避免尽力对象的实例。下面的例子将帮助你理解这条原则:
当你从用户输入的数据中截取一段字符串时,尽量使用substring函数取得原始数据的一个子串,而不是为子串另外建立一份拷贝。这样你就有一个新的String对象,它与原始数据共享一个char数组。 如果你有一个函数返回一个String对象,而你确切的知道这个字符串会被附加到一个StringBuffer,那么,请改变这个函数的参数和实现方式,直接把结果附加到StringBuffer中,而不要再建立一个短命的临时对象。
一个更极端的例子是,把多维数组分成多个一维数组:
int数组比Integer数组好,这也概括了一个基本事实,两个平行的int数组比 (int,int)对象数组性能要好很多。同理,这试用于所有基本类型的组合。如果你想用一种容器存储(Foo,Bar)元组,尝试使用两个单独的Foo[]数组和Bar[]数组,一定比(Foo,Bar)数组效率更高。(也有例外的情况,就是当你建立一个API,让别人调用它的时候。这时候你要注重对API接口的设计而牺牲一点儿速度。当然在API的内部,你仍要尽可能的提高代码的效率)
总体来说,就是避免创建短命的临时对象。减少对象的创建就能减少垃圾收集,进而减少对用户体验的影响。

3)静态方法代替虚拟方法
如果不需要访问某对象的字段,将方法设置为静态,调用会加速15%到20%。这也是一种好的做法,因为你可以从方法声明中看出调用该方法不需要更新此对象的状态。

4)避免内部Getters/Setters
在源生语言像C++中,通常做法是用Getters(i=getCount())代替直接字段访问(i=mCount)。这是C++中一个好的习惯,因为编译器会内联这些访问,并且如果需要约束或者调试这些域的访问,你可以在任何时间添加代码。
而在Android中,这不是一个好的做法。虚方法调用的代价比直接字段访问高昂许多。通常根据面向对象语言的实践,在公共接口中使用Getters和Setters是有道理的,但在一个字段经常被访问的类中宜采用直接访问。
无JIT时,直接字段访问大约比调用getter访问快3倍。有JIT时(直接访问字段开销等同于局部变量访问),要快7倍。

5)将成员缓存到本地
访问成员变量比访问本地变量慢得多,下面一段代码:

  • for(int i =0; i <this.mCount; i++)  {
  • dumpItem(this.mItems);
  • }

[color=rgb(51, 102, 153) !important]复制代码
最好改成这样:
  • int count = this.mCount;
  • Item[] items = this.mItems;
  • for(int i =0; i < count; i++)  {
  •        dumpItems(items);
  • }

[color=rgb(51, 102, 153) !important]复制代码
另一个相似的原则是:永远不要在for的第二个条件中调用任何方法。如下面方法所示,在每次循环的时候都会调用getCount()方法,这样做比你在一个int先把结果保存起来开销大很多。
  • for(int i =0; i < this.getCount(); i++) {
  • dumpItems(this.getItem(i));
  • }

[color=rgb(51, 102, 153) !important]复制代码
同样如果你要多次访问一个变量,也最好先为它建立一个本地变量,例如:
  • protected void drawHorizontalScrollBar(Canvas canvas, int width, int height) {
  • if(isHorizontalScrollBarEnabled()) {
  • intsize = mScrollBar.getSize(false);
  • if(size <=0) {
  •        size = mScrollBarSize;
  • }
  • mScrollBar.setBounds(0, height - size, width, height);
  • mScrollBar.setParams(computeHorizontalScrollRange(), computeHorizontalScrollOffset(), computeHorizontalScrollExtent(),false);
  • mScrollBar.draw(canvas);
  • }
  • }

[color=rgb(51, 102, 153) !important]复制代码
这里有4次访问成员变量mScrollBar,如果将它缓存到本地,4次成员变量访问就会变成4次效率更高的栈变量访问。
另外就是方法的参数与本地变量的效率相同。

1)对常量使用static final修饰符
让我们来看看这两段在类前面的声明:
  • static int intVal = 42;
  • static String strVal = "Hello, world!";

[color=rgb(51, 102, 153) !important]复制代码
必以其会生成一个叫做clinit的初始化类的方法,当类第一次被使用的时候这个方法会被执行。方法会将42赋给intVal,然后把一个指向类中常量表的引用赋给strVal。当以后要用到这些值的时候,会在成员变量表中查找到他们。 下面我们做些改进,使用“final”关键字:
  • static final int intVal = 42;
  • static final String strVal = "Hello, world!";

[color=rgb(51, 102, 153) !important]复制代码
现在,类不再需要clinit方法,因为在成员变量初始化的时候,会将常量直接保存到类文件中。用到intVal的代码被直接替换成42,而使用strVal的会指向一个字符串常量,而不是使用成员变量。
将一个方法或类声明为final不会带来性能的提升,但是会帮助编译器优化代码。举例说,如果编译器知道一个getter方法不会被重载,那么编译器会对其采用内联调用。
你也可以将本地变量声明为final,同样,这也不会带来性能的提升。使用“final”只能使本地变量看起来更清晰些(但是也有些时候这是必须的,比如在使用匿名内部类的时候)。

2)使用改进的For循环语法
改进for循环(有时被称为for-each循环)能够用于实现了iterable接口的集合类及数组中。在集合类中,迭代器让接口调用hasNext()和next()方法。在ArrayList中,手写的计数循环迭代要快3倍(无论有没有JIT),但其他集合类中,改进的for循环语法和迭代器具有相同的效率。下面展示集中访问数组的方法:
  • static class Foo {
  •         int mSplat;
  •     }
  •     Foo[] mArray = ...
  •     public void zero() {
  •         int sum = 0;
  •         for (int i = 0; i < mArray.length; ++i) {
  •             sum += mArray.mSplat;
  •         }
  •     }
  •     public void one() {
  •         int sum = 0;
  •         Foo[] localArray = mArray;
  •         int len = localArray.length;
  •         for (int i = 0; i < len; ++i) {
  •             sum += localArray.mSplat;
  •         }
  •     }
  •     public void two() {
  •         int sum = 0;
  •         for (Foo a : mArray) {
  •             sum += a.mSplat;
  •         }
  • }
  • }

[color=rgb(51, 102, 153) !important]复制代码

在zero()中,每次循环都会访问两次静态成员变量,取得一次数组的长度。
在one()中,将所有成员变量存储到本地变量。
two()使用了在java1.5中引入的foreach语法。编译器会将对数组的引用和数组的长度保存到本地变量中,这对访问数组元素非常好。但是编译器还会在每次循环中产生一个额外的对本地变量的存储操作(对变量a的存取)这样会比one()多出4个字节,速度要稍微慢一些。

3)避免使用浮点数
通常的经验是,在Android设备中,浮点数会比整型慢两倍,在缺少FPU和JIT的G1上对比有FPU和JIT的Nexus One中确实如此(两种设备间算术运算的绝对速度差大约是10倍)
从速度方面说,在现代硬件上,float和double之间没有任何不同。更广泛的讲,double大2倍。在台式机上,由于不存在空间问题,double的优先级高于float。
但即使是整型,有的芯片拥有硬件乘法,却缺少除法。这种情况下,整型除法和求模运算是通过软件实现的,就像当你设计Hash表,或是做大量的算术那样,例如a/2可以换成a*0.5。

4)了解并使用类库
选择Library中的代码而非自己重写,除了通常的那些原因外,考虑到系统空闲时会用汇编代码调用来替代library方法,这可能比JIT中生成的等价的最好的Java代码还要好。
i.当你在处理字串的时候,不要吝惜使用String.indexOf(),String.lastIndexOf()等特殊实现的方法。这些方法都是使用C/C++实现的,比起Java循环快10到100倍。
ii.System.arraycopy方法在有JIT的Nexus One上,自行编码的循环快9倍。
iii.android.text.format包下的Formatter类,提供了IP地址转换、文件大小转换等方法;DateFormat类,提供了各种时间转换,都是非常高效的方法。
详细请参考http://developer.android.com/ref ... ackage-summary.html
iv.TextUtils类
对于字符串处理Android为我们提供了一个简单实用的TextUtils类,如果处理比较简单的内容不用去思考正则表达式不妨试试这个在android.text.TextUtils的类,详细请参考http://developer.android.com/ref ... text/TextUtils.html
v.高性能MemoryFile类。
很多人抱怨Android处理底层I/O性能不是很理想,如果不想使用NDK则可以通过MemoryFile类实现高性能的文件读写操作。
MemoryFile适用于哪些地方呢?对于I/O需要频繁操作的,主要是和外部存储相关的I/O操作,MemoryFile通过将 NAND或SD卡上的文件,分段映射到内存中进行修改处理,这样就用高速的RAM代替了ROM或SD卡,性能自然提高不少,对于Android手机而言同时还减少了电量消耗。该类实现的功能不是很多,直接从Object上继承,通过JNI的方式直接在C底层执行。
详细请参考http://developer.android.com/reference/android/os/MemoryFile.html
在此,只简单列举几个常用的类和方法,更多的是要靠平时的积累和发现。多阅读Google给的帮助文档时很有益的。


6)复杂算法尽量用C完成
复杂算法尽量用C或者C++完成,然后用JNI调用。但是如果是算法比较单间,不必这么麻烦,毕竟JNI调用也会花一定的时间。请权衡。

7)减少不必要的全局变量
尽量避免static成员变量引用资源耗费过多的实例,比如Context。Android提供了很健全的消息传递机制(Intent)和任务模型(Handler),可以通过传递或事件的方式,防止一些不必要的全局变量。


9)了解Java四种引用方式
JDK 1.2版本开始,把对象的引用分为4种级别,从而使程序能更加灵活地控制对象的生命周期。这4种级别由高到低依次为:强引用、软引用、弱引用和虚引用。
i.强引用(StrongReference)
强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。
ii.软引用(SoftReference)
如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
iii.弱引用(WeakReference)
在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
iv.虚引用(PhantomReference)
顾名思义,就是形同虚设。与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。
了解并熟练掌握这4中引用方式,选择合适的对象应用方式,对内存的回收是很有帮助的。
详细请参考http://blog.csdn.net/feng88724/article/details/6590064
             
10)使用实体类比接口好
假设你有一个HashMap对象,你可以将它声明为HashMap或者Map:
  • Map map1 = new HashMap();
  • HashMap map2 = new HashMap();

[color=rgb(51, 102, 153) !important]复制代码
哪个更好呢?
按照传统的观点Map会更好些,因为这样你可以改变他的具体实现类,只要这个类继承自Map接口。传统的观点对于传统的程序是正确的,但是它并不适合嵌入式系统。调用一个接口的引用会比调用实体类的引用多花费一倍的时间。如果HashMap完全适合你的程序,那么使用Map就没有什么价值。如果有些地方你不能确定,先避免使用 Map,剩下的交给IDE提供的重构功能好了。(当然公共API是一个例外:一个好的API常常会牺牲一些性能)

11)避免使用枚举
枚举变量非常方便,但不幸的是它会牺牲执行的速度和并大幅增加文件体积。例如:
  • public class Foo {
  •        public enum Shrubbery { GROUND, CRAWLING, HANGING }
  • }

[color=rgb(51, 102, 153) !important]复制代码
会产生一个900字节的.class文件(Foo$Shubbery.class)。在它被首次调用时,这个类会调用初始化方法来准备每个枚举变量。每个枚举项都会被声明成一个静态变量,并被赋值。然后将这些静态变量放在一个名为”$VALUES”的静态数组变量中。而这么一大堆代码,仅仅是为了使用三个整数。
这样:Shrubbery shrub =Shrubbery.GROUND;会引起一个对静态变量的引用,如果这个静态变量是final int,那么编译器会直接内联这个常数。
一方面说,使用枚举变量可以让你的API更出色,并能提供编译时的检查。所以在通常的时候你毫无疑问应该为公共API选择枚举变量。但是当性能方面有所限制的时候,你就应该避免这种做法了。
有些情况下,使用ordinal()方法获取枚举变量的整数值会更好一些,举例来说:
  • for(int n =0; n < list.size(); n++) {
  •        if(list.items[n].e == MyEnum.VAL_X) {
  •               // do something
  •        } else if(list.items[n].e == MyEnum.VAL_Y) {
  •               // do something
  •        }
  • }

[color=rgb(51, 102, 153) !important]复制代码
替换为:
  • int valX = MyEnum.VAL_X.ordinal();
  • int valY = MyEnum.VAL_Y.ordinal();
  • int count = list.size();
  • MyItem items = list.items();
  • for(int n =0; n < count; n++) {
  •        intvalItem = items[n].e.ordinal();
  •        if(valItem == valX) {
  •               // do something
  •        } else if(valItem == valY) {
  •               // do something
  •        }
  • }

[color=rgb(51, 102, 153) !important]复制代码
会使性能得到一些改善,但这并不是最终的解决之道。

12)在私有内部内中,考虑用包访问权限替代私有访问权限
  • public class Foo {
  •            public class Inner {
  •                 public void stuff() {
  •                        Foo.this.doStuff(Foo.this.mValue);
  •                 }
  •            }
  •            private int mValue;
  •            public void run() {
  •                 Inner in = new Inner();
  •                 mValue = 27;
  •                 in.stuff();
  •            }
  •            private void doStuff(int value) {
  •                  System.out.println("value:"+value);
  •            }
  • }

[color=rgb(51, 102, 153) !important]复制代码
需要注意的关键是:我们定义的一个私有内部类(Foo$Inner),直接访问外部类中的一个私有方法和私有变量。这是合法的,代码也会打印出预期的“Value is 27”。
但问题是,虚拟机认为从Foo$Inner中直接访问Foo的私有成员是非法的,因为他们是两个不同的类,尽管Java语言允许内部类访问外部类的私有成员,但是通过编译器生成几个综合方法来桥接这些间隙的。
  • /*package*/static int Foo.access$100(Foo foo) {
  • return foo.mValue;
  • }
  • /*package*/static void Foo.access%200(Foo foo,int value) {
  •        foo.duStuff(value);
  • }

[color=rgb(51, 102, 153) !important]复制代码
内部类会在外部类中任何需要访问mValue字段或调用doStuff方法的地方调用这些静态方法。这意味着这些代码将直接存取成员变量表现为通过存取器方法访问。之前提到过存取器访问如何比直接访问慢,这例子说明,某些语言约会定导致不可见的性能问题。
如果你在高性能的Hotspot中使用这些代码,可以通过声明被内部类访问的字段和成员为包访问权限,而非私有。但这也意味着这些字段会被其他处于同一个包中的类访问,因此在公共API中不宜采用。

13)将与内部类一同使用的变量声明在包范围内
请看下面的类定义:
  • public class Foo {
  •        private class Inner {
  •            void stuff() {
  •                Foo.this.doStuff(Foo.this.mValue);
  •            }
  •        }
  •        private int mValue;
  •        public void run() {
  •            Inner in = new Inner();
  •            mValue = 27;
  •            in.stuff();
  •        }
  •        private void doStuff(int value) {
  •            System.out.println("Value is " + value);
  •        }
  • }

[color=rgb(51, 102, 153) !important]复制代码
这其中的关键是,我们定义了一个内部类(Foo$Inner),它需要访问外部类的私有域变量和函数。这是合法的,并且会打印出我们希望的结果Value is 27。
问题是在技术上来讲(在幕后)Foo$Inner是一个完全独立的类,它要直接访问Foo的私有成员是非法的。要跨越这个鸿沟,编译器需要生成一组方法:
  • /*package*/ static int Foo.access$100(Foo foo) {
  •     return foo.mValue;
  • }
  • /*package*/ static void Foo.access$200(Foo foo, int value) {
  •     foo.doStuff(value);
  • }

[color=rgb(51, 102, 153) !important]复制代码
内部类在每次访问mValueg和gdoStuffg方法时,都会调用这些静态方法。就是说,上面的代码说明了一个问题,你是在通过接口方法访问这些成员变量和函数而不是直接调用它们。在前面我们已经说过,使用接口方法(getter、setter)比直接访问速度要慢。所以这个例子就是在特定语法下面产生的一个“隐性的”性能障碍。
通过将内部类访问的变量和函数声明由私有范围改为包范围,我们可以避免这个问题。这样做可以让代码运行更快,并且避免产生额外的静态方法。(遗憾的是,这些域和方法可以被同一个包内的其他类直接访问,这与经典的OO原则相违背。因此当你设计公共API的时候应该谨慎使用这条优化原则)。

14)缓存
适量使用缓存,不要过量使用,因为内存有限,能保存路径地址的就不要存放图片数据,不经常使用的尽量不要缓存,不用时就清空。

15)关闭资源对象
对SQLiteOpenHelper,SQLiteDatabase,Cursor,文件,I/O操作等都应该记得显示关闭。
             
2.视图优化
1)View优化
i.减少不必要的View以及View的嵌套层次。
比如实现一个listview中常用的layout,可以使用RelativeLayout减少嵌套,要知道每个View的对象会耗费1~2k内存,嵌套层次过多会引起频繁的gc,造成ANR。
ii.通过HierarchyViewer查看布局结构
利用HierarchyViewer来查看View的结构:~/tools/hierarchyviewer,能很清楚地看到RelativeLayout下面的扁平结构,这样能加快dom的渲染速度。
详细请参考http://developer.android.com/gui ... erarchy-viewer.html
iii.通过Layoutopt优化布局
通过Android sdk中tools目录下的layoutopt 命令查看你的布局是否需要优化。详细请参考http://apps.hi.baidu.com/share/detail/34247942


4)对大型图片进行缩放
图片读取是OOM(Out of Memory)的常客,当在Android手机上直接读取4M的图片时,死神一般都会降临,所以导致往往自己手机拍摄的照片都不能直接读取。对大型图片进行缩放有,处理图片时我们经常会用到BitmapFactory类,android系统中读取位图Bitmap时分给虚拟机中图片的堆栈大小只有8M。用BitmapFactory解码一张图片时,有时也会遇到该错误。这往往是由于图片过大造成的。这时我们需要分配更少的内存空间来存储。BitmapFactory.Options.inSampleSize设置恰当的inSampleSize可以使BitmapFactory分配更少的空间以消除该错误。Android提供了一种动态计算的,如下:

读取图片之前先查看其大小:
  • BitmapFactory.Options opts = new BitmapFactory.Options();
  • opts.inJustDecodeBounds = true;
  • Bitmap bitmap = BitmapFactory.decodeFile(imageFile, opts);

[color=rgb(51, 102, 153) !important]复制代码
使用得到的图片原始宽高计算适合自己的smaplesize
  • BitmapFactory.Options opts = new BitmapFactory.Options();
  • opts.inSampleSize = 4 ;// 4就代表容量变为以前容量的1/4
  • Bitmap bitmap = BitmapFactory.decodeFile(imageFile, opts);

[color=rgb(51, 102, 153) !important]复制代码
                    
对于过时的Bitmap对象一定要及时recycle,并且把此对象赋值为null。
  • <span style="line-height: 1.5;">bitmap.recycle(); </span>
  • bitmap = null;

[color=rgb(51, 102, 153) !important]复制代码

6)针对ListView的性能优化
i.复用convertView。
ii.在getItemView中,判断convertView是否为空,如果不为空,可复用。如果couvertview中的view需要添加listerner,代码一定要在if(convertView==null){}之外。
iii.异步加载图片,item中如果包含有web image,那么最好异步加载。
iv.快速滑动时不显示图片
当快速滑动列表时(SCROLL_STATE_FLING),item中的图片或获取需要消耗资源的view,可以不显示出来;而处于其他两种状态(SCROLL_STATE_IDLE 和SCROLL_STATE_TOUCH_SCROLL),则将那些view显示出来。
v.item尽可能的减少使用的控件和布局的层次;背景色与cacheColorHint设置相同颜色;ListView中item的布局至关重要,必须尽可能的减少使用的控件,布局。RelativeLayout是绝对的利器,通过它可以减少布局的层次。同时要尽可能的复用控件,这样可以减少ListView的内存使用,减少滑动时gc次数。ListView的背景色与cacheColorHint设置相同颜色,可以提高滑动时的渲染性能。
vi.getView优化
ListView中getView是性能是关键,这里要尽可能的优化。getView方法中要重用view;getView方法中不能做复杂的逻辑计算,特别是数据库和网络访问操作,否则会严重影响滑动时的性能。优化如下所示:
  • @Override
  • public View getView(int position, View convertView, ViewGroup parent) {
  •        Log.d("MyAdapter", "Position:" + position + "---" + String.valueOf(System.currentTimeMillis()));
  •        final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
  •        View v = inflater.inflate(R.layout.list_item_icon_text, null);
  •        ((ImageView) v.findViewById(R.id.icon)).setImageResource(R.drawable.icon);
  •        ((TextView) v.findViewById(R.id.text)).setText(mData[position]);
  •        return v;
  • }

[color=rgb(51, 102, 153) !important]复制代码
建议改为:
  • @Override
  • public View getView(int position, View convertView, ViewGroup parent) {
  •        Log.d("Adapter", "Position:" + position + " : " + String.valueOf(System.currentTimeMillis()));
  •        ViewHolder holder;
  •        if (convertView == null) {
  •               final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
  •               convertView = inflater.inflate(R.layout.list_item_icon_text, null);
  •               holder = new ViewHolder();
  •              holder.icon = (ImageView) convertView.findViewById(R.id.icon);
  •              holder.text = (TextView) convertView.findViewById(R.id.text);
  •              convertView.setTag(holder);
  •        } else {
  •              holder = (ViewHolder) convertView.getTag();
  •        }
  •               holder.icon.setImageResource(R.drawable.icon);
  •               holder.text.setText(mData[position]);
  •               return convertView;
  •        }
  •        static class ViewHolder {
  •                ImageView icon;
  •                TextView text;
  •        }
  • }

[color=rgb(51, 102, 153) !important]复制代码
以上是Google IO大会上给出的优化建议,经过尝试ListView确实流畅了许多。使用1000条记录,经测试第一种方式耗时:25211ms,第二种方式耗时:16528ms。

4.结束语
本文给出了一些Android 移动开发中常见的优化方法,多数情况下利用这些优化方法优化后的代码,执行效率有明显的提高,内存溢出情况也有所改善。在实际应用中对程序的优化一定要权衡是否是必须的,因为优化可能会带来诸如增加BUG,降低代码的可读性,降低代码的移植性等不良效果。
希望不要盲目优化,请先确定存在问题,再进行优化。并且你知道当前系统的性能,否则无法衡量你进行尝试所得到的性能提升。
希望本文能给大家切实带来帮助。欢迎就上述问题进一步交流。如有发现错误或不足,请斧正。

0 0
原创粉丝点击