李斯维

李斯维 查看完整档案

北京编辑  |  填写毕业院校  |  填写所在公司/组织 lee-swifter.github.io/ 编辑
编辑

这个家伙很懒,总是喜欢宅在家里,以至于这里什么都没写...

个人动态

李斯维 发布了文章 · 1月9日

Android 应用的版本兼容 了解一下(理解 minSdkVersion、targetSdkVersion)

前言

众所周知,Android 适用于众多类型的设备,从手机到平板电脑和电视都能搭载使用。为了能在所有这些设备上顺利运行,Android 系统在应用到设备上时,必不可少的需要处理与 Android 应用的兼容性问题。这里就牵扯出两个概念:设备兼容性应用兼容性

  • 设备兼容性:设备能够正常运行我们编写的 Android 应用。
  • 应用兼容性:针对市面上千奇百怪的 Android 设备,应用是否兼容每一种可能的设备配置。

对于Android 应用开发者来说 ,我们无需担心设备是否兼容 Android,而是更加关注于我们开发的应用能够在尽可能多的 Android 设备上正常运行,即,上面所说的应用兼容性。

而一个应用的兼容性所覆盖的内容较多,从设备功能到平台版本再到屏幕配置,以及针对不同的国家或语言做出的修改,每一部分能涉及相当多的内容。

这篇文章就先介绍最常见的版本兼容性,顺便帮助大家理解 Android 开发中常见的几个版本相关的属性:minSdkVersiontargetSdkVersionmaxSdkVersioncompileSdkVersion

分割线

自己设计版本兼容

在说明 Android 的应用兼容性之前,我们先做这么一个假设:如果我们自己是 Android API 的开发者,为了让更多的 Android 应用能够跑在我们的系统上,我们应该处理版本兼容问题

一、版本号如何确立

这里我们简单地把 Android 框架 API 想象为一个给其他开发者使用的库。如果我们开发了一个库,让别的开发者拿去用,那么第一个问题就来了,那就是版本号的问题。在几乎所有情况下,我们每发一个版本,都会用一个依次增长的整数来表明这个库是什么版本。这里我们也从1开始来发一个版本:

// 版本号 1 的 平台 API
public boolean doSomething()    { /*do something ... */     return true;}
public void print(){
    System.out.println("hello version 1");
}

好了,那么这就是我们发的版本号为1的库了。里面包含了两个方法,一个返回库的版本号,一个仅仅做了打印的操作。

二、升级能直接修改代码吗

随着时间的推移,我们现在需要升级一下这个库。之前已经确定了版本号为一个不断增加的整数,之前发的版本号为1,那我们现在需要更新版本号为2的库了。

那么问题就来了,我们现在发现之前库中print()这个方法不太好,或者说打印的字符串不太对,那我们要怎么修改。这就牵扯出以下三个问题了:

  • 能不能删除之前版本中的方法
  • 能不能直接修改之前版本中的方法实现
  • 能不能直接修改方法名

很遗憾的说,上面的三个问题的答案都是否定的。如果直接删除print()方法,那么外部使用之前版本的应用一旦升级,就会因为找不到方法而崩溃;同理,修改方法名也是一样的;至于直接修改方法中的实现呢,那造成的后果可能会更加严重。这里如果我们更改了打印的字符串,外部引用会明显的发现这里不对,跟上个版本不一样;如果你说一个字符串还好,那我可以举一个极端的例子,原本这个方法是打开摄像头的,但是下一个版本你改成闪光灯了,这样外部引用此库的应用升级库版本之后是完全无法工作的。

这里就需要确认一个升级库的约定:向后兼容,进行新增更改!

虽然老的方法不能删除,那么我们可以增加新的方法啊,并且为了标识老的方法不再被维护,可以添加@Deprecated注解。于是下面就是我们的版本号为2的库:

//版本号2 的 平台 API 
public boolean doSomething()    { /*do something ... */     return true;}
@Deprecated
public void print(){
    System.out.println("hello version 1");
}
public void printNew() {
    System.out.println("hello verson 2");
}

这里在这个版本号为2的库中,增加了一个printNew()方法,并保留了版本1中的所有内容,只不过将print()方法标记为废弃了。

在开发一个依赖库的时候,我们需要只要考虑到这两方面的问题就基本可以解决版本兼容问题。但 Android API 版本并不是一个依赖库,它还需要被安装在各个设备上,因此我们还需要往下讨论几个问题。

三、应用需要告知什么信息

在继续讨论之前,我们先把依赖库的设想放下,而是将我们开发的东西想象为 Android API,它需要被安装在各种设备之上。由于我们刚发了两个版本,那么现在市面上就会有版本号为1和版本号为2的设备,而未来会有更多的版本号。

现在有这么一个应用,它是依赖版本号为 2 的 API 版本开发的,而且在这个应用中使用到了 printNew()方法。那这问题就来了,这个应用能安装在市面上所有的设备上么?显然不能:假如它被安装在一个 API 版本号为 1 设备上,而这个设备上是没有printNew()方法的,这样的话,应用就会因为找不到方法而崩溃。

因此,一个应用,在被安装到设备上时,必须能够告知设备一些信息。在这里,必须告知设备的信息就是应用在开发时是基于哪一个 API 版本进行开发的。但是在第二条里,我们确定了平台 API 的开发必须是新增更改,这就意味着一个应用如果是基于某一个平台版本开发的,那么在这个平台版本后续的版本上也能够完全支持这个应用。在 Android 开发里就是:Android 应用一般向前兼容新版本的 Android 平台,这个我们后面再说。

于是乎我们需要知道并不是这个应用是基于哪个 API 版本进行开发的,而是它最低能跑在哪个 API 版本之上。在这里,由于它使用了在API版本号为1的平台中没有的printNew()方法,因此这个应用只能指定为 2 了。而且由于保证了上面的平台 API 升级的约定,它既然能在版本号 2 上跑,那么它也就能够在 3、4、5... 以及后续的所有平台版本上跑了。

因为这是应用需要告知我们的信息,所以它需要在应用开发时指定,这里我们先命名它为 minSdkVersion,对于 Android 应用,我们就在 AndroidManifest.xml清单文件中指定:

//应用提供的信息
<uses-sdk android:minSdkVersion="2" />

四、提供一个信息就足够了么

现在应用只告诉了平台它能支持的最低版本,那现在我们就需要想一想了,仅仅告知这个信息足够么?

在回答此问题之前,我们来升级下前面写的平台 API。之前的版本为 2,且添加了printNew()方法,并打印了一个字符串。大家知道System.out.println()这个是在Java 中常用的方法,但是在 Android 中,我们常用android.util.Log工具类来打印某些文本。

但是由于之前我们确定了升级API的原则为新增更改,那么就意味着直接修改代码是绝对不行的,否则应用在新的平台上的行为会改变,这可不是我们想看到的。对于这个问题,我们应该怎么办呢?

既然不能直接修改,那么原来代码显然是要保留的,针对老平台编译的使用之前的打印方式,针对新平台那么我们就采用新的打印方式好了。那答案就出来了,我们可以在运行时判断。那么,版本号为 3 的平台 API 就出来了:

//版本号3 的 平台 API 
public boolean doSomething()    { /*do something ... */     return true;}
@Deprecated
public void print(){
    System.out.println("hello version 1");
}
public void printNew() {
    if(应用使用的API版本 <= 2) {
        System.out.println("hello verson 2");
    } else {
        Log.d("tag", "hello version > 2");
    }
}

通过这个代码我们就知道了,应用仅仅告诉我们它支持的最小 API 是不够的,我们还需要知道应用是基于哪个平台版本开发和测试的,在这里,如果应用是使用 2 版本,那么就用System.out.println,如果用的是之后的版本开发的,那我们就用android.util.Log来打印。这样就可以保证应用跑在任何设备上都是其想要的行为了。

于是,我们需要再定义一个应用针对哪个版本开发和测试的的属性,这里我们将其命名为targetSdkVersion。这样,最终应用的清单文件为:

<uses-sdk android:minSdkVersion="2"
          android:targetSdkVersion="3" />

五、版本兼容设计完成

这样看来好像没有其他问题了。那么现在总结一下,我们自己的平台API版本控制有这么四点需要注意的:

  1. 版本号的确立(从1开始增加的整数);
  2. 版本升级的原则,与所有早期版本保持兼容;
  3. 应用需要告知支持的最小平台版本号;
  4. 应用需要告知针对哪个版本进行开发和测试;

如果我们自己构建 API,大概就是这些问题了。

那么接下来,我们就来看看 Android 官方是如何处理这些问题的。
分割图

Android 的版本兼容

依照我们前面设计的四个问题,我们来依照顺序来看 Android 官方是怎么处理的。

Android API 级别

API 级别是对 Android 平台版本提供的框架 API 修订版进行唯一标识的整数值。

Android 平台提供的框架 API 使用称为“API 级别”的整数标识符指定。每个 Android 平台版本恰好支持一个 API 级别,但隐含对所有早期 API 级别(低至 API 级别 1)的支持。Android 平台初始版本提供的是 API 级别 1,后续版本的 API 级别则依次增加。

可见,Android 官方的版本号设计也是与我们所设计的版本号类似,都是从 1 开始的整数,并依次增加。官方还给出了Android 平台版本所支持的 API 级别,这里就不贴了,想看的话可以点文末的链接或者去 Android 的官方网站看看。

Android API 级别的兼容性

Android 平台的每个后续版本均可包括其提供的 Android 应用框架 API 的更新。

框架 API 更新的设计用途是使新 API 与早期版本的 API 保持兼容。换言之,大多数 API 更改都是新增更改,并且会引入新功能或替代功能。在 API 的某些部分得到升级时,系统会弃用经替换的旧版部分,但不会将其移除,以便其仍可供现有应用使用。在极少数情况下,系统可能会修改或移除 API 的某些部分,但通常只有在为确保 API 稳健性以及应用或系统安全性时,才需要进行此类更改。所有其他来自早期修订版的 API 部分都将继续保留,不做任何修改。

这里能看出 Android 的版本升级与我们设计的一样,首先就是要保证与早期版本的 API 兼容。在继续讨论应用的兼容性前我们先聊两个概念:

应用向前兼容性
Android 应用一般向前兼容新版本的 Android 平台。

由于几乎所有对框架 API 的更改都是新增更改,所以使用 API 任何给定版本(其 API 级别所指定版本)开发的 Android 应用均向前兼容更新版本的 Android 平台以及更高 API 级别。应用应能在所有后期版本的 Android 平台上运行,除非在个别情况下,系统后来因某种原因将应用使用的某个 API 部分移除。

应用向后兼容性
Android 应用未必向后兼容比其编译时所用目标版本更旧的 Android 平台版本。

每个新版本的 Android 平台都可能包含新的框架 API,例如能够让应用使用新的平台功能或替换现有 API 部分的 API。在新平台上运行时,应用可以使用这些新 API;且如上所述,在更新版本的平台(API 级别所指定的平台)上运行时,应用也可使用这些新 API。反之,由于早期版本的平台未包含新 API,因此使用新 API 的应用无法在这些平台上运行。

作为应用开发者,通过上面的描述咱们可以简单理解为:一个应用如果能在当前的API级别上跑,那么就可以在以后的API上,但未必能在早期的API上跑。于是乎,为了让平台知道这个应用能不能再自己的这个版本上跑,应用就需要提供一些信息。这就是我们提出的第三和第四个问题了。

Android 应用选择平台版本和 API 级别

首先,我们上面分析过了,应用必须向外面告知minSdkVersiontargetSdkVersion。在Android 上,是这么描述这个两个属性的,以及maxSdkVersion这个属性:

android:minSdkVersion
指定能够运行应用的最低 API 级别。默认值为“1”。

应用在 android:minSdkVersion 中声明 API 级别的主要原因是,告知 Android 系统,其正使用在指定 API 级别引入的 API。如果由于某种原因将应用安装在 API 级别较低的平台上,则它会在运行时试图访问不存在的 API 时发生崩溃。如果应用所需的最低 API 级别高于目标设备上平台版本的 API 级别,则系统不允许安装该应用,以防出现这种结果。

例如,android.appwidget 软件包是随 API 级别 3 引入的。如果应用使用该 API,则必须使用“3”一值声明 android:minSdkVersion 属性。随后,应用便可安装在 Android 1.5(API 级别 3)和 Android 1.6(API 级别 4)等平台上,但不能安装在 Android 1.1(API 级别 2)和 Android 1.0(API 级别 1)平台上。

android:targetSdkVersion
指定运行应用的目标 API 级别。在某些情况下,此属性允许应用使用在目标 API 级别中定义的清单元素或行为,而非仅限于使用针对最低 API 级别定义的元素或行为。

targetSdkVersion 属性不会阻止您的应用安装在高于指定值的平台版本上,但它很重要,因为它向系统指示您的应用是否应继承较新版本中的行为更改。如果您不将 targetSdkVersion 更新到最新版本,则系统会认为您的应用在最新版本上运行时需要一些向后兼容性行为。例如,在 Android 4.4 中的行为更改中,使用 AlarmManager API 创建的闹钟现在默认不精确,因此系统可以批量处理应用闹钟并节省系统电量,但如果您的目标 API 级别低于“19”,则系统会为您的应用保留之前的 API 行为。

这里通过一张图是最能说明这个属性是怎么用的了:
AlarmManager的构造和cancel方法
这是 AlarmManager的构造和cancel()方法。首先在构造方法中获取到应用指定的targetSdkVersion并存放在mTargetSdkVersion中。在cancel()方法里,Android 会判断应用针对哪个 API 级别开发和测试的。可以看到应用针对新的API级别和老的级别,反应到平台上,其行为是不一样的。

我们在看 Android 的源码时,会经常发现这样的代码,使用方法也类似,从这些代码中就能够看出targetSdkVersion的作用了。

android:maxSdkVersion
指定能够运行应用的最高 API 级别。其值必须大于或等于系统的 API 级别整数。如果未声明,则系统假定应用没有最高 API 级别。

不建议声明该属性。首先,您没有必要设置该属性,并将其作为阻止您的应用部署至新版本 Android 平台的一种手段。从设计上讲,新版本平台完全向后兼容。只要您的应用仅使用标准 API 并遵循部署最佳实践,其应能够在新版本平台上正常工作。其次,请注意在某些情况下,声明该属性可能会导致您的应用在系统更新至更高 API 级别后从用户设备中移除。大多数可能安装您应用的设备都会定期收到 OTA 系统更新,因此您应在设置该属性前考虑这些更新对应用的影响。

总结一下就是不要声明该属性,甚至你可以忘掉这个属性的存在。

compileSdkVersion

至于这个声明,其实不用太在意,它只是我们在查看源代码和编译时才发挥作用的,它与应用兼容性关系不大。它指定了 Gradle 用哪个版本的 API 级别来编译你的应用,这样你在代码里就能够使用这个 API 级别提供的方法和功能。

一般来说我会把这个属性设置为与targetSdkVersion相同,这样在点击查看某个源码时,查看的就是要针对的 API 级别对应的源代码。不过只要compileSdkVersion不低于targetSdkVersion就行了,否则 Android Studio 会有这样的警告:
compileSdkVersion低于targetSdkVersion的警告
另外,如果你需要查看某个版本的 Android 源码,那你也可以更改这个值。例如,你更改compileSdkVersion为28,那从代码点进去查看到的Android源码就是来源于28的;更改为30,那点进去查看的就是30的源码。

现在我们掌握了这个几个属性的作用了吧。作为开发者,理解这几个属性并选取对应的 API 级别是比较重要的。下面就来一下总结。

总结

这篇文章里,我们先自己设想了一下如果自己设计 Android 的版本兼容会是怎么样,并设计解决了发现的4个问题。然后再进入到 Android 官方的设计思维中,并看到 Google 的大佬们是怎么解决这些问题的。并顺便理解了应用版本声明的几个属性 minSdkVersiontargetSdkVersionmaxSdkVersioncompileSdkVersion

关于 Android 的版本兼容,我想,基本理解到这里也就可以了,至少作为应用开发者,我们知道了怎么选minSdkVersiontargetSdkVersion版本号以及它们背后的意义了。
结束图


Android 兼容性-平台版本.
Android API 级别 use-sdk.

查看原文

赞 0 收藏 0 评论 0

李斯维 关注了用户 · 2020-11-16

Android开发者 @androiddevs

Android 最新开发技术更新,包括 Kotlin、Android Studio、Jetpack 和 Android 最新系统技术特性分享。

关注 561

李斯维 回答了问题 · 2019-07-10

解决怎么无法在Stackoverflow上提问?

这个问题我之前碰到过,原因也就是加载 JavaScript 失败。如果不翻墙的话,基本都会遇到这个问题。
Stack Overflow 这个网站虽然在国内能访问,但是想用的话,就必须得挂代理,否则注册登录都不能用。

关注 2 回答 2

李斯维 赞了文章 · 2019-05-18

Android 静态代码分析工具

简评: 作者在文中提到的三个静态代码分析工具不是互相替代的关系,各有各的侧重点,如果有需要完全可以同时使用。

静态代码分析是指无需运行被测代码,仅通过分析或检查源程序的语法、结构、过程、接口等来检查程序的正确性,找出代码隐藏的错误和缺陷,如参数不匹配,有歧义的嵌套语句,错误的递归,非法计算,可能出现的空指针引用等等。

对于 Android 来说用得最多的三个静态代码分析工具当属:

  • Lint
  • PMD
  • Findbugs

Lint

Lint 是 Google 提供给 Android 开发者的静态代码分析工具,能帮助开发者优化代码和找到潜在的 bug。

配置:

在项目中创建 script-lint.gradle 文件:

android {
    lintOptions {
        lintConfig file("$project.rootDir/tools/rules-lint.xml")
        htmlOutput file("$project.buildDir/outputs/lint/lint.html")
        warningsAsErrors true
        xmlReport false
    }
}

其中两个重要的属性:

  • lintConfig : lint 规则文件的路径。
  • htmlOutput : html 报告生成的路径。

之后在 build.gradle 文件中引用:

apply plugin: 'com.android.application'
apply from: "$project.rootDir/tools/script-lint.gradle"

...

测试:

运行 ./gradlew lint 命令,就可以在上面设置的 htmlOutput 路径下找到 lint.html 文件,打开后就会看到类似下面的提示:
提示

Findbugs

Findbugs 分析的是 Java 字节码,能识别出上百种的潜在错误。

配置:

创建 script-findbugs.gradle文件:

apply plugin: 'findbugs'

task findbugs(type: FindBugs) {
    excludeFilter = file("$project.rootDir/tools/rules-findbugs.xml")
    classes = fileTree("$project.buildDir/intermediates/classes/dev/debug/com/dd")
    source = fileTree("$project.rootDir/src/main/java/com/dd/")
    classpath = files()

    reports {
        xml.enabled = false
        html.enabled = true
        html.destination = "$project.buildDir/outputs/findbugs/findbugs.html"
    }
}

属性:

  • excludeFilter:Findbugs 规则文件的路径。
  • classes:classes 文件的路径。
  • source:源码的路径。
  • html.destination:html 报告的生成地址。

同样需要在 build.gradle 中进行引用:

apply plugin: 'com.android.application'
apply from: "$project.rootDir/tools/script-findbugs.gradle"

...

测试:

下面的这段代码:

// MainActivity.java

...

private void someMethod(int variable) {
   switch (variable) {
       case 1:
           System.out.println("1");
       case 2:
           System.out.println("2");
   }
}

...

运行 ./gradlew findbugs 命令,findbugs.html 中就会生成检测结果,类似于下面这样:
检测结果

PMD

PMD 能发现常见的编程缺陷,比如未使用的变量、空的 catch 块、不必要的对象等等。

配置:

创建 script-pmd.gradle文件:

apply plugin: 'pmd'

task pmd(type: Pmd) {
    ruleSetFiles = files("$project.rootDir/tools/rules-pmd.xml")
    source = fileTree('src/main/java/')

    reports {
        xml.enabled = false
        html.enabled = true
        html.destination = "$project.buildDir/outputs/pmd/pmd.html"
    }
}

属性:

  • ruleSetFiles:规则文件路径。
  • source:源码路径。
  • html.destination:检测结果的 html 文件所在路径。

同样,再在 build.gradle 文件中引用:

apply plugin: 'com.android.application'
apply from: "$project.rootDir/tools/script-pmd.gradle"

...

测试:

// MainActivity.java

...

private void someMethod(int a, int b, int c, int d) {
   if (a > b) {
       if (b > c) {
           if (c > d) {
               if (d > a) {
                   // some logic
               }
           }
       }
   }
}

...

执行 ./gradlew pmd 命令,在 html.destination 设置的路径下找到 pmd.html,就能够看到如下的检测结果:
检测结果

上面提到的所有文件,和配置项都可以在作者的这个项目中找到:dmytrodanylyk/template


原文链接:Configuring Android Project — Static Code Analysis Tools
推荐阅读聊聊 Android StateListAnimator

欢迎关注微信号「极光开发者」,不定期赠书活动!

查看原文

赞 2 收藏 1 评论 1

李斯维 收藏了文章 · 2019-05-05

Android 网络优化,使用 HTTPDNS 优化 DNS,从原理到 OkHttp 集成

一、前言

谈到优化,首先第一步,肯定是把一个大功能,拆分成一个个细小的环节,再单个拎出来找到可以优化的点,App 的网络优化也是如此。

在 App 访问网络的时候,DNS 解析是网络请求的第一步,默认我们使用运营商的 LocalDNS 服务。有数据统计,在这一块 3G 网络下,耗时在 200~300ms,4G 网络下也需要 100ms。

解析慢,并不是 LocalDNS 最大的问题,它还存在一些更为严重的问题,例如:DNS 劫持、DNS 调度不准确(缓存、转发、NAT)导致性能退化等等,这些才是网络优化最应该解决的问题。

想要优化 DNS,现在最简单成熟的方案,就是使用 HTTPDNS。

今天就来聊聊,DNS、HTTPDNS,以及在 Android 下,如何使用 OKHttp 来集成 HTTPDNS。

二、DNS 和 HTTPDNS

2.1 什么是 DNS

在说到 HTTPDNS 之前,先简单了解一下什么是 DNS?

在网络的世界中,每个有效的域名背后都有为其提供服务的服务器,而我们网络通信的首要条件,就是知道服务器的 IP 地址。

但是记住域名(网址)肯定是比记住 IP 地址简单。如果有某种方法,可以通过域名,查到其提供服务的服务器 IP 地址,那就非常方便了。这里就需要用到 DNS 服务器以及 DNS 解析。

DNS(Domain Name System),它的作用就是根据域名,查出对应的 IP 地址,它是 HTTP 协议的前提。只有将域名正确的解析成 IP 地址后,后面的 HTTP 流程才可以继续进行下去。

DNS 服务器的要求,一定是高可用、高并发和分布式的服务器。它被分为多个层次结构。

  • 根 DNS 服务器:返回顶级域 DNS 服务器的 IP 地址。
  • 顶级域 DNS 服务器:返回权威 DNS 服务器的 IP 地址。
  • 权威 DNS 服务器:返回相应主机的 IP 地址。

这三类 DNS 服务器,类似一种树状的结构,分级存在。

当开始 DNS 解析的时候,如果 LocalDNS 没有缓存,那就会向 LocalDNS 服务器请求(通常就是运营商),如果还是没有,就会一级一级的,从根域名查对应的顶级域名,再从顶级域名查权威域名服务器,最后通过权威域名服务器,获取具体域名对应的 IP 地址。

DNS 在提供域名和 IP 地址映射的过程中,其实提供了很多基于域名的功能,例如服务器的负载均衡,但是它也带来了一些问题。

2.2 DNS 的问题

DNS 的细节还有很多,本文就不展开细说了,其问题总结来说就是几点。

1.不稳定

DNS 劫持或者故障,导致服务不可用。

2.不准确

LocalDNS 调度,并不一定是就近原则,某些小运营商没有 DNS 服务器,直接调用其他运营商的 DNS 服务器,最终直接跨网传输。例如:用户侧是移动运营商,调度到了电信的 IP,造成访问慢,甚至访问受限等问题。

3.不及时

运营商可能会修改 DNS 的 TTL(Time-To-Live,DNS 缓存时间),导致 DNS 的修改,延迟生效。

还有运营商为了保证网内用户的访问质量,同时减少跨网结算,运营商会在网内搭建内容缓存服务器,通过把域名强行指向内容缓存服务器的地址,来实现本地本网流量完全留在本地的目的。

对此不同运营商甚至实现都不一致,这对我们来说就是个黑匣子。

正是因为 DNS 存在种种问题,所以牵出了 HTTPDNS。

2.3 HTTPDNS 的解决方案

DNS 不仅支持 UDP,它还支持 TCP,但是大部分标准的 DNS 都是基于 UDP 与 DNS 服务器的 53 端口进行交互。

HTTPDNS 则不同,顾名思义它是利用 HTTP 协议与 DNS 服务器的 80 端口进行交互。不走传统的 DNS 解析,从而绕过运营商的 LocalDNS 服务器,有效的防止了域名劫持,提高域名解析的效率。

这就相当于,每家各自基于 HTTP 协议,自己实现了一套域名解析,自己去维护了一份域名与 IP 的地址簿,而不是使用同一的地址簿(DNS服务器)。

据说微信有自己部署的 NETDNS,而各大云服务商,阿里云和腾讯云也提供了自己的 HTTPDNS 服务,对于我们普通开发者,只需要付出少量的费用,在手机端嵌入支持 HTTPDNS 的客户端 SDK,即可使用。

三、 OKHttp 接入 HTTPDNS

既然了解了 HTTPDNS 的重要性,接下来看看如何在 OkHttp 中,集成 HTTPDNS。

OkHttp 是一个处理网络请求的开源项目,是 Android 端最火热的轻量级网络框架。在 OkHttp 中,默认是使用系统的 DNS 服务 InetAddress 进行域名解析。

InetAddress ip2= InetAddress.getByName("www.cxmydev.com");
System.out.println(ip2.getHostAddress());
System.out.println(ip2.getHostName());

而想在 OkHttp 中使用 HTTPDNS,有两种方式。

  1. 通过拦截器,在发送请求之前,将域名替换为 IP 地址。
  2. 通过 OkHttp 提供的 .dns() 接口,配置 HTTPDNS。

对这两种方法来说,当然是推荐使用标准 API 来实现了。拦截器的方式,也建议有所了解,实现很简单,但是有坑。

3.1 拦截器接入方式

1.拦截器接入

拦截器是 OkHttp 中,非常强大的一种机制,它可以在请求和响应之间,做一些我们的定制操作。

在 OkHttp 中,可以通过实现 Interceptor 接口,来定制一个拦截器。使用时,只需要在 OkHttpClient.Builder 中,调用 addInterceptor() 方法来注册此拦截器即可。

OkHttp 的拦截器不是本文的重点,我们还是回到拦截器去实现 HTTPDNS 的话题上,拦截器没什么好说的,直接上相关代码。

class HTTPDNSInterceptor : Interceptor{
    override fun intercept(chain: Interceptor.Chain): Response {
        val originRequest = chain.request()
        val httpUrl = originRequest.url()

        val url = httpUrl.toString()
        val host = httpUrl.host()

        val hostIP = HttpDNS.getIpByHost(host)
        val builder = originRequest.newBuilder()

        if(hostIP!=null){
            builder.url(HttpDNS.getIpUrl(url,host,hostIP))
            builder.header("host",hostIP)
        }
        val newRequest = builder.build()
        val newResponse = chain.proceed(newRequest)
        return newResponse
    }
}

在拦截器中,使用 HttpDNS 这个帮助类,通过 getIpByHost() 将 Host 转为对应的 IP。

如果通过抓包工具抓包,你会发现,原本的类似 http://www.cxmydev.com/api/user 的请求,被替换为:http://220.181.57.xxx/api/user

2.拦截器接入的坏处

使用拦截器,直接绕过了 DNS 的步骤,在请求发送前,将 Host 替换为对应的 IP 地址。

这种方案,在流程上很清晰,没有任何技术性的问题。但是这种方案存在一些问题,例如:HTTPS 下 IP 直连的证书问题、代理的问题、Cookie 的问题等等。

其中最严重的问题是,此方案(拦截器+HTTPDNS)遇到 https 时,如果存在一台服务器支持多个域名,可能导致证书无法匹配的问题。

在说到这个问题之前,就要先了解一下 HTTPS 和 SNI。

HTTPS 是为了保证安全的,在发送 HTTPS 请求之前,首先要进行 SSL/TLS 握手,握手的大致流程如下:

  1. 客户端发起握手请求,携带随机数、支持算法列表等参数。
  2. 服务端根据请求,选择合适的算法,下发公钥证书和随机数。
  3. 客户端对服务端证书,进行校验,并发送随机数信息,该信息使用公钥加密。
  4. 服务端通过私钥获取随机数信息。
  5. 双方根据以上交互的信息,生成 Session Ticket,用作该连接后续数据传输的加密密钥。

在这个流程中,客户端需要验证服务器下发的证书。首先通过本地保存的根证书解开证书链,确认证书可信任,然后客户端还需要检查证书的 domain 域和扩展域,看看是否包含本次请求的 HOST。

在这一步就出现了问题,当使用拦截器时,请求的 URL 中,HOST 会被替换成 HTTPDNS 解析出来的 IP。当服务器存在多域名和证书的情况下,服务器在建立 SSL/TLS 握手时,无法区分到底应该返回那个证书,此时的策略可能返回默认证书或者不返回,这就有可能导致客户端在证书验证 domain 时,出现不匹配的情况,最终导致 SSL/TLS 握手失败。

这就引发出来 SNI 方案,SNI(Server Name Indication)是为了解决一个服务器使用多个域名和证书的 SSL/TLS 扩展。

SNI 的工作原理,在连接到服务器建立 SSL 连接之前,先发送要访问站点的域名(hostname),服务器根据这个域名返回正确的证书。现在,大部分操作系统和浏览器,都已经很好的支持 SNI 扩展。

3.拦截器 + HTTPDNS 的解决方案

这个问题,其实也有解决方案,这里简单介绍一下。

针对 "domain 不匹配" 的问题,可以通过 hook 证书验证过程中的第二步,将 IP 直接替换成原来的域名,再执行证书验证。

而 HttpURLConnect,提供了一个 HostnameVerifier 接口,实现它即可完成替换。

public interface HostnameVerifier {
    public boolean verify(String hostname, SSLSession session);
}

如果使用 OkHttp,可以参考 OkHostnameVerifier (source://src/main/java/okhttp3/internal/tls/OkHostnameVerifier.java) 的实现,进行替换。

本身 OkHttp 就不建议通过拦截器去做 HTTPDNS 的支持,所以这里就不展开讨论了,这里只提出解决的思路,有兴趣可以研究研究源码。

3.2 OKHttp 标准 API 接入

OkHttp 其实本身已经暴露了一个 Dns 接口,默认的实现是使用系统的 InetAddress 类,发送 UDP 请求进行 DNS 解析。

我们只需要实现 OkHttp 的 Dns 接口,即可获得 HTTPDNS 的支持。

在我们实现的 Dns 接口实现类中,解析 DNS 的方式,换成 HTTPDNS,将解析结果返回。

class HttpDns : Dns {
    override fun lookup(hostname: String): List<InetAddress> {
        val ip = HttpDnsHelper.getIpByHost(hostname)
        if (TextUtils.isEmpty(ip)) {
            //返回自己解析的地址列表
            return InetAddress.getAllByName(ip).toList() 
        } else {
            // 解析失败,使用系统解析
            return Dns.SYSTEM.lookup(hostname)
        }
    }
}

使用也非常的简单,在 OkHttp.build() 时,通过 dns() 方法配置。

mOkHttpClient = httpBuilder
        .dns(HttpDns())
        .build();

这样做的好处在于:

  1. 还是用域名进行访问,只是底层 DNS 解析换成了 HTTPDNS,以确保解析的 IP 地址符合预期。
  2. HTTPS 下的问题也得到解决,证书依然使用域名进行校验。

OkHttp 既然暴露出 dns 接口,我们就尽量使用它。

四、小结时刻

现在大家知道,在做 App 的网络优化的时候,第一步就是使用 HTTPDNS 优化 DNS 的步骤。

所有的优化当然是以最终效果为目的,这里提两条大厂公开的数据,对腾讯的产品,在接入 HTTPDNS 后,用户平均延迟下降超过 10%,访问失败率下降超过五分之一。而百度 App 的 Feed 业务,Android 劫持率由 0.25% 降低到 0.05%。

此种优化方案,非常依赖 HTTPDNS 服务器,所以建议使用 阿里云、腾讯云 这样相对稳定的云服务商。

references:

【1】百度App网络深度优化系列一 DNS 优化

【2】SIN:https://blog.csdn.net/makenot...

【3】鹅厂网事,HTTPDNS 服务详解


公众号后台回复成长『成长』,将会得到我准备的学习资料,也能回复『加群』,一起学习进步;你还能回复『提问』,向我发起提问。

推荐阅读:

关于字符编码,你需要知道的都在这里 | 图解:HTTP 范围请求 | Java 异常处理 | 安卓防止用户关闭动画导致动画失效 | Git 找回遗失的代码 | 阿里的 Alpha 助力 App 启动速度优化

查看原文

李斯维 收藏了文章 · 2019-05-05

Android 开发工具推荐

简评: 自己过去在 Android 开发中发现的好工具,在这里分享给大家。: )

Library methods count

每一个 Android App 的开发中都会用到很多的库,这个工具能够让你看到不同库的大小和,帮助你回避 65K 方法数限制。

Stetho

Stetho 是 Facebook 构建的一个 Android debug 工具,能够和 Chrome 浏览器绑定,使得你可以通过 Chrome 来调试 Android 应用,比如查看网络请求情况和 SQLite 数据库。

LeakCanary

LeakCanary 是一个知名的用于分析 Android 内存泄漏的库。

APK method count

这个工具能够很清楚的展示出 Android APK 包中的方法数。

Material Design Icons

帮助你自定义自己的 Material Design 图标。

Buck

Buck 是一款 Facebook 发布的高性能构建工具。不仅能用于 Android 还支持 Go、iOS、Java、Groovy 等多种语言和平台。

ProGuard

ProGuard(或者 DexGuard)能帮助混淆你的代码,并减小应用包的体积。

Android Asset Studio

Android Asset Studio 是一个一站式的工具,能够生成应用图标、Action bar 图标、通知图标,生成 .9 图。

Device Art Generator

Device Art Generator 能方便的将应用截图嵌入到真是设备的效果图中。这样,当用户在你的网站上或其他宣传材料中看到你的应用截图时,就能更加直观地了解应用的内容环境。


欢迎关注微信号「极光开发者」

查看原文

李斯维 关注了专栏 · 2019-05-05

极光推送

极光是中国领先的移动大数据服务商。其团队核心成员来自腾讯、摩根士丹利、豆瓣、Teradata和中国移动等公司。公司自2011年成立以来专注于为app开发者提供稳定高效的消息推送、统计分析、即时通讯、短信和社会化分享组件等开发者服务。至今我们已经服务了超过50万款移动应用,累计覆盖超过80亿个移动终端,月独立活跃设备超过7亿,产品覆盖了中国国内90%以上的移动终端。

关注 999

李斯维 评论了文章 · 2019-04-20

浅谈移动端 View 的显示过程

作者:个推安卓开发工程师 一七

随着科技的发展,各种移动端早已成为人们日常生活中不可或缺的部分,人们使用移动端产品工作、社交、娱乐……移动端界面的流畅性已经成为影响用户体验的重要因素之一。那么你是否思考过移动端所展现的流畅画面是如何实现的呢?

本文通过对移动端View显示过程的简略分析,帮助开发者了解View渲染的逻辑,更好地优化自己的APP。

上图展示的是一个完整的页面渲染过程。通过上图,我们可以初步了解每一帧页面从代码布局的编写到展示给使用者,其背后的逻辑是如何一步一步执行的。

屏幕如何呈像

像素点

在电子屏幕中显示的图片,其实都是由一个个“小点”所组成的,这些“小点”被称为“像素点”。每一个像素点都有自己的颜色,每一张完整的图片都是由它们相连拼接形成的。

每个像素点一般都有 3 个子像素:红、绿、蓝,根据这三种原色,我们能够调制出各种各样的颜色。

大电视机

与现在的平板电视不同的是,以前的黑白电视机或者大背投彩电,总是带着大大的“后背”。“大后背”电视其实就是阴极射线管电视机,俗称显像管电视。其成像原理是电子枪发射出的电子束(阴极射线)通过聚焦系统和偏转系统,射向屏幕上涂有荧光层的指定位置。被电子束轰击的每个位置,荧光层都会产生一个小亮点,最终小亮点们将会组成一幅幅影像,显示在电视屏幕上。

这也是以前大电视机的屏幕都呈圆弧形的原因。因为越接近圆形,边长到中心的距离越相近,呈像越均匀。那为什么当磁铁贴近电视机时,会让电视机的成像出现问题呢?那是因为磁铁会干扰电子束的正常轨迹,并且在贴近屏幕的时候,也可能使得屏幕的荧光层磁化,出现一个个不正常的光斑。

下图展示的是摄像机慢放后,电子束的绘制过程。

LCD 和 OLED

随着科技的不断进步,电视、手机、电脑的体积越来越薄,射线管显像方式也逐渐被淘汰。目前在手机市场上占据主流地位的是 LCD 和 OLED 两种屏幕。

LCD 全称为 Liquid Crystal Display ,即液晶显示器。OLED 全称为 Organic Light-Emitting Diode ,即有机发光二极管。这两者之间存在显著的差别:

1. 两者成像原理不同
LCD 是靠白色的背光穿透彩色薄膜显色的,而 OLED 则是靠每个像素点自行发光。

2. 在耗电量方面
LCD的耗电量较高,即使只显示一个亮点,LCD 的背光源也需要一直发光,而且容易出现漏光现象。而OLED的每个像素都能独立工作,而且 可以自行发光,因此采用OLED的设备可以制作得更薄,甚至可以弯曲。

3.在制作方面
LCD使用的是无机材料, OLED 则需要使用有机材料,因此 OLED的制作费用更高,并且使用寿命不如 LCD 。

图形显示核心 GPU

与CPU相对比,GPU的计算单元更多,更擅长大规模并发计算,例如密码破解、图像处理等。CPU 则是遵循冯诺依曼架构存储程序顺序执行,在大规模并行计算能力上,受到的限制更大,因此更擅长逻辑控制。

应用程序编程接口 API (OpenGL)

在没有统一的 API 之前,开发者需要在各式各样的图形硬件上编写各种自定义接口和驱动程序,工作量极大。

1990 年 SGI(硅谷图形公司)成为了工作站 3D 图形领域的领导者,并将其 API 转变为一项开放标准,即 OpenGL。后来,SGI还促成了 OpenGL 架构审查委员会(OpenGL ARB)的创建。

垂直同步 Vertical Synchronization

当我们在使用手机 APP 的过程中,发现页面出现卡顿现象,那么极有可能是页面没有在 16ms 内更新导致的。实际上,人眼与大脑之间的协作无法感知超过 60fps 的画面更新。60fps 相当于是每秒 60 帧,那么每个页面需要在 1000/60 = 16ms 内更新为其他页面,才不会让我们感受到页面的卡顿。

而在没有 VSync 的情况下可能会出现以下情况:

如上图所示,在没有 VSync 的情况下,会出现需要显示第二帧时,其尚未处理完成的情况,因此Display 中显示的仍是第一帧。这会造成该帧显示时长超过16ms,从而导致页面卡顿的现象。

为了使 CPU、GPU 生成帧的速度与 Display 保持一致,Android 系统每 16ms 就会发出一次 VSYNC 信号,触发 UI 渲染更新。

从上图中我们可以看出,每隔 16ms ,安卓会发出一个 VSync 信号,收到信号后 CPU 开始处理下一帧的的内容,GPU 在 CPU 处理结束之后,将会进行光栅化,此时屏幕上显示的是上一帧已经处理完成的页面。如此反复,就可以在页面中展示一幅幅的指定画面。而确保画面流畅的前提是CPU 和 GPU 处理一帧所花费的时间不能超过 16 ms,否则就会出现以下情况:

当CPU 和 GPU 处理一帧的时间超过了16 ms时,在第一个 Display 中,由于 GPU 处理 B 画面的时间过长,导致系统发出 VSync 信号时, Display不能及时地显示出 B 画面,而重复显示A页面,造成卡顿。

此外,在第二个 Display 中,由于 A Buffer 还在被 Display 所使用,不能在收到 VSync 信号后开始处理下一帧的页面,导致该时间段内 CPU 的闲置。为了避免这种时间的浪费,三缓存机制由此出现:

如上图所示,在三缓存机制中,当 A 缓存被 Display 使用、B 缓存被 GPU 处理时,系统会发出 Vsync 信号,并加入新的缓存 C ,用来缓存下一帧的内容。这种方式虽然不能完全避免 A页面的重复显示,但是能够让后面页面的显示更加平滑。

View 的绘制流程

View 的绘制是从 ViewRootImpl 的 performTraversals() 方法开始的,其整体流程大致分为三步,如下图所示:

measure

控件测量过程从 performMeasure() 方法开始。在该方法中childWidthMeasureSpec 和 childHeightMeasureSpec,分别是用来确定宽度和高度的。

MeasureSpec 是一个 int 值,它存储着两个信息:低 30 位是 View 的 specSize,高 2 位是 View 的 specMode。

specMode 有三种类型:

1.UNSPECIFIED

父视图对子视图没有任何限制,可以将视图按照开发者的意愿设置成任意的大小,在一般开发过程中不会用到。

2.EXACTLY

父视图为子视图指定一个确切的尺寸,该尺寸由 specSize 的值来决定。

3.AT_MOST

父视图为子视图指定一个最大的尺寸,该尺寸的最大值是 specSize。

观察 View 的 measure() 方法,可以发现该方法是被 final 修饰的,因此 View 的子类只能够通过重载 onMeasure() 方法来完成自己的测量逻辑。


在 onMeasure() 方法中:

调用 getDefaultSize() 方法来获取视图的大小:

该方法中的第二个参数 measureSpec 是从 measure() 方法中传递过来的:通过 getMode() 和 getSize() 解析获取其中对应的值,再根据 specMode 给最终的 size 赋值。

不过以上只是一个简单控件的一次 measure 过程,在真正测量的过程中,由于一个页面往往包含多个子 View ,所以需要循环遍历测量,在 ViewGroup 中有一个 measureChildren() 方法,就是用来测量子视图的:

measure 整体流程的方法调用链如下:

layout

在performTraversals() 方法的测量过程结束后,进入 layout 布局过程:

performLayout(lp,desiredWindowWidth,desiredWindowHeight);

该过程的主要作用即根据子视图的大小以及布局参数,将相应的 View 放到合适的位置上。

host.layout(0,0,host.getMeasuredWidth(),host.getMeasuredHeight());

如上,layout() 方法接收了四个参数,按照顺时针,分别是左上右下。该坐标针对的是父视图,以左上为起始点,传入了之前测量出的宽度和高度。之后,让我们进入到 layout() 方法中观察:

我们通过 setFrame() 方法给四个变量赋值,判断 View 的位置是否变化以及是否需要重新进行 layout,而且其中还调用了 onLayout() 方法。

在进入该方法后,我们可以发现里面是空的,这是因为子视图的具体位置是相对于父视图而言的,所以 View 的 onLayout 为空实现。

再进入 ViewGroup 类中查看,我们可以发现,这其实是一个抽象的方法,在这样的情况下, ViewGroup 的子类便需要重写该方法:

draw

绘制的流程主要如下图所示,该流程也是存在遍历子 View 绘制的过程:

需要注意的是,View 的 onDraw() 方法是空的,这是因为每个视图的内容都不相同,这个部分交由子类根据自身的需要来处理,才更加合理:

安卓渲染机制的整体流程

1.APP 在 UI 线程构建 OpenGL 渲染需要的命令及数据;

2.CPU 将数据上传(共享或者拷贝)给 GPU 。(PC 上一般有显存,但是 ARM 这种嵌入式设备内存一般是 GPU 、 CPU 共享内存);

3.通知 GPU 渲染。一般而言,真机不会阻塞等待 GPU 渲染结束,通知结束后就返回执行其他任务;

4.通知 SurfaceFlinger 图层合成;

5.SurfaceFlinger 开始合成图层。

总结

移动端技术发展很快,而画面显示优化是一个持续发展的实践课题,贯穿于每个开发者的日常工作中。未来,个推技术团队将继续关注移动端的性能优化,为大家分享相关的技术干货。

查看原文

李斯维 关注了专栏 · 2019-04-19

个推技术学院

成立于2010年,是专业的数据智能服务商,致力于用数据让产业更智能。个推深耕开发者服务,并以海量的数据积累和创新的技术理念,构建了移动开发、用户增长、品牌营销、公共管理和智能风控等多领域的数据智能服务生态。

关注 1824

李斯维 收藏了文章 · 2019-04-17

浅谈移动端 View 的显示过程

作者:个推安卓开发工程师 一七

随着科技的发展,各种移动端早已成为人们日常生活中不可或缺的部分,人们使用移动端产品工作、社交、娱乐……移动端界面的流畅性已经成为影响用户体验的重要因素之一。那么你是否思考过移动端所展现的流畅画面是如何实现的呢?

本文通过对移动端View显示过程的简略分析,帮助开发者了解View渲染的逻辑,更好地优化自己的APP。

上图展示的是一个完整的页面渲染过程。通过上图,我们可以初步了解每一帧页面从代码布局的编写到展示给使用者,其背后的逻辑是如何一步一步执行的。

屏幕如何呈像

像素点

在电子屏幕中显示的图片,其实都是由一个个“小点”所组成的,这些“小点”被称为“像素点”。每一个像素点都有自己的颜色,每一张完整的图片都是由它们相连拼接形成的。

每个像素点一般都有 3 个子像素:红、绿、蓝,根据这三种原色,我们能够调制出各种各样的颜色。

大电视机

与现在的平板电视不同的是,以前的黑白电视机或者大背投彩电,总是带着大大的“后背”。“大后背”电视其实就是阴极射线管电视机,俗称显像管电视。其成像原理是电子枪发射出的电子束(阴极射线)通过聚焦系统和偏转系统,射向屏幕上涂有荧光层的指定位置。被电子束轰击的每个位置,荧光层都会产生一个小亮点,最终小亮点们将会组成一幅幅影像,显示在电视屏幕上。

这也是以前大电视机的屏幕都呈圆弧形的原因。因为越接近圆形,边长到中心的距离越相近,呈像越均匀。那为什么当磁铁贴近电视机时,会让电视机的成像出现问题呢?那是因为磁铁会干扰电子束的正常轨迹,并且在贴近屏幕的时候,也可能使得屏幕的荧光层磁化,出现一个个不正常的光斑。

下图展示的是摄像机慢放后,电子束的绘制过程。

LCD 和 OLED

随着科技的不断进步,电视、手机、电脑的体积越来越薄,射线管显像方式也逐渐被淘汰。目前在手机市场上占据主流地位的是 LCD 和 OLED 两种屏幕。

LCD 全称为 Liquid Crystal Display ,即液晶显示器。OLED 全称为 Organic Light-Emitting Diode ,即有机发光二极管。这两者之间存在显著的差别:

1. 两者成像原理不同
LCD 是靠白色的背光穿透彩色薄膜显色的,而 OLED 则是靠每个像素点自行发光。

2. 在耗电量方面
LCD的耗电量较高,即使只显示一个亮点,LCD 的背光源也需要一直发光,而且容易出现漏光现象。而OLED的每个像素都能独立工作,而且 可以自行发光,因此采用OLED的设备可以制作得更薄,甚至可以弯曲。

3.在制作方面
LCD使用的是无机材料, OLED 则需要使用有机材料,因此 OLED的制作费用更高,并且使用寿命不如 LCD 。

图形显示核心 GPU

与CPU相对比,GPU的计算单元更多,更擅长大规模并发计算,例如密码破解、图像处理等。CPU 则是遵循冯诺依曼架构存储程序顺序执行,在大规模并行计算能力上,受到的限制更大,因此更擅长逻辑控制。

应用程序编程接口 API (OpenGL)

在没有统一的 API 之前,开发者需要在各式各样的图形硬件上编写各种自定义接口和驱动程序,工作量极大。

1990 年 SGI(硅谷图形公司)成为了工作站 3D 图形领域的领导者,并将其 API 转变为一项开放标准,即 OpenGL。后来,SGI还促成了 OpenGL 架构审查委员会(OpenGL ARB)的创建。

垂直同步 Vertical Synchronization

当我们在使用手机 APP 的过程中,发现页面出现卡顿现象,那么极有可能是页面没有在 16ms 内更新导致的。实际上,人眼与大脑之间的协作无法感知超过 60fps 的画面更新。60fps 相当于是每秒 60 帧,那么每个页面需要在 1000/60 = 16ms 内更新为其他页面,才不会让我们感受到页面的卡顿。

而在没有 VSync 的情况下可能会出现以下情况:

如上图所示,在没有 VSync 的情况下,会出现需要显示第二帧时,其尚未处理完成的情况,因此Display 中显示的仍是第一帧。这会造成该帧显示时长超过16ms,从而导致页面卡顿的现象。

为了使 CPU、GPU 生成帧的速度与 Display 保持一致,Android 系统每 16ms 就会发出一次 VSYNC 信号,触发 UI 渲染更新。

从上图中我们可以看出,每隔 16ms ,安卓会发出一个 VSync 信号,收到信号后 CPU 开始处理下一帧的的内容,GPU 在 CPU 处理结束之后,将会进行光栅化,此时屏幕上显示的是上一帧已经处理完成的页面。如此反复,就可以在页面中展示一幅幅的指定画面。而确保画面流畅的前提是CPU 和 GPU 处理一帧所花费的时间不能超过 16 ms,否则就会出现以下情况:

当CPU 和 GPU 处理一帧的时间超过了16 ms时,在第一个 Display 中,由于 GPU 处理 B 画面的时间过长,导致系统发出 VSync 信号时, Display不能及时地显示出 B 画面,而重复显示A页面,造成卡顿。

此外,在第二个 Display 中,由于 A Buffer 还在被 Display 所使用,不能在收到 VSync 信号后开始处理下一帧的页面,导致该时间段内 CPU 的闲置。为了避免这种时间的浪费,三缓存机制由此出现:

如上图所示,在三缓存机制中,当 A 缓存被 Display 使用、B 缓存被 GPU 处理时,系统会发出 Vsync 信号,并加入新的缓存 C ,用来缓存下一帧的内容。这种方式虽然不能完全避免 A页面的重复显示,但是能够让后面页面的显示更加平滑。

View 的绘制流程

View 的绘制是从 ViewRootImpl 的 performTraversals() 方法开始的,其整体流程大致分为三步,如下图所示:

measure

控件测量过程从 performMeasure() 方法开始。在该方法中childWidthMeasureSpec 和 childHeightMeasureSpec,分别是用来确定宽度和高度的。

MeasureSpec 是一个 int 值,它存储着两个信息:低 30 位是 View 的 specSize,高 2 位是 View 的 specMode。

specMode 有三种类型:

1.UNSPECIFIED

父视图对子视图没有任何限制,可以将视图按照开发者的意愿设置成任意的大小,在一般开发过程中不会用到。

2.EXACTLY

父视图为子视图指定一个确切的尺寸,该尺寸由 specSize 的值来决定。

3.AT_MOST

父视图为子视图指定一个最大的尺寸,该尺寸的最大值是 specSize。

观察 View 的 measure() 方法,可以发现该方法是被 final 修饰的,因此 View 的子类只能够通过重载 onMeasure() 方法来完成自己的测量逻辑。


在 onMeasure() 方法中:

调用 getDefaultSize() 方法来获取视图的大小:

该方法中的第二个参数 measureSpec 是从 measure() 方法中传递过来的:通过 getMode() 和 getSize() 解析获取其中对应的值,再根据 specMode 给最终的 size 赋值。

不过以上只是一个简单控件的一次 measure 过程,在真正测量的过程中,由于一个页面往往包含多个子 View ,所以需要循环遍历测量,在 ViewGroup 中有一个 measureChildren() 方法,就是用来测量子视图的:

measure 整体流程的方法调用链如下:

layout

在performTraversals() 方法的测量过程结束后,进入 layout 布局过程:

performLayout(lp,desiredWindowWidth,desiredWindowHeight);

该过程的主要作用即根据子视图的大小以及布局参数,将相应的 View 放到合适的位置上。

host.layout(0,0,host.getMeasuredWidth(),host.getMeasuredHeight());

如上,layout() 方法接收了四个参数,按照顺时针,分别是左上右下。该坐标针对的是父视图,以左上为起始点,传入了之前测量出的宽度和高度。之后,让我们进入到 layout() 方法中观察:

我们通过 setFrame() 方法给四个变量赋值,判断 View 的位置是否变化以及是否需要重新进行 layout,而且其中还调用了 onLayout() 方法。

在进入该方法后,我们可以发现里面是空的,这是因为子视图的具体位置是相对于父视图而言的,所以 View 的 onLayout 为空实现。

再进入 ViewGroup 类中查看,我们可以发现,这其实是一个抽象的方法,在这样的情况下, ViewGroup 的子类便需要重写该方法:

draw

绘制的流程主要如下图所示,该流程也是存在遍历子 View 绘制的过程:

需要注意的是,View 的 onDraw() 方法是空的,这是因为每个视图的内容都不相同,这个部分交由子类根据自身的需要来处理,才更加合理:

安卓渲染机制的整体流程

1.APP 在 UI 线程构建 OpenGL 渲染需要的命令及数据;

2.CPU 将数据上传(共享或者拷贝)给 GPU 。(PC 上一般有显存,但是 ARM 这种嵌入式设备内存一般是 GPU 、 CPU 共享内存);

3.通知 GPU 渲染。一般而言,真机不会阻塞等待 GPU 渲染结束,通知结束后就返回执行其他任务;

4.通知 SurfaceFlinger 图层合成;

5.SurfaceFlinger 开始合成图层。

总结

移动端技术发展很快,而画面显示优化是一个持续发展的实践课题,贯穿于每个开发者的日常工作中。未来,个推技术团队将继续关注移动端的性能优化,为大家分享相关的技术干货。

查看原文

认证与成就

  • 获得 29 次点赞
  • 获得 102 枚徽章 获得 4 枚金徽章, 获得 35 枚银徽章, 获得 63 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2015-03-25
个人主页被 1.4k 人浏览