`
zhubin215130
  • 浏览: 142319 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

【转】编写高效的Android代码

阅读更多

编写高效的Android代码

毫无疑问,基于Android平台的设备一定是嵌入式设备。现代的手持设备不仅仅是一部电话那么简单,它还是一个小型的手持电脑,但是,即使是最快的最高端的手持设备也远远比不上一个中等性能的桌面机。

这就是为什么在编写Android程序时要时刻考虑执行的效率,这些系统不是想象中的那么快,并且你还要考虑它电池的续航能力。这就意味着没有多少剩余空间给你去浪费了,因此,在你写Android程序的时候,要尽可能的使你的代码优化而提高效率。

本页介绍了几种可以让开发者的Android程序运行的更加有效率的方法。通过下面的一些跳转的连接,你可以学会怎么让你的程序更加有效运行

内容

介绍

对于如何判断一个系统的不合理,这里有两个基本的原则:

  • 不要做不必要做的事情。
  • 尽可能的节省内存的使用。

下面的所有方法都是基于这两项的。

有人会认为本页花了大量的篇幅去讲如何进行“初步优化”( premature optimization)。虽然有时候微观优化对开发高效的数据结构和算法很困难,但是在嵌入式手持设备上面你毫无选择。例如,如果把桌面电脑的虚拟机移植到你的Android系统中,你会发现你写的程序会耗尽你的内存。这就会导致程序运行起来极度缓慢,即使不考虑它对系统上其他的运行程序的影响。

这就是为什么上面两条原则这么重要。Android的成功在于开发程序提供给用户的体验,然而用户体验的好坏又决定于你的代码是否能及时的响应而不至于慢的让人崩溃。因为我们所有的程序都会在同一个设备上面运行,所以我们把它们作为一个整体来考虑。本文就像你考驾照需要学习的交通规则一样:如果所有人遵守,事情就会很流畅;但当你不遵守时,你就会撞车。

在我们讨论实质问题之前,有一个简要的说明:无论虚拟机是否是Java编译器的一个特点,下面介绍的所有观点都是正确的。如果我们有两种方法完成同样的事情,但是foo()的解释执行要快于bar(),那么foo()的编译速度一定不会比bar()慢,仅仅靠编译器使你的代码运行速度提升是不明智的做法。

尽可能避免创建对象(Object)

对象的创建并不是没有代价的。一个带有线程分配池的generational的内存管理机制会使创建临时对象的代价减少,不是分配内存总比不上不分配内存好。

如果你在一个用户界面的循环中分配一个对象,你不得不强制的进行内存回收,那么就会使用户体验出现稍微“打嗝”的现象。

因此,如果没有必要你就不应该创建对象实例。下面是一件有帮助的例子:

  • 当从原始的输入数据中提取字符串时,试着从原始字符串返回一个子字符串,而不是创建一份拷贝。你将会创建一个新的字符串对象,但是它和你的原始数据共享数据空间。
  • 如果你有一个返回字符串地方法,你应该知道无论如何返回的结果是StringBuffer,改变你的函数的定义和执行,让函数直接返回而不是通过创建一个临时的对象。

一个比较激进的方法就是把一个多维数组分割成几个平行的一维数组:

  • 一个Int类型的数组要比一个Integer类型的数组要好,但着同样也可以归纳于这样一个原则,两个Int类型的数组要比一个(int,int)对象数组的效率要高的多。对于其他原始数据类型,这个原则同样适用。
  •  如果你需要创建一个包含一系列Foo和Bar对象的容器(container)时,记住:两个平行的Foo[]和Bar[]要比一个(Foo,Bar)对象数组的效率高得多。(这个例子也有一个例外,当你设计其他代码的接口API时;在这种情况下,速度上的一点损失就不用考虑了。但是,在你的代码里面,你应该尽可能的编写高效代码。)

一般来说,尽可能的避免创建短期的临时对象。越少的对象创建意味着越少的垃圾回收,这会提高你程序的用户体验质量。

使用自身方法(Use Native Methods)

当处理字符串的时候,不要犹豫,尽可能多的使用诸如String.indexOf()、String.lastIndexOf()这样对象自身带有的方法。因为这些方法使用C/C++来实现的,要比在一个java循环中做同样的事情快10-100倍。

还有一点要补充说明的是,这些自身方法使用的代价要比那些解释过的方法高很多,因而,对于细微的运算,尽量不用这类方法。

使用虚拟优于使用接口

假设你有一个HashMap对象,你可以声明它是一个HashMap或则只是一个Map:

Map myMap1 = new HashMap();
HashMap myMap2 = new HashMap();

哪一个更好呢?

一般来说明智的做法是使用Map,因为它能够允许你改变Map接口执行上面的任何东西,但是这种“明智”的方法只是适用于常规的编程,对于嵌入式系统并不适合。通过接口引用来调用会花费2倍以上的时间,相对于通过具体的引用进行虚拟函数的调用。

如果你选择使用一个HashMap,因为它更适合于你的编程,那么使用Map会毫无价值。假定你有一个能重构你代码的集成编码环境,那么调用Map没有什么用处,即使你不确定你的程序从哪开头。(同样,public的API是一个例外,一个好的API的价值往往大于执行效率上的那点损失)

使用静态优于使用虚拟

如果你没有必要去访问对象的外部,那么使你的方法成为静态方法。它会被更快的调用,因为它不需要一个虚拟函数导向表。这同时也是一个很好的实践,因为它告诉你如何区分方法的性质(signature),调用这个方法不会改变对象的状态。

尽可能避免使用内在的Get、Set方法

像C++iyangde编程语言,通常会使用Get方法(例如 i = getCount())去取代直接访问这个属性(i=mCount)。 这在C++编程里面是一个很好的习惯,因为编译器会把访问方式设置为Inline,并且如果想约束或调试属性访问,你只需要在任何时候添加一些代码。

在Android编程中,这不是一个很不好的主意。虚方法的调用会产生很多代价,比实例属性查询的代价还要多。我们应该在外部调用时使用Get和Set函数,但是在内部调用时,我们应该直接调用。

缓冲属性调用Cache Field Lookups

访问对象属性要比访问本地变量慢得多。你不应该这样写你的代码:

for (int i = 0; i < this.mCount; i++)
      dumpItem(this.mItems[i]);

而是应该这样写:

  int count = this.mCount;
  Item[] items = this.mItems;
 
  for (int i = 0; i < count; i++)
      dumpItems(items[i]);

(我们直接使用“this”表明这些是它的成员变量)

一个相似的原则就是:决不在一个For语句中第二次调用一个类的方法。例如,下面的代码就会一次又一次地执行getCount()方法,这是一个极大地浪费相比你把它直接隐藏到一个Int变量中。

for (int i = 0; i < this.getCount(); i++)
    dumpItems(this.getItem(i));

这是一个比较好的办法,当你不止一次的调用某个实例时,直接本地化这个实例,把这个实例中的某些值赋给一个本地变量。例如:

    protected void drawHorizontalScrollBar(Canvas canvas, int width, int height) {
        if (isHorizontalScrollBarEnabled()) {
            int size = mScrollBar.getSize(false);
            if (size <= 0) {
                size = mScrollBarSize;
            }
            mScrollBar.setBounds(0, height - size, width, height);
            mScrollBar.setParams(
                    computeHorizontalScrollRange(),
                    computeHorizontalScrollOffset(),
                    computeHorizontalScrollExtent(), false);
            mScrollBar.draw(canvas);
        }
    }

这里有四次mScrollBar的属性调用,把mScrollBar缓冲到一个堆栈变量之中,四次成员属性的调用就会变成四次堆栈的访问,这样就会提高效率。

附带说一下,对于方法同样也可以像本地变量一样具有相同的特点。

声明Final常量

我们可以看看下面一个类顶部的声明:

static int intVal = 42;
static String strVal = "Hello, world!";

当一个类第一次使用时,编译器会调用一个类初始化方法——<clinit>,这个方法将42存入变量intVal,并且为strVal在类文件字符串常量表中提取一个引用,当这些值在后面引用时,就会直接属性调用。

我们可以用关键字“final”来改进代码:

static final int intVal = 42;
static final String strVal = "Hello, world!";

这个类将不会调用es a <clinit>方法,因为这些常量直接写入了类文件静态属性初始化中,这个初始化直接由虚拟机来处理。代码访问intVal将会使用Integer类型的42,访问strVal将使用相对节省的“字符串常量”来替代一个属性调用。

将一个类或者方法声明为“final”并不会带来任何的执行上的好处,它能够进行一定的最优化处理。例如,如果编译器知道一个Get方法不能被子类重载,那么它就把该函数设置成Inline。

同时,你也可以把本地变量声明为final变量。但是,这毫无意义。作为一个本地变量,使用final只能使代码更加清晰(或者你不得不用,在匿名访问内联类时)。

慎重使用增强型For循环语句

增强型For循环(也就是常说的“For-each循环”)经常用于Iterable接口的继承收集接口上面。在这些对象里面,一个iterator被分配给对象去调用它的hasNext()和next()方法。在一个数组列表里面,你可以自己接的敷衍它,在其他的收集器里面,增强型的for循环将相当于iterator的使用。

尽管如此,下面的源代码给出了一个可以接受的增强型for循环的例子:

public class Foo {
    int mSplat;
    static Foo mArray[] = new Foo[27];

    public static void zero() {
        int sum = 0;
        for (int i = 0; i < mArray.length; i++) {
            sum += mArray[i].mSplat;
        }
    }

    public static void one() {
        int sum = 0;
        Foo[] localArray = mArray;
        int len = localArray.length;

        for (int i = 0; i < len; i++) {
            sum += localArray[i].mSplat;
        }
    }

    public static void two() {
        int sum = 0;
        for (Foo a: mArray) {
            sum += a.mSplat;
        }
    }
}

zero() 函数在每一次的循环中重新得到静态属性两次,获得数组长度一次。

one() 函数把所有的东西都变为本地变量,避免类查找属性调用

two() 函数使用Java语言的1.5版本中的for循环语句,编辑者产生的源代码考虑到了拷贝数组的引用和数组的长度到本地变量,是例遍数组比较好的方法,它在主循环中确实产生了一个额外的载入和储存过程(显然保存了“a”),相比函数one()来说,它有一点比特上的减慢和4字节的增长。

总结之后,我们可以得到:增强的for循环在数组里面表现很好,但是当和Iterable对象一起使用时要谨慎,因为这里多了一个对象的创建。

避免列举类型Avoid Enums

列举类型非常好用,当考虑到尺寸和速度的时候,就会显得代价很高,例如:

public class Foo {
   public enum Shrubbery { GROUND, CRAWLING, HANGING }
}

这会转变成为一个900字节的class文件(Foo$Shrubbery.class)。第一次使用时,类的初始化要在独享上面调用方法去描述列举的每一项,每一个对象都要有它自身的静态空间,整个被储存在一个数组里面(一个叫做“$VALUE”的静态数组)。那是一大堆的代码和数据,仅仅是为了三个整数值。

 

Shrubbery shrub = Shrubbery.GROUND;

这会引起一个静态属性的调用,如果GROUND是一个静态的Final变量,编译器会把它当做一个常数嵌套在代码里面。

还有一点要说的,通过列举,你可以得到更好地API和一些编译时间上的检查。因此,一种比较平衡的做法就是:你应该尽一切方法在你的公用API中使用列举型变量,当处理问题时就尽量的避免。

在一些环境下面,通过ordinal()方法获取一个列举变量的整数值是很有用的,例如:把下面代码

for (int n = 0; n < list.size(); n++) {
    if (list.items[n].e == MyEnum.VAL_X)
       // do stuff 1
    else if (list.items[n].e == MyEnum.VAL_Y)
       // do stuff 2
}

替换为:

   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++)
   {
        int  valItem = items[n].e.ordinal();

        if (valItem == valX)
          // do stuff 1
        else if (valItem == valY)
          // do stuff 2
   }

在一些条件下,这会执行的更快,虽然没有保障。

通过内联类使用包空间

我们看下面的类声明

public class Foo {
    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);
    }

    private class Inner {
        void stuff() {
            Foo.this.doStuff(Foo.this.mValue);
        }
    }
}

这里我们要注意的是我们定义了一个内联类,它调用了外部类的私有方法和私有属性。这是合法的调用,代码应该会显示"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);
}

当内联类需要从外部访问“mValue”和调用“doStuff”时,内联类就会调用这些静态的方法,这就意味着你不是直接访问类成员,而是通过公共的方法来访问的。前面我们谈过间接访问要比直接访问慢,因此这是一个按语言习惯无形执行的例子。

让拥有包空间的内联类直接声明需要访问的属性和方法,我们就可以避免这个问题,哲理诗是包空间而不是私有空间。这运行的更快并且去除了生成函数前面东西。(不幸的是,它同时也意味着该属性也能够被相同包下面的其他的类直接访问,这违反了标准的面向对象的使所有属性私有的原则。同样,如果是设计公共的API你就要仔细的考虑这种优化的用法)

避免浮点类型的使用

在奔腾CPU发布之前,游戏作者尽可能的使用Integer类型的数学函数是很正常的。在奔腾处理器里面,浮点数的处理变为它一个突出的特点,并且浮点数与整数的交互使用相比单独使用整数来说,前者会使你的游戏运行的更快,一般的在桌面电脑上面我们可以自由的使用浮点数。

不幸的是,嵌入式的处理器通常并不支持浮点数的处理,阴齿所有的“float”和“double”操作都是通过软件进行的,一些基本的浮点数的操作就需要花费毫秒级的时间。

同事,即使是整数,一些芯片也只有乘法而没有除法。在这些情况下,整数的除法和取模操作都是通过软件实现。当你创建一个Hash表或者进行大量的数学运算时,这都是你要考虑的。

一些标准操作的时间比较

为了距离说明我们的观点,下面有一张表,包括一些基本操作所使用的大概时间。注意这些时间并不是绝对的时间,绝对时间要考虑到CPU和时钟频率。系统不同,时间的大小也会有所差别。当然,这也是一种有意义的比较方法,我们可以比叫不同操作花费的相对时间。例如,添加一个成员变量的时间是添加一个本地变量的四倍。

Action Time
Add a local variable 1
Add a member variable 4
Call String.length() 5
Call empty static native method 5
Call empty static method 12
Call empty virtual method 12.5
Call empty interface method 15
Call Iterator:next() on a HashMap 165
Call put() on a HashMap 600
Inflate 1 View from XML 22,000
Inflate 1 LinearLayout containing 1 TextView 25,000
Inflate 1 LinearLayout containing 6 View objects 100,000
Inflate 1 LinearLayout containing 6 TextView objects 135,000
Launch an empty activity 3,000,000

结束语

写高效的嵌入式程序的最好方法就是要搞清楚你写的程序究竟做了些什么。如果你真的想分配一个iterator类,进一切方法的在一个List中使用增强型的for循环,使它成为一个有意而为之的做法,而不是一个无意的疏漏而产生负面影响。

有备无患,搞清楚你在做什么!你可以假如你自己的一些行为准则,但是一定要注意你的代码正在做什么,然后开始寻找方法去优化它。

分享到:
评论

相关推荐

    如何编写高效的ANDROID代码

    本文将基于提供的文件内容,深入探讨几个关键点,帮助开发者掌握编写高效Android代码的核心技巧。 #### 一、资源统计原则 在开始讨论具体的优化方法之前,我们需要明确一个基本原则:**资源统计原则**。这指的是在...

    编写高效的Android代码

    ### 编写高效的Android代码 在移动应用开发领域,特别是在Android平台上进行开发时,编写高效、性能优化的代码是至关重要的。高效的代码不仅能够提升应用的运行速度,还能有效降低内存消耗,这对于移动设备尤为重要...

    如何编写高效的Android代码

    总的来说,编写高效的Android代码需要对内存管理、线程调度、布局设计、代码结构优化、资源管理和性能分析有深刻理解。通过不断学习和实践,开发者可以构建出更加高效、流畅的应用,提供更好的用户体验。

    如何编写高效代码

    在IT领域,尤其是针对Android平台的开发,编写高效代码是确保应用程序性能的关键。本文将深入探讨高效代码编写的核心原则与实践技巧,帮助开发者优化程序,提升用户体验。 #### 引言 Android设备作为高性能的...

    教你写高效的android代码

    在Android开发中,编写高效的代码是提升应用性能和用户体验的关键。本教程将深入探讨如何在Android平台上实现这一目标,特别关注Java语言的应用。我们将从以下几个方面进行详细讲解: 1. **内存管理**:理解Android...

    编写高效的android代码.pptx

    看过后受益匪浅,知道大神和菜鸟的区别了,值得大力推荐,好好学习吧

    Eclipse编写的Android时钟应用实例

    在本实例中,我们探讨的是如何使用Eclipse V4.2.0开发一个Android时钟应用,这将涉及Android...通过这个实例,开发者不仅可以掌握Android应用的基本构建过程,还能深入理解如何在Eclipse环境中高效地进行Android开发。

    Android记事本程序代码

    在编码实践中,Android Studio作为集成开发环境(IDE)提供了丰富的工具支持,包括代码自动完成、调试器和模拟器等,帮助开发者高效地编写和测试代码。 总之,“Android记事本程序代码”项目涵盖了Android应用开发...

    个人编写Android进程管理器源代码,可完美运行

    在Android系统中,进程管理是至关重要的一个环节,它涉及到应用程序的运行效率、资源占用以及系统的稳定性。...通过研究这个个人编写的源代码,开发者不仅能提升技能,还能为自己的应用提供更高效的进程管理方案。

    Android-一个简洁的汇率转换软件使用kotlin编写

    Anko是Kotlin的一个非常有用的库,由JetBrains团队维护,它为Android开发者提供了简洁的DSL(领域特定语言)来替代XML布局文件,使UI编写更加直观和高效。Anko库包含对Android SDK的许多便捷函数,例如意图(Intent...

    使用Android studio的集成开发环境编写的Android studio的代码

    在Android应用开发领域,Android Studio是Google官方推荐的集成开发环境(IDE),它为开发者提供了高效、便捷的编码工具和丰富的功能。这篇压缩包文件的内容显然聚焦于使用Android Studio进行编程,并且包含了图片...

    Android 源代码

    首先,Android源代码是用Java和C/C++编写的,包含了操作系统内核、运行库层、应用程序框架以及预装的应用程序。了解源代码可以帮助开发者深入理解Android系统的工作原理,从而更好地调试、优化和定制系统。 1. **...

    Android Studio编写的简单数字时钟

    **Android Studio 编写的简单数字时钟** 在Android开发中,创建一个自定义的数字时钟可以提升应用程序的用户体验和个性化。本项目是基于Android Studio 3.2.1构建的,采用了一种高效且易懂的方法来实现:在子线程中...

    在android中编写干净,简短和可测试代码的最佳实践.zip

    在Android开发中,编写干净、简短且可测试的代码是至关重要的,这不仅可以提高代码质量,提升软件的可维护性,还能使团队合作更加高效。以下是一些基于标题和描述所涉及的最佳实践: 1. **模块化设计**:将应用程序...

    android源代码分析电子书

    源代码分析还涉及到实际的开发技巧,例如如何优化UI性能,如何使用调试工具进行问题定位,以及如何编写高效、可维护的代码。这对于提升开发者技能和解决实际问题非常有帮助。 八、安全与隐私 Android源代码分析还...

    android开发艺术代码

    在Android开发中,"代码艺术"指的是编写优雅、高效的代码,它包括了良好的编程习惯、设计模式的应用、性能优化以及错误处理等多方面内容。以下是一些关键的知识点: 1. **编程基础**:掌握Java或Kotlin语言,这是...

    android源代码讲解以及导入

    通过正确导入源代码到Eclipse,配合详细的文档和讲解,开发者可以更好地理解Android的工作原理,从而编写出更高效、更稳定的应用程序。同时,这也为定制和优化Android系统提供了可能,无论是对于个人开发者还是企业...

Global site tag (gtag.js) - Google Analytics