注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

深入理解 CocoaPods

CocoaPods 是开发 OS X 和 iOS 应用程序的一个第三方库的依赖管理工具。利用 CocoaPods,可以定义自己的依赖关系 (称作 pods),并且随着时间的变化,以及在整个开发环境中对第三方库的版本管理非常方便。CocoaPods 背...
继续阅读 »

CocoaPods 是开发 OS X 和 iOS 应用程序的一个第三方库的依赖管理工具。利用 CocoaPods,可以定义自己的依赖关系 (称作 pods),并且随着时间的变化,以及在整个开发环境中对第三方库的版本管理非常方便。

CocoaPods 背后的理念主要体现在两个方面。首先,在工程中引入第三方代码会涉及到许多内容。针对 Objective-C 初级开发者来说,工程文件的配置会让人很沮丧。在配置 build phases 和 linker flags 过程中,会引起许多人为因素的错误。CocoaPods 简化了这一切,它能够自动配置编译选项。

其次,通过 CocoaPods,可以很方便的查找到新的第三方库。当然,这并不是说你可以简单的将别人提供的库拿来拼凑成一个应用程序。它的真正作用是让你能够找到真正好用的库,以此来缩短我们的开发周期和提升软件的质量。

本文中,我们将通过分析 pod 安装 (pod install) 的过程,一步一步揭示 CocoaPods 背后的技术。

核心组件

CocoaPods是用 Ruby 写的,并由若干个 Ruby 包 (gems) 构成的。在解析整合过程中,最重要的几个 gems 分别是: CocoaPods/CocoaPodsCocoaPods/Core, 和 CocoaPods/Xcodeproj (是的,CocoaPods 是一个依赖管理工具 -- 利用依赖管理进行构建的!)。

编者注 CocoaPods 是一个 objc 的依赖管理工具,而其本身是利用 ruby 的依赖管理 gem 进行构建的

CocoaPods/CocoaPod

这是是一个面向用户的组件,每当执行一个 pod 命令时,这个组件都将被激活。该组件包括了所有使用 CocoaPods 涉及到的功能,并且还能通过调用所有其它的 gems 来执行任务。

CocoaPods/Core

Core 组件提供支持与 CocoaPods 相关文件的处理,文件主要是 Podfile 和 podspecs。

Podfile

Podfile 是一个文件,用于定义项目所需要使用的第三方库。该文件支持高度定制,你可以根据个人喜好对其做出定制。更多相关信息,请查阅 Podfile 指南

Podspec

.podspec 也是一个文件,该文件描述了一个库是怎样被添加到工程中的。它支持的功能有:列出源文件、framework、编译选项和某个库所需要的依赖等。

CocoaPods/Xcodeproj

这个 gem 组件负责所有工程文件的整合。它能够对创建并修改 .xcodeproj 和 .xcworkspace 文件。它也可以作为单独的一个 gem 包使用。如果你想要写一个脚本来方便的修改工程文件,那么可以使用这个 gem。

运行 pod install 命令

当运行 pod install 命令时会引发许多操作。要想深入了解这个命令执行的详细内容,可以在这个命令后面加上 --verbose。现在运行这个命令 pod install --verbose,可以看到类似如下的内容:

$ pod install --verbose

Analyzing dependencies

Updating spec repositories
Updating spec repo `master`
$ /usr/bin/git pull
Already up-to-date.


Finding Podfile changes
- AFNetworking
- HockeySDK

Resolving dependencies of `Podfile`
Resolving dependencies for target `Pods' (iOS 6.0)
- AFNetworking (= 1.2.1)
- SDWebImage (= 3.2)
- SDWebImage/Core

Comparing resolved specification to the sandbox manifest
- AFNetworking
- HockeySDK

Downloading dependencies

-> Using AFNetworking (1.2.1)

-> Using HockeySDK (3.0.0)
- Running pre install hooks
- HockeySDK

Generating Pods project
- Creating Pods project
- Adding source files to Pods project
- Adding frameworks to Pods project
- Adding libraries to Pods project
- Adding resources to Pods project
- Linking headers
- Installing libraries
- Installing target `
Pods-AFNetworking` iOS 6.0
- Adding Build files
- Adding resource bundles to Pods project
- Generating public xcconfig file at `
Pods/Pods-AFNetworking.xcconfig`
- Generating private xcconfig file at `
Pods/Pods-AFNetworking-Private.xcconfig`
- Generating prefix header at `
Pods/Pods-AFNetworking-prefix.pch`
- Generating dummy source file at `
Pods/Pods-AFNetworking-dummy.m`
- Installing target `
Pods-HockeySDK` iOS 6.0
- Adding Build files
- Adding resource bundles to Pods project
- Generating public xcconfig file at `
Pods/Pods-HockeySDK.xcconfig`
- Generating private xcconfig file at `
Pods/Pods-HockeySDK-Private.xcconfig`
- Generating prefix header at `
Pods/Pods-HockeySDK-prefix.pch`
- Generating dummy source file at `
Pods/Pods-HockeySDK-dummy.m`
- Installing target `
Pods` iOS 6.0
- Generating xcconfig file at `
Pods/Pods.xcconfig`
- Generating target environment header at `
Pods/Pods-environment.h`
- Generating copy resources script at `
Pods/Pods-resources.sh`
- Generating acknowledgements at `
Pods/Pods-acknowledgements.plist`
- Generating acknowledgements at `
Pods/Pods-acknowledgements.markdown`
- Generating dummy source file at `
Pods/Pods-dummy.m`
- Running post install hooks
- Writing Xcode project file to `
Pods/Pods.xcodeproj`
- Writing Lockfile in `
Podfile.lock`
- Writing Manifest in `
Pods/Manifest.lock`

Integrating client project

可以上到,整个过程执行了很多操作,不过把它们分解之后,再看看,会发现它们都很简单。让我们逐步来分析一下。

读取 Podfile 文件

你是否对 Podfile 的语法格式感到奇怪过,那是因为这是用 Ruby 语言写的。相较而言,这要比现有的其他格式更加简单好用一些。

在安装期间,第一步是要弄清楚显示或隐式的声明了哪些第三方库。在加载 podspecs 过程中,CocoaPods 就建立了包括版本信息在内的所有的第三方库的列表。Podspecs 被存储在本地路径 ~/.cocoapods 中。

版本控制和冲突

CocoaPods 使用语义版本控制 - Semantic Versioning 命名约定来解决对版本的依赖。由于冲突解决系统建立在非重大变更的补丁版本之间,这使得解决依赖关系变得容易很多。例如,两个不同的 pods 依赖于 CocoaLumberjack 的两个版本,假设一个依赖于 2.3.1,另一个依赖于 2.3.3,此时冲突解决系统可以使用最新的版本 2.3.3,因为这个可以向后与 2.3.1 兼容。

但这并不总是有效。有许多第三方库并不使用这样的约定,这让解决方案变得非常复杂。

当然,总会有一些冲突需要手动解决。如果一个库依赖于 CocoaLumberjack 的 1.2.5,另外一个库则依赖于 2.3.1,那么只有最终用户通过明确指定使用某个版本来解决冲突。

加载源文件

CocoaPods 执行的下一步是加载源码。每个 .podspec 文件都包含一个源代码的索引,这些索引一般包裹一个 git 地址和 git tag。它们以 commit SHAs 的方式存储在 ~/Library/Caches/CocoaPods 中。这个路径中文件的创建是由 Core gem 负责的。

CocoaPods 将依照 Podfile.podspec 和缓存文件的信息将源文件下载到 Pods 目录中。

生成 Pods.xcodeproj

每次 pod install 执行,如果检测到改动时,CocoaPods 会利用 Xcodeproj gem 组件对 Pods.xcodeproj进行更新。如果该文件不存在,则用默认配置生成。否则,会将已有的配置项加载至内存中。

安装第三方库

当 CocoaPods 往工程中添加一个第三方库时,不仅仅是添加代码这么简单,还会添加很多内容。由于每个第三方库有不同的 target,因此对于每个库,都会有几个文件需要添加,每个 target 都需要:

  • 一个包含编译选项的 .xcconfig 文件
  • 一个同时包含编译设置和 CocoaPods 默认配置的私有 .xcconfig 文件
  • 一个编译所必须的 prefix.pch 文件
  • 另一个编译必须的文件 dummy.m

一旦每个 pod 的 target 完成了上面的内容,整个 Pods target 就会被创建。这增加了相同文件的同时,还增加了另外几个文件。如果源码中包含有资源 bundle,将这个 bundle 添加至程序 target 的指令将被添加到 Pods-Resources.sh 文件中。还有一个名为 Pods-environment.h 的文件,文件中包含了一些宏,这些宏可以用来检查某个组件是否来自 pod。最后,将生成两个认可文件,一个是 plist,另一个是 markdown,这两个文件用于给最终用户查阅相关许可信息。

写入至磁盘

直到现在,许多工作都是在内存中进行的。为了让这些成果能被重复利用,我们需要将所有的结果保存到一个文件中。所以 Pods.xcodeproj 文件被写入磁盘,另外两个非常重要的文件:Podfile.lock 和 Manifest.lock 都将被写入磁盘。

Podfile.lock

这是 CocoaPods 创建的最重要的文件之一。它记录了需要被安装的 pod 的每个已安装的版本。如果你想知道已安装的 pod 是哪个版本,可以查看这个文件。推荐将 Podfile.lock 文件加入到版本控制中,这有助于整个团队的一致性。

Manifest.lock

这是每次运行 pod install 命令时创建的 Podfile.lock 文件的副本。如果你遇见过这样的错误 沙盒文件与 Podfile.lock 文件不同步 (The sandbox is not in sync with the Podfile.lock),这是因为 Manifest.lock 文件和 Podfile.lock 文件不一致所引起。由于 Pods 所在的目录并不总在版本控制之下,这样可以保证开发者运行 app 之前都能更新他们的 pods,否则 app 可能会 crash,或者在一些不太明显的地方编译失败。

xcproj

如果你已经依照我们的建议在系统上安装了 xcproj,它会对 Pods.xcodeproj 文件执行一下 touch 以将其转换成为旧的 ASCII plist 格式的文件。为什么要这么做呢?虽然在很久以前就不被其它软件支持了,但是 Xcode 仍然依赖于这种格式。如果没有 xcproj,你的 Pods.xcodeproj 文件将会以 XML 格式的 plist 文件存储,当你用 Xcode 打开它时,它会被改写,并造成大量的文件改动。

结果

运行 pod install 命令的最终结果是许多文件被添加到你的工程和系统中。这个过程通常只需要几秒钟。当然没有 Cocoapods 这些事也都可以完成。只不过所花的时间就不仅仅是几秒而已了。

补充:持续集成

CocoaPods 和持续集成在一起非常融洽。虽然持续集成很大程度上取决于你的项目配置,但 Cocoapods 依然能很容易地对项目进行编译。

Pods 文件夹的版本控制

如果 Pods 文件夹和里面的所有内容都在版本控制之中,那么你不需要做什么特别的工作,就能够持续集成。我们只需要给 .xcworkspace 选择一个正确的 scheme 即可。

不受版本控制的 Pods 文件夹

如果你的 Pods 文件夹不受版本控制,那么你需要做一些额外的步骤来保证持续集成的顺利进行。最起码,Podfile 文件要放入版本控制之中。另外强烈建议将生成的 .xcworkspace 和 Podfile.lock 文件纳入版本控制,这样不仅简单方便,也能保证所使用 Pod 的版本是正确的。

一旦配置完毕,在持续集成中运行 CocoaPods 的关键就是确保每次编译之前都执行了 pod install 命令。在大多数系统中,例如 Jenkins 或 Travis,只需要定义一个编译步骤即可 (实际上,Travis 会自动执行 pod install 命令)。对于 Xcode Bots,在书写这篇文章时我们还没能找到非常流畅的方式,不过我们正朝着解决方案努力,一旦成功,我们将会立即分享。

结束语

CocoaPods 简化了 Objective-C 的开发流程,我们的目标是让第三方库更容易被发现和添加。了解 CocoaPods 的原理能让你做出更好的应用程序。我们沿着 CocoaPods 的整个执行过程,从载入 specs 文件和源代码、创建 .xcodeproj 文件和所有组件,到将所有文件写入磁盘。所以接下来,我们运行 pod install --verbose,静静观察 CocoaPods 的魔力如何显现。


原文(英文):https://www.objc.io/issues/6-build-tools/cocoapods-under-the-hood/  

翻译:@BeyondVincent





收起阅读 »

iOS app的编译过程

iOS app的编译过程在 iOS 开发的过程中,Xcode 为我们提供了非常完善的编译能力,正常情况下,我们只需要 Command + R 就可以将应用运行到设备上,即使打包也是一个相对愉快的过程。但正如我们写代码无法避开 Bug 一样,项目在编译的时候也会...
继续阅读 »

iOS app的编译过程

在 iOS 开发的过程中,Xcode 为我们提供了非常完善的编译能力,正常情况下,我们只需要 Command + R 就可以将应用运行到设备上,即使打包也是一个相对愉快的过程。

但正如我们写代码无法避开 Bug 一样,项目在编译的时候也会出现各种各样的错误,最痛苦的莫过于处理这些错误。其中的各种报错都不是我们在日常编程中所能接触的,而我们无法快速精准的定位错误并解决的唯一原因就是我们根本不知道在编译的时候都做了些什么,都需要些什么。就跟使用一个新的类,如果不去查看其代码,永远也无法知道它到底能干什么一样。

这篇文章将从由简入繁的讲解 iOS App 在编译的时候到底干了什么。一个 iOS 项目的编译过程是比较繁琐的,针对源代码、xib、framework 等都将进行一定的编译和操作,再加上使用 Cocoapods,会让整个过程更加复杂。这篇文章将以 Swift 和 Objective-C 的不同角度来分析。

1.什么是编译

在开始之前,我们必须知道什么是编译?为什么要进行编译?

CPU 由上亿个晶体管组成,在运行的时候,单个晶体管只能根据电流的流通或关闭来确认两种状态,我们一般说 0 或 1,根据这种状态,人类创造了二进制,通过二进制编码我们可以表示所有的概念。但是,CPU 依然只能执行二进制代码。我们将一组二进制代码合并成一个指令或符号,创造了汇编语言,汇编语言以一种相对好理解的方式来编写,然后通过汇编过程生成 CPU 可以运行的二进制代码并运行在 CPU 上。

但是使用汇编语言开发仍然是一个相对痛苦的过程,于是通过上述方式,c、c++、Java 等语言就一层一层的被发明出来。Objective-c 和 Swift 就是这样一个过程,他们的基础都是 c 和 c++。

当我们使用 Objective-c 和 Swift 编写代码后,想要代码能运行在 CPU 上,我们必须进行编译,将我们写好的代码编译为机器可以理解的二进制代码。

1.1 LLVM

有了上面的简单介绍,可以发现,编译其实是一个用代码解释代码的过程。在 Objective-c 和 Swift 的编译过程中,用来解释代码的,就是 LLVM。点击可以看到 LLVM 的官方网站,在 Overview 的第一行就说明了 LLVM 到底是什么:

The LLVM Project is a collection of modular and reusable compiler and toolchain technologies. Despite its name, LLVM has little to do with traditional virtual machines. The name “LLVM” itself is not an acronym; it is the full name of the project.

LLVM 项目是一个模块化、可重用的编译器、工具链技术的集合。尽管它的名字叫 LLVM,但它与传统虚拟机的关系并不大。“LLVM”这个名字本身不是一个缩略词; 它的全称是这个项目。

// LLVM 命名最早源自于底层虚拟机(Low Level Virtual Machine)的缩写。

简单的说,LLVM 是一个项目,其作用就是提供一个广泛的工具,可以将任何高级语言的代码编译为任何架构的 CPU 都可以运行的机器代码。它将整个编译过程分类了三个模块:前端、公用优化器、后端。(这里不要去思考任何关于 web 前端和 service 后端的概念。)

前端:对目标语言代码进行语法分析,语义分析,生成中间代码。在这个过程中,会进行类型检查,如果发现错误或者警告会标注出来在哪一行。我们在开发的过程中,其实 Xcode 也会使用前端工具对你的代码进行分析,并实时的检查出来某些错误。前端是针对特定语言的,如果需要一个新的语言被编译,只需要再写一个针对新语言的前端模块即可。

公用优化器:将生成的中间文件进行优化,去除冗余代码,进行结构优化。

后端:后段将优化后的中间代码再次转换,变成汇编语言,并再次进行优化,最后将各个文件代码转换为机器代码并链接。链接是指将不同代码文件编译后的不同机器代码文件合并成一个可执行文件。
虽然目前 LLVM 并没有达到其目标(可以编译任何代码),但是这样的思路是很优秀的,在日常开发中,这种思路也会为我们提供不少的帮助。

1.2 clang

clang 是 LLVM 的一个前端,它的作用是针对 C 语言家族的语言进行编译,像 c、c++、Objective-C。而 Swift 则自己实现了一个前端来进行 Swift 编译,优化器和后端依然是使用 LLVM 来完成,后面会专门对 Swift 语言的 前端编译流程进行分析。

上面简单的介绍了为什么需要编译,以及 Objectie-C 和 Swift 代码的编译思路。这是基础,如果没有这些基础,后面针对我们整个项目的编译就无法理解,如果你理解了上面的知识点,那么下面将要讲述的整个项目的编译过程就会显得很简单了。

2.ios项目编译过程介绍

Xcode 在编译 iOS 项目的时候,使用的正是 LLVM,其实我们在编写代码以及调试的时候也在使用 LLVM 提供的功能。例如代码高亮(clang)、实时代码检查(clang)、代码提示(clang)、debug 断点调试(LLDB)。这些都是 LLVM 前端提供的功能,而对于后端来说,我们接触到的就是关于 arm64、armv7、armv7s 这些 CPU 架构了,记得之前还有 32 位架构处理器的时候,设定指定的编译的目标 CPU 架构就是一个比较痛苦的过程。

下面来简单的讲讲整个 iOS 项目的编译过程,其中可能会有一些疑问,先保留着,后面会详细解释:
我们的项目是一个 target,一个编译目标,它拥有自己的文件和编译规则,在我们的项目中可以存在多个子项目,这在编译的时候就导致了使用了 Cocoapods 或者拥有多个 target 的项目会先编译依赖库。这些库都和我们的项目编译流程一致。Cocoapods 的原理解释将在文章后面一部分进行解释。

1.写入辅助文件:将项目的文件结构对应表、将要执行的脚本、项目依赖库的文件结构对应表写成文件,方
便后面使用;并且创建一个 .app 包,后面编译后的文件都会被放入包中;

2.运行预设脚本:Cocoapods 会预设一些脚本,当然你也可以自己预设一些脚本来运行。这些脚本都在 Build Phases 中可以看到;

3.编译文件:针对每一个文件进行编译,生成可执行文件 Mach-O,这过程 LLVM 的完整流程,前端、优化器、后端;

4.链接文件:将项目中的多个可执行文件合并成一个文件;

5.拷贝资源文件:将项目中的资源文件拷贝到目标包;

6.编译 storyboard 文件:storyboard 文件也是会被编译的;

7.链接 storyboard 文件:将编译后的 storyboard 文件链接成一个文件;

8.编译 Asset 文件:我们的图片如果使用 Assets.xcassets 来管理图片,那么这些图片将会被编译成机器码,除了 icon 和 launchImage;

9.运行 Cocoapods 脚本:将在编译项目之前已经编译好的依赖库和相关资源拷贝到包中。

10.生成 .app 包

11.将 Swift 标准库拷贝到包中

12.对包进行签名

13.完成打包

在上述流程中:2 - 9 步骤的数量和顺序并不固定,这个过程可以在 Build Phases 中指定。Phases:阶段、步骤。这个 Tab 的意思就是编译步骤。其实不仅我们的整个编译步骤和顺序可以被设定,包括编译过程中的编译规则(Build Rules)和具体步骤的参数(Build Settings),在对应的 Tab 都可以看到。关于整个编译流程的日志和设定,可以查看这篇文章:Build 过程,跟着它的步骤来查看自己的项目将有助于你理解整个编译流程。后面也会详细讲解这些内容。

3.文件编译过程

Objective-C 的文件中,只有 .m 文件会被编译 .h 文件只是一个暴露外部接口的头文件,它的作用是为被编译的文件中的代码做简单的共享。下面拿一个单独的类文件进行分析。这些步骤中的每一步你都可以使用 clang 的命令来查看其进度,记住 clang 是一个命令行工具,它可以直接在终端中运行。这里我们使用 c 语言作为例子类进行分析,它的过程和 Objective-C 一样,后面 3.7 会讲到 Swift 文件是如何被编译的。

3.1预处理

在我们的代码中会有很多 #import 宏,预处理的第一步就是将 import 引入的文件代码放入对应文件。

然后将自定义宏替换,例如我们定义了如下宏并进行了使用:

#define Button_Height 44
#define Button_Width 100

button.frame = CGRectMake(0, 0, Button_Width, Button_Height);
那么代码将会被替换成

button.frame = CGRectMake(0, 0, 44, 100);

按照这样的思路可以发现,在自定义宏的时候要格外小心,尤其是一些携带参数和功能的宏,这些宏也只是简单的直接替换代码,不能真的代替方法或函数,中间会有很多问题。

在将代码完全拆开后,将会对代码进行符号化,对于分析代码的代码 (clang),我们写的代码就是一些字符串,为了后面给这些代码进行语法和语义分析,需要将我们的代码进行标记并符号化,例如一段 helloworld 的 c 代码:

#include <stdio.h>
int main(int argc, char *argv[])
{
printf("Hello World!\n");
return 0;
}

使用 clang 命令 clang -Xclang -dump-tokens helloworld.c 转化后的代码如下(去掉了 stdio.h 中的内容):

int 'int'    [StartOfLine]  Loc=<helloworld.c:2:1>
identifier 'main' [LeadingSpace] Loc=<helloworld.c:2:5>
l_paren '(' Loc=<helloworld.c:2:9>
int 'int' Loc=<helloworld.c:2:10>
identifier 'argc' [LeadingSpace] Loc=<helloworld.c:2:14>
comma ',' Loc=<helloworld.c:2:18>
char 'char' [LeadingSpace] Loc=<helloworld.c:2:20>
star '*' [LeadingSpace] Loc=<helloworld.c:2:25>
identifier 'argv' Loc=<helloworld.c:2:26>
l_square '[' Loc=<helloworld.c:2:30>
r_square ']' Loc=<helloworld.c:2:31>
r_paren ')' Loc=<helloworld.c:2:32>
l_brace '{' [StartOfLine] Loc=<helloworld.c:3:1>
identifier 'printf' [StartOfLine] [LeadingSpace] Loc=<helloworld.c:4:2>
l_paren '(' Loc=<helloworld.c:4:8>
string_literal '"Hello World!\n"' Loc=<helloworld.c:4:9>
r_paren ')' Loc=<helloworld.c:4:25>
semi ';' Loc=<helloworld.c:4:26>
return 'return' [StartOfLine] [LeadingSpace] Loc=<helloworld.c:5:2>
numeric_constant '0' [LeadingSpace] Loc=<helloworld.c:5:9>
semi ';' Loc=<helloworld.c:5:10>
r_brace '}' [StartOfLine] Loc=<helloworld.c:6:1>
eof '' Loc=<helloworld.c:6:2>

这里,每一个符号都会标记出来其位置,这个位置是宏展开之前的位置,这样后面如果发现报错,就可以正确的提示错误位置了。针对 Objective-C 代码,我们只需要转化对应的 .m 文件就可以查看。

3.2语意和语法分析

3.2.1AST

对代码进行标记之后,其实就可以对代码进行分析,但是这样分析起来的过程会比较复杂。于是 clang 又进行了一步转换:将之前的标记流转换为一颗抽象语法树(abstract syntax tree – AST)。

使用 clang 命令 clang -Xclang -ast-dump -fsyntax-only helloworld.c,转化后的树如下(去掉了 stdio.h 中的内容):


`-FunctionDecl 0x7f8eaf834bb0 <helloworld.c:2:1, line:6:1> line:2:5 main 'int (int, char **)'
|-ParmVarDecl 0x7f8eaf8349b8 <col:10, col:14> col:14 argc 'int'
|-ParmVarDecl 0x7f8eaf834aa0 <col:20, col:31> col:26 argv 'char **':'char **'
`-CompoundStmt 0x7f8eaf834dd8 <line:3:1, line:6:1>
|-CallExpr 0x7f8eaf834d40 <line:4:2, col:25> 'int'
| |-ImplicitCastExpr 0x7f8eaf834d28 <col:2> 'int (*)(const char *, ...)' <FunctionToPointerDecay>
| | `-DeclRefExpr 0x7f8eaf834c68 <col:2> 'int (const char *, ...)' Function 0x7f8eae836d78 'printf' 'int (const char *, ...)'
| `-ImplicitCastExpr 0x7f8eaf834d88 <col:9> 'const char *' <BitCast>
| `-ImplicitCastExpr 0x7f8eaf834d70 <col:9> 'char *' <ArrayToPointerDecay>
| `-StringLiteral 0x7f8eaf834cc8 <col:9> 'char [14]' lvalue "Hello World!\n"
`-ReturnStmt 0x7f8eaf834dc0 <line:5:2, col:9>
`-IntegerLiteral 0x7f8eaf834da0 <col:9> 'int' 0

这是一个 main 方法的抽象语法树,可以看到树顶是 FunctionDecl:方法声明(Function Declaration

这里因为截取了部分代码,其实并不是整个树的树顶。真正的树顶描述应该是:TranslationUnitDecl。
然后是两个 ParmVarDecl:参数声明。

接着下一层是 CompoundStmt:说明下面有一组复合的声明语句,指的是我们的 main 方法里面所使用到的所有代码。

再到里面就是每一行代码的使用,方法的调用,传递的参数,以及返回。在实际应用中还会有变量的声明、操作符的使用等。

3.2.2静态分析

有了这样的语法树,对代码的分析就会简单许多。对这棵树进行遍历分析,包括类型检查、实现检查(某个类是否存在某个方法)、变量使用,还会有一些复杂的检查,例如在 Objective-C 中,给某一个对象发送消息(调用某个方法),检查这个对象的类是否声明这个方法(但并不会去检查这个方法是否实现,这个错误是在运行时进行检查的),如果有什么错误就会进行提示。因此可见,Xcode 对 clang 做了非常深度的集成,在编写代码的过程中它就会使用 clang 来对你的代码进行分析,并及时的对你的代码错误进行提示。

3.3生成LLVM代码

当确认代码没有问题后(静态分析可分析出来的问题),前端就将进入最后一步:生成 LLVM 代码,并将代码递交给优化器。

使用命令 clang -S -emit-llvm helloworld.c -o helloworld.ll 将生成 LLVM IR。

其设计的最重要的部分是 LLVM 中间表示(IR),它是一种在编译器中表示代码的形式。LLVM IR 旨在承载在编译器的优化器中间的分析和转换。它的设计考虑了许多特定的目标,包括支持轻量级运行时优化,跨功能/进程间优化,整个程序分析和积极的重组转换等等。但它最重要的方面是它本身被定义为具有明确定义的语义的第一类语言。

例如我们上面的代码将会被生成为:


; ModuleID = 'helloworld.c'
source_filename = "helloworld.c"
target datalayout = "e-m:o-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-apple-macosx10.12.0"

@.str = private unnamed_addr constant [14 x i8] c"Hello World!\0A\00", align 1

; Function Attrs: nounwind ssp uwtable
define i32 @main(i32, i8**) #0 {
%3 = alloca i32, align 4
%4 = alloca i32, align 4
%5 = alloca i8**, align 8
store i32 0, i32* %3, align 4
store i32 %0, i32* %4, align 4
store i8** %1, i8*** %5, align 8
%6 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([14 x i8], [14 x i8]* @.str, i32 0, i32 0))
ret i32 0
}

declare i32 @printf(i8*, ...) #1

attributes #0 = { nounwind ssp uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+fxsr,+mmx,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #1 = { "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+fxsr,+mmx,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }

!llvm.module.flags = !{!0}
!llvm.ident = !{!1}

!0 = !{i32 1, !"PIC Level", i32 2}
!1 = !{!"Apple LLVM version 8.1.0 (clang-802.0.42)"}


其实还是能实现我们功能的代码,在这一步,所有 LLVM 前端支持的语言都将会被转换成这样的代码,主要是为了后面的工作可以共用。下面就是 LVVM 中的优化器的工作。

在这里简单介绍一些 LLVM IR 的指令:

%:局部变量
@:全局变量
alloca:分配内存堆栈
i32:32 位的整数
i32**:一个指向 32int 值的指针的指针
align 4:向 4 个字节对齐,即便数据没有占用 4 个字节,也要为其分配四个字节
call:调用

3.4优化

上面的代码是没有进行优化过的,在语言转换的过程中,有些代码是可以被优化以提升执行效率的。使用命令 clang -O3 -S -emit-llvm helloworld.c -o helloworld.ll,其实和上面的命令的区别只有 -O3 而已,注意,这里是大写字母 O 而不是数字 0。优化后的代码如下:


; ModuleID = 'helloworld.c'
source_filename = "helloworld.c"
target datalayout = "e-m:o-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-apple-macosx10.12.0"

@str = private unnamed_addr constant [13 x i8] c"Hello World!\00"

; Function Attrs: nounwind ssp uwtable
define i32 @main(i32, i8** nocapture readnone) local_unnamed_addr #0 {
%3 = tail call i32 @puts(i8* getelementptr inbounds ([13 x i8], [13 x i8]* @str, i64 0, i64 0))
ret i32 0
}

; Function Attrs: nounwind
declare i32 @puts(i8* nocapture readonly) #1

attributes #0 = { nounwind ssp uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+fxsr,+mmx,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #1 = { nounwind }

!llvm.module.flags = !{!0}
!llvm.ident = !{!1}

!0 = !{i32 1, !"PIC Level", i32 2}
!1 = !{!"Apple LLVM version 8.1.0 (clang-802.0.42)"}

可以看到,即使是最简单的 helloworld 代码,也会被优化。这一步骤的优化是非常重要的,很多直接转换来的代码是不合适且消耗内存的,因为是直接转换,所以必然会有这样的问题,而优化放在这一步的好处在于前端不需要考虑任何优化过程,减少了前端的开发工作。

3.5 生成目标文件

下面就是后端的工作了,将优化过的代码根据不同架构的 CPU 转化生成汇编代码,再生成对应的可执行文件,这样对应的 CPU 就可以执行了。

3.6可执行文件

在最后,LLVM 将会把这些汇编代码输出成二进制的可执行文件,使用命令 clang helloworld.c -o helloworld.out 即可查看,-o helloworld.out 如果不指定,将会被默认指定为 a.out。

可执行文件会有多个部分,对应了汇编指令中的 .section,它的名字也叫做 section,每个 section 都会被转换进某个 segment 里。这种方式用来区分不同功能的代码。将相同属性的 section 集合在一起,就是一个 segment。

使用 otool 工具可以查看生成的可执行文件的 section 和 segment:

Segment __PAGEZERO: 0x100000000 (vmaddr 0x0 fileoff 0)
Segment __TEXT: 0x1000 (vmaddr 0x100000000 fileoff 0)
Section __text: 0x34 (addr 0x100000f50 offset 3920)
Section __stubs: 0x6 (addr 0x100000f84 offset 3972)
Section __stub_helper: 0x1a (addr 0x100000f8c offset 3980)
Section __cstring: 0xe (addr 0x100000fa6 offset 4006)
Section __unwind_info: 0x48 (addr 0x100000fb4 offset 4020)
total 0xaa
Segment __DATA: 0x1000 (vmaddr 0x100001000 fileoff 4096)
Section __nl_symbol_ptr: 0x10 (addr 0x100001000 offset 4096)
Section __la_symbol_ptr: 0x8 (addr 0x100001010 offset 4112)
total 0x18
Segment __LINKEDIT: 0x1000 (vmaddr 0x100002000 fileoff 8192)
total 0x100003000

上面的代码中,每个 segment 的意义也不一样:

__ PAGEZERO segment 它的大小为 4GB。这 4GB 并不是文件的真实大小,但是规定了进程地址空间的前 4GB 被映射为 不可执行、不可写和不可读。

__ TEXT segment
包含了被执行的代码。它被以只读和可执行的方式映射。进程被允许执行这些代码,但是不能修改。

__ DATA segment 以可读写和不可执行的方式映射。它包含了将会被更改的数据。

__ LINKEDIT segment 指出了 link edit 表(包含符号和字符串的动态链接器表)的地址,里面包含了加载程序的元数据,例如函数的名称和地址。

4.静态库和动态库

说起来编译,就不得不说起动态库和静态库。这两个东西可是和编译过程息息相关的,这里有几篇文章的比较透彻,可以查看,想要了解整个编译过程,库是逃不开的:

iOS 静态库,动态库与 Framework:非常完美的讲解了静态库和动态库的概念,还有一些延伸阅读也非常好。https://skyline75489.github.io/post/2015-8-14_ios_static_dynamic_framework_learning.html

5.了解了这么多编译原理,除了写一个自动化编译脚本以外,还可以看懂很多之前完全看不明白的编译错误。在 Xcode 中,也可以对编译过程进行完整的设置,很多时候编译错误的解决就是在这里进行的。Xcode编译设置

5.1Build settings

这里是编译设置,针对编译流程中的各个过程进行参数和工具的配置:

1.Architectures:编译目标 CPU 架构,这里比较常见的是 Build Active Architectures Only(只编译为当前架构,是指你在 scheme 中选定的设备的 CPU 架构),debug 设置为 YES,Release 设置为 NO。


2.Assets:Assets.xcassets 资源组的配置。

3.Build Locations:查看 Build 日志可以看到在编译过程中的目标文件夹。

4.Build Options:这里是一些编译的选项设定,包含:

a.是否总是嵌入 Swift 标准库,这个在静态库和动态库的第一篇文章中有讲,iOS 系统目前是不包含 Swift 标准库的,都是被打包在项目中。

b.c/c++/objective-c 编译器:Apple LLVM 9.0

c.是否打开 Bitcode



5.Deployment:iOS 部署设置。说白了就是安装到手机的设置。

6.Headers:头文件?具体作用不详,知道的可以说一下。

7.Kernel Module:内核模块,作用不详。

8.Linking:链接设置,链接路径、链接标记、Mach-O 文件类型。

9.Packaging:打包设置,info.plist 的路径设置、Bundle ID 、App 显示名称的设置。

10.Search Paths:库的搜索路径、头文件的搜索路径。

11.Signing:签名设置,开发、生产的签名设置,这些都和你在开发者网站配置的证书相关。

12.Testing:测试设置,作用不详。

13.Text-Based API:基于文本的 API,字面翻译,作用不详。

14.Versioning:版本管理。

15.Apple LLVM 9.0 系列:LLVM 的配置,包含路径、编译器每一步的设置、语言设置。在这里 Apple LLVM 9.0 - Warnings 可以选择在编译的时候将哪些情况认定为错误(Error)和警告(Warning),可以开启困难模式,任何一个小的警告都会被认定为错误。

16.Asset Catalog Compiler - Options:Asset 文件的编译设置。

17.Interface Builder Storyboard Compiler - Options:Storyboard 的编译设置。

18.以及一些静态分析和 Swift 编译器的设定。

5.2Build Phases

编译阶段,编译的时候将根据顺序来进行编译。这里固定的有:

1.Compile Sources:编译源文件。

2.Link Binary With Libraries:相关的链接库。

3.Copy Bundle Resources:要拷贝的资源文件,有时候如果一个资源文件在开发过程中发现找不到,可以在这里找一下,看看是不是加进来了。

如果使用了 Cocoapods,那么将会被添加:

1.[CP] Check Pods Manifest.lock:检查 Podfile.lock 和 Manifest.lock 
文件的一致性,这个会再后面的 Cocoapods 原理中详细解释。

2.[CP] Embed Pods Frameworks:将所有 cocoapods 打的 framework 拷贝到包中。

3.[CP] Copy Pods Resources:将所有 cocoapods 的资源文件拷贝到包中。

5.3Build Rules

编译规则,这里设定了不同文件的处理方式,例如:

Copy Plist File:在编译打包的时候,将 info.plist 文件拷贝。

Compress PNG File:在编译打包的时候,将 PNG 文件压缩。

Swift Compiler:Swift 文件的编译方式,使用 Swift 编译器。

6. Cocoapods 原理

使用了 Cocoapods 后,我们的编译流程会多出来一些,虽然每个 target 的编译流程都是一致的,但是 Cocoapods 是如何将这些库导入我们的项目、原项目和其他库之间的依赖又是如何实现的仍然是一个需要了解的知识点




作者:帽子和五朵玫瑰
链接:https://www.jianshu.com/p/0ad0660ac63a

收起阅读 »

iOS Crash分析中的Signal

下面是一些信号说明SIGHUP本信号在用户终端连接(正常或非正常)结束时发出, 通常是在终端的控制进程结束时, 通知同一session内的各个作业, 这时它们与控制终端不再关联。登录Linux时,系统会分配给登录用户一个终端(Session)。在这个终端运行的...
继续阅读 »

下面是一些信号说明

  • SIGHUP
    本信号在用户终端连接(正常或非正常)结束时发出, 通常是在终端的控制进程结束时, 通知同一session内的各个作业, 这时它们与控制终端不再关联。
    登录Linux时,系统会分配给登录用户一个终端(Session)。在这个终端运行的所有程序,包括前台进程组和后台进程组,一般都属于这个 Session。当用户退出Linux登录时,前台进程组和后台有对终端输出的进程将会收到SIGHUP信号。这个信号的默认操作为终止进程,因此前台进 程组和后台有终端输出的进程就会中止。不过可以捕获这个信号,比如wget能捕获SIGHUP信号,并忽略它,这样就算退出了Linux登录, wget也 能继续下载。
    此外,对于与终端脱离关系的守护进程,这个信号用于通知它重新读取配置文件。
  • SIGINT
    程序终止(interrupt)信号, 在用户键入INTR字符(通常是Ctrl-C)时发出,用于通知前台进程组终止进程。
  • SIGQUIT
    SIGINT类似, 但由QUIT字符(通常是Ctrl-)来控制. 进程在因收到SIGQUIT退出时会产生core文件, 在这个意义上类似于一个程序错误信号。
  • SIGILL
    执行了非法指令. 通常是因为可执行文件本身出现错误, 或者试图执行数据段. 堆栈溢出时也有可能产生这个信号。
  • SIGTRAP
    由断点指令或其它trap指令产生. 由debugger使用。
  • SIGABRT
    调用abort函数生成的信号。
  • SIGBUS
    非法地址, 包括内存地址对齐(alignment)出错。比如访问一个四个字长的整数, 但其地址不是4的倍数。它与SIGSEGV的区别在于后者是由于对合法存储地址的非法访问触发的(如访问不属于自己存储空间或只读存储空间)。
  • SIGFPE
    在发生致命的算术运算错误时发出. 不仅包括浮点运算错误, 还包括溢出及除数为0等其它所有的算术的错误。
  • SIGKILL
    用来立即结束程序的运行. 本信号不能被阻塞、处理和忽略。如果管理员发现某个进程终止不了,可尝试发送这个信号。
  • SIGUSR1
    留给用户使用
  • SIGSEGV
    试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据.
  • SIGUSR2
    留给用户使用
  • SIGPIPE
    管道破裂。这个信号通常在进程间通信产生,比如采用FIFO(管道)通信的两个进程,读管道没打开或者意外终止就往管道写,写进程会收到SIGPIPE信号。此外用Socket通信的两个进程,写进程在写Socket的时候,读进程已经终止。
  • SIGALRM
    时钟定时信号, 计算的是实际的时间或时钟时间. alarm函数使用该信号.
  • SIGTERM
    程序结束(terminate)信号, 与SIGKILL不同的是该信号可以被阻塞和处理。通常用来要求程序自己正常退出,shell命令kill缺省产生这个信号。如果进程终止不了,我们才会尝试SIGKILL
  • SIGCHLD
    子进程结束时, 父进程会收到这个信号。
    如果父进程没有处理这个信号,也没有等待(wait)子进程,子进程虽然终止,但是还会在内核进程表中占有表项,这时的子进程称为僵尸进程。这种情 况我们应该避免(父进程或者忽略SIGCHILD信号,或者捕捉它,或者wait它派生的子进程,或者父进程先终止,这时子进程的终止自动由init进程 来接管)。
  • SIGCONT
    让一个停止(stopped)的进程继续执行. 本信号不能被阻塞. 可以用一个handler来让程序在由stopped状态变为继续执行时完成特定的工作. 例如, 重新显示提示符
  • SIGSTOP
    停止(stopped)进程的执行. 注意它和terminate以及interrupt的区别:该进程还未结束, 只是暂停执行. 本信号不能被阻塞, 处理或忽略.
  • SIGTSTP
    停止进程的运行, 但该信号可以被处理和忽略. 用户键入SUSP字符时(通常是Ctrl-Z)发出这个信号
  • SIGTTIN
    当后台作业要从用户终端读数据时, 该作业中的所有进程会收到SIGTTIN信号. 缺省时这些进程会停止执行.
  • SIGTTOU
    类似于SIGTTIN, 但在写终端(或修改终端模式)时收到.
  • SIGURG
    有”紧急”数据或out-of-band数据到达socket时产生.
  • SIGXCPU
    超过CPU时间资源限制. 这个限制可以由getrlimit/setrlimit来读取/改变。
  • SIGXFSZ
    当进程企图扩大文件以至于超过文件大小资源限制。
  • SIGVTALRM
    虚拟时钟信号. 类似于SIGALRM, 但是计算的是该进程占用的CPU时间.
  • SIGPROF
    类似于SIGALRM/SIGVTALRM, 但包括该进程用的CPU时间以及系统调用的时间.
  • SIGWINCH
    窗口大小改变时发出.
  • SIGIO
    文件描述符准备就绪, 可以开始进行输入/输出操作.
  • SIGPWR
    Power failure
  • SIGSYS
    非法的系统调用。

关键点注意

  • 在以上列出的信号中,程序不可捕获、阻塞或忽略的信号有:SIGKILL,SIGSTOP
  • 不能恢复至默认动作的信号有:SIGILL,SIGTRAP
  • 默认会导致进程流产的信号有:SIGABRT,SIGBUS,SIGFPE,SIGILL,SIGIOT,SIGQUIT,SIGSEGV,SIGTRAP,SIGXCPU,SIGXFSZ
  • 默认会导致进程退出的信号有:
    SIGALRM,SIGHUP,SIGINT,SIGKILL,SIGPIPE,SIGPOLL,SIGPROF,SIGSYS,SIGTERM,SIGUSR1,SIGUSR2,SIGVTALRM
  • 默认会导致进程停止的信号有:SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU
  • 默认进程忽略的信号有:SIGCHLD,SIGPWR,SIGURG,SIGWINCH
  • 此外,SIGIOSVR4是退出,在4.3BSD中是忽略;SIGCONT在进程挂起时是继续,否则是忽略,不能被阻塞。


作者:Cooci
链接:https://www.jianshu.com/p/3a9dc6bd5e58
收起阅读 »

Swift 反射,揭开面纱

与iOS Runtime不一样,Swift的反射用了另一套API,实现机制也完全不一样1. iOS Runtime其实基于Objc的Runtime是iOS开发的黑魔法,比如神奇的Method Swizzle可以交换任何iOS的系统方法, 再比如消息转发机制,又...
继续阅读 »

与iOS Runtime不一样,Swift的反射用了另一套API,实现机制也完全不一样


1. iOS Runtime

  • 其实基于Objc的Runtime是iOS开发的黑魔法,比如神奇的Method Swizzle可以交换任何iOS的系统方法, 再比如消息转发机制,又如class_copyIvarList等方法,可以动态获取一个类裡面所有的方法和属性, 以及动态给一个类新增属性和方法.
  • Objc的Runtime是如此的强大,再加上KVC和KVO这两个利器,可以实现很多你根本就想不到的功能,给iOS开发带来极大的便捷。

2. Apple推出全新的Swift语言后,单纯的Swift型别不再兼容Objc的Runtime,

Swift作为一门静态语言,所有资料的型别都是在编译时就确定好了的,但是Apple为了让Swift相容Objc,让Swift也使用了Runtime。这显然会拖累Swift的执行效率,和Apple所宣称Swift具有超越Objective-C的效能的观点完全不符。而Swift在将来是会慢慢替代 Objective-C的成为iOS或者OSX开发的主流语言,所以为了效能,我们应该尽量使用原生的Swift,避免让Runtime进行Swift型别->Objc型别的隐式转换。
Swift目前只有有限的反射功能,完全不能和Objc的Runtime相比。

首先作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS开发公众号:编程大鑫,不管你是小白还是大牛都欢迎入驻 ,让我们一起进步,共同发展!

什么是反射

反射是一种计算机处理方式。是程式可以访问、检测和修改它本身状态或行为的一种能力。
上面的话来自百度百科。使用反射有什么用,看一些iOS Runtime的文章应该会很明白。下面再列举一下

  • 动态地建立物件和属性,
  • 动态地获取一个类裡面所有的属性,方法。
  • 获取它的父类,或者是实现了什么样的介面(协议)
  • 获取这些类和属性的访问限制(Public 或者 Private)
  • 动态地获取执行中物件的属性值,同时也能给它赋值(KVC)
  • 动态呼叫例项方法或者类方法
  • 动态的给类新增方法或者属性,还可以交换方法(只限于Objective-C)

上面的一系列功能的细节和计算机语言的不同而不同。对于Objective-C来说,位于中的一系列方法就是完成这些功能的,严格来说Runtime并不是反射。而Swift真正拥有了反射功能,但是功能非常弱,目前只能访问和检测它本身,还不能修改。

Swift的反射

Swift的反射机制是基于一个叫Mirror的Stuct来实现的。具体的操作方式为:首先建立一个你想要反射的类的例项,再传给Mirror的构造器来例项化一个Mirror物件,最后使用这个Mirror来获取你想要的东西。

Mirror结构体常用属性:
subjectType:对象类型
children:反射对象的属性集合
displayStyle:反射对象展示类型

下面来简单介绍下Mirror的使用:

  1. 获取对象类型
  2. 获取一个类的属性名称和属性的值
        let p = Person()
p.name = "刘伟湘"
p.age = 22

let mirror:Mirror = Mirror(reflecting: p)

/*
* 1\. 获取对象类型
*/

print("获取对象类型:\(mirror.subjectType)")
//打印结果: 获取对象类型:Person

/*
* 2\. 获取对象的所有属性名称和属性值
*/

for property in mirror.children {
let propertyNameStr = property.label! // 属性名使用!,因为label是optional类型
let propertyValue = property.value // 属性的值
print("\(propertyNameStr)的值为:\(propertyValue)")
//打印结果: name的值为:Optional("刘伟湘") age的值为:22
}


swfit反射的应用场景现在还比较狭窄,因为功能还不够完善,比较常见的反射应用场景就是自定义类模型转字典

class NewViewController: UIViewController {

override func viewDidLoad() {
super.viewDidLoad()

self.navigationItem.title = "new"
self.view.backgroundColor = .white

// 创建一个User实例对象模型
let user1 = User()
user1.name = "刘伟湘"
user1.age = 100
user1.emails = ["506299396.qq.com","111111111111.qq.com"]
let tel1 = Telephone(title: "手机", number: "18711112222")
let tel2 = Telephone(title: "公司座机", number: "2800034")
user1.tels = [tel1, tel2]

// 模型转字典
if let model = user1.toJSONModel() {
print(model)
}
/*
*打印结果
["age": 100, "tels": ["item0": ["number": "18711112222", "title": "手机"], "item1": ["number": "2800034", "title": "公司座机"]], "emails": ["item0": "506299396.qq.com", "item1": "111111111111.qq.com"], "name": "刘伟湘"]
*
*/
}
}

class User {
var name:String = ""
var nickname:String?
var age:Int?
var emails:[String]?
var tels:[Telephone]?
}

// 电话结构体
struct Telephone {
var title:String // 电话标题
var number:String // 电话号码
}

// 自定义一个JSON协议
protocol Sam_JSON {
func toJSONModel() -> Any?
}

// 扩展协议方法,实现一个通用的toJSONModel方法(反射实现)
extension Sam_JSON {
// 将模型数据转成可用的字典数据,Any表示任何类型,除了方法类型
func toJSONModel() -> Any? {
// 根据实例创建反射结构体Mirror
let mirror = Mirror(reflecting: self)

if mirror.children.count > 0 {
// 创建一个空字典,用于后面添加键值对
var result: [String:Any] = [:]

for (idx, children) in mirror.children.enumerated() {
let propertyNameString = children.label ?? "item\(idx)"
let value = children.value
// 判断value的类型是否遵循JSON协议,进行深度递归调用
if let jsonValue = value as? Sam_JSON {
result[propertyNameString] = jsonValue.toJSONModel()
}
}
return result
}
return self
}
}

// 扩展可选类型,使其遵循JSON协议,可选类型值为nil时,不转化进字典中
extension Optional: Sam_JSON {
func toJSONModel() -> Any? {
if let x = self {
if let value = x as? Sam_JSON {
return value.toJSONModel()
}
}
return nil
}
}

// 扩展两个自定义类型,使其遵循JSON协议
extension User: Sam_JSON { }
extension Telephone: Sam_JSON { }

// 扩展Swift的基本数据类型,使其遵循JSON协议
extension String: Sam_JSON { }
extension Int: Sam_JSON { }
extension Bool: Sam_JSON { }
extension Dictionary: Sam_JSON { }
extension Array: Sam_JSON { }


作者:编程大鑫
链接:https://www.jianshu.com/p/9fdb13d62498


收起阅读 »

新时代iOS开发学习路线,预测未来不被淘汰

前言这里是大鑫,是一名正在努力学习的iOS开发工程师,目前致力于全栈方向的学习,希望可以和大家一起交流技术,共同进步,利用网络记录下自己的学习历程。本文阅读建议 1.一定要辩证的看待本文. 2.本文主要是本人对iOS开发经验中总结的知识点 3.本文所有观点仅代...
继续阅读 »

前言

这里是大鑫,是一名正在努力学习的iOS开发工程师,目前致力于全栈方向的学习,希望可以和大家一起交流技术,共同进步,利用网络记录下自己的学习历程。

本文阅读建议
1.一定要辩证的看待本文.
2.本文主要是本人对iOS开发经验中总结的知识点
3.本文所有观点仅代表本人.
4.本文只阐述学习路线和学习当中的重点问题.需要读者自己使用百度进行拓展学习.
5.本文所表达观点并不是最终观点,还会更新,因为本人还在学习过程中,有什么遗漏或错误还望各位指出.
6.觉得哪里不妥请在评论留下建议~
7.觉得还行的话就点个小心心鼓励下我吧~
目录
1.对本职业看法
2.学习方法
3.职业规划
4.产品公司&外包公司
5.做一个负责任的开发者
6.iOS开发学习路线
7.iOS基础知识点
8.iOS中级知识点
9.iOS高级知识点
10.官方Kit


我尝试加入各种iOS开发交流群,群里的气氛大致就是:学什么iOS,iOS完了,OC完了,群里大致三种人:谁有企业开发证书,马甲包了解一下,至今,大部分iOS开发群还都是仅供吹水用,偶尔能碰见几个好心人解决一下问题


个人观点

个人观点:iOS开发这个职业,不是别人说完就完的,那些说完了的人都是因为技术菜,没有权威性,不想想自己为什么菜,为什么没有和唐巧王巍在一个高度,因为菜.

还没有到达一个高度就轻易否定一个职业,注定被这个职业淘汰.

所以,无视掉这种人这么荒谬的观点,那些真正有技术,懂得学习的iOS高级开发工程师,现在正在各大企业(腾讯百度阿里等),一句话,不要有比上不足比下有余的态度.努力学习.

真正会学习的人,不会说iOS完了,而是想着如何提升自己,你想想,真正牛逼的人,真的只会iOS开发这一种吗?


学习方法

面对有难度的功能,不要忙着拒绝,而是挑战一下,学习更多知识.

尽量独立解决问题,而不是在遇到问题的第一想法是找人.

多学习别人开源的第三方库,能够开源的库一定有值得学习的地方,多去看别的大神的博客.

作为一个程序员,如果你停止了学习,你也必将会被这个团队淘汰.

要把学习的技能当做兴趣,而不是为了挣钱去学习,是为了学习技能而学习.

有给自己定制一个详细的职业规划和人生规划,大到5~10年,小到近3年(并且细化到月)的计划.

不要盲目的面试,要针对即将面试的工作,准备面试.

首先针对一个自己没有接触到的知识,先使用 百度\谷歌等网站搜索资料.然后进行学习

这是个好东西,我劝你多用用https://developer.apple.com/search/

尝试写一个demo,对新技术进行熟悉.

如果市面上有成熟的Demo或者第三方库,下载下来进行学习.

在熟悉的过程中,遇到了任何问题,再进行百度/谷歌,学习不同人的不同看法和解决方法.


职业规划

个人观点

首先是针对iOS这个行业,找不到工作要从自身找原因,为什么自己没有大公司的工作经历,为什么大公司会把自己毙掉,因为实力不够,因为你菜,你不够强.要从自身找原因,是,培训机构一阵子培训了一堆iOS开发工程师,但你不能从特么一堆菜鸟中杀出去,你就是菜鸟,不要怨天尤人了,好好努力学习.

不要只做到鹤立鸡群,而想着怎么离开这群鸡,重归鹤群.

针对程序员行业,这是一个需要努力奋斗的行业,也许他并不需要你有多高的文凭,好的文凭可以去大公司工作,没有好的文凭,但拥有丰富的工作经验,和开源库,也会是你本人实力的体现.所以,努力学习,路是自己走出来的,原地踏步谁也救不了你.

职业规划一般分为两种,横向和纵向,程序员行业横向走项目经理提成获得分红,纵向发展成为技术经理,必要时可以自行创业


产品公司&外包公司

外包公司与产品公司有什么区别呢,本质上的区别就是,模式不同。产品公司针对的是自己的产品,如何升级迭代做到更好,拥有更多的用户流量,如何设计功能进行盈利。而外包公司针对的是客户,项目经理往往会和销售谈妥一件件生意,隔一段时间开一个产品会议,使得开发部门,人手几个项目一起开发。这两种模式也是各有利弊。

先说外包公司的模式吧,一个好的外包公司,可能福利会好很多,阶级斗争不是很明显,大家就像打工的一样,拿着工资和项目提成,项目比较紧,成熟的外包公司拥有统一化的管理,和优秀的代码规范;

但如果是比较差的外包公司,那就不一样了,整体项目以完成为目的,不需要维护,往往只需要做出来一个雏形,不会到处崩溃,交货之后,此app将再也没有关系,如果需要维护,就再交钱。不论好与坏的外包公司,他的盈利模式就像是流水线,只需要出货量,不要求质量。这对于刚刚步入程序员行列的人会很不利,会养成不用维护,不用注重用户体验,不用做流畅度,耗电量,并发量的测试的坏习惯,得过且过。

总之不用考虑太多。这也是市面上大公司有些会看你之前的工作经历的原因,如果是外包,对不起,我们不要。

产品公司的模式,就是升职加薪,干得越久福利越好,万一你比较幸运,有幸成为未来几年要火的产品的开发者,那就是offer不断啊。产品公司往往分为有成品项目和创业两种。

成品项目人员变动一般较少,阶级斗争比较严重,为了职位更上一层楼,勾心斗角。不过在开发团队还是比较罕见的,大家大部分都是想跳槽的。

创业公司往往需要人才,全面性的人才,就单单说iOS,一个创业公司可能会要求你会 直播,支付,蓝牙,聊天,这也都是老功能了,现在都是什么 AR啊 人脸识别啊。你不学习新知识,注定被淘汰。外包公司也有一点好处就是,涉及的应用多,那功能也就自然而然比较多(如果全部接的那种简单的应用当我没说)。


做一个负责任的开发者

那么现在说正题,如何成为负责任的开发者?

首先要负责,对自己的项目负责。如果是自己新开的项目,要保证随时都能清晰的想到项目当中每个地方是怎么实现的,测试或者用户反馈了问题以后,能立马想到可能的错误原因。

如果是接手的项目,就要尽快去了解主要的界面和功能是如何实现的。你只有先做好自己分内的事,才有机会去顾暇别人的事。


1.保持一个良好的代码规范以及文件架构。
2.每天要给自己做一个TodoList 和一个BugList,时刻保持自己是在有效率的工作,严重的需要时间修复的bug汇报上去,小bug自己记下来偷偷修复。
3.有空时将排行榜上的应用下载排名靠前的应用,去欣赏并分析主流app的界面,功能实现,在拿到设计图时,去考虑界面的合理性,功能怎么实现最符合用户的操作习惯。
4.要有一定的协调能力,交流能力,稍微了解一点后台知识以及前端知识。
5.信念,一个不做初级iOS开发的信念。多去了解,不会被别人当小白,学多少都是自己的,至于在你去学习的时候,有人会说风言风语,这就是区别,他们活该初级,自己不会的东西,也看不惯别人去学习。所以,一定要有一个规划,按照自己正确的规划去学习,去成长,别原地踏步。


关于后台你需要懂什么呢,如何设计接口文档,接口怎么设计合理,后台拿到你请求的数据是怎么存储的,你需要的数据后台又是怎么查询给你的,请求方式什么时候用get什么时候适合post,JSON格式的数据以及XML数据又有什么好处。

关于前端你需要了解什么呢,这里大致提一下H5和app交互,比如H5怎么调你的方法,你怎么调H5的方法,数据如何传递,图片如何交给H5显示,这些都需要去了解。

有些人会觉得,我上面说的这都是废话,或者说你知道有什么用吗,又没你提意见的资格。iOS的群普遍是什么风气,就是你提出来一个建议或者意见,如果路人甲会,他就趾高气昂怼你一顿,如果他不会,他就会说,会这个又没用,懂这么多又没用什么的bulabulabula。这就是第五点。

如果你想变强,那就做点什么.


iOS开发学习路线

iOS定位

  • iOS定位

    • 简介:这里的定位,仅仅代表我个人意见,仅符合本笔记如何学习从哪里开始学习,怎么去学习来说.
    • 尚未入门
      • 如何判断自己是否入门
        • 是否了解Mac
        • 是否了解Xcode
        • 是否了解Objective-C
        • 是否会使用UI控件.
        • 如果上面的都不了解,那说明你还没有入门,请从iOS学习路线开始学习.
    • 初级iOS开发
      • 说明:作为一名初级的iOS开发,你需要具备以下技能
      • 必备技能(全部都会的情况下查看下一项)
        • Xcode的使用
        • 第三方库的灵活使用
          • AFN
          • MJRefresh
        • 各种网站的使用
      • 如何判断是否可以升阶
        • 是否了解AFNetworking 的实现原理
        • 是否了解SDAutolayout/Masonry 一种布局库的原理
        • 是否能够处理基本的iOS崩溃原因/无法编译原因/无法上架原因?
        • 是否拥有了一定的工作效率,稳定的工作效率.(而不是说,上面派了一个活下来,忙都忙不完,天天加班,还一堆bug)
        • 是否能够处理第三方库引起的崩溃.
        • 是否可以很好的融入工作环境,完成每一阶段的工作指标,而不会让自己疲惫不堪.
      • 结论
        • iOS中级开发说白了,就是你学会了基本的UI界面搭建,上架,沉淀一段时间,你觉得自己还适合这门行业,还适合,还能接受 这个所谓的iOS开发工程师的行业.你就可以说是一名中级iOS开发.
        • 这个沉淀时间 大约在1年的实际工作中,就可以完成.
        • 如果你觉得这门行业不适合你,请仔细结合自身情况,是否转另一门计算机语言,还是彻底转行.
    • 中级iOS开发
      • 说明:作为一名中级的iOS开发,你需要具备以下技能
      • 必备技能(全部都会的情况下查看下一项)
        • 应用的内存处理
        • 应用的推送处理
        • 应用的模块化/单元测试
        • 应用的第三方集成/集中化管理/稳定迭代
        • 阅读强大的第三方源码/拥有快速上手新的第三方库的能力.
        • 能够接受各种新功能的开发(这里是指,即使你没有做过,但是你仍然可以凭借着学习,解决任何业务需求:例如:蓝牙.AR.摄像头.硬件交互.等)
        • 清楚明白数据的传递方式,应用与后台如何交换数据,交换数据的过程,结果,格式.
        • 多线程的灵活使用.
        • 各种并发事件的处理/以及界面的合理性/流畅度
        • 设计模式的灵活使用.
      • 如何判断是否可以升阶
      • 结论
    • 高级iOS开发
      • 说明:作为一名高级的iOS开发,你需要具备以下技能(我不是高级开发,所以这里只能给你们提供建议.)
      • 必备技能
        • 应用的组件化/架构分层
        • 数据结构,操作系统,计算机网络都有自己的了解和认知
        • Shell脚本/python/Ruby/JS 至少会一种.

详细学习路线

  • 学习路线
    • 简介
      这里只简单阐述一些概念性的东西,以及学习路线规划,真正的知识请从iOS基础知识点往下开始看.
    • Objective-C
      • 介绍
      • 概念
      • 编译原理
    • 程序启动原理
      • App组成
        • Info.plist
        • .pch
      • 打开程序
      • 执行main函数
      • 执行UIApplicationMain函数
      • 初始化UIApplication(创建设置代理对象,开启事件循环)
      • 监听系统事件
      • 结束程序.
    • 语法.(此处定义可能略失严谨,口头教学为主)
      • 基础语法
      • 对象.
      • 属性
      • 数据类型
      • 方法
      • 继承
      • Frame/CGRect/CGPoint和CGSize
      • 内存(针对MRC下情况进行介绍)
      • ARC/MRC
      • 弱引用/强引用
      • Assign,retain,copy,strong
      • import 和@class的区别
    • Xcode使用
      • 首先是针对Xcode菜单栏,希望自己可以去翻译一下每个菜单里每项功能的英文都是什么意思,有助于自己熟悉并加深印象的使用Xcode.
      • 熟悉Xcode的各个功能.
    • UIKit控件.
    • 界面分析(下载App进行学习).
      • 在这里推荐有兴趣的开发人员,下载并分析,AppStore中的每项分类的top50的应用,多学习大公司以及流行应用是如何开发应用的,其中流行的,新颖的开发界面的方式可以总结下来,猜想在大应用中,别的程序员是如何开发的.
      • 界面适配
    • 代码架构.
    • 各种工具、第三方的使用.
      • 其实每个项目的建立都大致分为:项目框架搭建,原生界面搭建,嵌入第三方库.有很多功能都会用到第三方库,大多数第三方库都是本着快速开发,完整功能实现的目的存在的.需要开发人员能够根据业务逻辑不同,选择最好最优质的第三方库进行使用.
    • 代码封装
      • 当使用较多第三方库后,要求开发人员学习其开发特点,以及其封装手法,运用在自己的项目上,封装自己的代码.灵活运用.
    • 完整项目.
    • 开发技巧
    • 个人心得

iOS基础知识点

  • iOS基础知识点
    • 如何学习iOS
      • 刚刚入门(如何学习)
        • 打好基础,学习OC中各种常用语法.
        • 学习如何上架,上架会因为什么被拒,了解App上架规则.
        • 多学习官方说明文档.
      • 刚刚入职1年(如何稳定)
        • 多看开源或者注明的第三方库.
        • 收藏并阅读各种大神的博客或者论坛.
        • 开始考虑项目中的细节优化,内存处理和耗电情况
      • 入职3年(如何进阶)
        • 开始涉猎不止于iOS领域中的知识,会去了解相关职位的基础知识,例如前端和后台或者服务器运维,或者项目相关知识,具体往自己的职业规划靠拢
    • 框架的学习
      • 苹果自带框架
      • 第三方框架
        • AFNetworking
        • SDAutoLayout
        • YYKit
        • SDWebImage
        • MJRefresh
        • MJExtension
        • Bugly
        • Qiniu
        • Masonry
        • TZImagePickerController
        • Hyphenate_CN
    • 基础UI控件
      • UILabel 标题栏
      • UIButton 按钮
      • UIImageView 图片视图
      • UITextField 文本输入框
      • UITextView 文本展示视图
      • UIProgressView 进度条
      • UISlider 滑动开关
      • UIGesture 手势
      • UIActivityIndicator 菊花控件
      • UIAlertView(iOS8废除) 警告框
      • UIActionSheet(iOS8废除) 操作表单
      • UIAlertController(iOS8出现) 警告视图控制器
      • UIScrollView 滚动视图
      • UIPageControl 页面控制器
      • UISearchBar 搜索框
      • UITableView 表视图
      • UICollectionView集合视图
      • UIWebView网页浏览器
      • UISwitch开关
      • UISegmentControl选择按钮
      • UIPickerView选择器
      • UIDatePicker日期选择器
      • UIToolbar工具栏
      • UINavigationBar通知栏
      • UINavigationController通知视图控制器
      • UITabbarController选择视图控制器
      • UIImagePickerController相册
      • UIImage图片
    • Xcode的使用
      • 基础操作 状态栏
      • 偏好设置
      • Xcode Source Control 源代码管理器
      • Xcode workSpace工作组
      • Xcode Scheme 计划
      • Xcode AutoLayout 约束
      • Xcode CoreData数据库
      • LLDB 断点调试
      • StoryBoard
      • 界面预览
      • 界面适配
      • 内存监测
      • 全局断点
      • 全局搜索替换
    • 数据存储
      • plist
      • NSKeyedArchiver
      • SQLite
      • FMDB
      • CoreData
      • NSUserDefault
      • 沙盒存储
      • NSDictionary归档
    • App生命周期
      • 应用生命周期
      • 控制器生命周期
        • alloc
        • init
        • 创建View
        • ViewDidLoad
        • ViewWillAppear
        • ViewDidAppear
        • ViewWillDisappear
          • 视图将要消失 (做一些视图将要消失时的UI的处理)
        • ViewDidDisappear
          • 视图已经消失 (做一些视图消失之后数据的处理)
          • viewDidDisappear销毁定时器
        • dealloc
        • didReceiveMemoryWarning
    • 开发者账号&上架流程
      • 个人
      • 公司
      • 企业
    • 常用知识
      • 通信
      • NS系列
      • 宏定义
      • 视图层次
      • 切换视图
      • 深浅拷贝
      • 对象序列化
      • 写入文件
      • 获取沙盒路径
      • 翻转视图
      • 延伸视图
      • 九大基本数据类型
      • 九宫格
      • 坐标比较
      • UIColor 、CIColor和CGColor 之间的关系
      • 画图
      • 静态变量
      • tag值
      • 延时执行方法
      • 界面旋转+状态栏隐藏
      • plist文件
      • KVC/KVO
      • 谓词NSPredicate
      • 帧动画
      • AutoLayout
      • isKindOfClass 与 isMemberOfClass
      • Return/Break/Continue
      • Core Animation
      • CALayer
      • Quartz2D
      • 真机调试
      • 静态库
      • 内存管理
      • iPad与iPhone的区别
      • 响应链
      • 异常捕捉
      • 国际化
      • 代码模块化
      • 类别/扩展

中级知识点

  • 设计模式

  • UIScrollView/UITableView/UICollectionView 的嵌套

  • 动态行高

  • 通知/代理/block

  • 程序启动原理

  • 触摸事件/手势

  • 图文混编

  • Runtime

  • NSRunLoop

  • GCD

  • ReactiveCocoa开发

  • 3DTouch

  • 界面渲染

  • Charles花瓶抓包

  • 区分模拟器/真机项目

  • 常用知识

    • 单例模式
    • 多线程
    • 网络请求
    • 定位
    • 源代码管理Git
    • 真机调试
    • 苹果内购/广告
    • 推送/远程推送
    • 音频/视频/二维码
    • Block
    • 蓝牙/传感器
    • 物理仿真器UIDynamic
    • 通讯录获取

iOS高级知识点

  • iOS高级知识点
    • Socket
    • XMPP
    • 加密
      • MD5详解
      • Base64加密解密
      • RSA非对称加密
      • AES对称加密
    • 音频
      • 基础
      • Core Audio
      • Audio Toolbox
      • OpenAL
      • AVFoundation
      • Speex语音聊天
      • AudioQueue/AudioSession
      • Speex简介
    • 视频
      • AAC视频.H264推流
      • P2P传输
    • 直播
      • 直播的技术分析与实现
      • RTMP协议
      • RTMP直播应用与延时分析
      • 如果做一款inke版的App
      • 推流发布和播放RTMP
      • FFmpeg
      • 基于FFmpeg的推流器
      • HLS流媒体传输协议(HTTP Live Streaming)
      • FFmpeg
      • ijkPlayer
    • 算法
      • 简介
      • 冒泡排序
      • 快速排序
      • 插入排序
      • 归并排序
      • 二分查找
      • 希尔排序
      • 动态规划
      • 堆排序

官方Kit

  • ARKit.
  • SiriKit
  • HealthKit
  • HomeKit
  • SearchKit
  • IOKit
  • PDFKit
  • CloudKit
  • GameplayKit
  • SpriteKit
  • SceneKit
  • MusicKit
  • ResearchKit
  • MapKit
  • StoreKit
  • AVKit


作者:iOS_asuka
链接:https://www.jianshu.com/p/1ac0a69cd60a


收起阅读 »

Android转场动画的前世今生

前一段时间做图片查看器的升级时,在打开图片查看器的时,找不到好的过渡方式。医生推荐了Android最新的Material Motion动画,虽然最终没有给我们的App安排,但给我学习Material Motion动画提供了一次契机。推荐给大家的学习资料:什么是...
继续阅读 »

前一段时间做图片查看器的升级时,在打开图片查看器的时,找不到好的过渡方式。

医生推荐了Android最新的Material Motion动画,虽然最终没有给我们的App安排,但给我学习Material Motion动画提供了一次契机。

推荐给大家的学习资料:

什么是转场动画?

在学习动画的时候,我们总是会听到转场动画,那么,什么是转场动画呢?

首先,对于一个动画而言,两个关键帧是动画的开始帧和动画的结束帧,转场则是两个关键帧之间的过渡。

一个完整的转场动画如图:

完整转场

图片来自《动态设计的转场心法》

一、最初的转场

先教大家一个干货:

adb shell settings put global window_animation_scale 10
adb shell settings put global transition_animation_scale 10
adb shell settings put global animator_duration_scale 10

这个命令可以将动画放慢10倍,方便学习动画的细节,速度恢复则把10改成1。

还记得一开始两个 Activity 怎么过渡的吗?没错就是使用 overridePendingTransition 方法。

Android 2.0 以后可以使用 overridePendingTransition(int enterAnim, int exitAnim) 来完成 Activity 的跳转动画,其中,第一个参数 exitAnim 对应着上述图片转场中的 IN,第二个参数 enterAnim 对应着上述图片中的 OUT

如果要写一个平移和透明度跳转动画,它通常是这样的:

步骤一 设置进入和退出动画

在资源文件下 anim 目录下新建一个动画的资源文件,Activity 进入动画 anim_in 文件:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="500">

<translate
android:fromXDelta="100%p"
android:toXDelta="0"
/>

<alpha
android:fromAlpha="0.0"
android:toAlpha="1.0"
/>
</set>

Activity 退出动画 anim_out 文件:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="500">

<translate
android:fromXDelta="0"
android:toXDelta="-100%p"
/>

<alpha
android:fromAlpha="1.0"
android:toAlpha="0.0"
/>
</set>

步骤二 引用动画文件

在界面跳转的时候,调用 overridePendingTransition 方法:

companion object {
fun start(context: Context){
val intent = Intent(context, SecondActivity::class.java)
context.startActivity(intent)
if(context is Activity){
context.overridePendingTransition(R.anim.anim_in, R.anim.anim_out)
}
}
}

效果:

资源文件动画

overridePendingTransition写法的问题

和View动画一样,使用虽爽,但只支持平移、旋转、缩放和透明度这四种动画类型,遇到稍微复杂的动画也只能撒手了。

二、Android 5.0 Material 转场动画

在 Android 5.0 之后,我们可以使用 Material Design 为我们带来的转场动画。

不说别的,先看几个案例:

官方Demo掘金App我的App

从左到右依次是官方Demo、掘金App和我的开源项目Hoo,与最初的转场的不同点如下:

  1. 如果说 overridePendingTransition 对应着 View 动画,那么 Material 转场对应着的是属性动画,所以可以自定义界面过渡动画。
  2. 除了进入、退出场景,Material 转场为我们增加一种新的场景,共享元素,上述三图的动画过渡都用到了共享元素。
  3. 不仅仅能用在Activity,还可以用在Fragment和View之间。

三张图中都使用了ImageView作为共享元素(Hoo中使用更加复杂的PhotoView),共享元素的动画看着十分有趣,看着就像图片从A界面中跳到了B界面上。

为什么我可以判断掘金也是使用的 Material 转场?因为 Material 共享元素动画开始的时候默认会将 StartView 的 Alpha 设置为0,仔细看掘金大图打开的一瞬间,后面的图已经没了~,并且一开始过渡还有一点小瑕疵。

1. 进入和退出动画

进入和退出动画不包括共享元素的动画,只支持三种动画类型:

动画解释
Explode(爆炸式)将视图移入场景中心或从中移出
Slide(滑动式)将视图从场景的其中一个边缘移入或移出
Fade(淡入淡出式)通过更改视图的不透明度,在场景中添加视图或从中移除视图

细心的同学可能发现,Material Design没有支持 Scale 和 Rotation 这两种类型的动画,可能这两种类型的过渡动画使用场景实在太少,如果实在想用,可以自定义实现。

步骤一 创建Material Bundle

    startActivity(intent,
ActivityOptions.makeSceneTransitionAnimation(this).toBundle())

步骤二 设置动画

override fun onCreate(savedInstanceState: Bundle?) {
// 开启Material动画
window.requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS)
super.onCreate(savedInstanceState)
//setContentView(R.layout.detail_activity)
// 设置进入的动画
window.enterTransition = Slide()
// 设置退出动画
window.exitTransition = Slide()
}

除了这种方式,还可以通过设置主题的方式设置进入和退出动画,同样也适用于共享动画。

2. 共享元素动画

启用共享元素动画的步骤跟之前的步骤稍有不同。

步骤一 设置Activity A

先给 View 设置 transitionName

ivShoe.transitionName = transitionName

接着,它需要提供共享的 View 和 TransitionName

其实,就是想让你告诉系统,什么样的 View 需要做动画,那如果有多个 View 呢?所以,你还得给View 绑定一个 TransitionName,防止动画做混了。

代码:

val options = ActivityOptions.makeSceneTransitionAnimation(this, binding.ivShoe, transitionName)
ImageGalleryActivity.start(this, it, options.toBundle(), transitionName)

如果有多个共享元素,可以将关系存进 Pair,然后把 Pair 放进去,不懂的可以看一下 Api。

步骤二 为Activity B设置共享元素动画

默认支持的共享元素的动画也是有限的,支持的种类有:

动画说明
changeBounds为目标视图布局边界的变化添加动画效果
changeClipBounds为目标视图裁剪边界的变化添加动画效果
changeTransform为目标视图缩放和旋转方面的变化添加动画效果
changeImageTransform为目标图片尺寸和缩放方面的变化添加动画效果

通过 Window 设置 sharedElementEnterTransition 和 sharedElementExitTransition

override fun onCreate(savedInstanceState: Bundle?) {
// 开启Material动画
window.requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS)
val transitionSet = TransitionSet()
transitionSet.addTransition(ChangeBounds())
transitionSet.addTransition(ChangeClipBounds())
transitionSet.addTransition(ChangeImageTransform())
window.sharedElementEnterTransition = transitionSet
window.sharedElementExitTransition = transitionSet
super.onCreate(savedInstanceState)
// 我这里的transitionName是通过Intent传进去的
val transitionName = intent.getStringExtra(CUS_TRANSITION_NAME)
// 给ImageView设置transitionName
binding.ivShoe.transitionName = transitionName
}

这样写完大部分场景都是可以用的,但是,如果你是通过 Glide 加载或者其他图片库加载的网络图片,恭喜你,大概率会遇到这样的问题:

列表页动画

为什么会出现这样的情况?因为加载网络图片是需要时间的,我们可以等 B 页面的图片加载好了,再去开启动画,Material 装厂就支持这样的操作。

在 onCreate 中调用 postponeEnterTransition() 方法表明我们的动画需要延迟执行,等我们需要的时机,再调用 Activity 中的 startPostponedEnterTransition() 方法来开始执行动画,所以,即便是在 A 界面中,跳转到 B 界面中的 Fragment,动画也是一样可以执行的。

到这儿,界面就可以正常跳转了,图片就不放了。

共享元素动画原理其实也很简单,如果是 A 跳到 B,会先把 A 和 B 的共享元素的状态分别记录下来,之后跳到 B,根据先前记录的状态执行属性动画,虽然是叫共享元素,它们可是不同的 View

不仅仅 Activity 可以支持 Material 转场动画,Fragment 和 View 也都是可以的(之前我一直以为是不可以的~),感兴趣的同学可以自行研究。

三、Android Material Motion动画

新出的 Motion 动画是什么呢?

1. Android Motion 简介

其实它就是新支持的四种动画类型,分别是:

1.1 Container transform

container_transform

Container transform 也是基于共享元素的动画,跟之前共享元素动画最大的不同点在于它的 Start View可以是一个 ViewGroup,也可以是一个 View,如图一中所看到的那样,它的 Start View 是一个 CardView

1.2 Shared axis

shared_axis

Shared axis 看上去像平移动画,官方展示的三个例子分别是,横向平移、纵向平移和Z轴平移。

1.3 Fade Through

fade_through

Fade Through 本质上是一个透明度+缩放动画,官方的建议是用在两个关联性不强的界面的跳转中。

1.4 Fade

fade

乍一看,Fade 动画和上面的 Fade Through 是一致的,就动画本质而言,它们的确是一样的透明度+缩放动画,但是官方建议,如果发生在同一个界面,比如弹出Dialog、Menu等这类的弹框可以考虑这种动画。

Google 提供了两种库供大家使用。

一种是 AndroidX 包,特点是:

  • 兼容到 API 14
  • 仅支持 Fragment 和 View 之间的过渡
  • 行为一致性

另外一种是 Platform 包,特点是:

  • 兼容到 API 21
  • 支持 Fragment、View、Activity 和 Window
  • 在不同的 API 上,可能会有点差异

现在的 App,最低版本应该都在 21 了,而且支持 Activity,所以建议还是选择 Platform。

2. Material Motion 初体验

我们以 Container transform 为例,来个 Activity 之间的 Android Motion 动画的初体验:

Container Transform

步骤一 引入依赖

implementation 'com.google.android.material:material:1.4.0-alpha01'

步骤二 设置Activity A

这里的 Activity A 对应着 MainActivity,在 MainActivity 中启用转场动画:

class MainActivity : AppCompatActivity() {

//...
override fun onCreate(savedInstanceState: Bundle?) {
window.requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS)
setExitSharedElementCallback(MaterialContainerTransformSharedElementCallback())
window.sharedElementsUseOverlay = false
super.onCreate(savedInstanceState)
//...
}
}

步骤三 设置跳转事件

跟创建共享元素的步骤一样,先设置 TransitionName:

private fun onCreateListener(id: Long, url: String): View.OnClickListener {
return View.OnClickListener {
val transitionName = "${id}-${url}"
it.transitionName = transitionName
DetailActivity.start(context, id, it as ConstraintLayout, transitionName)
}
}

这里偷了懒,将 TransitionName 的设置放在了点击事件中,接着创建 Bundle:

const val CUS_TRANSITION_NAME: String = "transition_name"
class DetailActivity : AppCompatActivity() {
companion object {
fun start(context: Context, id: Long, viewGroup: ConstraintLayout, transitionName: String){
val intent = Intent(context, DetailActivity::class.java)
intent.putExtra(BaseConstant.DETAIL_SHOE_ID, id)
intent.putExtra(CUS_TRANSITION_NAME, transitionName)
if(context is Activity){
context.startActivity(intent, ActivityOptions.makeSceneTransitionAnimation(context, viewGroup, transitionName).toBundle())
}else {
context.startActivity(intent)
}
}
}
}

步骤四 设置Activity B

Demo 中的 Activity B 对应着 DetailActivity,这一步主要给进入和退出的共享动画设置 MaterialContainerTransform,具体的代码是:

override fun onCreate(savedInstanceState: Bundle?) {
// 1. 设置动画
window.requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS)
setEnterSharedElementCallback(MaterialContainerTransformSharedElementCallback())

super.onCreate(savedInstanceState)
//...

// 2. 设置transitionName
binding.mainContent.transitionName = intent.getStringExtra(CUS_TRANSITION_NAME)
// 3. 设置具体的动画
window.sharedElementEnterTransition = MaterialContainerTransform().apply {
addTarget(binding.mainContent)
duration = 300L
}
window.sharedElementExitTransition = MaterialContainerTransform().apply {
addTarget(binding.mainContent)
duration = 300L
}
}

Demo 中使用了 DataBinding,不过你只需要了解 binding.mainContent 是一个 ViewGroup。到这儿,你就可以成功的看到 Demo 中的效果了。

Material Motion 其实 Android 5.0 中加入的转场动画一样,它们也继承自 Transition,但给我们的使用带来了很大的方便。

四、总结

在 Android 转场的过程中:

  1. 最初的 View 转场带给我们平移、缩放、旋转和透明度四种基本能力的支持;
  2. 接着,Android 5.0 Material 转场给我们带来了共享元素动画的惊喜,并具备了自定义转场动画的能力,升级了Android转场的玩法;
  3. 最后是出来不久的 Android Motion,通过封装了四种动画,降低了我们转场的使用难度。

虽然起点的图片查看器的专场没有使用 Material 转场,但是过度依然丝滑,感兴趣的话我会在后面单独开一篇。

收起阅读 »

Android判断Activity是否在AndroidManifest.xml里面注册(源码分析)

Android判断Activity是否在AndroidManifest.xml里面注册(源码分析) 这个问题相信大家在实际的开发中,都遇到过这个问题,答案就不用说了,在AndroidManifest.xml中添加Activity的注册,毕竟Activity...
继续阅读 »


Android判断Activity是否在AndroidManifest.xml里面注册(源码分析)


在这里插入图片描述
这个问题相信大家在实际的开发中,都遇到过这个问题,答案就不用说了,在AndroidManifest.xml中添加Activity的注册,毕竟Activity属于四大组件之一,使用的时候,需要要在清单文件中注册。


<activity android:name=".TargetActivity"></activity>

但是这个出现这个问题的根源在哪里?下面我们就进入源码仔细看看。


这里就不一步一步进入源码,直接分析关键代码:


public ActivityResult execStartActivity( Context who, IBinder contextThread, IBinder token, Activity target, Intent intent, int requestCode, Bundle options) { ...
try {
intent.migrateExtraStreamToClipData();
intent.prepareToLeaveProcess(who);
? //1.通过IActivityManager调用我们执行AMS的startActivity方法,并返回执行 结果
int result = ActivityManager.getService() .startActivity(whoThread, who.getBasePackageName(), intent, intent.resolveTypeIfNeeded(who.getContentResolver()), token, target != null ? target.mEmbeddedID : null, requestCode, 0, null, options);
//2. 检查结果
checkStartActivityResult(result, intent);
} catch (RemoteException e) {
throw new RuntimeException("Failure from system", e);
}
return null;
}

通过源码execStartActivity这个方法可以看到主要是在这个检查结果这里面去分析的checkStartActivityResult(result, intent);


public static void checkStartActivityResult(int res, Object intent) {
if (!ActivityManager.isStartResultFatalError(res)) {
return;
}
switch (res) {
case ActivityManager.START_INTENT_NOT_RESOLVED:
case ActivityManager.START_CLASS_NOT_FOUND:
//3. 这里我们找到了报错的地方,原来是res结果为 START_INTENT_NOT_RESOLVED,
// START_CLASS_NOT_FOUND就会报这个错误
if (intent instanceof Intent && ((Intent) intent).getComponent() != null)
throw new ActivityNotFoundException("Unable to find explicit activity class " + ((Intent) intent).getComponent().toShortString() + "; have you declared this activity in your AndroidManifest.xml?");
throw new ActivityNotFoundException("No Activity found to handle " + intent);
case ActivityManager.START_PERMISSION_DENIED:
throw new SecurityException("Not allowed to start activity " + intent);
case ActivityManager.START_FORWARD_AND_REQUEST_CONFLICT:
throw new AndroidRuntimeException("FORWARD_RESULT_FLAG used while also requesting a result");
case ActivityManager.START_NOT_ACTIVITY:
throw new IllegalArgumentException("PendingIntent is not an activity");
case ActivityManager.START_NOT_VOICE_COMPATIBLE:
throw new SecurityException("Starting under voice control not allowed for: " + intent);
case ActivityManager.START_VOICE_NOT_ACTIVE_SESSION:
throw new IllegalStateException("Session calling startVoiceActivity does not match active session");
case ActivityManager.START_VOICE_HIDDEN_SESSION:
throw new IllegalStateException("Cannot start voice activity on a hidden session");
case ActivityManager.START_ASSISTANT_NOT_ACTIVE_SESSION:
throw new IllegalStateException("Session calling startAssistantActivity does not match active session");
case ActivityManager.START_ASSISTANT_HIDDEN_SESSION:
throw new IllegalStateException("Cannot start assistant activity on a hidden session");
case ActivityManager.START_CANCELED:
throw new AndroidRuntimeException("Activity could not be started for " + intent);
default:
throw new AndroidRuntimeException("Unknown error code " + res + " when starting " + intent);
}
}

可以看到当结果为


case ActivityManager.START_INTENT_NOT_RESOLVED:


case ActivityManager.START_CLASS_NOT_FOUND:


?就会报
throw new ActivityNotFoundException("Unable to find explicit activity class " + ((Intent) intent).getComponent().toShortString() + “; have you declared this activity in your AndroidManifest.xml?”);


这下我们就知道了如果没在清单文件中添加这个注册,报错的位置。


AMS是如何判断activity没有注册的,首先我们得明白startActivity执行的主流程


这个篇幅太多了,可以自己去源码跟一下,这里不作介绍,


我们这里分析主要流程代码


找到在ASR.startActivity (ActivityStarter)中返回了


START_INTENT_NOT_RESOLVED,START_CLASS_NOT_FOUND


private int startActivity(IApplicationThread caller, Intent intent, Intent ephemeralIntent, String resolvedType, ActivityInfo aInfo, ResolveInfo rInfo, IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor, IBinder resultTo, String resultWho, int requestCode, int callingPid, int callingUid, String callingPackage, int realCallingPid, int realCallingUid, int startFlags, SafeActivityOptions options, boolean ignoreTargetSecurity, boolean componentSpecified, ActivityRecord[] outActivity, TaskRecord inTask, boolean allowPendingRemoteAnimationRegistryLookup) {
int err = ActivityManager.START_SUCCESS;
...
//接下来开始做一些校验判断
if (err == ActivityManager.START_SUCCESS && intent.getComponent() == null) {
// We couldn't find a class that can handle the given Intent.
// That's the end of that! err = ActivityManager.START_INTENT_NOT_RESOLVED;
// 从Intent中无法找 到相应的Component
}
if (err == ActivityManager.START_SUCCESS && aInfo == null) {
// We couldn't find the specific class specified in the Intent.
// Also the end of the line.
err = ActivityManager.START_CLASS_NOT_FOUND;
// 从Intent中无法找到相 应的ActivityInfo
}
...
if (err != START_SUCCESS) {
//不能成功启动了,返回err
if (resultRecord != null) {
resultStack.sendActivityResultLocked(-1, resultRecord, resultWho, requestCode, RESULT_CANCELED, null);
}
SafeActivityOptions.abort(options);
return err;
}
//创建出我们的目标ActivityRecord对象,存到传入数组0索引上
ActivityRecord r = new ActivityRecord(mService, callerApp, callingPid, callingUid, callingPackage, intent, resolvedType, aInfo, mService.getGlobalConfiguration(), resultRecord, resultWho, requestCode, componentSpecified, voiceSession != null, mSupervisor, checkedOptions, sourceRecord);
...
return startActivity(r, sourceRecord, voiceSession, voiceInteractor, startFlags, true /* doResume */, checkedOptions, inTask, outActivity);
}

但是 intent.getComponent(),aInfo又是从哪儿获取的呢,我们回溯到


startActivityMayWait.


看下上面的aInfo哪来的.


ActivityInfo resolveActivity(Intent intent, ResolveInfo rInfo, int startFlags, ProfilerInfo profilerInfo) {
? final ActivityInfo aInfo = rInfo != null ? rInfo.activityInfo : null;
if (aInfo != null) {
// Store the found target back into the intent, because now that
// we have it we never want to do this again. For example, if the
// user navigates back to this point in the history, we should
// always restart the exact same activity.
intent.setComponent(new ComponentName(aInfo.applicationInfo.packageName, aInfo.name));
// Don't debug things in the system process ...
}
return aInfo;
}

发现是从rInfo来的


ResolveInfo resolveIntent(Intent intent, String resolvedType, int userId, int flags, int filterCallingUid) {
synchronized (mService) {
? try {...final long token = Binder.clearCallingIdentity();
try {
return mService.getPackageManagerInternalLocked().resolveIntent(intent, resolvedType, modifiedFlags, userId, true, filterCallingUid);
} finally {
Binder.restoreCallingIdentity(token);
} ...
}
}
}

rInfo的获取


PackageManagerInternal getPackageManagerInternalLocked() {
if (mPackageManagerInt == null) {
? mPackageManagerInt = LocalServices.getService(PackageManagerInternal.class);
}
return mPackageManagerInt;
}

具体实现类是PackageManagerService


?@Override
public ResolveInfo resolveIntent(Intent intent, String resolvedType, int flags, int userId) {
return resolveIntentInternal(intent, resolvedType, flags, userId, false, Binder.getCallingUid());
}

看resolveIntentInternal


private ResolveInfo resolveIntentInternal(Intent intent, String resolvedType,int flags, int userId, boolean resolveForStart, int filterCallingUid) {
try {...
//获取ResolveInfo列表
final List<ResolveInfo> query = queryIntentActivitiesInternal(intent, resolvedType, flags, filterCallingUid, userId, resolveForStart, true /*allowDynamicSplits*/);
? Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
//找出最好的返回
final ResolveInfo bestChoice = chooseBestActivity(intent, resolvedType, flags, query, userId);
return bestChoice;
} finally {
Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
}
}

看 queryIntentActivitiesInternal


private @NonNull List<ResolveInfo> queryIntentActivitiesInternal(Intent intent, String resolvedType, int flags, int filterCallingUid, int userId, boolean resolveForStart, boolean allowDynamicSplits) {
? ...
if (comp != null) {
final List<ResolveInfo> list = new ArrayList<ResolveInfo>(1);
final ActivityInfo ai = getActivityInfo(comp, flags, userId);
if (ai != null) {
...
}
}

原来是从getActivityInfo获取的


	@Override
public ActivityInfo getActivityInfo(ComponentName component, int flags, int userId) {
return getActivityInfoInternal(component, flags, Binder.getCallingUid(), userId);
? }

getActivityInfoInternal方法


private ActivityInfo getActivityInfoInternal(ComponentName component, int flags, int filterCallingUid, int userId) {
if (!sUserManager.exists(userId)) return null;
flags = updateFlagsForComponent(flags, userId, component);
if (!isRecentsAccessingChildProfiles(Binder.getCallingUid(), userId)) {
mPermissionManager.enforceCrossUserPermission(Binder.getCallingUid(), userId, false /* requireFullPermission */, false /* checkShell */, "get activity info");
}
synchronized (mPackages) {
//关键点
PackageParser.Activity a = mActivities.mActivities.get(component);
if (DEBUG_PACKAGE_INFO) Log.v(TAG, "getActivityInfo " + component + ": " + a);
? if (a != null && mSettings.isEnabledAndMatchLPr(a.info, flags, userId)) {
PackageSetting ps = mSettings.mPackages.get(component.getPackageName());
if (ps == null) return null;
if (filterAppAccessLPr(ps, filterCallingUid, component, TYPE_ACTIVITY, userId)) {
return null;
}
//关键点
return PackageParser.generateActivityInfo(a, flags, ps.readUserState(userId), userId);
}
if (mResolveComponentName.equals(component)) {
return PackageParser.generateActivityInfo(mResolveActivity, flags, new PackageUserState(), userId);
}
}
return null;
}

分析到这里,大家应该知道怎么回事了吧,其实就是解析了AndroidManifest.xml里面的信息,具体怎么解析,等有空了分析。


————————————————
版权声明:本文为CSDN博主「拖鞋王子猪」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/sinat_26397681/article/details/117904722



收起阅读 »

美团面试题:JVM的年轻代是怎么设计的?

1、JVM中的堆,一般分为三个部分,新生代、老年代和永久代。这个是你第一天学JVM就知道的。但你可以先想想,为什么需要把堆分代?不分代不能完成他所做的事情么? 2、是这样,如果没有分代,那我们所有的对象都在一块,GC 的时候就要先找到哪些对象没用,怎么找呢...
继续阅读 »

1、JVM中的堆,一般分为三个部分,新生代、老年代和永久代。这个是你第一天学JVM就知道的。但你可以先想想,为什么需要把堆分代?不分代不能完成他所做的事情么?


2、是这样,如果没有分代,那我们所有的对象都在一块,GC 的时候就要先找到哪些对象没用,怎么找呢?没分代就得对堆的所有区域进行扫描。但你知道,很多Java对象都是朝生夕死的,如果分代的话,我们可以把新创建的对象放到某一地方,GC的时候就可以迅速回收这块存“朝生夕死”对象的区域。


3、所以,一句话总结,分代的唯一理由就是优化 GC 性能。你这么记,就容易把知识串起来了。


4、HotSpot JVM 把年轻代分为了三部分:1 个 Eden 区和 2 个 Survivor 区(分别叫from 和 to),他们的默认比例为 8:1。一般情况下,新创建的对象都会被分配到 Eden区,这些对象经过第一次 Minor GC 后,如果仍然存活,将会被移到 Survivor 区。对象在 Survivor 区中每熬过一次 Minor GC,年龄就会增加 1 岁,当它的年龄增加到一定程度时,就会被移动到年老代中。这是一个对象的生存路径。


5、因为年轻代中的对象基本都是朝生夕死的( 80% 以上),所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片。


6、在 GC 开始的时候,对象只会存在于 Eden 区和名为“ From ”的 Survivor 区,Survivor 区“To”是空的。紧接着进行 GC,Eden 区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。


7、年龄达到一定值(可以通过-XX: MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次 GC 后,Eden 区和From 区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次 GC 前的“From”,新的“From”就是上次 GC 前的“To”。不管怎样,都会保证名为 To 的 Survivor 区域是空的。



8、Minor GC 会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。注意,我说的是 Minor GC,不是 Full GC,这俩的关系你要缕清楚。


9、好记吗?不好记,再给你做个类比。我叫小强,是一个普通的 Java 对象,我出生在Eden 区,在 Eden 区我还看到和我长的很像的小兄弟,我们在 Eden 区中玩了挺长时间。有一天 Eden 区中的人实在是太多了,我就被迫去了 Survivor 区的“From”区,自从去了 Survivor 区,我就开始漂了,有时候在 Survivor 的“From”区,有时候在 Survivor 的“To”区,居无定所。直到我 18 岁的时候,爸爸说我成人了,该去社会上闯闯了。于是我就去了年老代那边,年老代里,人很多,并且年龄都挺大的,我在这里也认识了很多人。在年老代里,我生活了20年(每次GC加一岁),然后去世了。


10、年轻代的JVM参数也不多,给你列出来了,你也不用记,用着多了自然就熟悉了。



总而言之,JVM 内存问题排查需要掌握一定的技巧,而这些技巧并不是告诉你,你就会用的,更重要的还是需要在实战中去应用



收起阅读 »

Android:OkHttp的理解和使用

OkHttp的理解和使用 1、什么是OkHttp 1、网络请求发展 历史上Http请求库优缺点 HttpURLConnection—>Apache HTTP Client—>Volley—->okHttp 2、项目开源...
继续阅读 »




OkHttp的理解和使用


在这里插入图片描述


1、什么是OkHttp


1、网络请求发展


历史上Http请求库优缺点



HttpURLConnection—>Apache HTTP Client—>Volley—->okHttp



2、项目开源地址



https://github.com/square/okhttp



3、OkHttp是什么



  • OKhttp是一个网络请求开源项目,Android网络请求轻量级框架,支持文件上传与下载,支持https。


2、OkHttp的作用


OkHttp是一个高效的HTTP库:



  • 支持HTTP/2, HTTP/2通过使用多路复用技术在一个单独的TCP连接上支持并发, 通过在一个连接上一次性发送多个请求来发送或接收数据

  • 如果HTTP/2不可用, 连接池复用技术也可以极大减少延时

  • 支持GZIP, 可以压缩下载体积

  • 响应缓存可以直接避免重复请求

  • 会从很多常用的连接问题中自动恢复

  • 如果您的服务器配置了多个IP地址, 当第一个IP连接失败的时候, OkHttp会自动尝试下一个IP OkHttp还处理了代理服务器问题和SSL握手失败问题


优势



  • 使用 OkHttp无需重写您程序中的网络代码。OkHttp实现了几乎和java.net.HttpURLConnection一样的API。如果您用了 Apache HttpClient,则OkHttp也提供了一个对应的okhttp-apache 模块


3、Okhttp的基本使用


Okhttp的基本使用,从以下五方面讲解:



  • 1.Get请求(同步和异步)

  • 2.POST请求表单(key-value)

  • 3.POST请求提交(JSON/String/文件等)

  • 4.文件下载

  • 5.请求超时设置


加入build.gradle


compile 'com.squareup.okhttp3:okhttp:3.6.0'

3.1、Http请求和响应的组成


http请求
在这里插入图片描述
所以一个类库要完成一个http请求, 需要包含 请求方法, 请求地址, 请求协议, 请求头, 请求体这五部分. 这些都在okhttp3.Request的类中有体现, 这个类正是代表http请求的类. 看下图:


在这里插入图片描述
其中HttpUrl类代表请求地址, String method代表请求方法, Headers代表请求头, RequestBody代表请求体. Object tag这个是用来取消http请求的标志, 这个我们先不管.


http响应


响应组成图:
在这里插入图片描述
可以看到大体由应答首行, 应答头, 应答体构成. 但是应答首行表达的信息过多, HTTP/1.1表示访问协议, 200是响应码, OK是描述状态的消息.


根据单一职责, 我们不应该把这么多内容用一个应答首行来表示. 这样的话, 我们的响应就应该由访问协议, 响应码, 描述信息, 响应头, 响应体来组成.


3.2、OkHttp请求和响应的组成


OkHttp请求


构造一个http请求, 并查看请求具体内容:


final Request request = new Request.Builder().url("https://github.com/").build();

我们看下在内存中, 这个请求是什么样子的, 是否如我们上文所说和请求方法, 请求地址, 请求头, 请求体一一对应.
在这里插入图片描述
OkHttp响应


OkHttp库怎么表示一个响应:
在这里插入图片描述
可以看到Response类里面有Protocol代表请求协议, int code代表响应码, String message代表描述信息, Headers代表响应头, ResponseBody代表响应体. 当然除此之外, 还有Request代表持有的请求, Handshake代表SSL/TLS握手协议验证时的信息, 这些额外信息我们暂时不问.


有了刚才说的OkHttp响应的类组成, 我们看下OkHttp请求后响应在内存中的内容:


final Request request = new Request.Builder().url("https://github.com/").build();
Response response = client.newCall(request).execute();

在这里插入图片描述


3.3、GET请求同步方法


同步GET的意思是一直等待http请求, 直到返回了响应. 在这之间会阻塞进程, 所以通过get不能在Android的主线程中执行, 否则会报错.


对于同步请求在请求时需要开启子线程,请求成功后需要跳转到UI线程修改UI。


public void getDatasync(){
new Thread(new Runnable() {
@Override
public void run() {
try {
OkHttpClient client = new OkHttpClient();//创建OkHttpClient对象
Request request = new Request.Builder()
.url("http://www.baidu.com")//请求接口。如果需要传参拼接到接口后面。
.build();//创建Request 对象
Response response = null;
response = client.newCall(request).execute();//得到Response 对象
if (response.isSuccessful()) {
Log.d("kwwl","response.code()=="+response.code());
Log.d("kwwl","response.message()=="+response.message());
Log.d("kwwl","res=="+response.body().string());
//此时的代码执行在子线程,修改UI的操作请使用handler跳转到UI线程。
}
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}

此时打印结果如下:


response.code()==200
response.message()OK;
res{“code”:200,“message”:success};

OkHttpClient实现了Call.Factory接口, 是Call的工厂类, Call负责发送执行请求和读取响应.
Request代表Http请求, 通过Request.Builder辅助类来构建.


client.newCall(request)通过传入一个http request, 返回一个Call调用. 然后执行execute()方法, 同步获得Response代表Http请求的响应. response.body()是ResponseBody类, 代表响应体


注意事项:


1,Response.code是http响应行中的code,如果访问成功则返回200.这个不是服务器设置的,而是http协议中自带的。res中的code才是服务器设置的。注意二者的区别。


2,response.body().string()本质是输入流的读操作,所以它还是网络请求的一部分,所以这行代码必须放在子线程。


3,response.body().string()只能调用一次,在第一次时有返回值,第二次再调用时将会返回null。原因是:response.body().string()的本质是输入流的读操作,必须有服务器的输出流的写操作时客户端的读操作才能得到数据。而服务器的写操作只执行一次,所以客户端的读操作也只能执行一次,第二次将返回null。


4、响应体的string()方法对于小文档来说十分方便高效. 但是如果响应体太大(超过1MB), 应避免使用 string()方法, 因为它会将把整个文档加载到内存中.


5、对于超过1MB的响应body, 应使用流的方式来处理响应body. 这和我们处理xml文档的逻辑是一致的, 小文件可以载入内存树状解析, 大文件就必须流式解析.


注解:


responseBody.string()获得字符串的表达形式, 或responseBody.bytes()获得字节数组的表达形式, 这两种形式都会把文档加入到内存. 也可以通过responseBody.charStream()和responseBody.byteStream()返回流来处理.


3.4、GET请求异步方法


异步GET是指在另外的工作线程中执行http请求, 请求时不会阻塞当前的线程, 所以可以在Android主线程中使用.


这种方式不用再次开启子线程,但回调方法是执行在子线程中,所以在更新UI时还要跳转到UI线程中。


下面是在一个工作线程中下载文件, 当响应可读时回调Callback接口. 当响应头准备好后, 就会调用Callback接口, 所以读取响应体时可能会阻塞. OkHttp现阶段不提供异步api来接收响应体。


private final OkHttpClient client = new OkHttpClient();

public void run() throws Exception {
Request request = new Request.Builder()
.url("http://publicobject.com/helloworld.txt")
.build();

client.newCall(request).enqueue(new Callback() {
@Override public void onFailure(Request request, Throwable throwable) {
throwable.printStackTrace();
}

@Override public void onResponse(Response response) throws IOException {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

Headers responseHeaders = response.headers();
for (int i = 0; i < responseHeaders.size(); i++) {
System.out.println(responseHeaders.name(i) + ": " + responseHeaders.value(i));
}

System.out.println(response.body().string());
}
});
}

异步请求的打印结果与注意事项与同步请求时相同。最大的不同点就是异步请求不需要开启子线程,enqueue方法会自动将网络请求部分放入子线程中执行。


注意事项:



  • 1,回调接口的onFailure方法onResponse执行在子线程。

  • 2,response.body().string()方法也必须放在子线程中。当执行这行代码得到结果后,再跳转到UI线程修改UI。


3.5、post请求方法


Post请求也分同步和异步两种方式,同步与异步的区别和get方法类似,所以此时只讲解post异步请求的使用方法。


private void postDataWithParame() {
OkHttpClient client = new OkHttpClient();//创建OkHttpClient对象。
FormBody.Builder formBody = new FormBody.Builder();//创建表单请求体
formBody.add("username","zhangsan");//传递键值对参数
Request request = new Request.Builder()//创建Request 对象。
.url("http://www.baidu.com")
.post(formBody.build())//传递请求体
.build();
client.newCall(request).enqueue(new Callback() {。。。});//回调方法的使用与get异步请求相同,此时略。
}



看完代码我们会发现:post请求中并没有设置请求方式为POST,回忆在get请求中也没有设置请求方式为GET,那么是怎么区分请求方式的呢?重点是Request.Builder类的post方法,在Request.Builder对象创建最初默认是get请求,所以在get请求中不需要设置请求方式,当调用post方法时把请求方式修改为POST。所以此时为POST请求。


3.6、POST请求传递参数的方法总结


3.6.1、Post方式提交String


下面是使用HTTP POST提交请求到服务. 这个例子提交了一个markdown文档到web服务, 以HTML方式渲染markdown. 因为整个请求体都在内存中, 因此避免使用此api提交大文档(大于1MB).


public static final MediaType MEDIA_TYPE_MARKDOWN
= MediaType.parse("text/x-markdown; charset=utf-8");

private final OkHttpClient client = new OkHttpClient();

public void run() throws Exception {
String postBody = ""
+ "Releases\n"
+ "--------\n"
+ "\n"
+ " * _1.0_ May 6, 2013\n"
+ " * _1.1_ June 15, 2013\n"
+ " * _1.2_ August 11, 2013\n";

Request request = new Request.Builder()
.url("https://api.github.com/markdown/raw")
.post(RequestBody.create(MEDIA_TYPE_MARKDOWN, postBody))
.build();

Response response = client.newCall(request).execute();
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

System.out.println(response.body().string());
}

3.6.2、Post方式提交


以流的方式POST提交请求体. 请求体的内容由流写入产生. 这个例子是流直接写入Okio的BufferedSink. 你的程序可能会使用OutputStream, 你可以使用BufferedSink.outputStream()来获取. OkHttp的底层对流和字节的操作都是基于Okio库, Okio库也是Square开发的另一个IO库, 填补I/O和NIO的空缺, 目的是提供简单便于使用的接口来操作IO.


public static final MediaType MEDIA_TYPE_MARKDOWN
= MediaType.parse("text/x-markdown; charset=utf-8");

private final OkHttpClient client = new OkHttpClient();

public void run() throws Exception {
RequestBody requestBody = new RequestBody() {
@Override public MediaType contentType() {
return MEDIA_TYPE_MARKDOWN;
}

@Override public void writeTo(BufferedSink sink) throws IOException {
sink.writeUtf8("Numbers\n");
sink.writeUtf8("-------\n");
for (int i = 2; i <= 997; i++) {
sink.writeUtf8(String.format(" * %s = %s\n", i, factor(i)));
}
}

private String factor(int n) {
for (int i = 2; i < n; i++) {
int x = n / i;
if (x * i == n) return factor(x) + " × " + i;
}
return Integer.toString(n);
}
};

Request request = new Request.Builder()
.url("https://api.github.com/markdown/raw")
.post(requestBody)
.build();

Response response = client.newCall(request).execute();
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

System.out.println(response.body().string());
}

3.6.3、Post方式提交文件


public static final MediaType MEDIA_TYPE_MARKDOWN
= MediaType.parse("text/x-markdown; charset=utf-8");

private final OkHttpClient client = new OkHttpClient();

public void run() throws Exception {
File file = new File("README.md");

Request request = new Request.Builder()
.url("https://api.github.com/markdown/raw")
.post(RequestBody.create(MEDIA_TYPE_MARKDOWN, file))
.build();

Response response = client.newCall(request).execute();
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

System.out.println(response.body().string());
}

3.6.4、Post方式提交表单


使用FormEncodingBuilder来构建和HTML标签相同效果的请求体. 键值对将使用一种HTML兼容形式的URL编码来进行编码.


 private final OkHttpClient client = new OkHttpClient();

public void run() throws Exception {
RequestBody formBody = new FormBody.Builder()
.add("search", "Jurassic Park")
.build();
Request request = new Request.Builder()
.url("https://en.wikipedia.org/w/index.php")
.post(formBody)
.build();

Response response = client.newCall(request).execute();
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

System.out.println(response.body().string());
}

3.7、POST其他用法


3.7.1、提取响应头


典型的HTTP头像是一个Map<String, String> : 每个字段都有一个或没有值. 但是一些头允许多个值, 像Guava的Multimap


例如:


HTTP响应里面提供的Vary响应头, 就是多值的. OkHttp的api试图让这些情况都适用.



  • 当写请求头的时候, 使用header(name, value)可以设置唯一的name、value. 如果已经有值, 旧的将被移除,然后添加新的. 使用addHeader(name, value)可以添加多值(添加, 不移除已有的).

  • 当读取响应头时, 使用header(name)返回最后出现的name、value. 通常情况这也是唯一的name、value.如果没有值, 那么header(name)将返回null. 如果想读取字段对应的所有值,使用headers(name)会返回一个list.


为了获取所有的Header, Headers类支持按index访问.


private final OkHttpClient client = new OkHttpClient();

public void run() throws Exception {
Request request = new Request.Builder()
.url("https://api.github.com/repos/square/okhttp/issues")
.header("User-Agent", "OkHttp Headers.java")
.addHeader("Accept", "application/json; q=0.5")
.addHeader("Accept", "application/vnd.github.v3+json")
.build();

Response response = client.newCall(request).execute();
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

System.out.println("Server: " + response.header("Server"));
System.out.println("Date: " + response.header("Date"));
System.out.println("Vary: " + response.headers("Vary"));
}

3.7.2、使用Gson来解析JSON响应


Gson是一个在JSON和Java对象之间转换非常方便的api库. 这里我们用Gson来解析Github API的JSON响应.


注意: ResponseBody.charStream()使用响应头Content-Type指定的字符集来解析响应体. 默认是UTF-8.


private final OkHttpClient client = new OkHttpClient();
private final Gson gson = new Gson();

public void run() throws Exception {
Request request = new Request.Builder()
.url("https://api.github.com/gists/c2a7c39532239ff261be")
.build();
Response response = client.newCall(request).execute();
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

Gist gist = gson.fromJson(response.body().charStream(), Gist.class);
for (Map.Entry<String, GistFile> entry : gist.files.entrySet()) {
System.out.println(entry.getKey());
System.out.println(entry.getValue().content);
}
}

static class Gist {
Map<String, GistFile> files;
}

static class GistFile {
String content;
}

3.7.3、响应缓存


为了缓存响应, 你需要一个你可以读写的缓存目录, 和缓存大小的限制. 这个缓存目录应该是私有的, 不信任的程序应不能读取缓存内容.


一个缓存目录同时拥有多个缓存访问是错误的. 大多数程序只需要调用一次new OkHttp(), 在第一次调用时配置好缓存, 然后其他地方只需要调用这个实例就可以了. 否则两个缓存示例互相干扰, 破坏响应缓存, 而且有可能会导致程序崩溃.


响应缓存使用HTTP头作为配置. 你可以在请求头中添加Cache-Control: max-stale=3600 , OkHttp缓存会支持. 你的服务通过响应头确定响应缓存多长时间, 例如使用Cache-Control: max-age=9600.


private final OkHttpClient client;

public CacheResponse(File cacheDirectory) throws Exception {
int cacheSize = 10 * 1024 * 1024; // 10 MiB
Cache cache = new Cache(cacheDirectory, cacheSize);

client = new OkHttpClient();
client.setCache(cache);
}

public void run() throws Exception {
Request request = new Request.Builder()
.url("http://publicobject.com/helloworld.txt")
.build();

Response response1 = client.newCall(request).execute();
if (!response1.isSuccessful()) throw new IOException("Unexpected code " + response1);

String response1Body = response1.body().string();
System.out.println("Response 1 response: " + response1);
System.out.println("Response 1 cache response: " + response1.cacheResponse());
System.out.println("Response 1 network response: " + response1.networkResponse());

Response response2 = client.newCall(request).execute();
if (!response2.isSuccessful()) throw new IOException("Unexpected code " + response2);

String response2Body = response2.body().string();
System.out.println("Response 2 response: " + response2);
System.out.println("Response 2 cache response: " + response2.cacheResponse());
System.out.println("Response 2 network response: " + response2.networkResponse());

System.out.println("Response 2 equals Response 1? " + response1Body.equals(response2Body));
}

如果需要阻值response使用缓存, 使用CacheControl.FORCE_NETWORK. 如果需要阻值response使用网络, 使用CacheControl.FORCE_CACHE.


警告
如果你使用FORCE_CACHE, 但是response要求使用网络, OkHttp将会返回一个504 Unsatisfiable Request响应.





收起阅读 »

LeakCanary原理分析

LeakCanary 是一个很好用的Android内存泄露检测工具,今天从源码角度分析下其检测内存泄露的原理,不同版本 源码 会有一定差异,这里参考的是2.7版本。1. Reference简介Java中的四种引用类型,我们先简单复习下强引用,对象有强引用时不能...
继续阅读 »

LeakCanary 是一个很好用的Android内存泄露检测工具,今天从源码角度分析下其检测内存泄露的原理,不同版本 源码 会有一定差异,这里参考的是2.7版本。

1. Reference简介

Java中的四种引用类型,我们先简单复习下

  • 强引用,对象有强引用时不能被回收
  • 软引用 SoftReference,对象只有软引用时,在内存不足时触发GC会回收该对象
  • 弱引用 WeakReference,对象只有弱引用时,下次GC就会回收该对象
  • 虚引用 PhantomReference,平常很少会用到,源码注释主要用来监听对象清理前的动作,比Java finalization更灵活,PhantomReference需要与 ReferenceQueue 一起配合使用。

Phantom references are most often used for scheduling pre-mortem cleanup actions in a more flexible way than is possible with the Java finalization mechanism.

ReferenceQueue

上面提到PhantomReferenceReferenceQueue配合监听对象被回收,实际上WeakReferenceSoftReference同样可以与ReferenceQueue关联使用,只要构造方法传入ReferenceQueue参数即可。在引用所指的对象被回收后,引用本身将会被加入到ReferenceQueue之中。

2. LeakCanary使用简介

  • 在app的build.gradle中加入依赖
dependencies {
// debugImplementation because LeakCanary should only run in debug builds.
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'
}
  • LeakCanary会自动监控Activity、Fragment、Fragment View、RootView、Service的泄露
  • 如果需要监控其它对象的泄露,可以手动添加如下代码
AppWatcher.objectWatcher.watch(myView, "View was detached")

3. LeakCanary源码分析

初始化

LeakCanary新版本使用ContentProvider自动初始化,不需要再手动调用install方法


    android:name="leakcanary.internal.AppWatcherInstaller$MainProcess"
android:authorities="${applicationId}.leakcanary-installer"
android:enabled="@bool/leak_canary_watcher_auto_install"
android:exported="false" />

如果想禁用自动初始化,在app res中加入

false

接下来我们从源码分析下LeakCanary初始化的流程:

internal sealed class AppWatcherInstaller : ContentProvider() {

/**
* [MainProcess] automatically sets up the LeakCanary code that runs in the main app process.
*/
internal class MainProcess : AppWatcherInstaller()

/**
* When using the `leakcanary-android-process` artifact instead of `leakcanary-android`,
* [LeakCanaryProcess] automatically sets up the LeakCanary code
*/
internal class LeakCanaryProcess : AppWatcherInstaller()

override fun onCreate(): Boolean {
val application = context!!.applicationContext as Application
AppWatcher.manualInstall(application)
return true
}

...
}

AppWatcherInstaller继承ContentProvider,onCreate会调用AppWatcher的manualInstall方法,完成自动初始化。

object AppWatcher {

/**
* The [ObjectWatcher] used by AppWatcher to detect retained objects.
* Only set when [isInstalled] is true.
*/
val objectWatcher = ObjectWatcher(
clock = { SystemClock.uptimeMillis() },
checkRetainedExecutor = {
check(isInstalled) {
"AppWatcher not installed"
}
mainHandler.postDelayed(it, retainedDelayMillis)
},
isEnabled = { true }
)

@JvmOverloads
fun manualInstall(
application: Application,
retainedDelayMillis: Long = TimeUnit.SECONDS.toMillis(5),
watchersToInstall: List = appDefaultWatchers(application)
) {
checkMainThread()
if (isInstalled) {
throw IllegalStateException(
"AppWatcher already installed, see exception cause for prior install call", installCause
)
}
check(retainedDelayMillis >= 0) {
"retainedDelayMillis $retainedDelayMillis must be at least 0 ms"
}
installCause = RuntimeException("manualInstall() first called here")
this.retainedDelayMillis = retainedDelayMillis
if (application.isDebuggableBuild) {
LogcatSharkLog.install()
}
// Requires AppWatcher.objectWatcher to be set
LeakCanaryDelegate.loadLeakCanary(application)

watchersToInstall.forEach {
it.install()
}
}

fun appDefaultWatchers(
application: Application,
reachabilityWatcher: ReachabilityWatcher = objectWatcher
): List {
return listOf(
ActivityWatcher(application, reachabilityWatcher),
FragmentAndViewModelWatcher(application, reachabilityWatcher),
RootViewWatcher(reachabilityWatcher),
ServiceWatcher(reachabilityWatcher)
)
}
...
}

manualInstall方法有3个参数:

  • application:application对象
  • retainedDelayMillis:默认值5s,表示5s后检测对象是否被回收
  • watchersToInstall:安装的监控器,每个监控器抽象成InstallableWatcher,默认值在appDefaultWatchers方法中定义,包括ActivityWatcher、FragmentAndViewModelWatcher、RootViewWatcher、ServiceWatcher,后面单独分析。

创建InstallableWatcher时需要传入ReachabilityWatcher,实现类是ObjectWatcher,这是监控对象可达性的核心。AppWatcher创建了ObjectWatcher对象,并在checkRetainedExecutor里面加入了延迟5s的逻辑,下面配合ObjectWatcher源码一起分析。

ObjectWatcher

上面说到手动监控其它对象是调用ObjectWatcher的watch方法,这里是真正的核心逻辑,我们看下其部分代码

class ObjectWatcher constructor(
private val clock: Clock,
private val checkRetainedExecutor: Executor,
/**
* Calls to [watch] will be ignored when [isEnabled] returns false
*/
private val isEnabled: () -> Boolean = { true }
) : ReachabilityWatcher {

fun watch(
watchedObject: Any,
description: String
) {
expectWeaklyReachable(watchedObject, description)
}

@Synchronized override fun expectWeaklyReachable(
watchedObject: Any,
description: String
) {
if (!isEnabled()) {
return
}
removeWeaklyReachableObjects()
val key = UUID.randomUUID()
.toString()
val watchUptimeMillis = clock.uptimeMillis()
val reference =
KeyedWeakReference(watchedObject, key, description, watchUptimeMillis, queue)
SharkLog.d {
"Watching " +
(if (watchedObject is Class<*>) watchedObject.toString() else "instance of ${watchedObject.javaClass.name}") +
(if (description.isNotEmpty()) " ($description)" else "") +
" with key $key"
}

watchedObjects[key] = reference
checkRetainedExecutor.execute {
moveToRetained(key)
}
}

@Synchronized private fun moveToRetained(key: String) {
removeWeaklyReachableObjects()
val retainedRef = watchedObjects[key]
if (retainedRef != null) {
retainedRef.retainedUptimeMillis = clock.uptimeMillis()
onObjectRetainedListeners.forEach { it.onObjectRetained() }
}
}

private fun removeWeaklyReachableObjects() {
// WeakReferences are enqueued as soon as the object to which they point to becomes weakly
// reachable. This is before finalization or garbage collection has actually happened.
var ref: KeyedWeakReference?
do {
ref = queue.poll() as KeyedWeakReference?
if (ref != null) {
watchedObjects.remove(ref.key)
}
} while (ref != null)
}

}

调用watch方法会走到expectWeaklyReachable,里面大致做了4件事情:

  • 从watchedObjects中移除已经正常释放的引用 removeWeaklyReachableObjects()。这里用到了前面讲的ReferenceQueue,对象被回收会加入到queue中,将queu中存在的从watchedObjects中移除。
  • 创建KeyedWeakReference引用,KeyedWeakReference继承WeakReference,创建时传入了ReferenceQueue,监听对象的回收。
  • 将引用加入到watchedObjects中 watchedObjects[key] = reference
  • 延迟5s之后执行moveToRetained(key),确认对象是否回收(延迟逻辑在AppWatcher传入的checkRetainedExecutor中实现)。moveToRetained中首先执行removeWeaklyReachableObjects,之后再判断watchedObjects中是否还存在此key,若存在说明对象未被回收,发生内存泄露。

到这里我们基本明白了LeakCanary监控对象是否回收的逻辑,接下来我们再看看他是如何自动监控Activity、Fragment等组件的。前面讲到AppWatcher初始化时,会自动创建ActivityWatcher、FragmentAndViewModelWatcher、RootViewWatcher、ServiceWatcher,我们阅读下他们的源码。

ActivityWatcher

class ActivityWatcher(
private val application: Application,
private val reachabilityWatcher: ReachabilityWatcher
) : InstallableWatcher {

private val lifecycleCallbacks =
object : Application.ActivityLifecycleCallbacks by noOpDelegate() {
override fun onActivityDestroyed(activity: Activity) {
reachabilityWatcher.expectWeaklyReachable(
activity, "${activity::class.java.name} received Activity#onDestroy() callback"
)
}
}

override fun install() {
application.registerActivityLifecycleCallbacks(lifecycleCallbacks)
}

override fun uninstall() {
application.unregisterActivityLifecycleCallbacks(lifecycleCallbacks)
}
}

ActivityWatcher的代码非常简单,注册Activity生命周期回调,在onActivityDestroyed中调用ObjectWatcher的expectWeaklyReachable,监控activity对象5s之内是否被释放。

FragmentAndViewModelWatcher

FragmentAndViewModelWatcher监控Fragment和Fragment View的泄露,原理与Activity类似,在onFragmentDestroyed和onFragmentViewDestroyed中调用ObjectWatcher的expectWeaklyReachable方法。只不过监听Fragment的onDestroy相对复杂点,原理是先监听Activity生命周期,然后在Activity onCreate时通过fragmentManager.registerFragmentLifecycleCallbacks注册Fragment生命周期回调。而且同时兼容了android.app.Fragment、androidx.fragment.app.Fragment、android.support.v4.app.Fragment。

class FragmentAndViewModelWatcher(
private val application: Application,
private val reachabilityWatcher: ReachabilityWatcher
) : InstallableWatcher {

private val fragmentDestroyWatchers: List<(Activity) -> Unit> = run {
val fragmentDestroyWatchers = mutableListOf<(Activity) -> Unit>()

if (SDK_INT >= O) {
fragmentDestroyWatchers.add(
AndroidOFragmentDestroyWatcher(reachabilityWatcher)
)
}

getWatcherIfAvailable(
ANDROIDX_FRAGMENT_CLASS_NAME,
ANDROIDX_FRAGMENT_DESTROY_WATCHER_CLASS_NAME,
reachabilityWatcher
)?.let {
fragmentDestroyWatchers.add(it)
}

getWatcherIfAvailable(
ANDROID_SUPPORT_FRAGMENT_CLASS_NAME,
ANDROID_SUPPORT_FRAGMENT_DESTROY_WATCHER_CLASS_NAME,
reachabilityWatcher
)?.let {
fragmentDestroyWatchers.add(it)
}
fragmentDestroyWatchers
}
}

@SuppressLint("NewApi")
internal class AndroidOFragmentDestroyWatcher(
private val reachabilityWatcher: ReachabilityWatcher
) : (Activity) -> Unit {
private val fragmentLifecycleCallbacks = object : FragmentManager.FragmentLifecycleCallbacks() {

override fun onFragmentViewDestroyed(
fm: FragmentManager,
fragment: Fragment
) {
val view = fragment.view
if (view != null) {
reachabilityWatcher.expectWeaklyReachable(
view, "${fragment::class.java.name} received Fragment#onDestroyView() callback " +
"(references to its views should be cleared to prevent leaks)"
)
}
}

override fun onFragmentDestroyed(
fm: FragmentManager,
fragment: Fragment
) {
reachabilityWatcher.expectWeaklyReachable(
fragment, "${fragment::class.java.name} received Fragment#onDestroy() callback"
)
}
}

override fun invoke(activity: Activity) {
val fragmentManager = activity.fragmentManager
fragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, true)
}
}

RootViewWatcher

RootViewWatcher监控RootView的泄露,在rootView onDetachedFromWindow回调时,调用ObjectWatcher的expectWeaklyReachable方法。rootView的onDetachedFromWindow回调监听是通过Square开源的Curtains库实现。

class RootViewWatcher(
private val reachabilityWatcher: ReachabilityWatcher
) : InstallableWatcher {

private val listener = OnRootViewAddedListener { rootView ->
val trackDetached = when(rootView.windowType) {
PHONE_WINDOW -> {
when (rootView.phoneWindow?.callback?.wrappedCallback) {
// Activities are already tracked by ActivityWatcher
is Activity -> false
is Dialog -> rootView.resources.getBoolean(R.bool.leak_canary_watcher_watch_dismissed_dialogs)
// Probably a DreamService
else -> true
}
}
// Android widgets keep detached popup window instances around.
POPUP_WINDOW -> false
TOOLTIP, TOAST, UNKNOWN -> true
}
if (trackDetached) {
rootView.addOnAttachStateChangeListener(object : OnAttachStateChangeListener {

val watchDetachedView = Runnable {
reachabilityWatcher.expectWeaklyReachable(
rootView, "${rootView::class.java.name} received View#onDetachedFromWindow() callback"
)
}

override fun onViewAttachedToWindow(v: View) {
mainHandler.removeCallbacks(watchDetachedView)
}

override fun onViewDetachedFromWindow(v: View) {
mainHandler.post(watchDetachedView)
}
})
}
}

override fun install() {
Curtains.onRootViewsChangedListeners += listener
}

override fun uninstall() {
Curtains.onRootViewsChangedListeners -= listener
}
}

ServiceWatcher

ServiceWatcher监控Service的泄露,原理也是在Service的onDestroy时调用ObjectWatcher的expectWeaklyReachable方法。Service的onDestroy是通过反射和动态代理ActivityManager和ActivityThread,代码比较巧妙,可以仔细消化下。

@SuppressLint("PrivateApi")
class ServiceWatcher(private val reachabilityWatcher: ReachabilityWatcher) : InstallableWatcher {

private val servicesToBeDestroyed = WeakHashMap>()

private val activityThreadClass by lazy { Class.forName("android.app.ActivityThread") }

private val activityThreadInstance by lazy {
activityThreadClass.getDeclaredMethod("currentActivityThread").invoke(null)!!
}

private val activityThreadServices by lazy {
val mServicesField =
activityThreadClass.getDeclaredField("mServices").apply { isAccessible = true }

@Suppress("UNCHECKED_CAST")
mServicesField[activityThreadInstance] as Map
}

private var uninstallActivityThreadHandlerCallback: (() -> Unit)? = null
private var uninstallActivityManager: (() -> Unit)? = null

override fun install() {
checkMainThread()
check(uninstallActivityThreadHandlerCallback == null) {
"ServiceWatcher already installed"
}
check(uninstallActivityManager == null) {
"ServiceWatcher already installed"
}
try {
swapActivityThreadHandlerCallback { mCallback ->
uninstallActivityThreadHandlerCallback = {
swapActivityThreadHandlerCallback {
mCallback
}
}
Handler.Callback { msg ->
if (msg.what == STOP_SERVICE) {
val key = msg.obj as IBinder
activityThreadServices[key]?.let {
onServicePreDestroy(key, it)
}
}
mCallback?.handleMessage(msg) ?: false
}
}
swapActivityManager { activityManagerInterface, activityManagerInstance ->
uninstallActivityManager = {
swapActivityManager { _, _ ->
activityManagerInstance
}
}
Proxy.newProxyInstance(
activityManagerInterface.classLoader, arrayOf(activityManagerInterface)
) { _, method, args ->
if (METHOD_SERVICE_DONE_EXECUTING == method.name) {
val token = args!![0] as IBinder
if (servicesToBeDestroyed.containsKey(token)) {
onServiceDestroyed(token)
}
}
try {
if (args == null) {
method.invoke(activityManagerInstance)
} else {
method.invoke(activityManagerInstance, *args)
}
} catch (invocationException: InvocationTargetException) {
throw invocationException.targetException
}
}
}
} catch (ignored: Throwable) {
SharkLog.d(ignored) { "Could not watch destroyed services" }
}
}

override fun uninstall() {
checkMainThread()
uninstallActivityManager?.invoke()
uninstallActivityThreadHandlerCallback?.invoke()
uninstallActivityManager = null
uninstallActivityThreadHandlerCallback = null
}

private fun onServicePreDestroy(
token: IBinder,
service: Service
) {
servicesToBeDestroyed[token] = WeakReference(service)
}

private fun onServiceDestroyed(token: IBinder) {
servicesToBeDestroyed.remove(token)?.also { serviceWeakReference ->
serviceWeakReference.get()?.let { service ->
reachabilityWatcher.expectWeaklyReachable(
service, "${service::class.java.name} received Service#onDestroy() callback"
)
}
}
}

private fun swapActivityThreadHandlerCallback(swap: (Handler.Callback?) -> Handler.Callback?) {
val mHField =
activityThreadClass.getDeclaredField("mH").apply { isAccessible = true }
val mH = mHField[activityThreadInstance] as Handler

val mCallbackField =
Handler::class.java.getDeclaredField("mCallback").apply { isAccessible = true }
val mCallback = mCallbackField[mH] as Handler.Callback?
mCallbackField[mH] = swap(mCallback)
}

@SuppressLint("PrivateApi")
private fun swapActivityManager(swap: (Class<*>, Any) -> Any) {
val singletonClass = Class.forName("android.util.Singleton")
val mInstanceField =
singletonClass.getDeclaredField("mInstance").apply { isAccessible = true }

val singletonGetMethod = singletonClass.getDeclaredMethod("get")

val (className, fieldName) = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
"android.app.ActivityManager" to "IActivityManagerSingleton"
} else {
"android.app.ActivityManagerNative" to "gDefault"
}

val activityManagerClass = Class.forName(className)
val activityManagerSingletonField =
activityManagerClass.getDeclaredField(fieldName).apply { isAccessible = true }
val activityManagerSingletonInstance = activityManagerSingletonField[activityManagerClass]

// Calling get() instead of reading from the field directly to ensure the singleton is
// created.
val activityManagerInstance = singletonGetMethod.invoke(activityManagerSingletonInstance)

val iActivityManagerInterface = Class.forName("android.app.IActivityManager")
mInstanceField[activityManagerSingletonInstance] =
swap(iActivityManagerInterface, activityManagerInstance!!)
}

companion object {
private const val STOP_SERVICE = 116

private const val METHOD_SERVICE_DONE_EXECUTING = "serviceDoneExecuting"
}
}

总结

今天从源码角度分析了LeakCanary监控内存泄露的核心原理。除此之外,LeakCanary还可以导出、分析、分类堆栈,如果后面有时间咱们再单独讲吧。

收起阅读 »

Android即时通讯系列文章(1)多进程:为什么要把消息服务拆分到一个独立的进程?

这是即时通讯系列文章的第一篇,正式开始对IM开发技术的讲解之前,我们先来谈谈客户端在完整聊天系统中所扮演的角色,为此,我们必须先明确客户端的职责。现今主流的IM应用几乎都是采用服务器中转的方式来进行消息传输的,为的是更好地支持离线、群组等业务。在这种模式下,所...
继续阅读 »

这是即时通讯系列文章的第一篇,正式开始对IM开发技术的讲解之前,我们先来谈谈客户端在完整聊天系统中所扮演的角色,为此,我们必须先明确客户端的职责。

现今主流的IM应用几乎都是采用服务器中转的方式来进行消息传输的,为的是更好地支持离线、群组等业务。在这种模式下,所有客户端都需连接到服务端,服务端将不同客户端发给自己的消息根据消息里携带的用户标识进行转发或广播。

因此,作为消息收发的终端设备,客户端的重要职责之一就是保持与服务端的连接,该连接的稳定性直接决定消息收发的实时性和可靠性。而在上篇文章我们讲过,移动设备是资源受限的,这对连接的稳定性提出了极大的挑战,具体可体现在以下两个方面:

  • 为了维持多任务环境的正常运行,Android为每个应用的堆大小设置了硬性上限,不同设备的确切堆大小取决于设备的总体可用RAM大小,如果应用在达到堆容量上限后尝试分配更多内容,则可能引发OOM。
  • 当用户切换到其他应用时,系统会将原有应用的进程保留在缓存中,稍后如果用户返回该应用,系统就会重复使用该进程,以便加快应用切换速度。但当系统资源(如内存)不足时,系统会考虑终止占用最多内存的、优先级较低的进程以释放RAM。

虽然ART和Dalvik虚拟机会例行执行垃圾回收任务,但如果应用存在内存泄漏问题,并且只有一个主进程,势必会随着应用使用时间的延长而逐步增大内存使用量,从而增加引发OOM的概率和缓存进程被系统终止的风险。

因此,为了保证连接的稳定性,可考虑将负责连接保持工作的消息服务放入一个独立的进程中,分离之后即使主进程退出、崩溃或者出现内存消耗过高等情况,该服务仍可正常运行,甚至可以在适当的时机通过广播等方式重新唤起主进程。

但是,给应用划分进程,往往就意味着需要编写额外的进程通讯代码,特别是对于消息服务这种需要高度交互的场景。而由于各个进程都运行在相对独立的内存空间,因而是无法直接通讯的。为此,Android提供了AIDL(Android Interface Definition Language,Android接口定义语言)用于实现进程间通信,其本质就是实现对象的序列化、传输、接收和反序列化,得到可操作的对象后再进行常规的方法调用。

接下来,就让我们来一步步实现跨进程的通讯吧。

Step1 创建服务

由于连接保持的工作是需要在后台执行长时间执行的操作,通常不提供操作界面,符合这个特性的组件就是Service了,因此我们选用Service作为与远程进程进行进程间通信(IPC)的组件。创建Service的子类时,必须实现onBind回调方法,此处我们暂时返回空实现。

class MessageAccessService : Service() {
override fun onBind(intent: Intent?): IBinder? {
return null
}
}

另外使用Service还有一个好处就是,我们可以在适当的时机将其升级为前台服务,前台服务是用户主动意识到的一种服务,进程优先级较高,因此在内存不足时,系统也不会考虑将其终止。

使用前台服务唯一的缺点就是必须在抽屉式通知栏提供一条不可移除的通知,对于用户体验极不友好,但是我们可以通过定制通知样式进行协调,后续的文章中会讲到。

step2 指定进程

默认情况下,同一应用的所有组件均在相同的进程中运行。如需控制某个组件所属的进程,可通过在清单文件中设置android:process属性实现:

<manifest ...>
<application ...>
<service
android:name=".service.MessageAccessService"
android:exported="true"
android:process=":remote" />
</application>
</manifest>

另外,为使其他进程的组件能调用服务或与之交互,还需设置android:exported属性为true。

step3 创建.aidl 文件

让我们重新把目光放回onBind回调方法,该方法要求返回IBinder对象,客户端可使用该对象定义好的接口与服务进行通信。IBinder是远程对象的基础接口,该接口描述了与远程对象交互的抽象协议,但不建议直接实现此接口,而应从Binder扩展。通常做法是是使用.aidl文件来描述所需的接口,使其生成适当的Binder子类。

那么,这个最关键的.aidl文件该如何创建,又该定义哪些接口呢?

创建.aidl文件很简单,Android Studio本身就提供了创建AIDL文件方法:项目右键 -> New -> AIDL -> AIDL File

前面讲过,客户端是消息收发的终端设备,而接入服务则是为客户端提供了消息收发的出入口。客户端发出的消息经由接入服务发送到服务端,同时客户端会委托接入服务帮忙收取消息,当服务端有消息推送过来时通知自己。

如此一来便很清晰了,我们要定义的接口总共有三个,分别为:

  • 发送消息
  • 注册消息接收器
  • 反注册消息接收器

MessageCarrier.aidl

package com.xxx.imsdk.comp.remote;
import com.xxx.imsdk.comp.remote.bean.Envelope;
import com.xxx.imsdk.comp.remote.listener.MessageReceiver;

interface MessageCarrier {
void sendMessage(in Envelope envelope);
void registerReceiveListener(MessageReceiver messageReceiver);
void unregisterReceiveListener(MessageReceiver messageReceiver);
}

这里解释一下上述接口中携带的参数的含义:

Envelope ->

解释这个参数之前,得先介绍Envelope.java这个类,该类是多进程通讯中作为数据传输的实体类。AIDL支持的数据类型除了基本数据类型、String和CharSequence,还有就是实现了Parcelable接口的对象,以及其中元素为以上几种的List和Map。

Envelope.java

**
* 用于多进程通讯的信封类
* <p>
* 在AIDL中传递的对象,需要在类文件相同路径下,创建同名、但是后缀为.aidl的文件,并在文件中使用parcelable关键字声明这个类;
* 但实际业务中需要传递的对象所属的类往往分散在不同的模块,所以通过构建一个包装类来包含真正需要被传递的对象(必须也实现Parcelable接口)
*/
@Parcelize
data class Envelope(val messageVo: MessageVo? = null,
val noticeVo: NoticeVo? = null) : Parcelable {
}

另外,在AIDL中传递的对象,需要在上述类文件的相同包路径下,创建同名、但是后缀为.aidl的文件,并在文件中使用parcelable关键字声明这个类,Envelope.aidl就是对应Envelope.java而创建的;

Envelope.aidl

package com.xxx.imsdk.comp.remote.bean;

parcelable Envelope;

两个文件对应的路径比较如下:

clipboard.png

那为什么是Envelope类而不直接是MessageVO类(消息视图对象)呢?这是由于考虑到实际业务中需要传递的对象所属的类往往分散在不同的模块(MessageVO从属于另外一个模块,需要被其他模块引用),所以通过构建一个包装类来包含真正需要被传递的对象(该对象必须也实现Parcelable接口),这也是该类命名为Envelope(信封)的含义。

MessageReceiver ->

跨进程的消息收取回调接口,用于将消息接入服务收取到的服务端消息传递到客户端。但这里使用的回调接口有点不一样,在AIDL中传递的接口,不能是普通的接口,只能是AIDL接口,因此我们还需要新建多一个.aidl文件:

MessageReceiver.aidl

package com.xxx.imsdk.comp.remote.listener;
import com.xxx.imsdk.comp.remote.bean.Envelope;

interface MessageReceiver {
void onMessageReceived(in Envelope envelope);
}

包目录结构如下图:

FE55B9D0FFFC48829667C01C212B2668.jpg

step4 返回IBinder接口

构建应用时,Android SDK会生成基于.aidl 文件的IBinder接口文件,并将其保存到项目的gen/目录中。生成文件的名称与.aidl 文件的名称保持一致,区别在于其使用.java 扩展名(例如,MessageCarrier.aidl 生成的文件名是 MessageCarrier .java)。此接口拥有一个名为Stub的内部抽象类,用于扩展 Binder 类并实现 AIDL 接口中的方法。

/** 根据MessageCarrier.aidl文件自动生成的Binder对象,需要返回给客户端 */
private val messageCarrier: IBinder = object : MessageCarrier.Stub() {

override fun sendMessage(envelope: Envelope?) {

}

override fun registerReceiveListener(messageReceiver: MessageReceiver?) {
remoteCallbackList.register(messageReceiver)
}

override fun unregisterReceiveListener(messageReceiver: MessageReceiver?) {
remoteCallbackList.unregister(messageReceiver)
}

}

override fun onBind(intent: Intent?): IBinder? {
return messageCarrier
}
step5 绑定服务

组件(例如 Activity)可以通过调用bindService方法绑定到服务,该方法必须提供ServiceConnection 的实现以监控与服务的连接。当组件与服务之间的连接建立成功后, ServiceConnection上的 onServiceConnected()方法将被回调,该方法包含上一步返回的IBinder对象,随后便可使用该对象与绑定的服务进行通信。

/**
* ## 绑定消息接入服务
* 同时调用bindService和startService, 可以使unbind后Service仍保持运行
* @param context 上下文
*/
@Synchronized
fun setupService(context: Context? = null) {
if (!::appContext.isInitialized) {
appContext = context!!.applicationContext
}

val intent = Intent(appContext, MessageAccessService::class.java)

// 记录绑定服务的结果,避免解绑服务时出错
if (!isBound) {
isBound = appContext.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
}

startService(intent)
}

/** 监听与服务连接状态的接口 */
private val serviceConnection = object : ServiceConnection {

override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
// 取得MessageCarrier.aidl对应的操作接口
messageCarrier = MessageCarrier.Stub.asInterface(service)
...
}

override fun onServiceDisconnected(name: ComponentName?) {
}

}

可以同时将多个组件绑定到同一个服务,但当最后一个组件取消与服务的绑定时,系统会销毁该服务。为了使服务能够无限期运行,可同时调用startService()和bindService(),创建同时具有已启动和已绑定两种状态的服务。这样,即使所有组件均解绑服务,系统也不会销毁该服务,直至调用 stopSelf() 或 stopService() 才会显式停止该服务。

/**
* 启动消息接入服务
* @param intent 意图
* @param action 操作
*/
private fun startService(
intent: Intent = Intent(appContext, MessageAccessService::class.java),
action: String? = null
) {
// Android8.0不再允许后台service直接通过startService方式去启动,将引发IllegalStateException
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
&& !ProcessUtil.isForeground(appContext)
) {
if (!TextUtils.isEmpty(action)) intent.action = action
intent.putExtra(KeepForegroundService.EXTRA_ABANDON_FOREGROUND, false)
appContext.startForegroundService(intent)
} else {
appContext.startService(intent)
}
}

/**
* 停止消息接入服务
*/
fun stopService() {
// 立即清除缓存的WebSocket服务器地址,防止登录时再次使用旧的WebSocket服务器地址(带的会话已失效),导致收到用户下线的通知
GlobalScope.launch {
DataStoreUtil.writeString(appContext, RemoteDataStoreKey.WEB_SOCKET_SERVER_URL, "")
}

unbindService()

appContext.stopService(Intent(appContext, MessageAccessService::class.java))
}

/**
* 解绑消息接入服务
*/
@Synchronized
fun unbindService() {
if (!isBound) return // 必须判断服务是否已解除绑定,否则会报java.lang.IllegalArgumentException: Service not registered

// 解除消息监听接口
if (messageCarrier?.asBinder()?.isBinderAlive == true) {
messageCarrier?.unregisterReceiveListener(messageReceiver)
messageCarrier = null
}

appContext.unbindService(serviceConnection)

isBound = false
}

总结

通过以上代码的实践,最终我们得以将应用拆分为主进程和远程进程。主进程主要负责用户交互、界面展示,而远程进程则主要负责消息收发、连接保持等。由于远程进程仅保持了最小限度的业务逻辑处理,内存增长相对稳定,因此会大大降低系统内存紧张时远端进程被终止的概率,即使主进程因为意外情况退出了,远程进程仍可保持运行,从而保证连接的稳定性。

收起阅读 »

Jetpack太香了,系统App也想用,怎么办?

第三方App使用Jetpack等开源框架非常流行,在Gradle文件简单指定即可。然而ROM内置的系统App在源码环境下进行开发,与第三方App脱节严重,采用开源框架的情况并不常见。但如果系统App也集成了Jetpack或第三方框架,开发效率则会大大提高。前言...
继续阅读 »

第三方App使用Jetpack等开源框架非常流行,在Gradle文件简单指定即可。然而ROM内置的系统App在源码环境下进行开发,与第三方App脱节严重,采用开源框架的情况并不常见。但如果系统App也集成了Jetpack或第三方框架,开发效率则会大大提高。

前言

系统App开发者,很少采用Jetpack 以及第三方框架的原因主要有几点:

  1. 导入麻烦:有的框架过于庞大,可能依赖的库比较多,编译文件的构建比较繁琐,没有gradle那么智能

  2. 功能单一:系统App注重功能性,业务逻辑较少,依赖庞大库文件的场景不多

  3. license风险:引用第三方框架的话,需要特别声明license ,会尽量避免采用

但对于功能复杂,架构庞大的系统App而言,集成第三方框架显得尤为必要。比如Android系统里最核心的App SystemUI,就采用了知名的DI框架Dagger2 。Dagger2的引入使得功能庞杂的SystemUI管理各个依赖模块变得游刃有余。

SystemUI将Dagger2集成的方式给了我启发,探索和总结了Android 源码中如何配置Jetpack 以及第三方库,希望能够帮到大家。

源码编译说明

与Gradle不同,源码环境里的编译构建都是配置在.mk或者.bp文件里的,配置起来较为繁琐。

.bp文件::Android.bp是用来替换Android.mk的配置文件,它使用Blueprint框架来解析。Blueprint是生成、解析Android.bp的工具,是Soong的一部分。Soong则是专为Android编译而设计的工具,Blueprint只是解析文件的形式,而Soong则解释内容的含义,最终转换成Ninja文件。下文bp 就是指.bp的文件

**注意:**以下基于Android 11上进行的演示,Android 10及之前部分Jetpack框架没有集成进源码,需留意

gradle切换到bp

gradle和bp的对比

看一个使用aar和注解库的例子。

看一个AndroidStudio(以下简称AS)下build.gradle 文件里包的导入代码:

dependencies {
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.2.1'
implementation 'androidx.constraintlayout:constraintlayout:2.0.1'
//room
def room_version = "2.3.0"
implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"
}

ROM环境里的编译依赖.bp 配置如下:

android_app {
......
static_libs: [
"androidx.appcompat_appcompat",
"com.google.android.material_material",
"androidx-constraintlayout_constraintlayout",
"androidx.room_room-runtime",
],
plugins: ["androidx.room_room-compiler-plugin"],
......
}

导入关键字的差异

依赖文件里的导入关键字:

在AS和AOSP里面导入包的关键字有些差异,又分为两种情况。

build.gradle.bp
代码库implementationstatic_libs
注解使用的库annotationProcessorplugins

引入库文件(libs):比较常见。引入的方式有多种。下文会讲具体的几种方式。

引入注解库:比较流行,源码中使用比较繁琐,下文会重点讲解。

库文件的导入规则

眼尖的同学已经看出规律了

如:implementation 'androidx.appcompat:appcompat:1.2.0'

bp 文件中:androidx.appcompat_appcompat,将“:” 改为 “”即可,不需要加版本号。其实就是group 与 name 中间用“”连接,基本上符合上述规则,当然也有特殊

注解库的导入规则

如今框架流行注解编程。

gradle 配置:annotationProcessor "androidx.room:room-compiler:$room_version"

bp 中就需要使用到plugins,对应配置plugins: ["androidx.room_room-compiler-plugin"]

根据jar 包的规则,那plugin 命名应该是“:” 改为 ”_" version+"-plugin" 。

SystemUI 使用Dagger2配置 plugins: ["dagger2-compiler-2.19"],所以命名规则并不是上文猜测的那样。

那如何确定Jetpack框架的名称呢?

确定Jetpack框架的名称

源码编译,所有的内容和都在源码中,都需要在源码环境中寻找。

以Room 为例

在prebuilts/sdk/current/androidx/Android.bp 配置了引入jar包 中有如下配置

android_library {
name: "androidx.room_room-runtime",//名称
......
manifest: "manifests/androidx.room_room-runtime/AndroidManifest.xml",//配置manifast
static_libs: [//两个room库文件,三个依赖的库文件
"androidx.room_room-runtime-nodeps",
"androidx.room_room-common",
"androidx.sqlite_sqlite-framework",
"androidx.sqlite_sqlite",
"androidx.arch.core_core-runtime",
],
}

插件配置在prebuilts/sdk/current/androidx/JavaPlugins.bp

java_plugin {
name: "androidx.room_room-compiler-plugin",//名称
static_libs: [//1个room库文件,1个依赖的库文件
"androidx.room_room-compiler",
"kotlin-reflect"
],
processor_class: "androidx.room.RoomProcessor",//需要指定处理的类
}

注意:AS 开发 并不需要配置 “processor_class”,我反编译了room-compiler,找到了RoomProcessor.java.(AS 为什么不需要指定,我这里我就不研究了)

看下图,META-INF/services/javax.annotation.processing.Processor 文件中配置了RoomProcessor.java(就按照这个文件配置就可以了)

2rVokj.png

如何确定源码中哪些jetpack 库可以使用呢?

在Android.bp 中搜索,或者看androidx目录下包含了什么

prebuilts/sdk/current/androidx/m2repository/androidx$ ls

导入第三方的开源框架

以上讲的是引入Jetpack相关jar包,其他常见的是否包含呢?如Glide,它是不属于androidx 的

第三方库,Android 源码中整理就不算好了,使用比较乱。下面我梳理下

导入下载的jar包

大家最常用的,把 jar 包 放到 libs,就可以了(当然,比较简单,与其他库关联较少可以采用此种方式)

java_import {
name: "disklrucache-jar",
jars: ["disklrucache-4.12.0.jar"],
sdk_version: "current",
}
android_library_import {
name: "glide-4.12.0",
aars: ["glide-4.12.0.aar"],
sdk_version: "current",
}
android_library_import {
name: "gifdecoder-4.12.0",
aars: ["gifdecoder-4.12.0.aar",],
sdk_version: "current",
}
android_library_import {
name: "exifinterface-1.2.0",
aars: ["exifinterface-1.2.0.aar",],
sdk_version: "current",
}
android_app {
......
static_libs: [
"disklrucache-jar",
"glide-4.12.0",
"gifdecoder-4.12.0",
"exifinterface-1.2.0"
],
}

导入AOSP内置的jar包

常用第三方放在了prebuilts/tools/common/m2/repository/下面包含了很多库文件,如Glide,Okhttp,但比较尴尬的是,.bp文件并没有写好。应用需要自己编写,编写方式可以参考上文。

以后google应该会把 external 下 的整合到这个里面,可以关注下prebuilts/tools/common/m2/repository 中Android.bp文件的变化。

如:prebuilts/maven_repo/bumptech/Android.bp

java_import {
name: "glide-prebuilt",
jars: [
"com/github/bumptech/glide/glide/4.8.0/glide-4.8.0.jar",
"com/github/bumptech/glide/disklrucache/4.8.0/disklrucache-4.8.0.jar",
"com/github/bumptech/glide/gifdecoder/4.8.0/gifdecoder-4.8.0.jar",
],
jetifier: true,
notice: "LICENSE",
}

Android.bp 直接用"glide"了

static_libs: [
"glide-prebuilt"
],

导入jar包源码

external 下面 很多第三方库的源码,如Glide的源码,目录为external/glide/

android_library {
name: "glide",
srcs: [
"library/src/**/*.java",
"third_party/disklrucache/src/**/*.java",
"third_party/gif_decoder/src/**/*.java",
"third_party/gif_encoder/src/**/*.java",
],
manifest: "library/src/main/AndroidManifest.xml",
libs: [
"android-support-core-ui",
"android-support-compat",
"volley",
],
static_libs: [
"android-support-fragment",
],
sdk_version: "current",
}

App 的Android.bp 直接用"glide"了

static_libs: [
"glide"
],

以上三种方式都是引入 Android 中源码存在的。不存在怎么办,Android源码 不像 AS,连上网,配置下版本号就可以下载。

内置新的Jetpack框架

引入第三方库文件方式,方式一:aar包导入。就可以。但这里不讨论,找些复杂的,包含annotationProcessor(bp 中的plugin) 。Hilt 是 Google 相对较新的框架。

Hilt基于Dagger2开发,又针对Android进行了专属的DI优化。

所以在导入Dagger2和它的依赖文件之外还需要导入Hilt专属的一堆库和依赖文件。

1. 获取框架的库文件

一般来说AS里导入完毕的目录下即可获取到对应的库文件,路径一般在 :C:\Users\xxx\.gradle\caches\modules-2\files-2.1\com.google.dagger\hilt-android

2. 确定额外的依赖文件

为什么需要额外的依赖文件?

完全依赖AS开发可能不知道,导入的包的同时可能引入其他的包。

如Hilt的是在dagger2基础上开发,当然会引入Dagger2,

使用注解,需要javax.annotation包。

Dagger2,javax.annotation 在Gradle 自动下载好的,非项目中明确配置的,我们称之为依赖包。

使用Gradle 自动下载,都会有pom 文件。“dependency”,表示需要依赖的jar 包,还包含了版本号等

如:hilt-android-2.28-alpha.pom

``
`com.google.dagger`
`dagger` //依赖的dagger2
`2.28`//dagger2的版本
`
`
``
`com.google.dagger`
`dagger-lint-aar`
`2.28`
`
`
``
`com.google.code.findbugs`
`jsr305`
`3.0.1`
`
`
......

3. 导入需要的依赖文件

比如SystemUI,已经导入了一些文件,只要导入剩余的文件即可。

一般常用的 源码中都是存在的,决定copy 之前,可以看下先源码中是否存在,存在可以考虑使用。

当然也有例外,如Hilt 我依赖的是源码中dagger2是2.19 版本,编译中报错,没有找到dagger2 中的class,反编译jar确实不存在,使用2.28 的dagger 版本,问题就解决了。所以说可能存在库文件版本较老的情况。

以下就是新增的文件夹,其中manifests 后文中有讲。

    manifests/ 
repository/com/google/dagger/dagger-compiler/2.28/
repository/com/google/dagger/dagger-producers/2.28/
repository/com/google/dagger/dagger-spi/2.28/
repository/com/google/dagger/dagger/2.28/
repository/com/google/dagger/hilt-android-compiler/
repository/com/google/dagger/hilt-android/

4. 编写最终的bp文件

这一步就是把依赖的包,关联起来,根据上文的 pom 文件。

  • 配置dagger2 2.28 的jar
java_import {

name: "dagger2-2.28",

jars: ["repository/com/google/dagger/dagger/2.28/dagger-2.28.jar"],

host_supported: true,

}
  • 配置 dagger2-compiler 2.28 的jar (annotationProcessor 依赖的jar包)
java_import_host {

name: "dagger2-compiler-2.28-import",

jars: [

"repository/com/google/dagger/dagger-compiler/2.28/dagger-compiler-2.28.jar",

"repository/com/google/dagger/dagger-producers/2.28/dagger-producers-2.28.jar",

"repository/com/google/dagger/dagger-spi/2.28/dagger-spi-2.28.jar",

"repository/com/google/dagger/dagger/2.28/dagger-2.28.jar",

"repository/com/google/guava/guava/25.1-jre/guava-25.1-jre.jar",

"repository/com/squareup/javapoet/1.11.1/javapoet-1.11.1.jar",

"repository/com/google/dagger/dagger-google-java-format/1.6/google-java-format-1.6-all-deps.jar",

],
}
  • 配置dagger2 的 plugin (annotationProcessor)
java_plugin {
name: "dagger2-compiler-2.28",
static_libs: [
"dagger2-compiler-2.28-import",
"jsr330",
],
processor_class: "dagger.internal.codegen.ComponentProcessor",
generates_api: true,
}
  • 配置 hilt 依赖的aar包
android_library_import {
name: "hilt-2.82-nodeps",
aars: ["repository/com/google/dagger/hilt-android/2.28-alpha/hilt-android-2.28-alpha.aar"],
sdk_version: "current",
apex_available: [
"//apex_available:platform",
"//apex_available:anyapex",
],
min_sdk_version: "14",
static_libs: [
"dagger2-2.28",
"jsr305",
"androidx.activity_activity",
"androidx.annotation_annotation",
"androidx.fragment_fragment",
],

}
  • 配置hilt 的包

    android_library 表示 aar 包,所以必须要配置manifests ,在上文中多出的manifasts文件夹中 放的就是这个文件,AndroidManifest.xml来自hilt-android-2.28-alpha.aar 中

android_library {
name: "hilt-2.82",
manifest: "manifests/dagger.hilt.android/AndroidManifest.xml",
static_libs: [
"hilt-2.82-nodeps",
"dagger2-2.28"
],
......
}
  • 配置 hilt-compiler 2.82 jar包
java_import_host {
name: "hilt-compiler-2.82-import",
jars: [
"repository/com/google/dagger/dagger-compiler/2.28/dagger-compiler-2.28.jar",
"repository/com/google/dagger/dagger-producers/2.28/dagger-producers-2.28.jar",
"repository/com/google/dagger/dagger-spi/2.28/dagger-spi-2.28.jar",
"repository/com/google/dagger/dagger/2.28/dagger-2.28.jar",
"repository/com/google/guava/guava/25.1-jre/guava-25.1-jre.jar",
"repository/com/squareup/javapoet/1.11.1/javapoet-1.11.1.jar",
"repository/com/google/dagger/dagger-google-java-format/1.6/google-java-format-1.6-all-deps.jar",
"repository/com/google/dagger/hilt-android-compiler/2.28-alpha/hilt-android-compiler-2.28-alpha.jar",
"repository/javax/inject/javax.inject/1/javax.inject-1.jar"
],
}
  • 配置hilt的 plugin (annotationProcessor)

    反编译查看需要配置的Processer

好吧,看到上图我傻眼了,11个。下文代码我只贴了一个,需要写11个,其他省略。

java_plugin {
name: "hilt-compiler-2.82",
static_libs: [
"hilt-compiler-2.82-import",
"jsr330",
],
processor_class: "dagger.hilt.processor.internal.root.RootProcessor",
generates_api: true,
}
  • 项目中引用
    `static_libs: [`
`"androidx-constraintlayout_constraintlayout",`
`"androidx.appcompat_appcompat",`
`"com.google.android.material_material",`
`"androidx.room_room-runtime",`
`"androidx.lifecycle_lifecycle-viewmodel",`
`"androidx.lifecycle_lifecycle-livedata",`
`"hilt-2.82",`
`"jsr330"`
`],`

`plugins: ["androidx.room_room-compiler-plugin",`
`"hilt-compiler-2.82",`
`"hilt-compiler-2.82-UninstallModulesProcessor",`
`"hilt-compiler-2.82-TestRootProcessor",`
`"hilt-compiler-2.82-DefineComponentProcessor",`
`"hilt-compiler-2.82-BindValueProcessor",`
`"hilt-compiler-2.82-CustomTestApplicationProcessor",`
`"hilt-compiler-2.82-AndroidEntryPointProcessor",`
`"hilt-compiler-2.82-AggregatedDepsProcessor",`
`"hilt-compiler-2.82-OriginatingElementProcessor",`
`"hilt-compiler-2.82-AliasOfProcessor",`
`"hilt-compiler-2.82-GeneratesRootInputProcessor",`
`],`
  • 编译确认

    编译失败了!看到报错,我的心也凉了。需要配置Gradle 插件。bp 可以配置Gradle插件?

    看了下com/google/dagger/hilt-android-gradle-plugin/,但是并不清楚bp 怎么配置,在源码里,只知道一处:prebuilts/gradle-plugin/Android.bp,但并没有尝试成功。有兴趣的同学,可以研究下。

    而且hilt-android-gradle-plugin 的jar包,依赖包 至少十几个。

public class MainActivity extends AppCompatActivity { ^ Expected @AndroidEntryPoint to have a value. Did you forget to apply the Gradle Plugin? [Hilt] Processing did not complete. See error above for details.

public class MainFragment extends BaseFragment { ^ Expected @AndroidEntryPoint to have a value. Did you forget to apply the Gradle Plugin? [Hilt] Processing did not complete. See error above for details.

public class AppApplication extends Application { ^ Expected @HiltAndroidApp to have a value. Did you forget to apply the Gradle Plugin? [Hilt] Processing did not complete. See error above for details.

虽然Hilt引入失败,但是整个过程我觉得有必要分享一下,给大家一些导入新框架的参考。

源码环境里集成开源框架的流程

2xipH1.png

常用开源框架的对照表

 
build.gradleAndroid.bpAOSP源码位置
androidx.appcompat:appcompatandroidx.appcompat_appcompat/sdk/current/androidx/Android.bp
androidx.core:coreandroidx.core_coreprebuilts/sdk/current/androidx/Android.bp
com.google.android.material:materialcom.google.android.material_materialprebuilts/sdk/current/extras/material-design-x/Android.bp
androidx.constraintlayout:constraintlayoutandroidx-constraintlayout_constraintlayoutprebuilts/sdk/current/extras/constraint-layout-x/Android.bp
androidx.lifecycle:lifecycle-livedataandroidx.lifecycle_lifecycle-livedataprebuilts/sdk/current/androidx/Android.bp
androidx.lifecycle:lifecycle-viewmodelandroidx.lifecycle_lifecycle-viewmodelprebuilts/sdk/current/androidx/Android.bp
androidx.recyclerview:recyclerviewandroidx.recyclerview_recyclerviewprebuilts/sdk/current/androidx/Android.bp
androidx.annotation:annotationandroidx.annotation_annotationprebuilts/sdk/current/androidx/Android.bp
androidx.viewpager2:viewpager2androidx.viewpager2_viewpager2prebuilts/sdk/current/androidx/Android.bp
androidx.room:room-runtimeandroidx.room_room-runtimeprebuilts/sdk/current/androidx/Android.bp
glideglide-prebuiltprebuilts/maven_repo/bumptech/Android.bp
gsongson-prebuilt-jarprebuilts/tools/common/m2/Android.bp
Robolectric相关Robolectric相关prebuilts/tools/common/m2/robolectric.bp
 

经验总结

1、build.gradle 需要配置 额外插件的,如hilt、databinding viewbinding 不建议使用源码编译。

2、建议使用 AOSP 源码 中 bp 已经配置好的。这样就可以直接使用了。

3、jetpack 包引入或者androidx 引入,建议先prebuilts/sdk/current/androidx 下寻找配置好的bp 文件

4、非androidx ,建议先在prebuilts/tools/common/m2下寻找寻找配置好的bp 文件

5、文章中的例子都是prebuilts目录下配置,项目中使用,也可以配置在项目中,都是可以的。

收起阅读 »

探究Android View绘制流程

1.简介在开发中,我们经常会遇到各种各样的View,这些View有的是系统提供的,有的是我们自定义的View,可见View在开发中的重要性,那么了解Android View的绘制流程对于我们更好地理解View的工作原理和自定义View相当有益,本文将依据And...
继续阅读 »

1.简介

在开发中,我们经常会遇到各种各样的View,这些View有的是系统提供的,有的是我们自定义的View,可见View在开发中的重要性,那么了解Android View的绘制流程对于我们更好地理解View的工作原理和自定义View相当有益,本文将依据Android源码(API=30)探究View的绘制流程,加深大家对其的理解和认知。

2.View绘制流程概览

应用的一个页面是由各种各样的View组合而成的,它们能够按照我们的期望呈现在屏幕上,实现我们的需求,其背后是有一套复杂的绘制流程的,主要涉及到以下三个过程:

  1. measure:顾名思义,是测量的意思,在这个阶段,做的主要工作是测量出View的尺寸大小并保存。

  2. layout:这是布局阶段,在这个阶段主要是根据上个测量阶段得到的View尺寸大小以及View本身的参数设置来确定View应该摆放的位置。

  3. draw:这是阶段相当重要,主要执行绘制的任务,它根据测量和布局的结果,完成View的绘制,这样我们就能看到丰富多彩的界面了。

    这些阶段执行的操作都比较复杂,幸运的是系统帮我们处理了很多这样的工作,并且当我们需要实现自定义View的时候,系统又给我们提供了onMeasure()、onLayout()、onDraw()方法,一般来说,我们重写这些方法,在其中加入我们自己的业务逻辑,就可以实现我们自定义View的需求了。

3.View绘制的入口

讲到View绘制的流程,就要提到ViewRootImpl类中的performTraversals()方法,这个方法中涉及到performMeasure()、performLayout()、performDraw()三个方法,其中performMeasure()方法是从ViewTree的根节点开始遍历执行测量View的工作,performLayout()方法是从ViewTree的根节点开始遍历执行View的布局工作,而performDraw()方法是从ViewTree的根节点开始遍历执行绘制View的工作,ViewTree的根节点是DecorView。performTraversals()方法内容很长,以下只是部分代码。

//ViewRootImpl
private void performTraversals() {
final View host = mView;
...
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
...
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
performLayout(lp, mWidth, mHeight);
...
performDraw();
}

4.measure阶段

measure是绘制流程的第一个阶段,在这个阶段主要是通过测量来确定View的尺寸大小。

4.1 MeasureSpec介绍

  1. MeasureSpec封装了从父View传递到子View的布局要求,MeasureSpec由大小和模式组成,它可能有三种模式。
  2. UNSPECIFIED模式:父View没有对子View施加任何约束,子View可以是它想要的任何大小。
  3. EXACTLY模式:父View已经为子View确定了精确的尺寸,不管子View想要多大尺寸,它都要在父View给定的界限内。
  4. AT_MOST模式:在父View指定的大小范围内,子View可以是它想要的大小。

4.2 View测量的相关方法

  1. ViewRootImpl.performMeasure()方法

    private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
    if (mView == null) {
    return;
    }
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
    try {
    mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    } finally {
    Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
    }

    在performMeasure()中,从根布局DecorView开始遍历执行measure()操作。

  2. View.measure()方法

    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    boolean optical = isLayoutModeOptical(this);
    if (optical != isLayoutModeOptical(mParent)) {
    Insets insets = getOpticalInsets();
    int oWidth = insets.left + insets.right;
    int oHeight = insets.top + insets.bottom;
    widthMeasureSpec = MeasureSpec.adjust(widthMeasureSpec, optical ? -oWidth : oWidth);
    heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
    }

    ...

    if (forceLayout || needsLayout) {
    // first clears the measured dimension flag
    mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;

    resolveRtlPropertiesIfNeeded();

    int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
    if (cacheIndex < 0 || sIgnoreMeasureCache) {
    // measure ourselves, this should set the measured dimension flag back
    onMeasure(widthMeasureSpec, heightMeasureSpec);
    mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
    } else {
    long value = mMeasureCache.valueAt(cacheIndex);
    // Casting a long to int drops the high 32 bits, no mask needed
    setMeasuredDimensionRaw((int) (value >> 32), (int) value);
    mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
    }
    ...
    }

    ...
    }

    调用这个方法是为了找出视图应该有多大,父View在宽度和高度参数中提供约束信息,其中widthMeasureSpec参数是父View强加的水平空间要求,heightMeasureSpec参数是父View强加的垂直空间要求,这是一个final方法,实际的测量工作是通过调用onMeasure()方法执行的,因此只有onMeasure()方法可以被子类重写。

  3. View.onMeasure()方法

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
    getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

    这个方法的作用是测量视图及其内容,以确定测量的宽度和高度,这个方法被measure()方法调用,并且应该被子类重写去对它们的内容进行准确和有效的测量,当重写此方法时,必须调用setMeasuredDimension()方法去存储这个View被测量出的宽度和高度。

  4. View.setMeasuredDimension()方法

    protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
    boolean optical = isLayoutModeOptical(this);
    if (optical != isLayoutModeOptical(mParent)) {
    Insets insets = getOpticalInsets();
    int opticalWidth = insets.left + insets.right;
    int opticalHeight = insets.top + insets.bottom;

    measuredWidth += optical ? opticalWidth : -opticalWidth;
    measuredHeight += optical ? opticalHeight : -opticalHeight;
    }
    setMeasuredDimensionRaw(measuredWidth, measuredHeight);
    }

    setMeasuredDimension()方法必须被onMeasure()方法调用去存储被测量出的宽度和高度,在测量的时候如果setMeasuredDimension()方法执行失败将会抛出异常。

    private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
    mMeasuredWidth = measuredWidth;
    mMeasuredHeight = measuredHeight;

    mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
    }

    setMeasuredDimensionRaw()方法被setMeasuredDimension()方法调用来设置出被测量出的宽度和高度给View的变量mMeasuredWidth和mMeasuredHeight。

    public static int getDefaultSize(int size, int measureSpec) {
    int result = size;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

    switch (specMode) {
    case MeasureSpec.UNSPECIFIED:
    result = size;
    break;
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
    result = specSize;
    break;
    }
    return result;
    }

    参数size是这个View的默认大小,参数measureSpec是父View对子View施加的约束,通过计算的得出这个View 应该的大小,如果MeasureSpec没有施加约束则使用提供的大小,如果是MeasureSpec.AT_MOST或MeasureSpec.EXACTLY模式则会使用specSize。

    protected int getSuggestedMinimumWidth() {
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }

    getSuggestedMinimumWidth()方法返回View应该使用的最小宽度,这个返回值是View的最小宽度和背景的最小宽度二者之中较大的那一个值。当在onMeasure()方法内被使用的时候,调用者依然应该确保返回的宽度符合父View的要求。

4.3 ViewGroup测量的相关方法

ViewGroup继承View,是一个可以包含其他子View的一个特殊的View,在执行测量工作的时候,它有几个比较重要的方法,measureChildren()、measureChild()和getChildMeasureSpec()。

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}

measureChildren()方法要求这个View的子View们去测量它们自己,处于GONE状态的子View不会执行measureChild()方法。

protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();

final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);

child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

measureChild()方法要求子View去测量它自身,测量的同时需要考虑到父布局的MeasureSpec要求和它自身的padding。

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);

int size = Math.max(0, specSize - padding);

int resultSize = 0;
int resultMode = 0;

switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;

// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;

// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

这个方法做了测量子View过程中复杂的工作,计算出MeasureSpec传递给特定的子节点,目标是根据来自MeasureSpec的信息以及子View的LayoutParams信息去得到一个最可能的结果。

4.4 DecorView的测量

DecorView继承了FrameLayout,FrameLayout又继承了ViewGroup,它重写了onMeasure()方法,并且调用了父类的onMeasure()方法,在遍历循环去测量它的子View,之后又调用了setMeasuredDimension()。

//DecorView
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final DisplayMetrics metrics = getContext().getResources().getDisplayMetrics();
final boolean isPortrait = getResources().getConfiguration().orientation == ORIENTATION_PORTRAIT;

final int widthMode = getMode(widthMeasureSpec);
final int heightMode = getMode(heightMeasureSpec);
...
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
...
}
//FrameLayout
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
...
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
maxWidth = Math.max(maxWidth,
child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
maxHeight = Math.max(maxHeight,
child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
childState = combineMeasuredStates(childState, child.getMeasuredState());
if (measureMatchParentChildren) {
if (lp.width == LayoutParams.MATCH_PARENT ||
lp.height == LayoutParams.MATCH_PARENT) {
mMatchParentChildren.add(child);
}
}
}
}
...
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec,
childState << MEASURED_HEIGHT_STATE_SHIFT));
...
}

5.layout阶段

当measure阶段完成后,就会进入到layout布局阶段,根据View测量的结果和其他参数来确定View应该摆放的位置。

5.1 performLayout()方法

测量完成后,在performTraverserals()方法中,会执行performLayout()方法,开始布局过程。

private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
mScrollMayChange = true;
mInLayout = true;
final View host = mView;
if (host == null) {
return;
}
...
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
...
}

5.2 layout()方法

//ViewGroup
@Override
public final void layout(int l, int t, int r, int b) {
if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
if (mTransition != null) {
mTransition.layoutChange(this);
}
super.layout(l, t, r, b);
} else {
// record the fact that we noop'd it; request layout when transition finishes
mLayoutCalledWhileSuppressed = true;
}
}

这个是ViewGroup的layout()方法,它是一个final类型的方法,在其内部又调用了父类View的layout()方法。

//View
@SuppressWarnings({"unchecked"})
public void layout(int l, int t, int r, int b) {
...
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);

if (shouldDrawRoundScrollbar()) {
if(mRoundScrollbarRenderer == null) {
mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
}
} else {
mRoundScrollbarRenderer = null;
}
...
}
...
}

View的layout()方法作用是为它本身及其后代View分配大小和位置,派生类不应重写此方法,带有子View的派生类应该重写onLayout()方法,参数l、t、r、b指的是相对于父View的位置。

5.3 setFrame()方法

//View
protected boolean setFrame(int left, int top, int right, int bottom) {
boolean changed = false;
...
if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
changed = true;

// Remember our drawn bit
int drawn = mPrivateFlags & PFLAG_DRAWN;

int oldWidth = mRight - mLeft;
int oldHeight = mBottom - mTop;
int newWidth = right - left;
int newHeight = bottom - top;
boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);

// Invalidate our old position
invalidate(sizeChanged);

mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);

mPrivateFlags |= PFLAG_HAS_BOUNDS;


if (sizeChanged) {
sizeChange(newWidth, newHeight, oldWidth, oldHeight);
}
...
}
return changed;
}

在View的layout()方法内会调用setFrame()方法,其作用是给这个视图分配一个大小和位置,如果新的大小和位置与原来的不同,那么返回值为true。

5.4 onLayout()方法

//View
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {

}

View的onLayout()方法是一个空方法,内部没有代码实现,带有子节点的派生类应该重写此方法,并在其每个子节点上调用layout。

//ViewGroup
@Override
protected abstract void onLayout(boolean changed,
int l, int t, int r, int b);

ViewGroup的onLayout()方法是一个抽象方法,因此直接继承ViewGroup的类需要重写此方法。

//DecorView
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
...
}

5.5 DecorView的布局

//DecorView
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (mApplyFloatingVerticalInsets) {
offsetTopAndBottom(mFloatingInsets.top);
}
if (mApplyFloatingHorizontalInsets) {
offsetLeftAndRight(mFloatingInsets.left);
}

// If the application changed its SystemUI metrics, we might also have to adapt
// our shadow elevation.
updateElevation();
mAllowUpdateElevation = true;

if (changed
&& (mResizeMode == RESIZE_MODE_DOCKED_DIVIDER
|| mDrawLegacyNavigationBarBackground)) {
getViewRootImpl().requestInvalidateRootRenderNode();
}
}

DecorView重写了onLayout()方法,并且调用了其父类FrameLayout的onLayout()方法。

//FrameLayout
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}

void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
final int count = getChildCount();

final int parentLeft = getPaddingLeftWithForeground();
final int parentRight = right - left - getPaddingRightWithForeground();

final int parentTop = getPaddingTopWithForeground();
final int parentBottom = bottom - top - getPaddingBottomWithForeground();

for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();

final int width = child.getMeasuredWidth();
final int height = child.getMeasuredHeight();
...
child.layout(childLeft, childTop, childLeft + width, childTop + height);
}
}
}

在FrameLayout的onLayout()方法中,调用了layoutChildren()方法,在此方法内开启循环,让子View调用layout()去完成布局。

收起阅读 »

Android或前端开发中--不得不说的登录、授权(Cookie、Authorization)

Cookie起源:购物车他的起源比较早,那个时候还不是IE,更不是现在的Chrome,是更早的Netscape(网景)那个时候浏览器的开发者、开发浏览器的公司也会去帮别人开发网站。当时是是什么情况?有个电商网站希望有购物车这个功能。(购物车现在都是怎么做?不管...
继续阅读 »

Cookie

起源:购物车

  • 他的起源比较早,那个时候还不是IE,更不是现在的Chrome,是更早的Netscape(网景)
  • 那个时候浏览器的开发者、开发浏览器的公司也会去帮别人开发网站。
  • 当时是是什么情况?有个电商网站希望有购物车这个功能。(购物车现在都是怎么做?不管是淘宝还是什么网站,他们的购物车都是存在服务器的)
  • 可是那个时候的开发商他不想在自己服务器上面存信息,你又没有买,我存什么呀,你存本地吧,但是本地怎么存呢?没这功能呀。
  • 他就去给他做网站的开发人员,同时也是开发浏览器公司的人说, 你去给浏览器加这个功能吧, 你同时是浏览器的开发者,又是网站的开发者,这个时候做这个事是比较方便的。然后浏览器开发者就说,可以,我给你做,然后他们就干脆一不做二不休,做了一个完整的功能。
  • 这个功能叫做Cookie。他就是用来记录购物车的,只不过因为服务器不想记,想记到本地去,记到每一个添加购物车的电脑上。

工作机制:

  • 他是在本地记录,那么本地记录一个服务器需要的信息,怎么做呢?
    • 大致就是,服务器需要你保存什么,本来是服务器自己保存的信息,改成客户端记录。
    • 服务器需要你保存什么,然后发过来,你把他保存到本地就可以了。
  • 第一步,我在这个网站里面,我想往我的购物车里面加一个苹果,怎么加呢?
    • 我访问cart(购物车)这个接口,用post传过去一个数据,叫apple=1,就表示我要往我的购物车里面加一个apple。(请求响应报文完全不懂的可以补充一下知识http的原理和工作机制
  • 好,服务器就知道了。然后服务器处理完就会给我返回一个信息:
    • 好的(200 OK),同时会加一个header,这个header叫Set-Cookie:cart="apple=1"(不是Cookie,是Set-Cookie。
    • Cookie是客户端给服务器用的,而 Set-Cookie就是服务器给客户端用的,意思是你把这个cookie存下来吧),表示购物车里面有一个苹果。
    • 然后客户端就记下来了。好的,我知道了,我下次再访问shop.com的时候附加进去
    • (下图客户端下面这个shop.com就是服务器的请求地址,跟上面的请求是对应的,而且这个cookie存下来以后,他还有继续的自动机制,当浏览器再次访问这个内容的时候,他的cookie会自动附加进去,这是自动的,由浏览器来实现的,网站开发者不需要关心,用户也不需要关心)

image.png

  • 下次,我要加一个东西,比如我要加一个香蕉,
    • 我只要发过去我需要一个香蕉,同时将本地的这个cookie附加过去,而且这个cookie我不需要管,浏览器自动处理,服务器发消息的时候也不需要管,
    • 现在,我通过这个post告诉服务器要加一个香蕉,并且通过cookie告诉他我已经加了一个苹果,
  • 然后服务器就知道了,然后就返回消息告诉你记下来,记在本地。这个时候客户端自动更新这个东西。
  • 注意到没有,服务器什么都没记,他不管,客户端记了,所以依然不影响通讯。

image.png

  • 这个时候如果客户端要结账,
    • 他不需要发其他内容,因为cookie会把他的购物车给带过去,然后服务器再把这东西返回来。
    • 这里有一个东西很浪费,我把购物车信息发给你,你再发给我,好浪费啊,真麻烦,既然你帮我处理,我自己处理不就完了吗?
      • 这是早期,js这个东西在早期还是很落后的,服务器帮你做一个事,而且他还会做一些额外的事情,帮你做验证啊,还有没有货这些,我只是把他传过去,然后传回来,但是这个工作还是要服务器做的。
    • cookie是谁管理的?
      • 是服务器,每次修改都是服务器在做。客户端完全只是一个被动保存的机器。
    • 这个就是cookie设计这个机制的原因。就是服务器需要存什么,告诉他,客户端无条件的存下来,然后每次访问回去的时候,再把他带回去。

image.png

  • 关于Cookie大致知道就可以,因为我们在工作中,在移动开发中用cookie用得非常少,前些年还用一用,现在都不用了。逐渐在抛弃,但是抛弃的不是cookie,而是抛弃使用cookie来做登录,用cookie来做认证。

作用:

会话管理:登录状态,购物车

  • 那么我们怎么用cookie来管理登录状态呢?
    • 首先,我向服务器发一个http请求,去login,
      • 我要登录,我把我的用户名和登录密码都添加过去,给服务器确认。
    • 服务器怎么确认?
      • 他会记录a(username)登录了,
      • 同时他会为这件事情创建一个会话,表示我现在在和某个客户端,或者说某个用户代理,我们在通信,他现在是一个什么状态,他也许是一个登录状态,也许是一个非登录状态,总之我们正在会话中。
      • 他会记录下来他的会话信息(session),比如他的id是123。他就会把这个东西给存下来,
      • 然后把我的session id返回给我,服务器返回给客户端,session是一个名字,我可以叫session,可以叫metting都可以,他只是表示我和客户端之间交互了,然后他就会发回来,
      • session也是通过set-cookie。他是通过服务端定的,客户端什么都不管,我只把他记下来。

image.png

  • 然后客户端再次访问的时候,
  • 就会自动把这个session给移过去,然后服务器收到以后就会去对照,有没有一个session id=123的,
  • 一看,有,哦,原来他是username叫a的这个人,并且他现在是登录状态,好的,他要用户信息是吧?我给他,因为他已经登录了。
  • 然后就会正常地返回过来。这个是使用cookie来管理登录状态。

image.png

  • 类似session的标识,cookie里面是可以有多个的
    • 现在我用sessionid来管理我的登录信息是吧,同时我虽然已经用session存了你的登录信息了,我还不想存你的购物车,没关系,你要添加一个apple对吧?跟刚才那个过程是一样的,你发一个apple=1过来,我给你返回一个cart=“apple=1”,你记下来,
    • 你可以记两个cookie,
      • 一个sessionid用来管理你的登录,
      • 一个cart来管理你的购物车,两个东西互相不干扰。cookie是可以记多个的。

image.png

个性化:用户偏好、主题

  • 怎么用cookie来管理用户偏好呢?
    • 比如我现在有一个网站,叫shop.com,一个叫黑色主题,一个叫白色主题,我向服务器请求,请求完了以后,服务器给我返回一个client_id,他和session id是一样的,是一个标记,我把这个标记给你发过去,啪嗒贴脸上,然后每次你找我说话,我都会看见,你是做了那个标记的人。

image.png

  • 下次你再跟我说,我想换个蓝色主题, 专门发一个请求去改变主题,服务器觉得可以,就返回ok,这是一个正常的返回,然后服务器就记住了,这个client_id=123的人喜欢蓝色主题,他帮你保存了。

image.png

  • http是无状态的,假如你不存这个123,你下次再访问另一个页面,那肯定还是给你默认主题,因为记不住你,我不知道你就是上次那个人。
  • 那么下次我再访问过来,这个cookie是会自动附加的,然后他访问的是另外一个页面,并不是原页面,也不是改变主题的页面,可是服务器就是知道我要给他蓝色主题,因为他是123。
  • 这个就是网站用来管理用户偏好的一种记录方式,使用cookie在用户端记录的。

image.png

分析用户行为:

  • 关于分析用户行为,用户的追踪的使用

    • 这个在国外是比较有争议的,尤其是欧洲。我们在登录网址的时候,不知道你们注意到没有,尤其是一些国外的网站,国内的网站还不是很在意这个,因为跟法律有关跟当地法律有关,跟道德关系其实很小很小,一般都是跟法律有关,法律要求你必须这么干,或者就是民众的意识,你必须这么干这么干,你如果不这么干大家不用你网站了,那怎么办?做呗。分析用户行为,追踪用户行为,就是你能知道用户去了哪些网站,那么我们访问国外网站的时候,他在顶部或者底部跟你说,你现在访问的网站正在使用cookie对你进行行迹的追踪,这个对你是没有害处的,而且我们一定不会把你的消息给公开,只是我们自己来用的,请你理解。都会用这样的东西。我们看起来可能会觉得你怎么这么多余啊,你用就用呗,你还告诉我,让我不爽干嘛?但其实是这样的,在那些国家尤其是欧洲,你不通知用户,就使用cookie追踪用户是非法的,是会遭到惩罚的,比如巨额罚款,那么他们要做,就只有把他申明出来,用户点一个确定。都会点的,少数人不点,这个用户追踪是很有价值的。如果你真的那么在意的话那你别用了。没办法,我很想让你用,我很想赚你的钱,我只有不挣了。
  • 说一下这个东西怎么工作的。

    • 我有一个客户端和服务器,我想向你请求一个数据,
    • 然后你返回的数据有点意思,在body里面返回一个图片的链接,从你这个链接可以定到那个图片去,而且是自动的。就好像在你的网站打开一个在线图片一样,
    • 但他有什么关键的地方?
      • 就下面图片标记的那个地方,他加了一个和图片本身无关的信息from=shop.com,我在访问shop.com对吧,他去另外一个网站(3rd-part.com),这个网站就是帮助记录用户行踪的网站。
    • 追踪用户信息,从来都不是从a网站记录a网站,从b网站记录b网站,永远都是有一个统一的站点,他去记录用户去各个站点的行迹,然后他做一个统计。然后让其他网站一起来用,一个网站只记录一个网站在当前,只记录用户在自己的行为是没用的。这个不叫用户追踪。都是有一个统一的网站去记。
    • 这个例子里面就是这个3rd-part.com,他去记录了这个用户在shop.com的行为,那么这个链接有什么效果?
      • 就是这个用户打开这个网站之后,自动地去显示一张图片,可能会是一张广告什么的,然后这个图片他会附加一个信息,这个信息跟图片显示无关。也就是我去shop.com去放这个图片,后面就是追这个,如果我去tabao.com就是追的tabao.com,他们显示的是同样的图片,但是对于第三方记录网站来说不一样,
      • 他就会记录下来,当前这个用户是从shop.com来的,他就会在他的数据里面加一条,加什么呢?这个用户他去过shop.com。

image.png

  • 服务器返回这个Cookie信息,客户端会记下来(client_id = 123),
  • 然后客户端在访问这个图片的时候会自动往这个第三方发一个请求,不发请求怎么显示图片吧?怎么把图片下载下来?那么就只能请求这个第三方。
  • 然后第三方网站(3rd-part.com)就会记录,在他的数据库里面加一条,这个叫123的用户他来自shop.com,他就会增加这个用户记录。
  • 接下来,他会把图片显示给你,然后呢,也给你这么一个记录,会给一个针对第三方网站的一个id,这两个id可以不是同一个东西,他们名字也不一样,他们后面值也可以不一样,因为他们只是一个三方网站用来记录用户行踪的。
  • 如果接下来,这个用户去上另外一个网站,假如这个网站也捆绑了同一个第三方的统计,那么会有什么一个结果,他也会访问这个网站的图片。
  • 然后这个第三方就会知道,这个用户已经来过了,之前是去过shop.com,现在来了taobao.com,我记录一下,这个用户喜欢shop.com和taobao.com,喜欢去这么两个网站。
  • 那么这个时候就对这个用户有一定的画像了。他去了两个网站,如果用户去的网站很多的话,虽然我不知道你具体是谁,但是我知道你是一个什么样的人,那么说明什么?说明要推广告了。比如我之前去了一个旅游网站,然后我现在去一个电商网站,那么他就可能会推旅游团信息。另外,想象的空间是非常大的,就是你拿到足够多的用户数据之后,你对这个用户的画像足够精确之后,他能推的广告非常多。其实这种追踪用户行为,都是为了什么?都是为了推广告,都是为了从你兜里拿钱。

image.png

说两个额外的东西

XSS跨站脚本攻击(Cross-site scripting):HttpOnly

  • XSS跨站脚本攻击
    • 攻击?攻击不只是我打你,我对你有人任何的侵犯行为都叫攻击。
    • 我们的js,我们的网页脚本,他可能会帮我们的网页去做一些很方便的事情,另外他也可以拿到他的cookie,去用cookie做一些很方便的事情。
    • 可是,他也有可能去做坏事,比如我们记录用户信息靠的是什么?靠得就是cookie,那么假如你的cookie存的不是购物车,存的不是什么喜好,而是登录信息,如果js是一个坏人写的,他会怎么做呢?他可能会拿到你本地的cookie,然后直接给发出去,他去访问一个地址,比如访问他们自己的网站的一个存cookie的地址,把这个cookie发过去了, 你都不知道,后台就做这件事了。你的cookie就这么泄漏了,你的登录信息就这么泄漏了。一个js,一个本地脚本去获取header信息多正常啊,非常正常。
    • 这就是为什么cookie这个东西他为什么危险。因为我们登录这个事,本来是不存在的,后来慢慢出了,就用cookie做吧,然后慢慢就有人利用这个漏洞去做坏事。我的一个脚本,拿到你的cookie,直接给发走了,连邮件都不用,直接访问一个网页,一个url就可以了。
  • 他的应对政策有很多,其中一个就是在cookie这个header后面加上HttpOnly限制。
    • 比如:Set-Cookie:sessionid=123;HttpOnly
  • 他有什么效果?
    • 就是这个cookie你的本地脚本看不到,他只用于我们http的交换,只用于信息交换,本地脚本看不到,那么如果你是一个包含敏感信息,比如登录信息的cookie的话,那么你加这个东西去做限制。这都是后来被摸索出来的。

XSRF跨站请求伪造(Cross-site request forgery):Referer

  • 跨站请求伪造
    • 这个更加猥琐,也让我们有点想不到。他是什么呢?cookie是个自动机制,假如我现在访问一个网站,是个银行网站,然后我又去访问一个坏人的网站,我并不知道这个坏人网站,这个坏人在他的脚本里面加东西了,加什么呢?让我去访问一个地址,比如我使用图片的时候访问一个地址,访问地址的时候会附加一些别的操作,让我去访问银行,他会对各种银行都去试一试,我就赌你最近访问过银行,并且有cookie。
      • 比如这么一个url:bank.com/transfer?am…
      • 就这么一个url,你访问过去,就把钱转给坏人了,不需要我登录,不需要我确认,为什么?因为有cookie,cookie是自动的,对吧?假如我之前去登录过这个网站,那么他只要暗中去访问这个网址,刷,我钱转走了。
      • 当然实际操作中会有各种各样的防范,银行方面也会做这方面的防范,浏览器他们也会互相配合做这些东西,只是这是一个搞坏事的原型。
  • 那么解决方案,其中一点就是Referer这个header,Referer他的拼写是错的,Referrer(转发者)由于历史原因,就应该错着写才行。
    • 用法:Referer:http://www.google.com
    • 他就是用来显示你是从哪个网站跳转过来的。假如这个银行发现你这个url,你这个申请转账的url他是来自一个我不认识的网站,或者一个危险的网站,那么我拒绝对你转账。不过这种解决方案也有他的缺陷,你需要依赖浏览器,浏览器需要能给你做这个功能,假如浏览器不帮你做这个功能,我从a网站跳到b网站的时候,我不帮你自动加这个Referer这个header,那不就是瞎了吗。
  • 不过说来说去就比较长了,这两个都是cookie比较危险的点。cookie危险的点很多,但是不是因为他不好,而是这个东西天生劣势,由于要获得什么什么好处,所以会有什么什么危险。cookie为什么被遗弃?并不是他被遗弃,而是不再用于授权。

2.Authorization

  • 相比于cookie,Authorization就更加流行,而且越来越流行。

Authorization最常用的有两种:

Basic:

  • basic就是基本的授权方式,即使我们用的很少,但是也是很实用的。
  • Authorization:Basic<username:password(Base64ed)>
    • 例子:
    • get /user http/1.1
    • host: xxxxxx.xxx
    • Authorization:Basic eGlhb21pbmc6cWl1bG9uZw==
  • 比如现在我要做一个请求,我有一个header叫Authorization:Basic xxxx,这个就是认证信息,如果这个数据对了,那么我可以获取到用户信息,如果这个错了,用户信息就获取不到,他会跟我说你没有权限,你的权限不足。这是一个http请求,用法就是这样的,他用在header里面。

那么这个xxxx里面是什么内容呢?

  • base64转化后的用户名和密码。
  • 比如我的用户名和密码是xiaoming和qiulong,
  • 合在一起就是:xiaoming:qiulong
  • 转化后就是:eGlhb21pbmc6cWl1bG9uZw==

我这么请求数据服务器就会给我返回正确信息,这个就是basic,

  • 为什么叫basic,因为他是最基本的信息了,用户名密码都给我就完了。
  • 这个东西他有什么缺陷呢?他是有安全风险的,你这个东西万一被截获怎么办?
    • 其实,现在大多数网站,尤其是api,就是浏览器之外的,你的应用使用的时候已经全都是https了,那么安全就交给https,我真的可以把我的用户名和密码直接传过去,因为他们都会被加密的,别人看不到的。
    • 不过呢,他确实还有个安全缺陷,有一点点,就是假如你需要这么做,你就需要把经过base64转化的用户名和密码,或者是base64之前的,保存到本地,这样你下次再去请求才能够自动化这个东西。而不是要用户每次都输入用户名和密码,对吧?你换个页面就让用户输一次,用户不疯了吗?
    • 那么你把她存到你本地,不管是存到你电脑,还是存到你手机,假如你这个设备被人给黑了,比如你的手机你去获取了root权限,然后root权限你又把他给了某个软件,然后他就可以随便操作你手机了,他把你的东西盗走就是有可能的了。
    • 其实说回来,这种安全风险倒还好,其实他把自己的手机root权限获取到,本身就放弃了一定的安全性,对吧,手机被破解了你能怪这个机制不好吗?还是有些软件在用的,而且做得比较大比较重技术的公司也有在用这个的。所以本身在安全上是没问题的。刚才我说的安全风险相对来说还算好吧。设备被破解了才有风险,那叫什么风险呢。这是第一种,不过用的公司还是少一点。挺好用的,挺简单的。

Bearer:

  • Bearer(持票人)
    • 也就是拿着尚方宝剑的人,你做事需要亮你的尚方宝剑。这种就叫做token,上面的basic长着token这种形式,但是并不能叫token,严格说起来也是,你把用户名密码揉起来base64一下也可以用作票根。不过Bearer才是真正的比较形象的token形式。
    • 本来我不是那个人,但是那个人给了我权限,那么我拿这个令牌,就可以用这个令牌去获取信息,去操作。这个是很常用的一种方式。他的格式是这个样子:
    • Authorization:Bearer<Bearer token>
    • 前面也有个头Bearer,表示我这种认证用的不是basic,而是持票这种方式,后面把持票人的token填进来就可以了。而这个token他就不是某种算法得到的,而是需要找授权方给你,有的时候,你会使用github或者使用新浪微博,去给别的软件授权的时候,并不需要你从这个网站跳过去登录一下,而是直接从你的账户信息里面,会有一个获取token,获取api token,你把这个token复制出来给某个软件就可以了。这是一种方式,token本身就可以拿出来给别人去用。另外一种是OAuth2。

OAuth2:

  • 他是一种第三方认证的机制。
  • OAuth我们现在都是用的2,1 是好几年前的,2和1差别不大,他们核心都是一样的,但是他做了一些工作,让开发者关心的事情更少,同时并不降低安全性。
OAuth2流程:第三方授权示例
  • 首先去github-setting-application里面把掘金的授权回收,

image.png

  • 然后现在去掘金点登录,选择三方github,他就会进入这样一个页面。
  • 谁是第三方?
    • 现在我是github的用户,并不是我是掘金的用户,现在我用github的时候我需要做一个第三方的授权,我要把我的一些权限授给别的网站,比如这个网站叫掘金,这个第三方是掘金,第三方是我从github跳到做认证的地方,这个地方可能会有点迷,额,我不是使用github来登录吗?第三方怎么成了掘金了?这里不是我要强调一些概念,而是你要搞清楚才能理解一些其他东西。
    • 记住,第三方是掘金,第一方和第二方是你和github。你要把信息授权给别的网站你要做这么一回事。

image.png

image.png

  • 那么我要授权给第三方的时候,我现在点一个github登录,我就到了一个github页面了,是谁过来的?
    • 是掘金给我带过来的。看上面这个授权页面地址是github.com,而这个里面有一个关键信息,就是client_id。
    • 这个client_id是github授予给掘金的一个id,他什么时候授予的呢?
    • 掘金的开发者当初找github申请下来的。这个就是github他会对掘金有一个标记,那么这个标记有什么用处呢?
    • 他的用处就是当你打开这个页面的时候, 传入client_id,那么github就会自动把掘金的图标,以及这个掘金的名字,以及他需要哪些权限,还有下面这个url地址给你,然后用户看到这些的时候,他就会去判断,我现在需要授权的目标对象,到底是不是我以为的那个对象,因为有的时候网站会做伪造,会做劫持什么的,假如你没这个一步,他可能在授权过程中他给你换了。你以为你授权给掘金了,其实你授权给别的什么坏人网站了,那么坏人就要滥用你的信息了。这个client_id就这个作用,他用来做识别的。

image.png

  • 那么接下来,我在github上点那个确定,授权给xitu,然后请求发送,页面消失,登录成功。
  • 那么这一系列过程又发生了什么呢?
  • 我点了之后,这个github就会跳回掘金的网站去,同时跳回去的时候会返回一个授权码(Authorization code)。
    • Authorization code并不是一个token,为什么他不直接给token呢?
      • 有个很关键的原因,https这个过程他并不是被强制的,他在OAuth这个过程里面不是强制你要使用https,那么我这个过程可能被拦截了,可能被别人窃取了,窃取到之后,假如Authorization code就是最终的那个码的话,你别人窃取到之后不就可以使用这个权限了吗?
      • 还有什么呢?浏览器都是不可靠的,你不知道用户在用什么浏览器,你不知道用户在用什么操作系统,Authorization code传输到第三方以后,还是有可能泄漏。github只是给你一个code,code表示我的用户已经告诉我了,他确实愿意把权限授权给你(掘金),这个是一个证明,他愿意把权限授权给你,可是这并不是钥匙,他只是个证明,你拿这个code给我,跟我要用户数据,我不会给你,你还需要真正的授权的票据,那个token给我
      • 那么token怎么获取?接下来继续说。

image.png

  • 现在,我的浏览器已经获取到这个信息了,接下来他还要去找他的服务器,他会把这个code发送给服务器,通过http也好,通过https也好,这个东西不怕被窃取,他只是证明用户愿意授权。

image.png

  • 然后,到了服务器以后,服务器就会跟github去做请求了。就是第三方的服务器会去向授权方的服务器请求。请求的时候他会附加授权的code以及一个叫client_secret的东西。
  • 这个client_secret是什么呢?
    • 他是在第三方,也就是掘金去github申请的时候跟client_id一起发过来的,这两个数据本身没有什么区别,只是他们在实际用处上有点区别,这个secret是绝对保密的,任何地方都不会看到,只有第三方的服务器拿着。这次链接也绝对是https链接,是个绝对安全的链接。那么现在,我有code证明用户愿意授权给我,又有client_secret证明我就是掘金。这个时候github就知道了,他给我足够的身份信息,并且这个信息来得足够安全,他不会被人截获,他通过https过来的,那么这个时候github就足够放心,他把真正的access token返回回去了。

image.png

  • 这个时候Server拿到这个token,现在,这个OAuth2的流程已经结束了。他不需要把你的token发给客户端,不需要发给浏览器。用户把他在github上的一些权限授予了掘金,并且掘金已经拿到token了,接下来OAuth就不再参与了。
  • 接下来,比如你的Server调取信息,比如用户头像什么的。怎么做呢?Server去请求github.com,同时附加上这个token。怎么附加呢?
    • 假设我的token是abccccc

image.png

image.png

  • 这个过程是很安全的,但是由于一些事实的限制,或者是一些安全上的不在意,还有一种什么情况呢?Server会把这个token发给客户端,这个不是说不允许,只是他会对安全有一定的影响,就是别人把你这个token拿到了,他也可以去做事。

image.png

  • 然后这么请求,也是可以的,很多软件也这么做。不过这么做,就把OAuth流程的好处给浪费掉了,你既然这样,那干脆在用户授权之后直接返回回来不就可以了吗?费这么多事干嘛呀?费这么多事不就是为了让别人截取不到吗?让你的浏览器被人黑掉,你的手机被人黑掉,你的网络被人黑掉都没关系。你的token依然是安全的,对吧?但是你这么做这些东西有一点白费了。但是这种用法还是有一定的使用的概率的,还是有些公司是这么用的。只不过他不太具有OAuth2的安全性。

image.png

第三方登录示例:微信登录
  • 说到微信登录,我先说一下使用github登录,刚才我说使用github登录这个第三方授权,他的第三方是掘金。但是我要说,第三方登录这个事比第三方授权要来得晚一点,第三方登录他的第三方真的是github,你在掘金,使用第三方登录,他的第三方真的是github,由于第三方登录这个词的出现,他导致第三方授权这个概念非常非常含糊,非常非常让人难以理解。你应该能明白为什么吧?

    • 第三方授权是什么?本来是我跟github的信息,结果分享给你了,那掘金不是第三方吗?
    • 而第三方登录是什么呢?本来我要登录掘金的,但是我用了github,那第三方就是github。
  • 其实这个登录和授权都是很直观的东西,但是你要知道他们分别是谁,分别是谁不是为了考试,不是为了面试,但是你在思考问题的时候你会非常清晰。你把这些搞明白以后,你再看一些api文档什么的,你脑子会非常清晰,你会比谁都想得明白,这个是重点。

  • 继续说微信登录,他是什么?

    • 他是一种第三方登录。
    • 比如你有一个手机软件,然后他里面有一个登录按钮, 你可以使用用户名登录,也可以使用第三方登录。比如你可以使用微信登录。那么有些人会做微信的开发,不管你有没有做过,我要说一下微信登录的手机流程是什么。
  • 第一步,你会使用微信给你的api, 你通过这个api调用微信给你的接口,去打开微信的授权界面,那个授权界面叫微信登录。「你看授权登录,第三方授权和第三方登录真的是互相之间没法说。那个界面是微信对你进行第三方授权,微信对这个第三方(你的应用)授权,但其实他叫什么?他叫微信登录。」你点这个之后,微信就会把他的页面关闭,返回给你的软件一个Authorization code(授权码)。

  • 为什么给你授权码?这是一个完整的OAuth2的流程,接下来,正规做法,就是你把这个code告诉你的服务器,然后你的服务器再拿你的这个code,以及你的secret去找微信的服务器,去要你的access token,拿到这个token之后,你们的客户端需要什么数据,你们服务器就去请求什么数据,比如你的客户端需要微信的用户名和他的头像,好,你的服务器就去拿,不是客户端拿。客户端不应该持有token,除非不得以。 就算是不得以,也不能持有secret,你去找微信服务器去拿token的这个过程,一定不能发生在客户端。

  • 我知道有些公司在这么做的,有些公司就在这么做。他们的后端人员可能会推这个事,这个事不应该我们做呀, 你看微信api里面说的明明白白呀,要你去请求,你拿到那个code再去请求不就完了。你要secret吗?我给你呀,我们这存在有啊,你去吧。其实这个过程是不对的。你的客户端拿到code,code交给服务器,其实就完了。这个是为了安全考虑,不是为了省事考虑。如果是为了省事,根本就不需要OAuth了。直接用户同意之后,把token给你就完了。还要什么用code换token的过程啊?用code换token就因为你客户端获取了这个数据,未必足够安全。这个是微信登录。微信登录他是一个完整的OAuth过程。

在自家软件里面使用Bearer token
  • 也是使用这种方式,比如我的软件,有个接口
    • (api.xxx.com/login?username=qiulong&password=123)
    • 我输入这些信息,我的服务器就直接给我返回这个token(access_token=bdcj55s),
    • 当我再次使用的时候,我不需要附加其他信息,我只要附加(Authorization:Bearer bdcj55s),
    • 这就是下次我再请求的时候,我们的做法,这个过程他并没有OAuth的过程,他就是我前面说的做OAuth不要这么干的过程。我把用户名密码传过去,你直接把我要的那个token给我,我说的那个不安全的过程。他就是一个模仿了OAuth2的这种使用token的方式他的token的用法,但是他并不是一个OAuth2的过程。要知道这个并不是OAuth。很多人不懂OAuth2的原因就是这样的。就是有些api,他在用一个简化版本的OAuth2,你再去使用一些第三方OAuth2的过程你会发现,这怎么比我们公司麻烦这么多?好烦啊,他怎么还要code啊?其实是你们公司使用的是一个简化版的流程。你们自己登录自己的账户使用这种简化版是理所应当的,对吧?不然你还用code的话,你自己的Server拿着自己的code和自己的secret去找自己Server去换那个token,那不多此一举吗?
  • 这种过程,他只是使用了Bearer token这种模式,但是他并不是OAuth2的过程。
refresh token
  • 一个刷新的票根。

image.png

  • 大概长这个样子。就是服务器返回的时候不只是access_token,还返回了一个refresh_token,他是什么呢?

image.png

  • 这个过程中返回的不只是一个access token,还返回一个refresh_token,你的server可以使用refresh_token来找github.com,然后这个github.com就会返回这个新的access_token和一个新的refresh_token,然后之前那个老的access_token就失效了。或者你那个老的access_token在经过一段时间以后,比如七天,十五天后,他也会失效。你的assess_token会自动失效,或者会被refresh_token请求强制失效。那么这个过程是什么作用?他有什么意义呢?我本来有token你为什么要刷一下换一下呢?他其实就是为了安全,就是你的access_token不管怎么样,他还是有一定概率会丢掉的。那么你这个token丢掉之后,你要用户重新过来再认证一次,这就有点不现实,用户都很懒,用户每一个获取成本都很高,喂,你的token失效了,请你过来再认证一次。谁搭理你啊?对吧,这个用户可能就流失了。那么你怎么做,你为了安全你需要快速的把这个token给失效掉,然后你再获取一个新的 token,怎么获取?refresh_token。大致是下面这样的。

image.png

  • 然后服务器就会给你返回一个新的token,并且同时把你那个旧的让他失效。这个是refresh_token,他是肯定要https的。他的流程跟获取token的流程他都应该尽量发生在服务端。
收起阅读 »

Kotlin 源码 | 降低代码复杂度的法宝

随着码龄增大,渐渐意识到团队代码中的最大的敌人是“复杂度”。不合理的复杂度是降低代码质量,增加沟通成本的元凶。Kotlin 在降低代码复杂度方面有着诸多法宝。这一篇就以两个常见的业务场景来剖析下简单和复杂的关系。若要用一句话概括这关系,我最喜欢这一句:“一切简...
继续阅读 »

随着码龄增大,渐渐意识到团队代码中的最大的敌人是“复杂度”。不合理的复杂度是降低代码质量,增加沟通成本的元凶。

Kotlin 在降低代码复杂度方面有着诸多法宝。这一篇就以两个常见的业务场景来剖析下简单和复杂的关系。若要用一句话概括这关系,我最喜欢这一句:“一切简单的背后都蕴藏着复杂”。

启动线程和读取文件容是 Android 开发中两个颇为常见的场景。分别给出 Java 和 Kotlin 的实现,在惊叹两种语言表达力上悬殊的差距的同时,逐层剖析 Kotlin 语法简单背后的复杂。

启动线程

先看一个简单的业务场景,在 java 中用下面的代码启动一个新线程:

 Thread thread = new Thread() {
@Override
public void run() {
doSomething() // 业务逻辑
super.run();
}
};
thread.setDaemon(false);
thread.setPriority(-1);
thread.setName("thread");
thread.start();

启动线程是一个常用操作,其中除了 doSomething() 之外的其他代码都具有通用性。难道每次启动线程时都复制粘贴这一坨代码吗?不优雅!得抽象成一个静态方法以便到处调用:

public class ThreadUtil {
public static Thread startThread(Callback callback) {
Thread thread = new Thread() {
@Override
public void run() {
if (callback != null) callback.action();
super.run();
}
};
thread.setDaemon(false);
thread.setPriority(-1);
thread.setName("thread");
thread.start();
return thread;
}

public interface Callback {
void action();
}
}

仔细分析下这里引入的复杂度,一个新的类ThreadUtil及静态方法startThread(),还有一个新的接口Callback

然后就可以像这样构建线程了:

ThreadUtil.startThread( new Callback() {
@Override
public void action() {
doSomething();
}
})

对比下 Kotlin 的解决方案thread()

public fun thread(
start:
Boolean = true,
isDaemon:
Boolean = false,
contextClassLoader:
ClassLoader? = null,
name:
String? = null,
priority:
Int = -1,
block: () ->
Unit
)
: Thread {
val thread = object : Thread() {
public override fun run() {
block()
}
}
if (isDaemon)
thread.isDaemon = true
if (priority > 0)
thread.priority = priority
if (name != null)
thread.name = name
if (contextClassLoader != null)
thread.contextClassLoader = contextClassLoader
if (start)
thread.start()
return thread
}

thread()方法把构建线程的细节全都隐藏在方法内部。

然后就可以像这样启动一个新线程:

thread { doSomething() }

这简洁的背后是一系列语法特性的支持:

1. 顶层函数

Kotlin 中把定义在类体外,不隶属于任何类的函数称为顶层函数thread()就是这样一个函数。这样定义的好处是,可以在任意位置,方便地访问到该函数。

Kotlin 的顶层函数被编译成 java 代码后就变成一个类中的静态函数,类名是顶层函数所在文件名+Kt 后缀。

2. 高阶函数

若函数的参数或者返回值是 lambda 表达式,则称该函数为高阶函数

thread()方法的最后一个参数是 lambda 表达式。在 Kotlin 中当调用函数只传入一个 lambda 类型的参数时,可以省去括号。所以就有了thread { doSomething() }这样简洁的调用。

3. 参数默认值 & 命名参数

thread()函数包含了 6 个参数,为啥在调用时可以只传最后一个参数?因为其余的参数都在定义时提供了默认值。这个语法特性叫参数默认值

当然也可以忽略默认值,重新为参数赋值:

thread(isDaemon = true) { doSomething() }

当只想重新为某一个参数赋值时,不用将其余参数都重写一遍,只需用参数名 = 参数值,这个语法特性叫命名参数

逐行读取文件内容

再看一个稍复杂的业务场景:“读取文件中每一行的内容并打印”,用 Java 实现的代码如下:

File file = new File(path)
BufferedReader bufferedReader = null;
try {
bufferedReader = new BufferedReader(new InputStreamReader(new FileInputStream(file)));
String line;
// 循环读取文件中的每一行并打印
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
// 关闭资源
if (bufferedReader != null) {
try {
bufferedReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

对比一下 Kotlin 的解决方案:

File(path).readLines().foreach { println(it) }

一句话搞定,就算没学过 Kotlin 也能猜到这是在干啥,语义是如此简洁清晰。这样的代码写的时候畅快,读的时候悦目。

之所以简单,是因为 Kotlin 通过各种语法特性将复杂度分层并隐藏在了后背。

1. 扩展方法

拨开简单的面纱,探究背后隐藏的复杂:

// 为 File 扩展方法 readLines()
public fun File.readLines(charset: Charset = Charsets.UTF 8): List {
// 构建字符串列表
val result = ArrayList()
// 遍历文件的每一行并将内容添加到列表中
forEachLine(charset) { result.add(it) }
// 返回列表
return result
}

扩展方法是 Kotlin 在类体外给类新增方法的语法,它用类名.方法名()表达。

把 Kotlin 编译成 java,扩展方法就是新增了一个静态方法:

final class FilesKt  FileReadWriteKt {
// 静态函数的第一个参数是 File
public static final List readLines(@NotNull File $this$readLines, @NotNull Charset charset) {
Intrinsics.checkNotNullParameter($this$readLines, "$this$readLines");
Intrinsics.checkNotNullParameter(charset, "charset");
final ArrayList result = new ArrayList();
FilesKt.forEachLine($this$readLines, charset, (Function1)(new Function1() {
public Object invoke(Object var1) {
this.invoke((String)var1);
return Unit.INSTANCE;
}

public final void invoke(@NotNull String it) {
Intrinsics.checkNotNullParameter(it, "it");
result.add(it);
}
}));
return (List)result;
}
}

静态方法中的第一个参数是被扩展对象的实例,所以在扩展方法中可以通过this访问到类实例及其公共方法。

File.readLines() 的语义简单明了:遍历文件的每一行,将其添加到列表中并返回。

复杂度都被隐藏在了forEachLine(),它也是 File 的扩展方法,此处应该是this.forEachLine(charset) { result.add(it) },this 通常可以省略。forEachLine()是个好名字,一眼看去就知道是在遍历文件的每一行。

public fun File.forEachLine(charset: Charset = Charsets.UTF 8, action: (line: String) -> Unit): Unit {
BufferedReader(InputStreamReader(FileInputStream(this), charset)).forEachLine(action)
}

forEachLine()中将 File 层层包裹最终形成一个 BufferReader 实例,并且调用了 Reader 的扩展方法forEachLine()

public fun Reader.forEachLine(action: (String) -> Unit): Unit = 
useLines { it.forEach(action) }

forEachLine()调用了同是 Reader 的扩展方法useLines(),从名字细微的差别就可以看出uselines()完成了文件所有行内容的整合,而且这个整合的结果是可以被遍历的。

2. 泛型

哪个类能整合一组元素,并可以被遍历?沿着调用链继续往下:

public inline fun  Reader.useLines(block: (Sequence<String>) -> T): T =
buffered().use { block(it.lineSequence()) }

Reader 在useLines()中被缓冲化:

public inline fun Reader.buffered(bufferSize: Int = DEFAULT BUFFER SIZE): BufferedReader =
// 如果已经是 BufferedReader 则直接返回,否则再包一层
if (this is BufferedReader) this else BufferedReader(this, bufferSize)

紧接着调用了use(),使用 BufferReader:

// Closeable 的扩展方法
public inline fun T.use(block: (T) -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY ONCE)
}
var exception: Throwable? = null
try {
// 触发业务逻辑(扩展对象实例被传入)
return block(this)
} catch (e: Throwable) {
exception = e
throw e
} finally {
// 无论如何都会关闭资源
when {
apiVersionIsAtLeast(1, 1, 0) -> this.closeFinally(exception)
this == null -> {}
exception == null -> close()
else ->
try {
close()
} catch (closeException: Throwable) {}
}
}
}

这次的扩展函数不是一个具体类,而是一个泛型,并且该泛型的上界是Closeable,即为所有可以被关闭的类新增一个use()方法。

use()扩展方法中,lambda 表达式block代表了业务逻辑,扩展对象作为实参传入其中。业务逻辑在try-catch代码块中被执行,最后在finally中关闭了资源。上层可以特别省心地使用这个扩展方法,因为不再需要在意异常捕获和资源关闭。

3. 重载运算符 & 约定

读取文件内容的场景中,use() 中的业务逻辑是将BufferReader转换成LineSequence,然后遍历它。这里的遍历和类型转换分别是怎么实现的?

// 将 BufferReader 转化成 Sequence
public fun BufferedReader.lineSequence(): Sequence =
LinesSequence(this).constrainOnce()

还是通过扩展方法,直接构造了LineSequence对象并将BufferedReader传入。这种通过组合方式实现的类型转换和装饰者模式颇为类似(关于装饰者模式的详解可以点击使用组合的设计模式 | 美颜相机中的装饰者模式

LineSequence 是一个 Sequence:

// 序列
public interface Sequence<out T> {
// 定义如何构建迭代器
public operator fun iterator(): Iterator
}

// 迭代器
public interface Iterator<out T> {
// 获取下一个元素
public operator fun next(): T
// 判断是否有后续元素
public operator fun hasNext(): Boolean
}

Sequence是一个接口,该接口需要定义如何构建一个迭代器iterator。迭代器也是一个接口,它需要定义如何获取下一个元素及是否有后续元素。

2 个接口中的 3 个方法都被保留词operator修饰,它表示重载运算符,即重新定义运算符的语义。Kotlin 中预定义了一些函数名和运算符的对应关系,称为约定。当前这个约定就是iterator() + next() + hasNext()for循环的约定。

for 循环在 Kotlin 中被定义为“遍历迭代器提供的元素”,需要和in保留词一起使用:

public inline fun  Sequence.forEach(action: (T) -> Unit): Unit {
for (element in this) action(element)
}

Sequence 有一个扩展法方法forEach()来简化遍历语法,内部就使用了“for + in”来遍历序列中所有的元素。

所以才可以在Reader.forEachLine()中用如此简单的语法实现遍历文件中的所有行。

public fun Reader.forEachLine(action: (String) -> Unit): Unit = 
useLines { it.forEach(action) }

关于 Sequence 的用法实例可以点击Kotlin 基础 | 望文生义的 Kotlin 集合操作

LineSequence 的语义是 Sequence 中每一个元素都是文件中的一行,它在内部实现iterator()接口,构造了一个迭代器实例:

// 行序列:在 BufferedReader 外面包一层 LinesSequence
private class LinesSequence(private val reader: BufferedReader) : Sequence {
override public fun iterator(): Iterator {
// 构建迭代器
return object : Iterator {
private var nextValue: String? = null // 下一个元素值
private var done = false // 迭代是否结束

// 判断迭代器中是否有下一个元素,并顺便获取下一个元素存入 nextValue
override public fun hasNext(): Boolean {
if (nextValue == null && !done) {
// 下一个元素是文件中的一行内容
nextValue = reader.readLine()
if (nextValue == null) done = true
}
return nextValue != null
}

// 获取迭代器中下一个元素
override public fun next(): String {
if (!hasNext()) {
throw NoSuchElementException()
}
val answer = nextValue
nextValue = null
return answer!!
}
}
}
}

LineSequence 内部的迭代器在hasNext()中获取了文件中一行的内容,并存储在nextValue中,完成了将文件中每一行的内容转换成 Sequence 中的一个元素。

当在 Sequence 上遍历时,文件中每一行的内容就一个个出现在迭代中。这样做的好处是对内存更加友好,LineSequence 并没有持有文件中所有行的内容,它只是定义了如何获取文件中下一行的内容,所有的内容只有等待遍历时,才一个个地浮现出来。

用一句话总结 Kotlin 逐行读取文件内容的算法:用缓冲流(BufferReader)包裹文件,再用行序列(LineSequence)包裹缓冲流,序列迭代行为被定义为读取文件中一行的内容。遍历序列时,文件内容就一行行地被添加到列表中。

总结

顶层函数、高阶函数、默认参数、命名参数、扩展方法、泛型、重载运算符,Kotlin 利用了这些语法特性隐藏了实现常用业务功能的复杂度,并且在内部将复杂度分层。

分层是降低复杂度的惯用手段,它不仅让复杂度分散,使得同一时刻只需面对有限的复杂度,并且可以通过对每一层取一个好名字来概括本层的语义。除此之外,它还有助于定位问题(缩小问题范围)并增加代码可复用性(每层单独复用)。

是不是也可以效仿这种分层的思想方法,在写代码之前,琢磨一下,复杂度是不是太高了?可以运用那些语言特性实现合理的抽象将复杂度分层?以避免复杂度在一个层次被铺开。

收起阅读 »

Android内存优化工具

整理下Android内存优化常用的几种工具,top命令、adb shell dumpsys meminfo、Memory Profiler、LeakCanary、MAT1. toptop命令是Linux下常用的性能分析工具,能够实时显示系统中各个进程的资源占用...
继续阅读 »

整理下Android内存优化常用的几种工具,top命令、adb shell dumpsys meminfo、Memory Profiler、LeakCanary、MAT

1. top

top命令是Linux下常用的性能分析工具,能够实时显示系统中各个进程的资源占用状况。

查看top命令的用法

$ adb shell top --help
usage: top [-Hbq] [-k FIELD,] [-o FIELD,] [-s SORT] [-n NUMBER] [-m LINES] [-d SECONDS] [-p PID,] [-u USER,]

Show process activity in real time.

-H Show threads
-k Fallback sort FIELDS (default -S,-%CPU,-ETIME,-PID)
-o Show FIELDS (def PID,USER,PR,NI,VIRT,RES,SHR,S,%CPU,%MEM,TIME+,CMDLINE)
-O Add FIELDS (replacing PR,NI,VIRT,RES,SHR,S from default)
-s Sort by field number (1-X, default 9)
-b Batch mode (no tty)
-d Delay SECONDS between each cycle (default 3)
-m Maximum number of tasks to show
-n Exit after NUMBER iterations
-p Show these PIDs
-u Show these USERs
-q Quiet (no header lines)

Cursor LEFT/RIGHT to change sort, UP/DOWN move list, space to force
update, R to reverse sort, Q to exit.

使用top命令显示一次进程信息,以便讲解进程信息中各字段的含义

^[[41;173RTasks: 754 total,   1 running, 753 sleeping,   0 stopped,   0 zombie
Mem: 5.5G total, 5.4G used, 165M free, 76M buffers
Swap: 2.5G total, 789M used, 1.7G free, 2.4G cached
800%cpu 100%user 3%nice 54%sys 641%idle 0%iow 3%irq 0%sirq 0%host
PID USER PR NI VIRT RES SHR S[%CPU] %MEM TIME+ ARGS
15962 u0 a894 10 -10 6.6G 187M 76M S 75.6 3.2 8:16.55 asia.bluepay.cl+
785 system -2 -8 325M 13M 7.6M S 29.7 0.2 84:03.91 surfaceflinger
25255 shell 20 0 35M 2.7M 1.6M R 21.6 0.0 0:00.16 top -n 1
739 system -3 -8 177M 3.6M 2.2M S 10.8 0.0 16:00.36 android.hardwar+
16154 u0 i9086 10 -10 1.3G 40M 19M S 5.4 0.6 0:46.18 com.google.andr+
13912 u0 a87 20 0 17G 197M 86M S 5.4 3.4 23:56.88 com.tencent.mm
24789 root RT -2 0 0 0 D 2.7 0.0 0:01.36 [mdss fb0]
24704 root 20 0 0 0 0 S 2.7 0.0 0:01.20 [kworker/u16:12]
20096 u0 a94 30 10 6.1G 137M 53M S 2.7 2.3 0:31.45 com.xiaomi.mark+
2272 system 18 -2 8.7G 407M 267M S 2.7 7.1 191:11.32 system server
744 system RT 0 1.3G 1.6M 1.4M S 2.7 0.0 72:22.41 android.hardwar+
442 root RT 0 0 0 0 S 2.7 0.0 5:59.68 [cfinteractive]
291 root -3 0 0 0 0 S 2.7 0.0 5:00.17 [kgsl worker th+
10 root 20 0 0 0 0 S 2.7 0.0 1:55.84 [rcuop/0]
7 root 20 0 0 0 0 S 2.7 0.0 2:46.82 [rcu preempt]
25186 shell 20 0 34M 1.9M 1.4M S 0.0 0.0 0:00.71 logcat -v long +
25181 root 20 0 0 0 0 S 0.0 0.0 0:00.00 [kworker/2:3]
25137 root 20 0 0 0 0 S 0.0 0.0 0:00.00 [kworker/1:3]
25118 system 20 0 5.2G 83M 54M S 0.0 1.4 0:01.05 com.android.set+
24946 u0 a57 20 0 5.1G 60M 37M S 0.0 1.0 0:00.82 com.xiaomi.acco+
复制代码
第 1 行:进程信息
  • 总共(total):754个
  • 运行中(running)状态:1个
  • 休眠(sleeping)状态:753个
  • 停止(stopped)状态:0个
  • 僵尸(zombie)状态:0个
第 2 行:内存信息
  • 5.5G total:物理内存总量
  • 5.4G used:使用中的内存量
  • 165M free:空闲内存量
  • 76M buffers: 缓存的内存量
第 3 行:Swap分区信息
  • 2.5G total:交换区总量
  • 789M used:使用的交换区大小
  • 1.7G free:空闲交换区大小
  • 2.4G cached:缓冲的交换区大小

内存监控时,可以监控swap交换分区的used,如果这个数值在不断的变化,说明内核在不断进行内存和swap的数据交换,这是内存不够用了。

第 4 行:CPU信息
  • 800%cpu:8核cpu
  • 100%user:用户进程使用CPU占比
  • 3%nice:优先值为负的进程占比
  • 54%sys:内核进程使用CPU占比
  • 641%idle:除IO等待时间以外的其它等待时间占比
  • 0%iow:IO等待时间占比
  • 3%irq:硬中断时间占比
  • 0%sirq:软中断时间占比
第 5 行及以下:各进程的状态监控
  • PID:进程id
  • USER:进程所属用户
  • PR:进程优先级
  • NI:nice值,负值表示高优先级,正值表示低优先级
  • VIRT:进程使用的虚拟内存总量,VIRT=SWAP+RES
  • RES:进程使用的、未被换出的物理内存大小,RES=CODE+DATA
  • SHR:共享内存大小
  • S:进程状态
  • %CPU:上次更新到现在的CPU占用时间比
  • %MEM:使用物理内存占比
  • TIME+:进程时间的CPU时间总计,单位1/100秒
  • ARGS:进程名

2. dumpsys meminfo

首先了解下Android中最重要的四大内存指标的概念

指标全称含义等价
USSUnique Set Size独占物理内存进程独占的内存
PSSProportional Set Size实际使用物理内存PSS = USS + 按比例包含共享库内存
RSSResident Set Size实际使用物理内存RSS = USS + 包含共享库内存
VSSVirtual Set Size虚拟耗用内存VSS = 进程占用内存(包括虚拟耗用) + 共享库(包括比例分配部分)

我们主要使用USS和PSS来衡量进程的内存使用情况

dumpsys meminfo命令展示的是系统整体内存情况,内存项按进程进行分类

$ adb shell dumpsys meminfo
Applications Memory Usage (in Kilobytes):
Uptime: 168829244 Realtime: 1465769995

// 根据进程PSS占用值从大到小排序
Total PSS by process:
272,029K: system (pid 2272)
234,043K: com.tencent.mm (pid 13912 / activities)
185,914K: com.android.systemui (pid 13606)
107,294K: com.tencent.mm:appbrand0 (pid 5563)
101,526K: com.tencent.mm:toolsmp (pid 9287)
96,645K: com.miui.home (pid 15116 / activities)
...

// 以oom来划分,会详细列举所有的类别的进程
Total PSS by OOM adjustment:
411,619K: Native
62,553K: android.hardware.camera.provider@2.4-service (pid 730)
21,630K: logd (pid 579)
16,179K: surfaceflinger (pid 785)
...
272,029K: System
272,029K: system (pid 2272)
361,942K: Persistent
185,914K: com.android.systemui (pid 13606)
37,917K: com.android.phone (pid 2836)
23,510K: com.miui.contentcatcher (pid 3717)
...
36,142K: Persistent Service
36,142K: com.android.bluetooth (pid 26472)
101,198K: Foreground
72,743K: com.miui.securitycenter.remote (pid 4125)
28,455K: com.android.settings (pid 30919 / activities)
338,088K: Visible
96,645K: com.miui.home (pid 15116 / activities)
46,939K: com.miui.personalassistant (pid 31043)
36,491K: com.xiaomi.xmsf (pid 4197)
...
47,703K: Perceptible
17,826K: com.xiaomi.metoknlp (pid 4477)
10,748K: com.lbe.security.miui (pid 5097)
10,528K: com.xiaomi.location.fused (pid 4563)
8,601K: com.miui.mishare.connectivity (pid 4227)
13,088K: Perceptible Low
13,088K: com.miui.analytics (pid 19306)
234,043K: Backup
234,043K: com.tencent.mm (pid 13912 / activities)
22,028K: A Services
22,028K: com.miui.powerkeeper (pid 29762)
198,787K: Previous
33,375K: com.android.quicksearchbox (pid 31023)
23,278K: com.google.android.webview:sandboxed process0:org.chromium.content.app.SandboxedProcessService0:0 (pid 16154)
171,434K: B Services
45,962K: com.tencent.mm:push (pid 14095)
31,514K: com.tencent.mobileqq:MSF (pid 12051)
22,691K: com.xiaomi.mi connect service (pid 22821)
...
538,062K: Cached
107,294K: com.tencent.mm:appbrand0 (pid 5563)
101,526K: com.tencent.mm:toolsmp (pid 9287)
72,112K: com.tencent.mm:tools (pid 9187)
...

// 按内存的类别来进行划分
Total PSS by category:
692,040K: Native
328,722K: Dalvik
199,826K: .art mmap
129,981K: .oat mmap
126,624K: .dex mmap
124,509K: Unknown
92,666K: .so mmap
68,189K: Dalvik Other
53,491K: .apk mmap
44,104K: Gfx dev
28,099K: Other mmap
24,960K: .jar mmap
7,956K: Ashmem
3,700K: Stack
3,368K: Other dev
450K: .ttf mmap
4K: Cursor
0K: EGL mtrack
0K: GL mtrack
0K: Other mtrack

// 手机整体内存使用情况
Total RAM: 5,862,068K (status normal)
Free RAM: 3,794,646K ( 538,062K cached pss + 3,189,244K cached kernel + 0K cached ion + 67,340K free)
Used RAM: 2,657,473K (2,208,101K used pss + 449,372K kernel)
Lost RAM: 487,987K
ZRAM: 219,996K physical used for 826,852K in swap (2,621,436K total swap)
Tuning: 256 (large 512), oom 322,560K, restore limit 107,520K (high-end-gfx)
复制代码

查看单个进程的内存信息,命令如下

adb shell dumpsys meminfo [pid | packageName]
复制代码

我们查看下微信的内存信息

$ adb shell dumpsys meminfo com.tencent.mm
Applications Memory Usage (in Kilobytes):
Uptime: 169473031 Realtime: 1466413783

** MEMINFO in pid 13912 [com.tencent.mm] **
Pss Private Private SwapPss Heap Heap Heap
Total Dirty Clean Dirty Size Alloc Free
------ ------ ------ ------ ------ ------ ------
Native Heap 51987 51924 0 61931 159044 139335 19708
Dalvik Heap 74302 74272 8 2633 209170 184594 24576
Dalvik Other 10136 10136 0 290
Stack 84 84 0 8
Ashmem 2 0 0 0
Gfx dev 8808 8808 0 0
Other dev 156 0 156 0
.so mmap 9984 984 7436 8493
.jar mmap 1428 0 560 0
.apk mmap 2942 0 1008 0
.ttf mmap 1221 0 1064 0
.dex mmap 31302 44 30004 528
.oat mmap 2688 0 232 0
.art mmap 2792 2352 40 3334
Other mmap 6932 2752 632 0
Unknown 4247 4232 4 7493
TOTAL 293721 155588 41144 84710 368214 323929 44284

App Summary
Pss(KB)
------
Java Heap: 76664
Native Heap: 51924
Code: 41332
Stack: 84
Graphics: 8808
Private Other: 17920
System: 96989

TOTAL: 293721 TOTAL SWAP PSS: 84710

Objects
Views: 623 ViewRootImpl: 1
AppContexts: 9 Activities: 1
Assets: 12 AssetManagers: 0
Local Binders: 198 Proxy Binders: 183
Parcel memory: 46 Parcel count: 185
Death Recipients: 125 OpenSSL Sockets: 1
WebViews: 0

SQL
MEMORY USED: 156
PAGECACHE OVERFLOW: 13 MALLOC SIZE: 117

DATABASES
pgsz dbsz Lookaside(b) cache Dbname
4 28 46 721/26/4 /data/user/0/com.tencent.mm/databases/Scheduler.db

Asset Allocations
: 409K
: 12K
: 1031K
复制代码
  1. App Summary各项指标解读如下,通常我们需要重点关注Java Heap和Native Heap的大小,如果持续上升,有可能存在内存泄露。
属性内存组成
Java HeapDalvik Heap的Private Dirty + .art mmap的Private Dirty&Private Clean
Native HeapNative Heap的Private Dirty
Code.so mmap + .jar mmap + .apk mmap + .ttf.mmap + .dex.mmap + .oat mmap的Private Dirty&Private Clean
StackStack的Private Dirty
GraphicsGfx dev + EGL mtrack + GL mtrack的Private Dirty&Private Clean
  1. Objects中Views、Activities、AppContexts的异常可以判断有内存泄露,比如刚退出应用,查看Activites是否为0,如果不为0,则有Activity没有销毁。

3. Memory Profiler

Memory Profiler是 Android Profiler 中的一个组件,实时图表展示应用内存使用量,识别内存泄露和抖动,提供捕获堆转储,强制GC以及跟踪内存分配的能力。

Android Profiler官方文档

4. Leak Canary

非常好用的内存泄露检测工具,对于Activity/Fragment的内存泄露检测非常方便。

Square公司开源 官网地址,原理后面单独分析。

5. MAT

MAT是Memory Analyzer tool的缩写,是一个非常全面的分析工具,使用相对复杂点。 关于安装和配置有很多很好的文章结束,这里就不单独讲了,后面分析具体案例。

Android 内存优化篇 - 使用profile 和 MAT 工具进行内存泄漏检测

使用Android Studio和MAT进行内存泄漏分析

内存问题高效分析方法

  1. 接入LeakCanary,监控所有Activity和Fragment的释放,App所有功能跑一遍,观察是否有抓到内存泄露的地方,分析引用链找到并解决问题,如此反复,直到LeakCanary检查不到内存泄露。
  2. adb shell dumpsys meminfo命令查看退出界面后Objects的Views和Activities数目,特别是退出App后数目为否为0。
  3. 打开Android Studio Memory Profiler,反复打开关闭页面多次,点击GC,如果内存没有恢复到之前的数值,则可能发生了内存泄露。再点击Profiler的垃圾桶图标旁的heap dump按钮查看当面内存堆栈情况,按包名找到当前测试的Activity,如果存在多份实例,则很可能发生了内存泄露。
  4. 对于可疑的页面dump出内存快照文件,转换后用MAT打开,针对性的分析。
  5. 观察Memory Profiler每个页面打开时的内存波峰和抖动情况,针对性分析。
  6. 开发者选项中打开“不保留后台活动”,App运行一段时间后退到后台,触发GC,dump内存快照。MAT分析静态内容是否有可以优化的地方,比如图片缓存、单例、内存缓存等。
收起阅读 »

Android字体系列 (四):全局替换字体方式

前言 很高兴遇见你~ 在本系列的上一篇文章中,我们了解了 Xml 中的字体,还没有看过上一篇文章的朋友,建议先去阅读Android字体系列 (三):Xml中的字体,有了前面的基础,接下来我们就看下 Android 中全局替换字体的几种方式 注意:本文所展...
继续阅读 »

前言


很高兴遇见你~


在本系列的上一篇文章中,我们了解了 Xml 中的字体,还没有看过上一篇文章的朋友,建议先去阅读

Android字体系列 (三):Xml中的字体

,有了前面的基础,接下来我们就看下 Android 中全局替换字体的几种方式


注意:本文所展示的系统源码都是基于Android-30 ,并提取核心部分进行分析


Github Demo 地址 , 大家可以看 Demo 跟随我的思路一起分析


一、方式一:通过遍历 ViewTree,全局替换字体


之前我讲过:在 Android 中,我们一般会直接或间接的通过 TextView 控件去承载字体的显示,因为关于 Android 提供的承载字体显示的控件都会直接或间接继承 TextView。


那么这就是一个突破口:我们可以在 Activity 或 Fragment 的基类里面获取当前布局的 ViewTree,遍历 ViewTree ,获取 TextView 及其子类,批量修改它们的字体,从而达到全局替换字体的效果。


代码如下:


//全局替换字体工具类
object ChangeDefaultFontUtils {

private const val NOTO_SANS_BOLD = R.font.noto_sans_bold
/**
* 方式一: 遍历布局的 ViewTree, 找到 TextView 及其子类进行批量替换
*
*
@param mContext 上下文
*
@param rootView 根View
*/

fun changeDefaultFont(mContext: Context?, rootView: View?){
when(rootView){
is ViewGroup -> {
rootView.forEach {
changeDefaultFont(mContext,it)
}
}
is TextView -> {
try {
val typeface = ResourcesCompat.getFont(mContext!!, NOTO_SANS_BOLD)
val fontStyle = rootView.typeface?.style ?: Typeface.NORMAL
rootView.setTypeface(typeface,fontStyle)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}
}

//Activity 基类
abstract class BaseActivity: AppCompatActivity(){

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val mRootView = LayoutInflater.from(this).inflate(getLayoutId(), null)
setContentView(mRootView)
ChangeDefaultFontUtils.changeDefaultFont(this,mRootView)
initView()
}

/**获取布局Id*/
abstract fun getLayoutId(): Int

/**初始化*/
abstract fun initView()
}

//MainActivity
class MainActivity : BaseActivity() {

override fun getLayoutId(): Int {
return R.layout.activity_main
}

override fun initView() {

}
}

上述代码:


1、创建了一个全局替换字体的工具类,主要逻辑:


判断当前 rootView 是否是一个 ViewGroup,如果是,遍历取出其所有的子 View,然后递归调用 changeDefaultFont 方法。再判断是否是 TextView 或其子类,如果是就替换字体


2、创建了一个 Activity 基类,并在其中写入字体替换的逻辑


3、最后让上层 Activity 继承基类 Activity


逻辑很简单,在看下我们编写的 Xml 的一个效果:


image-20210616144417422


接下来我们运行看下实际替换后的一个效果:


image-20210616144927196

可以看到,字体被替换了。


现在我们来讨论一下这种方式的优缺点:


优点:我们不需要修改 Xml 布局,不需要重写多个控件,只需要在 inflate View 之后调一下就可以了


缺点:不难发现这种方式会遍历 Xml 文件中的所有 View 和 ViewGroup,但是如果出现 RecyclerView , ListView,或者其他 ViewGroup 里面动态添加 View,那么我们还是需要去手动添加替换的逻辑,否则字体不会生效。而且它每次递归遍历 ViewTree,性能上多少会有点影响


接下来我们看第二种方式


二、方式二:通过 LayoutInflater,全局替换字体


讲这种方式前,我们首先要对 LayoutInflater 的 inflate 过程有一定的了解,以 AppCompatActivity 的 setContentView 为例大致说下流程:


我们在 Activity 的 setContentView 中传入一个布局 Xml,Activity 会通过代理类 AppCompatDelegateImpl 把它交由 LayoutInflater 进行解析,解析出来后,会交由自己的 3 个工厂去创建 View,优先级分别是mFactory2、mFactory、mPrivateFactory


流程大概就说到这里,具体过程我后续会写一篇文章专门去讲。


mFactory2、mFactory ,系统提供了开放的 Api 给我们去设置,如下:


//以下两个方法在 LayoutInflaterCompat.java 文件中
@Deprecated
public static void setFactory(@NonNull LayoutInflater inflater, @NonNull LayoutInflaterFactory factory) {
if (Build.VERSION.SDK_INT >= 21) {
inflater.setFactory2(factory != null ? new Factory2Wrapper(factory) : null);
} else {
final LayoutInflater.Factory2 factory2 = factory != null
? new Factory2Wrapper(factory) : null;
inflater.setFactory2(factory2);

final LayoutInflater.Factory f = inflater.getFactory();
if (f instanceof LayoutInflater.Factory2) {
forceSetFactory2(inflater, (LayoutInflater.Factory2) f);
} else {
forceSetFactory2(inflater, factory2);
}
}
}

public static void setFactory2(@NonNull LayoutInflater inflater, @NonNull LayoutInflater.Factory2 factory) {
inflater.setFactory2(factory);

if (Build.VERSION.SDK_INT < 21) {
final LayoutInflater.Factory f = inflater.getFactory();
if (f instanceof LayoutInflater.Factory2) {
forceSetFactory2(inflater, (LayoutInflater.Factory2) f);
} else {
forceSetFactory2(inflater, factory);
}
}
}

这两个方法在 LayoutInflaterCompat 这个类中,LayoutInflaterCompat 是 LayoutInflater 一个辅助类,可以看到:


1、setFactory 方法使用了 @Deprecated 注解表示这个 Api 被弃用


2、setFactory2 是 Android 3.0 引入的,它和 setFactory 功能是一致的,区别就在于传入的接口参数不一样,setFactory2 的接口参数要多实现一个方法


利用 setFactory 系列方法,我们可以:


1)、拿到 LayoutInflater inflate 过程中 Xml 控件对应的名称和属性


2)、我们可以对控件进行替换或者做相关的逻辑处理


看个实际例子:还是方式一的代码,我们在 BaseActivity 中增加如下代码:


//Activity 基类
abstract class BaseActivity: AppCompatActivity(){

//新增部分
private val TAG: String? = javaClass.simpleName

override fun onCreate(savedInstanceState: Bundle?) {
//...
//新增部分,其余代码省略
LayoutInflaterCompat.setFactory2(layoutInflater,object : LayoutInflater.Factory2{
override fun onCreateView(parent: View?, name: String, context: Context, attrs: AttributeSet
)
: View? {
Log.d(TAG, "name: $name" )
for (i in 0 until attrs.attributeCount){
Log.d(TAG, "attr: ${attrs.getAttributeName(i)} ${attrs.getAttributeValue(i)}")
}
return null
}

override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
return null
}

})
super.onCreate(savedInstanceState)
//...
}

//...
}

注意:上面 LayoutInflaterCompat.setFactory2 方法必须放在 super.onCreate(savedInstanceState) 的前面,不然会报错,因为系统会在 AppCompatActivity 的 oncreate 方法给 LayoutInflater 设置一个 Factory,而如果在已经设置的情况下再去设置,LayoutInflater 的 setFactory 系列方法就会抛异常,源码如下:


//AppCompatActivity 的 oncreate
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
final AppCompatDelegate delegate = getDelegate();
//调用 AppCompatDelegateImpl 的 installViewFactory 设置 Factory
delegate.installViewFactory();
//...
}

//AppCompatDelegateImpl 的 installViewFactory
@Override
public void installViewFactory() {
LayoutInflater layoutInflater = LayoutInflater.from(mContext);
if (layoutInflater.getFactory() == null) {
//如果当前 LayoutInflater 的 Factory 为空,则进行设置
LayoutInflaterCompat.setFactory2(layoutInflater, this);
} else {
//如果不为空,则进行 Log 日志打印
if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImpl)) {
Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
+ " so we can not install AppCompat's");
}
}
}

//LayoutInflater 的 setFactory2
public void setFactory2(Factory2 factory) {
//如果已经设置,则抛异常
if (mFactorySet) {
throw new IllegalStateException("A factory has already been set on this LayoutInflater");
}
if (factory == null) {
throw new NullPointerException("Given factory can not be null");
}
mFactorySet = true;
//...
}

注意:上面 AppCompatActivity 中设置 Factory 是 android.appcompat 1.1.0 版本,而如果是更高的版本,如 1.3.0,可能设置的地方会有点变化,但是不影响我们设置位置的变化,感兴趣的可以去看下源码,这里你只要知道我们必须在 Activity 的 super.onCreate(savedInstanceState) 之前设置 Factory 就可以了


运行应用程序,看下几个主要控件的截图打印信息:


image-20210616150016885

从 Log 输出可以看出,你所有的 Xml 控件,都会经过 LayoutInflaterFactory.onCreateView 方法走一遍去实现初始化的过程,在其中可以有效的分辨出是什么控件,以及它有什么属性。并且 onCreateView 方法的返回值就是一个 View,因此我们在此处可以对控件进行替换或者做相关的逻辑处理


到这里,你是否有了全体替换字体的思路了呢?


答案已经很明了:利用自定义的 Factory 进行字体的替换


这种方式我们只需要在 BaseActivity 里面操作就可以了,而且有效的解决了方式一带来的问题,提高了效率,如下:


abstract class BaseActivity: AppCompatActivity(){

override fun onCreate(savedInstanceState: Bundle?) {
LayoutInflaterCompat.setFactory2(layoutInflater,object : LayoutInflater.Factory2{
override fun onCreateView(parent: View?, name: String, context: Context, attrs: AttributeSet
)
: View? {
var view: View? = null
if(1 == name.indexOf(".")){
//表示自定义 View
//通过反射创建
view = layoutInflater.createView(name,null,attrs)
}

if(view == null){
//通过系统创建一系列 appcompat 的 View
view = delegate.createView(parent, name, context, attrs)
}

if(view is TextView){
//如果是 TextView 或其子类,则进行字体的替换
ChangeDefaultFontUtils.changeDefaultFont(this@BaseActivity,view)
}

return view
}

override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
return null
}

})
super.onCreate(savedInstanceState)
setContentView(getLayoutId())
initView()
}

/**获取布局Id*/
abstract fun getLayoutId(): Int

/**初始化*/
abstract fun initView()
}

上述代码我们做了:


1、判断是自定义 View ,通过反射创建


2、判断是系统提供的一些控件,使用 appcompat 系列 View 进行替换


3、判断是 TextView 或其子类,进行字体的替换


运行应用程序,最终实现了和方式一一样的效果:


image-20210616144927196

三、方式三:通过配置应用主题,全局替换默认字体


这种方式挺简单的,在 application 中,通过 android:theme 来配置一个 App 的主题。一般新创建的项目,都是会有一个默认基础主题。在其中追加关于字体的属性,就可以完成全局默认字体的替换,在主题中我们可以对以下三个属性进行配置:


 <item name="android:typeface">item>
<item name="android:fontFamily">item>
<item name="android:textStyle">item>

这三者的设置和关系我们在本系列的第一篇文章中已经讲过,还不清楚的可以去看下 传送门


关于 Xml 中使用字体的功能,我们上篇文章也已经讲过,还不清楚的可以去看下 传送门


因为我们只需要配置默认字体,所以新增一行如下配置,就可以实现全局替换默认字体的效果了:


<style name="Theme.ChangeDefaultFontDemo" parent="Theme.MaterialComponents.DayNight.DarkActionBar.Bridge">
//...
<item name="android:fontFamily">@font/noto_sans_bolditem>

//...
style>

那么凡事都有意外,假如你的 Activity 引用了自定义主题,且自定义主题没有继承基础主题,那么你就需要补上这一行配置,不然配置的默认字体不会生效


四、方式四:通过反射,全局替换默认字体


通过反射修改,其实和方式三有点类似。因为在 Android Support Library 26 之前,我们不能直接在 Xml 中设置第三方字体,而只能设置系统提供的一些默认字体,所以通过反射这种方式,可以把系统默认的字体替换为第三方的字体。而现在我们使用的版本基本上都会大于等于 26,因此通过配置应用主题的方式就可以实现全局替换默认字体的效果。但是这里并不妨碍我们讲反射修改默认字体。


1、步骤一:在 App 的主题配置默认字体


<style name="Theme.ChangeDefaultFontDemo" parent="Theme.MaterialComponents.DayNight.DarkActionBar.Bridge">
//...
<item name="android:typeface">serifitem>

//...
style>

这里随便选一个默认字体,后续我们反射的时候需要拿到你这个选的默认字体,然后进行一个替换


注意: 这里必须配置 android:typeface ,其他两个不行,在本系列的第一篇中,关于 typeface,textStyle 和 fontFamily 属性三者的关系我们分析过,还不清楚的可以去看看 传送门


setTypefaceFromAttrs 方法是 TextView 最终设置字体的方法,当 typeface 和 familyName 都为空,则会根据 typefaceIndex 的值取相应的系统默认字体。当我们设置 android:typeface 属性时,会将对应的属性值赋给 typefaceIndex ,并把 familyName 置为 null,而 typeface 默认为 null,因此满足条件


2、通过反射修改 Typeface 默认字体


注意:Google 在 Android 9.0 及之后对反射做了限制,被使用 @hide 标记的属性和方法通过反射拿不到


在 Typeface 中,自带的一些默认字体被标记的是 public static final,因此这里无需担心反射的限制


image-20210618174439624


因为在上一步配置的主题中,我们设置的是 serif ,所以这里替换它就好了,完整的方法就是通过反射拿到 Typeface 的默认字体 SERIF,然后使用反射将它修改成我们需要的字体即可:


object ChangeDefaultFontUtils {
const val NOTO_SANS_BOLD = R.font.noto_sans_bold

fun changeDefaultFont(mContext: Context) {
try {
val typeface = ResourcesCompat.getFont(mContext, NOTO_SANS_BOLD)
val defaultField = Typeface::class.java.getDeclaredField("SERIF")
defaultField.isAccessible = true
defaultField[null] = typeface
} catch (e: Exception) {
e.printStackTrace()
}
}
}

3、在 Application 里面,调用替换的方法


class MyApplication : Application() {

override fun onCreate() {
super.onCreate()
ChangeDefaultFontUtils.changeDefaultFont(this)
}
}

那么经过上面的三个步骤,我们同样可以实现全局替换默认字体的效果


五、项目实践


回到我们剩下的需求:全局替换默认字体


1、方式一和方式二都是全局替换字体,会将我们之前已经设置好的字体给覆盖,因此并不适合


2、方式三和方式四都是全局替换默认字体,我们之前已经设置好的字体不会被覆盖,满足我们的要求,但是方式四通过反射,是因为之前我们不能直接在 Xml 里面设置第三方字体。从 Android Support Library 26 及之后支持在 Xml 里面设置默认字体了,因此我在项目实践中,最终选择了方式三实现了全局替换默认字体的效果,需求完结 ?


六、总结


最后回顾一下我们讲的重点知识:


1、通过遍历 ViewTree,全局替换字体,这种方式每次都需要递归遍历,有性能问题


2、通过 LayoutInflater 设置自定义 Factory 全局替换字体,效率高


3、通过配置应用主题全局替换默认字体,简单高效


4、通过反射全局替换默认字体,相对于 3,性能会差点,使用步骤也相对复杂


5、我在项目实践过程中的一个选择


好了,本系列文章到这里就结束了,希望能给你带来帮助 ?


感谢你阅读这篇文章


参考和推荐


全局修改默认字体,通过反射也能做到


作者:sweetying
链接:https://juejin.cn/post/6975333037240221727
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android字体系列 (三):Xml中的字体

前言 很高兴遇见你~ 在本系列的上一篇文章中,我们对 Typeface 进行了深入的解析,还没有看过上一篇文章的朋友,建议先去阅读 Android字体系列(二):Typeface完全解析。接下来我们看下 Google 推出的 Xml 中使用字体 ...
继续阅读 »

前言


很高兴遇见你~


在本系列的上一篇文章中,我们对 Typeface 进行了深入的解析,还没有看过上一篇文章的朋友,建议先去阅读 

Android字体系列(二):Typeface完全解析

。接下来我们看下 Google 推出的 Xml 中使用字体


一、Xml 中字体介绍


Google 在 Android Support Library 26 引入了 Xml 中设置字体这项新功能,它可以让你将字体当成资源去使用,你可以在 res/font/ 文件夹中添加 font 文件,将字体捆绑为资源。这些字体会在 R 文件中编译,可直接在 Android Studio 中使用,如:


@font/myfont 
R.font.myfont

注意:要使用 Xml 字体功能,需引入 Android Support Library 26 及更高版本且要在 Android 4.1 及更高版本的设备


二、使用步骤


1、右键点击 res 文件夹,然后转到 New > Android resource directory


2、在 Resource type 列表中,选择 font,然后点击 OK


image-20210616203615018

3、在 font 文件夹中添加字体文件



关于字体,推荐两个免费下载的网站


fonts.google.com/


http://www.1001freefonts.com/



image-20210616203940427

添加之后就会生成 R.font.ma_shan_zhenng_regular 和 R.font.noto_sans_bold


4、双击字体文件可预览当前字体


image-20210616204148155


以上 4 个步骤完成后我们就可以在 Xml 中使用字体了


5、创建 font family


1)、右键点击 font 文件夹,然后转到 New > Font resource file。此时将显示 New Resource File 窗口。


2)、输入文件名,然后点击 OK。新的字体资源 Xml 会在编辑器中打开。


3)、将各个字体文件、样式和粗细属性都封装在 元素中。如下:



<font-family xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
tools:ignore="UnusedAttribute">


<font
android:fontStyle="normal"
android:fontWeight="400"
android:font="@font/ma_shan_zheng_regular"
tools:ignore="UnusedAttribute" />


<font
android:fontStyle="normal"
android:fontWeight="400"
android:font="@font/noto_sans_bold"
/>

font-family>

实践发现使用 font family 存在一些坑:


1、例如我上面添加了两个 font 标签,这个时候在 Xml 里面引用将不会有任何效果,而且设置的 fontStyle 等属性不会生效。


2、当只添加了一个 font 标签,此时字体会生效,但是设置的 fontStyle 等属性还是不会生效


因此我们在使用的时候建议直接对字体资源进行引用,样式粗细这些在进行单独的设置


三、在 XML 布局中使用字体


直接在布局 Xml 中使用 fontFamily 属性进行引用,如下图:


image-20210616205129045


四、在样式中添加并使用字体


1、在 style.xml 中添加样式


<style name="customfontstyle" parent="Theme.ChangeDefaultFontDemo">
<item name="android:fontFamily">@font/noto_sans_bolditem>

style>

2、在布局 Xml 中使用,如下图:


image-20210616205611588


五、在代码中使用字体


在代码中,我们可以通过 ResourcesCompat 或 Resource 的 gontFont 方法拿到 Typeface 对象,然后调用相关的 Api 去设置就行了,例如:


//方式1
val typeface = ResourcesCompat.getFont(context, R.font.myfont)
//方式2
val typeface = resources.getFont(R.font.myfont)
//设置字体
textView.typeface = typeface

为了方便在代码中使用,我们可以进行合理的封装:


object FontUtil {

const val NOTO_SANS_BOLD = R.font.noto_sans_bold
const val MA_SHAN_ZHENG_REGULAR = R.font.ma_shan_zheng_regular

/**缓存字体 Map*/
private val cacheTypeFaceMap: HashMap<Int,Typeface> = HashMap()

/**
* 设置 NotoSanUIBold 字体
*/

fun setNotoSanUIBold(mTextView: TextView){
try {
mTextView.typeface = getTypeface(NOTO_SANS_BOLD)
} catch (e: Exception) {
e.printStackTrace()
}
}

/**
* 设置 MaShanZhengRegular 字体
*/

fun setMaShanZhengRegular(mTextView: TextView){
try {
mTextView.typeface = getTypeface(MA_SHAN_ZHENG_REGULAR)
} catch (e: Exception) {
e.printStackTrace()
}
}

/**
* 获取字体 Typeface 对象
*/

fun getTypeface(fontResName: Int): Typeface? {
val cacheTypeface = cacheTypeFaceMap[fontResName]
if (cacheTypeface != null) {
return cacheTypeface
}
return try {
val typeface: Typeface? = ResourcesCompat.getFont(MyApplication.mApplication, fontResName)
cacheTypeFaceMap[fontResName] = typeface!!
typeface
} catch (e: Exception) {
e.printStackTrace()
Typeface.DEFAULT
}
}
}

那么后续我们在代码中使用字体,就只需调一行代码就 Ok 了


FontUtil.setMaShanZhengRegular(mTextView1)
FontUtil.setNotoSanUIBold(mTextView2)

六、项目需求实践


回顾一下我接到的项目需求:全局替换当前项目中的默认字体,并引入 UI 设计师提供的一些新字体


在学习本篇文章之前,我们引入字体都是放在 assets 文件目录下,这个目录下的字体文件,我们只能在代码中获取并使用。那么通过本篇文章的讲解,我们不仅可以在代码中进行使用,还可以在 Xml 中进行使用。现在我们解决了一半的需求,关于全局替换默认字体还需等到下一篇文章?


七、总结


回顾下本篇文章我们讲的一些重点内容:


1、将字体放在 res 的 font 目录下,这样我们就可以在 Xml 中使用字体了


2、通过字体 R 资源索引获取字体文件,封装相应的字体工具类,在代码中优雅的使用


好了,本篇文章到这里就结束了,希望能给你带来帮助 ?


Github Demo 地址


感谢你阅读这篇文章


下篇预告


下篇文章我会讲 Android 全局替换字体的几种方式,敬请期待吧 ?


参考和推荐


XML 中的字体



作者:sweetying
链接:https://juejin.cn/post/6974388756275019812
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android字体系列(二):Typeface完全解析

前言 很高兴遇见你~ 在本系列的上一篇文章中,我们介绍了关于 Android 字体的一些基础知识,还没有看过上一篇文章的朋友,建议先去阅读 Android字体系列 (一):Android字体基础,你会发现,我们设置的那三个属性最终都会去构建一个 ...
继续阅读 »

前言


很高兴遇见你~


在本系列的上一篇文章中,我们介绍了关于 Android 字体的一些基础知识,还没有看过上一篇文章的朋友,建议先去阅读 

Android字体系列 (一):Android字体基础

,你会发现,我们设置的那三个属性最终都会去构建一个 Typeface 对象,今天我们就好好的来讲讲它


注意:本文所展示的系统源码都是基于Android-30 ,并提取核心部分进行分析


一、Typeface 介绍


Typeface 负责 Android 字体的加载以及对上层提供相关字体 API 的调用


如果你想要操作字体,无论是使用 Android 系统自带的字体,还是加载自己内置的 .ttf(TureType) 或者 .otf(OpenType) 格式的字体文件,你都需要使用到 Typeface 这个类。因此我们要全局修改字体,首先就要把 Typeface 给弄明白


二、Typeface 源码分析


源码分析环节可能比较枯燥,坚持就是胜利 ??


1、Typeface 初始化


Typeface 这个类会在 Android 应用程序启动的过程中,通过反射的方式被加载。点击源码可以看到它里面有一个 static 代码块,它会随着类的加载而加载,并且只会加载一次,Typeface 就是通过这种方式来进行初始化的,如下:


static {
//创建一个存放字体的 Map
final HashMap systemFontMap = new HashMap<>();
//将系统的一些默认字体放入 Map 中
initSystemDefaultTypefaces(systemFontMap,SystemFonts.getRawSystemFallbackMap(),SystemFonts.getAliases());
//unmodifiableMap 方法的作用就是将当前 Map 进行包装,返回一个不可修改的Map,如果调用修改方法就会抛异常
sSystemFontMap = Collections.unmodifiableMap(systemFontMap);

// We can't assume DEFAULT_FAMILY available on Roboletric.
/**
* 设置系统默认字体 DEFAULT_FAMILY = "sans-serif";
* 因此系统默认的字体就是 sans-serif
*/

if (sSystemFontMap.containsKey(DEFAULT_FAMILY)) {
setDefault(sSystemFontMap.get(DEFAULT_FAMILY));
}

// Set up defaults and typefaces exposed in public API
//一些系统默认的字体
DEFAULT = create((String) null, 0);
DEFAULT_BOLD = create((String) null, Typeface.BOLD);
SANS_SERIF = create("sans-serif", 0);
SERIF = create("serif", 0);
MONOSPACE = create("monospace", 0);
//初始化一个 sDefaults 数组,并预加载好粗体、斜体等一些常用的 Style
sDefaults = new Typeface[] {
DEFAULT,
DEFAULT_BOLD,
create((String) null, Typeface.ITALIC),
create((String) null, Typeface.BOLD_ITALIC),
};

//...
}

上述代码写了详细的注释,我们可以发现,Typeface 初始化主要做了:


1、将系统的一些默认字体放入一个 Map 中


2、设置默认的字体


3、初始化一些默认字体


4、初始化一个 sDefaults 数组,存放一些常用的 Style


完成了 Typeface 的初始化,接下来看 Typeface 提供了一系列创建字体的 API ,其中对上层开放调用的有如下几个:


image-20210614130149262.png


下面我们来重点分析这几个方法


2、通过 Typeface 和 Style 获取新的 Typeface


对应上面截图的第一个 API , 看下它的源码:


public static Typeface create(Typeface family, @Style int style) {
//判断当前是否设置了 style , 如果没有设置,置为 NORMAL
if ((style & ~STYLE_MASK) != 0) {
style = NORMAL;
}
//判断当前传入的 Typeface 是否为空,如果是,置为默认字体
if (family == null) {
family = sDefaultTypeface;
}

// Return early if we're asked for the same face/style
//如果当前 Typeface 的 mStyle 属性和传入的 style 相同,直接返回 Typeface 对象
if (family.mStyle == style) {
return family;
}

final long ni = family.native_instance;

Typeface typeface;
//使用 sStyledCacheLock 保证线程安全
synchronized (sStyledCacheLock) {
//从缓存中获取存放 Typeface 的 SparseArray
SparseArray styles = sStyledTypefaceCache.get(ni);
if (styles == null) {
//存放 Typeface 的 SparseArray 为空,新创建一个,容量为 4
styles = new SparseArray(4);
//将当前 存放 Typeface 的 SparseArray 放入缓存中
sStyledTypefaceCache.put(ni, styles);
} else {
//存放 Typeface 的 SparseArray 不为空,直接获取 Typeface 并返回
typeface = styles.get(style);
if (typeface != null) {
return typeface;
}
}

//通过 native 层构建创建 Typeface 的参数并创建 Typeface 对象
typeface = new Typeface(nativeCreateFromTypeface(ni, style));
//将新创建的 Typeface 对象放入 SparseArray 中缓存起来
styles.put(style, typeface);
}
return typeface;
}

从上述代码我们可以知道:


1、当你设置的 Typeface 和 Style 为 null 和 0 时,会给它们设置一个默认值


注意:这里的 Style ,对应上一篇中讲的 android:textStyle 属性传递的值,用于设定字体的粗体、斜体等参数


2、如果当前设置的 Typeface 的 mStyle 属性和传入的 Style 相同,直接将 Typeface 给返回


3、从缓存中获取存放 Typeface 的容器,如果缓存中存在,则从容器中取出该 Typeface 并返回


4、如果不存在,则创建新的容器并加入缓存,然后通过 native 层创建 Typeface,并把当前 Typeface 放入到容器中


因此我们在使用的时候无需担心效率问题,它会把我们传入的字体进行一个缓存,后续都是从缓存中去拿的


3、通过字体名称和 Style 获取字体


对应上面截图的第二个 API:


public static Typeface create(String familyName, @Style int style) {
//调用截图的第一个 API
return create(getSystemDefaultTypeface(familyName), style);
}

//获取系统提供的一些默认字体,如果获取不到则返回系统的默认字体
private static Typeface getSystemDefaultTypeface(@NonNull String familyName) {
Typeface tf = sSystemFontMap.get(familyName);
return tf == null ? Typeface.DEFAULT : tf;
}

1、这个创建 Typeface 的 API 很简单,就是调用它的一个重载方法,我们已经分析过


2、getSystemDefaultTypeface 主要是通过 sSystemFontMap 获取字体,而这个 sSystemFontMap 在 Typeface 初始化的时候会存放系统提供的一些默认字体,因此这里直接取就可以了


4、通过 Typeface 、weight(粗体) 和 italic(斜体) 获取新的 Typeface


对应上面截图的第三个 API


public static @NonNull Typeface create(@Nullable Typeface family,
@IntRange(from = 1, to = 1000) int weight, boolean italic)
{
//校验传入的 weight 属性是否在范围内
Preconditions.checkArgumentInRange(weight, 0, 1000, "weight");
if (family == null) {
//如果当前传入的 Typeface 为 null, 则置为默认值
family = sDefaultTypeface;
}
//调用 createWeightStyle 方法创建 Typeface
return createWeightStyle(family, weight, italic);
}

private static @NonNull Typeface createWeightStyle(@NonNull Typeface base,
@IntRange(from = 1, to = 1000) int weight, boolean italic)
{
final int key = (weight << 1) | (italic ? 1 : 0);

Typeface typeface;
//使用 sWeightCacheLock 保证线程安全
synchronized(sWeightCacheLock) {
SparseArray innerCache = sWeightTypefaceCache.get(base.native_instance);
if (innerCache == null) {
//缓存 Typeface 的 SparseArray 为 null, 新建并缓存
innerCache = new SparseArray<>(4);
sWeightTypefaceCache.put(base.native_instance, innerCache);
} else {
//从缓存中拿取 typeface 并返回
typeface = innerCache.get(key);
if (typeface != null) {
return typeface;
}
}
//通过 native 创建 Typeface 对象
typeface = new Typeface(
nativeCreateFromTypefaceWithExactStyle(base.native_instance, weight, italic));
//将 Typeface 加入缓存
innerCache.put(key, typeface);
}
return typeface;
}

通过上述代码可以知道,他与截图一 API 的源码很类似,无非就是将之前需要设置的 Style 换成了 weight 和 italic,里面的实现机制是类似的


5、通过 AssetManager 和对应字体路径获取字体


对应上面截图的第四个 API


public static Typeface createFromAsset(AssetManager mgr, String path) {
//参数检查
Preconditions.checkNotNull(path); // for backward compatibility
Preconditions.checkNotNull(mgr);

//通过 Typeface 的 Builder 模式构建 typeface
Typeface typeface = new Builder(mgr, path).build();
//如果构建的 typeface 不为空则返回
if (typeface != null) return typeface;
// check if the file exists, and throw an exception for backward compatibility
//看当前字体路径是否存在,不存在直接抛异常
try (InputStream inputStream = mgr.open(path)) {
} catch (IOException e) {
throw new RuntimeException("Font asset not found " + path);
}
//如果构建的字体为 null 则返回默认字体
return Typeface.DEFAULT;
}

//接着看 Typeface 的 Builder 模式构建 typeface
//Builder 构造方法 主要就是初始化 mFontBuilder 和一些参数
public Builder(@NonNull AssetManager assetManager, @NonNull String path, boolean isAsset,
int cookie)
{
mFontBuilder = new Font.Builder(assetManager, path, isAsset, cookie);
mAssetManager = assetManager;
mPath = path;
}

//build 方法
public Typeface build() {
//如果 mFontBuilder 为 null,则会调用 resolveFallbackTypeface 方法
//resolveFallbackTypeface 内部会调用 createWeightStyle 创建 Typeface 并返回
if (mFontBuilder == null) {
return resolveFallbackTypeface();
}
try {
//通过 mFontBuilder 构建 Font
final Font font = mFontBuilder.build();
//使用 createAssetUid 方法获取到这个字体的唯一 key
final String key = mAssetManager == null ? null : createAssetUid(
mAssetManager, mPath, font.getTtcIndex(), font.getAxes(),
mWeight, mItalic,
mFallbackFamilyName == null ? DEFAULT_FAMILY : mFallbackFamilyName);
if (key != null) {
// Dynamic cache lookup is only for assets.
//使用 sDynamicCacheLock 保证线程安全
synchronized (sDynamicCacheLock) {
//通过 key 从缓存中拿字体
final Typeface typeface = sDynamicTypefaceCache.get(key);
//如果当前字体不为 null 直接返回
if (typeface != null) {
return typeface;
}
}
}
//如果当前字体不存在,通过 Builder 模式构建 FontFamily 对象
//通过 FontFamily 构建 CustomFallbackBuilder 对象
//最终通过 CustomFallbackBuilder 构建 Typeface 对象
final FontFamily family = new FontFamily.Builder(font).build();
final int weight = mWeight == RESOLVE_BY_FONT_TABLE
? font.getStyle().getWeight() : mWeight;
final int slant = mItalic == RESOLVE_BY_FONT_TABLE
? font.getStyle().getSlant() : mItalic;
final CustomFallbackBuilder builder = new CustomFallbackBuilder(family)
.setStyle(new FontStyle(weight, slant));
if (mFallbackFamilyName != null) {
builder.setSystemFallback(mFallbackFamilyName);
}
//builder.build 方法内部最终会通过调用 native 层创建 Typeface 对象
final Typeface typeface = builder.build();
//缓存 Typeface 对象并返回
if (key != null) {
synchronized (sDynamicCacheLock) {
sDynamicTypefaceCache.put(key, typeface);
}
}
return typeface;
} catch (IOException | IllegalArgumentException e) {
//如果流程有任何异常,则内部会调用 createWeightStyle 创建 Typeface 并返回
return resolveFallbackTypeface();
}
}

上述代码步骤:


1、大量运用了 Builder 模式去构建相关对象


2、具体逻辑就是使用 createAssetUid 方法获取到当前字体的唯一 key ,通过这个唯一 key ,从缓存中获取已经被加载过的字体,如果没有,则创建一个 FontFamily 对象,经过一系列 Builder 模式,最终调用 native 层创建 Typeface 对象,并将这个 Typeface 对象加入缓存并返回


3、如果流程有任何异常,内部会调用 createWeightStyle 创建 Typeface 并返回


6、通过字体文件获取字体


对应上面截图的第五个 API


public static Typeface createFromFile(@Nullable File file) {
// For the compatibility reasons, leaving possible NPE here.
// See android.graphics.cts.TypefaceTest#testCreateFromFileByFileReferenceNull
//通过 Typeface 的 Builder 模式构建 typeface
Typeface typeface = new Builder(file).build();
if (typeface != null) return typeface;

// check if the file exists, and throw an exception for backward compatibility
//文件不存在,抛异常
if (!file.exists()) {
throw new RuntimeException("Font asset not found " + file.getAbsolutePath());
}
//如果构建的字体为 null 则返回默认字体
return Typeface.DEFAULT;
}

//Builder 另外一个构造方法 主要是初始化 mFontBuilder
public Builder(@NonNull File path) {
mFontBuilder = new Font.Builder(path);
mAssetManager = null;
mPath = null;
}

从上述代码可以知道,这种方式主要也是通过 Builder 模式去构建 Typeface 对象,具体逻辑我们刚才已经分析过


7、通过字体路径获取字体


对应上面截图的第六个 API


public static Typeface createFromFile(@Nullable String path) {
Preconditions.checkNotNull(path); // for backward compatibility
return createFromFile(new File(path));
}

这个就更简单了,主要就是创建文件对象然后调用另外一个重载方法


8、Typeface 相关 Native 方法


在 Typeface 中,所有最终操作到加载字体的部分,全部都是 native 的方法。而 native 方法就是以效率著称的,这里只需要保证不频繁的调用(Typeface 已经做好了缓存,不会频繁的调用),基本上也不会存在效率的问题。


private static native long nativeCreateFromTypeface(long native_instance, int style);
private static native long nativeCreateFromTypefaceWithExactStyle(
long native_instance, int weight, boolean italic)
;
// TODO: clean up: change List to FontVariationAxis[]
private static native long nativeCreateFromTypefaceWithVariation(
long native_instance, List axes)
;
@UnsupportedAppUsage
private static native long nativeCreateWeightAlias(long native_instance, int weight);
@UnsupportedAppUsage
private static native long nativeCreateFromArray(long[] familyArray, int weight, int italic);
private static native int[] nativeGetSupportedAxes(long native_instance);

@CriticalNative
private static native void nativeSetDefault(long nativePtr);

@CriticalNative
private static native int nativeGetStyle(long nativePtr);

@CriticalNative
private static native int nativeGetWeight(long nativePtr);

@CriticalNative
private static native long nativeGetReleaseFunc();

private static native void nativeRegisterGenericFamily(String str, long nativePtr);

到这里,关于 Typeface 源码部分我们就介绍完了,下面看下它的一些其他细节


三、Typeface 其它细节


1、默认使用


在初始化那部分,Typeface 对字体和 Style 有一些默认实现


如果我们只想用系统默认的字体,直接拿上面的常量用就 ok 了,如:


Typeface.DEFAULT
Typeface.DEFAULT_BOLD
Typeface.SANS_SERIF
Typeface.SERIF
Typeface.MONOSPACE

而如果想要设置 Style ,我们不能通过 sDefaults 直接去拿,因为上层调用不到 sDefaults,但是可以通过 Typeface 提供的 API 获取:


public static Typeface defaultFromStyle(@Style int style) {
return sDefaults[style];
}

//具体调用
Typeface.defaultFromStyle(Typeface.NORMAL)
Typeface.defaultFromStyle(Typeface.BOLD)
Typeface.defaultFromStyle(Typeface.ITALIC)
Typeface.defaultFromStyle(Typeface.BOLD_ITALIC)

2、Typeface 中的 Style


1)、Typeface 中的 Style 可以通过 android:textStyle 属性去设置粗体、斜体等样式


2)、在 Typeface 中,这些样式也对应了一个个的常量,并且 Typeface 也提供了对应的 Api,让我们获取到当前字体的样式


// Style
public static final int NORMAL = 0;
public static final int BOLD = 1;
public static final int ITALIC = 2;
public static final int BOLD_ITALIC = 3;

/** Returns the typeface's intrinsic style attributes */
public @Style int getStyle() {
return mStyle;
}

/** Returns true if getStyle() has the BOLD bit set. */
public final boolean isBold() {
return (mStyle & BOLD) != 0;
}

/** Returns true if getStyle() has the ITALIC bit set. */
public final boolean isItalic() {
return (mStyle & ITALIC) != 0;
}

3、FontFamily 介绍


FontFamily 主要就是用来构建 Typeface 的一个类,注意和在 Xml 属性中设置的 android:fontFamily 区分开来就好了


四、总结


总结下本篇文章所讲的一些重点内容:


1、Typeface 初始化对字体和 Style 会有一些默认实现


2、Typeface create 系列方法支持从系统默认字体、 assets 目录、字体文件以及字体路径去获取字体


3、Typeface 本身支持缓存,我们在使用的时候无需注意效率问题


好了,本篇文章到这里就结束了,希望能给你带来帮助 ?


感谢你阅读这篇文章


下篇预告


下篇文章我会讲在 Xml 中使用字体,敬请期待吧 ?


参考和推荐


Android 修改字体,跳不过的 Typeface


作者:sweetying
链接:https://juejin.cn/post/6973553157326503943
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android字体系列 (一):Android字体基础

前言 很高兴遇见你~ 最近接到一个需求,大致内容是:全局替换当前项目中的默认字体,并引入 UI 设计师提供的一些新字体。于是对字体做了些研究,把自己的一些心得分享给大家。 注意:本文所展示的系统源码都是基于Android-30 ,并提取核心部分进行分析 ...
继续阅读 »

前言


很高兴遇见你~


最近接到一个需求,大致内容是:全局替换当前项目中的默认字体,并引入 UI 设计师提供的一些新字体。于是对字体做了些研究,把自己的一些心得分享给大家。


注意:本文所展示的系统源码都是基于Android-30 ,并提取核心部分进行分析


一、Android 默认字体介绍


1、Android 系统默认使用的是一款叫做 Roboto 的字体,这也是 Google 推荐使用的一款字体 传送门。它提供了多种字体形式的选择,例如:粗体,斜体等等。


2、在 Android 中,我们一般会直接或间接的通过 TextView 控件去承载字体的显示,因为关于 Android 提供的承载字体显示的控件都会直接或间接继承 TextView,例如:EditText,Button 等等,下面给出一张 TextView 继承图:


image-20210612124458481


3、TextView 中有三个属性可以设置字体的显示:


1)、textStyle


2)、typeface


3)、fontFamily


下面我们重点介绍下这三个属性


二、textStyle


textStyle 主要用来设置字体的样式,我们看下它在 TextView 的自定义属性中的一个体现:


//TextView 的自定义属性 textStyle
<attr name="textStyle">
<flag name="normal" value="0" />
<flag name="bold" value="1" />
<flag name="italic" value="2" />
</attr>

从上述自定义属性中我们可以知道:


1、textStyle 主要有 3 种样式:



  • normal:默认字体

  • bold:粗体

  • italic:斜体


2、textStyle 是用 flag 来承载的,flag 表示的值可以做或运算,也就是说我们可以设置多种字体样式进行叠加


接下来我们在 xml 中设置一下,如下图:


image-20210612205549971


可以看到,我们给 TextView 的 textStyle 属性设置了粗体和斜体两种样式叠加,右边可以看到预览效果


同样我们也可以在代码中对其进行设置,但是在代码中设置字体样式只能设置一种,不能叠加:


mTextView.setTypeface(null, Typeface.BOLD)

三、typeface


typeface 主要用于设置 TextView 的字体,我们看下它在 TextView 的自定义属性中的一个体现:


//TextView 的自定义属性 typeface
<attr name="typeface">
<enum name="normal" value="0" />
<enum name="sans" value="1" />
<enum name="serif" value="2" />
<enum name="monospace" value="3" />
</attr>

从上述自定义属性中我们可以知道:


1、typeface 提供了 4 种字体:



  • noraml:普通字体,系统默认使用的字体

  • sans:非衬线字体

  • serif:衬线字体

  • monospace:等宽字体


2、typeface 是用 enum 来承载的,enum 表示枚举类型,每次只能选择一个,因此我们每次只能设置一种字体,不能叠加


接下来我们在 xml 中设置一下,如下图:


image-20210612133722082


简单介绍这几种字体的区别:


serif (衬线字体):在字的笔划开始及结束的地方有额外的装饰,而且笔划的粗细会因直横的不同而有不同相


sans (非衬线字体):没有 serif 字体这些额外的装饰,和 noraml 字体是一样的


image-20210612134441993


monospace (等宽字体):限制每个字符的宽度,让它们达到一个等宽的效果


同样我们也可以在代码中进行设置:


mTv.setTypeface(Typeface.SERIF)

四、fontFamily


fontFamily 相当于是加强版的 typeface,它表示 android 系统支持的一系列字体,每个字体都有一个别名,我们通过别名就能设置这种字体,看下它在 TextView 的自定义属性中的一个体现:


//TextView 的自定义属性 fontFamily
<attr name="fontFamily" format="string" />

从上述自定义属性中我们可以知道:


fontFamily 接收的是一个 String 类型的值,也就是我们可以通过字体别名设置这种字体,如下图:


fontFamily


可以看到,它细致的区分了每个系列字体的样式,同样我们在 xml 中对它进行一个设置:


image-20210612212209243 我们在代码中在对他进行一个设置:


mTv.setTypeface(Typeface.create("sans-serif-medium",Typeface.NORMAL))

值的注意的是:fontFamily 设置的某些字体有兼容性问题,如我上面设置的 sans-serif-medium 字体,它在 Android 系统版本大于等于 21 才会生效,如果小于 21 ,则会使用默认字体,因此我们在使用 fontFamily 属性时,需要注意这个问题


到这里,我们就把影响 Android 字体的 3 个属性给讲完了,但是我心里有个疑问?? ?假设我这三个属性同时设置,会一起生效吗?


带着这个问题,我们探索一下源码


五、textStyle,typeface,fontFamily 三者关系分析


TextView 在我们使用它之前需进行一个初始化,最终会调用它参数最多的那个构造方法:


public TextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
//省略成吨代码.....
//读取设置的属性
readTextAppearance(context, appearance, attributes, false /* styleArray */);
//设置字体
applyTextAppearance(attributes);
}

private void applyTextAppearance(TextAppearanceAttributes attributes) {
//省略成吨代码.....
setTypefaceFromAttrs(attributes.mFontTypeface, attributes.mFontFamily,
attributes.mTypefaceIndex, attributes.mTextStyle, attributes.mFontWeight);
}

上面这条调用链,首先会读取 TextView 设置的相关属性,我们看下与字体相关的几个:


private void readTextAppearance(Context context, TypedArray appearance,
TextAppearanceAttributes attributes, boolean styleArray)
{
//...
switch (index) {
case com.android.internal.R.styleable.TextAppearance_typeface:
attributes.mTypefaceIndex = appearance.getInt(attr, attributes.mTypefaceIndex);
if (attributes.mTypefaceIndex != -1 && !attributes.mFontFamilyExplicit) {
attributes.mFontFamily = null;
}
break;
case com.android.internal.R.styleable.TextAppearance_fontFamily:
if (!context.isRestricted() && context.canLoadUnsafeResources()) {
try {
attributes.mFontTypeface = appearance.getFont(attr);
} catch (UnsupportedOperationException | Resources.NotFoundException e) {
// Expected if it is not a font resource.
}
}
if (attributes.mFontTypeface == null) {
attributes.mFontFamily = appearance.getString(attr);
}
attributes.mFontFamilyExplicit = true;
break;
case com.android.internal.R.styleable.TextAppearance_textStyle:
attributes.mTextStyle = appearance.getInt(attr, attributes.mTextStyle);
break;
//...
default:
}
}

从上述代码中我们可以看到:


1、当我们设置 typeface 属性时,会将对应的属性值赋给 mTypefaceIndex ,并把 mFontFamily 置为 null


2、当我们设置 fontFamily 属性时,首先会通过 appearance.getFont() 方法去获取字体文件,如果能获取到,则赋值给 mFontTypeface,如果获取不到,则通过 appearance.getString() 方法取获取当前字体别名并赋值给 mFontFamily


注意:当我们给 fontFamily 设置了一些第三方字体,那么此时 appearance.getFont() 方法就获取不到字体


3、当我们设置 textStyle 属性时,会将获取的属性值赋给 mTextStyle


上述方法走完了,会调 setTypefaceFromAttrs() 方法,这个方法就是最终 TextView 设置字体的方法,我们来解析下这个方法:


private void setTypefaceFromAttrs(@Nullable Typeface typeface, @Nullable String familyName,
@XMLTypefaceAttr int typefaceIndex, @Typeface.Style int style,
@IntRange(from = -1, to = FontStyle.FONT_WEIGHT_MAX) int weight)
{
if (typeface == null && familyName != null) {
// Lookup normal Typeface from system font map.
final Typeface normalTypeface = Typeface.create(familyName, Typeface.NORMAL);
resolveStyleAndSetTypeface(normalTypeface, style, weight);
} else if (typeface != null) {
resolveStyleAndSetTypeface(typeface, style, weight);
} else { // both typeface and familyName is null.
switch (typefaceIndex) {
case SANS:
resolveStyleAndSetTypeface(Typeface.SANS_SERIF, style, weight);
break;
case SERIF:
resolveStyleAndSetTypeface(Typeface.SERIF, style, weight);
break;
case MONOSPACE:
resolveStyleAndSetTypeface(Typeface.MONOSPACE, style, weight);
break;
case DEFAULT_TYPEFACE:
default:
resolveStyleAndSetTypeface(null, style, weight);
break;
}
}
}

上述代码步骤:


1、当 typeface 为空并且 familyName 不为空时,取 familyName 的字体


2、当 typeface 不为空并且 familyName 为空时,取 typeface 的字体


3、当 typeface 和 familyName 都为空,则根据 typefaceIndex 的值取相应的字体


4、typeface ,familyName 和 typefaceIndex 在我们分析的 readTextAppearance 方法会被赋值


5、resolveStyleAndSetTypefce 方法会进行字体和字体样式的设置


6、style 是在 readTextAppearance 方法中赋值的,他和设置字体并不冲突


好,现在代码分析的差不多了,我们再来看下上面那个疑问?我们使用假设法来进行推导:


假设在 Xml 中, typeface,familyName 和 textStyle 我都设置了,那么根据上面分析:


1、textStyle 肯定会生效


2、当设置了 typeface 属性,typefaceIndex 会被赋值,同时 familyName 会置为空


3、当设置了 familyName 属性,分情况:1、如果设置的是系统字体,typeface 会被赋值,familyName 还是为空。2、如果设置的是第三方字体,typeface 为空,familyName 被赋值


因此,当我们设置了这个三个属性,typeface 和 familyName 总有一个不会为空,因此不会走第三个条件体,那么 typeface 设置的属性就不会生效了,而剩下的两个属性都能够生效


最后对这三个属性做一个总结:


1、fontFamily、typeface 属性用于字体设置,如果都设置了,优先使用 fontFamily 属性,typeface 属性不会生效


2、textStyle 用于字体样式设置,与字体设置不会产生冲突


上面这段源码分析可能有点绕,如果有不清楚的地方,欢迎评论区给我留言提问


六、TextView 设置字体属性源码分析


通过上面源码的分析,我们清楚了 fontFamily,typeface 和 textStyle 这三者的关系。接下来我们研究一下,我们设置的这些属性是怎么实现这些效果的呢?又到了源码分析环节?,可能会有点枯燥,但是如果你能够认真看完,一定会收获很多,干就完了


我们上面用 Xml 或代码设置的字体属性,最终都会走到 TextView 的 setTypeface 重载方法:


//重载方法一
public void setTypeface(@Nullable Typeface tf) {
if (mTextPaint.getTypeface() != tf) {
//通过 mTextPaint 设置字体
mTextPaint.setTypeface(tf);

//刷新重绘
if (mLayout != null) {
nullLayouts();
requestLayout();
invalidate();
}
}
}

//重载方法二
public void setTypeface(@Nullable Typeface tf, @Typeface.Style int style) {
if (style > 0) {
if (tf == null) {
tf = Typeface.defaultFromStyle(style);
} else {
tf = Typeface.create(tf, style);
}
//调用重载方法一,设置字体
setTypeface(tf);
//经过一些算法
int typefaceStyle = tf != null ? tf.getStyle() : 0;
int need = style & ~typefaceStyle;
//打开画笔的粗体和斜体
mTextPaint.setFakeBoldText((need & Typeface.BOLD) != 0);
mTextPaint.setTextSkewX((need & Typeface.ITALIC) != 0 ? -0.25f : 0);
} else {
mTextPaint.setFakeBoldText(false);
mTextPaint.setTextSkewX(0);
setTypeface(tf);
}
}

分析下上述代码:


重载方法一:


TextView 设置字体实际上就是操作 mTextPaint,mTextPaint 是 TextPaint 的类对象,继承自 Paint 即画笔,因此我们设置的字体实际上会通过调用画笔的方法来进行绘制


重载方法二:


相对于重载方法一,法二多传递了一个 textStyle 参数,主要用来标记粗体和斜体的:


1)、如果设置了 textStyle ,进入第一个条件体,分情况:1、如果传进来的 tf 为 null ,则会根据传入的 style 去获取 Typeface 字体,2、如果不为 null ,则会根据传入的 tf 和 style 去获取 Typeface 字体。设置好字体后,接下来还会打开画笔的粗体和斜体设置


2)、如果没有设置 textStyle,则只会设置字体,并把画笔的粗斜体设置置为 false 和 0


从上述分析我们可以得知:TextView 设置字体和字体样式最终都是通过画笔来完成的


七、总结


本篇文章主要讲了:


1、Android 字体大概的一个介绍


2、关于影响 Android 字体显示的三个属性


3、textStyle,typeface,fontFamily 三者的一个关系


4、设置的这三个属性是怎么实现这些效果的?




好了,本篇文章到这里就结束了,如果有任何问题,欢迎给我留言,我们评论区一起讨论?


感谢你阅读这篇文章


作者:sweetying
链接:https://juejin.cn/post/6973064546420260878
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

【Kotlin篇】差异化分析,let,run,with,apply及also

作用域函数是Kotlin比较重要的一个特性,共分为以下5种:let、run、with、apply 以及 also,这五个函数的工作方式可以说非常相似,但是我们需要了解的是这5种函数的差异,以便在不同的场景更好的利用它。 读完这篇文章您将了解到: 什么是...
继续阅读 »

作用域函数是Kotlin比较重要的一个特性,共分为以下5种:letrunwithapply 以及 also,这五个函数的工作方式可以说非常相似,但是我们需要了解的是这5种函数的差异,以便在不同的场景更好的利用它。 读完这篇文章您将了解到:



  • 什么是Kotlin的作用域函数?

  • letrunwithapply 以及 also这5种作用域函数各自的角色定位;

  • 5种作用域函数的差异区分;

  • 何时何地使用这5种作用域?


Kotlin的作用域函数



Kotlin 标准库包含几个函数,它们的唯一目的是在对象的上下文中执行代码块。当对一个对象调用这样的函数并提供一个 lambda 表达式时,它会形成一个临时作用域。在此作用域中,可以访问该对象而无需其名称。这些函数称为作用域函数。



简单来说,作用域函数是为了方便对一个对象进行访问和操作,你可以对它进行空检查或者修改它的属性或者直接返回它的值等操作,下面提供了案例对作用域函数进行了详细说明。


角色定位


2.1 let


public inline fun <T, R> T.let(block: (T) -> R): R 

let函数是参数化类型 T 的扩展函数。在let块内可以通过 it 指代该对象。返回值为let块的最后一行或指定return表达式。


我们以一个Book对象为例,类中包含Book的name和price,如下:


class Book() {
var name = "《数据结构》"
var price = 60
fun displayInfo() = print("Book name : $name and price : $price")
}

fun main(args: Array<String>) {
val book = Book().let {
it.name = "《计算机网络》"
"This book is ${it.name}"
}
print(book)
}

控制台输出:
This book is 《计算机网络》

在上面案例中,我们对Book对象使用let作用域函数,在函数块的最后一句添加了一行字符串代码,并且对Book对象进行打印,我们可以看到最后控制台输出的结果为字符串“This book is 《计算机网络》”。


按照我们的编程思想,打印一个对象,输出必定是对象,但是使用let函数后,输出为最后一句字符串。这是由于let函数的特性导致。因为在Kotlin中,如果let块中的最后一条语句是非赋值语句,则默认情况下它是返回语句。


那如果我们将let块中最后一条语句修改为赋值语句,会发生什么变化?


fun main(args: Array<String>) {
val book = Book().let {
it.name = "《计算机网络》"
}
print(book)
}

控制台输出:
kotlin.Unit

可以看到我们将Book对象的name值进行了赋值操作,同样对Book对象进行打印,但是最后控制台的输出结果为“kotlin.Unit”,这是因为在let函数块的最后一句是赋值语句,print则将其当做是一个函数来看待。


这是let角色设定的第一点:1??



  • let块中的最后一条语句如果是非赋值语句,则默认情况下它是返回语句,反之,则返回的是一个 Unit类型


我们来看let第二点:2??



  • let可用于空安全检查。


如需对非空对象执行操作,可对其使用安全调用操作符 ?. 并调用 let 在 lambda 表达式中执行操作。如下案例:


var name: String? = null
fun main(args: Array<String>) {
val nameLength = name?.let {
it.length
} ?: "name为空时的值"
print(nameLength)
}

我们设置name为一个可空字符串,利用name?.let来进行空判断,只有当name不为空时,逻辑才能走进let函数块中。在这里,我们可能还看不出来let空判断的优势,但是当你有大量name的属性需要编写的时候,就能发现let的快速和简洁。


let第三点:3??



  • let可对调用链的结果进行操作。


关于这一点,官方教程给出了一个案例,在这里就直接使用:


fun main(args: Array<String>) { 
val numbers = mutableListOf("One","Two","Three","Four","Five")
val resultsList = numbers.map { it.length }.filter { it > 3 }
print(resultsList)
}

我们的目的是获取数组列表中长度大于3的值。因为我们必须打印结果,所以我们将结果存储在一个单独的变量中,然后打印它。但是使用“let”操作符,我们可以将代码修改为:


fun main(args: Array<String>) {
val numbers = mutableListOf("One","Two","Three","Four","Five")
numbers.map { it.length }.filter { it > 3 }.let {
print(it)
}
}

使用let后可以直接对数组列表中长度大于3的值进行打印,去掉了变量赋值这一步。


另外,let函数还存在一个特点。


let第四点:4??



  • let可以将“It”重命名为一个可读的lambda参数。


let是通过使用“It”关键字来引用对象的上下文,因此,这个“It”可以被重命名为一个可读的lambda参数,如下将it重命名为book


fun main(args: Array<String>) {
val book = Book().let {book ->
book.name = "《计算机网络》"
}
print(book)
}

2.2 run


run函数以“this”作为上下文对象,且它的调用方式与let一致。


另外,第一点:1?? 当 lambda 表达式同时包含对象初始化和返回值的计算时,run更适合


这句话是什么意思?我们还是用案例来说话:


fun main(args: Array<String>) {

Book().run {
name = "《计算机网络》"
price = 30
displayInfo()
}
}

控制台输出:
Book name : 《计算机网络》 and price : 30

如果不使用run函数,相同功能下代码会怎样?来看一看:


fun main(args: Array<String>) {

val book = Book()
book.name = "《计算机网络》"
book.price = 30
book.displayInfo()
}

控制台输出:
Book name : 《计算机网络》 and price : 30

输出结果还是一样,但是run函数所带来的代码简洁程度已经显而易见。


除此之外,让我们来看看run函数的其他优点:


通过查看源码,了解到run函数存在两种声明方式,


1、与let一样,run是作为T的扩展函数;


inline fun <T, R> T.run(block: T.() -> R): R 

2、第二个run的声明方式则不同,它不是扩展函数,并且块中也没有输入值,因此,它不是用于传递对象并更改属性的类型,而是可以使你在需要表达式的地方就可以执行一个语句。


inline fun <R> run(block: () -> R): R

如下利用run函数块执行方法,而不是作为一个扩展函数:


run {
val book = Book()
book.name = "《计算机网络》"
book.price = 30
book.displayInfo()
}

2.3 with


inline fun <T, R> with(receiver: T, block: T.() -> R): R 

with属于非扩展函数,直接输入一个对象receiver,当输入receiver后,便可以更改receiver的属性,同时,它也与run做着同样的事情。


还是提供一个案例说明:


fun main(args: Array<String>) {
val book = Book()

with(book) {
name = "《计算机网络》"
price = 40
}
print(book)
}

以上面为例,with(T)类型传入了一个参数book,则可以在with的代码块中访问book的name和price属性,并做更改。


with使用的是非null的对象,当函数块中不需要返回值时,可以使用with。


2.4 apply


inline fun <T> T.apply(block: T.() -> Unit): T

apply是 T 的扩展函数,与run函数有些相似,它将对象的上下文引用为“this”而不是“it”,并且提供空安全检查,不同的是,apply不接受函数块中的返回值,返回的是自己的T类型对象。


fun main(args: Array<String>) {
Book().apply {
name = "《计算机网络》"
price = 40

}
print(book)
}

控制台输出:
com.fuusy.kotlintest.Book@61bbe9ba

前面看到的 letwithrun 函数返回的值都是 R。但是,apply 和下面查看的 also 返回 T。例如,在 let 中,没有在函数块中返回的值,最终会成为 Unit 类型,但在 apply 中,最后返回对象本身 (T) 时,它成为 Book 类型。


apply函数主要用于初始化或更改对象,因为它用于在不使用对象的函数的情况下返回自身。


2.5 also


inline fun <T> T.also(block: (T) -> Unit): T 

also是 T 的扩展函数,返回值与apply一致,直接返回T。also函数的用法类似于let函数,将对象的上下文引用为“it”而不是“this”以及提供空安全检查方面


因为T作为block函数的输入,可以使用also来访问属性。所以,在不使用或不改变对象属性的情况下也使用also。


fun main(args: Array<String>) {
val book = Book().also {
it.name = "《计算机网络》"
it.price = 40
}
print(book)
}

控制台输出:
com.fuusy.kotlintest.Book@61bbe9ba

差异化


3.1 let & run



  • let将上下文对象引用为it ,而run引用为this;

  • run无法将“this”重命名为一个可读的lambda参数,而let可以将“it”重命名为一个可读的lambda参数。 在let多重嵌套时,就可以看到这个特点的优势所在。


3.2 with & run


with和run其实做的是同一种事情,对上下文对象都称之为“this”,但是他们又存在着不同,我们来看看案例。


先使用with函数:



fun main(args: Array<String>) {
val book: Book? = null
with(book){
this?.name = "《计算机网络》"
this?.price = 40
}
print(book)

}

我们创建了一个可空对象book,利用with函数对book对象的属性进行了修改。代码很直观,那么我们接着将with替换为run,代码更改为:


fun main(args: Array<String>) {
val book: Book? = null
book?.run{
name = "《计算机网络》"
price = 40
}
print(book)
}

首先run函数的调用省略了this引用,在外层就进行了空安全检查,只有非空时才能进入函数块内对book进行操作。



  • 相比较with来说,run函数更加简便,空安全检查也没有with那么频繁。


3.3 apply & let



  • apply不接受函数块中的返回值,返回的是自己的T类型对象,而let能返回。

  • apply上下文对象引用为“this”,let为“it”。


何时应该使用 apply、with、let、also 和 run ?



  • 用于初始化对象或更改对象属性,可使用apply

  • 如果将数据指派给接收对象的属性之前验证对象,可使用also

  • 如果将对象进行空检查并访问或修改其属性,可使用let

  • 如果是非null的对象并且当函数块中不需要返回值时,可使用with

  • 如果想要计算某个值,或者限制多个本地变量的范围,则使用run


总结


以上便是Kotlin作用域函数的作用以及使用场景,在Android实际开发中,5种函数使用的频次非常高,在使用过程中发现,当代码逻辑少的时候,作用域函数能带给我们代码的简洁性可读性,但是当逻辑复杂时,使用不同的函数,多次叠加都将降低可读性。这就要我们去区分它们各自的特点,以便在适合且复杂的场景下去使用它。


希望这篇文章能帮到您,感谢阅读。




作者:付十一
链接:https://juejin.cn/post/6975384870675546126
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

网易换肤第二篇:本地换肤实现!

完整脑图:https://note.youdao.com/s/V2csJmYS Demo源码:点击下载 技术分析 我们在换肤的第一篇介绍了换肤的核心思想。就是在setContentView()之前调用setFactory2()。 第一篇的Demo利...
继续阅读 »


在这里插入图片描述
完整脑图:https://note.youdao.com/s/V2csJmYS


Demo源码:点击下载


技术分析




我们在换肤的第一篇介绍了换肤的核心思想。就是在setContentView()之前调用setFactory2()


第一篇的Demo利用的是AOP切面方法registerActivityLifecycleCallbacks(xxx)回调在setContentView()之前,从而在registerActivityLifecycleCallbacks的onActivityCreated()方法中设置Factory。如此就能拦截到控件的属性,根据拦截到的控件的属性,重新赋值控件的textColor、background等属性,从而实现换肤的。


本Demo的实现,主要基于以下两个狙击点。



1、super.onCreate(savedInstanceState)方法
2、Activity实现了Factory接口



前面说过,只要在setContentView()之前setFactory2()就行。super.onCreate(savedInstanceState)方法就是在setContentView()方法之前执行的。


一直跟踪super.onCreate(savedInstanceState)方法,最终会发现setFactory的逻辑,如下:


AppCompatDelegateImpl.java(1008)


public void installViewFactory() {
LayoutInflater layoutInflater = LayoutInflater.from(this.mContext);
if (layoutInflater.getFactory() == null) {
LayoutInflaterCompat.setFactory2(layoutInflater, this);
} else if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImpl)) {
Log.i("AppCompatDelegate", "The Activity's LayoutInflater already has a Factory installed so we can not install AppCompat's");
}
}

它这里传了this,可以预见AppCompatDelegateImpl是实现了Factory接口的,最后会通过AppCompatDelegateImpl自身的onCreateView()方法创建的View。


onCreateView()中如何创建的View的,下面再看源码,先知道是通过AppCompatViewInflater来做控件的具体初始化的。


第一个狙击点可以抽出下图内容:


在这里插入图片描述
细心地同学肯定注意到了AppCompatDelegateImpl的installViewFactory()方法中,只有当layoutInflater.getFactory() == null的时候,才会去setFactory。


也就是说我在super.onCreate(savedInstanceState)之前,先给它setFactory就能走自己Factory的onCreateView()回调。


换肤第一篇中我们是自己去实现Factory2接口,在本例中,就用到了我们第二个狙击点。


Activity实现了Factory接口!!!


在这里插入图片描述


也就是说,只要我们在super.onCreate(savedInstanceState)之前,setFactory的时候,传this,就能走ActivityonCreateView()回调,来对控件属性做操作。


用归纳法,见下图:


在这里插入图片描述
最后,也就剩下Activity的onCreateView()中的回调怎么实现了。


直接模拟super.onCreate(savedInstanceState)中AppCompatViewInflater类中的实现就好了。


在这里插入图片描述
参考代码:


/**
* 自定义控件加载器(可以考虑该类不被继承)
*/

public final class CustomAppCompatViewInflater extends AppCompatViewInflater {

private String name; // 控件名
private Context context; // 上下文
private AttributeSet attrs; // 某控件对应所有属性

public CustomAppCompatViewInflater(@NonNull Context context) {
this.context = context;
}

public void setName(String name) {
this.name = name;
}

public void setAttrs(AttributeSet attrs) {
this.attrs = attrs;
}

/**
* @return 自动匹配控件名,并初始化控件对象
*/

public View autoMatch() {
View view = null;
switch (name) {
case "LinearLayout":
// view = super.createTextView(context, attrs); // 源码写法
view = new SkinnableLinearLayout(context, attrs);
this.verifyNotNull(view, name);
break;
case "RelativeLayout":
view = new SkinnableRelativeLayout(context, attrs);
this.verifyNotNull(view, name);
break;
case "TextView":
view = new SkinnableTextView(context, attrs);
this.verifyNotNull(view, name);
break;
case "ImageView":
view = new SkinnableImageView(context, attrs);
this.verifyNotNull(view, name);
break;
case "Button":
view = new SkinnableButton(context, attrs);
this.verifyNotNull(view, name);
break;
}

return view;
}

/**
* 校验控件不为空(源码方法,由于private修饰,只能复制过来了。为了代码健壮,可有可无)
*
* @param view 被校验控件,如:AppCompatTextView extends TextView(v7兼容包,兼容是重点!!!)
* @param name 控件名,如:"ImageView"
*/

private void verifyNotNull(View view, String name) {
if (view == null) {
throw new IllegalStateException(this.getClass().getName() + " asked to inflate view for <" + name + ">, but returned null");
}
}
}

详细实现就参考Demo吧,思路其实很简单,只是会有对setFactory这块逻辑的流程不了解的。建议跟踪着点几遍源码。





————————————————
版权声明:本文为CSDN博主「csdn小瓯」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/u014158743/article/details/117995256

收起阅读 »

网易换肤第一篇:换肤技术解密!

参考 脑图:https://note.youdao.com/s/Q1e6r39j 最终效果: Demo源码:点击跳转 技术点分析 换肤的核心思路主要是在setContentView()之前调用setFactory2()来收集控件属性,然后在F...
继续阅读 »


参考




脑图:https://note.youdao.com/s/Q1e6r39j


最终效果:
在这里插入图片描述
Demo源码:点击跳转


技术点分析




换肤的核心思路主要是在setContentView()之前调用setFactory2()来收集控件属性,然后在Factory的onCreateView()中利用收集到的属性来创建view。


不懂?没事,往下看。


在这里插入图片描述
弄明白换肤技术的实现之前,得有上图这几个知识储备。


首先得知道控件是在setContentView()方法中通过XmlPullParser解析我们在xml中定义的控件,然后显示在界面上的


LayoutInflater.java(451,注:本文源码为安卓9.0,api 28,下同


public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
...
if (TAG_MERGE.equals(name)) {
...
rInflate(parser, root, inflaterContext, attrs, false);
} else {
// Temp is the root view that was found in the xml
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
...
}
...

而且在createViewFromTag()方法中,有一个判断:当mFactory2 != null的时候,就会把从xml中解析到的属性等传给mFactory2.onCreateView(parent, name, context, attrs)方法,利用mFactory2来创建view。


先看源码片段:
LayoutInflater.java(748)


View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
...

try {
View view;
if (mFactory2 != null) {
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}

if (view == null && mPrivateFactory != null) {
view = mPrivateFactory.onCreateView(parent, name, context, attrs);
}

if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
if (-1 == name.indexOf('.')) {
view = onCreateView(parent, name, attrs);
} else {
view = createView(name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}

return view;
} catch (InflateException e) {
...
}
}

所以,我们只要通过LayoutlnflaterCompat.setFactory2(xx, yу)设置了Factory,就可以拦截到所有控件及其在xml中定义的属性了。


如此一来,问题就变成了如何在setContentView(R.layout.xxx)之前setFactory2()


答案就是利用AOP方法切面:registerActivityLifecycleCallbacks(xxx)ActivityLifecycleCallbacksonActivityCreated()方法正是在setContentView(R.layout.xxx)之前执行。


所以,我们可以实现Application.ActivityLifecycleCallbacks,然后在onActivityCreated()方法中LayoutInflaterCompat.setFactory2(xx, yy),这样换肤技术的核心部分,就被我们突破了。


参考代码:


public class SkinActivityLifecycleCallbacks implements Application.ActivityLifecycleCallbacks {
...
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
...

skinFactory = new SkinFactory(activity);
// mFactorySet = true是无法设置成功的(源码312行)
LayoutInflaterCompat.setFactory2(layoutInflater, skinFactory);

// 注册观察者(监听用户操作,点击了换肤,通知观察者更新)
SkinEngine.getInstance().addObserver(skinFactory);
}

...
}





————————————————
版权声明:本文为CSDN博主「csdn小瓯」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/u014158743/article/details/117921500

收起阅读 »

带着问题学,协程到底是什么?

前言 随着kotlin在Android开发领域越来越火,协程在各个项目中的应用也逐渐变得广泛 但是协程到底是什么呢? 协程其实是个古老的概念,已经非常成熟了,但大家对它的概念一直存在各种疑问,众说纷纷 有人说协程是轻量级的线程,也有人说kotlin协程其...
继续阅读 »



前言


随着kotlinAndroid开发领域越来越火,协程在各个项目中的应用也逐渐变得广泛


但是协程到底是什么呢?


协程其实是个古老的概念,已经非常成熟了,但大家对它的概念一直存在各种疑问,众说纷纷
有人说协程是轻量级的线程,也有人说kotlin协程其实本质是一套线程切换方案


显然这对初学者不太友好,当不清楚一个东西是什么的时候,就很难进入为什么怎么办的阶段了
本文主要就是回答这个问题,主要包括以下内容
1.关于协程的一些前置知识
2.协程到底是什么?
3.kotlin协程的一些基本概念,挂起函数,CPS转换,状态机等
以上问题总结为思维导图如下:



1. 关于协程的一些前置知识


为了了解协程,我们可以从以下几个切入点出发
1.什么是进程?为什么要有进程?
2.什么是线程?为什么要有线程?进程和线程有什么区别?
3.什么是协作式,什么是抢占式?
4.为什么要引入协程?是为了解决什么问题?


1.1 什么是进程?


我们在背进程的定义的时候,可能会经常看到一句话



进程是资源分配的最小单位



这个资源分配怎么理解呢?


在单核CPU中,同一时刻只有一个程序在内存中被CPU调用运行



假设有AB两个程序,A正在运行,此时需要读取大量输入数据(IO操作),那么CPU只能干等,直到A数据读取完毕,再继续往下执行,A执行完,再去执行程序B,白白浪费CPU资源。



这种方式会浪费CPU资源,我们可能更想要下面这种方式



当程序A读取数据的时,切换 到程序B去执行,当A读取完数据,让程序B暂停,切换 回程序A执行?



在计算机里 切换 这个名词被细分为两种状态:



挂起:保存程序的当前状态,暂停当前程序; 激活:恢复程序状态,继续执行程序;



这种切换,涉及到了 程序状态的保存和恢复,而且程序AB所需的系统资源(内存、硬盘等)是不一样的,那还需要一个东西来记录程序AB各自需要什么资源,还有系统控制程序AB切换,要一个标志来识别等等,所以就有了一个叫 进程的抽象。


1.1.1 进程的定义


进程是一个具有一定独立功能的程序在一个数据集上的一次动态执行的过程,是操作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体主要由以下三部分组成:


1.程序:描述进程要完成的功能及如何完成;
2.数据集:程序在执行过程中所需的资源;
3.进程控制块:记录进程的外部特征,描述执行变化过程,系统利用它来控制、管理进程,系统感知进程存在的唯一标志。


1.1.2 为什么要有进程


其实上文我们已经分析过了,操作系统之所以要支持多进程,是为了提高CPU的利用率
而为了切换进程,需要进程支持挂起恢复,不同进程间需要的资源不同,所以这也是为什么进程间资源需要隔离,这也是进程是资源分配的最小单位的原因


1.2 什么是线程?


1.2.1 线程的定义


轻量级的进程,基本的CPU执行单元,亦是 程序执行过程中的最小单元,由 线程ID程序计数器寄存器组合堆栈 共同组成。
线程的引入减小了程序并发执行时的开销,提高了操作系统的并发性能。


1.2.2 为什么要有线程?


这个问题也很好理解,进程的出现使得多个程序得以 并发 执行,提高了系统效率及资源利用率,但存在下述问题:




  1. 单个进程只能干一件事,进程中的代码依旧是串行执行。

  2. 执行过程如果堵塞,整个进程就会挂起,即使进程中某些工作不依赖于正在等待的资源,也不会执行。

  3. 多个进程间的内存无法共享,进程间通讯比较麻烦。



线程的出现是为了降低上下文切换消耗,提高系统的并发性,并突破一个进程只能干一件事的缺陷,使得进程内并发成为可能。


1.2.3 进程与线程的区别



  • 1.一个程序至少有一个进程,一个进程至少有一个线程,可以把进程理解做 线程的容器;

  • 2.进程在执行过程中拥有 独立的内存单元,该进程里的多个线程 共享内存;

  • 3.进程可以拓展到 多机,线程最多适合 多核;

  • 4.每个独立线程有一个程序运行的入口、顺序执行列和程序出口,但不能独立运行,需依存于应用程序中,由应用程序提供多个线程执行控制;

  • 5.「进程」是「资源分配」的最小单位,「线程」是 「CPU调度」的最小单位

  • 6.进程和线程都是一个时间段的描述,是 CPU工作时间段的描述,只是颗粒大小不同。


1.3 协作式 & 抢占式


单核CPU,同一时刻只有一个进程在执行,这么多进程,CPU的时间片该如何分配呢?


1.3.1 协作式多任务


早期的操作系统采用的就是协作时多任务,即:由进程主动让出执行权,如当前进程需等待IO操作,主动让出CPU,由系统调度下一个进程。
每个进程都循规蹈矩,该让出CPU就让出CPU,是挺和谐的,但也存在一个隐患:单个进程可以完全霸占CPU


计算机中的进程良莠不齐,先不说那种居心叵测的进程了,如果是健壮性比较差的进程,运行中途发生了死循环、死锁等,会导致整个系统陷入瘫痪!
在这种鱼龙混杂的大环境下,把执行权托付给进程自身,肯定是不科学的,于是由操作系统控制的抢占式多任务横空出世


1.3.2 抢占式多任务


由操作系统决定执行权,操作系统具有从任何一个进程取走控制权和使另一个进程获得控制权的能力。
系统公平合理地为每个进程分配时间片,进程用完就休眠,甚至时间片没用完,但有更紧急的事件要优先执行,也会强制让进程休眠。
这就是所谓的时间片轮转调度



时间片轮转调度是一种最古老,最简单,最公平且使用最广的算法。每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间。
如果在时间片结束时进程还在运行,则CPU将被剥夺并分配给另一个进程。如果进程在时间片结束前阻塞或结束,则CPU当即进行切换。调度程序所要做的就是维护一张就绪进程列表,当进程用完它的时间片后,它被移到队列的末尾。



有了进程设计的经验,线程也做成了抢占式多任务,但也带来了新的——线程安全问题,这个一般通过加锁的方式来解决,这里就不缀述了。


1.4 为什么要引入协程?


上面介绍进程与线程的时候也提到了,之所以引入进程与线程是为了异步并发的执行任务,提高系统效率及资源利用率
但作为Java开发者,我们很清楚线程并发是多么的危险,写出来的异步代码是多么的难以维护。


Java中,我们一般通过回调来处理异步任务,但是当异步任务嵌套时,往往程序就会变得很复杂与难维护


举个例子,当我们需要完成这样一个需求:查询用户信息 --> 查找该用户的好友列表 --> 查找该好友的动态
看一下Java回调的代码


getUserInfo(new CallBack() {
@Override
public void onSuccess(String user) {
if (user != null) {
System.out.println(user);
getFriendList(user, new CallBack() {
@Override
public void onSuccess(String friendList) {
if (friendList != null) {
System.out.println(friendList);
getFeedList(friendList, new CallBack() {
@Override
public void onSuccess(String feed) {
if (feed != null) {
System.out.println(feed);
}
}
});
}
}
});
}
}
});

这就是传说中的回调地狱,如果用kotlin协程实现同样的需求呢?


val user = getUserInfo()
val friendList = getFriendList(user)
val feedList = getFeedList(friendList)

相比之下,可以说是非常简洁了


Kotlin 协程的核心竞争力在于:它能简化异步并发任务,以同步方式写异步代码
这也是为什么要引入协程的原因了:简化异步并发任务


2.到底什么是协程


2.1 什么是协程?


一种非抢占式(协作式)的任务调度模式,程序可以主动挂起或者恢复执行。


2.2 协程与线程的区别是什么?


协程基于线程,但相对于线程轻量很多,可理解为在用户层模拟线程操作;每创建一个协程,都有一个内核态进程动态绑定,用户态下实现调度、切换,真正执行任务的还是内核线程。


线程的上下文切换都需要内核参与,而协程的上下文切换,完全由用户去控制,避免了大量的中断参与,减少了线程上下文切换与调度消耗的资源。


线程是操作系统层面的概念,协程是语言层面的概念


线程与协程最大的区别在于:线程是被动挂起恢复,协程是主动挂起恢复


2.3 协程可以怎样分类?


根据 是否开辟相应的函数调用栈 又分成两类:



  • 有栈协程:有自己的调用栈,可在任意函数调用层级挂起,并转移调度权;

  • 无栈协程:没有自己的调用栈,挂起点的状态通过状态机或闭包等语法来实现;


2.4 Kotlin中的协程是什么?


"假"协程,Kotlin在语言级别并没有实现一种同步机制(锁),还是依靠Kotlin-JVM的提供的Java关键字(如synchronized),即锁的实现还是交给线程处理
因而Kotlin协程本质上只是一套基于原生Java线程池 的封装。


Kotlin 协程的核心竞争力在于:它能简化异步并发任务,以同步方式写异步代码。
下面介绍一些kotin协程中的基本概念


3. 什么是挂起函数?


我们知道使用suspend关键字修饰的函数叫做挂起函数,挂起函数只能在协程体内或者其他挂起函数内使用.


协程内部挂起函数的调用处被称为挂起点,挂起点如果出现异步调用,那么当前协程就被挂起,直到对应的Continuationresume函数被调用才会恢复执行


我们下面来看看挂起函数具体执行的细节



可以看出kotlin协程可以做到一行代码切换线程
这些是怎么做到的呢,主要是通过suspend关键字


3.1 什么是suspend


suspend 的本质,就是 CallBack


suspend fun getUserInfo(): String {
withContext(Dispatchers.IO) {
delay(1000L)
}
return "BoyCoder"
}

不过当我们写挂起函数的时候,并没有写callback,所谓的callback从何而来呢?
我们看下反编译的结果


//                              Continuation 等价于 CallBack
// ↓
public static final Object getUserInfo(Continuation $completion) {
...
return "BoyCoder";
}

public interface Continuation<in T> {
public val context: CoroutineContext
// 相当于 onSuccess 结果
// ↓ ↓
public fun resumeWith(result: Result<T>)
}
复制代码

可以看出


1.编译器会给挂起函数添加一个Continuation参数,这被称为CPS 转换(Continuation-Passing-Style Transformation)
2.suspend函数不能在协程体外调用的原因也可以知道了,就是因为这个Continuation实例的传递


4. 什么是CPS转换


下面用动画演示挂起函数在 CPS 转换过程中,函数签名的变化:


可以看出主要有两点变化
1.增加了Continuation类型的参数
2.返回类型从String转变成了Any


参数的变化我们之前讲过,为什么返回值要变呢?


4.1 挂起函数返回值


挂起函数经过 CPS 转换后,它的返回值有一个重要作用:标志该挂起函数有没有被挂起。
听起来有点奇怪,挂起函数还会不挂起吗?



只要被suspend修饰的函数都是挂起函数,但是不是所有挂起函数都会被挂起
只有当挂起函数里包含异步操作时,它才会被真正挂起



由于 suspend 修饰的函数,既可能返回 CoroutineSingletons.COROUTINE_SUSPENDED,表示挂起
也可能返回同步运行的结果,甚至可能返回 null为了适配所有的可能性,CPS 转换后的函数返回值类型就只能是 Any?了。


4.2 小结


1.suspend修饰的函数就是挂起函数
2.挂起函数,在执行的时候并不一定都会挂起
3.挂起函数只能在其他挂起函数中被调用
4.挂起函数里包含异步操作的时候,它才会真正被挂起


5. Continuation是什么?


Continuation词源是continue,也就是继续,接下来要做的事的意思
放到程序中Continuation则代表了,接下来要执行的代码
以上面的代码为例,当程序运行 getUserInfo() 的时候,它的 Continuation则是下图红框的代码:


Continuation 就是接下来要运行的代码,剩余未执行的代码
理解了 Continuation,以后,CPS就容易理解了,它其实就是:将程序接下来要执行的代码进行传递的一种模式


CPS 转换,就是将原本的同步挂起函数转换成CallBack 异步代码的过程。
这个转换是编译器在背后做的,我们程序员对此无感知。


当然有人会问,这么简单粗暴?三个挂起函数最终变成三个 Callback 吗?
当然不是,思想仍然是CPS的思想,不过需要结合状态机
CPS状态机就是协程实现的核心


6. 状态机


kotlin协程的实现依赖于状态机
想要查看其实现,可以将kotin源码反编译成字节码来查看编译后的代码
关于字节码的分析之前已经有很多人做过了,而且做的很好。下面给出状态机的演示。



  1. 协程实现的核心就是CPS变换与状态机

  2. 协程执行到挂起函数,一个函数如果被挂起了,它的返回值会是:CoroutineSingletons.COROUTINE_SUSPENDED

  3. 挂起函数执行完成后,通过Continuation.resume方法回调,这里的Continuation是通过CPS传入的

  4. 传入的Continuation实际上是ContinuationImpl,resume方法最后会再次回到invokeSuspend方法中

  5. invokeSuspend方法即是我们写的代码执行的地方,在协程运行过程中会执行多次

  6. invokeSuspend中通过状态机实现状态的流转

  7. continuation.label 是状态流转的关键,label改变一次代表协程发生了一次挂起恢复

  8. 通过break label实现goTo的跳转效果

  9. 我们写在协程里的代码,被拆分到状态机里各个状态中,分开执行

  10. 每次协程切换后,都会检查是否发生异常

  11. 切换协程之前,状态机会把之前的结果以成员变量的方式保存在 continuation 中。


以上是状态机流转的大概流程,读者可跟着参考链接,过一下编译后的字节码执行流程后,再来判断这个流程是否正确


7. CoroutineContext是什么?


我们上面说了Continuation是继续要执行的代码,在实现上它也是一个接口


public interface Continuation<in T> {
public val context: CoroutineContext
public fun resumeWith(result: Result<T>)
}

1.Continuation主要由两部分组成,一个context,一个resumeWith方法
2.通过resumeWith方法执行接下去的代码
3.通过context获取上下文资源,保存挂起时的一些状态与资源



CoroutineContext即上下文,主要承载了资源获取,配置管理等工作,是执行环境相关的通用数据资源的统一提供者



CoroutineContext是一个特殊的集合,这个集合它既有Map的特点,也有Set的特点


集合的每一个元素都是Element,每个Element都有一个Key与之对应,对于相同KeyElement是不可以重复存在的Element之间可以通过+号组合起来,Element有几个子类,CoroutineContext也主要由这几个子类组成:



  • Job:协程的唯一标识,用来控制协程的生命周期(newactivecompletingcompletedcancellingcancelled);

  • CoroutineDispatcher:指定协程运行的线程(IODefaultMainUnconfined);

  • CoroutineName: 指定协程的名称,默认为coroutine;

  • CoroutineExceptionHandler: 指定协程的异常处理器,用来处理未捕获的异常.


7.1 CoroutineContext的数据结构


先来看看CoroutineContext的全家福


public interface CoroutineContext {

//操作符[]重载,可以通过CoroutineContext[Key]这种形式来获取与Key关联的Element
public operator fun <E : Element> get(key: Key<E>): E?

//它是一个聚集函数,提供了从left到right遍历CoroutineContext中每一个Element的能力,并对每一个Element做operation操作
public fun <R> fold(initial: R, operation: (R, Element) -> R): R

//操作符+重载,可以CoroutineContext + CoroutineContext这种形式把两个CoroutineContext合并成一个
public operator fun plus(context: CoroutineContext): CoroutineContext

//返回一个新的CoroutineContext,这个CoroutineContext删除了Key对应的Element
public fun minusKey(key: Key<*>): CoroutineContext

//Key定义,空实现,仅仅做一个标识
public interface Key<E : Element>

//Element定义,每个Element都是一个CoroutineContext
public interface Element : CoroutineContext {

//每个Element都有一个Key实例
public val key: Key<*>

//...
}
}

1.CoroutineContext内主要存储的就是Element,可以通过类似map[key] 来取值


2.Element也实现了CoroutineContext接口,这看起来很奇怪,为什么元素本身也是集合呢?主要是为了API设计方便,Element内只会存放自己


3.除了plus方法,CoroutineContext中的其他三个方法都被CombinedContextElementEmptyCoroutineContext重写


4.CombinedContext就是CoroutineContext集合结构的实现,它里面是一个递归定义,Element就是CombinedContext中的元素,而EmptyCoroutineContext就表示一个空的CoroutineContext,它里面是空实现


7.2 为什么CoroutineContext可以通过+号连接


CoroutineContext能通过+号连接,主要是因为重写了plus方法
当通过+号连接时,实际上是包装到了CombinedContext中,并指向上一个Context


如上所示,是一个单链表结构,在获取时也是通过这种方式去查询对应的key,操作大体逻辑都是先访问当前element,不满足,再访问leftelement,顺序都是从rightleft


最近我整理一些Android 开发相关的学习文档、面试题,希望能帮助到大家学习提升,如有需要参考的可以点击链接领取**点击这里免费领取点击这里免费领取





————————————————
版权声明:本文为CSDN博主「码农 小生」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/m0_58350991/article/details/117933297

收起阅读 »

Android 注解知多少

注解的概念什么是注解?注解又称为标注,用于为代码提供元数据。 作为元数据,注解不直接影响你的代码执行,但也有一些类型的注解实际上可以用于这一目的。可以作用在类、方法、变量、参数和包等上。 你可以通俗的理解成“标签”,这个标签可以标记类、方法、变量、参数和包。什...
继续阅读 »

注解的概念

什么是注解?

注解又称为标注,用于为代码提供元数据。 作为元数据,注解不直接影响你的代码执行,但也有一些类型的注解实际上可以用于这一目的。可以作用在类、方法、变量、参数和包等上。 你可以通俗的理解成“标签”,这个标签可以标记类、方法、变量、参数和包。

什么用处?

  1. 生成文档;
  2. 标识代码,便于查看;
  3. 格式检查(编译时);
  4. 注解处理(编译期生成代码、xml文件等;运行期反射解析;常用于三方框架)。

分类

  1. 元注解

元注解是用于定义注解的注解,元注解也是 Java 自带的标准注解,只不过用于修饰注解,比较特殊。 2. 内置的标准注解 就是用在代码上的注解,不同的语言或环境提供有不同的注解(Java Kotlin Android)。使用这些注解后编译器就会进行检查。 3. 自定义注解 用户可以根据自己的需求定义注解。

标准的注解讲解

Java 的标准注解

元注解在后面讲解自定义注解时再一起介绍,这里只先介绍标准注解。

名称描述
@Override检查该方法是否正确地重写了父类的方法。如果重写错误,会报编译错误;
@Deprecated标记过时方法。如果使用该方法,会报编译警告;
@SuppressWarnings指示编译器去忽略注解中声明的警告;
@SafeVarargs忽略任何使用参数为泛型变量的方法或构造函数调用产生的警告;(Java 7 开始支持)
@FunctionalInterface标识一个匿名函数或函数式接口(Java 8 开始支持)

Android 注解库

support.annotation 是 Android 提供的注解库,与 Android Studio 内置的代码检查工具配合,注解可以帮助检测可能发生的问题,例如 null 指针异常和资源类型冲突等。

使用前配置 在 Module 的 build.gradle 中添加配置:

implementation 'com.android.support:support-annotations:版本号'

注意:如果您使用 appcompat 库,则无需添加 support-annotations 依赖项。因为 appcompat 库已经依赖注解库。(一般创建项目时已自动导入)

Null 性注解

  • @Nullable 可以为 null
  • @NonNull 不可为 null

用于给定变量、参数或返回值是否可以为 null 。

import android.support.annotation.NonNull;
...
@NonNull // 检查 onCreateView() 方法本身是否会返回 null。
@Override
public View onCreateView(String name, @NonNull Context context,
@NonNull AttributeSet attrs) {
...
}
...

资源注解

验证资源类型时非常有用,因为 Android 对资源的引用以整型形式传递。如果代码需要一个参数来引用特定类型的资源,可以为该代码传递预期的引用类型 int,但它实际上会引用其他类型的资源,如 R.string.xxx 资源。

Android 中的资源类型有很多,Android 注解为每种资源类型都提供了相对应的注解。

  • AnimatorRes //动画资源(一般为属性动画)
  • AnimRes //动画资源(一般为视图动画)
  • AnyRes //任何类型的资源引用,int 格式
  • ArrayRes //数组资源 e.g. android.R.array.phoneTypes
  • AttrRes //属性资源 e.g. android.R.attr.action
  • BoolRes //布尔资源
  • ColorRes //颜色资源
  • DimenRes //尺寸资源
  • DrawableRes //可绘制资源
  • FontRes //字体资源
  • FractionRes //百分比数字资源
  • IdRes //Id 引用
  • IntegerRes //任意整数类型资源引用
  • InterpolatorRes //插值器资源 e.g. android.R.interpolator.cycle
  • LayoutRes //布局资源
  • MenuRes //菜单资源
  • NavigationRes //导航资源
  • PluralsRes //字符串集合资源
  • RawRes //Raw 资源
  • StringRes //字符串资源
  • StyleableRes //样式资源
  • StyleRes //样式资源
  • TransitionRes //转场动画资源
  • XmlRes //xml 资源
  • 使用 @AnyRes 可以指明添加了此类注解的参数可以是任何类型的 R 资源。
  • 尽管可以使用 @ColorRes 指定某个参数应为颜色资源,但系统不会将颜色整数(采用 RRGGBB 或 AARRGGBB 格式)识别为颜色资源。您可以改用 @ColorInt 注解来指明某个参数必须为颜色整数。

线程注解

线程注解可以检查某个方法是否从特定类型的线程调用。支持以下线程注解:

  • @MainThread
  • @UiThread
  • @WorkerThread
  • @BinderThread
  • @AnyThread

注意:构建工具会将 @MainThread 和 @UiThread 注解视为可互换,因此您可以从 @MainThread 方法调用 @UiThread 方法,反之亦然。不过,如果系统应用有多个视图在不同的线程上,那么界面线程可能会与主线程不同。因此,您应使用 @UiThread 为与应用的视图层次结构关联的方法添加注解,并使用 @MainThread 仅为与应用生命周期关联的方法添加注解。

如果某个类中的所有方法具有相同的线程要求,您可以为该类添加一个线程注解,以验证该类中的所有方法是否从同一类型的线程调用。

值约束注解

值约束注解可以验证所传递参数的值是否在指定范围内:

  • @IntRange
  • @FloatRange
  • @Size

@IntRange 和 @FloatRange 在应用到用户可能会弄错范围的参数时最为有用。

// 确保 alpha 参数是包含 0 到 255 之间的整数值
public void setAlpha(@IntRange(from=0,to=255) int alpha) { ... }

// 确保 alpha 参数是包含 0.0 到 1.0 之间的浮点值
public void setAlpha(@FloatRange(from=0.0, to=1.0) float alpha) {...}

@Size 注解可以检查集合或数组的大小,以及字符串的长度。@Size 注解可用于验证以下特性:

  • 最小大小(例如 @Size(min=2)
  • 最大大小(例如 @Size(max=2)
  • 确切大小(例如 @Size(2)
  • 大小必须是指定数字的倍数(例如 @Size(multiple=2)

例如,@Size(min=1) 可以检查某个集合是否不为空,@Size(3) 可以验证某个数组是否正好包含三个值。

// 确保 location 数组至少包含一个元素
void getLocation(View button, @Size(min=1) int[] location) {
button.getLocationOnScreen(location);
}

权限注解

使用 @RequiresPermission 注解可以验证方法调用方的权限。要检查有效权限列表中是否存在某个权限,请使用 anyOf 属性。要检查是否具有某组权限,请使用 allOf 属性。

// 以确保 setWallpaper() 方法调用方具有 permission.SET_WALLPAPERS 权限
@RequiresPermission(Manifest.permission.SET_WALLPAPER)
public abstract void setWallpaper(Bitmap bitmap) throws IOException;
// 要求 copyImageFile() 方法的调用方具有对外部存储空间的读取权限,以及对复制的映像中的位置元数据的读取权限
@RequiresPermission(allOf = {
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.ACCESS_MEDIA_LOCATION})

public static final void copyImageFile(String dest, String source) {
//...
}

对于 intent 的权限,请在用来定义 intent 操作名称的字符串字段上添加权限要求:

@RequiresPermission(android.Manifest.permission.BLUETOOTH)
public static final String ACTION_REQUEST_DISCOVERABLE =
"android.bluetooth.adapter.action.REQUEST_DISCOVERABLE";

如果您需要对内容提供程序拥有单独的读取和写入访问权限,则需要将每个权限要求封装在 @RequiresPermission.Read 或 @RequiresPermission.Write 注解中:

@RequiresPermission.Read(@RequiresPermission(READ_HISTORY_BOOKMARKS))
@RequiresPermission.Write(@RequiresPermission(WRITE_HISTORY_BOOKMARKS))
public static final Uri BOOKMARKS_URI = Uri.parse("content://browser/bookmarks");

返回值注解

使用 @CheckResult 注解可检查是否对方法的返回值进行处理,验证是否实际使用了方法的结果或返回值。

这个可能比较难理解,这里借助 Java String.trim() 举个例子解释一下(通过例子应该能够很直观的理解了,不需要过多解释了):

String str = new String("    http://www.ocnyang.com    ");
// 删除头尾空白
System.out.println("网站:" + str.trim() + "。");//打印结果:网站:www.ocnyang.com。
System.out.println("网站:" + str + "。");//打印结果:网站: http://www.ocnyang.com

以下示例为 checkPermissions() 方法添加了注解,以确保会实际引用该方法的返回值。此外,这还会将 enforcePermission() 方法指定为要向开发者建议的替代方法:

@CheckResult(suggest="#enforcePermission(String,int,int,String)")
public abstract int checkPermission(@NonNull String permission, int pid, int uid);

CallSuper 注解

@CallSuper 注解主要是用来强调在覆盖父类方法的时候,在实现父类的方法时及时调用对应的 super.xxx() 方法,当使用 @CallSuper 修饰了某个方法,如果子类覆盖父类该方法后没有实现对父类方法的调用就会报错。

Keep 注解

使用 @Keep 注解可以确保在构建混淆缩减代码大小时,不会移除带有该注解的类或方法。 该注解通常添加到通过反射访问的方法和类,以防止编译器将代码视为未使用。

注意:使用 @Keep 添加注解的类和方法会始终包含在应用的 APK 中,即使您从未在应用逻辑中引用这些类和方法也是如此。

代码公开范围注解(了解)

单元测试中可能要访问到一些不可见的类、函数或者变量,这时可以使用@VisibleForTesting 注解来对其可见。

Typedef 注解

枚举 Enum 在 Java 中是一个完整的类。而枚举中的每一个值在枚举类中都是一个对象。所以在我们使用时枚举的值将比整数常量消耗更多的内存。 那么我们最好使用常量来替代枚举。可是使用了常量代替后又不能限制取值了。上面这两个注解就是为了解决这个问题的。

@IntDef 和 @StringDef 注解是 Android 提供的魔术变量注解,您可以创建整数集和字符串集的枚举来代理 Java 的枚举类。 它将帮助我们在编译代码时期像 Enum 那样选择变量的功能。 @IntDef 和 typedef 作用非常类似,你可以创建另外一个注解,然后用 @IntDef 指定一个你期望的整型常量值列表,最后你就可以用这个定义好的注解修饰你的 API 了。接下来我们来使用 @IntDef 来替换 Enum 看一下.

public class MainActivity extends Activity {
public static final int SUNDAY = 0;
public static final int MONDAY = 1;
{...省略部分}

@IntDef({SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY})
@Retention(RetentionPolicy.SOURCE)
public @interface WeekDays {
}

@WeekDays
int currentDay = SUNDAY;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

setCurrentDay(WEDNESDAY);

@WeekDays int today = getCurrentDay();
switch (today) {
case SUNDAY:
break;
case MONDAY:
break;
{...省略部分}
default:
break;
}
}

/**
* 参数只能传入在声明范围内的整型,不然编译通不过
* @param currentDay
*/

public void setCurrentDay(@WeekDays int currentDay) {
this.currentDay = currentDay;
}

@WeekDays
public int getCurrentDay() {
return currentDay;
}
}

说明:

  1. 声明一些必要的 int 常量
  2. 声明一个注解为 WeekDays
  3. 使用 @IntDef 修饰 WeekDays,参数设置为待枚举的集合
  4. 使用 @Retention(RetentionPolicy.SOURCE) 指定注解仅存在与源码中,不加入到 class 文件中

需要在调用时只能传入指定类型,如果传入类型不对,编译不通过。

我们也可以指定整型值作为标志位,也就是说这些整型值可以使用 ’|’ 或者 ’&’ 进行与或等操作。如果我们把上面代码中的注解定义为如下标志位:

@IntDef(flag = true, value = {SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY})
public @interface Flavour {
}

那么可以如下调用:

setCurrentDay(SUNDAY & WEDNESDAY);

@StringDef 同理。

自定义注解

Java 元注解

名字描述
@Retention标识这个注解怎么保存,是只在代码中,还是编入class文件中,或者是在运行时可以通过反射访问
@Documented标记这些注解是否包含在用户文档中,即包含到 Javadoc 中去
@Target标记这个注解的作用目标
@Inherited标记这个注解是继承于哪个注解类
@RepeatableJava 8 开始支持,标识某注解可以在同一个声明上使用多次

@Retention
表示注解保留时间长短。可选的参数值在枚举类型 java.lang.annotation.RetentionPolicy 中,取值为:

  • RetentionPolicy.SOURCE:注解只在源码阶段保留,在编译器进行编译时它将被丢弃忽视,不会写入 class 文件;
  • RetentionPolicy.CLASS:注解只被保留到编译进行的时候,会写入 class 文件,它并不会被加载到 JVM 中;
  • RetentionPolicy.RUNTIME:注解可以保留到程序运行的时候,它会被加载进入到 JVM 中,所以在程序运行时可以反射获取到它们。

@Target
用于指明被修饰的注解最终可以作用的目标是谁,也就是指明,你的注解到底是用来修饰方法的?修饰类的?还是用来修饰字段属性的。 可能的值在枚举类 java.lang.annotation.ElementType 中,包括:

  • ElementType.TYPE:允许被修饰的注解作用在类、接口和枚举上;
  • ElementType.FIELD:允许作用在属性字段上;
  • ElementType.METHOD:允许作用在方法上;
  • ElementType.PARAMETER:允许作用在方法参数上;
  • ElementType.CONSTRUCTOR:允许作用在构造器上;
  • ElementType.LOCAL_VARIABLE:允许作用在本地局部变量上;
  • ElementType.ANNOTATION_TYPE:允许作用在注解上;
  • ElementType.PACKAGE:允许作用在包上。

@Target 注解的参数也可以接收一个数组,表示可以作用在多种目标类型上,如: @Target({ElementType.FIELD, ElementType.LOCAL_VARIABLE})

自定义注解

你可以根据需要自定义一些自己的注解,然后要在需要的地方加上自定义的注解。需要注意的是每当自定义注解时,相对应的一定要有处理这些自定义注解的流程,要不然可以说是没有实用价值的。注解真真的发挥作用,主要就在于注解处理方法。 注解的处理一般分为两种:

  • 保留注解信息到运行时,这时通过反射操作获取到类、方法和字段的注解信息,然后做相对应的处理
  • 保留到编译期,一般此方式是利用 APT 注释解释器,根据注解自动生成代码。简单来说,可以通过 APT,根据规则,帮我们生成代码、生成类文件。ButterKnife、Dagger、EventBus 等开源库都是利用注解实现的。

因为自定义注解的涉及到的内容较多。本期先不对自定义注解详细展开介绍,后续找时间再对它进行单独的文章讲解。

收起阅读 »

手把手带你走一遍Compose重组流程

前言我们都知道 Jetpack Compose 是一套声明式 UI 系统,当 UI 组件所依赖的状态发生改变时会自动发生重绘刷新,这个过程被官方称作重组,前面已经有人总结过 Compose 的重组范围了,文章详见 《Compose 的重组会影响性能吗?聊一聊 ...
继续阅读 »

前言

我们都知道 Jetpack Compose 是一套声明式 UI 系统,当 UI 组件所依赖的状态发生改变时会自动发生重绘刷新,这个过程被官方称作重组,前面已经有人总结过 Compose 的重组范围了,文章详见 《Compose 的重组会影响性能吗?聊一聊 recomposition scope》 ,并且也有人总结过重组过程使用到的快照系统,文章详见《Jetpack Compose · 快照系统》。本文就就带领大家一起来看看 Compose 源码中从状态更新到 recompose 过程的发生到底是如何进行的,并且快照系统是在 recompose 过程中如何被使用到的。

意义

本文将通过阅读源码的方式来解读 recompose 流程,阅读源码其实每个人都可以做到,但阅读源码本身是一个非常枯燥的过程,源码中存在着大量逻辑分支导致许多人看着看着就被绕晕了。本文将带领大家以 recompose 主线流程为导向来进行源码过程分析,许多与主线流程无关的逻辑分支都已被我剔除了,大家可以放心进行阅读。希望后来者能够在本文源码过程分析基础上继续深入探索下去。

⚠️ Tips:由于 recompose 流程十分复杂,本文目前仅对 recompose 主线流程进行了描述,其中很多很多技术细节没有深挖,等待后续进行补充。本人采用动静结合的方式进行源码分析,可能有些case流程没有覆盖到,如果文章存在错误欢迎在评论区进行补充。

recompose 流程分析

从 MutableState 更新开始

当你为 MutableState 赋值时将会默认调用 MutableState 的扩展方法 MutableState.setValue

// androidx.compose.runtime.SnapshotState
inline operator fun MutableState.setValue(thisObj: Any?, property: KProperty<*>, value: T) {
this.value = value
}

通过查看 mutableStateOf 源码我们可以发现 MutableState 实际上是一个 SnapshotMutableStateImpl 类型实例

// androidx.compose.runtime.SnapshotState
fun mutableStateOf(
value: T,
policy: SnapshotMutationPolicy<T> = structuralEqualityPolicy()
): MutableState = createSnapshotMutableState(value, policy)

// androidx.compose.runtime.ActualAndroid.android
internal actual fun createSnapshotMutableState(
value: T,
policy: SnapshotMutationPolicy<T>
): SnapshotMutableState = ParcelableSnapshotMutableState(value, policy)

// androidx.compose.runtime.ParcelableSnapshotMutableState
internal class ParcelableSnapshotMutableState<T>(
value: T,
policy: SnapshotMutationPolicy
) : SnapshotMutableStateImpl(value, policy), Parcelable

当 value 属性发生改变时会调用这个属性的 setter ,当然如果读取状态时也会走 getter。

此时的next是个 StateStateRecord 实例,其真正记录着当前state状态信息(通过当前value的getter与setter就可以看出)。此时首先会对当前值和要更新的值根据规则进行diff判断。当确定发生改变时会调用到 StateStateRecord 的 overwritable 方法。

internal open class SnapshotMutableStateImpl<T>(
value: T,
override val policy: SnapshotMutationPolicy
) : StateObject, SnapshotMutableState {
@Suppress("UNCHECKED_CAST")
override var value: T
get() = next.readable(this).value
set(value) = next.withCurrent {
if (!policy.equivalent(it.value, value)) {
// 此时的this还是当前SnapshotMutableStateImpl
next.overwritable(this, it) {
this.value = value // 此时的this指向的next,这部操作也就是更新next其中的value
}
}
}
...
private var next: StateStateRecord = StateStateRecord(value)
}

接下来会通过 Snapshot.current 获取当前上下文中的 Snapshot,如果你对 mutableState 更新操作在非Compose Scope中,其返回的实例类型是 GlobalSnapshot ,否则就是一个 MutableSnapShot。这将会影响到后续写入通知的执行流程(因为毕竟需要进行 recompose 嘛)。

⚠️ Tips:GlobalSnapshot 是 MutableSnapShot 的子类

// androidx.compose.runtime.snapshots.Snapshot
internal inline fun T.overwritable(
state: StateObject,
candidate: T,
block: T.() -> R
): R {
var snapshot: Snapshot = snapshotInitializer
return sync {
snapshot = Snapshot.current
this.overwritableRecord(state, snapshot, candidate).block() // 更新 next
}.also {
notifyWrite(snapshot, state) // 写入通知
}
}

我们进入 overwritableRecord 看看其中做了什么,注意此时 state 其实是 mutableState。在这其中通过 recordModified 方法记录了修改。我们可以看到此时将当前修改的 state 添加到当前 Snapshot 的 modified 中了,这个后续会用到的。

// androidx.compose.runtime.snapshots.Snapshot
internal fun T.overwritableRecord(
state: StateObject,
snapshot: Snapshot,
candidate: T
): T {
if (snapshot.readOnly) {
snapshot.recordModified(state)
}
val id = snapshot.id

if (candidate.snapshotId == id) return candidate

val newData = newOverwritableRecord(state, snapshot)
newData.snapshotId = id

snapshot.recordModified(state) // 记录修改

return newData
}

// androidx.compose.runtime.snapshots.Snapshot
override fun recordModified(state: StateObject) {
(modified ?: HashSet().also { modified = it }).add(state)
}

可能你对 mutableState 更新操作是否在 ComposeScope 中而感到困惑,举个例子其实就明白了。recompose 能够执行到就在 ComposeScope 中,不能执行到就不在 ComposeScope 中。

这个在后面 takeMutableSnapshot读观察者与写观察者 部分是会进行解释。

var display by mutableStateOf("Init")
@Preview
@Composable
fun Demo() {
Text (
text = display,
fontSize = 50.sp,
modifier = Modifier.clickable {
display = "change" // recompose不能执行到,此时是 GlobalSnapshot
}
)
display = "change" // recompose能够执行到,此时是 MutableSnapShot
}

接下来就是通过 notifyWrite 执行事件通知此时可以看到调用了写观察者 writeObserver 。

// androidx.compose.runtime.snapshots.Snapshot
@PublishedApi
internal fun notifyWrite(snapshot: Snapshot, state: StateObject) {
snapshot.writeObserver?.invoke(state)
}

此时会根据当前 Snapshot 不同而调用到不同的写观察者 writeObserver 。

GlobalSnapshot 写入通知

全局的写入观察者是在 setContent 时就进行了注册, 此时会回调 registerGlobalWriteObserver 的尾lambda,可以看到这里就一个channel (没错就是Kotlin协程那个热数据流Channel),我门可以看到很容易看到在上方以AndroidUiDispatcher.Main 作为调度器的 CoroutineScope 中进行了挂起等待消费,所以执行流程自然会进入 sendApplyNotifications() 之中。 (AndroidUiDispatcher.Main 与 Choreographer 息息相关,篇幅有限就不展开讨论了,有兴趣可以自己去跟源码)

internal object GlobalSnapshotManager {
private val started = AtomicBoolean(false)

fun ensureStarted() {
if (started.compareAndSet(false, true)) {
val channel = Channel<Unit>(Channel.CONFLATED)
CoroutineScope(AndroidUiDispatcher.Main).launch {
channel.consumeEach {
Snapshot.sendApplyNotifications()
}
}
Snapshot.registerGlobalWriteObserver {
channel.offer(Unit)
}
}
}
}

sendApplyNotifications

接下来,我们进入 sendApplyNotifications() 其中看看做了什么,可以看到这里使用我们前面提到的那个 modified ,当发生修改时 changes 必然为 true,所以接着会调用到 advanceGlobalSnapshot

// androidx.compose.runtime.snapshots.Snapshot
fun sendApplyNotifications() {
val changes = sync {
currentGlobalSnapshot.get().modified?.isNotEmpty() == true
}
if (changes)
advanceGlobalSnapshot()
}

我们继续往下跟下去走到了 advanceGlobalSnapshot ,此时将所有 modified 取出并便利调用 applyObservers 中包含的所有观察者。

// androidx.compose.runtime.snapshots.Snapshot
private fun advanceGlobalSnapshot() = advanceGlobalSnapshot { }

private fun advanceGlobalSnapshot(block: (invalid: SnapshotIdSet) -> T): T {
val previousGlobalSnapshot = currentGlobalSnapshot.get()
val result = sync {
takeNewGlobalSnapshot(previousGlobalSnapshot, block)
}
val modified = previousGlobalSnapshot.modified
if (modified != null) {
val observers: List<(Set, Snapshot) -> Unit> = sync { applyObservers.toMutableList() }
observers.fastForEach { observer ->
observer(modified, previousGlobalSnapshot)
}
}
....
return result
}

applyObservers之recompositionRunner

据我调查此时 applyObservers 中包含的观察者仅有两个,一个是 SnapshotStateObserver.applyObserver 用来更新快照状态信息,另一个就是 recompositionRunner 用来处理 recompose流程 的。由于我们是在研究recompose 流程的所以就不分开去讨论了。我们来看看处理 recompose 的 observer 都做了什么,首先他将所有改变的 mutableState 添加到了 snapshotInvalidations,这个后续会用到。后面可以看到有一个resume,说明lambda的最后调用的 deriveStateLocked 返回了一个协程 Continuation 实例。使得挂起点位置恢复执行,所以我们进入deriveStateLocked 看看这个协程 Continuation 实例到底是谁。

// androidx.compose.runtime.Recomposer
@OptIn(ExperimentalComposeApi::class)
private suspend fun recompositionRunner(
block: suspend CoroutineScope.(parentFrameClock: MonotonicFrameClock) -> Unit
) {
withContext(broadcastFrameClock) {
...
// 负责处理 recompose 的 observer 就是他
val unregisterApplyObserver = Snapshot.registerApplyObserver {
changed, _ ->
synchronized(stateLock) {
if (_state.value >= State.Idle) {
snapshotInvalidations += changed
deriveStateLocked()
} else null
}?.resume(Unit)
}
....
}
}

通过函数返回值可以看到这是一个可取消的Continuation实例 workContinuation

// androidx.compose.runtime.Recomposer
private fun deriveStateLocked(): CancellableContinuation<Unit>? {
....
return if (newState == State.PendingWork) {
workContinuation.also {
workContinuation = null
}
} else null
}

那这个workContinuation是在哪里赋值的呢,我们很容易就找到了其唯一被赋值的地方。此时 workContinuation 就是 co,此时resume也就是恢复执行 awaitWorkAvailable 调用挂起点。

// androidx.compose.runtime.Recomposer
private suspend fun awaitWorkAvailable() {
if (!hasSchedulingWork) {
suspendCancellableCoroutine<Unit> { co ->
synchronized(stateLock) {
if (hasSchedulingWork) {
co.resume(Unit)
} else {
workContinuation = co
}
}
}
}
}

runRecomposeAndApplyChanges 三步骤

我们可以找到在 runRecomposeAndApplyChanges 中调用 awaitWorkAvailable 而产生了挂起,所以此时会恢复调用 runRecomposeAndApplyChanges ,这里主要有三步操作接下来进行介绍

// androidx.compose.runtime.Recomposer
suspend fun runRecomposeAndApplyChanges() = recompositionRunner { parentFrameClock ->
val toRecompose = mutableListOf()
val toApply = mutableListOf()
while (shouldKeepRecomposing) {
awaitWorkAvailable()
// 从这开始恢复执行
if (
synchronized(stateLock) {
if (!hasFrameWorkLocked) {
// 步骤1
recordComposerModificationsLocked()
!hasFrameWorkLocked
} else false
}
) continue

// 等待Vsync信号,类似于传统View系统中scheduleTraversals?
parentFrameClock.withFrameNanos { frameTime ->
...
trace("Recomposer:recompose") {
synchronized(stateLock) {
recordComposerModificationsLocked()
// 步骤2
compositionInvalidations.fastForEach { toRecompose += it }
compositionInvalidations.clear()
}

val modifiedValues = IdentityArraySet()
val alreadyComposed = IdentityArraySet()
while (toRecompose.isNotEmpty()) {
try {
toRecompose.fastForEach { composition ->
alreadyComposed.add(composition)
// 步骤3
performRecompose(composition, modifiedValues)?.let {
toApply += it
}
}
} finally {
toRecompose.clear()
}
....
}
....
}
}
}
}

对于这三个步骤,我们分别来看首先是步骤1调用了 recordComposerModificationsLocked 方法, 还记得 snapshotInvalidations 嘛, 他记录着所有更改的 mutableState,此时回调所有已知composition的recordModificationsOf 方法。

// androidx.compose.runtime.Recomposer
private fun recordComposerModificationsLocked() {
if (snapshotInvalidations.isNotEmpty()) {
snapshotInvalidations.fastForEach { changes ->
knownCompositions.fastForEach { composition ->
composition.recordModificationsOf(changes)
}
}
snapshotInvalidations.clear()
if (deriveStateLocked() != null) {
error("called outside of runRecomposeAndApplyChanges")
}
}
}

经过一系列调用会将所有依赖当前 mutableState 的所有 Composable Scope 存入到 compositionInvalidations 这个 List 中。

// androidx.compose.runtime.Recomposer
internal override fun invalidate(composition: ControlledComposition) {
synchronized(stateLock) {
if (composition !in compositionInvalidations) {
compositionInvalidations += composition
deriveStateLocked()
} else null
}?.resume(Unit)
}

步骤2就很简单了,将 compositionInvalidations 的所有元素转移到了 toRecompose,而步骤3则是 recompose的重中之重,通过 performRecompose 使所有受到影响的 Composable Scope 重新执行。

performRecompose

我们可以看到 performRecompose 中间接调用了 composing ,而其中最关键 recompose 也在回调中完成,那么我们需要再进入 composing 看看什么时候会回调。

// androidx.compose.runtime.Recomposer
private fun performRecompose(
composition: ControlledComposition,
modifiedValues: IdentityArraySet<Any>?
): ControlledComposition? {
if (composition.isComposing || composition.isDisposed) return null
return if (
composing(composition, modifiedValues) {
if (modifiedValues?.isNotEmpty() == true) {
composition.prepareCompose {
modifiedValues.forEach { composition.recordWriteOf(it) }
}
}
composition.recompose() // 真正发生recompose的地方
}
) composition else null
}

composing 内部首先拍摄了一次快照,然后将我们的recompose过程在这次快照中执行,最后进行了apply。又关于快照系统的讲解详见 《Jetpack Compose · 快照系统》

// androidx.compose.runtime.Recomposer
private inline fun composing(
composition: ControlledComposition,
modifiedValues: IdentityArraySet<Any>?,
block: () -> T
): T {
val snapshot = Snapshot.takeMutableSnapshot(
readObserverOf(composition), writeObserverOf(composition, modifiedValues)
)
try {
return snapshot.enter(block)
} finally {
applyAndCheck(snapshot)
}
}

takeMutableSnapshot 读观察者与写观察者

值得注意的是此时调用的 takeMutableSnapshot 方法同时传入了一个读观察者和写观察者,而这两个观察者在什么时机回调呢?当我们每次 recompose 时都会拍摄一次快照,然后我们的重新执行过程在这次快照中执行,在重新执行过程中如果出现了 mutableState 的读取或写入操作都会相应的回调这里的读观察者和写观察者。也就说明每次recompose都会进行重新一次绑定。 读观察者回调时机比较好理解,写观察者在什么时机回调呢? 还记得我们刚开始说的 GlobalSnapshot 和 MutableSnapshot 嘛?

到这里我们一直都在分析 GlobalSnapshot 这条执行过程,通过调用 takeMutableSnapshot 将返回一个 MutableSnapshot 实例,我们的recompose重新执行过程发生在当前MutableSnapshot 实例的enter 方法中,此时重新执行过程中通过调用Snapshot.current 将返回当前MutableSnapshot 实例,所以重新执行过程中发生的写操作就会回调 takeMutableSnapshot 所传入的写观察者。也就是以下这种情况,当 Demo 发生recompose时 display所在 Snapshot 就是拍摄的MutableSnapshot 快照。

var display by mutableStateOf("Init")
@Preview
@Composable
fun Demo() {
Text (
text = display,
fontSize = 50.sp
)
display = "change" // recompose能够执行到,此时是 MutableSnapShot
}

MutableSnapshot 写入通知

接下来,我们来看看 takeMutableSnapshot 的写观察者是如何实现的。此时会将更新的值传入当前recompose composition 的 recordWriteOf 方法。

// androidx.compose.runtime.Recomposer
private fun writeObserverOf(
composition: ControlledComposition,
modifiedValues: IdentityArraySet<Any>?
): (Any) -> Unit {
return { value ->
composition.recordWriteOf(value)
modifiedValues?.add(value)
}
}

通过对于流程分析发现,实际上在recompose过程中进行状态写入操作时,并不会通过写观察者立即进行recompose 过程,而是等待到当前recompose过程结束后进行 apply 时再进行重新 recompose。

applyAndCheck

让我们回到Recomposer的 composing 方法,我们通过 applyAndCheck 完成后续 apply 操作。applyAndCheck 内部使用了 MutableSnapshot.apply

// androidx.compose.runtime.Recomposer
private inline fun composing(
composition: ControlledComposition,
modifiedValues: IdentityArraySet<Any>?,
block: () -> T
): T {
val snapshot = Snapshot.takeMutableSnapshot(
readObserverOf(composition), writeObserverOf(composition, modifiedValues)
)
try {
return snapshot.enter(block)
} finally {
applyAndCheck(snapshot) // 在这里
}
}

private fun applyAndCheck(snapshot: MutableSnapshot) {
val applyResult = snapshot.apply()
if (applyResult is SnapshotApplyResult.Failure) {
error(
"Unsupported concurrent change during composition. A state object was " +
"modified by composition as well as being modified outside composition."
)
}
}

apply中使用的applyObservers

我们再进入MutableSnapshot.apply 一探究竟,此时将当前 modified 在 snapshot.recordModified(state) 已经更新过了,忘记的话可以回头看看,前面已经讲过了。此时仍然使用了 applyObservers 进行遍历通知。这个applyObservers 其实是个静态变量,所以不同的 GlobalSnapshot 与MutableSnapshot 可以共享,接下来仍然通过预先订阅好的 recompositionRunner 用来处理 recompose 过程,详见 applyObservers之recompositionRunner,接下来的recompose流程就完全相同了。

// androidx.compose.runtime.snapshots.Snapshot
open fun apply(): SnapshotApplyResult {
val modified = modified
....
val (observers, globalModified) = sync {
validateOpen(this)
if (modified == null || modified.size == 0) {
....
} else {
....
applyObservers.toMutableList() to globalModified
}
}
....
if (modified != null && modified.isNotEmpty()) {
observers.fastForEach {
it(modified, this)
}
}
return SnapshotApplyResult.Success
}
收起阅读 »

偷师 - Kotlin 委托

关键字synchorinzedCAS委托/代理模式委托要理解 kotlin-委托 的作用和用法首先要理解什么是委托。初看委托二字如果不太理解的话不妨转换成代理二字。委托模式和代理模式是一种设计模式的两种称呼而已。委托/代理模式代理模式,字面...
继续阅读 »

关键字

  • synchorinzed
  • CAS
  • 委托/代理模式

委托

要理解 kotlin-委托 的作用和用法首先要理解什么是委托。初看委托二字如果不太理解的话不妨转换成代理二字。委托模式和代理模式是一种设计模式的两种称呼而已。

委托/代理模式

代理模式,字面理解就是自己不方便做或者不能做的事情,需要第三方代替来做,最终通过第三方来达到自己想要的目的或效果。举例:员工小李在B总公司打工,B总成天让小李加班不给加班费,小李忍受不住了,就想去法院告B总。虽然法律上允许打官司不请律师,允许自辩。但是小李第一不熟悉法律起诉的具体流程,第二嘴比较笨,人一多腿就抖得厉害。因此,小李决定去找律师帮忙打官司。找律师打官司和自己打官司相比,有相同的地方,也有不同的地方。

相同的地方在于:

  • 都需要提交原告的资料,如姓名、年龄、事情缘由、想达到的目的。
  • 都需要经过法院的取证调查,开庭争辩等过程。
  • 最后拿到审判结果。

不同地方在于:

  • 小李省事了,让专业的人做专业的事,不需要自己再去了解法院那一套繁琐复杂的流程。
  • 把握更大了。

通过上面的例子,我们注意到代理模式有几个重点。

  • 被代理的角色(小李)
  • 代理角色(律师)
  • 协议(不管是代理和被代理谁去做,都需要做的事情,抽象出来就是协议)

UML 类图: image

代码实现如下:

//协议
interface Protocol{
//登记资料
public void register(String name);
//调查案情,打官司
public void dosomething();
//官司完成,通知雇主
public void notifys();
}

//代理角色:律师类
class LawyerProxy implements Protocol{
private Employer employer;
public LawyerProxy(Employer employer){
this.employer=employer;
}
@Override
public void register(String name) {
// TODO Auto-generated method stub
this.employer.register(name);
}
public void collectInfo(){
System.out.println("作为律师,我需要根据雇主提供的资料,整理与调查,给法院写出书面文字,并提供证据。");
}
@Override
public void dosomething() {
// TODO Auto-generated method stub
collectInfo();
this.employer.dosomething();
finish();
}
public void finish(){
System.out.println("本次官司打完了...............");
}
@Override
public void notifys() {
// TODO Auto-generated method stub
this.employer.notifys();
}
}

//被代理角色:雇主类
class Employer implements Protocol{
String name=null;
@Override
public void register(String name) {
// TODO Auto-generated method stub
this.name=name;
}
@Override
public void dosomething() {
// TODO Auto-generated method stub
System.out.println("我是'"+this.name+"'要告B总,他每天让我不停的加班,还没有加班费。");
}
@Override
public void notifys() {
// TODO Auto-generated method stub
System.out.println("法院裁定,官司赢了,B总需要赔偿10万元精神补偿费。");
}
}

public class Client {
public static void main(String[] args) {
Employer employer=new Employer();
System.out.println("我受不了了,我要打官司告老板");
System.out.println("找律师解决一下吧......");
Protocol lawyerProxy=new LawyerProxy(employer);
lawyerProxy.register("朵朵花开");
lawyerProxy.dosomething();
lawyerProxy.notifys();
}
}
复制代码

运行后,打印如下:

我受不了了,我要打官司告老板
找律师解决一下吧......
作为律师,我需要根据雇主提供的资料,整理与调查,给法院写出书面文字,并提供证据。
我是'朵朵花开'要告B总,他每天让我不停的加班,还没有加班费。
本次官司打完了...............
法院裁定,官司赢了,B总需要赔偿10万元精神补偿费。
复制代码

类委托

对代理模式有了一些了解之后我们再来看 kotlin-类委托 是如何实现的:

interface Base {
fun print()
}

class BaseImpl(val x: Int) : Base {
override fun print() { print(x) }
}

class Derived(b: Base) : Base by b

fun main() {
val b = BaseImpl(10)
Derived(b).print()
}
复制代码

这是Kotlin 语言中文站的示例,转成 Javaa 代码如下:


public interface Base {
void print();
}

// BaseImpl.java
public final class BaseImpl implements Base {
private final int x;

public void print() {
int var1 = this.x;
boolean var2 = false;
System.out.print(var1);
}

public final int getX() {
return this.x;
}

public BaseImpl(int x) {
this.x = x;
}
}

// Derived.java
public final class Derived implements Base {
// $FF: synthetic field
private final Base $$delegate_0;

public Derived(@NotNull Base b) {
Intrinsics.checkNotNullParameter(b, "b");
super();
this.$$delegate_0 = b;
}

public void print() {
this.$$delegate_0.print();
}
}

// DelegateTestKt.java
public final class DelegateTestKt {
public static final void main() {
BaseImpl b = new BaseImpl(10);
(new Derived((Base)b)).print();
}

// $FF: synthetic method
public static void main(String[] var0) {
main();
}
}
复制代码

可以看到在 Derived 中已经实现了 Base 接口的抽象方法,而且方法的实际调用者是构造对象时传入的 Base 实例对象,也就是 BaseImpl 的实例对象。

对比上文介绍的代理模式:

  • Base:代理协议
  • BaseImpl:代理角色
  • Derived:被代理被代理角色

这样看的话,d上文类委托示例的结果包括重写方法实现和成员变量产生的结果的原因也就清晰明了了。

属性委托

kotlin 标准库中提供的属性委托有:

  • lazy:延迟属性;
  • Delegates.notNull():不能为空;
  • Delegates.observable():可观察属性;
  • Delegates.vetoable():可观察属性,可拒绝修改属性;

lazy 延迟属性下面再来分析,先来看 Delegates 的几个方法。

在 Delegate.kt 文件中定义了提供的标准属性委托方法,代码量很少就不贴代码了。可以看到三种委托方法都返回 ReadWriteProperty 接口的实例对象,它们的顶层接口是 ReadOnlyProperty 接口。名字就很提现它们各自的功用了:

  • ReadOnlyProperty:仅用于可读属性,val
  • ReadWriteProperty:用于可读-写属性,var

在属性委托的实现里,对应代理模式的角色如下:

  • 协议:ReadOnlyProperty 和 ReadWriteProperty
  • 代理者:Delegate
  • 被代理者:实际使用属性。

Delegates.notNull() 比较简单,拿它来分析下属性委托是如何实现的。

private class NotNullVar<T : Any>() : ReadWriteProperty<Any?, T> {
private var value: T? = null

public override fun getValue(thisRef: Any?, property: KProperty<*>): T {
return value ?: throw IllegalStateException("Property ${property.name} should be initialized before get.")
}

public override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
this.value = value
}
}
复制代码
class DelegateTest {
private val name: String by Delegates.notNull()
}
复制代码

kotlin 转 Java

public final class DelegateTest {
// $FF: synthetic field
static final KProperty[] $$delegatedProperties = new KProperty[]{(KProperty)Reflection.property1(new PropertyReference1Impl(DelegateTest.class, "name", "getName()Ljava/lang/String;", 0))};
private final ReadWriteProperty name$delegate;

private final String getName() {
return (String)this.name$delegate.getValue(this, $$delegatedProperties[0]);
}

public DelegateTest() {
this.name$delegate = Delegates.INSTANCE.notNull();
}
}
复制代码

可以看到 name 属性委托给了 NotNullVar 的 value 属性。当访问 name 属性时,其实访问的是 NotNullVar 的 value 属性。

自定义委托

上文提到 Delegates 中的委托方法都返回 ReadWriteProperty 接口的实例对象。如果需要自定义委托的话当然也是通过实现 ReadWriteProperty 接口了。

  • var 属性自定义委托:继承 ReadWriteProperty 接口,并实现 getValue()、setValue() 方法;
  • val 属性自定义委托:实现 ReadOnlyProperty 接口,并实现 getValue 方法。
public override operator fun getValue(thisRef: T, property: KProperty<*>): V

public operator fun setValue(thisRef: T, property: KProperty<*>, value: V)
复制代码

参数如下:

  • thisRef —— 必须与属性所有者类型相同或者是其超类型,通俗说就是属性所在类的类型或其父类型;
  • property —— 必须是 KProperty<*> 类型或其超类型。

Lazy

lazy 放到这里来分析是因为它虽然也是将属性委托给了其他类的属性,但它并没有继承 ReadWriteProperty 或 ReadOnlyProperty 接口并不是标准的属性委托。

lazy 源码如下:

public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)

public actual fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> =
when (mode) {
LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer)
LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)
LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)
}
复制代码

lazy 函数接收两个参数:

  • LazyThreadSafetyMode:线程安全模式;
  • initializer:初始化函数。

LazyThreadSafetyMode:不同模式的作用如下:

  • SYNCHRONIZED:通过 Volatile + synchorinzed 锁的方式保证在多线程情况下初始化函数仅调用一次,变量仅赋值一次;
  • PUBLICATION:通过 Volatile + CAS 的方式保证在多线程情况下变量仅赋值一次;
  • NONE:线程不安全。

lazy 函数返回 Lazy 接口实例。

注意:除非你能保证 lazy 实例的永远不会在多个线程初始化,否则不应该使用 NONE 模式。

lazy 函数会根据所选模式的不同返回不同的实例对象:SynchronizedLazyImplSafePublicationLazyImplUnsafeLazyImpl。这三者之间最大的的区别在于 getter() 函数的实现,但不管如何最终都是各自类中的 value 属性代理 lazy 函数所修饰的属性。

synchorinzedCAS 都是多线程中实现锁的常用烦恼干是,关于他们的介绍可以看我之前的文章:

应用

在项目中可以应用 kotlin 委托 可以辅助简写如下功能:

  • Fragment / Activity 传参
  • ViewBinding

本节所写的两个示例是摘自

Kotlin | 委托机制 & 原理 & 应用 -- 彭丑丑 View Binding 与Kotlin委托属性的巧妙结合,告别垃圾代码! -- Kirill Rozov 著,依然范特稀西 译

kotlin 委托 + Fragment / Activity 传参

示例来源: 彭丑丑 - Kotlin | 委托机制 & 原理 & 应用 项目地址: Github - DemoHall

属性委托前:

class OrderDetailFragment : Fragment(R.layout.fragment_order_detail) {

private var orderId: Int? = null
private var orderType: Int? = null

companion object {

const val EXTRA_ORDER_ID = "orderId"
const val EXTRA_ORDER_TYPE = "orderType";

fun newInstance(orderId: Int, orderType: Int?) = OrderDetailFragment().apply {
Bundle().apply {
putInt(EXTRA_ORDER_ID, orderId)
if (null != orderType) {
putInt(EXTRA_ORDER_TYPE, orderType)
}
}.also {
arguments = it
}
}
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

arguments?.let {
orderId = it.getInt(EXTRA_ORDER_ID, 10000)
orderType = it.getInt(EXTRA_ORDER_TYPE, 2)
}
}
}
复制代码

定义 ArgumentDelegate.kt

fun <T> fragmentArgument() = FragmentArgumentProperty<T>()

class FragmentArgumentProperty<T> : ReadWriteProperty<Fragment, T> {

override fun getValue(thisRef: Fragment, property: KProperty<*>): T {
return thisRef.arguments?.getValue(property.name) as? T
?: throw IllegalStateException("Property ${property.name} could not be read")
}

override fun setValue(thisRef: Fragment, property: KProperty<*>, value: T) {
val arguments = thisRef.arguments ?: Bundle().also { thisRef.arguments = it }
if (arguments.containsKey(property.name)) {
// The Value is not expected to be modified
return
}
arguments[property.name] = value
}
}
复制代码

使用属性委托后:

class OrderDetailFragment : Fragment(R.layout.fragment_order_detail) {

private lateinit var tvDisplay: TextView

private var orderId: Int by fragmentArgument()
private var orderType: Int? by fragmentArgumentNullable(2)

companion object {
fun newInstance(orderId: Int, orderType: Int?) = OrderDetailFragment().apply {
this.orderId = orderId
this.orderType = orderType
}
}

override fun onViewCreated(root: View, savedInstanceState: Bundle?) {
// Try to modify (UnExcepted)
this.orderType = 3
// Display Value
tvDisplay = root.findViewById(R.id.tv_display)
tvDisplay.text = "orderId = $orderId, orderType = $orderType"
}
}
复制代码

kotlin 委托 + ViewBinding

示例来源: ViewBindingPropertyDelegate

属性委托前:

class ProfileActivity : AppCompatActivity(R.layout.activity_profile) {

private var binding: ActivityProfileBinding? = null

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

binding = ActivityProfileBinding.inflate(layoutInflater)
binding!!.profileFragmentContainer
}
}
复制代码

属性委托后:

class ProfileActivity : AppCompatActivity(R.layout.activity_profile) {

private val viewBinding: ActivityProfileBinding by viewBinding()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
with(viewBinding) {
profileFragmentContainer
}
}
}
复制代码

使用过后代码非常的简洁而且也不需要再用 !! 或者定义一个新的变量,有兴趣的同学可以去看下源码。

收起阅读 »

Flutter IM跨端架构设计和实现

作者:闲鱼技术——祈晴1. 闲鱼IM现状闲鱼IM框架构建于2016-2017年,期间多次迭代升级导致历史包袱累积多,后经IM界面Flutter化,造成架构更复杂,开发层面总结闲鱼当前架构主要存在如下几个问题:•研发效率较低:当前架构开发需求涉及到Android...
继续阅读 »

作者:闲鱼技术——祈晴

1. 闲鱼IM现状

闲鱼IM框架构建于2016-2017年,期间多次迭代升级导致历史包袱累积多,后经IM界面Flutter化,造成架构更复杂,开发层面总结闲鱼当前架构主要存在如下几个问题:

•研发效率较低:当前架构开发需求涉及到Android/iOS双端的逻辑代码以及Flutter的UI界面代码,定位问题往往只能从Flutter UI表相追查到Native逻辑漏洞;•架构层次较差:架构设计上分层不清晰,业务逻辑夹杂在核心的逻辑层致使代码变更风险大;•性能测试略差:核心数据源存储Native内存,需经Flutter Plugin将数据源序列化上抛Flutter侧,在大批量数据源情况下性能表现较差;

从舆情层面总结闲鱼IM当前架构的主要问题如下:

•定位问题困难:线上舆情反馈千奇百怪,测试始终无法复现相关场景,因此很多时候只能靠现象猜测本质;•疑难杂症较多:架构不稳定性造成出现的问题反复出现,当前疑难杂症主要包括未读红点计数,iPhone5C低端机器架构,以及多媒体发送等多个问题;•问题差异性大:Android和iOS两端逻辑代码差异大,包括现存埋点逻辑都不尽相同,因此排查问题根源时候双端都会有不同问题根因,解决问题方案也不相同;

2.业界跨端方案

为解决当前IM痛点,闲鱼今年特起关于IM架构升级项目,重在解决客户端中双端一致性痛点,初步设想方案就是实现跨端统一的Android/iOS逻辑架构;在当前行业内跨端方案可初步归类如下图架构,在GUI层面的跨端方案有Weex,ReactNative,H5,Uni-APP等,其内存模型大多需要通过桥接到Native模式存储;在逻辑层面的跨端方案大致有C/C++等与虚拟机无关语言实现跨端,当然汇编语言也可行;此外有两个独立于上述体系之外的架构就是Flutter和KMM(谷歌基于Kotlin实现类似Flutter架构),其中Flutter运行特定DartVM,将内存数据挂载其自身的isolate中;undefined

考虑闲鱼是Flutter的前沿探索者,方案上优先使用Flutter;然而Flutter的isolate更像一个进程的概念(底层实现非使用进程模式),相比Android,同一进程场景中,Android的Dalvik虚拟机多个线程运行共享一个内存Heap,而DartVM的Isolate运行隔离各自的Heap,因而isolate之间通讯方式比较繁琐(需经过序列化反序列化过程);整个模型如下图所示:undefined

若按官方混合架构实现Flutter应用,开启多个FlutterAcitivty/FlutterController,底层会生成多个Engine,对应会存在多个isolate,而isolate通讯类似于进程通讯(类似socket或AIDL),这里借鉴闲鱼FlutterBoost的设计理念,FlutterIM架构将多个页面的Engine共享,则内存模型就天然支持共享读取,原理图如下:

undefined

3.Flutter IM架构设计

3.1 新老架构对比

如下图是一个老架构方案,其核心问题主要集中于Native逻辑抽象差,其中逻辑层面还设计到多线程并发使得问题倍增,Android/iOS/Flutter交互繁杂,开发维护成本高,核心层耦合较为严重,无插拔式概念;undefined

考虑到历史架构的问题,演进如下新架构设计undefined

架构从上至下依次为业务层分发层逻辑层以及数据源层,数据源层来源于推送或网络请求,其封装于Native层,通过Flutter插件将消息协议数据上抛到Flutter侧的核心逻辑层,处理完成后变成Flutter DB的Enitity实体,实体中挂载一些消息协议实体;核心逻辑层将繁杂数据扁平化打包挂载到分发层中的会话内存模型数据或消息内存模型数据,最后通过观察者模式的订阅分发到业务逻辑中;Flutter IM重点集中改造逻辑层和分发层,将IM核心逻辑和业务层面数据模型进行封装隔离,核心逻辑层和数据库交互后将数据封装到分发层的moduleData中,通过订阅方式分发到业务层数据模型中;此外在IM模型中DB也是重点依赖的,个人对DB数据库管理进行全面封装解,实现一种轻量级,性能佳的Flutter DB管理框架;

3.2 DB存储模型

Flutter IM架构的DB存储依赖数据库插件,目前主流插件是Sqflite,其存储模型如下:undefined依据上图Sqflite插件的DB存储模型会有2个等待队列,一个是Flutter层同步执行队列,一个是Native层的线程执行队列,其Android实现机制是HandlerThread,因此Query/Save读写在会同一线程队列中,导致响应速度慢,容易造成DB SQL堆积,此外缺失缓存模型,于是个人定制如下改进方案undefinedFlutter侧通过表的主键设计查询时候会优先从Entity Cache层去获取,若缓存不存在,则通过Sqflite插件查询,同时改造Sqflite插件成支持sync/Async同步异步两种方式操作,对应到Native侧也会有同步线程队列和异步线程队列,保证数据吞吐率;但是这里建议查询使用异步,存储使用同步更稳妥,主要怕出现多个相同的数据元model同一时间进入异步线程池中,存储先后顺序无法有效的保证;

3.3 ORM数据库方案

IM架构重度依赖DB数据库,而当前业界还没有一个完备的数据库ORM管理方案,参考了Android的OrmLite/GreenDao,个人自行设计一套Flutter ORM数据库管理方案,其核心思想如下:undefined由于Flutter不支持反射,因此无法直接像Android的开源数据库方式操作,但可通过APT方式,将Entity和Orm Entity绑定于一身,操作OrmEntity即操作Entity,整个代码风格设计也和OrmLite极其相似,参考代码如下:

undefined

3.4 IM内存数据模型

FlutterIM架构在内存数据模型主要划分为会话和消息两个颗粒度,会话内存数据模型交托于SessionModuleData,消息内存数据模型交托于MessageModuleData;会话内存数据有一个根节点RootNotice,然后其挂载PSessionMessageNotice(这里PSessionMessageNotice是ORM映射的会话DB表模型)子节点集合;消息内存数据会有一个MessageConatiner容器管理,其内部挂载此会话中的PMessage(PMessage是ORM映射的消息DB表模型)消息集合;

依据上一章节,PSessionMessageNotice设计了一个OrmEnitity Cache,考虑到IM中会话数是有限的,因此PSessionMessageNotice都是直接缓存到Cache中,这种做法的好处是各地去拿会话数据元时候都是缓存中同一个对象,容易保证多次重复读写的数据一致性;而PSessionMessageNotice考虑到其数量可以无限多的特殊性,因此这里将其挂载到MessageContainer的内存管理中,在退出会话的时机会校验容器中PMessage集合的数量,适当缩容可以减少内存开销,模型如下图所示:undefined

3.5 状态管理方案

Flutter IM状态管理方案比较简单,对数据源Session/Message维度使用观察者模式的订阅分发方式实现,架构类似于EventBus模式,页面级的状态管理无论使用fish-redux,scopeModel或者provider几乎影响面不大,核心还是需保留一种插拔式抽象更重要;架构如下图:undefined

3.6 IM同步模型方案

如下是当前现状的消息同步模型,模型中存在ACCS Thread/Main Thread/Region Thread等多线程并发场景,导致易出现多线程高并发的问题;native的推送和网络请求同步的隔离方案通过Lock的锁机制,并且通过队列降频等方式处理,流程繁琐且易出错。整体通过Region Version Gap去判断是否有域空洞,进而执行域同步补充数据。undefined改进的同步模型如下,在Flutter侧天然没多线程场景,通过一种标记位的转化同步异步实现类似Handler消息队列,架构清晰简约了很多,避免锁带来的开销以及同步问题,undefined

4.进展以及性能对比

•针对架构层面:在FlutterIM架构中,重点将双端逻辑差异性统一成同一份Dart代码,完全磨平Android/iOS的代码差异性带来的问题,降低开发维护,测试回归,视觉验收的一半成本,极大提高研发效率;架构上进行重构分层,实现一种解耦合,插拔式的IM架构;同时Native到Flutter侧的大量数据上抛序列化过程改造程Flutter引用传递,解决极限测试场景下的私聊卡顿问题;•针对线上舆情:补齐UT和TLog的集团日志方式做到可追踪,可排查;另外针对于很多现存的疑难杂症重点集中专项解决,比如iphone5C的架构在Flutter侧统一规划,未读红点计数等问题也在架构模型升级中修复,此外多媒体音视频发送模块进行改造升级;•性能数据对比:当IM架构的逻辑层和UI层都切换成Flutter后,和原先架构模式初步对比,整体内存水位持平,其中私聊场景下小米9测试结构内存下降40M,功耗降低4mah,CPU降低1%;极限测试场景下新架构内存数据相比于旧架构有一个较为明显的改观,主要由于两个界面都使用Flutter场景下,页面切换的开销降低很多;

5.展望

JS跨端不安全,C++跨端成本有点高,Flutter会是一个较好选择;彼时闲鱼FlutterIM架构升级根本目的从来不是因Flutter而Flutter,是由于历史包袱的繁重,代码层面的维护成本高,新业务的扩展性差,人力配比不协调以及疑难杂症的舆情持续反馈等等因素造成我们不得不去探索新方案。经过闲鱼IM超复杂业务场景验证Flutter模式的逻辑跨端可行性,闲鱼在Flutter路上会一直保持前沿探索,最后能反馈到生态圈;总结一句话,探索过程在于你勇于迈出第一步,后面才会不断惊喜发现

收起阅读 »

Jetpack—架构组件—App Startup

App Startup介绍作用这是官网的截图,大意就是 App Startup 是一种用来在 app 启动时候规范初始化数据的 library。同时使用 App Startup 可以解决我们平时滥用 ContentProvider 导致的启动变慢问题。还有一点...
继续阅读 »

App Startup

介绍作用

这是官网的截图,大意就是 App Startup 是一种用来在 app 启动时候规范初始化数据的 library。同时使用 App Startup 可以解决我们平时滥用 ContentProvider 导致的启动变慢问题。

还有一点,App Startup 可以用于 app 开发,也可以用来进行 sdk 开发

App Startup 的优势

  1. 平时使用 ContentProvider 自动获取 ApplicationContext 的方式管理混乱,并且多个 ContentProvider 初始化的方式也无法保证初始化的顺序

  2. 统一管理的方式可以明显提升 app 初始化速度,注:仅限于用较多 ContentProvider 来初始化应用的 app,反之不是不能用,只是没有优化效果

依赖

dependencies {
implementation("androidx.startup:startup-runtime:1.0.0")
}
复制代码

使用 AppStartup 初始化全局单例对象(main 分支)

  1. Car 对象
class Car(private val name: String) {
companion object {
var instance: Car? = null
fun getInstance(name: String): Car {
if (instance == null) {
instance = Car(name)
}
return instance!!
}
}

override fun toString(): String {
return "$name ${Random.nextInt(100)}"
}
}
复制代码
  1. 首先需要实现一个 Initializer
class AndroidInitializer : Initializer<Car> {
override fun create(context: Context): Car {
return Car.getInstance("出租车")
}

override fun dependencies(): MutableList<Class<out Initializer<*>>> {
return mutableListOf()
}
}
复制代码
  1. 在代码中注册 AndroidInitializer
 <provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
>
<meta-data
android:name="com.ananananzhuo.appstartupdemo.AndroidInitializer"
android:value="androidx.startup" />
</provider>
复制代码
  1. 分析

本例中 Car 对象,Car 对象内部维护了一个全局单例方法 getInstance。

前面说了,AppStartup 是用来维护全局单例的,那么实际上这个单例的初始化就是通过我们定义的 AndroidInitializer 对象 create 方法来初始化的。

  1. 我们会在 MainActivity 中调用 Car 的 toString 方法,代码如下
 logEE(Car.getInstance("小汽车:").toString())
logEE(Car.getInstance("小汽车:").toString())
logEE(Car.getInstance("小汽车:").toString())
复制代码

我们调用了,三次 toString 方法

代码输出如下:

我们 MainActivity 中代码 getInstance 传入的参数是 "小汽车",但是打印的却是 "出租车"。查看 AndroidInitializer 中的代码发现,我们在 AndroidInitializer 中的 create 方法中创建对象的参数是 "出租车"。

由此可以证明,我们的全局 Car 单例在 AndroidInitializer 中就已经初始化完成了。

手动初始化组件

上一节中我们使用在 Manifest 中注册组件的方式实现 Car 对象的自动初始化。

但是,实际上我们是可以不在 Manifest 中注册的方式实现初始化的,手动初始化的方式如下:

 AppInitializer.getInstance(this)
.initializeComponent(AndroidInitializer::class.java)
复制代码

这种方式的弊端是一次只能初始化一个组件

实现相互依赖的多实例的初始化(分支:multimodule)

通过上一节的学习,你可能会有这样的疑问:AppStartup 啥用没有吧,我直接在 Application 中一行代码初始化不香吗,非要用你这种方式???

那么现在我就要用 AppStartup 实现多实例的初始化,让你进一步了解 AppStartup 的应用

我们这一节的逻辑先描述一下:

本例中我们需要创建两个对象,Person 和 Noodle,两者都是全局单例的。

Person 持有 Noodle 对象的引用,

Person 中有一个 eat 方法,本例中我们的 eat 会输出一行 "某某人" 吃 "面条" 的日志

废话不多说,上代码:

不要嫌代码长,都是一看就懂的逻辑

  1. Person 和 Noodle
class Person(val name:String) {
private var noodle: Noodle? = null
companion object {
private var instance: Person? = null
fun getInstance(name:String): Person {
if (instance == null) {
instance = Person(name)
}
return instance!!
}
}

fun addNoodle(paramsnoodle: Noodle) {
noodle = paramsnoodle
}


fun eat() {
logEE("${name} 吃 ${noodle?.name}")
}
}
复制代码
class Noodle {
val name = "面条"

companion object {
private var instance: Noodle? = null
fun getInstance(): Noodle {
if (instance == null) {
instance = Noodle()
}
return instance!!
}
}
}
复制代码
  1. PersonInitializer、NoodleInitializer
class PersonInitializer : Initializer<Person> {
override fun create(context: Context): Person {
return Person.getInstance("李白").apply {
addNoodle(Noodle.getInstance())
}
}

override fun dependencies(): MutableList<Class<out Initializer<*>>> {
return mutableListOf(NoodleInitializer::class.java)
}
}
复制代码

class NoodleInitializer:Initializer<Noodle> {
override fun create(context: Context): Noodle {
return Noodle.getInstance()
}

override fun dependencies(): MutableList<Class<out Initializer<*>>> {
return mutableListOf()
}
}
复制代码

这两个组件中 PersonInitializer 的 create 方法中创建了 Person 的实例,并向里面添加 Noodle 的实例。

划重点:

PersonInitializer 的 dependencies 方法中返回了 mutableListOf(NoodleInitializer::class.java)。这句代码的意思是在 PersonInitializer 中的 Person 初始化之前会先初始化 NoodleInitializer 中的 Noodle 实例,然后当 PersonInitializer 中 addNoodle 的时候 Noodle 全局单例已经创建好了。

  1. 调用吃面条方法
Person.getInstance("杜甫").eat()
复制代码
  1. 打印日志输出

日志输出符合我们的预期

多实例的注册组件方式如下,我们将 PersonInitializer、NoodleInitializer 都被注册到 meta-data 中了。

实际上,NoodleInitializer 的组件是完全可以不注册的,因为在 PersonInitializer 的 dependencies 中已经声明了 NoodleInitializer 组件。

  <provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false">
<meta-data
android:name="com.ananananzhuo.appstartupdemo.PersonInitializer"
android:value="androidx.startup" />
<meta-data
android:name="com.ananananzhuo.appstartupdemo.NoodleInitializer"
android:value="androidx.startup" />
</provider>
复制代码

使用 AppStartup 进行 sdk 开发(分支:sdk_develop)

本例介绍 sdk 开发中 AppStartup 的使用,实际上与应用开发是一样的,但是感觉还是有必要说一下。

在本例中我们新建了一个 library 的 module,在 library 里面编写了我们的 AppStartup 的代码逻辑,然后将 Library 打包成 arr,集成到 app 模块中,在 app 的 Manifest 中注册组件,并调用组件的相关方法。

  1. aar 集成 

  2. library 中的代码

class LibraryInitializer:Initializer<Student> {
override fun create(context: Context): Student {
return Student.getInstance()
}

override fun dependencies(): MutableList<Class<out Initializer<*>>> {
return mutableListOf()
}
}
复制代码
class Student(val name: String) {
companion object {
private val student = Student("安安安安卓")
fun getInstance(): Student {
return student
}
}

fun study() {
Log.e("tag", "${name} 好好学习")
}
}
复制代码
  1. Manifest 中注册组件
 <provider
android:name="androidx.startup.InitializationProvider"
android:authorities="com.ananananzhuo.appstartupdemo.androidx-startup"
android:exported="false"
>
<meta-data
android:name="com.ananananzhuo.library.LibraryInitializer"
android:value="androidx.startup" />
</provider>
复制代码
  1. 日志打印

  1. 结论

通过这种方式,第三方 sdk 只需要定义自己的 AppStartup 组件就可以,我们在注册组件的时候在 manifest 中添加第三方组件的信息就可以完成第三方组件的初始化了。

这极大的避免了某些自以为是的 sdk,打着方便我们集成的名义搞 ContentProvider 初始化恶心我们

以后如果你合作的第三方 sdk 提供方再出现 ContentProvider 的初始化方式恶心你,那么拿出我的文章好好教他做人。

收起阅读 »

SpannableStringBuiler封装Kotlin

前言SpannableStringBuilder和SpannableString功能基本一样,不过SpannableStringBuilder可以拼接,主要是通过setSpan来实现各种效果,主要的方法如下:start: 指定Span的开始位置 end: 指定...
继续阅读 »

前言

SpannableStringBuilder和SpannableString功能基本一样,不过SpannableStringBuilder可以拼接,主要是通过setSpan来实现各种效果,主要的方法如下:

start: 指定Span的开始位置
end: 指定Span的结束位置,并不包括这个位置。
flags:取值有如下四个
Spannable. SPAN_INCLUSIVE_EXCLUSIVE:前面包括,后面不包括,即在文本前插入新的文本会应用该样式,而在文本后插入新文本不会应用该样式
Spannable. SPAN_INCLUSIVE_INCLUSIVE:前面包括,后面包括,即在文本前插入新的文本会应用该样式,而在文本后插入新文本也会应用该样式
Spannable. SPAN_EXCLUSIVE_EXCLUSIVE:前面不包括,后面不包括
Spannable. SPAN_EXCLUSIVE_INCLUSIVE:前面不包括,后面包括
what: 对应的各种Span,不同的Span对应不同的样式。已知的可用类有:
BackgroundColorSpan : 文本背景色
ForegroundColorSpan : 文本颜色
MaskFilterSpan : 修饰效果,如模糊(BlurMaskFilter)浮雕
RasterizerSpan : 光栅效果
StrikethroughSpan : 删除线
SuggestionSpan : 相当于占位符
UnderlineSpan : 下划线
AbsoluteSizeSpan : 文本字体(绝对大小)
DynamicDrawableSpan : 设置图片,基于文本基线或底部对齐。
ImageSpan : 图片
RelativeSizeSpan : 相对大小(文本字体)
ScaleXSpan : 基于x轴缩放
StyleSpan : 字体样式:粗体、斜体等
SubscriptSpan : 下标(数学公式会用到)
SuperscriptSpan : 上标(数学公式会用到)
TextAppearanceSpan : 文本外貌(包括字体、大小、样式和颜色)
TypefaceSpan : 文本字体
URLSpan : 文本超链接
ClickableSpan : 点击事件

简单使用示例

初始化SpannableString或SpannableStringBuilder,然后设置对应的setPan就可以实现对应的效果。

SpannableString spannableString = new SpannableString("要设置的内容");
ForegroundColorSpan colorSpan = new ForegroundColorSpan(Color.parseColor("#009ad6"));
spannableString.setSpan(colorSpan, 0, 8, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
((TextView)findViewById(R.id.mode1)).setText(spannableString);

具体使用详情可以参考:强大的SpannableStringBuilder

封装使用

对很多功能都可以封装,简化使用,这里使用了扩展函数,更方便在Kotlin中使用,不过在Java中也可以使用,使用方法如下:

第一种情况,要设置的内容已经是一段完整的内容

注意:链式调用时,只需要初始化第一个src就可以了,后续都会默认使用第一个,如果后续继续初始化src, 会导致前面的设置无效,只有最后一个生效。target和range都是为了确定要改变的文字的范围,两个初始化一个即可。

  1. 对整个字符串设置效果

    src 和target默认等于TextView的text

    //对整个 text 设置方式一,textView已经设置过内容,可以不用初始化src
    tvTvOne.sizeSpan(textSize = 20f)
    //对整个 text 设置方式二
    tvTvOne2.typeSpan(src = "全部文字加粗",target = "全部文字加粗",
    type = SsbKtx.type_bold)
  2. 设置部分文字效果

    type 有3个,对应加粗,倾斜,加粗倾斜

    //设置部分文字效果
    //tvTv2.typeSpan(range = 2..4,type = SsbKtx.type_bold)
    tvTv2.typeSpan(target = "部分",type = SsbKtx.type_bold)
    //设置加粗倾斜效果
    tvTv3.typeSpan(range = 0..4,type = SsbKtx.type_bold_italic)
  3. 对同一个文字设置多个效果

    对同一个部分做多种效果,只能第一个设置 src, 后续设置会导致前面的无效。

    //        tvTv4.typeSpan(range = 0..4,type = SsbKtx.type_bold_italic)
    // .foregroundColorIntSpan(range = 0..4,color = Color.GREEN)
    // .strikethroughSpan(range = 0..4)
    tvTv4.typeSpan(src = "只能这个可以设置 src,后面的再设置会导致前面效果无效",
    range = 0..4,type = SsbKtx.type_bold_italic)
    .foregroundColorIntSpan(range = 0..4,color = Color.GREEN)
    .strikethroughSpan(range = 0..4)
  4. 对多个不同的文字分别设置不同的效果

     tvTv5.typeSpan(range = 0..4,type = SsbKtx.type_bold_italic)
    .foregroundColorIntSpan(range = 7..11,color = Color.BLUE)
  5. 设置部分点击

    tvTv6.clickIntSpan(range = 0..4){
    Toast.makeText(this, "hello", Toast.LENGTH_SHORT).show()
    }
  6. 设置部分超链接

    tvTv7.urlSpan(range = 0..4,url = "https://www.baidu.com")

第二种情况,拼接成一个完整的字符串

  1. 拼接成完整的内容

     tvTv8.text = "拼接一段文字"
    tvTv8.appendTypeSpan("加粗",SsbKtx.type_bold)
    .strikethroughSpan(target = "加粗")//对同一部分文字做多个效果
    .appendForegroundColorIntSpan("改变字体颜色",Color.RED)

    如果想对拼接的内容做多个效果,可以在其后面调用对应的方法,只要traget或是range正确即可。

完整代码

object SsbKtx {
const val flag = SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE
const val type_bold = Typeface.BOLD
const val type_italic = Typeface.ITALIC
const val type_bold_italic = Typeface.BOLD_ITALIC

}
//-------------------CharSequence相关扩展-----------------------
/**
*CharSequence不为 null 或者 empty
*/
fun CharSequence?.isNotNullOrEmpty() = !isNullOrEmpty()

/**
*获取一段文字在文字中的范围
* @param target
* @return
*/
fun CharSequence.range(target: CharSequence): IntRange {
val start = this.indexOf(target.toString())
return start..(start + target.length)
}

/**
*将一段指定的文字改变大小
* @return
*/
fun CharSequence.sizeSpan(range: IntRange, textSize: Int): CharSequence {
return SpannableStringBuilder(this).apply {
setSpan(AbsoluteSizeSpan(textSize), range.first, range.last, SsbKtx.flag)
}
}


/**
*设置文字颜色
* @param range
* @return
*/
fun CharSequence.foregroundColorSpan(range: IntRange, color: Int = Color.RED): CharSequence {
return SpannableStringBuilder(this).apply {
setSpan(ForegroundColorSpan(color), range.first, range.last, SsbKtx.flag)
}
}

/**
*设置click,将一段文字中指定range的文字添加颜色和点击事件
* @param range
* @return
*/
fun CharSequence.clickSpan(
range: IntRange,
color: Int = Color.RED,
isUnderlineText: Boolean = false,
clickAction: () -> Unit
): CharSequence {
return SpannableString(this).apply {
val clickableSpan = object : ClickableSpan() {
override fun onClick(widget: View) {
clickAction()
}

override fun updateDrawState(ds: TextPaint) {
ds.color = color
ds.isUnderlineText = isUnderlineText
}
}
setSpan(clickableSpan, range.first, range.last, SsbKtx.flag)
}
}


//-------------------TextView相关扩展--------------------------
/**
*设置目标文字大小, src,target 为空时,默认设置整个 text
* @return
*/
fun TextView?.sizeSpan(
src: CharSequence? = this?.text,
target: CharSequence? = this?.text,
range: IntRange? = null,
@DimenRes textSize: Int
): TextView? {
return when {
this == null -> this
src.isNullOrEmpty() -> this
target.isNullOrEmpty() && range == null -> this
textSize == 0 -> this
range != null -> {
text = src.sizeSpan(range, ResUtils.getDimensionPixelSize(textSize))
this
}
target.isNotNullOrEmpty() -> {
text = src.sizeSpan(src.range(target!!), ResUtils.getDimensionPixelSize(textSize))
this
}
else -> this
}
}

/**
*设置目标文字大小, src,target 为空时,默认设置整个 text
* @return
*/
fun TextView?.sizeSpan(
src: CharSequence? = this?.text,
target: CharSequence? = this?.text,
range: IntRange? = null,
textSize: Float
): TextView? {
return when {
this == null -> this
src.isNullOrEmpty() -> this
target.isNullOrEmpty() && range == null -> this
textSize == 0f -> this
range != null -> {
text = src.sizeSpan(range, DensityUtils.dp2px(textSize))
this
}
target.isNotNullOrEmpty() -> {
text = src.sizeSpan(src.range(target!!), DensityUtils.dp2px(textSize))
this
}
else -> this
}
}

/**
*追加内容设置字体大小
* @param str
* @param textSize
* @return
*/
fun TextView?.appendSizeSpan(str: String?, textSize: Float): TextView? {
str?.let {
this?.append(it.sizeSpan(0..it.length, DensityUtils.dp2px(textSize)))
}
return this
}

fun TextView?.appendSizeSpan(str: String?, @DimenRes textSize: Int): TextView? {
str?.let {
this?.append(it.sizeSpan(0..it.length, ResUtils.getDimensionPixelSize(textSize)))
}
return this
}

/**
*设置目标文字类型(加粗,倾斜,加粗倾斜),src,target 为空时,默认设置整个 text
* @return
*/
fun TextView?.typeSpan(
src: CharSequence? = this?.text,
target: CharSequence? = this?.text,
range: IntRange? = null,
type: Int
): TextView? {
return when {
this == null -> this
src.isNullOrEmpty() -> this
target.isNullOrEmpty() && range == null -> this
range != null -> {
text = src.typeSpan(range, type)
this
}
target.isNotNullOrEmpty() -> {
text = src.typeSpan(src.range(target!!), type)
this
}
else -> this
}
}

fun TextView?.appendTypeSpan(str: String?, type: Int): TextView? {
str?.let {
this?.append(it.typeSpan(0..it.length, type))
}
return this
}

/**
*设置目标文字下划线
* @return
*/
fun TextView?.underlineSpan(
src: CharSequence? = this?.text,
target: CharSequence? = this?.text,
range: IntRange? = null
): TextView? {
return when {
this == null -> this
src.isNullOrEmpty() -> this
target.isNullOrEmpty() && range == null -> this
range != null -> {
text = src.underlineSpan(range)
this
}
target.isNotNullOrEmpty() -> {
text = src.underlineSpan(src.range(target!!))
this
}
else -> this
}
}


/**
*设置目标文字对齐方式
* @return
*/
fun TextView?.alignSpan(
src: CharSequence? = this?.text,
target: CharSequence? = this?.text,
range: IntRange? = null,
align: Layout.Alignment
): TextView? {
return when {
this == null -> this
src.isNullOrEmpty() -> this
target.isNullOrEmpty() && range == null -> this
range != null -> {
text = src.alignSpan(range, align)
this
}
target.isNotNullOrEmpty() -> {
text = src.alignSpan(src.range(target!!), align)
this
}
else -> this
}
}

fun TextView?.appendAlignSpan(str: String?, align: Layout.Alignment): TextView? {
str?.let {
this?.append(it.alignSpan(0..it.length, align))
}
return this
}

/**
*设置目标文字超链接
* @return
*/
fun TextView?.urlSpan(
src: CharSequence? = this?.text,
target: CharSequence? = this?.text,
range: IntRange? = null,
url: String
): TextView? {
return when {
this == null -> this
src.isNullOrEmpty() -> this
target.isNullOrEmpty() && range == null -> this
range != null -> {
movementMethod = LinkMovementMethod.getInstance()
text = src.urlSpan(range, url)
this
}
target.isNotNullOrEmpty() -> {
movementMethod = LinkMovementMethod.getInstance()
text = src.urlSpan(src.range(target!!), url)
this
}
else -> this
}
}

fun TextView?.appendUrlSpan(str: String?, url: String): TextView? {
str?.let {
this?.append(it.urlSpan(0..it.length, url))
}
return this
}

/**
*设置目标文字点击
* @return
*/
fun TextView?.clickIntSpan(
src: CharSequence? = this?.text,
target: CharSequence? = this?.text,
range: IntRange? = null,
color: Int = Color.RED,
isUnderlineText: Boolean = false,
clickAction: () -> Unit
): TextView? {
return when {
this == null -> this
src.isNullOrEmpty() -> this
target.isNullOrEmpty() && range == null -> this
range != null -> {
movementMethod = LinkMovementMethod.getInstance()
highlightColor = Color.TRANSPARENT // remove click bg color
text = src.clickSpan(range, color, isUnderlineText, clickAction)
this
}
target.isNotNullOrEmpty() -> {
movementMethod = LinkMovementMethod.getInstance()
highlightColor = Color.TRANSPARENT // remove click bg color
text = src.clickSpan(src.range(target!!), color, isUnderlineText, clickAction)
this
}
else -> this
}
}

fun TextView?.appendClickIntSpan(
str: String?, color: Int = Color.RED,
isUnderlineText: Boolean = false,
clickAction: () -> Unit
): TextView? {
str?.let {
this?.append(it.clickSpan(0..it.length, color, isUnderlineText, clickAction))
}
return this
}

/**
*设置目标文字点击
* @return
*/
fun TextView?.clickSpan(
src: CharSequence? = this?.text,
target: CharSequence? = this?.text,
range: IntRange? = null,
@ColorRes color: Int,
isUnderlineText: Boolean = false,
clickAction: () -> Unit
): TextView? {
return when {
this == null -> this
src.isNullOrEmpty() -> this
target.isNullOrEmpty() && range == null -> this
range != null -> {
movementMethod = LinkMovementMethod.getInstance()
highlightColor = Color.TRANSPARENT // remove click bg color
text = src.clickSpan(range, ResUtils.getColor(color), isUnderlineText, clickAction)
this
}
target.isNotNullOrEmpty() -> {
movementMethod = LinkMovementMethod.getInstance()
highlightColor = Color.TRANSPARENT // remove click bg color
text = src.clickSpan(
src.range(target!!),
ResUtils.getColor(color),
isUnderlineText,
clickAction
)
this
}
else -> this
}
}

fun TextView?.appendClickSpan(
str: String?,
@ColorRes color: Int,
isUnderlineText: Boolean = false,
clickAction: () -> Unit
): TextView? {
str?.let {
this?.append(
it.clickSpan(
0..it.length,
ResUtils.getColor(color),
isUnderlineText,
clickAction
)
)
}
return this
}

里面的ResUtils只是简单的获取资源文件,如果想直接引入,可以参考Github直接使用gradle依赖。

收起阅读 »

APP路由框架与组件化简析

前端开发经常遇到一个词:路由,在Android APP开发中,路由还经常和组件化开发强关联在一起,那么到底什么是路由,一个路由框架到底应该具备什么功能,实现原理是什么样的?路由是否是APP的强需求呢?与组件化到底什么关系,本文就简单分析下如上几个问题。 路由...
继续阅读 »

前端开发经常遇到一个词:路由,在Android APP开发中,路由还经常和组件化开发强关联在一起,那么到底什么是路由,一个路由框架到底应该具备什么功能,实现原理是什么样的?路由是否是APP的强需求呢?与组件化到底什么关系,本文就简单分析下如上几个问题。


路由的概念


路由这个词本身应该是互联网协议中的一个词,维基百科对此的解释如下:


路由(routing)就是通过互联的网络把信息从源地址传输到目的地址的活动。路由发生在OSI网络参考模型中的第三层即网络层。

个人理解,在前端开发中,路由就是通过一串字符串映射到对应业务的能力。APP的路由框首先能够搜集各组件的路由scheme,并生成路由表,然后,能够根据外部输入字符串在路由表中匹配到对应的页面或者服务,进行跳转或者调用,并提供会获取返回值等,示意如下


image.png


所以一个基本路由框架要具备如下能力:





    1. APP路由的扫描及注册逻辑




    1. 路由跳转target页面能力




    1. 路由调用target服务能力



APP中,在进行页面路由的时候,经常需要判断是否登录等一些额外鉴权逻辑所以,还需要提供拦截逻辑等,比如:登陆。


三方路由框架是否是APP强需求


答案:不是,系统原生提供路由能力,但功能较少,稍微大规模的APP都采用三方路由框架。


Android系统本身提供页面跳转能力:如startActivity,对于工具类APP,或单机类APP,这种方式已经完全够用,完全不需要专门的路由框架,那为什么很多APP还是采用路由框架呢?这跟APP性质及路由框架的优点都有关。比如淘宝、京东、美团等这些大型APP,无论是从APP功能还是从其研发团队的规模上来说都很庞大,不同的业务之间也经常是不同的团队在维护,采用组件化的开发方式,最终集成到一个APK中。多团队之间经常会涉及业务间的交互,比如从电影票业务跳转到美食业务,但是两个业务是两个独立的研发团队,代码实现上是完全隔离的,那如何进行通信呢?首先想到的是代码上引入,但是这样会打破了低耦合的初衷,可能还会引入各种问题。例如,部分业务是外包团队来做,这就牵扯到代码安全问题,所以还是希望通过一种类似黑盒的方式,调用目标业务,这就需要中转路由支持,所以国内很多APP都是用了路由框架的。其次我们各种跳转的规则并不想跟具体的实现类扯上关系,比如跳转商详的时候,不希望知道是哪个Activity来实现,只需要一个字符串映射过去即可,这对于H5、或者后端开发来处理跳转的时候,就非常标准。


原生路由的限制:功能单一,扩展灵活性差,不易协同


传统的路由基本上就限定在startActivity、或者startService来路由跳转或者启动服务。拿startActivity来说,传统的路由有什么缺点:startActivity有两种用法,一种是显示的,一种是隐式的,显示调用如下:


<!--1 导入依赖-->
import com.snail.activityforresultexample.test.SecondActivity;

public class MainActivity extends AppCompatActivity {

void jumpSecondActivityUseClassName(){
<!--显示的引用Activity类-->
Intent intent =new Intent(MainActivity.this, SecondActivity.class);
startActivity(intent);
}


显示调用的缺点很明显,那就是必须要强依赖目标Activity的类实现,有些场景,尤其是大型APP组件化开发时候,有些业务逻辑出于安全考虑,并不想被源码或aar依赖,这时显式依赖的方式就无法走通。再来看看隐式调用方法。


第一步:manifest中配置activity的intent-filter,至少要配置一个action


<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.snail.activityforresultexample">
<application
...
<activity android:name=".test.SecondActivity">
<intent-filter>
<!--隐式调用必须配置android.intent.category.DEFAULT-->
<category android:name="android.intent.category.DEFAULT"/>
<!--至少配置一个action才能通过隐式调用-->
<action android:name="com.snail.activityforresultexample.SecondActivity" />
<!--可选-->
<!-- <data android:mimeType="video/mpeg" android:scheme="http" ... />-->
</intent-filter>
</activity>
</application>
</manifest>

第二步:调用


void jumpSecondActivityUseFilter() {
Intent intent = new Intent();
intent.setAction("com.snail.activityforresultexample.SecondActivity");
startActivity(intent);
}

如果牵扯到数据传递写法上会更复杂一些,隐式调用的缺点有如下几点:



  • 首先manifest中定义复杂,相对应的会导致暴露的协议变的复杂,不易维护扩展。

  • 其次,不同Activity都要不同的action配置,每次增减修改Activity都会很麻烦,对比开发者非常不友好,增加了协作难度。

  • 最后,Activity的export属性并不建议都设置成True,这是降低风险的一种方式,一般都是收归到一个Activity,DeeplinkActivitiy统一处理跳转,这种场景下,DeeplinkActivitiy就兼具路由功能,隐式调用的场景下,新Activitiy的增减势必每次都要调整路由表,这会导致开发效率降低,风险增加。


可以看到系统原生的路由框架,并没太多考虑团队协同的开发模式,多限定在一个模块内部多个业务间直接相互引用,基本都要代码级依赖,对于代码及业务隔离很不友好。如不考虑之前Dex方法树超限制,可以认为三方路由框架完全是为了团队协同而创建的


APP三方路由框架需具备的能力


目前市面上大部分的路由框架都能搞定上述问题,简单整理下现在三方路由的能力,可归纳如下:



  • 路由表生成能力:业务组件**[UI业务及服务]**自动扫描及注册逻辑,需要扩展性好,无需入侵原有代码逻辑

  • scheme与业务映射逻辑 :无需依赖具体实现,做到代码隔离

  • 基础路由跳转能力 :页面跳转能力的支持

  • 服务类组件的支持 :如去某个服务组件获取一些配置等

  • [扩展]路由拦截逻辑:比如登陆,统一鉴权

  • 可定制的降级逻辑:找不到组件时的兜底


可以看下一个典型的Arouter用法,第一步:对新增页面添加Router Scheme 声明,


	@Route(path = "/test/activity2")
public class Test2Activity extends AppCompatActivity {
...
}

build阶段会根据注解搜集路由scheme,生成路由表。第二步使用


        ARouter.getInstance()
.build("/test/activity2")
.navigation(this);

如上,在ARouter框架下,仅需要字符串scheme,无需依赖任何Test2Activity就可实现路由跳转。


APP路由框架的实现


路由框架实现的核心是建立scheme和组件**[Activity或者其他服务]**的映射关系,也就是路由表,并能根据路由表路由到对应组件的能力。其实分两部分,第一部分路由表的生成,第二部分,路由表的查询


路由表的自动生成


生成路由表的方式有很多,最简单的就是维护一个公共文件或者类,里面映射好每个实现组件跟scheme,


image.png


不过,这种做法缺点很明显:每次增删修改都要都要修改这个表,对于协同非常不友好,不符合解决协同问题的初衷。不过,最终的路由表倒是都是这条路,就是将所有的Scheme搜集到一个对象中,只是实现方式的差别,目前几乎所有的三方路由框架都是借助注解+APT[Annotation Processing Tool]工具+AOP(Aspect-Oriented Programming,面向切面编程)来实现的,基本流程如下:


image.png


其中牵扯的技术有注解、APT(Annotation Processing Tool)、AOP(Aspect-Oriented Programming,面向切面编程)。APT常用的有JavaPoet,主要是遍历所有类,找到被注解的Java类,然后聚合生成路由表,由于组件可能有很多,路由表可能也有也有多个,之后,这些生成的辅助类会跟源码一并被编译成class文件,之后利用AOP技术【如ASM或者JavaAssist】,扫描这些生成的class,聚合路由表,并填充到之前的占位方法中,完成自动注册的逻辑。



JavaPoet如何搜集并生成路由表集合?



以ARouter框架为例,先定义Router框架需要的注解如:


@Target({ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public @interface Route {

/**
* Path of route
*/
String path();

该注解用于标注需要路由的组件,用法如下:


@Route(path = "/test/activity1", name = "测试用 Activity")
public class Test1Activity extends BaseActivity {
@Autowired
int age = 10;

之后利用APT扫描所有被注解的类,生成路由表,实现参考如下:


@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (CollectionUtils.isNotEmpty(annotations)) {
<!--获取所有被Route.class注解标注的集合-->
Set<? extends Element> routeElements = roundEnv.getElementsAnnotatedWith(Route.class);
<!--解析并生成表-->
this.parseRoutes(routeElements);
...
return false;
}

<!--生成中间路由表Java类-->
private void parseRoutes(Set<? extends Element> routeElements) throws IOException {
...
// Generate groups
String groupFileName = NAME_OF_GROUP + groupName;
JavaFile.builder(PACKAGE_OF_GENERATE_FILE,
TypeSpec.classBuilder(groupFileName)
.addJavadoc(WARNING_TIPS)
.addSuperinterface(ClassName.get(type_IRouteGroup))
.addModifiers(PUBLIC)
.addMethod(loadIntoMethodOfGroupBuilder.build())
.build()
).build().writeTo(mFiler);

产物如下:包含路由表,及局部注册入口。


image.png



自动注册:ASM搜集上述路由表并聚合插入Init代码区



为了能够插入到Init代码区,首先需要预留一个位置,一般定义一个空函数,以待后续填充:


	public class RouterInitializer {

public static void init(boolean debug, Class webActivityClass, IRouterInterceptor... interceptors) {
...
loadRouterTables();
}
//自动注册代码
public static void loadRouterTables() {

}
}

首先利用AOP工具,遍历上述APT中间产物,聚合路由表,并注册到预留初始化位置,遍历的过程牵扯是gradle transform的过程,



  • 搜集目标,聚合路由表

      /**扫描jar*/
    fun scanJar(jarFile: File, dest: File?) {

    val file = JarFile(jarFile)
    var enumeration = file.entries()
    while (enumeration.hasMoreElements()) {
    val jarEntry = enumeration.nextElement()
    if (jarEntry.name.endsWith("XXRouterTable.class")) {
    val inputStream = file.getInputStream(jarEntry)
    val classReader = ClassReader(inputStream)
    if (Arrays.toString(classReader.interfaces)
    .contains("IHTRouterTBCollect")
    ) {
    tableList.add(
    Pair(
    classReader.className,
    dest?.absolutePath
    )
    )
    }
    inputStream.close()
    } else if (jarEntry.name.endsWith("HTRouterInitializer.class")) {
    registerInitClass = dest
    }
    }
    file.close()
    }

  • 对目标Class注入路由表初始化代码

      fun asmInsertMethod(originFile: File?) {

    val optJar = File(originFile?.parent, originFile?.name + ".opt")
    if (optJar.exists())
    optJar.delete()
    val jarFile = JarFile(originFile)
    val enumeration = jarFile.entries()
    val jarOutputStream = JarOutputStream(FileOutputStream(optJar))

    while (enumeration.hasMoreElements()) {
    val jarEntry = enumeration.nextElement()
    val entryName = jarEntry.getName()
    val zipEntry = ZipEntry(entryName)
    val inputStream = jarFile.getInputStream(jarEntry)
    //插桩class
    if (entryName.endsWith("RouterInitializer.class")) {
    //class文件处理
    jarOutputStream.putNextEntry(zipEntry)
    val classReader = ClassReader(IOUtils.toByteArray(inputStream))
    val classWriter = ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
    val cv = RegisterClassVisitor(Opcodes.ASM5, classWriter,tableList)
    classReader.accept(cv, EXPAND_FRAMES)
    val code = classWriter.toByteArray()
    jarOutputStream.write(code)
    } else {
    jarOutputStream.putNextEntry(zipEntry)
    jarOutputStream.write(IOUtils.toByteArray(inputStream))
    }
    jarOutputStream.closeEntry()
    }
    //结束
    jarOutputStream.close()
    jarFile.close()
    if (originFile?.exists() == true) {
    Files.delete(originFile.toPath())
    }
    optJar.renameTo(originFile)
    }


最终RouterInitializer.class的 loadRouterTables会被修改成如下填充好的代码:


 public static void loadRouterTables() {

<!---->
register("com.alibaba.android.arouter.routes.ARouter$$Root$$modulejava");
register("com.alibaba.android.arouter.routes.ARouter$$Root$$modulekotlin");
register("com.alibaba.android.arouter.routes.ARouter$$Root$$arouterapi");
register("com.alibaba.android.arouter.routes.ARouter$$Interceptors$$modulejava");
...
}

如此就完成了路由表的搜集与注册,大概的流程就是如此。当然对于支持服务、Fragment等略有不同,但大体类似。


Router框架对服务类组件的支持


通过路由的方式获取服务属于APP路由比较独特的能力,比如有个用户中心的组件,我们可以通过路由的方式去查询用户是否处于登陆状态,这种就不是狭义上的页面路由的概念,通过一串字符串如何查到对应的组件并调用其方法呢?这种的实现方式也有多种,每种实现方式都有自己的优劣。



  • 一种是可以将服务抽象成接口,沉到底层,上层实现通过路由方式映射对象

  • 一种是将实现方法直接通过路由方式映射


先看第一种,这种事Arouter的实现方式,它的优点是所有对外暴露的服务都暴露接口类【沉到底层】,这对于外部的调用方,也就是服务使用方非常友好,示例如下:



先定义抽象服务,并沉到底层



image.png


public interface HelloService extends IProvider {
void sayHello(String name);
}


实现服务,并通过Router注解标记



@Route(path = "/yourservicegroupname/hello")
public class HelloServiceImpl implements HelloService {
Context mContext;

@Override
public void sayHello(String name) {
Toast.makeText(mContext, "Hello " + name, Toast.LENGTH_SHORT).show();
}


使用:利用Router加scheme获取服务实例,并映射成抽象类,然后直接调用方法。



  ((HelloService) ARouter.getInstance().build("/yourservicegroupname/hello").navigation()).sayHello("mike");

这种实现方式对于使用方其实是很方便的,尤其是一个服务有多个可操作方法的时候,但是缺点是扩展性,如果想要扩展方法,就要改动底层库。


再看第二种:将实现方法直接通过路由方式映射


服务的调用都要落到方法上,参考页面路由,也可以支持方法路由,两者并列关系,所以组要增加一个方法路由表,实现原理与Page路由类似,跟上面的Arouter对比,不用定义抽象层,直接定义实现即可:



定义Method的Router



	public class HelloService {

<!--参数 name-->
@MethodRouter(url = {"arouter://sayhello"})
public void sayHello(String name) {
Toast.makeText(mContext, "Hello " + name, Toast.LENGTH_SHORT).show();
}


使用即可



 RouterCall.callMethod("arouter://sayhello?name=hello");

上述的缺点就是对于外部调用有些复杂,尤其是处理参数的时候,需要严格按照协议来处理,优点是,没有抽象层,如果需要扩展服务方法,不需要改动底层。


上述两种方式各有优劣,不过,如果从左服务组件的初衷出发,第一种比较好:对于调用方比较友好。另外对于CallBack的支持,Arouter的处理方式可能也会更方便一些,可以比较方便的交给服务方定义。如果是第二种,服务直接通过路由映射的方式,处理起来就比较麻烦,尤其是Callback中的参数,可能要统一封装成JSON并维护解析的协议,这样处理起来,可能不是很好。


路由表的匹配


路由表的匹配比较简单,就是在全局Map中根据String输入,匹配到目标组件,然后依赖反射等常用操作,定位到目标。


组件化与路由的关系


组件化是一种开发集成模式,更像一种开发规范,更多是为团队协同开发带来方便。组件化最终落地是一个个独立的业务及功能组件,这些组件之间可能是不同的团队,处于不同的目的在各自维护,甚至是需要代码隔离,如果牵扯到组件间的调用与通信,就不可避免的借助路由,因为实现隔离的,只能采用通用字符串scheme进行通信,这就是路由的功能范畴。


组件化需要路由支撑的根本原因:组件间代码实现的隔离


总结



  • 路由不是一个APP的必备功能,但是大型跨团队的APP基本都需要

  • 路由框架的基本能力:路由自动注册、路由表搜集、服务及UI界面路由及拦截等核心功能

  • 组件化与路由的关系:组件化的代码隔离导致路由框架成为必须




作者:看书的小蜗牛
链接:https://juejin.cn/post/6973905775940861966
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

浅谈Android插件化

一、认识插件化 1.1 插件化起源 插件化技术最初源于免安装运行 Apk的想法,这个免安装的 Apk 就可以理解为插件,而支持插件的 app 我们一般叫 宿主。 想必大家都知道,在 Android 系统中,应用是以 Apk 的形式存在的,应用都需要安装才...
继续阅读 »

一、认识插件化


1.1 插件化起源


插件化技术最初源于免安装运行 Apk的想法,这个免安装的 Apk 就可以理解为插件,而支持插件的 app 我们一般叫 宿主。


想必大家都知道,在 Android 系统中,应用是以 Apk 的形式存在的,应用都需要安装才能使用。但实际上 Android 系统安装应用的方式相当简单,其实就是把应用 Apk 拷贝到系统不同的目录下、然后把 so 解压出来而已。


常见的应用安装目录有:



  • /system/app:系统应用

  • /system/priv-app:系统应用

  • /data/app:用户应用


那可能大家会想问,既然安装这个过程如此简单,Android 是怎么运行应用中的代码的呢,我们先看 Apk 的构成,一个常见的 Apk 会包含如下几个部分:



  • classes.dexJava 代码字节码

  • res:资源文件

  • libso 文件

  • assets:静态资产文件

  • AndroidManifest.xml:清单文件


其实 Android 系统在打开应用之后,也只是开辟进程,然后使用 ClassLoader 加载 classes.dex 至进程中,执行对应的组件而已。


那大家可能会想一个问题,既然 Android 本身也是使用类似反射的形式加载代码执行,凭什么我们不能执行一个 Apk 中的代码呢?


1.2 插件化优点


插件化让 Apk 中的代码(主要是指 Android 组件)能够免安装运行,这样能够带来很多收益:



  • 减少安装Apk的体积、按需下载模块

  • 动态更新插件

  • 宿主和插件分开编译,提升开发效率

  • 解决方法数超过65535的问题


想象一下,你的应用拥有 Native 应用一般极高的性能,又能获取诸如 Web 应用一样的收益。


嗯,理想很美好不是嘛?


1.3 与组件化的区别



  • 组件化:是将一个App分成多个模块,每个模块都是一个组件(module),开发过程中可以让这些组件相互依赖或独立编译、调试部分组件,但是这些组件最终会合并成一个完整的Apk去发布到应用市场。

  • 插件化:是将整个App拆分成很多模块,每个模块都是一个Apk(组件化的每个模块是一个lib),最终打包的时候将宿主Apk和插件Apk分开打包,只需发布宿主Apk到应用市场,插件Apk通过动态按需下发到宿主Apk。


二、插件化的技术难点


? 想让插件的Apk真正运行起来,首先要先能找到插件Apk的存放位置,然后我们要能解析加载Apk里面的代码。


? 但是光能执行Java代码是没有意义的,在Android系统中有四大组件是需要在系统中注册的,具体来说是在 Android 系统的 ActivityManagerService (AMS)PackageManagerService (PMS) 中注册的,而四大组件的解析和启动都需要依赖 AMSPMS,如何欺骗系统,让他承认一个未安装的 Apk 中的组件,如何让宿主动态加载执行插件Apk中 Android 组件(即 ActivityServiceBroadcastReceiverContentProviderFragment)等是插件化最大的难点。


? 另外,应用资源引用(特指 R 中引用的资源,如 layoutvalues 等)也是一大问题,想象一下你在宿主进程中使用反射加载了一个插件 Apk,代码中的 R 对应的 id 却无法引用到正确的资源,会产生什么后果。


总结一下,其实做到插件化的要点就这几个:



  • 如何加载并执行插件 Apk 中的代码(ClassLoader Injection

  • 让系统能调用插件 Apk 中的组件(Runtime Container

  • 正确识别插件 Apk 中的资源(Resource Injection


当然还有其他一些小问题,但可能不是所有场景下都会遇到,我们后面再单独说。


三、ClassLoader Injection


ClassLoader 是插件化中必须要掌握的,因为我们知道Android 应用本身是基于魔改的 Java 虚拟机的,而由于插件是未安装的 apk,系统不会处理其中的类,所以需要使用 ClassLoader 加载 Apk,然后反射里面的代码。


3.1 java 中的 ClassLoader



  • BootstrapClassLoader 负责加载 JVM 运行时的核心类,比如 JAVA_HOME/lib/rt.jar 等等


  • ExtensionClassLoader 负责加载 JVM 的扩展类,比如 JAVA_HOME/lib/ext 下面的 jar 包


  • AppClassLoader 负责加载 classpath 里的 jar 包和目录



3.2 android 中的 ClassLoader


在Android系统中ClassLoader是用来加载dex文件的,有包含 dex 的 apk 文件以及 jar 文件,dex 文件是一种对class文件优化的产物,在Android中应用打包时会把所有class文件进行合并、优化(把不同的class文件重复的东西只保留一份),然后生成一个最终的class.dex文件



  • PathClassLoader 用来加载系统类和应用程序类,可以加载已经安装的 apk 目录下的 dex 文件

    public class PathClassLoader extends BaseDexClassLoader {
    public PathClassLoader(String dexPath, ClassLoader parent) {
    super(dexPath, null, null, parent);
    }

    public PathClassLoader(String dexPath, String libraryPath,
    ClassLoader parent)
    {
    super(dexPath, null, libraryPath, parent);
    }
    }

  • DexClassLoader 用来加载 dex 文件,可以从存储空间加载 dex 文件。

    public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory,
    String libraryPath, ClassLoader parent)
    {
    super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }
    }


我们在插件化中一般使用的是 DexClassLoader。


3.3 双亲委派机制


每一个 ClassLoader 中都有一个 parent 对象,代表的是父类加载器,在加载一个类的时候,会先使用父类加载器去加载,如果在父类加载器中没有找到,自己再进行加载,如果 parent 为空,那么就用系统类加载器来加载。通过这样的机制可以保证系统类都是由系统类加载器加载的。 下面是 ClassLoader 的 loadClass 方法的具体实现。


    protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
// 先从父类加载器中进行加载
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}

if (c == null) {
// 没有找到,再自己加载
c = findClass(name);
}
}
return c;
}
复制代码

3.4 如何加载插件中的类


要加载插件中的类,我们首先要创建一个 DexClassLoader,先看下 DexClassLoader 的构造函数需要哪些参数。


public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
// ...
}
}
复制代码

构造函数需要四个参数: dexPath 是需要加载的 dex / apk / jar 文件路径 optimizedDirectory 是 dex 优化后存放的位置,在 ART 上,会执行 oat 对 dex 进行优化,生成机器码,这里就是存放优化后的 odex 文件的位置 librarySearchPath 是 native 依赖的位置 parent 就是父类加载器,默认会先从 parent 加载对应的类


创建出 DexClassLaoder 实例以后,只要调用其 loadClass(className) 方法就可以加载插件中的类了。具体的实现在下面:


    // 从 assets 中拿出插件 apk 放到内部存储空间
private fun extractPlugin() {
var inputStream = assets.open("plugin.apk")
File(filesDir.absolutePath, "plugin.apk").writeBytes(inputStream.readBytes())
}

private fun init() {
extractPlugin()
pluginPath = File(filesDir.absolutePath, "plugin.apk").absolutePath
nativeLibDir = File(filesDir, "pluginlib").absolutePath
dexOutPath = File(filesDir, "dexout").absolutePath
// 生成 DexClassLoader 用来加载插件类
pluginClassLoader = DexClassLoader(pluginPath, dexOutPath, nativeLibDir, this::class.java.classLoader)
}


3.5 执行插件类的方法


通过反射来执行类的方法


val loadClass = pluginClassLoader.loadClass(activityName)
loadClass.getMethod("test",null).invoke(loadClass)

我们称这个过程叫做 ClassLoader 注入。完成注入后,所有来自宿主的类使用宿主的 ClassLoader 进行加载,所有来自插件 Apk 的类使用插件 ClassLoader 进行加载,而由于 ClassLoader 的双亲委派机制,实际上系统类会不受 ClassLoader 的类隔离机制所影响,这样宿主 Apk 就可以在宿主进程中使用来自于插件的组件类了。


四、Runtime Container


我们之前说到 Activity 插件化最大的难点是如何欺骗系统,让他承认一个未安装的 Apk 中的组件。 因为插件是动态加载的,所以插件的四大组件不可能注册到宿主的 Manifest 文件中,而没有在 Manifest 中注册的四大组件是不能和系统直接进行交互的。 如果直接把插件的 Activity 注册到宿主 Manifest 里就失去了插件化的动态特性,因为每次插件中新增 Activity 都要修改宿主 Manifest 并且重新打包,那就和直接写在宿主中没什么区别了。


4.1 为什么没有注册的 Activity 不能和系统交互


这里的不能直接交互的含义有两个



  1. 系统会检测 Activity 是否注册 如果我们启动一个没有在 Manifest 中注册的 Activity,会发现报如下 error:

    android.content.ActivityNotFoundException: Unable to find explicit activity class {com.zyg.commontec/com.zyg.plugin.PluginActivity}; have you declared this activity in your AndroidManifest.xml?



这个 log 在 Instrumentation 的 checkStartActivityResult 方法中可以看到:


public class Instrumentation {
public static void checkStartActivityResult(int res, Object intent) {
if (!ActivityManager.isStartResultFatalError(res)) {
return;
}

switch (res) {
case ActivityManager.START_INTENT_NOT_RESOLVED:
case ActivityManager.START_CLASS_NOT_FOUND:
if (intent instanceof Intent && ((Intent)intent).getComponent() != null)
throw new ActivityNotFoundException(
"Unable to find explicit activity class "
+ ((Intent)intent).getComponent().toShortString()
+ "; have you declared this activity in your AndroidManifest.xml?");
throw new ActivityNotFoundException(
"No Activity found to handle " + intent);
...
}
}
}



  1. Activity 的生命周期无法被调用,其实一个 Activity 主要的工作,都是在其生命周期方法中调用了,既然上一步系统检测了 Manifest 注册文件,启动 Activity 被拒绝,那么其生命周期方法也肯定不会被调用了。从而插件 Activity 也就不能正常运行了。


4.2 运行时容器技术


由于Android中的组件(Activity,Service,BroadcastReceiver和ContentProvider)是由系统创建的,并且由系统管理生命周期。 仅仅构造出这些类的实例是没用的,还需要管理组件的生命周期。其中以Activity最为复杂,不同框架采用的方法也不尽相同。插件化如何支持组件生命周期的管理。 大致分为两种方式:



  • 运行时容器技术(ProxyActivity代理)

  • 预埋StubActivity,hook系统启动Activity的过程


我们的解决方案很简单,即运行时容器技术,简单来说就是在宿主 Apk 中预埋一些空的 Android 组件,以 Activity 为例,我预置一个 ContainerActivity extends Activity 在宿主中,并且在 AndroidManifest.xml 中注册它。


它要做的事情很简单,就是帮助我们作为插件 Activity 的容器,它从 Intent 接受几个参数,分别是插件的不同信息,如:



  • pluginName

  • pluginApkPath

  • pluginActivityName


等,其实最重要的就是 pluginApkPathpluginActivityName,当 ContainerActivity 启动时,我们就加载插件的 ClassLoaderResource,并反射 pluginActivityName 对应的 Activity 类。当完成加载后,ContainerActivity 要做两件事:



  • 转发所有来自系统的生命周期回调至插件 Activity

  • 接受 Activity 方法的系统调用,并转发回系统


我们可以通过复写 ContainerActivity 的生命周期方法来完成第一步,而第二步我们需要定义一个 PluginActivity,然后在编写插件 Apk 中的 Activity 组件时,不再让其集成 android.app.Activity,而是集成自我们的 PluginActivity


public class ContainerActivity extends Activity {
private PluginActivity pluginActivity;

@Override
protected void onCreate(Bundle savedInstanceState) {
String pluginActivityName = getIntent().getString("pluginActivityName", "");
pluginActivity = PluginLoader.loadActivity(pluginActivityName, this);
if (pluginActivity == null) {
super.onCreate(savedInstanceState);
return;
}

pluginActivity.onCreate();
}

@Override
protected void onResume() {
if (pluginActivity == null) {
super.onResume();
return;
}
pluginActivity.onResume();
}

@Override
protected void onPause() {
if (pluginActivity == null) {
super.onPause();
return;
}
pluginActivity.onPause();
}

// ...
}

public class PluginActivity {
private ContainerActivity containerActivity;

public PluginActivity(ContainerActivity containerActivity) {
this.containerActivity = containerActivity;
}

@Override
public <T extends View> T findViewById(int id) {
return containerActivity.findViewById(id);
}
// ...
}

// 插件 `Apk` 中真正写的组件
public class TestActivity extends PluginActivity {
// ......
}

是不是感觉有点看懂了,虽然真正搞的时候还有很多小坑,但大概原理就是这么简单,启动插件组件需要依赖容器,容器负责加载插件组件并且完成双向转发,转发来自系统的生命周期回调至插件组件,同时转发来自插件组件的系统调用至系统。


4.3 字节码替换


该方式虽然能够很好的实现启动插件Activity的目的,但是由于开发式侵入性很强,插件中的Activity必须继承PluginActivity,如果想把之前的模块改造成插件需要很多额外的工作。


class TestActivity extends Activity {}
->
class TestActivity extends PluginActivity {}

有没有什么办法能让插件组件的编写与原来没有任何差别呢?


Shadow 的做法是字节码替换插件,这是一个非常棒的想法,简单来说,Android 提供了一些 Gradle 插件开发套件,其中有一项功能叫 Transform Api,它可以介入项目的构建过程,在字节码生成后、dex 文件生成前,对代码进行某些变换,具体怎么做的不说了,可以自己看文档。


实现的功能嘛,就是用户配置 Gradle 插件后,正常开发,依然编写:


class TestActivity extends Activity {}

然后完成编译后,最后的字节码中,显示的却是:


class TestActivity extends PluginActivity {}

到这里基本的框架就差不多结束了。


五、Resource Injection


最后要说的是资源注入,其实这一点相当重要,Android 应用的开发其实崇尚的是逻辑与资源分离的理念,所有资源(layoutvalues 等)都会被打包到 Apk 中,然后生成一个对应的 R 类,其中包含对所有资源的引用 id


资源的注入并不容易,好在 Android 系统给我们留了一条后路,最重要的是这两个接口:



  • PackageManager#getPackageArchiveInfo:根据 Apk 路径解析一个未安装的 ApkPackageInfo

  • PackageManager#getResourcesForApplication:根据 ApplicationInfo 创建一个 Resources 实例


我们要做的就是在上面 ContainerActivity#onCreate 中加载插件 Apk 的时候,用这两个方法创建出来一份插件资源实例。具体来说就是先用 PackageManager#getPackageArchiveInfo 拿到插件 ApkPackageInfo,有了 PacakgeInfo 之后我们就可以自己组装一份 ApplicationInfo,然后通过 PackageManager#getResourcesForApplication 来创建资源实例,大概代码像这样:


PackageManager packageManager = getPackageManager();
PackageInfo packageArchiveInfo = packageManager.getPackageArchiveInfo(
pluginApkPath,
PackageManager.GET_ACTIVITIES
| PackageManager.GET_META_DATA
| PackageManager.GET_SERVICES
| PackageManager.GET_PROVIDERS
| PackageManager.GET_SIGNATURES
);
packageArchiveInfo.applicationInfo.sourceDir = pluginApkPath;
packageArchiveInfo.applicationInfo.publicSourceDir = pluginApkPath;

Resources injectResources = null;
try {
injectResources = packageManager.getResourcesForApplication(packageArchiveInfo.applicationInfo);
} catch (PackageManager.NameNotFoundException e) {
// ...
}

拿到资源实例后,我们需要将宿主的资源和插件资源 Merge 一下,编写一个新的 Resources 类,用这样的方式完成自动代理:


public class PluginResources extends Resources {
private Resources hostResources;
private Resources injectResources;

public PluginResources(Resources hostResources, Resources injectResources) {
super(injectResources.getAssets(), injectResources.getDisplayMetrics(), injectResources.getConfiguration());
this.hostResources = hostResources;
this.injectResources = injectResources;
}

@Override
public String getString(int id, Object... formatArgs) throws NotFoundException {
try {
return injectResources.getString(id, formatArgs);
} catch (NotFoundException e) {
return hostResources.getString(id, formatArgs);
}
}

// ...
}

然后我们在 ContainerActivity 完成插件组件加载后,创建一份 Merge 资源,再复写 ContainerActivity#getResources,将获取到的资源替换掉:


public class ContainerActivity extends Activity {
private Resources pluginResources;

@Override
protected void onCreate(Bundle savedInstanceState) {
// ...
pluginResources = new PluginResources(super.getResources(), PluginLoader.getResources(pluginApkPath));
// ...
}

@Override
public Resources getResources() {
if (pluginActivity == null) {
return super.getResources();
}
return pluginResources;
}
}

这样就完成了资源的注入。



作者:QiShare
链接:https://juejin.cn/post/6973888932572315678
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

二阶贝塞尔仿微信扔炸弹动画

前言 新出来的微信炸屎动画很多人都玩过了,所以先仿照一个微信扔炸弹的动画,在后续有时间会做一个完整的,效果如下: 具体实现 其中最麻烦的就是绘制抛物线了,爆炸的效果只是播放了一个动画,另外微信貌似都是通过代码绘制的,可能不是动画,奈何没有人家那技术,...
继续阅读 »

前言


新出来的微信炸屎动画很多人都玩过了,所以先仿照一个微信扔炸弹的动画,在后续有时间会做一个完整的,效果如下:



具体实现


其中最麻烦的就是绘制抛物线了,爆炸的效果只是播放了一个动画,另外微信貌似都是通过代码绘制的,可能不是动画,奈何没有人家那技术,只能找一张动画来凑合。


二阶贝塞尔曲线


抛物线在这里是通过二阶贝塞尔曲线来完成,所以先来了解下什么是二阶贝塞尔曲线,从下图中可以发现,二阶贝塞尔曲线有三个关键点,我们可以称作起点坐标、终点坐标,还有控制点。


录屏_选择区域_20210615170032.gif


起点和终点坐标好理解,控制点可以理解成开始下降的转折点,而古老的数学大神早就提供好了公式,我们只需要向这个公式提供这几个参数即可得到x、y,当然还有个参数是时间,有了时间控制,我们可以在指定秒内把他平滑的绘制完成。


公式如下:


x = (1 - t)^2 * 0 + 2 t (1 - t) * 1 + t^2 * 1 = 2 t (1 - t) + t^2
y= (1 - t)^2 * 1 + 2 t (1 - t) * 1 + t^2 * 0 = (1 - t)^2 + 2 t (1 - t)

自定义二阶贝塞尔曲线计算器


提到动画,首先可能会想到ObjectAnimator类,没错,抛物线也是通过ObjectAnimator来完成的,只不过我们需要自定义一个TypeEvaluator,用来提供二阶贝塞尔曲线的x和y。


TypeEvaluator只有一个方法,定义如下:


public abstract T evaluate (float fraction, 
T startValue,
T endValue)



fraction表示开始值和结束值之间的比例,startValue、endValue分别是开始值和结束值,这个比例也可以当作是时间,可能官方一点叫比例,他会自动计算,值的范围是0-1,比如取值0.5的时候就是动画完成了一半,1的时候动画完成。


所以套入二阶贝塞尔曲线公式得到如下代码:


class PointFTypeEvaluator(var control: PointF) : TypeEvaluator<PointF> {
override fun evaluate(fraction: Float, startValue: PointF, endValue: PointF): PointF {
return getPointF(startValue, endValue, control, fraction)
}

private fun getPointF(start: PointF, end: PointF, control: PointF, t: Float): PointF {
val pointF = PointF()
pointF.x = (1 - t) * (1 - t) * start.x + 2 * t * (1 - t) * control.x + t * t * end.x
pointF.y = (1 - t) * (1 - t) * start.y + 2 * t * (1 - t) * control.y + t * t * end.y
return pointF
}

}

播放动画


然后使用ObjectAnimator进行播放。


 val animator = ObjectAnimator.ofObject(activityMainBinding.boom, "mPointF",
PointFTypeEvaluator(controlP), startP, endP)

注意的是这个View需要有point方法,参数是PointF,方法内主要完成x和y的设置。


 public void setPoint(PointF pointF) {
setX(pointF.x);
setY(pointF.y);
}

当然微信炸弹落地的位置是随机的,我们也加个随机。


class MainActivity : AppCompatActivity() {
lateinit var binding: ActivityMainBinding;

private fun getRandom(max: Int, min: Int): Int {
val random = java.util.Random()
return random.nextInt(max - min + 1) + min
}

private fun getRandomPointF():PointF{
val outMetrics = DisplayMetrics()
val offset = 100
windowManager.defaultDisplay.getMetrics(outMetrics)
val width = outMetrics.widthPixels
val height = outMetrics.heightPixels
return PointF(getRandom(width / 2 + offset, width / 2 - offset).toFloat(), getRandom(height / 2 + offset, height / 2 - offset).toFloat())
}


override fun onCreate(savedInstanceState: Bundle?)
{
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main);



binding.button.setOnClickListener {
binding!!.boom.visibility = View.VISIBLE
val startP = PointF()
val endP = PointF()
val controlP = PointF()
val randomPointF = getRandomPointF()
startP.x = 916f
startP.y = 1353f
endP.x = randomPointF.x
endP.y = randomPointF.y
controlP.x = randomPointF.x + getRandom(200, 50)
controlP.y = randomPointF.y - getRandom(200, 50)
val animator = ObjectAnimator.ofObject(binding.boom, "point",
PointFTypeEvaluator(controlP), startP, endP)

animator.start()
}
}

}

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">


<data>

</data>

<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">



<com.airbnb.lottie.LottieAnimationView
android:visibility="gone"
android:id="@+id/lottie"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:lottie_fileName="boom.json">
</com.airbnb.lottie.LottieAnimationView>

<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="开始">
</Button>

<com.example.kotlindemo.widget.MyImageView
android:id="@+id/boom"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/ic_boom"
android:visibility="gone">
</com.example.kotlindemo.widget.MyImageView>
</RelativeLayout>
</layout>

效果如下:


录屏_选择区域_20210615174149.gif


爆炸效果


爆炸效果是使用的动画,用的lottie框架,这里提供爆炸文件的下载地址。


https://lottiefiles.com/download/public/9990-explosion

有了结束的坐标点,只需要吧LottieAnimationView移动到对应位置进行播放即可,播放后隐藏,完整代码如下:


package com.example.kotlindemo

import android.animation.Animator
import android.animation.ObjectAnimator
import android.content.Context
import android.content.Intent
import android.graphics.BitmapFactory
import android.graphics.PointF
import android.media.projection.MediaProjectionManager
import android.os.Build
import android.os.Bundle
import android.util.DisplayMetrics
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import com.example.kotlindemo.databinding.ActivityMainBinding
import com.example.kotlindemo.widget.PointFTypeEvaluator
import meow.bottomnavigation.MeowBottomNavigation
import kotlin.random.Random


class MainActivity : AppCompatActivity() {
lateinit var binding: ActivityMainBinding;

private fun getRandom(max: Int, min: Int): Int {
val random = java.util.Random()
return random.nextInt(max - min + 1) + min
}

private fun getRandomPointF():PointF{
val outMetrics = DisplayMetrics()
val offset = 100
windowManager.defaultDisplay.getMetrics(outMetrics)
val width = outMetrics.widthPixels
val height = outMetrics.heightPixels
return PointF(getRandom(width / 2 + offset, width / 2 - offset).toFloat(), getRandom(height / 2 + offset, height / 2 - offset).toFloat())
}


override fun onCreate(savedInstanceState: Bundle?)
{
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main);


binding!!.button.setOnClickListener {
binding!!.boom.visibility = View.VISIBLE
val startP = PointF()
val endP = PointF()
val controlP = PointF()
val randomPointF = getRandomPointF()
startP.x = 916f
startP.y = 1353f
endP.x = randomPointF.x
endP.y = randomPointF.y
controlP.x = randomPointF.x + getRandom(200, 50)
controlP.y = randomPointF.y - getRandom(200, 50)
val animator = ObjectAnimator.ofObject(binding.boom, "point",
PointFTypeEvaluator(controlP), startP, endP)
animator.duration = 600
animator.addListener(object : Animator.AnimatorListener {
override fun onAnimationStart(animation: Animator) {}
override fun onAnimationEnd(animation: Animator) {
val measuredHeight = binding.lottie.measuredHeight
val measuredWidth = binding.lottie.measuredWidth
binding.lottie.x = randomPointF.x - measuredWidth / 2
binding.lottie.y = randomPointF.y - measuredHeight / 2
binding.lottie.visibility = View.VISIBLE
binding.boom.visibility = View.GONE
binding.lottie.playAnimation()
binding.lottie.addAnimatorListener(object : Animator.AnimatorListener {
override fun onAnimationStart(animation: Animator) {}
override fun onAnimationEnd(animation: Animator) {
binding.lottie.visibility = View.GONE
}

override fun onAnimationCancel(animation: Animator) {}
override fun onAnimationRepeat(animation: Animator) {}
})
}

override fun onAnimationCancel(animation: Animator) {}
override fun onAnimationRepeat(animation: Animator) {}
})
animator.start()
}

}

}




作者:i听风逝夜
链接:https://juejin.cn/post/6973957344845627399
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android App唤醒丶保活详解 , 以及代码展示

安卓进程进程保活分为: 黑色保活,白色保活,灰色保活 黑色保活: 可以说黑色保活,可以通过网络切换,拍照,拍视频,开机,利用系统产生的广播唤醒app,接入三方的sdk也会唤醒一些app,如支付宝,微信..........这样的话,这样的话,不敢想象系统存...
继续阅读 »

安卓进程进程保活分为:


黑色保活,白色保活,灰色保活


黑色保活:


可以说黑色保活,可以通过网络切换,拍照,拍视频,开机,利用系统产生的广播唤醒app,接入三方的sdk也会唤醒一些app,如支付宝,微信..........这样的话,这样的话,不敢想象系统存活会给系统带来多大的负担,所以我们的安卓手机也变得卡了,google官方可能也认识了这么一点,所以取消了


ACTION_NEW_PICTURE(拍照),ACTION_NEW_VIDEO(拍视频),CONNECTIVITY_ACTION(网络切换)


app也会随着做一点改变,(不过sdk的使用还是会通过一个app启动相关的一些app , 黑色保活我个人认为不推荐使用,毕竟为了我们广大安卓用户。)


白色保活:


白色保活手段非常简单,就是调用系统api启动一个前台的Service进程,这样会在系统的通知栏生成一个Notification,用来让用户知道有这样一个app在运行着,哪怕当前的app退到了后台。




不过用户看到这个图标的时候,都会把它清空的。。。。



灰色保活:


可以说,灰色保活是用的最多,当用户不知不觉中这个app程序已经在后台运行了。


它是利用系统的漏洞来启动一个前台的Service进程,与普通的启动方式区别在于,它不会在系统通知栏处出现一个Notification,看起来就如同运行着一个后台Service进程一样。这样做带来的好处就是,用户无法察觉到你运行着一个前台进程(因为看不到Notification),但你的进程优先级又是高于普通后台进程的。API < 18,启动前台Service时直接传入new Notification();API >= 18,同时启动两个id相同的前台Service,然后再将后启动的Service做stop处理;


安卓app唤醒:


其实app唤醒的介绍很好说,app唤醒就是当打开一个app的时候,另一个app里有对应刚打开那个app的属性标志,根据你想要的唤醒方式,执行不同的代码操作,这样就可以唤醒另一个没打开的app了。(代码在最下面)


下面我展示一下这几种状态下的代码:


这个是xml布局,主要是为了展示我所介绍的几种保活方式:


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin">

<Button
android:id="@+id/mBtn_white"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="白色保活" />

<Button
android:id="@+id/mBtn_gray"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="灰色保活" />

<Button
android:id="@+id/mBtn_black"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="黑色保活(发广播)" />

<Button
android:id="@+id/mBtn_background_service"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="普通后台 Service 进程" />

</LinearLayout>

下面是主要实现类:


WakeReceiver


import android.app.Notification;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.IBinder;
import android.util.Log;

public class WakeReceiver extends BroadcastReceiver {
private final static String TAG = WakeReceiver.class.getSimpleName();
private final static int WAKE_SERVICE_ID = -1111;
/**
* 灰色保活手段唤醒广播的action
*/
public final static String GRAY_WAKE_ACTION = "com.wake.gray";
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (GRAY_WAKE_ACTION.equals(action)) {
Log.i(TAG, "wake !! wake !! ");

Intent wakeIntent = new Intent(context, WakeNotifyService.class);
context.startService(wakeIntent);
}
}
/**
* 用于其他进程来唤醒UI进程用的Service
*/
public static class WakeNotifyService extends Service {

@Override
public void onCreate() {
Log.i(TAG, "WakeNotifyService->onCreate");
super.onCreate();
}

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.i(TAG, "WakeNotifyService->onStartCommand");
if (Build.VERSION.SDK_INT < 18) {
startForeground(WAKE_SERVICE_ID, new Notification());//API < 18 ,此方法能有效隐藏Notification上的图标
} else {
Intent innerIntent = new Intent(this, WakeGrayInnerService.class);
startService(innerIntent);
startForeground(WAKE_SERVICE_ID, new Notification());
}
return START_STICKY;
}

@Override
public IBinder onBind(Intent intent) {
// TODO: Return the communication channel to the service.
throw new UnsupportedOperationException("Not yet implemented");
}

@Override
public void onDestroy() {
Log.i(TAG, "WakeNotifyService->onDestroy");
super.onDestroy();
}
}

/**
* 给 API >= 18 的平台上用的灰色保活手段
*/
public static class WakeGrayInnerService extends Service {

@Override
public void onCreate() {
Log.i(TAG, "InnerService -> onCreate");
super.onCreate();
}

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.i(TAG, "InnerService -> onStartCommand");
startForeground(WAKE_SERVICE_ID, new Notification());
//stopForeground(true);
stopSelf();
return super.onStartCommand(intent, flags, startId);
}

@Override
public IBinder onBind(Intent intent) {
// TODO: Return the communication channel to the service.
throw new UnsupportedOperationException("Not yet implemented");
}

@Override
public void onDestroy() {
Log.i(TAG, "InnerService -> onDestroy");
super.onDestroy();
}
}
}

BackGroundService


import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.util.Log;

/**
* 普通的后台Service进程
*
* @author clock
* @since 2016-04-12
*/
public class BackgroundService extends Service {

private final static String TAG = BackgroundService.class.getSimpleName();

@Override
public void onCreate() {
Log.i(TAG, "onCreate");
super.onCreate();
}

@Override
public IBinder onBind(Intent intent) {
// TODO: Return the communication channel to the service.
throw new UnsupportedOperationException("Not yet implemented");
}

@Override
public void onDestroy() {
Log.i(TAG, "onDestroy");
super.onDestroy();
}
}

GrayService


import android.app.AlarmManager;
import android.app.Notification;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.IBinder;
import android.util.Log;
import com.example.renzheng.receiver.WakeReceiver;

/**
* 灰色保活手法创建的Service进程
*
* @author Clock
* @since 2016-04-12
*/
public class GrayService extends Service {

private final static String TAG = GrayService.class.getSimpleName();
/**
* 定时唤醒的时间间隔,5分钟
*/
private final static int ALARM_INTERVAL = 5 * 60 * 1000;
private final static int WAKE_REQUEST_CODE = 6666;

private final static int GRAY_SERVICE_ID = -1001;

@Override
public void onCreate() {
Log.i(TAG, "GrayService->onCreate");
super.onCreate();
}

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.i(TAG, "GrayService->onStartCommand");
if (Build.VERSION.SDK_INT < 18) {
startForeground(GRAY_SERVICE_ID, new Notification());//API < 18 ,此方法能有效隐藏Notification上的图标
} else {
Intent innerIntent = new Intent(this, GrayInnerService.class);
startService(innerIntent);
startForeground(GRAY_SERVICE_ID, new Notification());
}

//发送唤醒广播来促使挂掉的UI进程重新启动起来
AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
Intent alarmIntent = new Intent();
alarmIntent.setAction(WakeReceiver.GRAY_WAKE_ACTION);
PendingIntent operation = PendingIntent.getBroadcast(this, WAKE_REQUEST_CODE, alarmIntent, PendingIntent.FLAG_UPDATE_CURRENT);
alarmManager.setInexactRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis(), ALARM_INTERVAL, operation);

return START_STICKY;
}

@Override
public IBinder onBind(Intent intent) {
throw new UnsupportedOperationException("Not yet implemented");
}

@Override
public void onDestroy() {
Log.i(TAG, "GrayService->onDestroy");
super.onDestroy();
}

/**
* 给 API >= 18 的平台上用的灰色保活手段
*/
public static class GrayInnerService extends Service {

@Override
public void onCreate() {
Log.i(TAG, "InnerService -> onCreate");
super.onCreate();
}

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.i(TAG, "InnerService -> onStartCommand");
startForeground(GRAY_SERVICE_ID, new Notification());
//stopForeground(true);
stopSelf();
return super.onStartCommand(intent, flags, startId);
}

@Override
public IBinder onBind(Intent intent) {
// TODO: Return the communication channel to the service.
throw new UnsupportedOperationException("Not yet implemented");
}

@Override
public void onDestroy() {
Log.i(TAG, "InnerService -> onDestroy");
super.onDestroy();
}
}
}

WhileService


import android.app.Notification;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.support.v7.app.NotificationCompat;
import android.util.Log;
import com.example.renzheng.MainActivity;
import com.example.renzheng.R;
/**
* 正常的系统前台进程,会在系统通知栏显示一个Notification通知图标
*
* @author clock
* @since 2016-04-12
*/
public class WhiteService extends Service {

private final static String TAG = WhiteService.class.getSimpleName();
private final static int FOREGROUND_ID = 1000;

@Override
public void onCreate() {
Log.i(TAG, "WhiteService->onCreate");
super.onCreate();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.i(TAG, "WhiteService->onStartCommand");
NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
builder.setSmallIcon(R.mipmap.ic_launcher);
builder.setContentTitle("Foreground");
builder.setContentText("I am a foreground service");
builder.setContentInfo("Content Info");
builder.setWhen(System.currentTimeMillis());
Intent activityIntent = new Intent(this, MainActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 1, activityIntent, PendingIntent.FLAG_UPDATE_CURRENT);
builder.setContentIntent(pendingIntent);
Notification notification = builder.build();
startForeground(FOREGROUND_ID, notification);
return super.onStartCommand(intent, flags, startId);
}

@Override
public IBinder onBind(Intent intent) {
// TODO: Return the communication channel to the service.
throw new UnsupportedOperationException("Not yet implemented");
}

@Override
public void onDestroy() {
Log.i(TAG, "WhiteService->onDestroy");
super.onDestroy();
}
}

MainActivity


import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import com.example.renzheng.service.BackgroundService;
import com.example.renzheng.service.GrayService;
import com.example.renzheng.service.WhiteService;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

private final static String TAG = MainActivity.class.getSimpleName();
/**
* 黑色唤醒广播的action
*/
private final static String BLACK_WAKE_ACTION = "com.wake.black";

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.mBtn_white).setOnClickListener(this);
findViewById(R.id.mBtn_gray).setOnClickListener(this);
findViewById(R.id.mBtn_black).setOnClickListener(this);
findViewById(R.id.mBtn_background_service).setOnClickListener(this);
}

@Override
public void onClick(View v) {
int viewId = v.getId();
if (viewId == R.id.mBtn_white) { //系统正常的前台Service,白色保活手段
Intent whiteIntent = new Intent(getApplicationContext(), WhiteService.class);
startService(whiteIntent);

} else if (viewId == R.id.mBtn_gray) {//利用系统漏洞,灰色保活手段(API < 18 和 API >= 18 两种情况)
Intent grayIntent = new Intent(getApplicationContext(), GrayService.class);
startService(grayIntent);

} else if (viewId == R.id.mBtn_black) { //拉帮结派,黑色保活手段,利用广播唤醒队友
Intent blackIntent = new Intent();
blackIntent.setAction(BLACK_WAKE_ACTION);
sendBroadcast(blackIntent);

} else if (viewId == R.id.mBtn_background_service) {//普通的后台进程
Intent bgIntent = new Intent(getApplicationContext(), BackgroundService.class);
startService(bgIntent);
}
}
}

代码注册权限:


 


<receiver
android:name=".receiver.WakeReceiver"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="com.wake.gray" />
</intent-filter>
</receiver>

<service
android:name=".service.WhiteService"
android:enabled="true"
android:exported="false"
android:process=":white" />
<service
android:name=".service.GrayService"
android:enabled="true"
android:exported="false"
android:process=":gray" />
<service
android:name=".service.GrayService$GrayInnerService"
android:enabled="true"
android:exported="false"
android:process=":gray" />
<service
android:name=".service.BackgroundService"
android:enabled="true"
android:exported="false"
android:process=":bg" />
<service
android:name=".receiver.WakeReceiver$WakeNotifyService"
android:enabled="true"
android:exported="false" />

<service
android:name=".receiver.WakeReceiver$WakeGrayInnerService"
android:enabled="true"
android:exported="false" />

 


下面是app唤醒代码:


有2个APP,分别为A和B,当A活着的时候,试着开启B的后台服务,将原本杀死的B的后台服务程序活起来。反之也一样。


1.先看B的代码:


创建一个服务B,给服务添加一个process属性,设置action。


 

<service
android:name=".B"
android:process=":test">
<intent-filter>
<action android:name="yangyang" />
</intent-filter>
</service>

B的代码,在onStartCommand方法中弹出toast:


public class B extends Service {
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}

@Override
public int onStartCommand(Intent intent, int flags, int startId) {

Toast.makeText(this, "B 已经唤醒", Toast.LENGTH_SHORT).show();
return START_STICKY;
}
}

2.看A的代码,在MainActivity中点击开启B应用的B服务的代码:


public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

Button btn = (Button) findViewById(R.id.btn);

btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
sendService();
}
});
}

private void sendService() {
boolean find = false;

ActivityManager mActivityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
Intent serviceIntent = new Intent();

for (ActivityManager.RunningServiceInfo runningServiceInfo : mActivityManager.getRunningServices(100)) {
if (runningServiceInfo.process.contains(":test")) {//判断service是否在运行
Log.e("zhang", "process:" + runningServiceInfo.process);
find = true;
}
}
//判断服务是否起来,如果服务没起来,就唤醒
if (!find) {
serviceIntent.setPackage("com.example.b);
serviceIntent.setAction("yangyang");
startService(serviceIntent);
Toast.makeText(this, "开始唤醒 B", Toast.LENGTH_SHORT).show();
}else {
Toast.makeText(this, "B 不用唤醒", Toast.LENGTH_SHORT).show();
}
}
}

这里只是写了A启动B服务的代码,反之也是一样的。被启动应用的Servcie在AndroidMainfest.xml中注册时注意,添加process属性,和设置action匹配规则。


————————————————
版权声明:本文为CSDN博主「看美丽风晴」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/nazicsdn/article/details/79752617

收起阅读 »

iOS - Block 准备面试必须了解的东西

一.Block的本质        block本质是一个OC对象,它里面有个isa指针,封装了函数调用环境的OC对象,封装了函数调用上下文的OC对象。查看Block源码:struct __block_impl {    void*isa;    int Fla...
继续阅读 »

一.Block的本质

        block本质是一个OC对象,它里面有个isa指针,封装了函数调用环境的OC对象,封装了函数调用上下文的OC对象。


查看Block源码:

struct __block_impl {

    void*isa;

    int Flags;

    int Reserved;

    void *FuncPtr;

};

struct __main_block_impl_0 {

  struct __block_impl impl;

  struct__main_block_desc_0* Desc;

  // 构造函数(类似于OC的init方法),返回结构体对象

  __main_block_impl_0(void*fp,struct__main_block_desc_0 *desc,intflags=0) {

    impl.isa = &_NSConcreteStackBlock;

    impl.Flags = flags;

    impl.FuncPtr = fp;

    Desc = desc;

  }

};

// 封装了block执行逻辑的函数

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_main_c60393_mi_0);

        }

static struct __main_block_desc_0 {

  size_treserved;

  size_tBlock_size;

} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

int main(intargc,constchar* argv[]) {

    /* @autoreleasepool */{__AtAutoreleasePool__autoreleasepool;

        // 定义block变量

        void(*block)(void) = &__main_block_impl_0(

                                                   __main_block_func_0,

                                                   &__main_block_desc_0_DATA

                                                   );

        // 执行block内部的代码

        block->FuncPtr(block);

    }

    return0;

}

说明:FuncPtr:指向调用函数的地址,__main_block_desc_0 :block描述信息,Block_size:block的大小

二.Block变量的捕获

2.1局部变量的捕获

        对于 block 外的变量引用,block 默认是将其复制到其数据结构中来实现访问的。也就是说block的自动变量截获只针对block内部使用的自动变量, 不使用则不截获, 因为截获的自动变量会存储于block的结构体内部, 会导致block体积变大。特别要注意的是默认情况下block只能访问不能修改局部变量的值。

int age=10;

void(^Block)(void)=^{

NSLog(@"age:%d",age);

};

age=20;

Block();

2.2__block 修饰的外部变量

        对于用 __block 修饰的外部变量引用,block 是复制其引用地址来实现访问的。block可以修改__block 修饰的外部变量的值

__block int age=10;

myBlock block=^{

NSLog(@"age = %d",age);

};

age=18;

block();

输出:18;

auto int age=10;

static int num=25;

void(^Block)(void)=^{

NSLog(@"age:%d,num:%d",age,num);

};

age=20;

num=11;

Block();

        输出结果为:age:10,num:11,auto变量block访问方式是值传递,也就是当block定义的时候,值已经传到block里面了,static变量block访问方式是指针传递,auto自动变量可能会销毁的,内存可能会消失,不采用指针访问;static变量一直保存在内存中,指针访问即可,block不需要对全局变量捕获,都是直接采用取值的,局部变量的捕获是因为考虑作用域的问题,需要跨函数访问,就需要捕获,当出了作用域,局部变量已经被销毁,这时候如果block访问,就会出问题。

2.2.block变量捕获机制




 block里访问self,self是当调用block函数的参数,参数是局部变量,self指向调用者,所以它也会捕获self,block里访问成员,成员变量的访问其实是self->xx,先捕获self,再通过self访问里面的成员变量。

3.3Block的类型

        block的类型,取决于isa指针,可以通过调用class方法或者isa指针查看具体类型,最终都是继承自NSBlock类型

__NSGlobalBlock __ ( _NSConcreteGlobalBlock )全局block即数据区

__NSStackBlock __ ( _NSConcreteStackBlock )堆区block

__NSMallocBlock __ ( _NSConcreteMallocBlock )栈区block

        说明:堆区,程序员自己控制,程序员自己管理,栈区,系统自动控制,一般我们使用最多的是堆区Block,判断类型的根据是没有访问auto变量的block是__NSGlobalBlock __ ,放在数据段访问了auto变量的block是__NSStackBlock __;[__NSStackBlock __ copy]操作就变成了__NSMallocBlock __,__NSGlobalBlock __ 调用copy操作后,什么也不做__NSStackBlock __ 调用copy操作后,复制效果是:从栈复制到堆;副本存储位置是堆__NSMallocBlock __ 调用copy操作后,复制效果是:引用计数增加;副本存储位置是堆,在ARC环境下,编译器会根据情况自动将栈上的block复制到堆上的几种情况是:

                1.block作为函数返回值时

                2.将block赋值给__strong指针时

                3.block作为Cocoa API中方法名含有usingBlock的方法参数时

                4.block作为GCD API的方法参数时

三.对象类型的auto变量

typedefvoid(^XBTBlock)(void);

XBTBlock block;

{

Person*p=[[Person alloc]init];

p.age=10;

block=^{

NSLog(@"======= %d",p.age);

};}

Person.m

-(void)dealloc{

NSLog(@"Person - dealloc");

}

        说明:block为堆block,block里面有一个Person指针,Person指针指向Person对象。只要block还在,Person就还在。block强引用了Person对象。在MRC下,就会打印,因为堆空间的block会对Person对象retain操作,拥有一次Person对象。无论MRC还是ARC,栈空间上的block,不会持有对象;堆空间的block,会持有对象。

特别说明:block内部访问了对象类型的auto变量时,是否会强引用?

栈block

a) 如果block是在栈上,将不会对auto变量产生强引用

b) 栈上的block随时会被销毁,也没必要去强引用其他对象

堆block

1.如果block被拷贝到堆上:

a) 会调用block内部的copy函数

b) copy函数内部会调用_Block_object_assign函数

c) _Block_object_assign函数会根据auto变量的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用

2.如果block从堆上移除

a) 会调用block内部的dispose函数

b) dispose函数内部会调用_Block_object_dispose函数

c) _Block_object_dispose函数会自动释放引用的auto变量(release)

正确答案:

如果block在栈空间,不管外部变量是强引用还是弱引用,block都会弱引用访问对象

如果block在堆空间,如果外部强引用,block内部也是强引用;如果外部弱引用,block内部也是弱引用

3.2gcd的block中引用 Person对象什么时候销毁?

eg:-(void)touchesBegan:(NSSet *)toucheswithEvent:(UIEvent*)event{

    Person*person = [[Personalloc]init];

    person.age=10;

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

        NSLog(@"age:%d",person.age);

    });

    NSLog(@"touchesBegan");

}

输出:touchesBegan

            age:10

            Person-dealloc

        说明:gcd的block默认会做copy操作,即dispatch_after的block是堆block,block会对Person强引用,block销毁时候Person才会被释放,如果上诉Person用__weak。即添加代码为__weak Person*weakPerson=person;,在Block中变成NSLog(@"age:%p",weakPerson);,它就不输出age,使用__weak修饰过后的对象,堆block会采用弱引用,无法延时Person的寿命,所以在touchesBegan函数结束后,Person就会被释放,gcd就无法捕捉到Person,gcd内部只要有强引用Person,Person就会等待执行完再销毁!如果gcd内部先强引用后弱引用,Person会等待强引用执行完毕后释放,只要强引用执行完,就不会等待后执行的弱引用,会直接释放的

eg:-(void)touchesBegan:(NSSet *)toucheswithEvent:(UIEvent*)event{

    Person*person = [[Personalloc]init];

    person.age=10;

    __weakPerson*weakPerson = person;

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4.0 * NSEC_PER_SEC)),

                   dispatch_get_main_queue(), ^{

        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

            NSLog(@"2-----age:%p",weakPerson);

        });

        NSLog(@"1-----age:%p",person);

    });

    NSLog(@"touchesBegan");

}

四.Block的修饰符

        block在修改NSMutableArray,不需要加__block,auto修饰变量,block无法修改,因为block使用的时候是内部创建了变量来保存外部的变量的值,block只有修改内部自己变量的权限,无法修改外部变量的权限。

        static修饰变量,block可以修改,因为block把外部static修饰变量的指针存入,block直接修改指针指向变量值,即可修改外部变量值。全局变量值,全局变量无论哪里都可以修改,当然block内部也可以修改。

eg:__block int age = 10,系统做了哪些---》编译器会将__block变量包装成一个对象

__block 修饰符作用:

        __block可以用于解决block内部无法修改auto变量值的问题

        __block不能修饰全局变量、静态变量(static)

        编译器会将__block变量包装成一个对象

        __block修改变量:age->__forwarding->age        

        __Block_byref_age_0结构体内部地址和外部变量age是同一地址

        __block的内存管理---->当block在栈上时,并不会对__block变量产生强引用

block的属性修饰词为什么是copy?

        block一旦没有进行copy操作,就不会在堆上

        block在堆上,程序员就可以对block做内存管理等操作,可以控制block的生命周期,会调用block内部的copy函数

        copy函数内部会调用_Block_object_assign函数

        _Block_object_assign函数会对__block变量形成强引用(retain)

        对于__block 修饰的变量 assign函数对其强引用;对于外部对象 assign函数根据外部如何引用而引用,当block从堆中移除时,会调用block内部的dispose函数dispose函数内部会调用_Block_object_dispose函数_Block_object_dispose函数会自动释放引用的__block变量(release),当block在栈上时,对它们都不会产生强引用,当block拷贝到堆上时,都会通过copy函数来处理它们,对于__block 修饰的变量 assign函数对其强引用;对于外部对象 assign函数根据外部如何引用而引用

__block的__forwarding指针说明:

        栈上__block的__forwarding指向本身

        栈上__block复制到堆上后,栈上block的__forwarding指向堆上的block,堆上block的__forwarding指向本身

五. block循环引用

        1.ARC下如何解决block循环引用的问题?

        三种方式:__weak、__unsafe_unretained、__block

        1)第一种方式:__weak

        Person*person=[[Person alloc]init];

        // __weak Person *weakPerson = person;

        __weaktypeof(person)weakPerson=person;

        person.block=^{

            NSLog(@"age is %d",weakPerson.age);

        };

        2)第二种方式:__unsafe_unretained

        __unsafe_unretained Person*person=[[Person alloc]init];

        person.block=^{

            NSLog(@"age is %d",weakPerson.age);

        };

        3)第三种方式:__block

        __block Person*person=[[Person alloc]init];

        person.block=^{

            NSLog(@"age is %d",person.age);

            person=nil;

        };

        person.block();

三种方法比较:__weak:不会产生强引用,指向的对象销毁时,会自动让指针置为nil,__unsafe_unretained:不会产生强引用,不安全,指向的对象销毁时,指针存储的地址值不变,__block:必须把引用对象置位nil,并且要调用该block









作者:枫紫
链接:https://www.jianshu.com/p/4bde3936b154






收起阅读 »

iOS - Metal的认识

一.Metal 简介        在 WWDC 2014 上,Apple为游戏开发者推出了新的平台技术 Metal,该技术能够为 3D 图像提高 10 倍的渲...
继续阅读 »

一.Metal 简介

        在 WWDC 2014 上,Apple为游戏开发者推出了新的平台技术 Metal,该技术能够为 3D 图像提高 10 倍的渲染性能,并支持大家熟悉的游戏引擎及公司。

        Metal 是一种低层次的渲染应用程序编程接口,提供了软件所需的最低层,保证软件可以运行在不同的图形芯片上。Metal 提升了 A7 与 A8 处理器效能,让其性能完全发挥。

        Metal,充分利用GPU的运算能力,在现阶段,AVFoundation ⼈脸识别/.... 等大量需要显示计算的时候,苹果采用了硬件加速器驱动GPU工作,在音视频方面,⾳频编码/解码 / 视频编码/解码 ->压缩任务 ->都与硬件加速器分不开,苹果提供的Metal,能发挥GPU/CPu的最大性能,并且管理我们的资源。

二.Metal的渲染流程

        Metal的渲染流程借鉴了OpenGLES的流程,它通过控制顶点着色器/片元着色器(Metal里面叫顶点函数/片元函数),交给帧缓冲区,最后显示到屏幕上





值得注意的是,在OpenGlES中,图元装配有9中,在Metal中,图元装配只有五种,他们分别是:

                 MTLPrimitiveTypePoint = 0, 点

                 MTLPrimitiveTypeLine = 1, 线段

                 MTLPrimitiveTypeLineStrip = 2, 线环

                 MTLPrimitiveTypeTriangle = 3,  三角形

                 MTLPrimitiveTypeTriangleStrip = 4, 三角型扇

三.Metal的初级准备工作

3.1Metal的注意事项

        在讲Metal的初级使用之前,我们先来看看苹果爸爸给我们的建议,首先,苹果建议我们Separate Your Rendering Loop,即分离我们渲染,Metal给我们提供了一个View,叫MTKView,它继承自UiView,它主要的渲染是通过MTKViewDelegate协议回调实现,两个重要的协议方法是:

        1)当MTKView视图发生大小改变时调用

        /*!

         @method mtkView:drawableSizeWillChange:

         @abstract Called whenever the drawableSize of the view will change

         @discussion Delegate can recompute view and projection matricies or regenerate any buffers to be compatible with the new view size or resolution

         @paramviewMTKView which called this method

         @paramsizeNew drawable size in pixels

         */

- (void)mtkView:(nonnull MTKView *)view drawableSizeWillChange:(CGSize)size;

        2)每当视图需要渲染时调用

        /*!

         @method drawInMTKView:

         @abstract Called on the delegate when it is asked to render into the view

         @discussion Called on the delegate when it is asked to render into the view

         */

        - (void)drawInMTKView:(nonnullMTKView*)view;

    3.2  Metal是如何驱动GPU工作的?



相关对应代码:在ViewController中,我们把当前的View变成MTKView,当然你也可以用self.view添加一个子视图View,CCRenderer是自定义的一个类,主要是分离MTview的渲染,

         _view.device = MTLCreateSystemDefaultDevice();一个MTLDevice 对象就代表这着一个GPU,通常我们可以调用方法MTLCreateSystemDefaultDevice()来获取代表默认的GPU单个对象.

        在CCRenderer中的初始化方法中- (id)initWithMetalKitView:(MTKView *)mtkView我们拿到device,创建newCommandQueue队列:

                _commandQueue = [_device newCommandQueue];

        所有应用程序需要与GPU交互的第一个对象是一个对象->MTLCommandQueue. 你使用MTLCommandQueue 去创建对象,并且加入MTLCommandBuffer 对象中.确保它们能够按照正确顺序发送到GPU.对于每一帧,一个新的MTLCommandBuffer 对象创建并且填满了由GPU执行的命令.

        在CCRenderer中,我们实现了MTKView的协议代理方法,在- (void)drawInMTKView:(nonnullMTKView*)view中,我们通过创建好的队列再创建命令缓冲区并且加入到MTCommandBuffer对象中去:

                id<MTLCommandBuffer> commandBuffer = [_commandQueue commandBuffer];

        值得注意的是,在创建好命令缓冲区后,Metal提出了一个概念叫渲染描述符:(个人理解这个渲染描述符是给每个命令打上一个标记,GPU在工作的时候通过这个渲染描述符取出相应的命令,如果说的不对,请大神指点)从视图绘制中,获得渲染描述符:

                MTLRenderPassDescriptor *renderPassDescriptor = view.currentRenderPassDescriptor;

通过渲染描述符renderPassDescriptor创建MTLRenderCommandEncoder                

                id<MTLRenderCommandEncoder> renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor];

        最后 [renderEncoderendEncoding];

当编码器结束之后,命令缓存区就会接受到2个命令.

         1) present

         2) commit

         因为GPU是不会直接绘制到屏幕上,因此你不给出去指令.是不会有任何内容渲染到屏幕上.

        [commandBuffer presentDrawable:view.currentDrawable];

        [commandBuffercommit];

        至此,Metal的准备工作已经完成

四.用Metal渲染一个简单的三角形

在做好上面的准备的准备工作后:


//初始化MTKView

- (nonnull instancetype)initWithMetalKitView:(nonnull MTKView *)mtkView

{

    self= [superinit];

    if(self)

    {

        NSError*error =NULL;


        //1.获取GPU 设备

        _device= mtkView.device;

        //2.在项目中加载所有的(.metal)着色器文件

        // 从bundle中获取.metal文件

        id defaultLibrary = [_devicenewDefaultLibrary];

        //从库中加载顶点函数

        id vertexFunction = [defaultLibrarynewFunctionWithName:@"vertexShader"];

        //从库中加载片元函数

        id fragmentFunction = [defaultLibrarynewFunctionWithName:@"fragmentShader"];

        //3.配置用于创建管道状态的管道

        MTLRenderPipelineDescriptor *pipelineStateDescriptor = [[MTLRenderPipelineDescriptor alloc] init];

        //管道名称

        pipelineStateDescriptor.label=@"Simple Pipeline";

        //可编程函数,用于处理渲染过程中的各个顶点

        pipelineStateDescriptor.vertexFunction= vertexFunction;

        //可编程函数,用于处理渲染过程中各个片段/片元

        pipelineStateDescriptor.fragmentFunction= fragmentFunction;

        //一组存储颜色数据的组件

        pipelineStateDescriptor.colorAttachments[0].pixelFormat= mtkView.colorPixelFormat;


        //4.同步创建并返回渲染管线状态对象

        _pipelineState= [_devicenewRenderPipelineStateWithDescriptor:pipelineStateDescriptorerror:&error];

        //判断是否返回了管线状态对象

        if (!_pipelineState)

        {


            //如果我们没有正确设置管道描述符,则管道状态创建可能失败

            NSLog(@"Failed to created pipeline state, error %@", error);

            returnnil;

        }

        //5.创建命令队列

        _commandQueue = [_device newCommandQueue];

    }

    return self;

}

//每当视图需要渲染帧时调用

- (void)drawInMTKView:(nonnullMTKView*)view

{

    //1. 顶点数据/颜色数据

    staticconstCCVertextriangleVertices[] =

    {

        //顶点,    RGBA 颜色值

        { {  0.5, -0.25,0.0,1.0}, {1,0,0,1} },

        { { -0.5, -0.25,0.0,1.0}, {0,1,0,1} },

        { { -0.0f,0.25,0.0,1.0}, {0,0,1,1} },

    };

    //2.为当前渲染的每个渲染传递创建一个新的命令缓冲区

    id<MTLCommandBuffer> commandBuffer = [_commandQueue commandBuffer];

    //指定缓存区名称

    commandBuffer.label=@"MyCommand";


    //3.

    // MTLRenderPassDescriptor:一组渲染目标,用作渲染通道生成的像素的输出目标。

    MTLRenderPassDescriptor *renderPassDescriptor = view.currentRenderPassDescriptor;

    //判断渲染目标是否为空

    if(renderPassDescriptor !=nil)

    {

        //4.创建渲染命令编码器,这样我们才可以渲染到something

        id renderEncoder =[commandBufferrenderCommandEncoderWithDescriptor:renderPassDescriptor];

        //渲染器名称

        renderEncoder.label=@"MyRenderEncoder";

        //5.设置我们绘制的可绘制区域

        /*

        typedef struct {

            double originX, originY, width, height, znear, zfar;

        } MTLViewport;

         */

        //视口指定Metal渲染内容的drawable区域。 视口是具有x和y偏移,宽度和高度以及近和远平面的3D区域

        //为管道分配自定义视口需要通过调用setViewport:方法将MTLViewport结构编码为渲染命令编码器。 如果未指定视口,Metal会设置一个默认视口,其大小与用于创建渲染命令编码器的drawable相同。

        MTLViewportviewPort = {

            0.0,0.0,_viewportSize.x,_viewportSize.y,-1.0,1.0

        };

        [renderEncodersetViewport:viewPort];

        //[renderEncoder setViewport:(MTLViewport){0.0, 0.0, _viewportSize.x, _viewportSize.y, -1.0, 1.0 }];


        //6.设置当前渲染管道状态对象

        [renderEncodersetRenderPipelineState:_pipelineState];



        //7.从应用程序OC 代码 中发送数据给Metal 顶点着色器 函数

        //顶点数据+颜色数据

        //  1) 指向要传递给着色器的内存的指针

        //  2) 我们想要传递的数据的内存大小

        //  3)一个整数索引,它对应于我们的“vertexShader”函数中的缓冲区属性限定符的索引。

        [renderEncodersetVertexBytes:triangleVertices

                               length:sizeof(triangleVertices)

                              atIndex:CCVertexInputIndexVertices];

        //viewPortSize 数据

        //1) 发送到顶点着色函数中,视图大小

        //2) 视图大小内存空间大小

        //3) 对应的索引

        [renderEncodersetVertexBytes:&_viewportSize

                               length:sizeof(_viewportSize)

                              atIndex:CCVertexInputIndexViewportSize];



        //8.画出三角形的3个顶点

        // @method drawPrimitives:vertexStart:vertexCount:

        //@brief 在不使用索引列表的情况下,绘制图元

        //@param 绘制图形组装的基元类型

        //@param 从哪个位置数据开始绘制,一般为0

        //@param 每个图元的顶点个数,绘制的图型顶点数量

        /*

         MTLPrimitiveTypePoint = 0, 点

         MTLPrimitiveTypeLine = 1, 线段

         MTLPrimitiveTypeLineStrip = 2, 线环

         MTLPrimitiveTypeTriangle = 3,  三角形

         MTLPrimitiveTypeTriangleStrip = 4, 三角型扇

         */


        [renderEncoderdrawPrimitives:MTLPrimitiveTypeTriangle

                          vertexStart:0

                          vertexCount:3];

        //9.表示已该编码器生成的命令都已完成,并且从NTLCommandBuffer中分离

        [renderEncoderendEncoding];

        //10.一旦框架缓冲区完成,使用当前可绘制的进度表

        [commandBufferpresentDrawable:view.currentDrawable];

    }

    //11.最后,在这里完成渲染并将命令缓冲区推送到GPU

    [commandBuffercommit];

}



 

Metal文件:(语法下篇介绍)

#include 

//使用命名空间 Metal

using namespace metal;

// 导入Metal shader 代码和执行Metal API命令的C代码之间共享的头

#import "CCShaderTypes.h"

// 顶点着色器输出和片段着色器输入

//结构体

typedef struct

{

    //处理空间的顶点信息

    float4clipSpacePosition [[position]];

    //颜色

    float4color;

} RasterizerData;

//顶点着色函数

vertex RasterizerData

vertexShader(uintvertexID [[vertex_id]],

             constantCCVertex*vertices [[buffer(CCVertexInputIndexVertices)]],

             constantvector_uint2*viewportSizePointer [[buffer(CCVertexInputIndexViewportSize)]])

{

    /*

     处理顶点数据:

        1) 执行坐标系转换,将生成的顶点剪辑空间写入到返回值中.

        2) 将顶点颜色值传递给返回值

     */


    //定义out

    RasterizerDataout; 

//    //初始化输出剪辑空间位置

//    out.clipSpacePosition = vector_float4(0.0, 0.0, 0.0, 1.0);

//

//    // 索引到我们的数组位置以获得当前顶点

//    // 我们的位置是在像素维度中指定的.

//    float2 pixelSpacePosition = vertices[vertexID].position.xy;

//

//    //将vierportSizePointer 从verctor_uint2 转换为vector_float2 类型

//    vector_float2 viewportSize = vector_float2(*viewportSizePointer);

//

//    //每个顶点着色器的输出位置在剪辑空间中(也称为归一化设备坐标空间,NDC),剪辑空间中的(-1,-1)表示视口的左下角,而(1,1)表示视口的右上角.

//    //计算和写入 XY值到我们的剪辑空间的位置.为了从像素空间中的位置转换到剪辑空间的位置,我们将像素坐标除以视口的大小的一半.

//    out.clipSpacePosition.xy = pixelSpacePosition / (viewportSize / 2.0);

    out.clipSpacePosition= vertices[vertexID].position;

    //把我们输入的颜色直接赋值给输出颜色. 这个值将于构成三角形的顶点的其他颜色值插值,从而为我们片段着色器中的每个片段生成颜色值.

    out.color= vertices[vertexID].color;

    //完成! 将结构体传递到管道中下一个阶段:

    returnout;

}

//当顶点函数执行3次,三角形的每个顶点执行一次后,则执行管道中的下一个阶段.栅格化/光栅化.

// 片元函数

//[[stage_in]],片元着色函数使用的单个片元输入数据是由顶点着色函数输出.然后经过光栅化生成的.单个片元输入函数数据可以使用"[[stage_in]]"属性修饰符.

//一个顶点着色函数可以读取单个顶点的输入数据,这些输入数据存储于参数传递的缓存中,使用顶点和实例ID在这些缓存中寻址.读取到单个顶点的数据.另外,单个顶点输入数据也可以通过使用"[[stage_in]]"属性修饰符的产生传递给顶点着色函数.

//被stage_in 修饰的结构体的成员不能是如下这些.Packed vectors 紧密填充类型向量,matrices 矩阵,structs 结构体,references or pointers to type 某类型的引用或指针. arrays,vectors,matrices 标量,向量,矩阵数组.

fragmentfloat4fragmentShader(RasterizerDatain [[stage_in]])

{

    //返回输入的片元颜色

    returnin.color;

}

用于OC和Metal桥接的文件:

/*

 介绍:

 头文件包含了 Metal shaders 与C/OBJC 源之间共享的类型和枚举常数

*/

#ifndef CCShaderTypes_h

#define CCShaderTypes_h

// 缓存区索引值 共享与 shader 和 C 代码 为了确保Metal Shader缓存区索引能够匹配 Metal API Buffer 设置的集合调用

typedef enum CCVertexInputIndex

{

    //顶点

    CCVertexInputIndexVertices    =0,

    //视图大小

    CCVertexInputIndexViewportSize =1,

} CCVertexInputIndex;

//结构体: 顶点/颜色值

typedef struct

{

    // 像素空间的位置

    // 像素中心点(100,100)

    vector_float4 position;

    // RGBA颜色

    vector_float4 color;

} CCVertex;

#endif


作者:枫紫
链接:https://www.jianshu.com/p/a6f3c90d6ba5





收起阅读 »

iOS KVO底层原理&&KVO的isa指向

一.简单复习一下KVO的使用定义一个类,继承自NSObject,并添加一个name的属性#import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN @interface TCPerson ...
继续阅读 »

一.简单复习一下KVO的使用

  • 定义一个类,继承自NSObject,并添加一个name的属性
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface TCPerson : NSObject

@property (nonatomic, copy) NSString *name;

@end

NS_ASSUME_NONNULL_END

  • 在ViewController我们简单的使用一下KVO
#import "ViewController.h"
#import "TCPerson.h"
@interface ViewController ()
@property (nonatomic, strong) TCPerson *person1;
@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[TCPerson alloc]init];
self.person1.name = @"liu yi fei";
[self.person1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
}

/// 点击屏幕出发改变self.person1的name
/// @param touches touches description
/// @param event event description
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.person1.name = @"cang lao shi";
}

/// 监听回调
/// @param keyPath 监听的属性名字
/// @param object 被监听的对象
/// @param change 改变的新/旧值
/// @param context context description
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"监听到%@对象的%@发生了改变%@",object,keyPath,change);
}

/// 移除观察者
- (void)dealloc{
[self.person1 removeObserver:self forKeyPath:@"name"];
}
@end


当点击屏幕的时候,控制台输出:

2020-09-24 15:53:52.527734+0800 KVO_TC[9255:98204] 监听到<TCPerson: 0x600003444d10>对象的name发生了改变{
kind = 1;
new = "cang lao shi";
old = "liu yi fei";
}

二.深入剖析KVO的底层

  • 在- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.person1.name = @"cang lao shi";
    }我们知道self.person1.name的本质是[self.person1 setName:@"cang lao shi"];

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
// self.person1.name = @"cang lao shi";
[self.person1 setName:@"cang lao shi"];
}
在TCPerson的.m文件,我们从写setter方法并打断点,可以看到当我们点击屏幕的时候,我们发现进入了setter方法:

- (void)setName:(NSString *)name{
_name = name;
}

  • 在ViewController我们新建一个person2,代码变成了:
#import "ViewController.h"
#import "TCPerson.h"
@interface ViewController ()
@property (nonatomic, strong) TCPerson *person1;
@property (nonatomic, strong) TCPerson *person2;
@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[TCPerson alloc]init];
self.person1.name = @"liu yi fei";
[self.person1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];


self.person2 = [[TCPerson alloc] init];
self.person2.name = @"yyyyyyyy";
}

/// 点击屏幕出发改变self.person1的name
/// @param touches touches description
/// @param event event description
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.person1.name = @"cang lao shi";
// [self.person1 setName:@"cang lao shi"];

self.person2.name = @"ttttttttt";
}

/// 监听回调
/// @param keyPath 监听的属性名字
/// @param object 被监听的对象
/// @param change 改变的新/旧值
/// @param context context description
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"监听到%@对象的%@发生了改变%@",object,keyPath,change);
}

/// 移除观察者
- (void)dealloc{
[self.person1 removeObserver:self forKeyPath:@"name"];
}
@end

  • 注意:当我们点击屏幕的时候输出的结果是:

2020-09-24 16:10:36.750153+0800 KVO_TC[9313:105906] 监听到<TCPerson: 0x600002ce8230>对象的name发生了改变{
kind = 1;
new = "cang lao shi";
old = "liu yi fei";
}

  • 既然我们改变name的值的时候走的都是setName:setter方法,按理说观察属性变化的时候,person2的值也应该被观察到,为什么它不会观察到person2?



三.KVO的isa指向

  • 上篇文章中我分析了实例对象,类对象,元类对象的isa,既然当我们改变属性值的时候,其本质是调用setter方法,那么在KVO中,person1和person2的setName方法应该存储在类对象中,我们先来看看这两个实例对象的isa指向:
    打开lldb

(lldb) p self.person1.isa
(Class) $0 = NSKVONotifying_TCPerson
Fix-it applied, fixed expression was:
self.person1->isa
(lldb) p self.person2.isa
(Class) $1 = TCPerson
Fix-it applied, fixed expression was:
self.person2->isa
(lldb)
  • 从上面的打印我们看到 self.person1的isa指向了NSKVONotifying_TCPerson,而没有添加观察着的self.person2的isa却指向的是TCPerson
  • NSKVONotifying_TCPerson是runtime动态创建的类,继承自TCPerson,其内部实现可以看成(模拟的NSKVONotifying_TCPerson流程,下面代码不能在xcode中运行):

  • #import "NSKVONotifying_TCPerson.h"

    @implementation NSKVONotifying_TCPerson
    //NSKVONotifying_TCPerson的set方法实现,其本质来自于foundation框架
    - (void)setName:(NSString *)name{
    _NSSetIntVaueAndNotify();
    }
    //改变过程
    void _NSSetIntVaueAndNotify(){
    [self willChangeValueForKey:@"name"];
    [super setName:name];
    [self didChangeValueForKey:@"name"];
    }
    //通知观察者
    - (void)didChangeValueForKey:(NSString *key){
    [observe observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context];
    }
    @end
    未添加观察self.person2实例对象的isa指向流程图:



    添加观察self.person1实例对象的isa指向流程图:




    所以KVO其本质是动态生成一个NSKVONotifying_TCPerson
    类,继承自TCPerson,当实例对象添加观察着之后,实例对象的isa指向了这个动态创建的类,当其属性发生改变时,调用的是该类的setter方法,而不是父类的类对象中的setter方法



    作者:枫紫_6174
    链接:https://www.jianshu.com/p/0b6083b91b04
    来源:简书
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。







    收起阅读 »

    View系列:事件分发(一)

    基础相关View坐标系MotionEvent当用户触摸屏幕的时候,产生Touch事件,事件的相关细节(发生触摸的位置、时间等)被封装成MotionEvent对象事件类型具体动作MotionEvent.ACTION_DOWN按下View(所有事件的开始)Moti...
    继续阅读 »

    基础相关

    View坐标系

    View坐标系

    MotionEvent

    当用户触摸屏幕的时候,产生Touch事件,事件的相关细节(发生触摸的位置、时间等)被封装成MotionEvent对象

    image-20210531100221285

    image-20210531100250617

    事件类型具体动作
    MotionEvent.ACTION_DOWN按下View(所有事件的开始)
    MotionEvent.ACTION_MOVE滑动View
    MotionEvent.ACTION_UP抬起View(与DOWN对应)
    MotionEvent.ACTION_CANCEL结束事件
    MotionEvent.ACTION_OUTSIDE事件发生在视图范围外

    辅助类

    辅助类-dev

    View触摸相关工具类全解

    ViewConfiguration

    获取 Android 系统常用的距离、速度、时间等常量

    VelocityTracker

    跟踪触摸事件的速度。此设置对于手势标准中包含速度的手势(例如滑动)非常有用。

    GestureDetector

    手势检测,该类支持的一些手势包括 onDown()、onLongPress()、onFling() 等。可以将 GestureDetector 与onTouchEvent() 方法结合使用。

    OverScroller

    回弹工具类,不同的回弹效果可以自定义不同的动画插值器

    TouchDelegate

    扩展子视图的可轻触区域

    img

    view1.post(new Runnable() {
    @Override
    public void run() {
    Rect bounds = new Rect();
    // 获取View2占据的矩形区域在其父View(也就是View1)中的相对坐标
    view2.getHitRect(bounds);
    // 计算扩展后的矩形区域Bounds相对于View1的坐标
    bounds.left -= 100;
    bounds.top -= 50;
    bounds.right += 100;
    bounds.bottom += 50;
    TouchDelegate touchDelegate = new TouchDelegate(bounds, view2);
    // 为View1设置TouchDelegate
    view1.setTouchDelegate(touchDelegate);
    }
    });

    事件处理

    image-20210531100411928

    • 每一个DOWN / MOVE / UP / CANCLE都是一个事件,并不是连起来才是一个事件
    • 事件的消费,是看返回true/false,而不是看有没有处理操作
    • Activity、ViewGroup、View
      • 都有分发、消费事件的能力
      • 只有ViewGroup有拦截事件的能力

    事件分发

    window中的View是树形结构,可能会重叠在一起,当我们点击的区域有多个View都可以响应的时候,事件分发机制决定了这个点击事件应该给谁处理。

    分发机制类似洋葱模型、责任链模式、冒泡...

    分发:Activity -> PhoneWindow -> DecorView -> ViewGroup ->  @1 -> ... -> View
    消费:Activity <- PhoneWindow <- DecorView <- ViewGroup <- @1 <- ... <- View
    • 如果事件被消费,就意味着事件信息传递终止 如果在@1处消费事件,就不在往下传递了,直接返回
    • 如果事件一直没有被消费,最后会传给Activity,如果Activity也不需要就被抛弃事

    image

    View

    优先级:

    1. OnTouchListener.onTouch
    2. onTouchEven

    注意:OnTouchListener.onTouch返回false,并不代表该View不消费事件了,得看dispatchTouchEvent返回的结果

    public boolean dispatchTouchEvent(MotionEvent event) {
    ...
    // 被遮盖,不响应事件
    if (onFilterTouchEventForSecurity(event)) {
    ...
    //setOnTouchListener设置的监听,优先级高
    ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnTouchListener != null
    && (mViewFlags & ENABLED_MASK) == ENABLED
    && li.mOnTouchListener.onTouch(this, event)) {
    result = true;
    }

    // 系统已实现好的,优先级低。
    if (!result && onTouchEvent(event)) {
    result = true;
    }
    }
    ...
    return result;
    }

    onTouchEvent:

    • View即使设置了setEnable(false),只要是可点击状态就会消费事件,只是不做出回应
    • 只要进入CLICKABLE判断,就返回true消费时间
    事件处理
    DOWN发送LongClick延迟消息,过期触发
    MOVE移除LongClick消息
    CANCLE移除LongClick消息
    UP移除LongClick消息
    触发Click事件
    <!--只关注事件的分发,不关注其它状态的变化-->
    public boolean onTouchEvent(MotionEvent event) {
    final float x = event.getX();
    final float y = event.getY();
    final int action = event.getAction();

    //View被禁用的话,如果是可以点击的,一样返回true,表示消费了事件。只是不作出回应。
    if ((viewFlags & ENABLED_MASK) == DISABLED) {
    return (((viewFlags & CLICKABLE) == CLICKABLE
    || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
    || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
    }

    // 委托:扩大点击事件、委托其它处理
    if (mTouchDelegate != null) {
    if (mTouchDelegate.onTouchEvent(event)) {
    return true;
    }
    }

    /**
    * 只要进入该if,就返回true,消费事件
    */

    if (((viewFlags & CLICKABLE) == CLICKABLE ||
    (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
    (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
    switch (action) {
    case MotionEvent.ACTION_DOWN:
    if (isInScrollingContainer) {
    } else {
    //长按事件,发送延时消息到队列
    checkForLongClick(0, x, y);
    }
    break;
    case MotionEvent.ACTION_MOVE:
    if (!pointInView(x, y, mTouchSlop)) {
    if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
    //移除长按事件的消息。
    removeLongPressCallback();
    setPressed(false);
    }
    }
    break;
    case MotionEvent.ACTION_UP:
    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
    if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
    // 移除长按事件的消息
    removeLongPressCallback();

    //点击事件: 可知onclick事件是在UP的时候触发
    if (!focusTaken) {
    if (!post(mPerformClick)) {
    performClick();
    }
    }
    }
    }
    break;
    case MotionEvent.ACTION_CANCEL:
    //移除长按事件
    removeLongPressCallback();
    mHasPerformedLongPress = false;
    break;
    }
    return true;
    }

    return false;
    }

    ViewGroup

    1. DOWN事件:
      • 清除之前状态,mFirstTouchTarget = null
      • 进入逻辑1、2寻找接收事件的子View
        • mFirstTouchTarget = null,进入逻辑3
        • mFirstTouchTarget != null, 进入逻辑4
    2. MOVE/UP事件:
      • mFirstTouchTarget = null,注释1处不满足逻辑1判断条件,进入逻辑3
      • mFirstTouchTarget != null,不满足逻辑2判断条件,进入逻辑4
    3. CANCLE事件:
      • mFirstTouchTarget = null,注释2处不满足逻辑1判断条件,进入逻辑3
      • mFirstTouchTarget != null,注释2处不满足逻辑1判断条件,进入逻辑4

    总结,

    • DOWN事件就是用来清理状态、寻找新接收事件子View的

    • DOWN事件的后续事件:

      • 未找到子View接收情况下,直接自己处理
      • 找到子View接收的情况下,直接给子View
        @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
    ....
    // 如果该View被遮蔽,并且在被遮蔽时不响应点击事件,则不分发该触摸事件,即返回false。
    if (onFilterTouchEventForSecurity(ev)) {
    final int action = ev.getAction();
    final int actionMasked = action & MotionEvent.ACTION_MASK;

    /**
    * step1:DOWN事件的时候,表示最初开始事件,清除之前的状态。
    */

    if (actionMasked == MotionEvent.ACTION_DOWN) {
    // 关键:每次DOWN的时候,清除前一个手势的mFirstTouchTarget = null
    cancelAndClearTouchTargets(ev);
    // 清除状态
    resetTouchState();
    }


    /**
    * step2:拦截判断
    */

    final boolean intercepted;
    // ACTION_DOWN(初始状态)或 有子View处理事件:判断是否拦截
    if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
    //默认没有该标记位,返回false
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    if (!disallowIntercept) {//requestDisallowInterceptTouchEvent(false)
    //默认返回false,并不是每次都会调用
    intercepted = onInterceptTouchEvent(ev);
    ev.setAction(action);
    } else {//requestDisallowInterceptTouchEvent(true)
    intercepted = false;
    }
    } else {
    //[注释1],没有子View接收事件,拦截
    intercepted = true;
    }


    /**
    * step3:找能接收事件的子View,并赋值给mFirstTouchTarget
    */

    final boolean canceled = resetCancelNextUpFlag(this)
    || actionMasked == MotionEvent.ACTION_CANCEL; //[注释2]
    // *****每次都会初始化这两个变量****
    TouchTarget newTouchTarget = null;
    boolean alreadyDispatchedToNewTouchTarget = false;
    //如果在这一层不满足判断条件,直接就到[逻辑3,4]了。
    //[逻辑1]


    /*
    step4:到这,已经跳出了上面的大嵌套判断!--上面的大嵌套就是用来找接收事件的子View的。
    一旦确定找到了或者没有接收者,后面的事件:
    1. 检查intercepte状态。
    2. 进入下面的逻辑,后面的事件直接确定分发给谁
    */

    // 没有找到接收事件的View,以后的move/up也通过这一步给ViewGroup
    [逻辑3] if (mFirstTouchTarget == null) {
    //没有接收事件的子View,调用自己的dispatchTouchEvent
    handled = dispatchTransformedTouchEvent(ev, canceled, null,
    TouchTarget.ALL_POINTER_IDS);
    [逻辑4] } else {//找到了接收事件的View
    TouchTarget predecessor = null;
    TouchTarget target = mFirstTouchTarget;
    while (target != null) {
    final TouchTarget next = target.next;
    // 在DOWN找到接受事件的子View时,赋值alreadyDispatchedToNewTouchTarget = true
    // 此时已经消费了事件,所以直接返回true
    // 后面的其它事件中,alreadyDispatchedToNewTouchTarget被重置,不在满足该条件
    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
    handled = true;
    } else {
    // 判断是否 收到CANCEL事件 或 需要拦截事件
    final boolean cancelChild = resetCancelNextUpFlag(target.child)
    || intercepted;
    // 子View消费事件
    //如果cancelChild为true,给子View发送cancle事件
    [逻辑5] if (dispatchTransformedTouchEvent(ev, cancelChild,
    get.child, target.pointerIdBits)) {
    handled = true;
    }
    // 修改mFirstTouchTarget,使原来的子View不再接收事件
    if (cancelChild) {
    if (predecessor == null) {
    mFirstTouchTarget = next;
    } else {
    predecessor.next = next;
    }
    target.recycle();
    target = next;
    continue;
    }
    }
    }
    }
    }
    return handled;
    }

    Activity

    Touch事件先是传递到Activity,接着由Activity传递到最外层布局,然后一层层遍历循环到View

        public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
    // 交互 空实现
    onUserInteraction();
    }
    // DecorView实际是ViewGroup的dispatchTouchEvent方法
    if (getWindow().superDispatchTouchEvent(ev)) {
    return true;
    }
    // down点击到外部区域,消费事件,finish
    return onTouchEvent(ev);
    }

    onUserInteraction()

    这是一个空实现,用的也比较少,不深究: 此方法是activity的方法,当此activity在栈顶时,触屏点击按home,back,menu键等都会触发此方法。下拉statubar、旋转屏幕、锁屏不会触发此方法。所以它会用在屏保应用上,因为当你触屏机器 就会立马触发一个事件,而这个事件又不太明确是什么,正好屏保满足此需求;或者对于一个Activity,控制多长时间没有用户点响应的时候,自己消失等。

    onTouchEvent(event)

        public boolean onTouchEvent(MotionEvent event) {
    if (mWindow.shouldCloseOnTouch(this, event)) {
    finish();
    return true;
    }
    return false;
    }

    mWindow即使PhoneWindow,该方法是@hide,并且在Window类中定义。

        /** @hide */
    public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
    if (mCloseOnTouchOutside && event.getAction() == MotionEvent.ACTION_DOWN
    && isOutOfBounds(context, event) && peekDecorView() != null) {
    return true;
    }
    return false;
    }
    • mCloseOnTouchOutside是一个boolean变量,它是由Window的android:windowCloseOnTouchOutside属性值决定。
    • isOutOfBounds(context, event)是判断该event的坐标是否在context(对于本文来说就是当前的Activity)之外。是的话,返回true;否则,返回false。
    • peekDecorView()则是返回PhoneWindow的mDecor。

    总的来说:如果设置了android:windowCloseOnTouchOutside为true,并且是DOWN事件点击了Activity外部区域(比如Activity是一个Dialog),返回true,消费事件,并且finish。

    ACTION_CANCEL

    子View在接收事件过程中,被中断,父View会传给子View一个CANCEL事件

     [逻辑4]      } else {//找到了接收事件的View
    TouchTarget predecessor = null;
    TouchTarget target = mFirstTouchTarget;
    while (target != null) {
    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
    } else {
    // 判断是否 收到CANCEL事件 或 需要拦截事件
    final boolean cancelChild = resetCancelNextUpFlag(target.child)
    || intercepted; //注释1

    //如果cancelChild为true,给子View发送cancle事件
    [逻辑5] if (dispatchTransformedTouchEvent(ev, cancelChild,
    get.child, target.pointerIdBits)) {
    handled = true;
    }
    // 修改mFirstTouchTarget,使原来的子View不再接收事件
    if (cancelChild) {
    if (predecessor == null) {
    mFirstTouchTarget = next;
    } else {
    predecessor.next = next;
    }
    target.recycle();
    target = next;
    continue;
    }
    //...
    }
    }
    }


    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
    View child, int desiredPointerIdBits) {
    final boolean handled;
    final int oldAction = event.getAction();
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
    event.setAction(MotionEvent.ACTION_CANCEL);
    if (child == null) {
    handled = super.dispatchTouchEvent(event);
    } else {
    //发送CANCEL事件给子View
    handled = child.dispatchTouchEvent(event);
    }
    event.setAction(oldAction);
    return handled;
    }
    //...
    }

    ACTION_OUTSIDE

    设置了FLAG_WATCH_OUTSIDE_TOUCH,事件发生在当前视图的范围之外

    例如,点击音量键之外的区域取消音量键显示:

    //frameworks/base/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java        
    // 给音量键Window设置FLAG_WATCH_OUTSIDE_TOUCH
    mDialog = new CustomDialog(mContext);
    mWindow = mDialog.getWindow();
    mWindow.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
    | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
    | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
    | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
    | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH //设置Window Flag
    | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
    ......

    // 重写onTouchEvent并处理ACTION_OUTSIDE事件
    @Override
    public boolean onTouchEvent(MotionEvent event) {
    if (mShowing) {
    if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
    dismissH(Events.DISMISS_REASON_TOUCH_OUTSIDE);
    return true;
    }
    }
    return false;
    }

    事件拦截

    一文解决Android View滑动冲突

    只有ViewGroup有事件拦截的能力,View可根据情况申请父View进行拦截

    image-20210531100411928

    View

    View没有拦截事件的能力,只能根据不同需求调用mParent.requestDisallInterceptTouchEvent(true/false) 申请父View是否进行拦截。

    注意:如果在子View接收事件的过程中被父View拦截,父View会给子View一个CANCEL事件,注意处理相关逻辑。

    ViewGroup

    onInterceptTouchEvent
    • 设置了FLAG_DISALLOW_INTERCEPT标记时,不会调用
    • 其它时候都会调用
        /**
    * ViewGroup事件分发时的拦截检查机制
    */

    //默认没有该标记位,返回false
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;//注释1
    if (!disallowIntercept) {//requestDisallowInterceptTouchEvent(false)
    intercepted = onInterceptTouchEvent(ev);//默认返回false
    } else {
    intercepted = false;//requestDisallowInterceptTouchEvent(true)
    }


    /**
    * 默认返回false
    */

    public boolean onInterceptTouchEvent(MotionEvent ev) {
    if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
    && ev.getAction() == MotionEvent.ACTION_DOWN
    && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
    && isOnScrollbarThumb(ev.getX(), ev.getY())) {
    return true;
    }
    return false;
    }

    /*
    * disallowIntercept = true时,不允许拦截,注释1为true
    * disallowIntercept = false时,允许拦截,注释1为false
    */

    @Override
    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
    // We're already in this state, assume our ancestors are too
    if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
    return;
    }

    if (disallowIntercept) {
    mGroupFlags |= FLAG_DISALLOW_INTERCEPT;// 添加标记,使得注释1为true
    } else {
    mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;// 清除标记,使得注释1为false
    }

    if (mParent != null) {
    mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
    }
    }
    requestDisallowInterceptTouchEvent
    • true,不允许拦截,注释1为true,不会调用onInterceptTouchEvent
    • false,允许拦截,注释1为false(默认),调用onInterceptTouchEvent

    注意:调用requestDisallowInterceptTouchEvent(false)申请拦截,并不会真的就被父View拦截了。它只是一个标记,使得父View会检查onInterceptTouchEvent这个方法(默认也会调用)。 它只会影响 mGroupFlags & FLAG_DISALLOW_INTERCEPT值,真正决定要不要被拦截是看 onInterceptTouchEvent的返回值。如果为true:

    在注释1处cancelChild = true,会导致给子类发送CANCEL事件,然后修改mFirstTouchTarget,不再给子View传递事件。

    [逻辑4]      } else {//找到了接收事件的View
    TouchTarget predecessor = null;
    TouchTarget target = mFirstTouchTarget;
    while (target != null) {
    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
    } else {
    // 判断是否 收到CANCEL事件 或 需要拦截事件
    final boolean cancelChild = resetCancelNextUpFlag(target.child)
    || intercepted; //注释1
    // 子View消费事件
    //如果cancelChild为true,给子View发送cancle事件
    [逻辑5] if (dispatchTransformedTouchEvent(ev, cancelChild,
    get.child, target.pointerIdBits)) {
    handled = true;
    }
    // 修改mFirstTouchTarget,使原来的子View不再接收事件
    if (cancelChild) {
    if (predecessor == null) {
    mFirstTouchTarget = next;
    } else {
    predecessor.next = next;
    }
    target.recycle();
    target = next;
    continue;
    }
    }
    }
    }

    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
    View child, int desiredPointerIdBits) {
    final boolean handled;
    final int oldAction = event.getAction();
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
    event.setAction(MotionEvent.ACTION_CANCEL);
    if (child == null) {
    handled = super.dispatchTouchEvent(event);
    } else {
    handled = child.dispatchTouchEvent(event);
    }
    event.setAction(oldAction);
    return handled;
    }
    //...
    }

    Activity

    Activity没有onInterceptTouchEvent方法,也没有mParent,不具备主动或被动拦截能力

    收起阅读 »

    View系列:动画

    View Animation(视图动画)最大的特点是:并没有改变目标实际的属性(宽高/位置等)。例如:移动后,点击原来的位置出发点击事件;移动后再旋转,还是回到原来的位置旋转。Tween Animation(补间动画)锚点可以是数值、百分数、百分数p三种样式,...
    继续阅读 »

    View Animation(视图动画)

    最大的特点是:并没有改变目标实际的属性(宽高/位置等)。例如:移动后,点击原来的位置出发点击事件;移动后再旋转,还是回到原来的位置旋转。

    Tween Animation(补间动画)

    锚点

    可以是数值、百分数、百分数p三种样式,比如50、50%、50%p。[不是只有pivotx/y才可以用这3中样式,其它变换的属性也可以]

    • 当为数值时,表示在当前View的左上角,即原点处加上50px,做为起始缩放点;
    • 如果是50%,表示在当前控件的左上角加上自己宽度的50%做为起始点;
    • 如果是50%p,那么就是表示在当前的左上角加上父控件宽度的50%做为起始点x轴坐标(是在目标的左上角原点加上相对于父控件宽度的距离,不是锚点在父控件的那个位置)。

    fromX/toX等等类型的数据也可以用上面的3中数据 类型,只不过有的不适合。比如scale用%p就没意义了。养成好习惯,只在锚点的属性上随便用这3中类型,from/to属性分清类型用相应的数值(浮点倍数/角度...)。

    从Animation继承的属性
    android:duration 动画持续时间,以毫秒为单位 
    android:fillAfter 如果设置为true,控件动画结束时,将保持动画最后时的状态
    android:fillBefore 如果设置为true,控件动画结束时,还原到开始动画前的状态
    android:fillEnabled 与android:fillBefore 效果相同,都是在动画结束时,将控件还原到初始化状态
    android:repeatCount 重复次数
    android:repeatMode 重复类型,有reverse和restart两个值,reverse表示倒序回放,restart表示重新放一遍,必须与repeatCount一起使用才能看到效果。因为这里的意义是重复的类型,即回放时的动作。
    android:interpolator 设定插值器,其实就是指定的动作效果,比如弹跳效果等,不在这小节中讲解,后面会单独列出一单讲解。
    scale
    <?xml version="1.0" encoding="utf-8"?>
    <scale xmlns:android="http://schemas.android.com/apk/res/android"
    android:interpolator="@android:anim/accelerate_decelerate_interpolator"
    android:duration="700"
    android:fromXScale="50%" //也可以用上面的3中类型
    android:fromYScale="50%"
    android:toXScale="200%"
    android:toYScale="200%"
    android:pivotX="0.5"
    android:pivotY="0.5"
    android:repeatCount = "2"
    android:repeatMode = "reverse"
    android:fillAfter = "true"
    />

    alpha
    <?xml version="1.0" encoding="utf-8"?>
    <alpha xmlns:android="http://schemas.android.com/apk/res/android"
    android:interpolator="@android:anim/accelerate_interpolator"
    android:fromAlpha="0.1"
    android:toAlpha="1"
    android:duration="1500"
    android:repeatMode = "reverse"
    android:repeatCount = "2"
    android:fillAfter = "true"
    >

    </alpha>
    rotate
    <?xml version="1.0" encoding="utf-8"?>
    <rotate xmlns:android="http://schemas.android.com/apk/res/android"
    android:interpolator="@android:anim/accelerate_interpolator"
    android:fromDegrees="0"
    android:toDegrees="270"
    android:pivotX="50%"
    android:pivotY="50%"
    android:duration="700"
    android:repeatMode = "reverse"
    android:repeatCount = "3"
    android:fillAfter = "true"
    >

    </rotate>
    translate
    <?xml version="1.0" encoding="utf-8"?>
    <translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:interpolator="@android:anim/accelerate_interpolator"
    android:duration="700"
    android:fillAfter="true"
    android:fromXDelta="50"
    android:fromYDelta="50%p"
    android:repeatCount="3"
    android:repeatMode="reverse"
    android:toXDelta="70%p"
    android:toYDelta="80%p">

    </translate>
    AnimationSet animSet = new AnimationSet(false);
    Animation scaleAnim = AnimationUtils.loadAnimation(this, R.anim.scale_anim); //资源文件
    Animation rotateAnim = AnimationUtils.loadAnimation(this, R.anim.rotate_anim);
    AlphaAnimation alphaAnim = new AlphaAnimation(0.2f, 1.0f); //代码生成
    //valueType 3中类型的数据(px, 自身%, 父类%p),这里已自身为参照物。
    TranslateAnimation traslateAnim = new TranslateAnimation(
    Animation.RELATIVE_TO_SELF, 0.2f,
    Animation.RELATIVE_TO_SELF, 3.0f,
    Animation.RELATIVE_TO_SELF, 0f,
    Animation.RELATIVE_TO_SELF, 1.0f);
    ivTarget.startAnimation(animSet);
    自定义Animation
    private class MoveAnimation extends Animation {
    @Override
    protected void applyTransformation(float interpolatedTime, Transformation t) {
    super.applyTransformation(interpolatedTime, t);
    mInterpolatedTime = interpolatedTime;
    invalidate();
    }
    }

    Frame Animation(逐帧动画)

    <?xml version="1.0" encoding="utf-8"?>
    <animation-list xmlns:android="http://schemas.android.com/apk/res/android"
    android:oneshot="true">
    //false 一直重复执行,true执行一次。
    <item
    android:duration="200"
    android:drawable="@drawable/frame_anim_1"/>

    <item
    android:duration="200"
    android:drawable="@drawable/frame_anim_2"/>

    <item
    android:duration="200"
    android:drawable="@drawable/frame_anim_3"/>

    <item
    android:duration="200"
    android:drawable="@drawable/frame_anim_4"/>

    <item
    android:duration="200"
    android:drawable="@drawable/frame_anim_4"/>

    </animation-list>
    • 需要注意的是,动画的启动需要在view和window建立连接后才可以绘制,比如上面代码是在用户触摸后启动。如果我们需要打开界面就启动动画的话,则可以在Activity的onWindowFocusChanged()方法中启动。

    Property Animation(属性动画)

    属性动画是指通过改变View属性来实现动画效果,包括:ValueAnimator、ObjectAnimator、TimeAnimator

    ValueAnimator

    该类主要针对数值进行改变,不对View进行操作

    ValueAnimator animator = ValueAnimator.ofInt(0,400);  
    animator.setDuration(1000);
    animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
    //拿到监听结果,自己处理。
    int curValue = (int)animation.getAnimatedValue();
    tvTextView.layout(curValue,curValue,curValue+tv.getWidth(),curValue+tv.getHeight());
    }
    });
    animator.setInterpolator(new LinearInterpolator());
    animator.start();

    监听:

    /**
    * 监听器一:监听动画变化时的实时值
    * 添加方法为:public void addUpdateListener(AnimatorUpdateListener listener)
    */

    public static interface AnimatorUpdateListener {
    void onAnimationUpdate(ValueAnimator animation);
    }
    /**
    * 监听器二:监听动画变化时四个状态
    * 添加方法为: public void addListener(AnimatorListener listener)
    */

    public static interface AnimatorListener {
    void onAnimationStart(Animator animation);
    void onAnimationEnd(Animator animation);
    void onAnimationCancel(Animator animation);
    void onAnimationRepeat(Animator animation);
    }


    /**
    * 移除AnimatorUpdateListener
    */

    void removeUpdateListener(AnimatorUpdateListener listener);
    void removeAllUpdateListeners();
    /**
    * 移除AnimatorListener
    */

    void removeListener(AnimatorListener listener);
    void removeAllListeners();

    ObjectAnimator

    ValueAnimator只能对数值进行计算,不能直接操作View,需要我们在监听器中自己去操作控件。这样就有点麻烦了,于是Google在ValueAmimator的基础上又派生出了ObjerctAnimator类,让动画直接与控件关联起来。

     	ObjectAnimator rotateObject = ObjectAnimator.ofFloat(tvPropertyTarget, 
    "Rotation",
    0, 20, -20, 40, -40, 0);
    rotateObject.setDuration(2000);
    rotateObject.start();
    setter/getter 属性名

    在View中已经实现了一些属性的setter/getter方法,在构造动画时可以直接对控件使用。

    • 要使用一个属性,必须在控件中有对应的setter/getter方法,属性setter/getter方法的命名必须以驼峰方式
    • ObjectAnimator在使用该属性的时候,会把setter/getter和属性第一个字母大写转换后的字段拼接成方法名,通过反射的方式调用该方法传值。 所以,上文中"Rotation/rotation"可以首字母可以大小写都行
    //1、透明度:alpha  
    public void setAlpha(float alpha)

    //2、旋转度数:rotation、rotationX、rotationY
    public void setRotation(float rotation) //围绕Z轴旋转
    public void setRotationX(float rotationX)
    public void setRotationY(float rotationY)

    //3、平移:translationX、translationY
    public void setTranslationX(float translationX)
    public void setTranslationY(float translationY)

    //缩放:scaleX、scaleY
    public void setScaleX(float scaleX)
    public void setScaleY(float scaleY)

    image-20210603130556023

    自定义属性做动画
    public class PointView extends View {
    private float mRadius = 0;
    public PointView(Context context, @Nullable AttributeSet attrs) {
    super(context, attrs);
    }
    @Override
    protected void onDraw(Canvas canvas) {
    }

    public void setRadius(float radius){
    this.mRadius = radius;
    invalidate();
    }

    public float getRadius(){
    return mRadius;
    }
    }

    //radius属性首字母大小写无所谓,最后都是要转成大些的。
    ObjectAnimator pointAnim = ObjectAnimator.ofFloat(pointPropertyAnim,
    "Radius",
    10, 40, 40, 80, 60, 100, 80, 120,60);
    pointAnim.start();

    什么时候需要用到get方法呢? 前面构造动画时传入的取值范围都是多个参数,Animator知道是从哪个值变化到哪个值。当只传入一个参数的时候,Animator怎么知道哪里是起点?这时通过get方法找到初始值。 如果没有找到get方法,会用该参数类型的默认初始值复制。如:ofInt方法传入一个值,找不到get方法时,默认给的初始值是Int类型的初始值0.

    原理

    image-20210603131108900ObjectAnimator的方便之处在于:

    ValueAnimator只负责把数值给监听器,ObjectAnimator只负责调用set方法。至于实现,都是靠我们自己或者set中的方法。

    插值器

    设置动画运行过程中的进度比例,类似匀速变化、加速变化、回弹等

    • 参数input:是一个float类型,它取值范围是0到1,表示当前动画的进度,取0时表示动画刚开始,取1时表示动画结束,取0.5时表示动画中间的位置,其它类推。
    • 返回值:表示当前实际想要显示的进度。取值可以超过1也可以小于0,超过1表示已经超过目标值,小于0表示小于开始位置。(给估值器使用
    • 插值器默认每10ms刷新一次
    public class PointInterpolator implements Interpolator {
    /**
    * input 是实际动画执行的时间比例 0~1
    * newInput 你想让动画已经执行的比例 0~1。
    * 注意:都是比例,而不是实际的值。
    *
    * setDuration(1000)情况下:前200ms走了3/4的路程比例,后800ms走了1/4的路程比例。
    */

    @Override
    public float getInterpolation(float input) {
    if (input <= 0.2) {//后1/4的时间,输出3/4的比例
    float newInput = input*4;
    return newInput;
    }else {//后3/4的时间,输出1/4的比例
    float newInput = (float) (input - 0.2)/4 + 0.8f;
    return newInput;
    }
    }
    }

    使用方式和默认插值器

    在xml和代码中使用插值器,省略代码中使用方式

    <?xml version="1.0" encoding="utf-8"?>
    <scale xmlns:android="http://schemas.android.com/apk/res/android"
    // 通过资源ID设置插值器
    android:interpolator="@android:anim/overshoot_interpolator"
    android:duration="3000"
    android:fromXScale="0.0"
    android:fromYScale="0.0"
    android:pivotX="50%"
    android:pivotY="50%"
    android:toXScale="2"
    android:toYScale="2" />

    内置插值器动画展示

    Android动画之Interpolator

    Android动画插值器

    作用资源ID对应的Java类
    动画加速进行@android:anim/accelerte_interpolatorAcceleraterplator
    快速完成动画,超出再回到到结束样式@android:anim/overshoot_interpolatorOvershootInterpolator
    先加速再减速@android:anim/accelerate_decelerate_interpolatorAccelerateDecelerateInterpolator
    先退后再加速前进@android:anim/anticipate_interpolatorAnticipateInterpolator
    先退后再加速前进,超出终点后再回终点@android:anim/anticipate_overshoot_interpolatorAnticipateOvershootInterpolator
    最后阶段弹球效果@android:anim/bounce_interpolatorBounceInterpolator
    周期运动@android:anim/cycle_interpolatorCycleInterpolator
    减速@android:anim/decelerate_interpolatorDecelerateInterpolator
    匀速@android:anim/linear_interpolatorLinearInterpolator

    估值器

    设置 属性值 从初始值过渡到结束值 的变化具体数值

    • 参数fraction: 表示当前动画的进度(插值器返回值
    • 返回值:表示当前对应类型的取值,也就是UpdateListener接口方法中传入的值
    public class PointEvaluator implements TypeEvaluator<Point> {
    @Override
    public Point evaluate(float fraction, Point startValue, Point endValue) {
    int radius = (int) (startValue.getRadius() +
    fraction*(endValue.getRadius() - startValue.getRadius()));
    return new Point(radius);
    }
    }

    自定义插值器、估值器、属性的使用:

    public void doAnimation(){
    //ObjectAnimator animator = ObjectAnimator.ofInt(mView, "Radius", 20, 80);
    ValueAnimator animatior = new ValueAnimator();
    animatior.setObjectValues(new Point(20), new Point(80));
    animatior.setInterpolator(new PointInterpolator());
    animatior.setEvaluator(new PointEvaluator());

    animatior.setDuration(2000);
    animatior.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
    mPoint = (Point) animation.getAnimatedValue();
    invalidate();
    }
    });
    animatior.start();
    }

    PropertyValuesHolder

    它其中保存了动画过程中所需要操作的属性和对应的值

    通过ObjectAnimator.ofFloat(Object target, String propertyName, float… values)构造的动画,ofFloat()的内部实现其实就是将传进来的参数封装成PropertyValuesHolder实例来保存动画状态,后期的各种操作也是以PropertyValuesHolder为主。

    //将需要操作的多个属性和值封装起来,一起放到ObjectAnimator中,相当于set操作。
    PropertyValuesHolder rotateHolder = PropertyValuesHolder.ofFloat("Rotation", 0, 360, 0);
    PropertyValuesHolder scaleXHolder = PropertyValuesHolder.ofFloat("scaleX", 1, 2, 1,2,1);
    PropertyValuesHolder scaleYHolder = PropertyValuesHolder.ofFloat("scaleY", 1, 2, 1,2,1);
    ObjectAnimator objectAnim = ObjectAnimator.ofPropertyValuesHolder(ivHolderTarget,
    rotateHolder,
    scaleXHolder,
    scaleYHolder);
    objectAnim.setDuration(2000);
    objectAnim.setInterpolator(new LinearInterpolator());
    objectAnim.start();

    KeyFrame(主要帧)

    如果想要更精确的控制动画,想要控制整个动画过程的某个点或某个时段达到的值,可以通过自定义插值器或估值器来实现,但是那样又有些费事,并且不容易计算这段时间内值的变化。 这时可以用Keyframe来实现,即设置好某个时间点和值,系统会自动计算该点和上个点之间,值的变化。

    /***
    * 实现左右摇晃,每边最后有震动的效果。
    * 摇晃角度100度:0.2f/0.2~0.4/0.4~0.5,分别设置不同的角度和加速器。
    * 每个比例点达到哪个角度,这在估值器中也能做到,但是需要自己算每个时间段内值的变化过程。
    * KeyFrame可以设置好 比例-值 以后,系统根据默认或设置的加速器改变:上个点和该点内的值如何变换。
    * 这样可以更精确的控制动画过程,同时也不用自己费劲去计算值因该如何变换。
    */

    Keyframe kfRotation1 = Keyframe.ofFloat(0, 0); //第一帧,如果没有该帧,会直接跳到第二帧开始动画。
    //第二帧 0.2f时达到60度,线性加速应该作用于从0~0.2f的这段时间,而不是作用在0.2~0.4f这段。因为已经定好60度是要的结果了,那么实现就应该在前面这段。
    Keyframe kfRotation2 = Keyframe.ofFloat(0.2f, 60);
    kfRotation2.setInterpolator(new LinearInterpolator());
    Keyframe kfRotation3 = Keyframe.ofFloat(0.4f, 100);
    kfRotation3.setInterpolator(new BounceInterpolator());
    Keyframe kfRotation4 = Keyframe.ofFloat(0.5f, 0);
    kfRotation4.setInterpolator(new LinearInterpolator()); //最少有2帧
    Keyframe kfRotation5 = Keyframe.ofFloat(0.7f, -60);
    kfRotation5.setInterpolator(new LinearInterpolator());
    Keyframe kfRotation6 = Keyframe.ofFloat(0.9f, -100);
    kfRotation6.setInterpolator(new BounceInterpolator());
    Keyframe kfRotation7 = Keyframe.ofFloat(1f, 0);//最后一帧,如果没有该帧,会以最后一个KeyFrame做结尾
    kfRotation7.setInterpolator(new LinearInterpolator());

    Keyframe kfScaleX1 = Keyframe.ofFloat(0, 1);
    Keyframe kfScaleX2 = Keyframe.ofFloat(0.01f,2.8f);
    Keyframe kfScaleX3 = Keyframe.ofFloat(0.8f,2.0f);
    Keyframe kfScaleX4 = Keyframe.ofFloat(1f,1.0f);

    Keyframe kfScaleY1 = Keyframe.ofFloat(0, 1);
    Keyframe kfScaleY2 = Keyframe.ofFloat(0.01f,2.8f);
    Keyframe kfScaleY4 = Keyframe.ofFloat(0.8f,2.0f);
    Keyframe kfScaleY5 = Keyframe.ofFloat(1f,1.0f);

    PropertyValuesHolder rotationHolder = PropertyValuesHolder.ofKeyframe("rotation", kfRotation1, kfRotation2, kfRotation3,kfRotation4, kfRotation5, kfRotation6, kfRotation7);
    PropertyValuesHolder scaleXHolder = PropertyValuesHolder.ofKeyframe("scaleX", kfScaleX1, kfScaleX2, kfScaleX3, kfScaleX4);
    PropertyValuesHolder scaleYHolder = PropertyValuesHolder.ofKeyframe("scaleY", kfScaleY1, kfScaleY2, kfScaleY4, kfScaleY5);


    ObjectAnimator objectAnim = ObjectAnimator.ofPropertyValuesHolder(ivHolderTarget,
    rotationHolder,
    scaleXHolder,
    scaleYHolder);
    objectAnim.setDuration(1500);

    AnimatorSet

    AnimatorSet针对ValueAnimator和ObjectAnimator都是适用的,但一般而言,我们不会用到ValueAnimator的组合动画。

    playTogether/playSequentially

    无论是playTogether还是playSequentially方法,它们只是,仅仅是激活了动画什么时候开始,并不参与动画的具体操作。 例如:如果是playTogether,它只负责这个动画什么时候一起激活,至于anim1/anim2/anim3...哪个马上开始,哪个有延迟,哪个会无限重复,set都不管,只负责一起激活。 如果是playSequentially,它只负责什么时候开始激活第一个(因为有可能set设置延迟),并在第一个动画结束的时候,激活第二个,以此类推。

    ObjectAnimator anim1 = ObjectAnimator.ofInt(mTv1, "BackgroundColor",  0xffff00ff, 0xffffff00, 0xffff00ff);

    ObjectAnimator anim2 = ObjectAnimator.ofFloat(mTv1, "translationY", 0, 400, 0);
    anima2.setStartDelay(2000);
    anima2.setRepeatCount(ValueAnimator.INFINITE);

    ObjectAnimator anim3 = ObjectAnimator.ofFloat(mTv2, "translationY", 0, 400, 0);
    anim3.setStartDelay(2000);

    AnimatorSet animatorSet = new AnimatorSet();
    animatorSet.playTogether(anim1, anim2, anim3);//playSequentially(按次序播放)
    animatorSet.setDuration(2000);
    animatorSet.setStartDelay(2000);
    animatorSet.start();
    play(x).with(x)
    • play(anim1).with(anim2):2000ms后set开始激活动画,anim1启动,再过2000ms后anim2启动。
    • play(anim2).with(anim1):2000ms后set开始激活动画,再过2000ms后启动anim2,并且启动anim1.
    set监听

    addListener监听的是AnimatorSet的start/end/cacle/repeat。不会监听anim1/anim2的动画状态的。

    联合动画XML实现
    单独设置和Set中设置
    • 以set为准:
    //设置单次动画时长
    public AnimatorSet setDuration(long duration);
    //设置加速器
    public void setInterpolator(TimeInterpolator interpolator)
    //设置ObjectAnimator动画目标控件
    public void setTarget(Object target)
    ObjectAnimator anim1 = ObjectAnimator.ofFloat(mTv1, "translationY", 0, 400, 0);
    anim1.setDuration(500000000);

    ObjectAnimator anim2 = ObjectAnimator.ofFloat(mTv2, "translationY", 0, 400, 0);
    anim2.setDuration(3000);//每次3000,而不是3次3000ms
    anim2.setRepeatCount(3);

    AnimatorSet animatorSet = new AnimatorSet();
    animatorSet.play(tv2TranslateY).with(tv1TranslateY);
    animatorSet.setDuration(2000);//以Set为准
    animatorSet.start();

    setDuration()是指单个动画的时间,并不是指总共做完这个动画过程的时间。比如:anim2中设置了3000ms,重复3次。是指每次3000ms,不是3次3000ms。
    另外animatorSet设置了时间以后,anim1/anim2虽然也设置了,但是这时以set为准。即,anim1/anim2的单个动画时间为2000ms。只不过anim2是每次2000ms,重复3次,共6000ms。

    • 不以set为准:setStartDelay
    ObjectAnimator anim1 = ObjectAnimator.ofFloat(mTv1, "translationY", 0, 400, 0);
    ObjectAnimator anim2 = ObjectAnimator.ofFloat(mTv2, "translationY", 0, 400, 0);
    anim2.setStartDelay(2000);

    AnimatorSet animatorSet = new AnimatorSet();
    animatorSet.addListener(new Animator.AnimatorListener(){...});
    animatorSet.play(anim1).with(anim2);
    animatorSet.setStartDelay(3000);//指的是Set的激活延迟,而不是动画延迟
    animatorSet.setDuration(2000);
    animatorSet.start();

    setStartDelay不会覆盖单个动画的该方法,只会延长set的激活时间。所以,上面代码中动画的启动过程是:3000ms后set开始激活动画,anim1启动,再过2000ms后anim2启动。

    ViewPropertyAnimator

    属性动画已不再是针对于View而进行设计的了,而是一种对数值不断操作的过程,我们将属性动画对数值的操作过程设置到指定对象的属性上来,从而形成一种动画的效果。 虽然属性动画给我们提供了ValueAnimator类和ObjectAnimator类,在正常情况下,基本都能满足我们对动画操作的需求,但ValueAnimator类和ObjectAnimator类本身并不是针对View对象的而设计的,而我们在大多数情况下主要都还是对View进行动画操作的。

    因此Google官方在Android 3.1系统中补充了ViewPropertyAnimator类,这个类便是专门为View动画而设计的。

    • 专门针对View对象动画而操作的类
    • 更简洁的链式调用设置多个属性动画,这些动画可以同时进行
    • 拥有更好的性能,多个属性动画是一次同时变化,只执行一次UI刷新(也就是只调用一次invalidate,而n个ObjectAnimator就会进行n次属性变化,就有n次invalidate)
    • 每个属性提供两种类型方法设置。scaleX()/scaleXBy()
    • 该类只能通过View的animate()获取其实例对象的引用
    • 自动调用start
    btn.animate()
    .alpha(0.5f)
    .rotation(360)
    .scaleX(1.5f).scaleY(1.5f)
    .translationX(50).translationY(50)
    .setDuration(5000);

    image-20210604100429893

    layoutAnimation

    布局动画,api1,该属性只对创建ViewGroup时,对其子View有动画。已经创建过了该ViewGroup的话,再向其添加子View不会有动画。

    • onCreat创建加载布局时:
    //anim -> rotate_anim.xml
    <?xml version="1.0" encoding="utf-8"?>


    // layoutAnimation标签
    <?xml version="1.0" encoding="utf-8"?>
    <layoutAnimation
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:delay="1"
    android:animationOrder="normal"
    android:animation="@anim/rotate_anim">

    </layoutAnimation>

    //定义在LinearLayout上,在该界面生成时,Button显示动画。但是,后面在LinearLayout中添加Button时,不再有动画。
    <LinearLayout
    android:id="@+id/ll_tips_target_animation"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layoutAnimation="@anim/layout_animation"
    android:tag="在xml中设置的layoutAnimation"
    android:orientation="vertical">

    <Button
    style="@style/base_button"
    android:text="ViewGroup初始化时,子View有动画"/>

    </LinearLayout>
    • 代码中动态设置layoutAnimation,添加View
            //代码生成ViewGroup
    LinearLayout linear = new LinearLayout(this);

    Animation animation = AnimationUtils.loadAnimation(this, R.anim.rotate_anim);
    LayoutAnimationController controller = new LayoutAnimationController(animation);
    controller.setDelay(1);
    //动画模式,正常/倒叙/随机
    controller.setOrder(LayoutAnimationController.ORDER_NORMAL);
    //设置layoutAnimation
    linear.setLayoutAnimation(controller);
    linear.setLayoutAnimationListener(new Animation.AnimationListener() {

    });

    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
    LinearLayout.LayoutParams.MATCH_PARENT,
    LinearLayout.LayoutParams.WRAP_CONTENT);
    linear.setLayoutParams(params);
    //给该ViewGroup添加子View,子View会有动画。
    addVeiw(linear,null);
    llTargetAnim.addView(linear, 0);

    使用场景:

    该属性只有ViewGroup创建的时候才能有效果,所以不适合动态添加子View的操作显示动画。一般做界面显示的时候的入场动画,比如打开一个界面,多个固定不变的item有动画的显示出来。(进入设置界面,信息展示界面)。

    android:animateLayoutChanges属性:

    Api11后,添加/移除子View时所带的默认动画,在Xml中设置。不能自定义动画,只能使用默认的。所以,使用范围较小。

    <LinearLayout
    android:animateLayoutChanges="true"
    />

    image


    收起阅读 »

    「Java 路线」| 关于泛型能问的都在这里了(含Kotlin)

    前言 泛型(Generic Type) 无论在哪一门语言里,都是最难语法的存在,细节之繁杂、理解之困难,令人切齿; 在这个系列里,我将总结Java & Kotlin中泛型的知识点,带你从 语法 & 原理 全面理解泛型。追求简单易懂又...
    继续阅读 »

    前言



    • 泛型(Generic Type) 无论在哪一门语言里,都是最难语法的存在,细节之繁杂、理解之困难,令人切齿;

    • 在这个系列里,我将总结Java & Kotlin中泛型的知识点,带你从 语法 & 原理 全面理解泛型。追求简单易懂又不失深度,如果能帮上忙,请务必点赞加关注!

    • 首先,尝试回答这些面试中容易出现的问题,相信看完这篇文章,这些题目都难不倒你:


    1、下列代码中,编译出错的是:
    public class MyClass<T> {
    private T t0; // 0
    private static T t1; // 1
    private T func0(T t) { return t; } // 2
    private static T func1(T t) { return t; } // 3
    private static <T> T func2(T t) { return t; } // 4
    }
    2、泛型的存在是用来解决什么问题?
    3、请说明泛型的原理,什么是泛型擦除机制,具体是怎样实现的?



    目录





    1. 泛型基础




    • 问:什么是泛型,有什么作用?


    答:在定义类、接口和方法时,可以附带类型参数,使其变成泛型类、泛型接口和泛型方法。与非泛型代码相比,使用泛型有三大优点:更健壮(在编译时进行更强的类型检查)、更简洁(消除强转,编译后自动会增加强转)、更通用(代码可适用于多种类型)



    • 问:什么是类型擦除机制?


    答:泛型本质上是 Javac 编译器的一颗 语法糖,这是因为:泛型是 JDK1.5 中引进的新特性,为了 向下兼容,Java 虚拟机和 Class 文件并没有提供泛型的支持,而是让编译器擦除 Code 属性中所有的泛型信息,需要注意的是,泛型信息会保留在类常量池的属性中。



    • 问:类型擦除的具体步骤?


    答:类型擦除发生在编译时,具体分为以下 3 个步骤:



    • 1:擦除所有类型参数信息,如果类型参数是有界的,则将每个参数替换为其第一个边界;如果类型参数是无界的,则将其替换为 Object

    • 2:(必要时)插入类型转换,以保持类型安全

    • 3:(必要时)生成桥接方法以在子类中保留多态性


    举个例子:


    源码:
    public class Parent<T> {
    public void func(T t){
    }
    }

    public class Child<T extends Number> extends Parent<T> {
    public T get() {
    return null;
    }
    public void func(T t){
    }
    }

    void test(){
    Child<Integer> child = new Child<>();
    Integer i = child.get();
    }
    ---------------------------------------------------------
    字节码:
    public class Parent {
    public void func(Object t){
    }
    }

    public class Child extends Parent {
    public Number get() {
    return null;
    }
    public void func(Number t) {
    }

    桥方法 - synthetic
    public void func(Object t){
    func((Number)t);
    }
    }

    void test() {
    Child<Integer> child = new Child();
    // 插入强制类型转换
    Integer i = (Integer) child.get();
    }

    步骤1:Parent 中的类型参数 T 被擦除为 Object,而 Child 中的类型参数 T 被擦除为 Number;


    步骤2:child.get(); 插入了强制类型转换


    步骤3:在 Child 中生成桥方法,桥方法是编译器生成的,所以会带有 synthetic 标志位。为什么子类中需要增加桥方法呢,可以先思考这个问题:假如没有桥方法,会怎么样?你可以看看下列代码调用的是子类还是父类方法:


    Parent<Integer> child = new Child<>();
    Parent<Integer> parent = new Parent<>();

    child.func(1); // Parent#func(Object);
    parent.func(1); // Parent#func(Object);

    这两句代码都会调用到 Parent#func(),如果你看过之前我写过的一篇文章,相信难不到你:《Java | 深入理解方法调用的本质(含重载与重写区别)》。在这里我简单分析下:



    1、方法调用的本质是根据方法的符号引用确定方法的直接引用(入口地址)


    2、这两句代码调用的方法符号引用为:


    child.func(new Object()) => com/xurui/Child.func(Object)


    parent.func(new Object()) => com/xurui/Parent.func(Object)


    3、这两句方法调用的字节码指令为 invokevirtual


    4、类加载解析阶段解析类的继承关系,生成类的虚方法表


    5、调用阶段(动态分派):Child 没有重写 func(Object),所以 Child 的虚方法表中存储的是Parent#func(Object);Parent 的虚方法表中存储的是Parent#func(Object);



    可以看到,即使使用对象的实际类型为 Child ,这里调用的依旧是父类的方法。这样就 失去了多态性。 因此,才需要在泛型子类中添加桥方法。



    • 问:为什么擦除后,反编译还是看到类型参数 T ?


    反编译Parent.class,可以看到 T ,不是已经擦除了吗?

    public class Parent<T> {
    public Parent() {
    }

    public void func(T t) {
    }
    }

    答:泛型中所谓的类型擦除,其实只是擦除Code 属性中的泛型信息,在类常量池属性(Signature 属性、LocalVariableTypeTable 属性)中其实还保留着泛型信息,这也是在运行时可以反射获取泛型信息的根本依据,我在第 4 节说。



    • 问:泛型的限制 & 类型擦除会带来什么影响?


    由于类型擦除的影响,在运行期是不清楚类型实参的实际类型的。为了避免程序的运行结果与程序员语义不一致的情况,泛型在使用上存在一些限制。好处是类型擦除不会为每种参数化类型创建新的类,因此泛型不会增大内存消耗。


    泛型的限制




    2. Kotlin的实化类型参数


    前面我们提到,由于类型擦除的影响,在运行期是不清楚类型实参的实际类型的。例如下面的代码是不合法的,因为T并不是一个真正的类型,而仅仅是一个符号:


    在这个函数里,我们传入一个List,企图从中过滤出 T 类型的元素:

    Java:
    <T> List<T> filter(List list) {
    List<T> result = new ArrayList<>();
    for (Object e : list) {
    if (e instanceof T) { // compiler error
    result.add(e);
    }
    }
    return result;
    }
    ---------------------------------------------------
    Kotlin:
    fun <T> filter(list: List<*>): List<T> {
    val result = ArrayList<T>()
    for (e in list) {
    if (e is T) { // cannot check for instance of erased type: T
    result.add(e)
    }
    }
    return result
    }

    Kotlin中,有一种方法可以突破这种限制,即:带实化类型参数的内联函数


    Kotlin:
    inline fun <reified T> filter(list: List<*>): List<T> {
    val result = ArrayList<T>()
    for (e in list) {
    if (e is T) {
    result.add(e)
    }
    }
    return result
    }

    关键在于inlinereified,这两者的语义是:



    • inline(内联函数): Kotlin编译器将内联函数的字节码插入到每一次调用方法的地方

    • reified(实化类型参数): 在插入的字节码中,使用类型实参的确切类型代替类型实参


    规则很好理解,对吧。很明显,当发生方法内联时,方法体字节码就变成了:


    调用:
    val list = listOf("", 1, false)
    val strList = filter<String>(list)
    ---------------------------------------------------
    内联后:
    val result = ArrayList<String>()
    for (e in list) {
    if (e is String) {
    result.add(e)
    }
    }

    需要注意的是,内联函数整个方法体字节码会被插入到调用位置,因此控制内联函数体的大小。如果函数体过大,应该将不依赖于T的代码抽取到单独的非内联函数中。



    注意,无法从 Java 代码里调用带实化类型参数的内联函数



    实化类型参数的另一个妙用是代替 Class 对象引用,例如:


    fun Context.startActivity(clazz: Class<*>) {
    Intent(this, clazz).apply {
    startActivity(this)
    }
    }

    inline fun <reified T> Context.startActivity() {
    Intent(this, T::class.java).apply {
    startActivity(this)
    }
    }

    调用方:
    context.startActivity(MainActivity::class.java)
    context.startActivity<MainActivity>() // 第二种方式会简化一些



    3. 变型:协变 & 逆变 & 不变


    变型(Variant)描述的是相同原始类型的不同参数化类型之间的关系。说起来有点绕,其实就是说:IntegerNumber的子类型,问你List<Integer>是不是List<Number>的子类型?


    变型的种类具体分为三种:协变型 & 逆变型 & 不变型



    • 协变型(covariant): 子类型关系被保留

    • 逆变型(contravariant): 子类型关系被翻转

    • 不变型(invariant): 子类型关系被消除


    在 Java 中,类型参数默认是不变型的,例如:


    List<Number> l1;
    List<Integer> l2 = new ArrayList<>();
    l1 = l2; // compiler error

    相比之下,数组是支持协变型的:


    Number[] nums;
    Integer[] ints = new Integer[10];
    nums = ints; // OK 协变,子类型关系被保留

    那么,当我们需要将List<Integer>类型的对象,赋值给List<Number>类型的引用时,应该怎么做呢?这个时候我们需要限定通配符



    • <? extends> 上界通配符


    要想类型参数支持协变,需要使用上界通配符,例如:


    List<? extends Number> l1;
    List<Integer> l2 = new ArrayList<>();
    l1 = l2; // OK

    但是这会引入一个编译时限制:不能调用参数包含类型参数 E 的方法,也不能设置类型参数的字段,简单来说,就是只能访问不能修改(非严格):


    // ArrayList.java
    public boolean add(E e) {
    ...
    }

    l1.add(1); // compiler error


    • <? super> 下界通配符


    要想类型参数支持逆变,需要使用下界通配符,例如:


    List<? super Integer> l1;
    List<Number> l2 = new ArrayList<>();
    l1 = l2; // OK

    同样,这也会引入一个编译时限制,但是与协变相反:不能调用返回值为类型参数的方法,也不能访问类型参数的字段,简单来说,就是只能修改不能访问(非严格):


    // ArrayList.java
    public E get(int index) {
    ...
    }

    Integer i = l1.get(0); // compiler error


    • <?> 无界通配符

    其实很简单,很多资料其实都解释得过于复杂了。 < ?> 其实就是 的缩写。例如:
    List<?> l1;
    List<Integer> l2 = new ArrayList<>();
    l1 = l2; // OK

    理解了这点,这个问题就很好回答了:



    • 问:List 与 List<?>有什么区别?


    答:List 是原生类型,可以添加或访问元素,不具备编译期安全性,而 List 其实是 List的缩写,是协变型的(可引出协变型的特点与限制);从语义上,List 表明使用者清楚变量是类型安全的,而不是因为疏忽而使用了原生类型 List。



    泛型代码的设计,应遵循PECS原则(Producer extends Consumer super):



    • 如果只需要获取元素,使用 <? extends T>

    • 如果只需要存储,使用<? super T>


    举例:


    // Collections.java public static void copy(List<? super T> dest, List<? extends T> src) { }



    在 Kotlin 中,变型写法会有些不同,但是语义是完全一样的:


    协变:
    val l0: MutableList<*> 相当于MutableList<out Any?>
    val l1: MutableList<out Number>
    val l2 = ArrayList<Int>()
    l0 = l2 // OK
    l1 = l2 // OK
    ---------------------------------------------------
    逆变:
    val l1: MutableList<in Int>
    val l2 = ArrayList<Number>()
    l1 = l2 // OK

    另外,Kotlin 的in & out不仅仅可以用在类型实参上,还可以用在泛型类型声明的类型参数上。其实这是一种简便写法,表示类设计者知道类型参数在整个类上只能协变或逆变,避免在每个使用的地方增加,例如 Kotlin 的List被设计为不可修改的协变型:


    public interface List<out E> : Collection<E> {
    ...
    }


    注意:在 Java 中,只支持使用点变型,不支持 Kotlin 类似的声明点变型



    小结一下:





    4. 使用反射获取泛型信息


    前面提到了,编译期会进行类型擦除,Code 属性中的类型信息会被擦除,但是在类常量池属性(Signature属性、LocalVariableTypeTable属性)中还保留着泛型信息,因此我们可以通过反射来获取这部分信息。


    获取泛型类型实参:需要利用Type体系


    4.1 获取泛型类 & 泛型接口声明


    TypeVariable ParameterizedType GenericArrayType WildcardType


    Gson TypeToken


    Editting....




    5. 总结



    • 应试建议

      • 1、第 1 节非常非常重点,着重记忆:泛型的本质和设计缘由、泛型擦除的三个步骤、限制和优点,已经总结得很精华了,希望能帮到你;

      • 2、着重理解变型(Variant)的概念,以及各种限定符的含义;

      • 3、Kotlin 相关的部分,作为知识积累和思路扩展为主,非应试重点。







    作者:彭丑丑
    链接:https://juejin.cn/post/6888345234653052941
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    收起阅读 »

    「Java 路线」| 反射机制(含 Kotlin)

    前言 反射(Reflection)是一种在运行时 动态访问类型信息 的机制。 在这篇文章里,我将带你梳理Java & Kotlin反射的使用攻略,追求简单易懂又不失深度,如果能帮上忙,请务必点赞加关注! 目录 1. 类型系统...
    继续阅读 »

    前言



    • 反射(Reflection)是一种在运行时 动态访问类型信息 的机制。

    • 在这篇文章里,我将带你梳理Java & Kotlin反射的使用攻略,追求简单易懂又不失深度,如果能帮上忙,请务必点赞加关注!




    目录



    1. 类型系统的基本概念


    首先,梳理一一下类型系统的基础概念:



    • 问:什么是强 / 弱类型语言?


    答:强 / 弱类型语言的区分,关键在于变量是否 (倾向于) 类型兼容。例如,Java 是强类型语言,变量有固定的类型,以下代码在 Java 中是非法的:


    public class MyRunnable {
    public abstract void run();
    }

    // 编译错误:Incompatible types
    java.lang.Runnable runnable = new MyRunnable() {
    @Override
    public void run() {

    }
    }
    runnable.run(); // X

    相对地,JavaScript 是弱类型语言,一个变量没有固定的类型,允许接收不同类型的值:


    function MyRunnable(){
    this.run = function(){
    }
    }
    function Runnable(){
    this.run = function(){
    }
    }
    var ss = new MyRunnable();
    ss.run(); // 只要对象有相同方法签名的方法即可
    ss = new Runnable();
    ss.run();

    更具体地描述,Java的强类型特性体现为:变量仅允许接收相同类型或子类型的值。 嗯(黑人问号脸)?和你的理解一致吗?请看下面代码,哪一行是有问题的:


    注意,请读者假设 1 ~ 4 号代码是单独运行的

    long numL = 1L;
    int numI = 0;
    numL = numI; // 1
    numI = (int)numL; // 2

    Integer integer = new Integer(0);
    Object obj = new Object();
    integer = (Integer) obj; // 3 ClassCastException
    obj = integer; // 4

    在这里,第 3 句代码会发生运行时异常,结论:



    • 1:调用字节码指令 i2l,将 int 值转换为 long 值。(此时,numL 变量接收的是相同类型的值,命题正确)


    • 2:调用字节码指令 l2i,将 long 值转换为 int 值。(此时,numI 变量接收的是相同类型的值,命题正确)


    • 3:调用字节码指令 checkcast,发现 obj 变量的值不是 Integer 类型,抛出 ClassCastException。(此时,Integer 变量不允许接收 Object 对象,命题正确)


    • 4:integer 变量的值是 obj 变量的子类型,可以接收。(此时,Object 变量允许接收 Integer 对象,命题正确)



    用一张图概括一下:






    • 问:什么是静态 / 动态类型语言?


    答:静态 / 动态类型语言的区分,关键在于类型检查是否 (倾向于) 编译时执行。例如, Java & C/C++ 是静态类型语言,而 JavaScript 是动态类型语言。需要注意的是,这个定义并不是绝对的,例如 Java 也存在运行时类型检查的方式,例如上面提到的 checkcast 指令本质上是在运行时检查变量的类型与对象的类型是否相同。 那么 Java 是如何在运行时获得类型信息的呢?这就是我们下一节要讨论的问题。




    2. 反射的基本概念



    • 问:什么是反射?为什么要使用反射?


    答:反射(Reflection)是一种在运行时 动态访问类型信息 的机制。Java 是静态强类型语言,它倾向于在编译时进行类型检查,因此当我们访问一个类时,它必须是编译期已知的,而使用反射机制可以解除这种限制,赋予 Java 语言动态类型的特性。例如:


    void func(Object obj) {
    try {
    Method method = obj.getClass().getMethod("run",null);
    method.invoke(obj,null);
    }
    ... 省略 catch
    }
    func(runnable); 调用 Runnale#run()
    func(myRunnable); 调用 MyRunnale#run()


    • 问:Java 运行时类型信息是如何表示的?


    所有的类在第一次使用时动态加载到内存中,并构造一个 Class 对象,其中包含了与类有关的所有信息,Class 对象是运行时访问类型信息的入口。需要注意的是,每个类 / 内部类 / 接口都拥有各自的 Class 对象。



    • 问:获取 Class 对象有几种方式,有什么区别?


    答:获取 Class 对象是反射的起始步骤,具体来说,分为以下三种方式:



    • 问:为什么反射性能差,怎么优化?


    答:主要有以下原因:


    性能差原因优化方法
    产生大量中间变量缓存元数据对象
    增加了检查可见性操作调用Method#setAccessible(true),减少不必要的检查
    Inflation 机制会生成字节码,而这段字节码没有经过优化/
    缺少编译器优化,普通调用有一系列优化手段,例如方法内联,而反射调用无法应用此优化/
    增加了装箱拆箱操作,反射调用需要构建包装类/


    3. 反射调用的 Inflation 机制


    反射调用是反射的一个较为常用的场景,这里我们来分析下反射调用的源码。反射调用需要使用Method#invoke(...),源码如下:


    Method.java


    public Object invoke(Object obj, Object... args) {
    MethodAccessor ma = methodAccessor;
    if (ma == null) {
    ma = acquireMethodAccessor();
    }
    return ma.invoke(obj, args);
    }

    NativeMethodAccessorImpl.java


    class NativeMethodAccessorImpl extends MethodAccessorImpl {
    private final Method method;
    private DelegatingMethodAccessorImpl parent;
    private int numInvocations;

    NativeMethodAccessorImpl(Method var1) {
    this.method = var1;
    }

    public Object invoke(Object var1, Object[] var2) {
    1. 检查调用次数是否超过阈值
    if (++this.numInvocations > ReflectionFactory.inflationThreshold() && !ReflectUtil.isVMAnonymousClass(this.method.getDeclaringClass())) {
    2. ASM 生成新类
    MethodAccessorImpl var3 = (MethodAccessorImpl)(new MethodAccessorGenerator()).generateMethod(this.method.getDeclaringClass(), this.method.getName(), this.method.getParameterTypes(), this.method.getReturnType(), this.method.getExceptionTypes(), this.method.getModifiers());
    3. 设置为代理
    this.parent.setDelegate(var3);
    }
    4. 调用 native 方法
    return invoke0(this.method, var1, var2);
    }

    void setParent(DelegatingMethodAccessorImpl var1) {
    this.parent = var1;
    }

    private static native Object invoke0(Method var0, Object var1, Object[] var2);
    }

    ReflectionFactory.java


    public class ReflectionFactory {

    private static int inflationThreshold = 15;

    static int inflationThreshold() {
    return inflationThreshold;
    }
    }

    可以看到,反射调用最终会委派给 NativeMethodAccessorImpl ,要点如下:



    • 当反射调用执行次数较少时,直接通过 native 方法调用;

    • 当反射调用执行次数较多时,则通过 ASM 字节码生成技术生成新的类,以后的反射调用委派给新生成的类来处理。



    提示: 为什么不一开始就生成新类呢?因为生成字节码的时间成本高于执行一次 native 方法的时间成本,所以在反射调用执行次数较少时,就直接调用 native 方法了。





    4. 反射的应用场景


    4.1 类型判断



    4.2 创建对象



    • 1、使用 Class.newInstance(),适用于类拥有无参构造方法


    Class classType = Class.forName("java.lang.String");
    String str= (String) classType.newInstance();


    • 2、Constructor.newInstance(),适用于使用带参数的构造方法


    Class classType = Class.forName("java.lang.String");
    Constructor constructor = classType.getConstructor(new Class[]{String.class});
    constructor.setAccessible(true);
    String employee3 = (String) constructor.newInstance(new Object[]{"123"});

    4.3 创建数组


    创建数组需要元素的 Class 对象作为 ComponentType:



    • 1、创建一维数组


    Class classType = Class.forName("java.lang.String");
    String[] array = (String[]) Array.newInstance(classType, 5); 长度为5
    Array.set(array, 3, "abc"); 设置元素
    String string = (String) Array.get(array,3); 读取元素


    • 2、创建多维数组


    Class[] dimens = {3, 3};
    Class[][] array = (Class[][]) Array.newInstance(int.class, dimens);

    4.3 访问字段、方法


    Editting...


    4.4 获取泛型信息


    我们知道,编译期会进行类型擦除,Code 属性中的类型信息会被擦除,但是在类常量池属性(Signature属性、LocalVariableTypeTable属性)中还保留着泛型信息,因此我们可以通过反射来获取这部分信息。在这篇文章里,我们详细讨论:《Java | 关于泛型能问的都在这里了(含Kotlin)》,请关注!


    4.5 获取运行时注解信息


    注解是一种添加到声明上的元数据,而RUNTIME注解在类加载后会保存在 Class 对象,可以反射获取。在这篇文章里,我们详细讨论:《Java | 这是一篇全面的注解使用攻略(含 Kotlin)》,请关注!






    作者:彭丑丑
    链接:https://juejin.cn/post/6889833658669072397
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    Java | JDK 动态代理的原理其实很简单

    前言 代理模式(Proxy Pattern)也称委托模式(Delegate Pattern),是一种结构型设计模式,也是一项基础设计技巧; 其中,动态代理有很多有意思的应用场景,比如 AOP、日志框架、全局性异常处理、事务处理等。这篇文章,我们主要...
    继续阅读 »

    前言



    • 代理模式(Proxy Pattern)也称委托模式(Delegate Pattern),是一种结构型设计模式,也是一项基础设计技巧;

    • 其中,动态代理有很多有意思的应用场景,比如 AOP、日志框架、全局性异常处理、事务处理等。这篇文章,我们主要讨论最基本的 JDK 动态代理。




    目录





    前置知识


    这篇文章的内容会涉及以下前置 / 相关知识,贴心的我都帮你准备好了,请享用~





    1. 概述



    • 什么是代理 (模式)? 代理模式 (Proxy Pattern) 也称委托模式 (Deletage Pattern),属于结构型设计模式,也是一项基本的设计技巧。通常,代理模式用于处理两种问题:

      • 1、控制对基础对象的访问

      • 2、在访问基础对象时增加额外功能



    这是两种非常朴素的场景,正因如此,我们常常会觉得其它设计模式中存在代理模式的影子。UML 类图和时序图如下:





    • 代理的基本分类: 静态代理 + 动态代理,分类的标准是 “代理关系是否在编译期确定;


    • 动态代理的实现方式: JDK、CGLIB、Javassist、ASM





    2. 静态代理


    2.1 静态代理的定义


    静态代理是指代理关系在编译期确定的代理模式。使用静态代理时,通常的做法是为每个业务类抽象一个接口,对应地创建一个代理类。举个例子,需要给网络请求增加日志打印:


    1、定义基础接口
    public interface HttpApi {
    String get(String url);
    }

    2、网络请求的真正实现
    public class RealModule implements HttpApi {
    @Override
    public String get(String url) {
    return "result";
    }
    }

    3、代理类
    public class Proxy implements HttpApi {
    private HttpApi target;

    Proxy(HttpApi target) {
    this.target = target;
    }

    @Override
    public String get(String url) {
    // 扩展的功能
    Log.i("http-statistic", url);
    // 访问基础对象
    return target.get(url);
    }
    }

    2.2 静态代理的缺点



    • 1、重复性: 需要代理的业务或方法越多,重复的模板代码越多;

    • 2、脆弱性: 一旦改动基础接口,代理类也需要同步修改(因为代理类也实现了基础接口)。




    3. 动态代理


    3.1 动态代理的定义


    动态代理是指代理关系在运行时确定的代理模式。需要注意,JDK 动态代理并不等价于动态代理,前者只是动态代理的实现之一,其它实现方案还有:CGLIB 动态代理、Javassist 动态代理和 ASM 动态代理等。因为代理类在编译前不存在,代理关系到运行时才能确定,因此称为动态代理。


    3.2 JDK 动态代理示例


    我们今天主要讨论JDK 动态代理(Dymanic Proxy API),它是 JDK1.3 中引入的特性,核心 API 是 Proxy 类和 InvocationHandler 接口。它的原理是利用反射机制在运行时生成代理类的字节码。


    我们继续用打印日志的例子,使用动态代理时:


    public class ProxyFactory {
    public static HttpApi getProxy(HttpApi target) {
    return (HttpApi) Proxy.newProxyInstance(
    target.getClass().getClassLoader(),
    target.getClass().getInterfaces(),
    new LogHandler(target));
    }

    private static class LogHandler implements InvocationHandler {
    private HttpApi target;

    LogHandler(HttpApi target) {
    this.target = target;
    }
    // method底层的方法无参数时,args为空或者长度为0
    @Override
    public Object invoke(Object proxy, Method method, @Nullable Object[] args)
    throws Throwable
    {
    // 扩展的功能
    Log.i("http-statistic", (String) args[0]);
    // 访问基础对象
    return method.invoke(target, args);
    }
    }
    }

    如果需要兼容多个业务接口,可以使用泛型:


    public class ProxyFactory {
    @SuppressWarnings("unchecked")
    public static T getProxy(T target) {
    return (T) Proxy.newProxyInstance(
    target.getClass().getClassLoader(),
    target.getClass().getInterfaces(),
    new LogHandler(target));
    }

    private static class LogHandler implements InvocationHandler {
    // 同上
    }
    }

    客户端调用:


    HttpAPi proxy = ProxyFactory.getProxy(target);
    OtherHttpApi proxy = ProxyFactory.getProxy(otherTarget);

    通过泛型参数传递不同的类型,客户端可以按需实例化不同类型的代理对象。基础接口的所有方法都统一到 InvocationHandler#invoke() 处理。静态代理的两个缺点都得到解决:



    • 1、重复性:即使有多个基础业务需要代理,也不需要编写过多重复的模板代码;

    • 2、脆弱性:当基础接口变更时,同步改动代理并不是必须的。


    3.3 静态代理 & 动态代理对比



    • 共同点:两种代理模式实现都在不改动基础对象的前提下,对基础对象进行访问控制和扩展,符合开闭原则。

    • 不同点:静态代理存在重复性和脆弱性的缺点;而动态代理(搭配泛型参数)可以实现了一个代理同时处理 N 种基础接口,一定程度上规避了静态代理的缺点。从原理上讲,静态代理的代理类 Class 文件在编译期生成,而动态代理的代理类 Class 文件在运行时生成,代理类在 coding 阶段并不存在,代理关系直到运行时才确定。




    4. JDK 动态代理源码分析


    这一节,我们来分析 JDK 动态代理的源码,核心类是 Proxy,主要分析 Proxy 如何生成代理类,以及如何将方法调用统一分发到 InvocationHandler 接口。


    4.1 API 概述


    Proxy 类主要包括以下 API:



























    Proxy 描述
    getProxyClass(ClassLoader, Class...) : Class 获取实现目标接口的代理类 Class 对象
    newProxyInstance(ClassLoader,Class[],InvocationHandler) : Object 获取实现目标接口的代理对象
    isProxyClass(Class) : boolean 判断一个 Class 对象是否属于代理类
    getInvocationHandler(Object) : InvocationHandler 获取代理对象内部的 InvocationHandler

    4.2 核心源码


    Proxy.java


    1、获取代理类 Class 对象
    public static Class getProxyClass(ClassLoader loader,Class... interfaces){
    final Class[] intfs = interfaces.clone();
    ...
    1.1 获得代理类 Class 对象
    return getProxyClass0(loader, intfs);
    }

    2、实例化代理类对象
    public static Object newProxyInstance(ClassLoader loader,Class[] interfaces,InvocationHandler h){
    ...
    final Class[] intfs = interfaces.clone();
    2.1 获得代理类 Class对象
    Class cl = getProxyClass0(loader, intfs);
    ...
    2.2 获得代理类构造器 (接收一个 InvocationHandler 参数)
    // private static final Class[] constructorParams = { InvocationHandler.class };
    final Constructor cons = cl.getConstructor(constructorParams);
    final InvocationHandler ih = h;
    ...
    2.3 反射创建实例
    return newInstance(cons, ih);
    }

    可以看到,实例化代理对象也需要先通过 getProxyClass0(...) 获取代理类 Class 对象,而 newProxyInstance(...) 随后会获取参数为 InvocationHandler 的构造函数实例化一个代理类对象。


    我们先看下代理类 Class 对象是如何获取的:


    Proxy.java


    -> 1.12.1 获得代理类 Class对象
    private static Class getProxyClass0(ClassLoader loader,Class... interfaces) {
    ...
    从缓存中获取代理类,如果缓存未命中,则通过ProxyClassFactory生成代理类
    return proxyClassCache.get(loader, interfaces);
    }

    private static final class ProxyClassFactory implements BiFunction[], Class>{

    3.1 代理类命名前缀
    private static final String proxyClassNamePrefix = "$Proxy";

    3.2 代理类命名后缀,从 0 递增(原子 Long)
    private static final AtomicLong nextUniqueNumber = new AtomicLong();

    @Override
    public Class apply(ClassLoader loader, Class[] interfaces)
    {
    Map, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length);
    3.3 参数校验
    for (Class intf : interfaces) {
    // 验证参数 interfaces 和 ClassLoder 中加载的是同一个类
    // 验证参数 interfaces 是接口类型
    // 验证参数 interfaces 中没有重复项
    // 否则抛出 IllegalArgumentException
    }
    // 验证所有non-public接口来自同一个包

    3.4(一般地)代理类包名
    // public static final String PROXY_PACKAGE = "com.sun.proxy";
    String proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";

    3.5 代理类的全限定名
    long num = nextUniqueNumber.getAndIncrement();
    String proxyName = proxyPkg + proxyClassNamePrefix + num;

    3.6 生成字节码数据
    byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces);

    3.7 从字节码生成 Class 对象
    return defineClass0(loader, proxyName,proxyClassFile, 0, proxyClassFile.length);
    }
    }

    -> 3.6 生成字节码数据
    public static byte[] generateProxyClass(final String var0, Class[] var1) {
    ProxyGenerator var2 = new ProxyGenerator(var0, var1);
    ...
    final byte[] var3 = var2.generateClassFile();
    return var3;
    }

    ProxyGenerator.java


    private byte[] generateClassFile() {
    3.6.1 只代理Object的hashCode、equals和toString
    this.addProxyMethod(hashCodeMethod, Object.class);
    this.addProxyMethod(equalsMethod, Object.class);
    this.addProxyMethod(toStringMethod, Object.class);

    3.6.2 代理接口的每个方法
    ...
    for(var1 = 0; var1 < this.interfaces.length; ++var1) {
    ...
    }

    3.6.3 添加带有 InvocationHandler 参数的构造器
    this.methods.add(this.generateConstructor());
    var7 = this.proxyMethods.values().iterator();
    while(var7.hasNext()) {
    ...
    3.6.4 在每个代理的方法中调用InvocationHandler#invoke()
    }

    3.6.5 输出字节流
    ByteArrayOutputStream var9 = new ByteArrayOutputStream();
    DataOutputStream var10 = new DataOutputStream(var9);
    ...
    return var9.toByteArray();
    }

    以上代码已经非常简化了,主要关注核心流程:JDK 动态代理生成的代理类命名为 com.sun.proxy$Proxy[从0开始的数字](例如:com.sun.proxy$Proxy0),这个类继承自 java.lang.reflect.Proxy。其内部还有一个参数为 InvocationHandler 的构造器,对于代理接口的方法调用都会分发到 InvocationHandler#invoke()。


    UML 类图如下,需要注意图中红色箭头,表示代理类和 HttpApi 接口的代理关系在运行时才确定:




    提示: Android 系统中生成字节码和从字节码生成 Class 对象的步骤都是 native 方法:



    • private static native Class generateProxy(…)

    • 对应的native方法:dalvik/vm/native/java_lang_reflect_Proxy.cpp



    4.3 查看代理类源码


    可以看到,ProxyGenerator#generateProxyClass() 其实是一个静态 public 方法,所以我们直接调用,并将代理类 Class 的字节流写入磁盘文件,使用 IntelliJ IDEA 的反编译功能查看源代码。


    输出字节码:


    byte[] classFile = ProxyGenerator.generateProxyClass("$proxy0",new Class[]{HttpApi.class});
    // 直接写入项目路径下,方便使用IntelliJ IDEA的反编译功能
    String path = "/Users/pengxurui/IdeaProjects/untitled/src/proxy/HttpApi.class";
    try(FileOutputStream fos = new FileOutputStream(path)){
    fos.write(classFile);
    fos.flush();
    System.out.println("success");
    } catch (Exception e){
    e.printStackTrace();
    System.out.println("fail");
    }

    反编译结果:


    public final class $proxy0 extends Proxy implements HttpApi {
    //反射的元数据Method存储起来,避免重复创建
    private static Method m1;
    private static Method m2;
    private static Method m3;
    private static Method m0;

    public $proxy0(InvocationHandler var1) throws {
    super(var1);
    }

    /**
    * Object#hashCode()
    * Object#equals(Object)
    * Object#toString()
    */


    // 实现了HttpApi接口
    public final String get() throws {
    try {
    //转发到Invocation#invoke()
    return (String)super.h.invoke(this, m3, (Object[])null);
    } catch (RuntimeException | Error var2) {
    throw var2;
    } catch (Throwable var3) {
    throw new UndeclaredThrowableException(var3);
    }
    }

    static {
    try {
    //Object#hashCode()
    //Object#equals(Object)
    //Object#toString()
    m3 = Class.forName("HttpApi").getMethod("get");
    } catch (NoSuchMethodException var2) {
    throw new NoSuchMethodError(var2.getMessage());
    } catch (ClassNotFoundException var3) {
    throw new NoClassDefFoundError(var3.getMessage());
    }
    }
    }

    4.4 常见误区



    • 基础对象必须实现基础接口,否则不能使用动态代理


    这个想法可能来自于一些没有实现任何接口的类,因此就没有办法得到接口的Class对象作为Proxy#newProxyInstance() 的参数,这确实会带来一些麻烦,举个例子:


    package com.domain;
    public interface HttpApi {
    String get();
    }

    // 另一个包的non-public接口
    package com.domain.inner;
    /**non-public**/interface OtherHttpApi{
    String get();
    }

    package com.domain.inner;
    // OtherHttpApiImpl类没有实现HttpApi接口或者没有实现任何接口
    public class OtherHttpApiImpl /**extends OtherHttpApi**/{
    public String get() {
    return "result";
    }
    }

    // Client:
    HttpApi api = (HttpApi) Proxy.newProxyInstance(...}, new InvocationHandler() {
    OtherHttpApiImpl impl = new OtherHttpApiImpl();

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    // TODO:扩展的新功能
    // IllegalArgumentException: object is not an instance of declaring class
    return method.invoke(impl,args);
    }
    });
    api.get();

    在这个例子里,OtherHttpApiImpl 类因为历史原因没有实现 HttpApi 接口,虽然方法签名与 HttpApi 接口的方法签名完全相同,但是遗憾,无法完成代理。也有补救的办法,找到 HttpApi 接口中签名相同的 Method,使用这个 Method 来转发调用。例如:


    HttpApi api = (HttpApi) Proxy.newProxyInstance(...}, new InvocationHandler() {
    OtherHttpApiImpl impl = new OtherHttpApiImpl();

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    // TODO:扩展的新功能
    if (method.getDeclaringClass() != impl.getClass()) {
    // 找到相同签名的方法
    Method realMethod = impl.getClass().getDeclaredMethod(method.getName(), method.getParameterTypes());
    return realMethod.invoke(impl, args);
    }else{
    return method.invoke(impl,args);
    }
    }
    });



    5. 总结


    今天,我们讨论了静态代理和动态代理两种代理模式,静态代理在设计模式中随处可见,但存在重复性和脆弱性的缺点,动态代理的代理关系在运行时确定,可以实现一个代理处理 N 种基础接口,一定程度上规避了静态代理的缺点。在我们熟悉的一个网络请求框架中,就充分利用了动态代理的特性,你知道是在说哪个框架吗?




    参考资料







    作者:彭丑丑
    链接:https://juejin.cn/post/6974018412158664734
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    这一次,彻底搞懂SparseArray实现原理

    最近在整理SparseArray这一知识点的时候,发现网上大多数SparseArray原理分析的文章都存在很多问题(可以说很多作者并没有读懂SparseArray的源码),也正因此,才有了这篇文章。我们知道,SparseArray与ArrayMap是Andro...
    继续阅读 »

    最近在整理SparseArray这一知识点的时候,发现网上大多数SparseArray原理分析的文章都存在很多问题(可以说很多作者并没有读懂SparseArray的源码),也正因此,才有了这篇文章。我们知道,SparseArray与ArrayMap是Android中高效存储K-V的数据结构,也是是Android面试中的常客,弄懂它们的实现原理是很有必要的,本篇文章就以SparseArray的源码为例进行深入分析。


    一、SparseArray的类结构


    SparseArray可以翻译为稀疏数组,从字面上可以理解为松散不连续的数组。虽然叫做Array,但它却是存储K-V的一种数据结构。其中Key只能是int类型,而Value是Object类型。我们来看下它的类结构:


    public class SparseArray<E> implements Cloneable {
    // 用来标记此处的值已被删除
    private static final Object DELETED = new Object();
    // 用来标记是否有元素被移除
    private boolean mGarbage = false;
    // 用来存储key的集合
    private int[] mKeys;
    // 用来存储value的集合
    private Object[] mValues;
    // 存入的元素个数
    private int mSize;

    // 默认初始容量为10
    public SparseArray() {
    this(10);
    }

    public SparseArray(int initialCapacity) {
    if (initialCapacity == 0) {
    mKeys = EmptyArray.INT;
    mValues = EmptyArray.OBJECT;
    } else {
    mValues = ArrayUtils.newUnpaddedObjectArray(initialCapacity);
    mKeys = new int[mValues.length];
    }
    mSize = 0;
    }

    // ...省略其他代码

    }

    可以看到SparseArray仅仅实现了Cloneable接口并没有实现Map接口,并且SparseArray内部维护了一个int数组和一个Object数组。在无参构造方法中调用了有参构造,并将其初始容量设置为了10。


    二、SparseArray的remove()方法


    是不是觉得很奇怪?作为一个容器类,不先讲put方法怎么先将remove呢?这是因为remove方法的一些操作会影响到put的操作。只有先了解了remove才能更容易理解put方法。我们来看remove的代码:



    // SparseArray
    public void remove(int key) {
    delete(key);
    }

    public void delete(int key) {
    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

    if (i >= 0) {
    if (mValues[i] != DELETED) {
    mValues[i] = DELETED;
    mGarbage = true;
    }
    }
    }

    可以看到remove方法直接调用了delete方法。而在delete方法中会先通过二分查找(二分查找代码后边分析)找到key所在的位置,然后将这一位置的value值置为DELETE,注意,这里还将mGarbage设置为了true来标记集合中存在删除元素的情况。想象一下,在删除多个元素后这个集合中是不是就可能会出现不连续的情况?大概这也是SparseArray名字的由来吧。


    三、SparseArray的put()方法


    作为一个存储K-V类型的数据结构,put方法是key和value的入口。也是SparseArray中最重要的一个方法。先来看下put方法的代码:


    // SparseArray
    public void put(int key, E value) {
    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

    if (i >= 0) { // 意味着之前mKeys中已经有对应的key存在了,第i个位置对应的就是key。
    mValues[i] = value; // 直接更新value
    } else { // 返回负数说明未在mKeys中查找到key

    // 取反得到待插入key的位置
    i = ~i;

    // 如果插入位置小于size,并且这个位置的value刚好是被删除掉的,那么直接将key和value分别插入mKeys和mValues的第i个位置
    if (i < mSize && mValues[i] == DELETED) {
    mKeys[i] = key;
    mValues[i] = value;
    return;
    }
    // mGarbage为true说明有元素被移除了,此时mKeys已经满了,但是mKeys内部有被标记为DELETE的元素
    if (mGarbage && mSize >= mKeys.length) {
    // 调用gc方法移动mKeys和mValues中的元素,这个方法可以后边分析
    gc();

    // 由于gc方法移动了数组,因此插入位置可能有变化,所以需要重新计算插入位置
    i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
    }
    // GrowingArrayUtils的insert方法将会将插入位置之后的所有数据向后移动一位,然后将key和value分别插入到mKeys和mValue对应的第i个位置,如果数组空间不足还会开启扩容,后边分析这个insert方法
    mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
    mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
    mSize++;
    }
    }

    虽然这个方法只有寥寥数行,但是想要完全理解却并非易事,即使写了很详细的注释也不容易读懂。我们不妨来详细分析一下。第一行代码通过二分查找得到了一个index。看下二分查找的代码:


    // ContainerHelpers
    static int binarySearch(int[] array, int size, int value) {
    int lo = 0;
    int hi = size - 1;

    while (lo <= hi) {
    final int mid = (lo + hi) >>> 1;
    final int midVal = array[mid];

    if (midVal < value) {
    lo = mid + 1;
    } else if (midVal > value) {
    hi = mid - 1;
    } else {
    return mid; // value found
    }
    }
    return ~lo; // value not present
    }

    关于二分查找相信大家都是比较熟悉的,这一算法用于在一组有序数组中查找某一元素所在位置的。如果数组中存在这一元素,则将这个元素对应的位置返回。如果不存在那么此时的lo就是这个元素的最佳存储位置。上述代码中将lo取反作为了返回值。因为lo一定是大于等于0的数,因此取反后的返回值必定小于等于0.明白了这一点,再来看put方法中的这个if...else是不是很容易理解了?


    // SparseArray
    public void put(int key, E value) {

    if (i >= 0) {
    mValues[i] = value; // 直接更新value
    } else {
    i = ~i;
    // ... 省略其它代码
    }
    }

    如果i>=0,意味着当前的这个key已经存在于mKeys中了,那么此时put只需要将最新的value更新到mValues中即可。而如果i<=0就意味着mKeys中之前没有对应的key。因此就需要将key和value分别插入到mKeys和mValues中。而插入的最佳位置就是对i取反。


    得到插入位置之后,如果这个位置是被标记为删除的元素,那么久可以直接将其覆盖掉了,因此有以下代码:


    public void put(int key, E value) {
    // ...
    if (i >= 0) {
    // ...
    } else {
    // 如果i对应的位置是被删除掉的,可以直接将其覆盖
    if (i < mSize && mValues[i] == DELETED) {
    mKeys[i] = key;
    mValues[i] = value;
    return;
    }
    // ...
    }

    }

    如果上边条件不满足,那么继续往下看:


    public void put(int key, E value) {
    // ...
    if (i >= 0) {
    // ...
    } else {
    // mGarbage为true说明有元素被移除了,此时mKeys已经满了,但是mKeys内部有被标记为DELETE的元素
    if (mGarbage && mSize >= mKeys.length) {
    // 调用gc方法移动mKeys和mValues中的元素,这个方法可以后边分析
    gc();

    // 由于gc方法移动了数组,因此插入位置可能有变化,所以需要重新计算插入位置
    i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
    }
    // ...
    }

    }

    上边我们已经知道,在remove元素的时候mGarbage会被置为true,这段代码意味着有被移除的元素,被移除的位置并不是要插入的位置,并且如果mKeys已经满了,那么就调用gc方法来移动元素填充被移除的位置。由于mKeys中元素位置发生了变化,因此key插入的位置也可能改变,因此需要再次调用二分法来查找key的插入位置。


    以上代码最终会确定key被插入的位置,接下来调用GrowingArrayUtils的insert方法来进行key的插入操作:


    // SparseArray
    public void put(int key, E value) {
    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

    if (i >= 0) {
    // ...
    } else {
    // ...

    // GrowingArrayUtils的insert方法将会将插入位置之后的所有数据向后移动一位,然后将key和value分别插入到mKeys和mValue对应的第i个位置,如果数组空间不足还会开启扩容,后边分析这个insert方法
    mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
    mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
    mSize++;
    }
    }

    GrowingArrayUtils的insert方法代码如下:


    // GrowingArrayUtils
    public static <T> T[] insert(T[] array, int currentSize, int index, T element) {
    assert currentSize <= array.length;
    // 如果插入后数组size小于数组长度,能进行插入操作
    if (currentSize + 1 <= array.length) {
    // 将index之后的所有元素向后移动一位
    System.arraycopy(array, index, array, index + 1, currentSize - index);
    // 将key插入到index的位置
    array[index] = element;
    return array;
    }

    // 来到这里说明数组已满,需需要进行扩容操作。newArray即为扩容后的数组
    T[] newArray = ArrayUtils.newUnpaddedArray((Class<T>)array.getClass().getComponentType(),
    growSize(currentSize));
    System.arraycopy(array, 0, newArray, 0, index);
    newArray[index] = element;
    System.arraycopy(array, index, newArray, index + 1, array.length - index);
    return newArray;
    }

    // 返回扩容后的size
    public static int growSize(int currentSize) {
    return currentSize <= 4 ? 8 : currentSize * 2;
    }

    insert方法的代码比较容易理解,如果数组容量足够,那么就将index之后的元素向后移动一位,然后将key插入index的位置。如果数组容量不足,那么则需要进行扩容,然后再进行插入操作。


    四、SparseArray的gc()方法


    这个方法其实很容易理解,我们知道Java虚拟机在内存不足时会进行GC操作,标记清除法在回收垃圾对象后为了避免内存碎片化,会将存活的对象向内存的一端移动。而SparseArray中的这个gc方法其实就是借鉴了垃圾收集整理碎片空间的思想。


    关于mGarbage这个参数上边已经有提到过了,这个变量会在删除元素的时候被置为true。如下:


    // SparseArray中所有移除元素的方法中都将mGarbage置为true

    public E removeReturnOld(int key) {
    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

    if (i >= 0) {
    if (mValues[i] != DELETED) {
    final E old = (E) mValues[i];
    mValues[i] = DELETED;
    mGarbage = true;
    return old;
    }
    }
    return null;
    }

    public void delete(int key) {
    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

    if (i >= 0) {
    if (mValues[i] != DELETED) {
    mValues[i] = DELETED;
    mGarbage = true;
    }
    }
    }


    public void removeAt(int index) {
    if (index >= mSize && UtilConfig.sThrowExceptionForUpperArrayOutOfBounds) {
    throw new ArrayIndexOutOfBoundsException(index);
    }
    if (mValues[index] != DELETED) {
    mValues[index] = DELETED;
    mGarbage = true;
    }
    }



    而SparseArray中所有插入和查找元素的方法中都会判断如果mGarbage为true,并且mSize >= mKeys.length时调用gc,以append方法为例,代码如下:


    public void append(int key, E value) {

    if (mGarbage && mSize >= mKeys.length) {
    gc();
    }

    // ... 省略无关代码
    }

    源码中调用gc方法的地方多达8处,都是与添加和查找元素相关的方法。例如put()、keyAt()、setValueAt()等方法中。gc的实现其实比较简单,就是将删除位置后的所有数据向前移动一下,代码如下:


    private void gc() {
    // Log.e("SparseArray", "gc start with " + mSize);

    int n = mSize;
    int o = 0;
    int[] keys = mKeys;
    Object[] values = mValues;

    for (int i = 0; i < n; i++) {
    Object val = values[i];

    if (val != DELETED) {
    if (i != o) {
    keys[o] = keys[i];
    values[o] = val;
    values[i] = null;
    }

    o++;
    }
    }

    mGarbage = false;
    mSize = o;

    // Log.e("SparseArray", "gc end with " + mSize);
    }

    五、SparseArray的get()方法


    这个方法就比较简单了,因为put的时候是维持了一个有序数组,因此通过二分查找可以直接确定key在数组中的位置。


    public E get(int key, E valueIfKeyNotFound) {
    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

    if (i < 0 || mValues[i] == DELETED) {
    return valueIfKeyNotFound;
    } else {
    return (E) mValues[i];
    }
    }

    六、总结


    可见SparseArray是一个使用起来很简单的数据结构,但是它的原理理解起来似乎却没那么容易。这也是网上大部分文章对应SparseArray的解析都是含糊不清的原因。相信通过本篇文章的学习一定对SparseArray的实现有了新的认识!


    作者:我赌一包辣条
    链接:https://juejin.cn/post/6972985532397649933
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    iOS 音视频编解码----H264-I(关键)帧,B/P(参考)帧

    内容元素1.图像(image)2.音频(Audio)3.元素信息(Meta-data)编码格式1.Video:H2642.Audio:AAC3.容器封装:MP4/MOV/FLV/RM/RMVB/AVIH264当我们需要对发送的视频文件进行编码时,只要是H264...
    继续阅读 »




  • 内容元素

    1.图像(image)


    2.音频(Audio)


    3.元素信息(Meta-data)


  • 编码格式

    1.Video:H264


    2.Audio:AAC


    3.容器封装:MP4/MOV/FLV/RM/RMVB/AVI


  • H264

    当我们需要对发送的视频文件进行编码时,只要是H264文件,AVFoundation都提供视频编解码器支持,这个标准被广泛应用于消费者视频摄像头捕捉到的资源并成为网页流媒体视频最主要的格式。H264规范是MPEG4定义的一部分,H264遵循早期的MPEG-1/MPEG-2标准,但是在以更低比特率得到 更高图片质量方面有了进步。


  • 编码的本质






    • 为什么要编码?

    举个例子: (具体大小我没数过,只做讲解参考)
    你的老公,Helen,将于明天晚上6点零5份在重庆的江北机场接你
    ----------------------23 * 2 + 10 = 56个字符--------------------------


    你的老公将于明天晚上6点零5分在江北机场接你
    ----------------------20 * 2 + 2 = 42个字符----------------------------


    Helen将于明天晚上6点在机场接你
    ----------------------10 * 2 + 2 = 26个字符----------------------------


    相信大家看了这个例子之后,心里应该大概明白编码的本质:只要不接收方不会产生误解,就可以产生数据的承载量,编码视频的本质也是如此,编码的本质就是减少数据的冗余


    • 在引入I(关键)帧,B/P(参考)帧之前,我们先来了解一下人眼和视频摄像头的对帧的识别

      我们人眼看实物的时候,一般的一秒钟只要是连续的16帧以上,我们就会认为事物是动的,对于摄像头来说,一秒钟所采集的图片远远高于了16帧,可以达到几十帧,对于一些高质量的摄像头,一秒钟可以达到60帧,对于一般的事物来说,你一秒钟能改变多少个做动作?所以,当摄像头在一秒钟内采集视频的时候,前后两帧的图片数据里有大量的相同数据,对于这些数据我们该怎么处理了?当然前后两帧也有不同





  • I帧(I-frames,也叫关键帧)


    • 我们知道在视频的传输过程中,它是分片段传输的,这个片段的第一帧,也就是第一张图片,就是I帧,也就是传说中的关键帧
    • I帧:也就是关键帧,帧内压缩(也就是压缩独立视频帧,被称为帧内压缩),帧内压缩通过消除包含在每个独立视频帧内的色彩以及结构中的冗余信息来进行压缩,因此可在不降低图片适量的情况下尽可能的缩小尺寸,这类似于JEPG压缩的原理,帧内压缩也可以称为有损压缩算法,但通常用于对原因图片的一部分进行处理,以生成极高质量的照片,通过这一过程的创建的帧称为I-frames;将第一帧完整的保存下来.如果没有这个关键帧后面解码数据,是完成不了的.所以I帧特别关键.

  • P帧(P-frames,又称为预测帧)

    • P帧:向前参考帧(在I帧(关键帧)后,P帧参考关键帧,保存下一帧和前一帧的不同数据).压缩时只参考前一个帧.属于帧间压缩技术.
    • 帧间压缩技术:很多帧被组合在一起作为一组图片(简称GOP),对于GOP所存在的时间维度的冗余可以被消除,如果想象视频文件中的典型场景,就会有一些特定的运动元素的概念,比如行驶中的汽车或者街道上行走的路人,场景的背景环信通道是固定的,或者在一定的时间内,有些元素的改变很小或者不变,这些数据就称为时间上的冗余,这些数据就可以通过帧间压缩的方式进行消除,也就是帧间压缩,视频的第一帧会被作为关键帧完整保存下来.而后面的帧会向前依赖.也就是第二帧依赖于第一个帧.后面所有的帧只存储于前一帧的差异.这样就能将数据大大的减少.从而达到一个高压缩率的效果.这就是P帧,保存前后两帧不通的数据

  • B帧(B-frames,又称为双向帧)

    • B帧,又叫双向帧,它是基于使用前后两帧进行编码后得到的帧,几乎不需要存储空间,但是解压过程会消耗较长的时间,因为它依赖周围其他的帧
    • B帧的特点:B帧使得视频的压缩率更高.存储的数据量更小.如果B帧的数量越多,你的压缩率就越高.这是B帧的优点,但是B帧最大的缺点是,如果是实时互动的直播,那时与B帧就要参考后面的帧才能解码,那在网络中就要等待后面的帧传输过来.这就与网络有关了.如果网络状态很好的话,解码会比较快,如果网络不好时解码会稍微慢一些.丢包时还需要重传.对实时互动的直播,一般不会使用B帧.如果在泛娱乐的直播中,可以接受一定度的延时,需要比较高的压缩比就可以使用B帧.如果我们在实时互动的直播,我们需要提高时效性,这时就不能使用B帧了.

  • GOP一组帧的理解

    • 如果在一秒钟内,有30帧.这30帧可以画成一组.如果摄像机或者镜头它一分钟之内它都没有发生大的变化.那也可以把这一分钟内所有的帧画做一组.

    • 什么叫一组帧?
      就是一个I帧到下一个I帧.这一组的数据.包括B帧/P帧.我们称为GOP






    • 视频花屏/卡顿原因

      • 我们平常在观看视频的时候,出现视频的花屏或者卡顿,第一反应就是我们的网络出现了问题,其实我们的网络没有问题,是我们在解码的时候I帧,B/P帧出现了丢失
      • 如果GOP分组中的P帧丢失就会造成解码端的图像发生错误.
      • 为了避免花屏问题的发生,一般如果发现P帧或者I帧丢失.就不显示本GOP内的所有帧.只到下一个I帧来后重新刷新图像.
      • 当这时因为没有刷新屏幕.丢包的这一组帧全部扔掉了.图像就会卡在哪里不动.这就是卡顿的原因.

    所以总结起来,花屏是因为你丢了P帧或者I帧.导致解码错误. 而卡顿是因为为了怕花屏,将整组错误的GOP数据扔掉了.直达下一组正确的GOP再重新刷屏.而这中间的时间差,就是我们所感受的卡顿.



    作者:枫紫
    链接:https://www.jianshu.com/p/94d2a8bbc3ac





    收起阅读 »

    OpenGLES/(GLKit/CoreAnimation正方体的渲染+旋转)

    一.Hello--OpenGLES                 OpenGL可用于渲染...
    继续阅读 »

    一.Hello--OpenGLES 

                    OpenGL可用于渲染2D和3D图像,是一个多用途的开源图形库。OpenGL设计用来将函数命令转换成图形命令,发送到GPU中。GPU正是被设计用来处理图形命令的,所以OpenGL的绘制非常高效。

                    OpenGLES是OpenGL的简化版本,抛弃了冗余的文件及命令,使之专用于嵌入式设备。OpenGLES使得移动APP能充分利用GPU的强大运算能力。iOS设备上的GPU能执行更精确的2D和3D绘制,以及更加复杂的针对每个像素的图形脚本(shader)计算。⽀持的平台: iOS, Andriod , BlackBerry ,bada ,Linux ,Windows。

    1.1准备工程

                    iOS新建工程,@interface ViewController : UIViewController改成-->@interface ViewController : GLKViewController,.h文件导入#import,.m导入#import#import,最后在Main.storyboard中将view改成GLVIew






    GLView

    1.2EAGLContext(OpenGL 上下文)

                    EAGLContext对象管理着OpenGLES的渲染context,即所有绘制的状态,命令及资源信息,并控制GPU去执行渲染运算。 绘制如textures及renderbuffers的过程,是由一个与context绑定的EAGLSharegroup对象来管理的。当初始化一个EAGLContext对象的时候,可选择新建一个sharegroup,或者使用已有的,这一点我们往往采用系统默认即可。在绘制到context之前,我们要先绑定一个完整的framebuffer对象到context中。






    Hello-OpenGLES

                    1)初始化上写文:context = [[EAGLContext alloc]initWithAPI:kEAGLRenderingAPIOpenGLES3];(参数知识选择版本)

                    2)设置当前上下文:[EAGLContext setCurrentContext:context];

                    3)GLView绑定上下文:GLKView *view =(GLKView *) self.view;  view.context=context;

                    注意:在使用GLview中,我们必须实现它的协议:GLKViewDelegate--->- (void)glkView:(GLKView*)viewdrawInRect:(CGRect)rect,GLKView对象使其OpenGL ES上下文成为当前上下文,并将其framebuffer绑定为OpenGL ES呈现命令的目标。然后,委托方法应该绘制视图的内容。我们给GLview设置颜色,看一下效果:glClearColor(1, 0, 0, 1.0);






    Hello--OpenGlES

    二.显示图片







    加载图片

    2.1设置顶点坐标/纹理坐标

                     在OpenGl中我们显示一张图片,首先我们设置顶点数组,绑定纹理,在OpenGLES中,我们一样这么设置:

                                    GLfloatvertexData[] = {

                                                                            0.5, -0.5,0.0f,    1.0f,0.0f,//右下

                                                                            0.5,0.5,  0.0f,    1.0f,1.0f,//右上

                                                                            -0.5,0.5,0.0f,    0.0f,1.0f,//左上


                                                                            0.5, -0.5,0.0f,    1.0f,0.0f,//右下

                                                                            -0.5,0.5,0.0f,    0.0f,1.0f,//左上

                                                                            -0.5, -0.5,0.0f,  0.0f,0.0f,//左下

                                                                        };

                      在OpenGL中我们提到了图形绘制是点,线,三角形,正方形由两个三角形组成,就是六个顶点,而我们知道,纹理的坐标范围是(0,1),其原点是在左下角,所以坐标(0,0)是原点,右上角(1,1);

    2.2开辟顶点缓存区并把数据存到缓中区

                    (1).创建顶点缓存区标识符ID

                            GLuint  bufferID;

                            glGenBuffers(1, &bufferID);(分配纹理)

                    (2).绑定顶点缓存区.(明确作用)

                            glBindBuffer(GL_ARRAY_BUFFER, bufferID);

                    (3).将顶点数组的数据copy到顶点缓存区中(GPU显存中)

                            glBufferData(GL_ARRAY_BUFFER,sizeof(vertexData), vertexData,GL_STATIC_DRAW);

                    (4).打开读取通道.

                            1)顶点坐标数据

                                glEnableVertexAttribArray(GLKVertexAttribPosition);

                                glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 5, (GLfloat *)NULL + 0);

                            2)纹理坐标数据

                                glEnableVertexAttribArray(GLKVertexAttribTexCoord0);

                                glVertexAttribPointer(GLKVertexAttribTexCoord0, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 5, (GLfloat *)NULL + 3);

    特别说明:

                        (1)在iOS中, 默认情况下,出于性能考虑,所有顶点着色器的属性(Attribute)变量都是关闭的.意味着,顶点数据在着色器端(服务端)是不可用的. 即使你已经使用glBufferData方法,将顶点数据从内存拷贝到顶点缓存区中(GPU显存中).所以, 必须由glEnableVertexAttribArray 方法打开通道.指定访问属性.才能让顶点着色器能够访问到从CPU复制到GPU的数据.

                         注意: 数据在GPU端是否可见,即,着色器能否读取到数据,由是否启用了对应的属性决定,这就是glEnableVertexAttribArray的功能,允许顶点着色器读取GPU(服务器端)数据。

                        (2)方法简介

                            glVertexAttribPointer (GLuint indx, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid* ptr)    功能: 上传顶点数据到显

    存的方法(设置合适的方式从buffer里面读取数据)

             参数列表:

                            index,指定要修改的顶点属性的索引值,例如 size, 每次读取数量。(如position是由3个(x,y,z)组成,而颜色是4个(r,g,b,a),纹理则是2个,type,指定数组中每个组件的数据类型。可用的符号常量有GL_BYTE, GL_UNSIGNED_BYTE,GL_SHORT,GL_UNSIGNED_SHORT, GL_FIXED, 和 GL_FLOAT,初始值为GL_FLOAT。 normalized,指定当被访问时,固定点数据值是否应该被归一化(GL_TRUE)或者直接转换为固定点值(GL_FALSE)stride,指定连续顶点属性之间的偏移量。如果为0,那么顶点属性会被理解为:它们是紧密排列在一起的。初始值为0 ,ptr指定一个指针,指向数组中第一个顶点属性的第一个组件。初始值为0







    参数流程说明

    2.2获取纹理







    纹理

                    1)路径:NSString *filePath = [[NSBundle mainBundle]pathForResource:@"kunkun" ofType:@"jpg"];

                    2)参数:NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:@(1),GLKTextureLoaderOriginBottomLeft, nil];

                                    GLKTextureInfo *textureInfo = [GLKTextureLoader textureWithContentsOfFile:filePath options:options error:nil];

                    说明:纹理坐标原点是左下角,但是图片显示原点应该是左上角.我们要设置图片绘制从左上角开始绘制GLKTextureLoaderOriginBottomLeft;

                    3)cEffect:你可以把它理解成UIimageVIew,用于显示图片的控件,iOS提供GLKBaseEffect 完成着色器工作(顶点/片元)

                                cEffect = [[GLKBaseEffect alloc]init];

                                cEffect.texture2d0.enabled = GL_TRUE;

                                cEffect.texture2d0.name= textureInfo.name;

                    最后在GLVIew的delegate中:

                                                    1.清除颜色缓冲区

                                                    glClear(GL_COLOR_BUFFER_BIT);

                                                    2.准备绘制

                                                    [cEffect prepareToDraw];

                                                    3.开始绘制

                                                    glDrawArrays(GL_TRIANGLES, 0, 6);







    效果

    三.OpenGLES绘制立方体

            在OpenGLES绘制立方体,相当于绘制六个面,十二个三角形,60个数据(当然你在图元连接方式那里可以选择平面,GL_TRIANGLE_FAN,这样就会少设置一点数据,这里我选择GL_TRIANGLES)

    GLfloatvertexData[] = {

            //第一个面

            0.5, -0.5, -0.5f,    1.0f,0.0f,//右下

            0.5,0.5,  -0.5f,    1.0f,1.0f,//右上

            -0.5,0.5, -0.5f,    0.0f,1.0f,//左上

            -0.5, -0.5, -0.5f,  0.0f,0.0f,//左下

            0.5, -0.5,0.5f,    1.0f,0.0f,//右下

            0.5,0.5,  0.5f,    1.0f,1.0f,//右上

            -0.5,0.5,0.5f,    0.0f,1.0f,//左上

            -0.5, -0.5,0.5f,  0.0f,0.0f,//左下

    //

            //2

            0.5, -0.5, -0.5f,    1.0f,0.0f,//右下

            0.5, -0.5,0.5f,    1.0f,1.0f,//右下

            -0.5, -0.5,0.5f,    0.0f,1.0f,//右下

            -0.5, -0.5, -0.5f,    0.0f,0.0f,//右下

            0.5,0.5, -0.5f,    1.0f,0.0f,//右下

            0.5,0.5,0.5f,    1.0f,1.0f,//右下

            -0.5,0.5,0.5f,    0.0f,1.0f,//右下

            -0.5,0.5, -0.5f,    0.0f,0.0f,//右下

            //3

            0.5, -0.5, -0.5f,    1.0f,0.0f,//右下

            0.5, -0.5,0.5f,    1.0f,1.0f,//右下

            0.5,0.5,0.5f,    0.0f,1.0f,//右下

            0.5,0.5, -0.5f,    0.0f,0.0f,//右下

            -0.5, -0.5, -0.5f,    1.0f,0.0f,//右下

            -0.5, -0.5,0.5f,    1.0f,1.0f,//右下

            -0.5,0.5,0.5f,    0.0f,1.0f,//右下

            -0.5,0.5, -0.5f,    0.0f,0.0f,//右下

        };







    效果


    四。CoreAnimation正方体的大体原理就是一个VIew上放六个imageVIew,并设置imageVIew旋转组成一个立方体,一共6个,最后添加定时器控制view的layer转动,达到效果,因为比较简单,这里不做展示



    作者:枫紫_6174
    链接:https://www.jianshu.com/p/035061d80d5c
    来源:简书
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    收起阅读 »

    OpenGl纹理相关常用API

    一.原始图像数据1.像素包装:                    图像存储空间=图像的宽度*图像的高度*每个像素的字节数二.相关函数(加粗部分表示常用)2....
    继续阅读 »

    一.原始图像数据

    1.像素包装:

                        图像存储空间=图像的宽度*图像的高度*每个像素的字节数

    二.相关函数(加粗部分表示常用)

    2.1  改变像素存储方式----->void glPixelStorei(GLenum pname,GLint param);

            恢复像素存储⽅式----->void glPixelStoref(GLenum pname,GLfloat param);

    参数说明:

                    //参数1:GL_UNPACK_ALIGNMENT 指定OpenGL 如何从数据缓存区中解包图像数据

                    //参数2:表示参数GL_UNPACK_ALIGNMENT 设置的值

                   //GL_UNPACK_ALIGNMENT 指内存中每个像素⾏起点的排列请求

                    允许设置为1 (byte排列)

                                        2(排列为偶数byte的⾏)

                                        4(字word排列)

                                        8(⾏从双字节边界开始)

             举例: glPixelStorei(GL_UNPACK_ALIGNMENT,1);

    2.2  从颜⾊缓存区内容作为像素图直接读取

    void glReadPixels(GLint x,GLint y,GLSizei width,GLSizei height, GLenum format, GLenum type,const void * pixels);

    参数说明:

                    //参数1:x,矩形左下⻆的窗⼝坐标

                    //参数2:y,矩形左下⻆的窗⼝坐标

                    //参数3:width,矩形的宽,以像素为单位

                    //参数4:height,矩形的⾼,以像素为单位

                    //参数5:format,OpenGL 的像素格式,参考 表6-1

                    //参数6:type,解释参数pixels指向的数据,告诉OpenGL 使⽤缓存区中的什么数据类型来存储颜⾊分量,像素数据的数据类型,参考 表6-2

                    //参数7:pixels,指向图形数据的指针

                    glReadBuffer(mode);—> 指定读取的缓存

                    glWriteBuffer(mode);—> 指定写⼊的缓存

    2.3载⼊纹理

                    void glTexImage1D(GLenum target,GLint level,GLint internalformat,GLsizei width,GLint border,GLenum format,GLenum type,void *data);

                    void glTexImage2D(GLenum target,GLint level,GLint internalformat,GLsizei width,GLsizei height,GLint border,GLenum format,GLenum type,void * data);(这个是比较常用的)

                    void glTexImage3D(GLenum target,GLint level,GLint internalformat,GLSizei width,GLsizei height,GLsizei depth,GLint border,GLenum format,GLenum type,void *data);

    参数说明:

                        * target:`GL_TEXTURE_1D`、`GL_TEXTURE_2D`、`GL_TEXTURE_3D`。 

                        * Level:指定所加载的mip贴图层次。⼀般我们都把这个参数设置为0。

                        * internalformat:每个纹理单元中存储多少颜⾊成分。

                        * width、height、depth参数:指加载纹理的宽度、⾼度、深度。==注意!==这些值必须是2的整数次⽅。(这是因为OpenGL 旧版本上的遗留下的⼀个要求。当然现在已经可以⽀持不是2的整数次⽅。但是开发者们还是习惯使⽤以2的整数次⽅去设置这些参数。)

                        * border参数:允许为纹理贴图指定⼀个边界宽度。

                        * format、type、data参数:与我们在讲glDrawPixels 函数对于的参数相同

    2.4更新纹理

                    void glTexSubImage1D(GLenum target,GLint level,GLint xOffset,GLsizei width,GLenum format,GLenum type,const GLvoid *data);

                    void glTexSubImage2D(GLenum target,GLint level,GLint xOffset,GLint yOffset,GLsizei width,GLsizei height,GLenum format,GLenum type,const GLvoid *data);

                    void glTexSubImage3D(GLenum target,GLint level,GLint xOffset,GLint yOffset,GLint zOffset,GLsizei width,GLsizei height,GLsizei depth,Glenum type,const GLvoid * data);

    参数说明:同载入纹理一样

    2.5插入替换纹理

                    void glCopyTexSubImage1D(GLenum target,GLint level,GLint xoffset,GLint x,GLint y,GLsizei width);

                    void glCopyTexSubImage2D(GLenum target,GLint level,GLint xoffset,GLint yOffset,GLint x,GLint y,GLsizei width,GLsizei height);

                    void glCopyTexSubImage3D(GLenum target,GLint level,GLint xoffset,GLint yOffset,GLint zOffset,GLint x,GLint y,GLsizei width,GLsizei height);

    参数说明:同载入纹理一样

    2.6使⽤颜⾊缓存区加载数据,形成新的纹理使⽤

                    void glCopyTexImage1D(GLenum target,GLint level,GLenum internalformt,GLint x,GLint y,GLsizei width,GLint border);

                    void glCopyTexImage2D(GLenum target,GLint level,GLenum internalformt,GLint x,GLint y,GLsizei width,GLsizei height,GLint border);

    特别说明:x,y 在颜⾊缓存区中指定了开始读取纹理数据的位置;缓存区⾥的数据,是源缓存区通过glReadBuffer设置的。注意:不存在glCopyTextImage3D ,因为我们⽆法从2D 颜⾊缓存区中获取体积 数据

    三.纹理对象

    3.1使⽤函数分配纹理对象&&指定纹理对象的数量 和 指针(指针指向⼀个⽆符号整形数组,由纹理对象标识符填充)。

                    void glGenTextures(GLsizei n,GLuint * textTures);

    3.2绑定纹理状态

                    void glBindTexture(GLenum target,GLunit texture);

    参数说明:

                    参数target:GL_TEXTURE_1D、GL_TEXTURE_2D、GL_TEXTURE_3D

                    参数texture:需要绑定的纹理对象

    3.2删除纹理对象

                    void glDeleteTextures(GLsizei n,GLuint *textures);

                    参数说明:同分配纹理对象一样

    3.3测试纹理对象是否有效

                    GLboolean glIsTexture(GLuint texture);

                    说明:如果texture是⼀个已经分配空间的纹理对象,那么这个函数会返回GL_TRUE,否则会返回GL_FALSE。

    3.4设置纹理参数

                    glTexParameterf(GLenum target,GLenum pname,GLFloat param);

                    glTexParameteri(GLenum target,GLenum pname,GLint param);

                    glTexParameterfv(GLenum target,GLenum pname,GLFloat *param);

                    glTexParameteriv(GLenum target,GLenum pname,GLint *param);

    参数说明:

                    参数1:target,指定这些参数将要应⽤在那个纹理模式上,⽐如GL_TEXTURE_1D、GL_TEXTURE_2D、GL_TEXTURE_3D。

                    参数2:pname,指定需要设置那个纹理参数

                    参数3:param,设定特定的纹理参数的值

    3.5过滤方式

            1)邻近过滤(GL_NEAREST)


      说明:当一像素点靠近A时,返回离这个点最近的像素值

            2)线性过滤(GL_LINEAR)













    说明:两种过滤效果本质上没有多大区别,肉眼很难区分的出来,只有当图片放大后,可惜清晰的看清楚两种过滤方式的差别,一般情况下,glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR) 纹理放⼤时,使⽤线性过滤

    3.6设置环绕⽅式

    当纹理坐标超出默认范围时,每个选项都有不同的输出效果



    设置环绕方式;

    glTextParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAR_S,GL_CLAMP_TO_EDGE);

    glTextParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAR_T,GL_CLAMP_TO_EDGE);

    参数说明:

                    参数1:GL_TEXTURE_1D、GL_TEXTURE_2D、GL_TEXTURE_3D

                    参数2:GL_TEXTURE_WRAP_S、GL_TEXTURE_T、GL_TEXTURE_R,针对s,t,r坐标(s->x,t->y,r->z)

                    参数3:GL_REPEAT、GL_CLAMP、GL_CLAMP_TO_EDGE、GL_CLAMP_TO_BORDER

                    GL_REPEAT:OpenGL 在纹理坐标超过1.0的⽅向上对纹理进⾏重复;

                    GL_CLAMP:所需的纹理单元取⾃纹理边界或TEXTURE_BORDER_COLOR.

                    GL_CLAMP_TO_EDGE环绕模式强制对范围之外的纹理坐标沿着合法的纹理单元的最后⼀⾏或者最后⼀列来进⾏采样。

                    GL_CLAMP_TO_BORDER:在纹理坐标在0.0到1.0范围之外的只使⽤边界纹理单元。边界纹理单元是作为围绕基本图像的额外的⾏和列,并与基本纹理图像⼀起加载的。

    3.7OpenGL 像素格式

    常量说明

    GL_RGB                                                         描述红、绿、蓝顺序排列的颜⾊

    GL_RGBA                                                      按照红、绿、蓝、Alpha顺序排列的颜⾊

    GL_BGR                                                         按照蓝、绿、红顺序排列颜⾊

    GL_BGRA                                                       按照蓝、绿、红、Alpha顺序排列颜⾊

    GL_RED                                                         每个像素只包含了⼀个红⾊分量

    GL_GREEN                                                    每个像素只包含了⼀个绿⾊分量

    GL_BLUE                                                       每个像素只包含了⼀个蓝⾊分量

    GL_RG                                                           每个像素依次包含了一个红色和绿色的分量

    GL_RED_INTEGER                                        每个像素包含了一个整数形式的红⾊分量

    GL_GREEN_INTEGER                                   每个像素包含了一个整数形式的绿色分量

    GL_BLUE_INTEGER                                     每个像素包含了一个整数形式的蓝色分量

    GL_RG_INTEGER                                          每个像素依次包含了一个整数形式的红⾊、绿⾊分量

    GL_RGB_INTEGER                                       每个像素包含了一个整数形式的红⾊、蓝⾊、绿色分量

    GL_RGBA_INTEGER                                     每个像素包含了一个整数形式的红⾊、蓝⾊、绿⾊、Alpah分量

    GL_BGR_INTEGER                                        每个像素包含了一个整数形式的蓝⾊、绿⾊、红色分量

    GL_BGRA_INTEGER                                     每个像素包含了一个整数形式的蓝⾊、绿⾊、红色、Alpah分量

    GL_STENCIL_INDEX                                    每个像素只包含了一个模板值

    GL_DEPTH_COMPONENT                          每个像素只包含一个深度值

    GL_DEPTH_STENCIL                                 每个像素包含一个深度值和一个模板值

    3.8像素数据的数据类型

    GL_UNSIGNED_BYTE                        每种颜色分量都是一个8位无符号整数

    GL_BYTE                                            8位有符号整数

    GL_UNSIGNED_SHORT                    16位无符号整数

    GL_SHORT                                         16位有符号整数

    CL_UNSIGNED_INT                            32位无符号整数

    GL_INT                                               32位有符号整数

    GL_FLOAT                                        单精度浮点数

    GL_HALF_FLOAT                                半精度浮点数

    GL_UNSIGNED_BYTE_3_2_3            包装的RGB值

    GL_UNSIGNED_BYTE_2_3_3_REV    包装的RGB值

    GL_UNSIGNED_SHORT_5_6_5         包装的RGB值

    GL_UNSIGNED_SHORT_5_6_5_REV  包装的RGB值

    GL_UNSIGNED_SHORT_4_4_4_4      包装的RGB值

    GL_UNSIGNED_SHORT_4_4_4_4_REV   包装的RGB值

    GL_UNSIGNED_SHORT_5_5_5_1        包装的RGB值

    GL_UNSIGNED_SHORT_1_5_5_5_REV   包装的RGB值

    GL_UNSIGNED_INT_8_8_8_8               包装的RGB值

    GL_UNSIGNED_INT_8_8_8_8_REV      包装的RGB值

    GL_UNSIGNED_INT_10_10_10_2       包装的RGB值

    GL_UNSIGNED_INT_2_10_10_10_REV   包装的RGB值

    GL_UNSIGNED_INT_24_8                   包装的RGB值

    GL_UNSIGNED_INT_10F_11F_REV       包装的RGB值

    GL_FLOAT_24_UNSIGNED_INT_24_8_REV     包装的RGB值




    作者:枫紫
    链接:https://www.jianshu.com/p/bea1fd229b18


    收起阅读 »

    iOS---webView相关及原生和web的交互

    webView的基本应用,监听加载进度,返回上一页,异常处理web调用原生:处理跳转到指定的原生页面,拦截跳转其他app,添加app白名单,拦截通用链接跳转,js注入,关闭webView原生调用web:获取webView的标题等web原生互相调用:web获取a...
    继续阅读 »


  • webView的基本应用,监听加载进度,返回上一页,异常处理
  • web调用原生:处理跳转到指定的原生页面,拦截跳转其他app,添加app白名单,拦截通用链接跳转,js注入,关闭webView
  • 原生调用web:获取webView的标题等
  • web原生互相调用:web获取app当前的id、token等用户信息
  • 微信web里打开原生app
  • 一、webView的基本应用

    现在基本每个app都会或多或少用到web来实现快速迭代。正常都会将其封装在一个控制器里,以使其样式、功能统一
    (iOS8引入了WKWebView,使用独立的进程渲染web,解决了之前UIWebView内存泄漏和crash率高等被诟病已久的问题,所以现在基本都是用WKWebView了)


        //如果不考虑和原生的交互
    _webView = [[WKWebView alloc] initWithFrame:CGRectZero];
    [self.view addSubview:_webView];
    [_webView mas_makeConstraints:^(MASConstraintMaker *make) {
    make.edges.equalTo(self.view);
    }];
    _webView.UIDelegate = self;
    _webView.navigationDelegate = self;
    [_webView loadRequest:[NSURLRequest requestWithURL:URL]];//这里的url是经过校检的

    如果要监听webview的加载进度

        //kvo监听
    [_webView addObserver:self forKeyPath:@"estimatedProgress" options:0 context:nil];

    //创建加载进度条UIProgressView
    {
    init progressView
    }

    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if (object == _webView && [keyPath isEqualToString:NSStringFromSelector(@selector(estimatedProgress))]) {
    self.progressView.alpha = 1.0f;
    BOOL animated = _webView.estimatedProgress > self.progressView.progress;
    [self.progressView setProgress:_webView.estimatedProgress animated:animated];

    if (_webView.estimatedProgress >= 1.0f) {
    [UIView animateWithDuration:0.3f delay:0.3f options:UIViewAnimationOptionCurveEaseOut animations:^{
    self.progressView.alpha = 0.0f;
    } completion:^(BOOL finished) {}];
    }
    } else {
    [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
    }
    返回上一页

            //kvo监听
    [_webView addObserver:self forKeyPath:@"canGoBack" options:0 context:nil];//监听是否有上一页

    //configBackButton里判断canGoBack,如果不可以返回就将按钮置灰或者隐藏
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if (object == _webView && [keyPath isEqual: @"canGoBack"]) {
    [self configBackButton];
    }
    }
    - (void)webView:(WKWebView *)webView didCommitNavigation:(WKNavigation *)navigation {
    [self configBackButton];
    }

    - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
    [self configBackButton];
    }

    //按钮事件
    if ([weakSelf.webView canGoBack]) {
    [weakSelf.webView goBack];
    }
    当 WKWebView 总体内存占用过大,页面即将白屏的时候,系统会调用下面的回调函数,我们在这里执行[webView reload]解决白屏问题

    - (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView {

    [webView reload];
    }

    二、web调用原生

    1.这三个代理方法是可以接收到web的调用比如 window.prompt("xxx")

    - (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler;

    - (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL result))completionHandler;

    - (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(nullable NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * _Nullable result))completionHandler;
    1. 在发送请求之前,决定是否跳转
    - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler{
    NSURL *url = navigationAction.request.URL;
    //可以在这里处理一些跳转,比如通过scheme来处理跳转到指定的原生页面(xxx://xxx),拦截跳转其他app,添加app白名单,拦截通用链接跳转等等

    //比如
    if ([@"mailto" isEqualToString:url.scheme] || [@"tel" isEqualToString:url.scheme]) {//系统scheme
    if ([[UIApplication sharedApplication] canOpenURL:url]) {
    [[UIApplication sharedApplication] openURL:url];
    }
    decisionHandler(WKNavigationActionPolicyCancel);
    } if ([@"xxxx" isEqualToString:url.scheme]) {
    // 如果该scheme是你定义好的scheme,可以根据后面的参数去处理跳转到app内的指定页面,或者其他操作
    decisionHandler(WKNavigationActionPolicyCancel);
    }else if ([scheme白名单 containsObject:url.scheme]) {//白名单
    // 打开scheme
    [[UIApplication sharedApplication] openURL:url];
    decisionHandler(WKNavigationActionPolicyCancel);
    } else {
    BOOL canOpenUniversalUrl = NO;
    for (NSString *str in universalLink白名单) {
    if ([url.absoluteString rangeOfString:str].location != NSNotFound) {
    canOpenUniversalUrl = YES;
    break;
    }
    }
    if (canOpenUniversalUrl) {
    // 打开通用链接
    decisionHandler(WKNavigationActionPolicyAllow);
    } else {
    // Default 可以正常访问网页,但禁止打开通用链接
    decisionHandler(WKNavigationActionPolicyAllow+2);
    }
    }
    }
    web只需
    window.location.href = "xxx"//这里的地址就是上方代理方法的url
    WKWebView可以使用WKScriptMessageHandler来实现JS调用原生方法
    首先初始化的时候,这里拿最常用的web调用关闭webView:xxx_close举例(也可以用上边的href的scheme方式实现,但不太合理)
        _webView = [[WKWebView alloc] initWithFrame:CGRectZero configuration:[self configWKConfiguration]];

    // config,js注入
    - (WKWebViewConfiguration *)configWKConfiguration {
    WKWebViewConfiguration* webViewConfig = [WKWebViewConfiguration new];
    WKUserContentController *userContentController = [WKUserContentController new];
    //这里如果用的不多,可以不用单独写一个js文件,直接用字符串就行了
    NSString *jsStr = [NSString stringWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"js文件地址"] encoding:NSUTF8StringEncoding error:nil];
    WKUserScript *userScript = [[WKUserScript alloc] initWithSource:jsStr injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES];
    [userContentController addUserScript:userScript];
    [userContentController addScriptMessageHandler:self name:"closeWebView"];

    webViewConfig.userContentController = userContentController;
    webViewConfig.preferences = [[WKPreferences alloc] init];
    webViewConfig.preferences.javaScriptEnabled = YES;
    return webViewConfig;
    }

    //app里的js文件里实现
    var xxx = {
    close: function() {
    window.webkit.messageHandlers.closeWebView.postMessage(null);
    },
    }

    //在这里能收到回调
    - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{
    if ([message.name isEqualToString:@"closeWebView"]) {
    // 关闭页面
    [self.navigationController popViewControllerAnimated:YES];
    }
    }

    web中只需
          try {
    window.xxx.close();
    } catch (err) {}

    三、原生调用web,就是app调用web里的js方法

    1.比较常用的一种,获取webView的标题
    //也可以用正则去获取标题、图片之类的

        [webView evaluateJavaScript:@"document.title" completionHandler:^(id result, NSError * _Nullable error) {
    }];

    四. web原生互相调用

    比如一个场景,在web里获取app当前登录的账号id

    1. 首先像上边一样,通过js注入的方式web向app发送getUserId请求,app也要同步处理

    //web代码

          try {
    window.xxx.getUserId();//这里可以直接加返回值,但是app内的js没办法直接去获取原生用户信息这些变量,所以还是要通过原生的代理去实现
    } catch (err) {}
    1. 这时候app接收到这个请求,但还要将userId告诉web

    - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
    if ([message.name isEqualToString:"getUserId"]){
    NSDictionary *dict = xxx;//因为这个过程是异步的,所以这里最好多加一点信息,以便于web确认该结果是上边请求的返回
    NSString * jsMethod = [NSString stringWithFormat:@"window.getUserId(%@)",[dict yy_modelToJSONString]];
    [webView evaluateJavaScript:@"xxx" completionHandler:^(id result, NSError * _Nullable error) {
    }];
    }
    }

    1. web需要将getUserId方法挂载到window上,算是自定义回调,将上一步生成的用户信息dic当成参数传进来,然后去处理接收到的信息

    //web代码

        window["getUserId"] = function(value) {
    //在这里解析处理接收到的用户信息
    };

    五. web如何在微信里打开原生?

    普通的scheme跳转被微信给禁了,所以现在基本都是通过universalLink通用链接的方式,设置universalLink的方式网上有好多,另外通用链接可以设置多个,最好设置两个以上(因为这里有个隐藏的坑:web的域名不能和universalLink一样,否则无法跳转)
    web代码:


    window.location.href = '通用链接://具体落地页'//可以通过参数跳转到具体的页面
    作者:Theendisthebegi
    链接:https://www.jianshu.com/p/d66d694b762f










    收起阅读 »

    iOS开发宏定义整理

    宏定义今天整理一些自己的项目里零零碎碎的东西,发现有些东西太杂太乱,就是定义的全局.这里一个宏,那边一个#define,发现这东西会左右引用,很影响性能下面分开介绍各种宏:Macros.h这里面就放各各宏的头文件,然后在PCH文件中引用着这个一个头文件就OK#...
    继续阅读 »

    宏定义

    今天整理一些自己的项目里零零碎碎的东西,发现有些东西太杂太乱,就是定义的全局.这里一个宏,那边一个#define,发现这东西会左右引用,很影响性能

    下面分开介绍各种宏:

    • Macros.h

    这里面就放各各宏的头文件,然后在PCH文件中引用着这个一个头文件就OK
    #import "DimensMacros.h"
    #import "UtilsMacros.h"
    #import "PathMacros.h"
    #import "NotificationMacros.h"
    #import "APIStringMacros.h"

    • APIStringMacros_h(服务端API接口的宏)

    这里面主要放一些API相关的东西:比如你请求网络的接口hostname,port还有一些第三方的关键字段:极光推送的appkey....

    • DimensMacros.h (定义尺寸类的宏)

    这里面定义一些尺寸相关的宏:

    #pragma mark - 系统UI
    #define kNavigationBarHeight 44
    #define kStatusBarHeight 20
    #define kTopBarHeight 64
    #define kToolBarHeight 44
    #define kTabBarHeight 49
    #define kiPhone4_W 320
    #define kiPhone4_H 480
    #define kiPhone5_W 320
    #define kiPhone5_H 568
    #define kiPhone6_W 375
    #define kiPhone6_H 667
    #define kiPhone6P_W 414
    #define kiPhone6P_H 736
    /*** 当前屏幕宽度 */
    #define kScreenWidth [[UIScreen mainScreen] bounds].size.width
    /*** 当前屏幕高度 */
    #define kScreenHeight [[UIScreen mainScreen] bounds].size.height
    /*** 普通字体 */
    #define kFont(size) [UIFont systemFontOfSize:size]
    /*** 粗体 */
    #define kBoldFont(size) [UIFont boldSystemFontOfSize:size]
    #define kLineHeight (1 / [UIScreen mainScreen].scale)
    • NotificationMacros.h(通知Notification相关宏)

    这里面放一些关于通知定义的宏

    #define TNCancelFavoriteProductNotification     @"TNCancelFavoriteProductNotification"      //取消收藏时
    #define TNMarkFavoriteProductNotification @"TNMarkFavoriteProductNotification" //标记收藏时

    #define kNotficationDownloadProgressChanged @"kNotficationDownloadProgressChanged" //下载进度变化
    #define kNotificationPauseDownload @"kNotificationPauseDownload" //暂停下载
    #define kNotificationStartDownload @"kNotificationStartDownload" //开始下载

    #define kNotificationDownloadSuccess @"kNotificationDownloadSuccess" //下载成功
    #define kNotificationDownloadFailed @"kNotificationDownloadFailed" //下载失败
    #define kNotificationDownloadNewMagazine @"kNotificationDownloadNewMagazine"
    • UtilsMacros_h(工具类的宏)

    这里面存放一些方便开发的工具:颜色,打印,单利,版本...
    // 日志输出
    #ifdef DEBUG
    #define LMLog(fmt, ...) NSLog((@"%s [Line %d] " fmt), PRETTY_FUNCTIONLINE, ##VA_ARGS);
    #else
    #define LMLog(...)
    #endif

    #define WeakSelf(weakSelf)  __weak __typeof(&*self)weakSelf = self;

    #pragma mark - 颜色
    #define kWhiteColor [UIColor whiteColor]
    #define kBlackColor [UIColor blackColor]
    #define kDarkGrayColor [UIColor darkGrayColor]
    #define kLightGrayColor [UIColor lightGrayColor]
    #define kGrayColor [UIColor grayColor]
    #define kRedColor [UIColor redColor]
    #define kGreenColor [UIColor greenColor]
    #define kBlueColor [UIColor blueColor]
    #define kCyanColor [UIColor cyanColor]
    #define kYellowColor [UIColor yellowColor]
    #define kMagentaColor [UIColor magentaColor]
    #define kOrangeColor [UIColor orangeColor]
    #define kPurpleColor [UIColor purpleColor]
    #define kBrownColor [UIColor brownColor]
    #define kClearColor [UIColor clearColor]

    //16进制
    #define LMColorFromHex(s) [UIColor colorWithRed:(((s & 0xFF0000) >> 16))/255.0green:(((s &0xFF00) >>8))/255.0blue:((s &0xFF))/255.0alpha:1.0]
    //RGB
    #define kRGBAColor(r,g,b,a) [UIColor colorWithRed:r/255.0f green:g/255.0f blue:b/255.0f alpha:a]
    #define kRGBColor(r,g,b) kRGBAColor(r,g,b,1.0f)
    #define kSeperatorColor kRGBColor(234,237,240)
    #define kBgColor kRGBColor(243,245,247)

    #define krgbaColor(r,g,b,a) [UIColor colorWithRed:r green:g blue:b alpha:a]
    #define krgbColor(r,g,b) krgbColor(r,g,b,1.0f)

    #define kCommonHighLightRedColor krgbColor(1.00f,0.49f,0.65f)
    #define kCommonGrayTextColor krgbColor(0.63f,0.63f,0.63f)
    #define kCommonRedColor krgbColor(0.91f,0.33f,0.33f)
    #define kCommonBlackColor krgbColor(0.17f,0.23f,0.28f)
    #define kCommonTintColor krgbColor(0.42f,0.33f,0.27f)
    #define kCommonBgColor krgbColor(0.86f,0.85f,0.80f)
    #define kDetailTextColor krgbColor(0.56f,0.60f,0.62f)
    #define kLineBgColor krgbColor(0.86f,0.88f,0.89f)
    #define kTextColor krgbColor(0.32f,0.36f,0.40f)


    #define kVersion [NSString stringWithFormat:@"%@",[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"]]

    //System version utils

    #define SYSTEM_VERSION_EQUAL_TO(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] == NSOrderedSame)
    #define SYSTEM_VERSION_GREATER_THAN(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] == NSOrderedDescending)
    #define SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] != NSOrderedAscending)
    #define SYSTEM_VERSION_LESS_THAN(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] == NSOrderedAscending)
    #define SYSTEM_VERSION_LESS_THAN_OR_EQUAL_TO(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] != NSOrderedDescending)

    //大于等于7.0的ios版本
    #define iOS7_OR_LATER SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"7.0")

    //大于等于8.0的ios版本
    #define iOS8_OR_LATER SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"8.0")

    //iOS6时,导航VC中view的起始高度
    #define YH_HEIGHT (iOS7_OR_LATER ? 64:0)

    //获取系统时间戳
    #define getCurentTime [NSString stringWithFormat:@"%ld", (long)[[NSDate date] timeIntervalSince1970]]

    #define kWindow [UIApplication sharedApplication].keyWindow //主窗口
    #define kUserDefault [NSUserDefaults standardUserDefaults]

    #pragma mark - 字符串转化
    #define kEmptyStr @""
    #define kIntToStr(i) [NSString stringWithFormat: @"%d", i]
    #define kIntegerToStr(i) [NSString stringWithFormat: @"%ld", i]
    #define kValidStr(str) [NHUtils validString:str]

    #pragma mark - 单利
    #define SingletonH(methodName) + (instancetype)shared##methodName;
    // .m文件的实现
    #if __has_feature(objc_arc) // 是ARC
    #define SingletonM(methodName) \
    static id _instace = nil; \
    + (id)allocWithZone:(struct _NSZone *)zone \
    { \
    if (_instace == nil) { \
    static dispatch_once_t onceToken; \
    dispatch_once(&onceToken, ^{ \
    _instace = [super allocWithZone:zone]; \
    }); \
    } \
    return _instace; \
    } \
    \
    - (id)init \
    { \
    static dispatch_once_t onceToken; \
    dispatch_once(&onceToken, ^{ \
    _instace = [super init]; \
    }); \
    return _instace; \
    } \
    \
    + (instancetype)shared##methodName \
    { \
    return [[self alloc] init]; \
    } \
    + (id)copyWithZone:(struct _NSZone *)zone \
    { \
    return _instace; \
    } \
    \
    + (id)mutableCopyWithZone:(struct _NSZone *)zone \
    { \
    return _instace; \
    }

    #else // 不是ARC

    #define SingletonM(methodName) \
    static id _instace = nil; \
    + (id)allocWithZone:(struct _NSZone *)zone \
    { \
    if (_instace == nil) { \
    static dispatch_once_t onceToken; \
    dispatch_once(&onceToken, ^{ \
    _instace = [super allocWithZone:zone]; \
    }); \
    } \
    return _instace; \
    } \
    \
    - (id)init \
    { \
    static dispatch_once_t onceToken; \
    dispatch_once(&onceToken, ^{ \
    _instace = [super init]; \
    }); \
    return _instace; \
    } \
    \
    + (instancetype)shared##methodName \
    { \
    return [[self alloc] init]; \
    } \
    \
    - (oneway void)release \
    { \
    \
    } \
    \
    - (id)retain \
    { \
    return self; \
    } \
    \
    - (NSUInteger)retainCount \
    { \
    return 1; \
    } \
    + (id)copyWithZone:(struct _NSZone *)zone \
    { \
    return _instace; \
    } \
    \
    + (id)mutableCopyWithZone:(struct _NSZone *)zone \
    { \
    return _instace; \
    }

    *PathMacros.h(沙河路径宏)

    这里面是一些沙河路径,还有一些plist路径
    //文件目录
    #define kPathTemp NSTemporaryDirectory()
    #define kPathDocument [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0]
    #define kPathCache [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0]
    #define kPathSearch [kPathDocument stringByAppendingPathComponent:@"Search.plist"]

    #define kPathMagazine               [kPathDocument stringByAppendingPathComponent:@"Magazine"]
    #define kPathDownloadedMgzs [kPathMagazine stringByAppendingPathComponent:@"DownloadedMgz.plist"]
    #define kPathDownloadURLs [kPathMagazine stringByAppendingPathComponent:@"DownloadURLs.plist"]
    #define kPathOperation [kPathMagazine stringByAppendingPathComponent:@"Operation.plist"]

    #define kPathSplashScreen [kPathCache stringByAppendingPathComponent:@"splashScreen"]

    这样导入宏,简单明了



    作者:Cooci
    链接:https://www.jianshu.com/p/db4f67e56214

    收起阅读 »

    iOS开发必备 - iOS 的锁

    这次主要想解决这些疑问:锁是什么?为什么要有锁?锁的分类问题为什么 OSSpinLock 不安全?解决自旋锁不安全问题有几种方式为什么换用其它的锁,可以解决 OSSpinLock 的问题?自旋锁和互斥锁的关系是平行对立的吗?信号量和互斥量的关系信号量和条件变量...
    继续阅读 »

    这次主要想解决这些疑问:

      1. 锁是什么?
      1. 为什么要有锁?
      1. 锁的分类问题
      1. 为什么 OSSpinLock 不安全?
      1. 解决自旋锁不安全问题有几种方式
      1. 为什么换用其它的锁,可以解决 OSSpinLock 的问题?
      1. 自旋锁和互斥锁的关系是平行对立的吗?
      1. 信号量和互斥量的关系
      1. 信号量和条件变量的区别


    锁是什么

    锁 -- 是保证线程安全常见的同步工具。锁是一种非强制的机制,每一个线程在访问数据或者资源前,要先获取(Acquire) 锁,并在访问结束之后释放(Release)锁。如果锁已经被占用,其它试图获取锁的线程会等待,直到锁重新可用。

    为什么要有锁?

    前面说到了,锁是用来保护线程安全的工具。

    可以试想一下,多线程编程时,没有锁的情况 -- 也就是线程不安全。

    当多个线程同时对一块内存发生读和写的操作,可能出现意料之外的结果:

    程序执行的顺序会被打乱,可能造成提前释放一个变量,计算结果错误等情况。

    所以我们需要将线程不安全的代码 “锁” 起来。保证一段代码或者多段代码操作的原子性,保证多个线程对同一个数据的访问 同步 (Synchronization)

    属性设置 atomic

    上面提到了原子性,我马上想到了属性关键字里, atomic 的作用。

    设置 atomic 之后,默认生成的 getter 和 setter 方法执行是原子的。

    但是它只保证了自身的读/写操作,却不能说是线程安全。

    如下情况:

    //thread A
    for (int i = 0; i < 100000; i ++) {
    if (i % 2 == 0) {
    self.arr = @[@"1", @"2", @"3"];
    }else {
    self.arr = @[@"1"];
    }
    NSLog(@"Thread A: %@\n", self.arr);
    }

    //thread B
    if (self.arr.count >= 2) {
    NSString* str = [self.arr objectAtIndex:1];
    }

    就算在 thread B 中针对 arr 数组进行了大小判断,但是仍然可能在 objectAtIndex: 操作时被改变数组长度,导致出错。这种情况声明为 atomic 也没有用。

    而解决方式,就是进行加锁。

    需要注意的是,读/写的操作都需要加锁,不仅仅是对一段代码加锁。

    锁的分类

    锁的分类方式,可以根据锁的状态,锁的特性等进行不同的分类,很多锁之间其实并不是并列的关系,而是一种锁下的不同实现。关于锁的分类,可以参考 Java中的锁分类 看一下。

    自旋锁和互斥锁的关系

    很多谈论锁的文章,都会提到互斥锁,自旋锁。很少有提到它们的关系,其实自旋锁,也是互斥锁的一种实现,而 spin lock和 mutex 两者都是为了解决某项资源的互斥使用,在任何时刻只能有一个保持者。

    区别在于 spin lock和 mutex 调度机制上有所不同。

    OSSpinLock

    OSSpinLock 是一种自旋锁。它的特点是在线程等待时会一直轮询,处于忙等状态。自旋锁由此得名。

    自旋锁看起来是比较耗费 cpu 的,然而在互斥临界区计算量较小的场景下,它的效率远高于其它的锁。

    因为它是一直处于 running 状态,减少了线程切换上下文的消耗。

    为什么 OSSpinLock 不再安全?

    关于 OSSpinLock 不再安全,原因就在于优先级反转问题。

    优先级反转(Priority Inversion)

    什么情况叫做优先级反转?

    wikipedia 上是这么定义的:

    优先级倒置,又称优先级反转、优先级逆转、优先级翻转,是一种不希望发生的任务调度状态。在该种状态下,一个高优先级任务间接被一个低优先级任务所抢先(preemtped),使得两个任务的相对优先级被倒置。 这往往出现在一个高优先级任务等待访问一个被低优先级任务正在使用的临界资源,从而阻塞了高优先级任务;同时,该低优先级任务被一个次高优先级的任务所抢先,从而无法及时地释放该临界资源。这种情况下,该次高优先级任务获得执行权。

    再消化一下

    有:高优先级任务A / 次高优先级任务B / 低优先级任务C / 资源Z 。
    A 等待 C 执行后的 Z
    而 B 并不需要 Z,抢先获得时间片执行
    C 由于没有时间片,无法执行(优先级相对没有B高)。
    这种情况造成 A 在C 之后执行,C在B之后,间接的高优先级A在次高优先级任务B 之后执行, 使得优先级被倒置了。(假设: A 等待资源时不是阻塞等待,而是忙循环,则可能永远无法获得资源。此时 C 无法与 A 争夺 CPU 时间,从而 C 无法执行,进而无法释放资源。造成的后果,就是 A 无法获得 Z 而继续推进。)

    而 OSSpinLock 忙等的机制,就可能造成高优先级一直 running ,占用 cpu 时间片。而低优先级任务无法抢占时间片,变成迟迟完不成,不释放锁的情况。

    优先级反转的解决方案

    关于优先级反转一般有以下三种解决方案

    优先级继承

    优先级继承,故名思义,是将占有锁的线程优先级,继承等待该锁的线程高优先级,如果存在多个线程等待,就取其中之一最高的优先级继承。

    优先级天花板

    优先级天花板,则是直接设置优先级上限,给临界区一个最高优先级,进入临界区的进程都将获得这个高优先级。

    如果其他试图进入临界区的进程的优先级,都低于这个最高优先级,那么优先级反转就不会发生。

    禁止中断

    禁止中断的特点,在于任务只存在两种优先级:可被抢占的 / 禁止中断的 。

    前者为一般任务运行时的优先级,后者为进入临界区的优先级。

    通过禁止中断来保护临界区,没有其它第三种的优先级,也就不可能发生反转了。

    为什么使用其它的锁,可以解决优先级反转?

    我们看到很多本来使用 OSSpinLock 的知名项目,都改用了其它方式替代,比如 pthread_mutex 和 dispatch_semaphore 。

    那为什么其它的锁,就不会有优先级反转的问题呢?如果按照上面的想法,其它锁也可能出现优先级反转。

    原因在于,其它锁出现优先级反转后,高优先级的任务不会忙等。因为处于等待状态的高优先级任务,没有占用时间片,所以低优先级任务一般都能进行下去,从而释放掉锁。

    线程调度

    为了帮助理解,要提一下有关线程调度的概念。

    无论多核心还是单核,我们的线程运行总是 "并发" 的。

    当 cpu 数量大于等于线程数量,这个时候是真正并发,可以多个线程同时执行计算。

    当 cpu 数量小于线程数量,总有一个 cpu 会运行多个线程,这时候"并发"就是一种模拟出来的状态。操作系统通过不断的切换线程,每个线程执行一小段时间,让多个线程看起来就像在同时运行。这种行为就称为 "线程调度(Thread Schedule)"

    线程状态

    在线程调度中,线程至少拥有三种状态 : 运行(Running),就绪(Ready),等待(Waiting)

    处于 Running的线程拥有的执行时间,称为 时间片(Time Slice),时间片 用完时,进入Ready状态。如果在Running状态,时间片没有用完,就开始等待某一个事件(通常是 IO 或 同步 ),则进入Waiting状态。

    如果有线程从Running状态离开,调度系统就会选择一个Ready的线程进入 Running 状态。而Waiting的线程等待的事件完成后,就会进入Ready状态。

    dispatch_semaphore

    dispatch_semaphore 是 GCD 中同步的一种方式,与他相关的只有三个函数,一个是创建信号量,一个是等待信号,一个是发送信号。

    信号量机制

    信号量中,二元信号量,是一种最简单的锁。只有两种状态,占用和非占用。二元信号量适合唯一一个线程独占访问的资源。而多元信号量简称 信号量(Semaphore)。

    信号量和互斥量的区别

    信号量是允许并发访问的,也就是说,允许多个线程同时执行多个任务。信号量可以由一个线程获取,然后由不同的线程释放。

    互斥量只允许一个线程同时执行一个任务。也就是同一个程获取,同一个线程释放。

    之前我对,互斥量只由一个线程获取和释放,理解的比较狭义,以为这里的获取和释放,是系统强制要求的,用 NSLock 实验发现它可以在不同线程获取和释放,感觉很疑惑。

    实际上,的确能在不同线程获取/释放同一个互斥锁,但互斥锁本来就用于同一个线程中上锁和解锁。这里的意义更多在于代码使用的层面。

    关键在于,理解信号量可以允许 N 个信号量允许 N 个线程并发地执行任务。

    @synchonized

    @synchonized 是一个递归锁。

    递归锁

    递归锁也称为可重入锁。互斥锁可以分为非递归锁/递归锁两种,主要区别在于:同一个线程可以重复获取递归锁,不会死锁; 同一个线程重复获取非递归锁,则会产生死锁。

    因为是递归锁,我们可以写类似这样的代码:


    - (void)testLock{
    if(_count>0){
    @synchronized (obj) {
    _count = _count - 1;
    [self testLock];
    }
    }
    }

    而如果换成NSLock,它就会因为递归发生死锁了。

    实际使用问题

    如果obj 为 nil,或者 obj地址不同,锁会失效。

    所以我们要防止如下的情况:

    @synchronized (obj) {
    obj = newObj;
    }

    这里的 obj 被更改后,等到其它线程访问时,就和没加锁一样直接进去了。

    另外一种情况,就是 @synchonized(self). 不少代码都是直接将self传入@synchronized当中,而 self 很容易作为一个外部对象,被调用和修改。所以它和上面是一样的情况,需要避免使用。

    正确的做法是什么?obj 应当传入一个类内部维护的NSObject对象,而且这个对象是对外不可见的,不被随便修改的。

    pthread_mutex

    pthread定义了一组跨平台的线程相关的 API,其中可以使用 pthread_mutex作为互斥锁。

    pthread_mutex 不是使用忙等,而是同信号量一样,会阻塞线程并进行等待,调用时进行线程上下文切换。

    pthread_mutex` 本身拥有设置协议的功能,通过设置它的协议,来解决优先级反转:

    pthread_mutexattr_setprotocol(pthread_mutexattr_t *attr, int protocol)

    其中协议类型包括以下几种:

    • PTHREAD_PRIO_NONE:线程的优先级和调度不会受到互斥锁拥有权的影响。
    • PTHREAD_PRIO_INHERIT:当高优先级的等待低优先级的线程锁定互斥量时,低优先级的线程以高优先级线程的优先级运行。这种方式将以继承的形式传递。当线程解锁互斥量时,线程的优先级自动被降到它原来的优先级。该协议就是支持优先级继承类型的互斥锁,它不是默认选项,需要在程序中进行设置。
    • PTHREAD_PRIO_PROTECT:当线程拥有一个或多个使用 PTHREAD_PRIO_PROTECT初始化的互斥锁时,此协议值会影响其他线程(如 thrd2)的优先级和调度。thrd2 以其较高的优先级或者以thrd2 拥有的所有互斥锁的最高优先级上限运行。基于被thrd2拥有的任一互斥锁阻塞的较高优先级线程对于 thrd2的调度没有任何影响。

    设置协议类型为 PTHREAD_PRIO_INHERIT ,运用优先级继承的方式,可以解决优先级反转的问题。

    而我们在 iOS 中使用的 NSLock,NSRecursiveLock等都是基于pthread_mutex 做实现的。

    NSLock

    NSLock属于 pthread_mutex的一层封装, 设置了属性为 PTHREAD_MUTEX_ERRORCHECK 。

    它会损失一定性能换来错误提示。并简化直接使用 pthread_mutex 的定义。

    NSCondition

    NSCondition是通过pthread中的条件变量(condition variable) pthread_cond_t来实现的。

    条件变量

    在线程间的同步中,有这样一种情况: 线程 A 需要等条件 C 成立,才能继续往下执行.现在这个条件不成立,线程 A 就阻塞等待. 而线程 B 在执行过程中,使条件 C 成立了,就唤醒线程 A 继续执行。

    对于上述情况,可以使用条件变量来操作。

    条件变量,类似信号量,提供线程阻塞与信号机制,可以用来阻塞某个线程,等待某个数据就绪后,随后唤醒线程。

    一个条件变量总是和一个互斥量搭配使用。

    NSCondition其实就是封装了一个互斥锁和条件变量,互斥锁的lock/unlock方法和后者的wait/signal统一封装在 NSCondition对象中,暴露给使用者。

    用条件变量控制线程同步,最为经典的例子就是 生产者-消费者问题。

    生产者-消费者问题

    生产者消费者问题,是一个著名的线程同步问题,该问题描述如下:

    有一个生产者在生产产品,这些产品将提供给若干个消费者去消费。要求让生产者和消费者能并发执行,在两者之间设置一个具有多个缓冲区的缓冲池,生产者将它生产的产品放入一个缓冲区中,消费者可以从缓冲区中取走产品进行消费,显然生产者和消费者之间必须保持同步,即不允许消费者到一个空的缓冲区中取产品,也不允许生产者向一个已经放入产品的缓冲区中再次投放产品。

    我们可以刚好可以使用 NSCondition解决生产者-消费者问题。具体的代码放置在文末的 Demo 里了。

    if(count==0){
    [condition wait];
    }

    上面这样是不能保证消费者是线程安全的。

    因为NSCondition可以给每个线程分别加锁,但加锁后不影响其他线程进入临界区。所以 NSCondition使用 wait并加锁后,并不能真正保证线程的安全。

    当一个signal操作发出时,如果有两个线程都在做 消费者 操作,那同时都会消耗掉资源,于是绕过了检查。

    例如我们的条件是,count == 0 执行等待。

    假设当前 count = 0,线程A 要判断到 count == 0,执行等待;

    线程B 执行了count = 1,并唤醒线程A 执行 count - 1,同时线程C 也判断到 count > 0 。因为处在不同的线程锁,同样判断执行了 count - 1。2 个线程都会执行count - 1,但是 count = 1,实际就出现count = -1的情况。

    所以为了保证消费者操作的正确,使用 while 循环中的判断,进行二次确认:


     while (count == 0) {
    [condition wait];
    }

    条件变量和信号量的区别

    每个信号量有一个与之关联的值,发出时+1,等待时-1,任何线程都可以发出一个信号,即使没有线程在等待该信号量的值。

    可是对于条件变量,例如 pthread_cond_signal发出信号后,没有任何线程阻塞在 pthread_cond_wait 上,那这个条件变量上的信号会直接丢失掉。

    NSConditionLock

    NSConditionLock称为条件锁,只有 condition 参数与初始化时候的 condition相等,lock才能正确进行加锁操作。

    这里分清两个概念:

    • unlockWithCondition:,它是先解锁,再修改 condition 参数的值。 并不是当 condition 符合某个件值去解锁。
    • lockWhenCondition:,它与 unlockWithCondition: 不一样,不会修改 condition 参数的值,而是符合 condition 的值再上锁。

    在这里可以利用 NSConditionLock实现任务之间的依赖.

    NSRecursiveLock

    NSRecursiveLock 和前面提到的 @synchonized一样,是一个递归锁。

    NSRecursiveLock 与 NSLock 的区别在于内部封装的pthread_mutex_t 对象的类型不同,NSRecursiveLock 的类型被设置为 PTHREAD_MUTEX_RECURSIVE

    NSDistributedLock

    这里顺带提一下 NSDistributedLock, 是 macOS 下的一种锁.

    苹果文档 对于NSDistributedLock 的描述是:

    A lock that multiple applications on multiple hosts can use to restrict access to some shared resource, such as a file

    意思是说,它是一个用在多个主机间的多应用的锁,可以限制访问一些共享资源,例如文件。

    按字面意思翻译,NSDistributedLock 应该就叫做 分布式锁。但是看概念和资料,在 解决NSDistributedLock进程互斥锁的死锁问题(一) 里面看到,NSDistributedLock 更类似于文件锁的概念。 有兴趣的可以看一看 Linux 2.6 中的文件锁

    其它保证线程安全的方式

    除了用锁之外,有其它方法保证线程安全吗?

    使用单线程访问

    首先,尽量避免多线程的设计。因为多线程访问会出现很多不可控制的情况。有些情况即使上锁,也无法保证百分之百的安全,例如自旋锁的问题。

    不对资源做修改

    而如果还是得用多线程,那么避免对资源做修改。

    如果都是访问共享资源,而不去修改共享资源,也可以保证线程安全。

    比如NSArry作为不可变类是线程安全的。然而它们的可变版本,比如 NSMutableArray 是线程不安全的。事实上,如果是在一个队列中串行地进行访问的话,在不同线程中使用它们也是没有问题的。

    总结

    如果实在要使用多线程,也没有必要过分追求效率,而更多的考虑线程安全问题,使用对应的锁。

    对于平时编写应用里的多线程代码,还是建议用 @synchronized,NSLock 等,可读性和安全性都好,多线程安全比多线程性能更重要。



    作者:Cooci
    链接:https://www.jianshu.com/p/c557308c0ec5




    收起阅读 »

    浅谈Android插件化

    一、认识插件化1.1 插件化起源插件化技术最初源于免安装运行 Apk的想法,这个免安装的 Apk 就可以理解为插件,而支持插件的 app 我们一般叫 宿主。想必大家都知道,在 Android ...
    继续阅读 »

    一、认识插件化

    1.1 插件化起源

    插件化技术最初源于免安装运行 Apk的想法,这个免安装的 Apk 就可以理解为插件,而支持插件的 app 我们一般叫 宿主。

    想必大家都知道,在 Android 系统中,应用是以 Apk 的形式存在的,应用都需要安装才能使用。但实际上 Android 系统安装应用的方式相当简单,其实就是把应用 Apk 拷贝到系统不同的目录下、然后把 so 解压出来而已。

    常见的应用安装目录有:

    • /system/app:系统应用
    • /system/priv-app:系统应用
    • /data/app:用户应用

    那可能大家会想问,既然安装这个过程如此简单,Android 是怎么运行应用中的代码的呢,我们先看 Apk 的构成,一个常见的 Apk 会包含如下几个部分:

    • classes.dexJava 代码字节码
    • res:资源文件
    • libso 文件
    • assets:静态资产文件
    • AndroidManifest.xml:清单文件

    其实 Android 系统在打开应用之后,也只是开辟进程,然后使用 ClassLoader 加载 classes.dex 至进程中,执行对应的组件而已。

    那大家可能会想一个问题,既然 Android 本身也是使用类似反射的形式加载代码执行,凭什么我们不能执行一个 Apk 中的代码呢?

    1.2 插件化优点

    插件化让 Apk 中的代码(主要是指 Android 组件)能够免安装运行,这样能够带来很多收益:

    • 减少安装Apk的体积、按需下载模块
    • 动态更新插件
    • 宿主和插件分开编译,提升开发效率
    • 解决方法数超过65535的问题

    想象一下,你的应用拥有 Native 应用一般极高的性能,又能获取诸如 Web 应用一样的收益。

    嗯,理想很美好不是嘛?

    1.3 与组件化的区别

    • 组件化:是将一个App分成多个模块,每个模块都是一个组件(module),开发过程中可以让这些组件相互依赖或独立编译、调试部分组件,但是这些组件最终会合并成一个完整的Apk去发布到应用市场。
    • 插件化:是将整个App拆分成很多模块,每个模块都是一个Apk(组件化的每个模块是一个lib),最终打包的时候将宿主Apk和插件Apk分开打包,只需发布宿主Apk到应用市场,插件Apk通过动态按需下发到宿主Apk。

    二、插件化的技术难点

    想让插件的Apk真正运行起来,首先要先能找到插件Apk的存放位置,然后我们要能解析加载Apk里面的代码。

    但是光能执行Java代码是没有意义的,在Android系统中有四大组件是需要在系统中注册的,具体来说是在 Android 系统的 ActivityManagerService (AMS) 和 PackageManagerService (PMS) 中注册的,而四大组件的解析和启动都需要依赖 AMS 和 PMS,如何欺骗系统,让他承认一个未安装的 Apk 中的组件,如何让宿主动态加载执行插件Apk中 Android 组件(即 ActivityServiceBroadcastReceiverContentProviderFragment)等是插件化最大的难点。

    另外,应用资源引用(特指 R 中引用的资源,如 layoutvalues 等)也是一大问题,想象一下你在宿主进程中使用反射加载了一个插件 Apk,代码中的 R 对应的 id 却无法引用到正确的资源,会产生什么后果。

    总结一下,其实做到插件化的要点就这几个:

    • 如何加载并执行插件 Apk 中的代码(ClassLoader Injection
    • 让系统能调用插件 Apk 中的组件(Runtime Container
    • 正确识别插件 Apk 中的资源(Resource Injection

    当然还有其他一些小问题,但可能不是所有场景下都会遇到,我们后面再单独说。

    三、ClassLoader Injection

    ClassLoader 是插件化中必须要掌握的,因为我们知道Android 应用本身是基于魔改的 Java 虚拟机的,而由于插件是未安装的 apk,系统不会处理其中的类,所以需要使用 ClassLoader 加载 Apk,然后反射里面的代码。

    3.1 java 中的 ClassLoader

    • BootstrapClassLoader 负责加载 JVM 运行时的核心类,比如 JAVA_HOME/lib/rt.jar 等等

    • ExtensionClassLoader 负责加载 JVM 的扩展类,比如 JAVA_HOME/lib/ext 下面的 jar 包

    • AppClassLoader 负责加载 classpath 里的 jar 包和目录

    3.2 android 中的 ClassLoader

    在Android系统中ClassLoader是用来加载dex文件的,有包含 dex 的 apk 文件以及 jar 文件,dex 文件是一种对class文件优化的产物,在Android中应用打包时会把所有class文件进行合并、优化(把不同的class文件重复的东西只保留一份),然后生成一个最终的class.dex文件

    • PathClassLoader 用来加载系统类和应用程序类,可以加载已经安装的 apk 目录下的 dex 文件

      public class PathClassLoader extends BaseDexClassLoader {
      public PathClassLoader(String dexPath, ClassLoader parent) {
      super(dexPath, null, null, parent);
      }

      public PathClassLoader(String dexPath, String libraryPath,
      ClassLoader parent) {
      super(dexPath, null, libraryPath, parent);
      }
      }

    • DexClassLoader 用来加载 dex 文件,可以从存储空间加载 dex 文件。

      public class DexClassLoader extends BaseDexClassLoader {
      public DexClassLoader(String dexPath, String optimizedDirectory,
      String libraryPath, ClassLoader parent) {
      super(dexPath, new File(optimizedDirectory), libraryPath, parent);
      }
      }

    我们在插件化中一般使用的是 DexClassLoader。

    3.3 双亲委派机制

    每一个 ClassLoader 中都有一个 parent 对象,代表的是父类加载器,在加载一个类的时候,会先使用父类加载器去加载,如果在父类加载器中没有找到,自己再进行加载,如果 parent 为空,那么就用系统类加载器来加载。通过这样的机制可以保证系统类都是由系统类加载器加载的。 下面是 ClassLoader 的 loadClass 方法的具体实现。

        protected Class loadClass(String name, boolean resolve)
    throws ClassNotFoundException
    {
    // First, check if the class has already been loaded
    Class c = findLoadedClass(name);
    if (c == null) {
    try {
    if (parent != null) {
    // 先从父类加载器中进行加载
    c = parent.loadClass(name, false);
    } else {
    c = findBootstrapClassOrNull(name);
    }
    } catch (ClassNotFoundException e) {
    // ClassNotFoundException thrown if class not found
    // from the non-null parent class loader
    }

    if (c == null) {
    // 没有找到,再自己加载
    c = findClass(name);
    }
    }
    return c;
    }

    3.4 如何加载插件中的类

    要加载插件中的类,我们首先要创建一个 DexClassLoader,先看下 DexClassLoader 的构造函数需要哪些参数。

    public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
    // ...
    }
    }

    构造函数需要四个参数: dexPath 是需要加载的 dex / apk / jar 文件路径 optimizedDirectory 是 dex 优化后存放的位置,在 ART 上,会执行 oat 对 dex 进行优化,生成机器码,这里就是存放优化后的 odex 文件的位置 librarySearchPath 是 native 依赖的位置 parent 就是父类加载器,默认会先从 parent 加载对应的类

    创建出 DexClassLaoder 实例以后,只要调用其 loadClass(className) 方法就可以加载插件中的类了。具体的实现在下面:

        // 从 assets 中拿出插件 apk 放到内部存储空间
    private fun extractPlugin() {
    var inputStream = assets.open("plugin.apk")
    File(filesDir.absolutePath, "plugin.apk").writeBytes(inputStream.readBytes())
    }

    private fun init() {
    extractPlugin()
    pluginPath = File(filesDir.absolutePath, "plugin.apk").absolutePath
    nativeLibDir = File(filesDir, "pluginlib").absolutePath
    dexOutPath = File(filesDir, "dexout").absolutePath
    // 生成 DexClassLoader 用来加载插件类
    pluginClassLoader = DexClassLoader(pluginPath, dexOutPath, nativeLibDir, this::class.java.classLoader)
    }

    3.5 执行插件类的方法

    通过反射来执行类的方法

    val loadClass = pluginClassLoader.loadClass(activityName)
    loadClass.getMethod("test",null).invoke(loadClass)

    我们称这个过程叫做 ClassLoader 注入。完成注入后,所有来自宿主的类使用宿主的 ClassLoader 进行加载,所有来自插件 Apk 的类使用插件 ClassLoader 进行加载,而由于 ClassLoader 的双亲委派机制,实际上系统类会不受 ClassLoader 的类隔离机制所影响,这样宿主 Apk 就可以在宿主进程中使用来自于插件的组件类了。

    四、Runtime Container

    我们之前说到 Activity 插件化最大的难点是如何欺骗系统,让他承认一个未安装的 Apk 中的组件。 因为插件是动态加载的,所以插件的四大组件不可能注册到宿主的 Manifest 文件中,而没有在 Manifest 中注册的四大组件是不能和系统直接进行交互的。 如果直接把插件的 Activity 注册到宿主 Manifest 里就失去了插件化的动态特性,因为每次插件中新增 Activity 都要修改宿主 Manifest 并且重新打包,那就和直接写在宿主中没什么区别了。

    4.1 为什么没有注册的 Activity 不能和系统交互

    这里的不能直接交互的含义有两个

    1. 系统会检测 Activity 是否注册 如果我们启动一个没有在 Manifest 中注册的 Activity,会发现报如下 error:

      android.content.ActivityNotFoundException: Unable to find explicit activity class {com.zyg.commontec/com.zyg.plugin.PluginActivity}; have you declared this activity in your AndroidManifest.xml?


    这个 log 在 Instrumentation 的 checkStartActivityResult 方法中可以看到:

    public class Instrumentation {
    public static void checkStartActivityResult(int res, Object intent) {
    if (!ActivityManager.isStartResultFatalError(res)) {
    return;
    }

    switch (res) {
    case ActivityManager.START_INTENT_NOT_RESOLVED:
    case ActivityManager.START_CLASS_NOT_FOUND:
    if (intent instanceof Intent && ((Intent)intent).getComponent() != null)
    throw new ActivityNotFoundException(
    "Unable to find explicit activity class "
    + ((Intent)intent).getComponent().toShortString()
    + "; have you declared this activity in your AndroidManifest.xml?");
    throw new ActivityNotFoundException(
    "No Activity found to handle " + intent);
    ...
    }
    }
    }


    1. Activity 的生命周期无法被调用,其实一个 Activity 主要的工作,都是在其生命周期方法中调用了,既然上一步系统检测了 Manifest 注册文件,启动 Activity 被拒绝,那么其生命周期方法也肯定不会被调用了。从而插件 Activity 也就不能正常运行了。

    4.2 运行时容器技术

    由于Android中的组件(Activity,Service,BroadcastReceiver和ContentProvider)是由系统创建的,并且由系统管理生命周期。 仅仅构造出这些类的实例是没用的,还需要管理组件的生命周期。其中以Activity最为复杂,不同框架采用的方法也不尽相同。插件化如何支持组件生命周期的管理。 大致分为两种方式:

    • 运行时容器技术(ProxyActivity代理)
    • 预埋StubActivity,hook系统启动Activity的过程

    我们的解决方案很简单,即运行时容器技术,简单来说就是在宿主 Apk 中预埋一些空的 Android 组件,以 Activity 为例,我预置一个 ContainerActivity extends Activity 在宿主中,并且在 AndroidManifest.xml 中注册它。

    它要做的事情很简单,就是帮助我们作为插件 Activity 的容器,它从 Intent 接受几个参数,分别是插件的不同信息,如:

    • pluginName
    • pluginApkPath
    • pluginActivityName

    等,其实最重要的就是 pluginApkPath 和 pluginActivityName,当 ContainerActivity 启动时,我们就加载插件的 ClassLoaderResource,并反射 pluginActivityName 对应的 Activity 类。当完成加载后,ContainerActivity 要做两件事:

    • 转发所有来自系统的生命周期回调至插件 Activity
    • 接受 Activity 方法的系统调用,并转发回系统

    我们可以通过复写 ContainerActivity 的生命周期方法来完成第一步,而第二步我们需要定义一个 PluginActivity,然后在编写插件 Apk 中的 Activity 组件时,不再让其集成 android.app.Activity,而是集成自我们的 PluginActivity

    public class ContainerActivity extends Activity {
    private PluginActivity pluginActivity;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    String pluginActivityName = getIntent().getString("pluginActivityName", "");
    pluginActivity = PluginLoader.loadActivity(pluginActivityName, this);
    if (pluginActivity == null) {
    super.onCreate(savedInstanceState);
    return;
    }

    pluginActivity.onCreate();
    }

    @Override
    protected void onResume() {
    if (pluginActivity == null) {
    super.onResume();
    return;
    }
    pluginActivity.onResume();
    }

    @Override
    protected void onPause() {
    if (pluginActivity == null) {
    super.onPause();
    return;
    }
    pluginActivity.onPause();
    }

    // ...
    }

    public class PluginActivity {

    private ContainerActivity containerActivity;

    public PluginActivity(ContainerActivity containerActivity) {
    this.containerActivity = containerActivity;
    }

    @Override
    public T findViewById(int id) {
    return containerActivity.findViewById(id);
    }
    // ...
    }

    // 插件 `Apk` 中真正写的组件
    public class TestActivity extends PluginActivity {
    // ......
    }

    是不是感觉有点看懂了,虽然真正搞的时候还有很多小坑,但大概原理就是这么简单,启动插件组件需要依赖容器,容器负责加载插件组件并且完成双向转发,转发来自系统的生命周期回调至插件组件,同时转发来自插件组件的系统调用至系统。

    4.3 字节码替换

    该方式虽然能够很好的实现启动插件Activity的目的,但是由于开发式侵入性很强,插件中的Activity必须继承PluginActivity,如果想把之前的模块改造成插件需要很多额外的工作。

    class TestActivity extends Activity {}
    ->
    class TestActivity extends PluginActivity {}

    有没有什么办法能让插件组件的编写与原来没有任何差别呢?

    Shadow 的做法是字节码替换插件,这是一个非常棒的想法,简单来说,Android 提供了一些 Gradle 插件开发套件,其中有一项功能叫 Transform Api,它可以介入项目的构建过程,在字节码生成后、dex 文件生成前,对代码进行某些变换,具体怎么做的不说了,可以自己看文档。

    实现的功能嘛,就是用户配置 Gradle 插件后,正常开发,依然编写:

    class TestActivity extends Activity {}
    然后完成编译后,最后的字节码中,显示的却是:
    class TestActivity extends PluginActivity {}

    到这里基本的框架就差不多结束了。

    五、Resource Injection

    最后要说的是资源注入,其实这一点相当重要,Android 应用的开发其实崇尚的是逻辑与资源分离的理念,所有资源(layoutvalues 等)都会被打包到 Apk 中,然后生成一个对应的 R 类,其中包含对所有资源的引用 id

    资源的注入并不容易,好在 Android 系统给我们留了一条后路,最重要的是这两个接口:

    • PackageManager#getPackageArchiveInfo:根据 Apk 路径解析一个未安装的 Apk 的 PackageInfo
    • PackageManager#getResourcesForApplication:根据 ApplicationInfo 创建一个 Resources 实例

    我们要做的就是在上面 ContainerActivity#onCreate 中加载插件 Apk 的时候,用这两个方法创建出来一份插件资源实例。具体来说就是先用 PackageManager#getPackageArchiveInfo 拿到插件 Apk 的 PackageInfo,有了 PacakgeInfo 之后我们就可以自己组装一份 ApplicationInfo,然后通过 PackageManager#getResourcesForApplication 来创建资源实例,大概代码像这样:

    PackageManager packageManager = getPackageManager();
    PackageInfo packageArchiveInfo = packageManager.getPackageArchiveInfo(
    pluginApkPath,
    PackageManager.GET_ACTIVITIES
    | PackageManager.GET_META_DATA
    | PackageManager.GET_SERVICES
    | PackageManager.GET_PROVIDERS
    | PackageManager.GET_SIGNATURES
    );
    packageArchiveInfo.applicationInfo.sourceDir = pluginApkPath;
    packageArchiveInfo.applicationInfo.publicSourceDir = pluginApkPath;

    Resources injectResources = null;
    try {
    injectResources = packageManager.getResourcesForApplication(packageArchiveInfo.applicationInfo);
    } catch (PackageManager.NameNotFoundException e) {
    // ...
    }

    拿到资源实例后,我们需要将宿主的资源和插件资源 Merge 一下,编写一个新的 Resources 类,用这样的方式完成自动代理:

    public class PluginResources extends Resources {
    private Resources hostResources;
    private Resources injectResources;

    public PluginResources(Resources hostResources, Resources injectResources) {
    super(injectResources.getAssets(), injectResources.getDisplayMetrics(), injectResources.getConfiguration());
    this.hostResources = hostResources;
    this.injectResources = injectResources;
    }

    @Override
    public String getString(int id, Object... formatArgs) throws NotFoundException {
    try {
    return injectResources.getString(id, formatArgs);
    } catch (NotFoundException e) {
    return hostResources.getString(id, formatArgs);
    }
    }

    // ...
    }

    然后我们在 ContainerActivity 完成插件组件加载后,创建一份 Merge 资源,再复写 ContainerActivity#getResources,将获取到的资源替换掉:

    public class ContainerActivity extends Activity {
    private Resources pluginResources;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    // ...
    pluginResources = new PluginResources(super.getResources(), PluginLoader.getResources(pluginApkPath));
    // ...
    }

    @Override
    public Resources getResources() {
    if (pluginActivity == null) {
    return super.getResources();
    }
    return pluginResources;
    }
    }

    这样就完成了资源的注入。

    收起阅读 »