注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

kotlin 协变、逆变 - 猫和鱼的故事

网上找的一段协变、逆变比较正式的定义:逆变与协变用来描述类型转换后的继承关系,其定义:如果 A、B 表示类型,f(⋅) 表示类型转换,≦ 表示继承关系(比如,A≦B 表示 A 是由 B 派生出来的子类): 当 A ≦ B 时,如果有 f(A) ≦ f(B) ,...
继续阅读 »

网上找的一段协变、逆变比较正式的定义:

逆变与协变用来描述类型转换后的继承关系,其定义:如果 A、B 表示类型,f(⋅) 表示类型转换, 表示继承关系(比如,A≦B 表示 A 是由 B 派生出来的子类): 当 A ≦ B 时,如果有 f(A) ≦ f(B) ,那么 f 是协变的; 当 A ≦ B 时,如果有 f(B) ≦ f(A) ,那么 f 是逆变的; 如果上面两种关系都不成立,即 (A) 与 f(B) 相互之间没有继承关系,则叫做不变的。

java 中可以通过如下泛型通配符以支持协变和逆变:

  • ? extends 来使泛型支持协变。修饰的泛型集合只能读取不能修改,这里的修改仅指对泛型集合添加元素,如果是 remove(int index) 以及 clear 当然是可以的。
  • ? super 来使泛型支持逆变。修饰的泛型集合只能修改不能读取,这里说的不能读取是指不能按照泛型类型读取,你如果按照 Object 读出来再强转当然也是可以的。

以动物举例,看代码。

abstract class Animal {
void eat() {
System.out.println("我是" + myName() + ", 我最喜欢吃" + myFavoriteFood());
}

abstract String myName();

abstract String myFavoriteFood();
}

class Fish extends Animal {

@Override
String myName() {
return "鱼";
}

@Override
String myFavoriteFood() {
return "虾米";
}
}

class Cat extends Animal {

@Override
String myName() {
return "猫";
}

@Override
String myFavoriteFood() {
return "小鱼干";
}
}

public static void extendsFun() {
List fishList = new ArrayList<>();
fishList.add(new Fish());
List catList = new ArrayList<>();
catList.add(new Cat());
List animals1 = fishList;
List animals2 = catList;

animals2.add(new Fish()); // 报错
Animal animal1 = animals1.get(0);
Animal animal2 = animals2.get(0);
animal1.eat();
animal2.eat();
}

//输出结果:
我是鱼, 我最喜欢吃虾米
我是猫, 我最喜欢吃小鱼干

协变就好比有多个集合,每个集合存储的是某中特定动物(extends Animal),但是不告诉你那个集合里存储的是鱼,哪个是猫。所以你虽然可以从任意一个集合中读取一个动物信息,没有问题,但是你没办法将一条鱼的信息存储到鱼的集合里,因为仅从变量 animals1、animals2 的类型声明上来看你不知道哪个集合里存储的是鱼,哪个集合里是猫。 假如报错的代码不报错了,那不就说明把一条鱼塞进了一堆猫里,这属于给猫加菜啊,所以肯定是不行的。? extends 类型通配符所表达的协变就是这个意思。

那逆变是什么意思呢?还是以上面的动物举例:

public static void superFun() {
List fishList = new ArrayList<>();
fishList.add(new Fish());
List animalList = new ArrayList<>();
animalList.add(new Cat());
animalList.add(new Fish());
List fish1 = fishList;
List fish2 = animalList;

fish1.add(new Fish());
Fish fish = fish2.get(0); //报错
}

从变量 fish1、fish2 的类型声明上只能知道里面存储的都是鱼的父类,如果这里也不报错的话可就从 fish2 的集合里拿出一只猫赋值给一条鱼了,这属于谋杀亲鱼。所以肯定也是不行。? super 类型通配符所表达的逆变就是这个意思。

kotlin 中对于协变和逆变也提供了两个修饰符:

  • out:声明协变;
  • in:声明逆变。

它们有两种使用方式:

  • 第一种:和 java 一样在使用处声明;
  • 第二种:在类或接口的定义处声明。

当和 java 一样在使用处声明时,将上面 java 示例转换为 kotlin

fun extendsFun() {
val fishList: MutableList = ArrayList()
fishList.add(Fish())
val catList: MutableList = ArrayList()
catList.add(Cat())
val animals1: MutableList = fishList
val animals2: MutableList = catList
animals2.add(Fish()) // 报错
val animal1 = animals1[0]
val animal2 = animals2[0]
animal1.eat()
animal2.eat()
}

fun superFun() {
val fishList: MutableList = ArrayList()
fishList.add(Fish())
val animalList: MutableList = ArrayList()
animalList.add(Cat())
animalList.add(Fish())
val fish1: MutableList = fishList
val fish2: MutableList = animalList
fish1.add(Fish())
val fish: Fish = fish2[0] //报错
}

可以看到在 kotlin 代码中除了将 ? extends 替换为了 out,将 ? super 替换为了 in,其他地方并没有发生变化,而产生的结果是一样的。那在类或接口的定义处声明 in、out 的作用是什么呢。

假设有一个泛型接口 Source,该接口中不存在任何以 T 作为参数的方法,只是方法返回 T 类型值:

// Java
interface Source {
T nextT();
}

那么,在 Source  类型的变量中存储 Source  实例的引用是极为安全的——没有消费者-方法可以调用。但是 Java 并不知道这一点,并且仍然禁止这样操作:

// Java
void demo(Source strs) {
Source objects = strs; // !!!在 Java 中不允许
// ……
}

为了修正这一点,我们必须声明对象的类型为 Source,但这样的方式很复杂。而在 kotlin 中有一种简单的方式向编译器解释这种情况。我们可以标注 Source 的类型参数 T 来确保它仅从 Source 成员中返回(生产),并从不被消费。为此我们使用 out 修饰符修饰泛型 T

interface Source {
fun nextT(): T
}

fun demo(strs: Source) {
val objects: Source = strs // 这个没问题,因为 T 是一个 out-参数
// ……
}

还记得开篇协变的定义吗?

当 A ≦ B 时,如果有 f(A) ≦ f(B) ,那么 f 是协变的; 当 A ≦ B 时,如果有 f(B) ≦ f(A) ,那么 f 是逆变的;

也就是说:

当一个类 C 的类型参数 T 被声明为 out 时,那么就意味着类 C 在参数 T 上是协变的;参数 T 只能出现在类 C 的输出位置,不能出现在类 C 的输入位置。

同样的,对于 in 修饰符来说

当一个类 C 的类型参数 T 被声明为 in 时,那么就意味着类 C 在参数 T 上是逆变的;参数 T 只能出现在类 C 的输如位置,不能出现在类 C 的输出位置。

interface Comparable {
operator fun compareTo(other: T): Int
}

fun demo(x: Comparable) {
x.compareTo(1.0) // 1.0 拥有类型 Double,它是 Number 的子类型
// 因此,我们可以将 x 赋给类型为 Comparable 的变量
val y: Comparable = x // OK!
}

总结如下表:

image

收起阅读 »

Cocoapods原理总结

CocoaPods是IOS项目的依赖管理工具,类似于Android的gradle,不过gradle不仅有依赖管理功能,还能负责构建。CocoaPods只负责管理依赖,即对第三方库的依赖,像gradle一样支持传递依赖,即如果A依赖于B,B依赖C,我们在A工程里...
继续阅读 »

CocoaPods是IOS项目的依赖管理工具,类似于Android的gradle,不过gradle不仅有依赖管理功能,还能负责构建。CocoaPods只负责管理依赖,即对第三方库的依赖,像gradle一样支持传递依赖,即如果A依赖于B,B依赖C,我们在A工程里指出A依赖于B,CocoaPods会自动为我们下载C,并在构建时链接C库。

IOS工程有3种库项目,framework,static library,meta library,我们通常只使用前两种。我们在使用static library库工程时,一般使用它编译出来的静态库libxxx.a,以及对应的头文件,在写应用时,将这些文件拷贝到项目里,然后将静态库添加到链接的的依赖库路径里,并将头文件目录添加到头文件搜索目录中。而framework库的依赖会简单很多,framework是资源的集合,将静态库和其头文件包含在framework目录里。framework库类似于Android工程的aar库。而static library类似于Android工程的jar包。

CocoaPods同时支持static library和framework的依赖管理,下面介绍这两种情况下CocoaPods是如何实现构建上的依赖的

static library

先看一下使用CocoaPods管理依赖前项目的文件结构

1
2
3
4
5
6
7
8
CardPlayer
├── CardPlayer
│   ├── CardPlayer
│   ├── CardPlayer.xcodeproj
│   ├── CardPlayerTests
│   └── CardPlayerUITests
├── exportOptions.plist
└── wehere-dev-cloud.mobileprovision

然后我们使用Pod来管理依赖,编写的PodFile如下所示:

1
2
3
4
5
6
project 'CardPlayer/CardPlayer.xcodeproj'

target 'CardPlayer' do
pod 'AFNetworking', '~> 1.0'
end

文件结构的变化

然后使用pod install,添加好依赖之后,项目的文件结构如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
CardPlayer
├── CardPlayer
│   ├── CardPlayer
│   ├── CardPlayer.xcodeproj
│   ├── CardPlayerTests
│   └── CardPlayerUITests
├── CardPlayer.xcworkspace
│   └── contents.xcworkspacedata
├── PodFile
├── Podfile.lock
├── Pods
│   ├── AFNetworking
│   ├── Headers
│   ├── Manifest.lock
│   ├── Pods.xcodeproj
│   └── Target\ Support\ Files
├── exportOptions.plist
└── wehere-dev-cloud.mobileprovision

可以看到我们添加了如下文件

  1. PodFile 依赖描述文件

  2. Podfile.lock 当前安装的依赖库的版本

  3. CardPlayer.xcworkspace

    xcworkspace文件,使用CocoaPod管理依赖的项目,XCode只能使用workspace编译项目,如果还只打开以前的xcodeproj文件进行开发,编译会失败

    xcworkspace文件实际是一个文件夹,实际Workspace信息保存在contents.xcworkspacedata里,该文件的内容非常简单,实际上只指示它所使用的工程的文件目录

    如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <?xml version="1.0" encoding="UTF-8"?>
    <Workspace
    version = "1.0">
    <FileRef
    location = "group:CardPlayer/CardPlayer.xcodeproj">
    </FileRef>
    <FileRef
    location = "group:Pods/Pods.xcodeproj">
    </FileRef>
    </Workspace>

  4. Pods目录

    1. Pods.xcodeproj,Pods工程,所有第三方库由Pods工程构建,每个第3方库对应Pods工程的1个target,并且这个工程还有1个Pods-Xxx的target,接下来在介绍工程时再详细介绍

    2. AFNetworking 每个第3方库,都会在Pods目录下有1个对应的目录

    3. Headers

      在Headers下有两个目录,Private和Public,第3方库的私有头文件会在Private目录下有对应的头文件,不过是1个软链接,链接到第3方库的头文件 第3方库的Pubic头文件会在Public目录下有对应的头文件,也是软链接

      如下所示:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      Headers/
      ├── Private
      │   └── AFNetworking
      │   ├── AFHTTPClient.h -> ../../../AFNetworking/AFNetworking/AFHTTPClient.h
      │   ├── AFHTTPRequestOperation.h -> ../../../AFNetworking/AFNetworking/AFHTTPRequestOperation.h
      │   ├── AFImageRequestOperation.h -> ../../../AFNetworking/AFNetworking/AFImageRequestOperation.h
      │   ├── AFJSONRequestOperation.h -> ../../../AFNetworking/AFNetworking/AFJSONRequestOperation.h
      │   ├── AFNetworkActivityIndicatorManager.h -> ../../../AFNetworking/AFNetworking/AFNetworkActivityIndicatorManager.h
      │   ├── AFNetworking.h -> ../../../AFNetworking/AFNetworking/AFNetworking.h
      │   ├── AFPropertyListRequestOperation.h -> ../../../AFNetworking/AFNetworking/AFPropertyListRequestOperation.h
      │   ├── AFURLConnectionOperation.h -> ../../../AFNetworking/AFNetworking/AFURLConnectionOperation.h
      │   ├── AFXMLRequestOperation.h -> ../../../AFNetworking/AFNetworking/AFXMLRequestOperation.h
      │   └── UIImageView+AFNetworking.h -> ../../../AFNetworking/AFNetworking/UIImageView+AFNetworking.h
      └── Public
      └── AFNetworking
      ├── AFHTTPClient.h -> ../../../AFNetworking/AFNetworking/AFHTTPClient.h
      ├── AFHTTPRequestOperation.h -> ../../../AFNetworking/AFNetworking/AFHTTPRequestOperation.h
      ├── AFImageRequestOperation.h -> ../../../AFNetworking/AFNetworking/AFImageRequestOperation.h
      ├── AFJSONRequestOperation.h -> ../../../AFNetworking/AFNetworking/AFJSONRequestOperation.h
      ├── AFNetworkActivityIndicatorManager.h -> ../../../AFNetworking/AFNetworking/AFNetworkActivityIndicatorManager.h
      ├── AFNetworking.h -> ../../../AFNetworking/AFNetworking/AFNetworking.h
      ├── AFPropertyListRequestOperation.h -> ../../../AFNetworking/AFNetworking/AFPropertyListRequestOperation.h
      ├── AFURLConnectionOperation.h -> ../../../AFNetworking/AFNetworking/AFURLConnectionOperation.h
      ├── AFXMLRequestOperation.h -> ../../../AFNetworking/AFNetworking/AFXMLRequestOperation.h
      └── UIImageView+AFNetworking.h -> ../../../AFNetworking/AFNetworking/UIImageView+AFNetworking.h

    4. Manifest.lock manifest文件 描述第3方库对其它库的依赖

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      PODS:
      - AFNetworking (1.3.4)

      DEPENDENCIES:
      - AFNetworking (~> 1.0)

      SPEC CHECKSUMS:
      AFNetworking: cf8e418e16f0c9c7e5c3150d019a3c679d015018

      PODFILE CHECKSUM: 349872ccf0789fbe3fa2b0f912b1b5388eb5e1a9

      COCOAPODS: 1.3.1

    5. Target Support Files 支撑target的文件

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      Target\ Support\ Files/
      ├── AFNetworking
      │   ├── AFNetworking-dummy.m
      │   ├── AFNetworking-prefix.pch
      │   └── AFNetworking.xcconfig
      └── Pods-CardPlayer
      ├── Pods-CardPlayer-acknowledgements.markdown
      ├── Pods-CardPlayer-acknowledgements.plist
      ├── Pods-CardPlayer-dummy.m
      ├── Pods-CardPlayer-frameworks.sh
      ├── Pods-CardPlayer-resources.sh
      ├── Pods-CardPlayer.debug.xcconfig
      └── Pods-CardPlayer.release.xcconfig

      在Target Support Files目录下每1个第3方库都会有1个对应的文件夹,比如AFNetworking,该目录下有一个空实现文件,也有预定义头文件用来优化头文件编译速度,还会有1个xcconfig文件,该文件会在工程配置中使用,主要存放头文件搜索目录,链接的Flag(比如链接哪些库)

      在Target Support Files目录下还会有1个Pods-XXX的文件夹,该文件夹存放了第3方库声明文档markdown文档和plist文件,还有1个dummy的空实现文件,还有debug和release各自对应的xcconfig配置文件,另外还有2个脚本文件,Pods-XXX-frameworks.sh脚本用于实现framework库的链接,当依赖的第3方库是framework形式才会用到该脚本,另外1个脚本文件: Pods-XXX-resources.sh用于编译storyboard类的资源文件或者拷贝*.xcassets之类的资源文件

工程结构的变化

上一节里提到在引入CocoaPods管理依赖后,会新增workspace文件,新增的workspace文件会引用原有的应用主工程,还会引用新增的Pods工程。后续不能再直接打开原来的应用主工程进行编译,否则会失败。实际上是因为原来的应用主工程的配置现在也有了变化。下面分别介绍一下Pods工程以及主工程的变化。

Pods工程

Pods工程配置

Pods工程会为每个依赖的第3方库定义1个Target,还会定义1个Pods-Xxx的target,每个Target会生成1个静态库,如下图所示:

cocoapods_pod_project_target

Pods工程会新建Debug和Release两个Configuration,每个Configuration会为不同的target设置不同的xcconfig,xcconfig指出了头文件查找目录,要链接的第3方库,链接目录等信息,如下图所示:

cocoapods_project_target_configuration

AFNetworking.xcconfig文件的内容如下所示:

1
2
3
4
5
6
7
8
9
10
CONFIGURATION_BUILD_DIR = $PODS_CONFIGURATION_BUILD_DIR/AFNetworking
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
HEADER_SEARCH_PATHS = "${PODS_ROOT}/Headers/Private" "${PODS_ROOT}/Headers/Private/AFNetworking" "${PODS_ROOT}/Headers/Public" "${PODS_ROOT}/Headers/Public/AFNetworking"
OTHER_LDFLAGS = -framework "CoreGraphics" -framework "MobileCoreServices" -framework "Security" -framework "SystemConfiguration"
PODS_BUILD_DIR = $BUILD_DIR
PODS_CONFIGURATION_BUILD_DIR = $PODS_BUILD_DIR/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
PODS_ROOT = ${SRCROOT}
PODS_TARGET_SRCROOT = ${PODS_ROOT}/AFNetworking
PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier}
SKIP_INSTALL = YES

上述内容说明了AFNetworking编译时查找头文件的目录Header_SERACH_PATHS,OTHER_LD_FLAGS指明了要链接的framework

Pods-CardPlayer.debug.xcconfig文件的内容如下所示:

1
2
3
4
5
6
7
8
9
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
HEADER_SEARCH_PATHS = $(inherited) "${PODS_ROOT}/Headers/Public" "${PODS_ROOT}/Headers/Public/AFNetworking"
LIBRARY_SEARCH_PATHS = $(inherited) "$PODS_CONFIGURATION_BUILD_DIR/AFNetworking"
OTHER_CFLAGS = $(inherited) -isystem "${PODS_ROOT}/Headers/Public" -isystem "${PODS_ROOT}/Headers/Public/AFNetworking"
OTHER_LDFLAGS = $(inherited) -ObjC -l"AFNetworking" -framework "CoreGraphics" -framework "MobileCoreServices" -framework "Security" -framework "SystemConfiguration"
PODS_BUILD_DIR = $BUILD_DIR
PODS_CONFIGURATION_BUILD_DIR = $PODS_BUILD_DIR/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
PODS_PODFILE_DIR_PATH = ${SRCROOT}/..
PODS_ROOT = ${SRCROOT}/../Pods

Pods-CardPlayer.debug文件中OTHER_LDFLAGS说明了编译Pods时需要链接AFNetworking库,还需要链接其它framework

所以我们在xcode里能看到AFNetworking依赖的framework:

cocoapods_target_lib_dependency

Pods工程文件组织

IOS工程在XCode上看到的结构和文件系统的结构并不一致,在XCode上看到的文件夹并不是物理的文件夹,而是叫做Group,在组织IOS工程时,会将逻辑关系较近的文件放在同一个Group下。如下图所示:

cocoapods_pods_project_files

coacoapods_pods_project_afnetworking_support

可以看到Group的组织大概是以下形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Pods
├── Podfile # 指向根目录下的Podfile 说明依赖的第3方库
├── Frameworks # 文件系统并没有对应的目录 这只是1个虚拟的group 表示需要链接的frameowork
├── └── iOS # 文件系统并没有对应的目录 这只是1个虚拟的group 这里表示是ios需要链接的framework
├── └── Xxx.framework # 链接的frameowork列表
├── Pods # 虚拟的group 管理所有第3方库
│   └── AFNetwoking #AFNetworking库 虚拟group 对应文件系统Pods/AFNetworking/AFNetworking目录下的内容
│   ├── xxx.h #AFNetworking库的头文件 对应文件系统Pods/AFNetworking/AFNetworking目录下的所有头文件
│ ├── xxx.m #AFNetworking库的实现文件 对应文件系统Pods/AFNetworking/AFNetworking目录下的所有实现文件
│   └── Support Files # 虚拟group 支持文件 没有直接对应的文件系统目录,该group下的文件都属于目录: Pods/Target Support Files/AFNetworking/
│ ├── AFNetworking.xcconfig # AFNetworking编译的工程配置文件
│ ├── AFNetworking-prefix.pch # AFNetworking编译用的预编译头文件
│ └── AFNetworking-dummy.m # 空实现文件
├── Products # 虚拟group
│ ├── libAFNetworking.a # AFNetworking target将生成的静态库
│ └── libPods-CardPlayer.a # Pods-CardPlayer target将生成的静态库
└── Targets Support Files # 虚拟group 管理支持文件
└── Pods-CardPlayer # 虚拟group Pods-CardPlayer target
├── Pods-CardPlayer-acknowledgements.markdown # 协议说明文档
├── Pods-CardPlayer-acknowledgements.plist # 协议说明文档
├── Pods-CardPlayer-dummy.m # 空实现
├── Pods-CardPlayer-frameworks.sh # 安装framework的脚本
├── Pods-CardPlayer-resources.sh # 安装resource的脚本
├── Pods-CardPlayer.debug.xcconfig # debug configuration 的 配置文件
└── Pods-CardPlayer.release.xcconfig # release configuration 的 配置文件

主工程

引入CocoaPods之后, 主工程的设置其实也会变化, 我们先看一下引入之前,主工程的Configuration设置,如下图所示:       

cocoapods_before_project_config

可以看到Debug和Release的Configuration没有设置任何配置文件,再看引入CocoaPods之后,主工程的Configuration如下图所示:

cocoapod_main_project_configuration

可以看到采用CocoaPods之后,Debug Configuration设置了配置文件Pods-CardPlayer.debug.xcconfig文件,Release Configuration则设置了配置文件Pods-CardPlayer.release.xcconfig文件,这些配置文件指明了头文件的查找目录,要链接的第三方库

编译并链接第3方库的原理

   

  1. 头文件的查找

    上一节里已经讲到主工程的Configuration已经设置了配置文件,而这份配置文件里说明了头文件的查找目录:

    1
    2
    HEADER_SEARCH_PATHS = $(inherited) "${PODS_ROOT}/Headers/Public" "${PODS_ROOT}/Headers/Public/AFNetworking"
    OTHER_CFLAGS = $(inherited) -isystem "${PODS_ROOT}/Headers/Public" -isystem "${PODS_ROOT}/Headers/Public/AFNetworking"

    所以主工程可以引用第3方库的头文件,比如像这样: #import <AFNetworking/AFHTTPClient.h>

  2. 如何链接库

    配置文件同样说明了链接库的查找目录以及要链接的库:

    1
    2
    3
    LIBRARY_SEARCH_PATHS = $(inherited) "$PODS_CONFIGURATION_BUILD_DIR/AFNetworking"
    OTHER_LDFLAGS = $(inherited) -ObjC -l"AFNetworking" -framework "CoreGraphics" -framework "MobileCoreServices" -framework "Security" -framework "SystemConfiguration"

    而在我们主工程的main target还会添加对libPods-CardPlayer.a的链接,如下图所示:

    cocoapod_main_project_dependency_pods

  3. 编译顺序

    我们的主工程的main target显示指出了需要链接库libPods-CardPlayer.a,而libPods-CardPlayer.a由target Pods-CardPlayer产生,所以主工程的main target将会隐式依赖于target Pods-CardPlayer,而在target Pods-CardPlayer的配置中,显示指出了依赖对第三方库对应的target的依赖,如下所示:

    cocoapods_pods_dendency

    所以main target -> target Pods-CardPlayer -> 第3方库对应的target

    因为存在上述依赖关系,所以能保证编译顺序,保证编译链接都不会有问题

framework

如果我们在PodFile设置了use_frameworks!,则第3方库使用Framework形式的库,PodFile的内容如下所示:

1
2
3
4
5
6
7
8
project 'CardPlayer/CardPlayer.xcodeproj'

use_frameworks!

target 'CardPlayer' do
pod 'AFNetworking', '~> 1.0'
end

framework这类型的库和static library比较类似,在文件结构上没什么太大变化,都是新增了Pods工程,和管理Pods工程及原主工程的workspace,但是Pods工程设置的target的类型都是framework,而不是static library,而主工程对Pods的依赖,也不再是依赖libPods-CardPlayer.a,而是Pods_CardPlayer.framework。

如下图所示:

cocoapods_framework_dependency

cocoapods_pods_framework_thrid_party

另外编译配置文件也有一些不同:

AFNetworking.xcconfig文件如下所示:

1
2
3
4
5
6
7
8
9
10
CONFIGURATION_BUILD_DIR = $PODS_CONFIGURATION_BUILD_DIR/AFNetworking
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
HEADER_SEARCH_PATHS = "${PODS_ROOT}/Headers/Private" "${PODS_ROOT}/Headers/Public"
OTHER_LDFLAGS = -framework "CoreGraphics" -framework "MobileCoreServices" -framework "Security" -framework "SystemConfiguration"
PODS_BUILD_DIR = $BUILD_DIR
PODS_CONFIGURATION_BUILD_DIR = $PODS_BUILD_DIR/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
PODS_ROOT = ${SRCROOT}
PODS_TARGET_SRCROOT = ${PODS_ROOT}/AFNetworking
PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier}
SKIP_INSTALL = YES

而Pods-CardPlayer.debug.xcconfig文件的内容如下所示:

1
2
3
4
5
6
7
8
9
FRAMEWORK_SEARCH_PATHS = $(inherited) "$PODS_CONFIGURATION_BUILD_DIR/AFNetworking"
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks'
OTHER_CFLAGS = $(inherited) -iquote "$PODS_CONFIGURATION_BUILD_DIR/AFNetworking/AFNetworking.framework/Headers"
OTHER_LDFLAGS = $(inherited) -framework "AFNetworking"
PODS_BUILD_DIR = $BUILD_DIR
PODS_CONFIGURATION_BUILD_DIR = $PODS_BUILD_DIR/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
PODS_PODFILE_DIR_PATH = ${SRCROOT}/..
PODS_ROOT = ${SRCROOT}/../Pods

使用framework形式的库之后,Pods-CardPlayer-frameworks.sh脚本也有一些不同,

1
2
3
4
5
6
7
8
9
10
11
...
f [[ "$CONFIGURATION" == "Debug" ]]; then
install_framework "${BUILT_PRODUCTS_DIR}/AFNetworking/AFNetworking.framework"
fi
if [[ "$CONFIGURATION" == "Release" ]]; then
install_framework "${BUILT_PRODUCTS_DIR}/AFNetworking/AFNetworking.framework"
fi
if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then
wait
fi

编译framework后,它会将AFNetworking.framework安装到产品编译目录下,这样才能在运行时链接该framework

而我们的主工程的main target配置Build Phases有一项安装pod的framework,会调用Pod-CardPlayer-frameworks.sh,所以能保证正确安装framework,如下图所示:

cocoapods_target_embed_pods_framework



本文原创作者:Cloud Chou

链接:http://www.cloudchou.com/ios/post-990.html



收起阅读 »

深入理解 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


收起阅读 »

iOS - Path menu 的动画效果

AwesomeMenu 是一个与Path的故事菜单外观相同的菜单。通过设置菜单项来创建菜单:UIImage *storyMenuItemImage = [UIImage imageNamed:@"bg-menuitem.png"]; UIImage *sto...
继续阅读 »

AwesomeMenu 是一个与Path的故事菜单外观相同的菜单

通过设置菜单项来创建菜单:

UIImage *storyMenuItemImage = [UIImage imageNamed:@"bg-menuitem.png"];
UIImage *storyMenuItemImagePressed = [UIImage imageNamed:@"bg-menuitem-highlighted.png"];
UIImage *starImage = [UIImage imageNamed:@"icon-star.png"];
AwesomeMenuItem *starMenuItem1 = [[AwesomeMenuItem alloc] initWithImage:storyMenuItemImage
highlightedImage:storyMenuItemImagePressed
ContentImage:starImage
highlightedContentImage:nil];
AwesomeMenuItem *starMenuItem2 = [[AwesomeMenuItem alloc] initWithImage:storyMenuItemImage
highlightedImage:storyMenuItemImagePressed
ContentImage:starImage
highlightedContentImage:nil];
// the start item, similar to "add" button of Path
AwesomeMenuItem *startItem = [[AwesomeMenuItem alloc] initWithImage:[UIImage imageNamed:@"bg-addbutton.png"]
highlightedImage:[UIImage imageNamed:@"bg-addbutton-highlighted.png"]
ContentImage:[UIImage imageNamed:@"icon-plus.png"]
highlightedContentImage:[UIImage imageNamed:@"icon-plus-highlighted.png"]];




然后,设置菜单和选项:


AwesomeMenu *menu = [[AwesomeMenu alloc] initWithFrame:self.window.bounds startItem:startItem optionMenus:[NSArray arrayWithObjects:starMenuItem1, starMenuItem2]];
menu.delegate = self;
[self.window addSubview:menu];

您还可以使用菜单选项:

找到“添加”按钮的中心:

menu.startPoint = CGPointMake(160.0, 240.0);

设置旋转角度:

menu.rotateAngle = 0.0;

设置整个菜单角度:

menu.menuWholeAngle = M_PI * 2;

设置每个菜单飞出动画的延迟:

menu.timeOffset = 0.036f;

调整弹跳动画:

menu.farRadius = 140.0f;
menu.nearRadius = 110.0f;

设置“添加”按钮和菜单项之间的距离:

menu.endRadius = 120.0f;


常见问题及demo下载:https://github.com/levey/AwesomeMenu

源码下载:AwesomeMenu-master.zip

收起阅读 »

iOS 滑动效果cell - SWTableViewCell

SWTableViewCell一个易于使用的 UITableViewCell 子类,它实现了一个可滑动的内容视图,它公开了实用程序按钮(类似于 iOS 7 邮件应用程序)在你的 Podfile 中:- (void)tableView:(UITableView ...
继续阅读 »

SWTableViewCell

一个易于使用的 UITableViewCell 子类,它实现了一个可滑动的内容视图,它公开了实用程序按钮(类似于 iOS 7 邮件应用程序)

在你的 Podfile 中:

pod 'SWTableViewCell', '~> 0.3.7'


或者只是克隆这个 repo 并手动将源添加到项目

当用户向左滑动时,在表格视图单元格右侧可见的实用程序按钮。此行为类似于在 iOS 应用程序邮件和提醒中看到的行为。



实用程序按钮 当用户向右滑动时,在表格视图单元格左侧可见的实用程序按钮。


  • 动态实用程序按钮缩放。当您向单元格添加更多按钮时,该侧的其他按钮会变小以腾出空间
  • 智能选择:单元格将拾取触摸事件并将单元格滚动回中心或触发委托方法 - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath

因此,当实用程序按钮可见时,当用户触摸单元格时,单元格不会被视为选中,相反,单元格将滑回原位(与 iOS 7 邮件应用程序功能相同) * 创建带有标题或图标的实用程序按钮以及RGB 颜色 * 在 iOS 6.1 及更高版本上测试,包括 iOS 7

用法

标准表格视图单元格

在您的tableView:cellForRowAtIndexPath:方法中,您设置 SWTableView 单元格并使用包含的NSMutableArray+SWUtilityButtons类别向其中添加任意数量的实用程序按钮


- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *cellIdentifier = @"Cell";

SWTableViewCell *cell = (SWTableViewCell *)[tableView dequeueReusableCellWithIdentifier:cellIdentifier];

if (cell == nil) {
cell = [[SWTableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:cellIdentifier];
cell.leftUtilityButtons = [self leftButtons];
cell.rightUtilityButtons = [self rightButtons];
cell.delegate = self;
}

NSDate *dateObject = _testArray[indexPath.row];
cell.textLabel.text = [dateObject description];
cell.detailTextLabel.text = @"Some detail text";

return cell;
}

- (NSArray *)rightButtons
{
NSMutableArray *rightUtilityButtons = [NSMutableArray new];
[rightUtilityButtons sw_addUtilityButtonWithColor:
[UIColor colorWithRed:0.78f green:0.78f blue:0.8f alpha:1.0]
title:@"More"];
[rightUtilityButtons sw_addUtilityButtonWithColor:
[UIColor colorWithRed:1.0f green:0.231f blue:0.188 alpha:1.0f]
title:@"Delete"];

return rightUtilityButtons;
}

- (NSArray *)leftButtons
{
NSMutableArray *leftUtilityButtons = [NSMutableArray new];

[leftUtilityButtons sw_addUtilityButtonWithColor:
[UIColor colorWithRed:0.07 green:0.75f blue:0.16f alpha:1.0]
icon:[UIImage imageNamed:@"check.png"]];
[leftUtilityButtons sw_addUtilityButtonWithColor:
[UIColor colorWithRed:1.0f green:1.0f blue:0.35f alpha:1.0]
icon:[UIImage imageNamed:@"clock.png"]];
[leftUtilityButtons sw_addUtilityButtonWithColor:
[UIColor colorWithRed:1.0f green:0.231f blue:0.188f alpha:1.0]
icon:[UIImage imageNamed:@"cross.png"]];
[leftUtilityButtons sw_addUtilityButtonWithColor:
[UIColor colorWithRed:0.55f green:0.27f blue:0.07f alpha:1.0]
icon:[UIImage imageNamed:@"list.png"]];

return leftUtilityButtons;
}

###Custom Table View Cells

Thanks to Matt Bowman you can now create custom table view cells using Interface Builder that have the capabilities of an SWTableViewCell

The first step is to design your cell either in a standalone nib or inside of a table view using prototype cells. Make sure to set the custom class on the cell in interface builder to the subclass you made for it:

Then set the cell reuse identifier:

When writing your custom table view cell's code, make sure your cell is a subclass of SWTableViewCell:

#import <SWTableViewCell.h>

@interface MyCustomTableViewCell : SWTableViewCell

@property (weak, nonatomic) UILabel *customLabel;
@property (weak, nonatomic) UIImageView *customImageView;

@end

If you are using a separate nib and not a prototype cell, you'll need to be sure to register the nib in your table view:

- (void)viewDidLoad
{
[super viewDidLoad];

[self.tableView registerNib:[UINib nibWithNibName:@"MyCustomTableViewCellNibFileName" bundle:nil] forCellReuseIdentifier:@"MyCustomCell"];
}

Then, in the tableView:cellForRowAtIndexPath: method of your UITableViewDataSource (usually your view controller), initialize your custom cell:

- (UITableViewCell*)tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath
{
static NSString *cellIdentifier = @"MyCustomCell";

MyCustomTableViewCell *cell = (MyCustomTableViewCell *)[tableView dequeueReusableCellWithIdentifier:cellIdentifier
forIndexPath:indexPath];

cell.leftUtilityButtons = [self leftButtons];
cell.rightUtilityButtons = [self rightButtons];
cell.delegate = self;

cell.customLabel.text = @"Some Text";
cell.customImageView.image = [UIImage imageNamed:@"MyAwesomeTableCellImage"];
[cell setCellHeight:cell.frame.size.height];
return cell;
}

代理方法

// click event on left utility button
- (void)swipeableTableViewCell:(SWTableViewCell *)cell didTriggerLeftUtilityButtonWithIndex:(NSInteger)index;

// click event on right utility button
- (void)swipeableTableViewCell:(SWTableViewCell *)cell didTriggerRightUtilityButtonWithIndex:(NSInteger)index;

// utility button open/close event
- (void)swipeableTableViewCell:(SWTableViewCell *)cell scrollingToState:(SWCellState)state;

// prevent multiple cells from showing utilty buttons simultaneously
- (BOOL)swipeableTableViewCellShouldHideUtilityButtonsOnSwipe:(SWTableViewCell *)cell;

// prevent cell(s) from displaying left/right utility buttons
- (BOOL)swipeableTableViewCell:(SWTableViewCell *)cell canSwipeToState:(SWCellState)state;


常见问题及demo下载:https://github.com/CEWendel/SWTableViewCell

源码下载:SWTableViewCell-master.zip


收起阅读 »

iOS 标签浮动-JVFloatLabeledTextField

JVFloatLabeledTextFieldJVFloatLabeledTextField是 UX 模式的第一个实现,后来被称为“浮动标签模式”。由于移动设备的空间限制,通常仅依靠占位符来标记字段。这带来了 UX 问题,因为一旦用户开始填写表单,就不会出现任...
继续阅读 »

JVFloatLabeledTextField

JVFloatLabeledTextField是 UX 模式的第一个实现,后来被称为“浮动标签模式”

由于移动设备的空间限制,通常仅依靠占位符来标记字段。这带来了 UX 问题,因为一旦用户开始填写表单,就不会出现任何标签。

这个 UI 组件库包括 aUITextFieldUITextView子类,旨在通过将占位符转换为浮动标签来改善用户体验,这些标签在填充文本后悬停在字段上方。

马特 D. 史密斯的设计


通过 CocoaPods 

sudo gem install cocoapods

Podfile在您的项目目录中创建一个

pod init

将以下内容添加到您的Podfile项目目标中:

pod 'JVFloatLabeledTextField'

然后运行 CocoaPods pod install

最后,将JVFloatLabeledTextField.h包含JVFloatLabeledTextView.h在您的项目中。

Carthage

brew update
brew install carthage

Cartfile在您的项目目录中创建一个包含:

github "jverdi/JVFloatLabeledTextField"

然后运行 carthagecarthage updateJVFloatLabeledText.frameworkCarthage/Build/iOS目录中添加到您的项目中

最后,JVFloatLabeledText.h在您的项目中包含

#import <JVFloatLabeledText/JVFloatLabeledText.h>


常见问题及demo下载:https://github.com/jverdi/JVFloatLabeledTextField

源码下载:JVFloatLabeledTextField-main.zip








收起阅读 »

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响应.





收起阅读 »

功能强大的升级库

CheckVersionLib V2版震撼来袭,功能强大,链式编程,调用简单,集成轻松,扩展性强大老规矩先看V2效果,这个版本最大的特点就是使用非常简单,相对于1.+版本 效果 特点 任何地方都可以调用 简单简单简单简单(重要的话我说四遍) 扩...
继续阅读 »

CheckVersionLib


V2版震撼来袭,功能强大,链式编程,调用简单,集成轻松,扩展性强大

老规矩先看V2效果,这个版本最大的特点就是使用非常简单,相对于1.+版本


效果

V2.gif



特点



  • 任何地方都可以调用




  • 简单简单简单简单(重要的话我说四遍)




  • 扩展性强大




  • 所有具有升级功能的app均可使用,耶稣说的




  • 更强大的自定义界面支持




  • 支持强制更新(一行代码)




  • 支持静默下载 (一行代码)




  • 适配到Android Q




导入

allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}


appcompat

  implementation 'com.github.AlexLiuSheng:CheckVersionLib:2.4.1_appcompat'


jitpack && android x

dependencies {
implementation 'com.github.AlexLiuSheng:CheckVersionLib:2.4.1_androidx'
}


使用

和1.+版本一样,两种模式


只使用下载模式


先来个最简单的调用

        AllenVersionChecker
.getInstance()
.downloadOnly(
UIData.create().setDownloadUrl(downloadUrl)
)
.executeMission(context);

UIData:UIData是一个Bundle,用于存放用于UI展示的一些数据,后面自定义界面时候可以拿来用


请求服务器版本+下载


该模式最简单的使用

   AllenVersionChecker
.getInstance()
.requestVersion()
.setRequestUrl(requestUrl)
.request(new RequestVersionListener() {
@Nullable
@Override
public UIData onRequestVersionSuccess(String result) {
//拿到服务器返回的数据,解析,拿到downloadUrl和一些其他的UI数据
...
//如果是最新版本直接return null
return UIData.create().setDownloadUrl(downloadUrl);
}

@Override
public void onRequestVersionFailure(String message) {

}
})
.executeMission(context);

请求版本一些其他的http参数可以设置,如下

 AllenVersionChecker
.getInstance()
.requestVersion()
.setHttpHeaders(httpHeader)
.setRequestMethod(HttpRequestMethod.POSTJSON)
.setRequestParams(httpParam)
.setRequestUrl(requestUrl)
.request(new RequestVersionListener() {
@Nullable
@Override
public UIData onRequestVersionSuccess(String result) {
//拿到服务器返回的数据,解析,拿到downloadUrl和一些其他的UI数据
...
UIData uiData = UIData
.create()
.setDownloadUrl(downloadUrl)
.setTitle(updateTitle)
.setContent(updateContent);
//放一些其他的UI参数,拿到后面自定义界面使用
uiData.getVersionBundle().putString("key", "your value");
return uiData;

}

@Override
public void onRequestVersionFailure(String message) {

}
})
.executeMission(context);


合适的地方关闭任务

为了避免不必要的内存泄漏,需要在合适的地方取消任务

AllenVersionChecker.getInstance().cancelAllMission();

以上就是最基本的使用(库默认会有一套界面),如果还不满足项目需求,下面就可以用这个库来飙车了


一些其他的function设置

解释下,下面的builder叫DownloadBuilder

 DownloadBuilder builder=AllenVersionChecker
.getInstance()
.downloadOnly();


or



DownloadBuilder builder=AllenVersionChecker
.getInstance()
.requestVersion()
.request()

取消任务


 AllenVersionChecker.getInstance().cancelAllMission(this);

静默下载


 builder.setSilentDownload(true); 默认false

设置当前服务器最新的版本号,供库判断是否使用缓存



  • 缓存策略:如果本地有安装包,首先判断与当前运行的程序的versionCode是否不一致,然后判断是否有传入最新的
    versionCode,如果传入的versionCode大于本地的,重新从服务器下载,否则使用缓存

 builder.setNewestVersionCode(int); 默认null

强制更新


设置此listener即代表需要强制更新,会在用户想要取消下载的时候回调
需要你自己关闭所有界面

builder.setForceUpdateListener(() -> {
forceUpdate();
});

update in v2.2.1
动态设置是否强制更新,如果使用本库来请求服务器,可以在回调时动态设置一些参数或者回调

   public UIData onRequestVersionSuccess(DownloadBuilder downloadBuilder,String result) {
downloadBuilder.setForceUpdateListener(() -> {
forceUpdate();
});
Toast.makeText(V2Activity.this, "request successful", Toast.LENGTH_SHORT).show();
return crateUIData();
}

下载忽略本地缓存


如果本地有安装包缓存也会重新下载apk

 builder.setForceRedownload(true); 默认false

是否显示下载对话框


builder.setShowDownloadingDialog(false); 默认true

是否显示通知栏


builder.setShowNotification(false);  默认true

以前台service运行(update in 2.2.2)
推荐以前台服务运行更新,防止在后台时,服务被杀死


builder.setRunOnForegroundService(true); 默认true

自定义通知栏


      builder.setNotificationBuilder(
NotificationBuilder.create()
.setRingtone(true)
.setIcon(R.mipmap.dialog4)
.setTicker("custom_ticker")
.setContentTitle("custom title")
.setContentText(getString(R.string.custom_content_text))
);

是否显示失败对话框


  builder.setShowDownloadFailDialog(false); 默认true

自定义下载路径


  builder.setDownloadAPKPath(address); 默认:/storage/emulated/0/AllenVersionPath/

自定义下载文件名


  builder.setApkName(apkName); 默认:getPackageName()

可以设置下载监听


   builder.setApkDownloadListener(new APKDownloadListener() {
@Override
public void onDownloading(int progress) {

}

@Override
public void onDownloadSuccess(File file) {

}

@Override
public void onDownloadFail() {

}
});

设置取消监听
此回调会监听所有cancel事件


 
builder.setOnCancelListener(() -> {
Toast.makeText(V2Activity.this,"Cancel Hanlde",Toast.LENGTH_SHORT).show();
});

如果想单独监听几种状态下的cancel,可像如下这样设置


  • builder.setDownloadingCancelListener();

  • builder.setDownloadFailedCancelListener();

  • builder.setReadyDownloadCancelListener();


设置确定监听(added after 2.2.2)



  • builder.setReadyDownloadCommitClickListener();

  • builder.setDownloadFailedCommitClickListener();


静默下载+直接安装(不会弹出升级对话框)


    builder.setDirectDownload(true);
builder.setShowNotification(false);
builder.setShowDownloadingDialog(false);
builder.setShowDownloadFailDialog(false);

自定义安装回调


    setCustomDownloadInstallListener(CustomInstallListener customDownloadInstallListener)


自定义界面

自定义界面使用回调方式,开发者需要返回自己定义的Dialog(父类android.app)



  • 所有自定义的界面必须使用listener里面的context实例化




  • 界面展示的数据通过UIData拿




自定义显示更新界面


设置CustomVersionDialogListener



  • 定义此界面必须有一个确定下载的按钮,按钮id必须为@id/versionchecklib_version_dialog_commit




  • 如果有取消按钮(没有忽略本条要求),则按钮id必须为@id/versionchecklib_version_dialog_cancel



eg.

  builder.setCustomVersionDialogListener((context, versionBundle) -> {
BaseDialog baseDialog = new BaseDialog(context, R.style.BaseDialog, R.layout.custom_dialog_one_layout);
//versionBundle 就是UIData,之前开发者传入的,在这里可以拿出UI数据并展示
TextView textView = baseDialog.findViewById(R.id.tv_msg);
textView.setText(versionBundle.getContent());
return baseDialog;
});

自定义下载中对话框界面


设置CustomDownloadingDialogListener


  • 如果此界面要设计取消操作(没有忽略),请务必将id设置为@id/versionchecklib_loading_dialog_cancel

    builder.setCustomDownloadingDialogListener(new CustomDownloadingDialogListener() {
@Override
public Dialog getCustomDownloadingDialog(Context context, int progress, UIData versionBundle) {
BaseDialog baseDialog = new BaseDialog(context, R.style.BaseDialog, R.layout.custom_download_layout);
return baseDialog;
}
//下载中会不断回调updateUI方法
@Override
public void updateUI(Dialog dialog, int progress, UIData versionBundle) {
TextView tvProgress = dialog.findViewById(R.id.tv_progress);
ProgressBar progressBar = dialog.findViewById(R.id.pb);
progressBar.setProgress(progress);
tvProgress.setText(getString(R.string.versionchecklib_progress, progress));
}
});

自定义下载失败对话框


设置CustomDownloadFailedListener



  • 如果有重试按钮请将id设置为@id/versionchecklib_failed_dialog_retry




  • 如果有 确认/取消按钮请将id设置为@id/versionchecklib_failed_dialog_cancel



   builder.setCustomDownloadFailedListener((context, versionBundle) -> {
BaseDialog baseDialog = new BaseDialog(context, R.style.BaseDialog, R.layout.custom_download_failed_dialog);
return baseDialog;
});


update Log


  • 2.2.1

    • 修复内存泄漏问题

    • 使用binder传递参数

    • 一些已知的bug




混淆配置

 -keepattributes *Annotation*
-keepclassmembers class * {
@org.greenrobot.eventbus.Subscribe ;
}
-keep enum org.greenrobot.eventbus.ThreadMode { *; }

# Only required if you use AsyncExecutor
-keepclassmembers class * extends org.greenrobot.eventbus.util.ThrowableFailureEvent {
(java.lang.Throwable);
}

git地址:https://github.com/AlexLiuSheng/CheckVersionLib

下载地址:CheckVersionLib-master.zip

收起阅读 »

优秀优秀,Android图片涂鸦库

DoodleImage doodle for Android. You can undo, zoom, move, add text, textures, etc. Also, a powerful, customizable and extensible d...
继续阅读 »

Doodle


Image doodle for Android. You can undo, zoom, move, add text, textures, etc. Also, a powerful, customizable and extensible doodle framework & multi-function drawing board.

Android图片涂鸦,具有撤消、缩放、移动、添加文字,贴图等功能。还是一个功能强大,可自定义和可扩展的涂鸦框架、多功能画板。

01.gif

01
02
03


Feature 特性



  • Brush and shape 画笔及形状


    The brush can choose hand-painted, mosaic, imitation, eraser, text, texture, and the imitation function is similar to that in PS, copying somewhere in the picture. Shapes can be selected from hand-drawn, arrows, lines, circles, rectangles, and so on. The background color of the brush can be selected as a color, or an image.


    画笔可以选择手绘、马赛克、仿制、橡皮擦、文字、贴图,其中仿制功能跟PS中的类似,复制图片中的某处地方。形状可以选择手绘、箭头、直线、圆、矩形等。画笔的底色可以选择颜色,或者一张图片。




  • Undo/Redo 撤销/重做


    Each step of the doodle operation can be undone or redone.


    每一步的涂鸦操作都可以撤销。




  • Zoom, move, and rotate 放缩、移动及旋转


    In the process of doodle, you can freely zoom, move and rotate the picture with gestures. Also, you can move,rotate and scale the doodle item.


    在涂鸦的过程中,可以自由地通过手势缩放、移动、旋转图片。可对涂鸦移动、旋转、缩放等。




  • Zoomer 放大器


    In order to doodle more finely, an zoomer can be set up during the doodle process.


    为了更细微地涂鸦,涂鸦过程中可以设置出现放大器。




Usage 用法


Gradle

allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}

dependencies {
compile 'com.github.1993hzw:Doodle:5.5.3'
}

There are two ways to use the Doodle library:

这里有两种方式使用Doodle涂鸦库


A. Launch DoodleActivity directly (the layout is like demo images above). If you need to customize more interactions, please use another method (Way B).

使用写好的涂鸦界面,直接启动.启动的页面可参看上面的演示图片。如果需要自定义更多的交互方式,则请使用另一种方式(即B方式)。

DoodleParams params = new DoodleParams(); // 涂鸦参数
params.mImagePath = imagePath; // the file path of image
DoodleActivity.startActivityForResult(MainActivity.this, params, REQ_CODE_DOODLE);

See DoodleParams for more details.

查看DoodleParams获取更多涂鸦参数信息。


B. Recommend, use DoodleView and customize your layout.

推荐的方法:使用DoodleView,便于拓展,灵活性高,自定义自己的交互界面.

/*
Whether or not to optimize drawing, it is suggested to open, which can optimize the drawing speed and performance.
Note: When item is selected for editing after opening, it will be drawn at the top level, and not at the corresponding level until editing is completed.
是否优化绘制,建议开启,可优化绘制速度和性能.
注意:开启后item被选中编辑时时会绘制在最上面一层,直到结束编辑后才绘制在相应层级
*/
boolean optimizeDrawing = true;
DoodleView mDoodleView = new DoodleView(this, bitmap, optimizeDrawing, new IDoodleListener() {
/*
called when save the doodled iamge.
保存涂鸦图像时调用
*/
@Override
public void onSaved(IDoodle doodle, Bitmap bitmap, Runnable callback) {
//do something
}

/*
called when it is ready to doodle because the view has been measured. Now, you can set size, color, pen, shape, etc.
此时view已经测量完成,涂鸦前的准备工作已经完成,在这里可以设置大小、颜色、画笔、形状等。
*/
@Override
public void onReady(IDoodle doodle) {
//do something
}
});

mTouchGestureListener = new DoodleOnTouchGestureListener(mDoodleView, new DoodleOnTouchGestureListener.ISelectionListener() {
/*
called when the item(such as text, texture) is selected/unselected.
item(如文字,贴图)被选中或取消选中时回调
*/
@Override
public void onSelectedItem(IDoodle doodle, IDoodleSelectableItem selectableItem, boolean selected) {
//do something
}

/*
called when you click the view to create a item(such as text, texture).
点击View中的某个点创建可选择的item(如文字,贴图)时回调
*/
@Override
public void onCreateSelectableItem(IDoodle doodle, float x, float y) {
//do something
/*
if (mDoodleView.getPen() == DoodlePen.TEXT) {
IDoodleSelectableItem item = new DoodleText(mDoodleView, "hello", 20 * mDoodleView.getUnitSize(), new DoodleColor(Color.RED), x, y);
mDoodleView.addItem(item);
} else if (mDoodleView.getPen() == DoodlePen.BITMAP) {
IDoodleSelectableItem item = new DoodleBitmap(mDoodleView, bitmap, 80 * mDoodle.getUnitSize(), x, y);
mDoodleView.addItem(item);
}
*/
}
});

// create touch detector, which dectects the gesture of scoll, scale, single tap, etc.
// 创建手势识别器,识别滚动,缩放,点击等手势
IDoodleTouchDetector detector = new DoodleTouchDetector(getApplicationContext(), mTouchGestureListener);
mDoodleView.setDefaultTouchDetector(detector);

// Setting parameters.设置参数
mDoodleView.setPen(DoodlePen.TEXT);
mDoodleView.setShape(DoodleShape.HAND_WRITE);
mDoodleView.setColor(new DoodleColor(Color.RED));

When turning off optimized drawing, you only need to call addItem(IDoodleItem) when you create it. When you start optimizing drawing, the created or selected item needs to call markItemToOptimizeDrawing(IDoodleItem), and you should call notifyItemFinishedDrawing(IDoodleItem) when you finish drawing. So this is generally used in code:

当关闭优化绘制时,只需要在创建时调用addItem(IDoodleItem);而当开启优化绘制时,创建或选中的item需要调用markItemToOptimizeDrawing(IDoodleItem),结束绘制时应调用notifyItemFinishedDrawing(IDoodleItem)。因此在代码中一般这样使用:

// when you are creating a item or selecting a item to edit
if (mDoodle.isOptimizeDrawing()) {
mDoodle.markItemToOptimizeDrawing(item);
} else {
mDoodle.addItem(item);
}

...

// finish creating or editting
if (mDoodle.isOptimizeDrawing()) {
mDoodle.notifyItemFinishedDrawing(item);
}

Then, add the DoodleView to your layout. Now you can start doodling freely.

把DoodleView添加到布局中,然后开始涂鸦。


Demo 实例

Here are other simple examples to teach you how to use the doodle framework.



  1. Mosaic effect
    马赛克效果




  2. Change text's size by scale gesture
    手势缩放文本大小



More...

Now I think you should know that DoodleActivity has used DoodleView. You also can customize your layout like DoodleActivity. See DoodleActivity for more details.

现在你应该知道DoodleActivity就是使用了DoodleView实现涂鸦,你可以参照DoodleActivity是怎么实现涂鸦界面的交互来实现自己的自定义页面。

DoodleView has implemented IDoodle.

DoodleView实现了IDoodle接口。

public interface IDoodle {
...
public float getUnitSize();
public void setDoodleRotation(int degree);
public void setDoodleScale(float scale, float pivotX, float pivotY);
public void setPen(IDoodlePen pen);
public void setShape(IDoodleShape shape);
public void setDoodleTranslation(float transX, float transY);
public void setSize(float paintSize);
public void setColor(IDoodleColor color);
public void addItem(IDoodleItem doodleItem);
public void removeItem(IDoodleItem doodleItem);
public void save();
public void topItem(IDoodleItem item);
public void bottomItem(IDoodleItem item);
public boolean undo(int step);
...
}


Framework diagram 框架图

structure


Doodle Coordinate 涂鸦坐标

coordinate


Extend 拓展

You can create a customized item like DoodlePath, DoodleText, DoodleBitmap which extend DoodleItemBase or implement IDoodleItem.

实现IDoodleItem接口或基础DoodleItemBase,用于创建自定义涂鸦条目item,比如DoodlePath, DoodleText, DoodleBitmap

You can create a customized pen like DoodlePen which implements IDoodlePen.

实现IDoodlePen接口用于创建自定义画笔pen,比如DoodlePen

You can create a customized shape like DoodleShape which implements IDoodleShape.

实现IDoodleShape接口用于创建自定义形状shape,比如DoodleShape

You can create a customized color like DoodleColor which implements IDoodleColor.

实现IDoodleColor接口用于创建自定义颜色color,比如DoodleColor

You can create a customized touch gesture detector like DoodleTouchDetector(GestureListener) which implements IDoodleTouchDetector.

实现IDoodleTouchDetector接口用于创建自定义手势识别器,比如DoodleTouchDetector


git地址:https://github.com/1993hzw/doodle

下载地址:doodle-master.zip

收起阅读 »

Swift - 第三方日历组件CVCalendar使用详解1(配置、基本用法)

CVCalendar 是一款超好用的第三方日历组件,不仅功能强大,而且可以方便地进行样式自定义。同时,CVCalendar 还提供月视图、周视图两种展示模式,我们可以根据需求自由选择使用。一、安装配置1. 从 GitHub 上下载最新的代码:https://g...
继续阅读 »

CVCalendar 是一款超好用的第三方日历组件,不仅功能强大,而且可以方便地进行样式自定义。同时,CVCalendar 还提供月视图、周视图两种展示模式,我们可以根据需求自由选择使用。

一、安装配置

1. 从 GitHub 上下载最新的代码:https://github.com/Mozharovsky/CVCalendar
2. 将下载下来的源码包中 CVCalendar.xcodeproj 拖拽至你的工程中 


3. 工程 -> General -> Embedded Binaries 项,把 iOS 版的 framework 添加进来:CVCalendar.framework


4. 最后,在需要使用 CVCalendar 的地方 import 进来就可以了

import CVCalendar

二、基本用法

1,月视图使用样例 

① 效果图
1. 初始化的时候自动显示当月日历,且“今天”的日期文字是红色的。
2. 顶部导航栏标题显示当前日历的年、月信息,日历左右滑动切换的时候,标题内容也会随之改变。
3. 点击导航栏右侧的“今天”按钮,日历又会跳回到当前日期。
4. 点击日历上的任一日期时间后,该日期背景色会变蓝色(如果是今天则变红色)。同时我们在日期选择响应中,将选择的日期弹出显示。

      

② 样例代码
日历组件分为:CVCalendarMenuView 和 CVCalendarView 两部分。前者是显示星期的菜单栏,后者是日期表格视图。这二者的位置和大小我们可以随意调整设置。
组件提供了许多代理协议让我进行样式调整或功能响应,我们可以选择使用。但其中 CVCalendarViewDelegate, CVCalendarMenuViewDelegate 这两个协议是必须的。

import UIKit
import CVCalendar

class ViewController: UIViewController {
//星期菜单栏
private var menuView: CVCalendarMenuView!

//日历主视图
private var calendarView: CVCalendarView!

var currentCalendar: Calendar!

override func viewDidLoad() {
super.viewDidLoad()

currentCalendar = Calendar.init(identifier: .gregorian)

//初始化的时候导航栏显示当年当月
self.title = CVDate(date: Date(), calendar: currentCalendar).globalDescription

//初始化星期菜单栏
self.menuView = CVCalendarMenuView(frame: CGRect(x:0, y:80, width:300, height:15))

//初始化日历主视图
self.calendarView = CVCalendarView(frame: CGRect(x:0, y:110, width:300,
height:450))

//星期菜单栏代理
self.menuView.menuViewDelegate = self

//日历代理
self.calendarView.calendarDelegate = self

//将菜单视图和日历视图添加到主视图上
self.view.addSubview(menuView)
self.view.addSubview(calendarView)
}

//今天按钮点击
@IBAction func todayButtonTapped(_ sender: AnyObject) {
let today = Date()
self.calendarView.toggleViewWithDate(today)
}

override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()

//更新日历frame
self.menuView.commitMenuViewUpdate()
self.calendarView.commitCalendarViewUpdate()
}

override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}

extension ViewController: CVCalendarViewDelegate,CVCalendarMenuViewDelegate {
//视图模式
func presentationMode() -> CalendarMode {
//使用月视图
return .monthView
}

//每周的第一天
func firstWeekday() -> Weekday {
//从星期一开始
return .monday
}

func presentedDateUpdated(_ date: CVDate) {
//导航栏显示当前日历的年月
self.title = date.globalDescription
}

//每个日期上面是否添加横线(连在一起就形成每行的分隔线)
func topMarker(shouldDisplayOnDayView dayView: CVCalendarDayView) -> Bool {
return true
}

//切换月的时候日历是否自动选择某一天(本月为今天,其它月为第一天)
func shouldAutoSelectDayOnMonthChange() -> Bool {
return false
}

//日期选择响应
func didSelectDayView(_ dayView: CVCalendarDayView, animationDidFinish: Bool) {
//获取日期
let date = dayView.date.convertedDate()!
// 创建一个日期格式器
let dformatter = DateFormatter()
dformatter.dateFormat = "yyyy年MM月dd日"
let message = "当前选择的日期是:\(dformatter.string(from: date))"
//将选择的日期弹出显示
let alertController = UIAlertController(title: "", message: message,
preferredStyle: .alert)
let okAction = UIAlertAction(title: "确定", style: .cancel, handler: nil)
alertController.addAction(okAction)
self.present(alertController, animated: true, completion: nil)
}
}

2,周视图使用样例

同月视图模式相比,周视图日历区域只有一行(每次显示7天日期)。其它方面和月视图相比差别不大,也都是左右滑动切换显示下一周、下一周日期。


import UIKit
import CVCalendar

class ViewController: UIViewController {
//星期菜单栏
private var menuView: CVCalendarMenuView!

//日历主视图
private var calendarView: CVCalendarView!

var currentCalendar: Calendar!

override func viewDidLoad() {
super.viewDidLoad()

currentCalendar = Calendar.init(identifier: .gregorian)

//初始化的时候导航栏显示当年当月
self.title = CVDate(date: Date(), calendar: currentCalendar).globalDescription

//初始化星期菜单栏
self.menuView = CVCalendarMenuView(frame: CGRect(x:0, y:80, width:300, height:15))

//初始化日历主视图
self.calendarView = CVCalendarView(frame: CGRect(x:0, y:110, width:300,
height:50))

//星期菜单栏代理
self.menuView.menuViewDelegate = self

//日历代理
self.calendarView.calendarDelegate = self

//将菜单视图和日历视图添加到主视图上
self.view.addSubview(menuView)
self.view.addSubview(calendarView)
}

//今天按钮点击
@IBAction func todayButtonTapped(_ sender: AnyObject) {
let today = Date()
self.calendarView.toggleViewWithDate(today)
}

override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()

//更新日历frame
self.menuView.commitMenuViewUpdate()
self.calendarView.commitCalendarViewUpdate()
}

override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}

extension ViewController: CVCalendarViewDelegate,CVCalendarMenuViewDelegate {
//视图模式
func presentationMode() -> CalendarMode {
//使用周视图
return .weekView
}

//每周的第一天
func firstWeekday() -> Weekday {
//从星期一开始
return .monday
}

func presentedDateUpdated(_ date: CVDate) {
//导航栏显示当前日历的年月
self.title = date.globalDescription
}

//每个日期上面是否添加横线(连在一起就形成每行的分隔线)
func topMarker(shouldDisplayOnDayView dayView: CVCalendarDayView) -> Bool {
return true
}

//切换周的时候日历是否自动选择某一天(本周为今天,其它周为第一天)
func shouldAutoSelectDayOnWeekChange() -> Bool {
return false
}

//日期选择响应
func didSelectDayView(_ dayView: CVCalendarDayView, animationDidFinish: Bool) {
//获取日期
let date = dayView.date.convertedDate(calendar: currentCalendar)!
// 创建一个日期格式器
let dformatter = DateFormatter()
dformatter.dateFormat = "yyyy年MM月dd日"
let message = "当前选择的日期是:\(dformatter.string(from: date))"
//将选择的日期弹出显示
let alertController = UIAlertController(title: "", message: message,
preferredStyle: .alert)
let okAction = UIAlertAction(title: "确定", style: .cancel, handler: nil)
alertController.addAction(okAction)
self.present(alertController, animated: true, completion: nil)
}
}

转自:https://www.hangge.com/blog/cache/detail_1504.html#

收起阅读 »

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()去完成布局。

收起阅读 »

字节跳动杨震原:没有“天才架构师”,技术团队需要市场化管理

在近日召开的火山引擎品牌发布会上,字节跳动副总裁杨震原在会上表示,火山引擎是把字节跳动内部的技术和工具开放给企业客户,由字节跳动的技术团队为外部客户提供服务。杨震原认为,把技术开放出去有助于提升科技公司的创新力。“当公司规模变得很大,有时候也会导致效率降低。因...
继续阅读 »

在近日召开的火山引擎品牌发布会上,字节跳动副总裁杨震原在会上表示,火山引擎是把字节跳动内部的技术和工具开放给企业客户,由字节跳动的技术团队为外部客户提供服务。杨震原认为,把技术开放出去有助于提升科技公司的创新力。

“当公司规模变得很大,有时候也会导致效率降低。因为没有人能够很好地规划一切,很难有一个‘天才架构师’去了解公司的方方面面,权衡各方面的优先级”,杨震原认为,公司在成长的过程中会达到一个临界值。超过这个值之后,只有引入一些市场机制,公司效率才会变得更高。

以亚马逊为例,亚马逊最强大的是它的AWS云计算能力,AWS就是把亚马逊核心技术开放给外部企业用,通过外部客户把AWS打磨得更好,既更好地服务内部客户,又给外部企业独立做大的机会。这个过程类似于种树,所以亚马逊的生态中长出了巨头企业,包括Uber、Airbnb等。

杨震原认为亚马逊是值得学习的楷模。公司本质是降低交易成本,提高效率,当公司规模变大后,技术团队需要市场化的管理。

“为什么会有公司,它的本质是什么?比如说,你需要一个程序员写代码,如果没有公司,你就要跟他结算,他写200行给他多少钱”,杨震原说,“但是有公司这个形态,你们形成一个小组,就可以对齐目标、使命、文化。他可以按月发工资,也可以考虑他的成长,交易成本变得很低。这就是为什么在市场上交易的是很多很多的公司,而非一个一个的个体”。

但是当公司规模逐步变大时,管理者很难规划所有团队的发展,必须让技术团队面对更大的市场、在更多的场景去服务外部的客户,才能打磨好团队,这也是字节跳动打造火山引擎为外部企业客户提供服务的重要动力。

据杨震原透露,火山引擎对外服务和对内服务在团队方面基本上是融合在一起的,或者说管理线的分叉很靠下,不是在比较高的管理层级就分对外或对内。以数据中台为例,细分到数据中台的一个子方向,才会有对外和对内的人员分工,相当于字节跳动是把技术中台直接市场化支持火山引擎。

收起阅读 »

手把手教你在Flutter项目优雅的使用ORM数据库

Flutter ORM数据库介绍Flutter现在开发上最大的槽点可能就是数据库使用了,Flutter现在只提供了sqflite插件,这表明开发者手动写sql代码,建表、建索引、transation、db线程控制等等繁琐的事情必然接踵而至,这种数据库使用方式是...
继续阅读 »

Flutter ORM数据库介绍

Flutter现在开发上最大的槽点可能就是数据库使用了,Flutter现在只提供了sqflite插件,这表明开发者手动写sql代码,建表、建索引、transation、db线程控制等等繁琐的事情必然接踵而至,这种数据库使用方式是最低效的了。例如IOS平台有coredata、realm等等的框架提供便捷的数据库操作,但来到flutter就又倒退回去裸写sql,这对大部分团队都是重大的成本。

本文将详细介绍一种在Flutter项目中优雅的使用ORM数据库的方法,我们使用的ORM框架是包含在一个Flutter插件flutter_luakit_plugin(如何使用可参考介绍文章)中的其中一个功能,本文只详细介绍这套ORM框架的使用和实现原理。我们给出了一个demo。

我们demo中实现了一个简单的功能,从一个天气网站上查询北京的天气信息,解析返回的json然后存数据库,下次启动优先从数据库查数据马上显示,再发请求向天气网站更新天气信息,就这么简单的一个功能。虽然功能简单,但是我们99%日常的业务逻辑也就是由这些简单的逻辑组成的了。下面是demo运行的效果图。


看完运行效果,我们开始看看ORM数据库的使用。ORM数据库的核心代码都是lua,其中WeatherManager.lua是业务逻辑代码,其他的lua文件是ORM数据库的核心代码,全部是lua实现的,所有代码文件加起来也就120k左右,非常轻量。

针对上面提到的天气信息的功能,我们来设计数据模型,从demo的展示我们看到每天天气信息包含几个信息,城市名、日出日落时间、最高温度、最低温度、风向、风力,然后为了区分是哪一天的数据,我们给每条信息加上个id的属性,作为主键。想好我们就开始定义第一个ORM数据模型,有几个必要的信息,db名,表名,后面的就是我们需要的各个字段了,我们提供IntegerField、RealField、BlobField、TextField、BooleandField。等常用的数据类型。weather 就是这个模型的名字,之后我们weather为索引使用这个数据模型。定义模型代码如下。

weather = {
__dbname__ = "test.db",
__tablename__ = "weather",
id = {"IntegerField",{unique = true, null = false, primary_key = true}},
wind = {"TextField",{}},
wind_direction = {"TextField",{}},
sun_info = {"TextField",{}},
low = {"IntegerField",{}},
high = {"IntegerField",{}},
city = {"TextField",{}},
},

定义好模型后,我们看看如何使用,我们跟着业务逻辑走,首先网络请求回来我们要生成模型对象存到数据库,分下面几步

获取模型对象

local Table = require('orm.class.table')
local _weatherTable = Table("weather”)

准备数据,建立数据对象

local t = {}
t.wind = flDict[v.fg]
t.wind_direction = fxDict[v.ff]
t.sun_info = v.fi
t.low = tonumber(v.fd)
t.high = tonumber(v.fc)
t.id = i
t.city = city
local weather = _weatherTable(t)

保存数据

weather:save()

读取数据

_weatherTable.get:all():getPureData()

是不是很简单,很优雅,什么建表、拼sql、transation、线程安全等等都不用考虑,傻瓜式地使用,一个业务就几行代码搞定。这里只演示了简单的存取,更多的select、update、联表等高级用法可参考db_test demo。

Flutter ORM数据库原理详解

好了,上面已经介绍完如何使用了,如果大家仅仅关心使用下面的可以不看了,如果大家想了解这套跨平台的ORM框架的实现原理,下面就会详细介绍,其实了解了实现原理,对大家具体业务使用还是很有好处的,虽然我感觉大家用的时候极少了解原理。

我们把orm框架分为三层接入层,cache层,db操作层,三个层分别处于对应的线程,具体可以参考下图。接入层可以在任意线程发起,接入层也是每次数据库操作的发起点,上面的demo所有操作都是在接入层,cache层,db操作层仅仅是ORM内部划分,对使用者来讲不需要关心cache层和db操作层。我们把所有的操作分成两种,db后续相关的,和db后续无关的。


db后续无关的操作是从接入层不同的线程进入到cache层的队列,所有操作在这个队列里先同步完成内存操作,然后即可马上返回接入层,异步再到db操作层进行db操作。db后续无关的操作包括 save、update、delete。

db后续相关的操作依赖db操作层操作的结果,这样的话就必须等真实的db操作完成了再返回接入层。db后续相关的操作包括select。

要做到这种数据同步,我们必须先把orm操作接口抽象化,只给几个常用的接口,所有操作都必须通过指定的接口来完成。我们总结了如下基本操作接口。

1、save

2、select where

3、select PrimaryKey

4、update where

5、update PrimaryKey

6、delete where

7、delete PrimaryKey

这七种操作只要在操作前返回前对内存中的cache做相应的处理,即可保证内存cache始终和db保持一致,这样以后我们就可以优先使用cache层的数据了。这七种操作的实现逻辑,这里先说明一下,cache里面的对象都是以主键为key,orm对象为value的形式存储在内存中的,这些控制逻辑是写在cache.lua里面的。

下面详细介绍七种基本操作的逻辑。

save操作,同步修改内存cache,然后马上返回接入层,再异步进行db replace into 的操作


where条件select,这个必须先同步到db线程获取查询结果,再同步修改内存里面的cache值,再返回给接入层


select PrimaryKey,就是选一定PrimaryKey值的orm对象,这个操作首先看cache里面是否有primarykey 值的orm对,如果有,直接返回,如果没有,先同步到db线程获取查询结果,再同步修改内存里面的cache值,再返回给接入层


update where,先同步到db线程通过where 条件select出需要update的主键值,根据主键值和需要update的内容,同步更新内存cache,然后异步进行db的update操作


update PrimaryKey,根据PrimaryKey进行update操作,先同步更新内存cache,然后异步进行db的update操作


delete where,先同步到db线程通过where 条件select出需要delete的主键值,根据主键值删除内存cache,然后异步进行db的delete操作


delete PrimaryKey,根据PrimaryKey进行delete操作,先同步删除内存cache,然后异步进行db的delete操作


只要保证上面七种基本操作逻辑,即可保证cache中的内容和db最终的内容是一致的,这种尽量使用cache的特性可以提升数据库操作的效率,而且保证同一个db的所有操作都在指定的cache线程和db线程里面完成,也可以保证线程安全。

最后,由于我们所有的db操作都集中起来了,我们可以定时的transation 保存,这样可以大幅提升数据库操作的性能。

结语

目前Flutter领域最大的痛点就是数据库操作,本文提供了一种优雅使用ORM数据库的方法,大幅降低了使用数据库的门槛。希望这篇文章和flutter_luakit_plugin可以帮到大家更方便的开发Flutter
应用。

链接:https://www.jianshu.com/p/62500ae08a07

收起阅读 »

纯 CSS 创建五彩斑斓的智慧阴影!让前景图片自动转化为对应彩色的背景阴影

几天前,我在 Home Depot(aka Toys "R" Us for big kids)处发现,他们有一个巨大的显示器来展示所有这些彩色的供销售的电灯泡!其中一项是y一组在电视后面的智能灯泡。它们会在电视的后面投影近似于电视在播出的内容的彩色阴影,与以下...
继续阅读 »

几天前,我在 Home Depot(aka Toys "R" Us for big kids)处发现,他们有一个巨大的显示器来展示所有这些彩色的供销售的电灯泡!其中一项是y一组在电视后面的智能灯泡。它们会在电视的后面投影近似于电视在播出的内容的彩色阴影,与以下内容 类似



注意电视后面发生的事情。屏幕前景中显示的颜色会被灯泡投影为电视机身后面的彩色阴影。随着屏幕上的颜色发生变化,投射在背景中的颜色也会发生变化。真的很酷,对吧?


自然,看到这个之后,我的第一个想法是,我们是否可以使用网络技术创建一个足够智能以模仿前景色的彩色阴影。事实证明,我们完全可以只使用 CSS 构建出这个案例。在本文中,我们将了解如何创建这种效果。


走起!


让它变成真的!


正如您将在以下部分中看到的,使用 CSS 创建这种彩色阴影似乎是一项艰巨的任务(当然,只是就刚开始而言)。当我们开始进入它并将这个任务的核心分解成更小的部分时,我们其实能够发现这真的很容易实现。在接下来的几节中,我们将创建以下示例:



你应该看到的是一张寿司的图片,后面出现了一个五颜六色的阴影。(只是为了强调我们正在做这一切,阴影被添加了脉冲的效果)抛开示例,让我们深入了解实现,看看 HTML 和 CSS 如何让这一切变为现实!


展示我们的照片


展示我们的寿司的图片对应的 HTML 起始没什么特别的:



<div class="parent">
<div class="colorfulShadow sushi"></div>
</div>


我们有一个父 div 元素,包含一个负责显示寿司的子 div 元素。我们显示寿司的方式是将其指定为背景图像,并由以下 .sushi 样式规则处理:


.sushi {
margin: 100px;
width: 150px;
height: 150px;
background-image: url("https://www.kirupa.com/icon/1f363.svg");
background-repeat: no-repeat;
background-size: contain;
}


在此样式规则中,我们将 div 的大小指定为 150 x 150 像素,并在其上设置 background-image 和相关的其他属性。就目前而言,我们所看到的 HTML 和 CSS 会给我们提供如下所示的内容:



现在是阴影时间


现在我们的图像出现了,剩下的就是我们定义阴影这一有趣的部分。我们要定义阴影的方法是指定一个子伪元素(使用 ::after),它将做三件事:



  1. 直接定位在我们的形象后面;

  2. 继承与父元素相同的背景图片;

  3. 依靠滤镜应用多彩的阴影效果;


这三件事是通过以下两条样式规则完成的:


.colorfulShadow {
position: relative;
}

.colorfulShadow::after {
content: "";
width: 100%;
height: 100%;
position: absolute;
background: inherit;
background-position: center center;
filter: drop-shadow(0px 0px 10px rgba(0, 0, 0, 0.50)) blur(20px);
z-index: -1;
}


让我们花一点时间来看看这里发生了些什么:先注意每一个属性和对应的值,有一些值得注意的标记是 backgroundfilterbackground 属性使用了 inherit 继承父元素,意味着能够继承父元素的背景:


.colorfulShadow::after {
content: "";
width: 100%;
height: 100%;
position: absolute;
background: inherit;
background-position: center center;
filter: drop-shadow(0px 0px 10px rgba(0, 0, 0, 0.50)) blur(20px);
z-index: -1;
}


我们为 filter 属性定义了两个过滤的属性,分别是 drop-shadowblur


.colorfulShadow::after {
content: "";
width: 100%;
height: 100%;
position: absolute;
background: inherit;
background-position: center center;
filter: drop-shadow(0px 0px 10px rgba(0, 0, 0, 0.50)) blur(20px);
z-index: -1;
}


我们的 drop-shadow 过滤器设置为显示不透明度为 50% 的黑色阴影,而我们的 blur 过滤器会将我们的伪元素模糊 20px。 这两个过滤器的组合最终创建了彩色的阴影,当应用这两个样式规则时,该阴影现在将出现在我们的寿司图像后面:



在这一点上,我们已经完成了。为完整起见,如果我们想要彩色阴影缩放的动画,如下 CSS 代码的添加能够助力我们实现目标:


.colorfulShadow {
position: relative;
}

.colorfulShadow::after {
content: "";
width: 100%;
height: 100%;
position: absolute;
background: inherit;
background-position: center center;
filter: drop-shadow(0px 0px 10px rgba(0, 0, 0, 0.50)) blur(20px);
z-index: -1;

/* animation time! */
animation: oscillate 1s cubic-bezier(.17, .67, .45, 1.32) infinite alternate;
}

@keyframes oscillate {
from {
transform: scale(1, 1);
}

to {
transform: scale(1.3, 1.3);
}
}


如果您想要一些交互性而没有不断循环的动画,您还可以使用 CSS 过渡来更改阴影在某些动作(如悬停)上的行为方式。困难的部分是像对待在 HTML 中明确定义或使用 JavaScript 动态创建的任何其他元素一样对待伪元素。唯一的区别是这个元素是完全使用 CSS 创建的!


结语


小结


伪元素允许我们使用 CSS 来完成一些历史上属于 HTML 和 JavaScript 领域的元素创建任务。对于我们多彩而智能的阴影,我们能够依靠父元素来设置背景图像。这使我们能够轻松定义一个既继承了父元素的背景图像细节,又允许我们为其设置一系列属性以实现模糊和阴影效果的子伪元素。虽然所有这些都很好,并且我们最大限度地减少了大量复制和粘贴,但这种方法不是很灵活。


如果我想将这样的阴影应用到一个不只是带有背景图像的空元素上怎么办?如果我有一个像 ButtonComboBox 这样的 HTML 元素想要应用这种阴影效果怎么办?一种解决方案是依靠 JavaScript 在 DOM 中复制适当的元素,将它们放置在前景元素下方,应用过滤器,然后就可以了。虽然这有效,但考虑到该过程的复杂程度,实在是有些不寒而栗。太糟糕了,JavaScript 没有等效的 renderTargetBitmap 这种能够把我们的视觉效果渲染成位图,然后你可以做任何你想做的事的 API…… 🥶


以上内容为译文翻译,下面为一些拓展:




拓展


说实在的,我们其实并不需要那么多复杂的内容,图片可以是任意的,比如说 PNG、SVG,最终精简后,HTML 代码仅仅为任意一个元素,附上 style 规定图片地址与大小:


<div class="shadowedImage" style="--data-width: 164px; --data-height: 48px; --data-image: url('https://sf3-scmcdn2-tos.pstatp.com/xitu_juejin_web/dcec27cc6ece0eb5bb217e62e6bec104.svg');"></div>


CSS 代码如下:


.shadowedImage {
position: relative;
margin: 100px;
width: var(--data-width);
height: var(--data-height);
max-height: 150px;
background-image: var(--data-image);
background-repeat: no-repeat;
background-size: contain;
}

.shadowedImage::after {
content: "";
width: 100%;
height: 100%;
position: absolute;
background: inherit;
background-position: center center;
filter: drop-shadow(0px 0px 10px rgba(0, 0, 0, 0.50)) blur(20px);
z-index: -1;
}


示例代码


一段示例代码如下:


<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>

<style>
.shadowedImage {
position: relative;
}

.shadowedImage::after {
content: "";
width: 100%;
height: 100%;
position: absolute;
background: inherit;
background-position: center center;
filter: drop-shadow(0px 0px 10px rgba(0, 0, 0, 0.50)) blur(20px);
z-index: -1;

/* animation time! */
animation: oscillate 1s cubic-bezier(.17, .67, .45, 1.32) infinite alternate;
}

@keyframes oscillate {
from {
transform: scale(1, 1);
}

to {
transform: scale(1.1, 1.1);
}
}

.shadowedImage {
margin: 100px;
width: var(--data-width);
height: var(--data-height);
max-height: 150px;
background-image: var(--data-image);
background-repeat: no-repeat;
background-size: contain;
}
</style>
</head>
<body>
<div class="parent">
<div class="shadowedImage" style="--data-width: 164px; --data-height: 48px; --data-image: url('https://sf3-scmcdn2-tos.pstatp.com/xitu_juejin_web/dcec27cc6ece0eb5bb217e62e6bec104.svg');"></div>
<div class="shadowedImage" style="--data-width: 164px; --data-height: 164px; --data-image: url('https://sf1-dycdn-tos.pstatp.com/img/bytedance-cn/4ac74bbefc4455d0b350fff1fcd530c7~noop.image');"></div>
<div class="shadowedImage" style="--data-width: 164px; --data-height: 164px; --data-image: url('https://sf1-ttcdn-tos.pstatp.com/img/bytedance-cn/4bcac7e2843bd01c3158dcaefda77ada~noop.image');"></div>
</div>
</body>
</html>


示例效果


效果如下:


image.png



如果发现译文存在错误或其他需要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可获得相应奖励积分。文章开头的 本文永久链接 即为本文在 GitHub 上的 MarkDown 链接。





链接:https://juejin.cn/post/6975818153376874503

收起阅读 »

小程序自定义TabBar 如何实现“keep-alive”

自定义TabBar方案 虽然在之前文章提到过了,本次采用组件化实现,具体实现如下: 我们可以新建一个home文件夹,在home/index.wxml中写一个tabBar,然后把TabBar页面写成组件,然后点击TabBar切换相应的组件展示就可以。代码...
继续阅读 »

自定义TabBar方案



虽然在之前文章提到过了,本次采用组件化实现,具体实现如下:





  • 我们可以新建一个home文件夹,在home/index.wxml中写一个tabBar,然后把TabBar页面写成组件,然后点击TabBar切换相应的组件展示就可以。代码如下:




  • wxml部分




<!-- home页面 -->

<view id='index'>
<!-- 自定义头部 -->
<head name='{{name}}' bgshow="{{bgshow}}" backShow='false'></head>

<!-- 首页 -->
<index change='{{activeIndex==0}}'></index>
<!-- 购物车 -->
<cart change='{{activeIndex==1}}'></cart>
<!-- 订单 -->
<order change='{{activeIndex==2}}'></order>
<!-- 我的 -->
<my change='{{activeIndex==2}}'></my>
<!-- tabbar -->
<view class="tab ios">
<view class="items {{activeIndex==index?'active':''}}" wx:for="{{tab}}" bindtap="choose" data-index='{{index}}' wx:key='index' wx:for-item="items">
<image wx:if="{{activeIndex==index}}" src="{{items.activeImage}}"></image>
<image wx:else src="{{items.image}}"></image>
<text>{{items.name}}</text>
</view>
</view>
</view>




  • home页面的ts


Page({
data: {
activeIndex:0,
tab:[
{
name:'商品',
image:'../../images/index.png',
activeImage:'../../images/index-hover.png',
},
{
name:'购物车',
image:'../../images/cart.png',
activeImage:'../../images/cart-hover.png',
},
{
name:'订单',
image:'../../images/order.png',
activeImage:'../../images/order-hover.png',
},
{
name:'我的',
image:'../../images/my.png',
activeImage:'../../images/my-hover.png',
}
]
},
// 切换事件
choose(e:any){
const _this=this;
const {activeIndex}=_this.data;
if(e.currentTarget.dataset.index==activeIndex){
return
}else{
_this.setData({
activeIndex:e.currentTarget.dataset.index
})
}
},
})




  • 上面代码不难理解,点击以后改变activeIndex从而控制每个组件的渲染和销毁,这样付出的代价还是比较大的,需要我们进一步的优化。


如何实现keep-alive



我们知道,这里主要是避免组件反复创建和渲染,有效提升系统性能。



实现思路




  • 1.在tab每个选项增加两个值:statusshowshow控制组件是否需要渲染,status控制组件display




  • 2.初始化时候设置首页的statusshow,其他都为false




  • 3.当我们切换时:把上一个tab页面的status改为false,然后把当前要切换页面的tab数据中的statusshow都改为true,最后再更新一下activeIndex的值。




  • wxml代码:




    <!-- 首页 -->
<view wx:if="{{tab[0].show}}" hidden="{{!tab[0].status}}">
<index></index>
</view>
<!-- 购物车 -->
<view wx:if="{{tab[1].show}}" hidden="{{!tab[1].status}}">
<cart></cart>
</view>
<!-- 订单 -->
<view wx:if="{{tab[2].show}}" hidden="{{!tab[2].status}}">
<order></order>
</view>
<!-- 我的 -->
<view wx:if="{{tab[3].show}}" hidden="{{!tab[3].status}}">
<my></my>
</view>



  • ts代码


Page({
data: {
activeIndex:0, //当前选中的index
tab:[
{
name:'商品',
image:'../../images/index.png',
activeImage:'../../images/index-hover.png',
status:true,//控制组件的display
show:true, //控制组件是否被渲染
},
{
name:'购物车',
image:'../../images/cart.png',
activeImage:'../../images/cart-hover.png',
status:false,
show:false,
},
{
name:'订单',
image:'../../images/order.png',
activeImage:'../../images/order-hover.png',
status:false,
show:false,
},
{
name:'我的',
image:'../../images/my.png',
activeImage:'../../images/my-hover.png',
status:false,
show:false,
}
]
},

choose(e:any){
const _this=this;
const {activeIndex}=_this.data;
//如果点击的选项是当前选中,就不执行
if(e.currentTarget.dataset.index==activeIndex){
return
}else{
//修改上一个tab页面的status
let prev='tab['+activeIndex+'].status',
//修改当前选中元素的status
status='tab['+e.currentTarget.dataset.index+'].status',
//修改当前选中元素的show
show='tab['+e.currentTarget.dataset.index+'].show';

_this.setData({
[prev]:false,
[status]:true,
[show]:true,
activeIndex:e.currentTarget.dataset.index,//更新activeIndex
})
}
},

})




  • 这样基本就大功告成了,来看一下效果:


Rp63gH.gif



  • 当我们点击切换时候,如果当前组件没有渲染就会进行渲染,如果渲染过后进行切换只是改变display,完美实现了需求,大功告成!


实际业务场景分析



在实际使用中还有两种种情况:



情况1:比如某些数据并不希望他首次加载后就数据保持不变,当切换页面时候希望数据进行更新,比如笔者做的电商小程序,在首页点击商品加入购物车,然后切换到购物车,每次切换时候肯定需要再次进行请求。

情况2:像个人中心这种页面,数据基本请求一次就可以,没必要每次切换请求数据,这种我们不需要进行改进。




  • 我们给组件传递一个值:status,然后在组件中监听这个值的变化,当值为true时候,去请求接口更新数据。具体代码如下:




  • wxml代码(只列举关键部分):




<!-- 首页 -->
<view wx:if="{{tab[0].show}}" hidden="{{!tab[0].status}}">
<index change='{{tab[0].status}}'></index>
</view>

<!-- 购物车 -->
<view wx:if="{{tab[1].show}}" hidden="{{!tab[1].status}}">
<cart change='{{tab[0].status}}'></cart>
</view>



  • 首页组件/购物车组件ts代码:


Component({
/**
* 组件的属性列表
*/
properties: {
change: {
type: String,//类型
value: ''//默认值
},
},
observers: {
//监听数据改变进行某种操作
'change': function(change) {
if(change=='true'){
console.log('更新首页数据'+change)
}
}
},
})



  • 来看一下最终效果:


Rp618e.gif



链接:https://juejin.cn/post/6976072584127315999

收起阅读 »

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分析静态内容是否有可以优化的地方,比如图片缓存、单例、内存缓存等。
收起阅读 »

环信IM会话列表和聊天界面修改头像和昵称

如何修改会话列表和聊天界面的头像和昵称?方法简单,但这里先说明一下设计思路:MVVMModel view viewModel思路明确后,我们需要拿到其中的viewModel,然后修改其中的值.会话列表控制器和viewModel聊天控制器和viewModel如果...
继续阅读 »

如何修改会话列表和聊天界面的头像和昵称?


方法简单,但这里先说明一下设计思路:

MVVM

Model view viewModel

思路明确后,我们需要拿到其中的viewModel,然后修改其中的值.




会话列表控制器和viewModel



聊天控制器和viewModel


如果我们不考虑其中的结构/思路/思想,单纯为了解决问题,那么上述截图已经可以解决问题了.


我的理解:
为什么返回的viewModel一定是遵循某协议的?



我们正常理解的协议是:制定协议,指定委托,实现协议方法.

小了!格局小了!

当我思考上面截图这个协议之后.才明白,这里的协议是为了要求子类遵循标准.

这里协议本意并非是为了让实现什么,而是为了限定参数类型/参数名.是对数据模型的一种约束.

对于一个类型,无论是这个类型持有的方法还是属性,都是其特有的特点,既然是特点,便可继承.而这些方法啊,属性啊,不都是对此类型的一种约束吗?所以,我们可以看做 类型持有其特有的属性和方法,一些属性和一些方法约束了某一个类型.

如果同时了解java的同学都知道.java中有一个类型关键字为interface,我们称之为接口类,抽象类的一种,那么本意指的是,它也是一个类,只是无法实例化.

回头再看oc语言中的protocol,不就是java中的interface吗?

看到如此高质量的demo,使我的技术提升很大.多看大神的代码和多思考其思路,都是学习机会.

收起阅读 »

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
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

iOS开发中的小玩意儿-加速计和陀螺仪

前言最近因为工作需要对加速计和陀螺仪进行学习和了解,过程中有所收获。正文一、加速计iPhone在静止时会受到地球引力,以屏幕中心为坐标原点,建立一个三维坐标系(如右图),此时iPhone收到的地球引力会分布到三个轴上。iOS开发者可以通过CoreMotion框...
继续阅读 »

前言

最近因为工作需要对加速计和陀螺仪进行学习和了解,过程中有所收获。

正文

一、加速计

iPhone在静止时会受到地球引力,以屏幕中心为坐标原点,建立一个三维坐标系(如右图),此时iPhone收到的地球引力会分布到三个轴上。
iOS开发者可以通过CoreMotion框架获取分布到三个轴的值。如果iPhone是如图放置,则分布情况为x=0,y=-1.0,z=0。
在CoreMotion中地球引力(重力)的表示为1.0。

手机如果屏幕朝上的放在水平桌面上,此时的(x,y,z)分布是什么?


上面答案是(0,0, -1.0);

如何检测手机的运动?
CoreMotion框架中有CMDeviceMotion类,其中的gravity属性用来描述前面介绍的重力;另外的userAcceleration是用来描述手机的运动。
当手机不动时,userAcceleration的(x, y, z)为(0, 0, 0);
当手机运动,比如在屏幕水平朝上的自由落体时,检测到的(x, y, z)将为(0, 0, 1);
当手机屏幕水平朝上,往屏幕左边以9.8m/s2的加速度运动时,检测到的(x, y, z)将为(1, 0, 0);

1、gravity是固定不变,因为地球引力的不变;但是xyz的分布会变化,收到手机朝向的影响;
2、userAcceleration是手机的运动相关属性,但是检测到的值为运动加速度相反的方向;
3、一种理解加速计的方式:在水平的路上有一辆车,车上有一个人;当车加速向右运动时,人会向左倾斜;此时可以人不需要知道外面的环境如何,根据事先在车里建立好的方向坐标系,可以知道车在向右加速运动。

二、加速计的简单应用

图片悬浮
手机旋转,但是图片始终保持水平。


实现流程
1、加载图片,创建CMMotionManager;
2、监听地球重力的变化,根据x和y轴的重力变化计算出来手机与水平面的夹角;
3、将图片逆着旋转相同的角度;
x、y轴和UIKit坐标系相反,原点在屏幕中心,向上为y轴正方向,向右为x轴正方向,屏幕朝外是z轴正方向;
在处理图片旋转角度时需要注意。

三、陀螺仪

如图,建立三维坐标系;
陀螺仪描述的是iPhone关于x、y、z轴的旋转速率;
静止时(x, y, z)为(0, 0, 0);
当右图手机绕Y轴正方向旋转,速率为每秒180°,则(x, y, z)为(0, 0, 3.14);


陀螺仪和加速计是同样的坐标系,但是新增了旋转的概念,可以用右手法则来辅助记忆;
陀螺仪回调结构体的单位是以弧度为单位,这个不是加速度而是速率;

四、CoreMotion的使用
CoreMotion的使用有两种方式 :

1、Push方式:设置间隔,由manager不断回调;

self.motionManager = [[CMMotionManager alloc] init];
self.motionManager.deviceMotionUpdateInterval = 0.2;
[self.motionManager startDeviceMotionUpdatesToQueue:[NSOperationQueue mainQueue]
withHandler:^(CMDeviceMotion * _Nullable motion, NSError * _Nullable error) {

}];

2、Pull方式:启动监听,自定义定时器,不断读取manager的值;

self.motionManager = [[CMMotionManager alloc] init];
self.motionManager.deviceMotionUpdateInterval = 0.2;
[self.motionManager startDeviceMotionUpdates];
// self.motionManager.deviceMotion 后续通过这个属性可以直接读取结果

iOS系统在监听到运动信息的时候,需要把信息回调给开发者,方式就有push和pull两种;
push 是系统在规定的时间间隔,不断的回调;
pull 是由开发则自己去读取结果值,但同样需要设定一个更新频率;
两种方式的本质并无太大区别,都需要设置回调间隔,只是读取方式的不同;
在不使用之后(比如说切后台)要关闭更新,这是非常耗电量的操作。

五、demo实践

基于加速计,做了一个小游戏,逻辑不复杂详见具体代码,分享几个处理逻辑:

1、圆球的边界处理;(以球和右边界的碰撞为例)

if (self.ballView.right > self.gameContainerView.width) {
self.ballView.right = self.gameContainerView.width;
self.ballSpeedX /= -1;
}

2、圆球是否触碰目标的检测;

- (BOOL)checkTarget {
CGFloat disX = (self.ballView.centerX - self.targetView.centerX);
CGFloat disY = (self.ballView.centerY - self.targetView.centerY);
return sqrt(disX * disX + disY * disY) <= (kConstBallLength / 2 + kConstTargetLength / 2);
}

3、速度的平滑处理;

static CGFloat lySlowLowPassFilter(NSTimeInterval elapsed,
GLfloat target,
GLfloat current) {
return current + (4.0 * elapsed * (target - current));
}


总结

加速计和陀螺仪的原理复杂但使用简单,实际应用也比较广。
之前就用过加速计和陀螺仪,但是没有系统的学习过。在完整的学习一遍之后,我才知道原来加速计的单位是以重力加速度(9.8 m/s2)为标准单位,陀螺仪的数据仅仅是速率,单位是弧度每秒。
上面的小游戏代码地址在Github

链接:https://www.jianshu.com/p/6d6b213912f5

收起阅读 »

当前端基建任务落到你身上,该如何推动协作?

前言 作为一名野生的前端开发,自打本猿入行起,就未经过什么系统的学习,待过的团队也是大大小小没个准儿: 要么大牛带队,但是后端大牛。要么临时凑的团队,受制于从前,前端不自由。要么从0到项目部署,都是为了敏捷而敏捷,颇不规范。 话虽如此,经过4年生涯摧残的废猿...
继续阅读 »

前言


作为一名野生的前端开发,自打本猿入行起,就未经过什么系统的学习,待过的团队也是大大小小没个准儿:


要么大牛带队,但是后端大牛。
要么临时凑的团队,受制于从前,前端不自由。
要么从0到项目部署,都是为了敏捷而敏捷,颇不规范。


话虽如此,经过4年生涯摧残的废猿我,也是有自己的一番心得体会的。


1. 从DevOps流程看前端基建



很多专注于切图的萌新前端看到这张图是蒙圈的:


DevOps是什么?这些工具都是啥?我在哪?


很多前端在接触到什么前端工程化,什么持续构建/集成相关知识时就犯怂。也有觉得这与业务开发无关,不必理会。


但是往长远想,切图是不可能一辈子切图的,你业务再怎么厉害,前端代码再如何牛,没有了后端运维测试大佬们相助,一个完整的软件生产周期就没法走完。


成为一名全栈很难,更别说全链路开发者了。


言归正传,当你进入一个新团队,前端从0开始,怎样从DevOps的角度去提高团队效能呢?



一套简易的DevOps流程包含了协作、构建、测试、部署、运行。


而前端常说的开发规范、代码管理、测试、构建部署以及工程化其实都是在这一整个体系中。


当然,中小团队想玩好DevOps整套流程,需要的时间与研发成本,不比开发项目少。


DevOps核心思想就是:“快速交付价值,灵活响应变化”。其基本原则如下:


高效的协作和沟通;
自动化流程和工具;
快速敏捷的开发;
持续交付和部署;
不断学习和创新。


接下来我将从协作、构建、测试、部署、运行五个方面谈谈,如何快速打造用于中小团队的前端基建。


2. 在团队内/外促进协作


前端基建协作方面可以写的东西太多了,暂且粗略分为:团队内 与 团队外。



以下可能是前端们都能遇到的问题:


成员间水平各异,编写代码的风格各不相同,项目间难以统一管理。
不同项目Webpack配置差异过大,基础工具函数库和请求封装不一样。
项目结构与技术栈上下横跳,明明是同一UI风格,基础组件没法复用,全靠复制粘贴。
代码没注释,项目没文档,新人难以接手,旧项目无法维护。


三层代码规范约束



  • 第一层,ESLint


常见的ESLint风格有:airbnb,google,standard


在多个项目间,规则不应左右横跳,如果项目周期紧张,可以适当放宽规则,让warning类弱警告可以通过。且一般建议成员的IDE和插件要统一,将客观因素影响降到最低。



  • 第二层,Git Hooks


git 自身包含许多 hooks,在 commitpushgit 事件前后触发执行。


husky能够防止不规范代码被commitpushmerge等等。


代码提交不规范,全组部署两行泪。


npm install husky pre-commit  --save-dev


拿我以前的项目为例子:


// package.json
"scripts": {
// ...
"lint": "node_modules/.bin/eslint '**/*.{js,jsx}' && node_modules/.bin/stylelint '**/*.{css,scss}'",
"lint:fix": "node_modules/.bin/eslint '**/*.{js,jsx}' --fix && node_modules/.bin/stylelint '**/*.{css,scss}' --fix"
},
"husky": {
"hooks": {
"pre-commit": "npm run lint",
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
},


通过简单的安装配置,无论你通过命令行还是Sourcetree提交代码,都需要通过严格的校验。



建议在根目录README.md注明提交规范:


## Git 规范

使用 [commitlint](https://github.com/conventional-changelog/commitlint
) 工具,常用有以下几种类型:

-
feat :新功能
- fix :修复 bug
- chore :对构建或者辅助工具的更改
- refactor :既不是修复 bug 也不是添加新功能的代码更改
- style :不影响代码含义的更改 (例如空格、格式化、少了分号)
- docs : 只是文档的更改
- perf :提高性能的代码更改
- revert :撤回提交
- test :添加或修正测试

举例
git commit -m 'feat: add list'



  • 第三层,CI(持续集成)。



《前端代码规范最佳实践》



前两步的校验可以手动跳过(找骂),但CI中的校验是绝对绕不过的,因为它在服务端校验。使用 gitlab CI 做持续集成,配置文件 .gitlab-ci.yaml 如下所示:


lint:
stage:lint
only:
-/^feature\/.*$/
script:
-npmlint


这层校验,一般在稍大点的企业中,会由运维部的配置组完成。



统一前端物料


公共组件、公共UI、工具函数库、第三方sdk等该如何规范?


如何快速封装部门UI组件库?

  • 将业务从公共组件中抽离出来。

  • 在项目中安装StoryBook(多项目时另起)

  • 按官方文档标准,创建stories,并设定参数(同时也建议先写Jest测试脚本),写上必要的注释。

  • 为不同组件配置StoryBook控件,最后部署。

如何统一部门所用的工具函数库和第三方sdk


其实这里更多的是沟通的问题,首先需要明确的几点:



  • 部门内对约定俗成的工具库要有提前沟通,不能这头装一个MomentJs,另一头又装了DayJS。一般的原则是:轻量的自己写,超过可接受大小的找替代,譬如:DayJS替代MomentJsImmerJS替代immutableJS等。

  • 部门间的有登录机制,请求库封装协议等。如果是SSO/扫码登录等,就协定只用一套,不允许后端随意变动。如果是请求库封装,就必须要后端统一Restful风格,相信我,不用Restful规范的团队都是灾难。前端联调会生不如死。

  • Mock方式、路由管理以及样式写法也应当统一。


在团队外促进协作


核心原则就是:“能用文档解决的就尽量别BB。”


虽说现今前端的地位愈发重要,但我们经常在项目开发中遇到以下问题:


不同的后端接口规范不一样,前端需要耗费大量时间去做数据清洗兼容。
前端静态页开发完了,后端迟迟不给接口,因为没有接口文档,天天都得问。
测试反馈的问题,在原型上没有体现。


首先是原型方面:

  • 一定要看明白产品给的原型文档!!!多问多沟通,这太重要了。

  • 好的产品一般都会提供项目流程详图,但前端还是需要基于实际,做一张页面流程图。

  • 要产品提供具体字段类型相关定义,不然得和后端扯皮。。。

其次是后端:

执行Restful接口规范,不符合规范的接口驳回。

劝退师就经历过,前东家有个JAVA架构师,连跨域和Restful都不知道,定的规范不成规范,一个简单查询接口返回五六级,其美名曰:“结构化数据”

遇到这种沉浸于自己世界不听劝的后端,我只有一句劝:要么把他搞走,要么跑路吧

必要的接口文档站点与API测试(如SwaggerApidoc),不接受文件传输形式的接口

早期的联调都是通过呐喊告知对方接口的标准。刚开始有什么不清楚的直接问就好了,但是到了后面的时候连写接口代码的那个人都忘了这接口怎么用,维护成本巨高

在没有接口文档站点出现前,接口文档以word文档出现,辅以postmanhttpcurl等工具去测试。但仍然不够直观,维护起来也难

以web交互为主的Swagger解决了测试,维护以及实时性的问题。从一定程度上也避免了扯皮问题:只有你后端没更新文档,这联调滞后时间就不该由前端担起。

最后是运维方面:

除了CI/CD相关的,其实很可以和运维一起写写nginx和插件开发。

效率沟通工具


可能大家比较习惯的是使用QQ或者微信去传输文件,日常沟通还行,就是对开发者不太友好。


如何是跨国家沟通,一般都是建议jira+slack的组合,但这两个工具稍微有些水土不服。


这四个工具随意选择都不会有太大问题。


链接:https://juejin.cn/post/6844904145602740231

收起阅读 »

手把手带你入门Webpack Plugin

关于 Webpack 在讲 Plugin 之前,我们先来了解下 Webpack。本质上,Webpack 是一个用于现代 JavaScript 应用程序的静态模块打包工具。它能够解析我们的代码,生成对应的依赖关系,然后将不同的模块达成一个或多个 bundle。 ...
继续阅读 »

关于 Webpack


在讲 Plugin 之前,我们先来了解下 Webpack。本质上,Webpack 是一个用于现代 JavaScript 应用程序的静态模块打包工具。它能够解析我们的代码,生成对应的依赖关系,然后将不同的模块达成一个或多个 bundle。



Webpack 的基本概念包括了如下内容:



  1. Entry:Webpack 的入口文件,指的是应该从哪个模块作为入口,来构建内部依赖图。

  2. Output:告诉 Webpack 在哪输出它所创建的 bundle 文件,以及输出的 bundle 文件该如何命名、输出到哪个路径下等规则。

  3. Loader:模块代码转化器,使得 Webpack 有能力去处理除了 JS、JSON 以外的其他类型的文件。

  4. Plugin:Plugin 提供执行更广的任务的功能,包括:打包优化,资源管理,注入环境变量等。

  5. Mode:根据不同运行环境执行不同优化参数时的必要参数。

  6. Browser Compatibility:支持所有 ES5 标准的浏览器(IE8 以上)。


了解完 Webpack 的基本概念之后,我们再来看下,为什么我们会需要 Plugin。


Plugin 的作用


我先举一个我们政采云内部的案例:


在 React 项目中,一般我们的 Router 文件是写在一个项目中的,如果项目中包含了许多页面,不免会出现所有业务模块 Router 耦合的情况,所以我们开发了一个 Plugin,在构建打包时,该 Plugin 会读取所有文件夹下的 index.js 文件,再合并到一起形成一个统一的 Router 文件,轻松解决业务耦合问题。这就是 Plugin 的应用(具体实现会在最后一小节说明)。


来看一下我们合成前项目代码结构:


├── package.json
├── README.md
├── zoo.config.js
├── .eslintignore
├── .eslintrc
├── .gitignore
├── .stylelintrc
├── buildWebpack 配置目录)
│ └── webpack.dev.conf.js
├── src
│ ├── index.hbs
│ ├── main.js (入口文件)
│ ├── common (通用模块,包权限,统一报错拦截等)
│ └── ...
│ ├── components (项目公共组件)
│ └── ...
│ ├── layouts (项目顶通)
│ └── ...
│ ├── utils (公共类)
│ └── ...
│ ├── routes (页面路由)
│ │ ├── Hello (对应 Hello 页面的代码)
│ │ │ ├── config (页面配置信息)
│ │ │ └── ...
│ │ │ ├── modelsdva数据中心)
│ │ │ └── ...
│ │ │ ├── services (请求相关接口定义)
│ │ │ └── ...
│ │ │ ├── views (请求相关接口定义)
│ │ │ └── ...
│ │ │ └── index.jsrouter定义的路由信息)
├── .eslintignore
├── .eslintrc
├── .gitignore
└── .stylelintrc


再看一下经过 Plugin 合成 Router 之后的结构:


├── package.json
├── README.md
├── zoo.config.js
├── .eslintignore
├── .eslintrc
├── .gitignore
├── .stylelintrc
├── buildWebpack 配置目录)
│ └── webpack.dev.conf.js
├── src
│ ├── index.hbs
│ ├── main.js (入口文件)
│ ├── router-config.js (合成后的router文件)
│ ├── common (通用模块,包权限,统一报错拦截等)
│ └── ...
│ ├── components (项目公共组件)
│ └── ...
│ ├── layouts (项目顶通)
│ └── ...
│ ├── utils (公共类)
│ └── ...
│ ├── routes (页面路由)
│ │ ├── Hello (对应 Hello 页面的代码)
│ │ │ ├── config (页面配置信息)
│ │ │ └── ...
│ │ │ ├── modelsdva数据中心)
│ │ │ └── ...
│ │ │ ├── services (请求相关接口定义)
│ │ │ └── ...
│ │ │ ├── views (请求相关接口定义)
│ │ │ └── ...
├── .eslintignore
├── .eslintrc
├── .gitignore
└── .stylelintrc


总结来说 Plugin 的作用如下:



  1. 提供了 Loader 无法解决的一些其他事情

  2. 提供强大的扩展方法,能执行更广的任务


了解完 Plugin 的大致作用之后,我们来聊一聊如何创建一个 Plugin。


创建一个 Plugin


Hook


在聊创建 Plugin 之前,我们先来聊一下什么是 Hook。


Webpack 在编译的过程中会触发一系列流程,而在这样一连串的流程中,Webpack 把一些关键的流程节点暴露出来供开发者使用,这就是 Hook,可以类比 React 的生命周期钩子。


Plugin 就是在这些 Hook 上暴露出方法供开发者做一些额外操作,在写 Plugin 的时候,也需要先了解我们应该在哪个 Hook 上做操作。


如何创建 Plugin


我们先来看一下 Webpack 官方给的案例:


const pluginName = 'ConsoleLogOnBuildWebpackPlugin';

class ConsoleLogOnBuildWebpackPlugin {
apply(compiler) {
// 代表开始读取 records 之前执行
compiler.hooks.run.tap(pluginName, compilation => {
console.log("webpack 构建过程开始!");
});
}
}


从上面的代码我们可以总结如下内容:



  • Plugin 其实就是一个类。

  • 类需要一个 apply 方法,执行具体的插件方法。

  • 插件方法做了一件事情就是在 run 这个 Hook 上注册了一个同步的打印日志的方法。

  • apply 方法的入参注入了一个 compiler 实例,compiler 实例是 Webpack 的支柱引擎,代表了 CLI 和 Node API 传递的所有配置项。

  • Hook 回调方法注入了 compilation 实例,compilation 能够访问当前构建时的模块和相应的依赖。


Compiler 对象包含了 Webpack 环境所有的的配置信息,包含 options,loaders,plugins 这些信息,这个对象在 Webpack 启动时候被实例化,它是全局唯一的,可以简单地把它理解为 Webpack 实例;

Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。当 Webpack 以开发模式运行时,每当检测到一个文件变化,一次新的 Compilation 将被创建。Compilation 对象也提供了很多事件回调供插件做扩展。通过 Compilation 也能读取到 Compiler 对象。
—— 摘自「深入浅出 Webpack」



  • compiler 实例和 compilation 实例上分别定义了许多 Hooks,可以通过 实例.hooks.具体Hook 访问,Hook 上还暴露了 3 个方法供使用,分别是 tap、tapAsync 和 tapPromise。这三个方法用于定义如何执行 Hook,比如 tap 表示注册同步 Hook,tapAsync 代表 callback 方式注册异步 hook,而 tapPromise 代表 Promise 方式注册异步 Hook,可以看下 Webpack 中关于这三种类型实现的源码,为方便阅读,我加了些注释。


// tap方法的type是sync,tapAsync方法的type是async,tapPromise方法的type是promise
// 源码取自Hook工厂方法:lib/HookCodeFactory.js
create(options) {
this.init(options);
let fn;
// Webpack 通过new Function 生成函数
switch (this.options.type) {
case "sync":
fn = new Function(
this.args(), // 生成函数入参
'"use strict";\n' +
this.header() + // 公共方法,生成一些需要定义的变量
this.contentWithInterceptors({ // 生成实际执行的代码的方法
onError: err => `throw ${err};\n`, // 错误回调
onResult: result => `return ${result};\n`, // 得到值的时候的回调
resultReturns: true,
onDone: () => "",
rethrowIfPossible: true
})
);
break;
case "async":
fn = new Function(
this.args({
after: "_callback"
}),
'"use strict";\n' +
this.header() + // 公共方法,生成一些需要定义的变量
this.contentWithInterceptors({
onError: err => `_callback(${err});\n`, // 错误时执行回调方法
onResult: result => `_callback(null, ${result});\n`, // 得到结果时执行回调方法
onDone: () => "_callback();\n" // 无结果,执行完成时
})
);
break;
case "promise":
let errorHelperUsed = false;
const content = this.contentWithInterceptors({
onError: err => {
errorHelperUsed = true;
return `_error(${err});\n`;
},
onResult: result => `_resolve(${result});\n`,
onDone: () => "_resolve();\n"
});
let code = "";
code += '"use strict";\n';
code += this.header(); // 公共方法,生成一些需要定义的变量
code += "return new Promise((function(_resolve, _reject) {\n"; // 返回的是 Promise
if (errorHelperUsed) {
code += "var _sync = true;\n";
code += "function _error(_err) {\n";
code += "if(_sync)\n";
code +=
"_resolve(Promise.resolve().then((function() { throw _err; })));\n";
code += "else\n";
code += "_reject(_err);\n";
code += "};\n";
}
code += content; // 判断具体执行_resolve方法还是执行_error方法
if (errorHelperUsed) {
code += "_sync = false;\n";
}
code += "}));\n";
fn = new Function(this.args(), code);
break;
}
this.deinit(); // 清空 options 和 _args
return fn;
}


Webpack 共提供了以下十种 Hooks,代码中所有具体的 Hook 都是以下这 10 种中的一种。


// 源码取自:lib/index.js
"use strict";

exports.__esModule = true;
// 同步执行的钩子,不能处理异步任务
exports.SyncHook = require("./SyncHook");
// 同步执行的钩子,返回非空时,阻止向下执行
exports.SyncBailHook = require("./SyncBailHook");
// 同步执行的钩子,支持将返回值透传到下一个钩子中
exports.SyncWaterfallHook = require("./SyncWaterfallHook");
// 同步执行的钩子,支持将返回值透传到下一个钩子中,返回非空时,重复执行
exports.SyncLoopHook = require("./SyncLoopHook");
// 异步并行的钩子
exports.AsyncParallelHook = require("./AsyncParallelHook");
// 异步并行的钩子,返回非空时,阻止向下执行,直接执行回调
exports.AsyncParallelBailHook = require("./AsyncParallelBailHook");
// 异步串行的钩子
exports.AsyncSeriesHook = require("./AsyncSeriesHook");
// 异步串行的钩子,返回非空时,阻止向下执行,直接执行回调
exports.AsyncSeriesBailHook = require("./AsyncSeriesBailHook");
// 支持异步串行 && 并行的钩子,返回非空时,重复执行
exports.AsyncSeriesLoopHook = require("./AsyncSeriesLoopHook");
// 异步串行的钩子,下一步依赖上一步返回的值
exports.AsyncSeriesWaterfallHook = require("./AsyncSeriesWaterfallHook");
// 以下 2 个是 hook 工具类,分别用于 hooks 映射以及 hooks 重定向
exports.HookMap = require("./HookMap");
exports.MultiHook = require("./MultiHook");


举几个简单的例子:



  • 上面官方案例中的 run 这个 Hook,会在开始读取 records 之前执行,它的类型是 AsyncSeriesHook,查看源码可以发现,run Hook 既可以执行同步的 tap 方法,也可以执行异步的 tapAsync 和 tapPromise 方法,所以以下写法也是可以的:


const pluginName = 'ConsoleLogOnBuildWebpackPlugin';

class ConsoleLogOnBuildWebpackPlugin {
apply(compiler) {
compiler.hooks.run.tapAsync(pluginName, (compilation, callback) => {
setTimeout(() => {
console.log("webpack 构建过程开始!");
callback(); // callback 方法为了让构建继续执行下去,必须要调用
}, 1000);
});
}
}



  • 再举一个例子,比如 failed 这个 Hook,会在编译失败之后执行,它的类型是 SyncHook,查看源码可以发现,调用 tapAsync 和 tapPromise 方法时,会直接抛错。


对于一些同步的方法,推荐直接使用 tap 进行注册方法,对于异步的方案,tapAsync 通过执行 callback 方法实现回调,如果执行的方法返回的是一个 Promise,推荐使用 tapPromise 进行方法的注册


Hook 的类型可以通过官方 API 查询,地址传送门


// 源码取自:lib/SyncHook.js
const TAP_ASYNC = () => {
throw new Error("tapAsync is not supported on a SyncHook");
};

const TAP_PROMISE = () => {
throw new Error("tapPromise is not supported on a SyncHook");
};

function SyncHook(args = [], name = undefined) {
const hook = new Hook(args, name);
hook.constructor = SyncHook;
hook.tapAsync = TAP_ASYNC;
hook.tapPromise = TAP_PROMISE;
hook.compile = COMPILE;
return hook;
}


讲解完具体的执行方法之后,我们再聊一下 Webpack 流程以及 Tapable 是什么。


Webpack && Tapable


Webpack 运行机制


要理解 Plugin,我们先大致了解 Webpack 打包的流程



  1. 我们打包的时候,会先合并 Webpack config 文件和命令行参数,合并为 options。

  2. 将 options 传入 Compiler 构造方法,生成 compiler 实例,并实例化了 Compiler 上的 Hooks。

  3. compiler 对象执行 run 方法,并自动触发 beforeRun、run、beforeCompile、compile 等关键 Hooks。

  4. 调用 Compilation 构造方法创建 compilation 对象,compilation 负责管理所有模块和对应的依赖,创建完成后触发 make Hook。

  5. 执行 compilation.addEntry() 方法,addEntry 用于分析所有入口文件,逐级递归解析,调用 NormalModuleFactory 方法,为每个依赖生成一个 Module 实例,并在执行过程中触发 beforeResolve、resolver、afterResolve、module 等关键 Hooks。

  6. 将第 5 步中生成的 Module 实例作为入参,执行 Compilation.addModule() 和 Compilation.buildModule() 方法递归创建模块对象和依赖模块对象。

  7. 调用 seal 方法生成代码,整理输出主文件和 chunk,并最终输出。



Tapable


Tapable 是 Webpack 核心工具库,它提供了所有 Hook 的抽象类定义,Webpack 许多对象都是继承自 Tapable 类。比如上面说的 tap、tapAsync 和 tapPromise 都是通过 Tapable 进行暴露的。源码如下(截取了部分代码):


// 第二节 “创建一个 Plugin” 中说的 10 种 Hooks 都是继承了这两个类
// 源码取自:tapable.d.ts
declare class Hook {
tap(options: string | Tap & IfSet, fn: (...args: AsArray) => R): void;
}

declare class AsyncHook extends Hook {
tapAsync(
options: string | Tap & IfSet,
fn: (...args: Append, InnerCallback>) => void
): void;
tapPromise(
options: string | Tap & IfSet,
fn: (...args: AsArray) => Promise
): void;
}



常见 Hooks API


可以参考 Webpack


本文列举一些常用 Hooks 和其对应的类型:


Compiler Hooks
































Hooktype调用
runAsyncSeriesHook开始读取 records 之前
compileSyncHook一个新的编译 (compilation) 创建之后
emitAsyncSeriesHook生成资源到 output 目录之前
doneSyncHook编译 (compilation) 完成

Compilation Hooks



























Hooktype调用
buildModuleSyncHook在模块构建开始之前触发
finishModulesSyncHook所有模块都完成构建
optimizeSyncHook优化阶段开始时触发

Plugin 在项目中的应用


讲完这么多理论知识,接下来我们来看一下 Plugin 在项目中的实战:如何将各个子模块中的 router 文件合并到 router-config.js 中。


背景:


在 React 项目中,一般我们的 Router 文件是写在一个项目中的,如果项目中包含了许多页面,不免会出现所有业务模块 Router 耦合的情况,所以我们开发了一个 Plugin,在构建打包时,该 Plugin 会读取所有文件夹下的 Router 文件,再合并到一起形成一个统一的 Router Config 文件,轻松解决业务耦合问题。这就是 Plugin 的应用。


实现:


const fs = require('fs');
const path = require('path');
const _ = require('lodash');

function resolve(dir) {
return path.join(__dirname, '..', dir);
}

function MegerRouterPlugin(options) {
// options是配置文件,你可以在这里进行一些与options相关的工作
}

MegerRouterPlugin.prototype.apply = function (compiler) {
// 注册 before-compile 钩子,触发文件合并
compiler.plugin('before-compile', (compilation, callback) => {
// 最终生成的文件数据
const data = {};
const routesPath = resolve('src/routes');
const targetFile = resolve('src/router-config.js');
// 获取路径下所有的文件和文件夹
const dirs = fs.readdirSync(routesPath);
try {
dirs.forEach((dir) => {
const routePath = resolve(`src/routes/${dir}`);
// 判断是否是文件夹
if (!fs.statSync(routePath).isDirectory()) {
return true;
}
delete require.cache[`${routePath}/index.js`];
const routeInfo = require(routePath);
// 多个 view 的情况下,遍历生成router信息
if (!_.isArray(routeInfo)) {
generate(routeInfo, dir, data);
// 单个 view 的情况下,直接生成
} else {
routeInfo.map((config) => {
generate(config, dir, data);
});
}
});
} catch (e) {
console.log(e);
}

// 如果 router-config.js 存在,判断文件数据是否相同,不同删除文件后再生成
if (fs.existsSync(targetFile)) {
delete require.cache[targetFile];
const targetData = require(targetFile);
if (!_.isEqual(targetData, data)) {
writeFile(targetFile, data);
}
// 如果 router-config.js 不存在,直接生成文件
} else {
writeFile(targetFile, data);
}

// 最后调用 callback,继续执行 webpack 打包
callback();
});
};
// 合并当前文件夹下的router数据,并输出到 data 对象中
function generate(config, dir, data) {
// 合并 router
mergeConfig(config, dir, data);
// 合并子 router
getChildRoutes(config.childRoutes, dir, data, config.url);
}
// 合并 router 数据到 targetData 中
function mergeConfig(config, dir, targetData) {
const { view, models, extraModels, url, childRoutes, ...rest } = config;
// 获取 models,并去除 src 字段
const dirModels = getModels(`src/routes/${dir}/models`, models);
const data = {
...rest,
};
// view 拼接到 path 字段
data.path = `${dir}/views${view ? `/${view}` : ''}`;
// 如果有 extraModels,就拼接到 models 对象上
if (dirModels.length || (extraModels && extraModels.length)) {
data.models = mergerExtraModels(config, dirModels);
}
Object.assign(targetData, {
[url]: data,
});
}
// 拼接 dva models
function getModels(modelsDir, models) {
if (!fs.existsSync(modelsDir)) {
return [];
}
let files = fs.readdirSync(modelsDir);
// 必须要以 js 或者 jsx 结尾
files = files.filter((item) => {
return /\.jsx?$/.test(item);
});
// 如果没有定义 models ,默认取 index.js
if (!models || !models.length) {
if (files.indexOf('index.js') > -1) {
// 去除 src
return [`${modelsDir.replace('src/', '')}/index.js`];
}
return [];
}
return models.map((item) => {
if (files.indexOf(`${item}.js`) > -1) {
// 去除 src
return `${modelsDir.replace('src/', '')}/${item}.js`;
}
});
}
// 合并 extra models
function mergerExtraModels(config, models) {
return models.concat(config.extraModels ? config.extraModels : []);
}
// 合并子 router
function getChildRoutes(childRoutes, dir, targetData, oUrl) {
if (!childRoutes) {
return;
}
childRoutes.map((option) => {
option.url = oUrl + option.url;
if (option.childRoutes) {
// 递归合并子 router
getChildRoutes(option.childRoutes, dir, targetData, option.url);
}
mergeConfig(option, dir, targetData);
});
}

// 写文件
function writeFile(targetFile, data) {
fs.writeFileSync(targetFile, `module.exports = ${JSON.stringify(data, null, 2)}`, 'utf-8');
}

module.exports = MegerRouterPlugin;



结果:


合并前的文件:


module.exports = [
{
url: '/category/protocol',
view: 'protocol',
},
{
url: '/category/sync',
models: ['sync'],
view: 'sync',
},
{
url: '/category/list',
models: ['category', 'config', 'attributes', 'group', 'otherSet', 'collaboration'],
view: 'categoryRefactor',
},
{
url: '/category/conversion',
models: ['conversion'],
view: 'conversion',
},
];



合并后的文件:


module.exports = {
"/category/protocol": {
"path": "Category/views/protocol"
},
"/category/sync": {
"path": "Category/views/sync",
"models": [
"routes/Category/models/sync.js"
]
},
"/category/list": {
"path": "Category/views/categoryRefactor",
"models": [
"routes/Category/models/category.js",
"routes/Category/models/config.js",
"routes/Category/models/attributes.js",
"routes/Category/models/group.js",
"routes/Category/models/otherSet.js",
"routes/Category/models/collaboration.js"
]
},
"/category/conversion": {
"path": "Category/views/conversion",
"models": [
"routes/Category/models/conversion.js"
]
},
}



最终项目就会生成 router-config.js 文件



结尾


希望大家看完本章之后,对 Webpack Plugin 有一个初步的认识,能够上手写一个自己的 Plugin 来应用到自己的项目中。


文章中如有不对的地方,欢迎指正。


链接:https://juejin.cn/post/6968988552075952141

收起阅读 »

当面试官问Webpack的时候他想知道什么

前言 在前端工程化日趋复杂的今天,模块打包工具在我们的开发中起到了越来越重要的作用,其中webpack就是最热门的打包工具之一。 说到webpack,可能很多小伙伴会觉得既熟悉又陌生,熟悉是因为几乎在每一个项目中我们都会用上它,又因为webpack复杂的配置和...
继续阅读 »

前言


在前端工程化日趋复杂的今天,模块打包工具在我们的开发中起到了越来越重要的作用,其中webpack就是最热门的打包工具之一。


说到webpack,可能很多小伙伴会觉得既熟悉又陌生,熟悉是因为几乎在每一个项目中我们都会用上它,又因为webpack复杂的配置和五花八门的功能感到陌生。尤其当我们使用诸如umi.js之类的应用框架还帮我们把webpack配置再封装一层的时候,webpack的本质似乎离我们更加遥远和深不可测了。


当面试官问你是否了解webpack的时候,或许你可以说出一串耳熟能详的webpack loaderplugin的名字,甚至还能说出插件和一系列配置做按需加载和打包优化,那你是否了解他的运行机制以及实现原理呢,那我们今天就一起探索webpack的能力边界,尝试了解webpack的一些实现流程和原理,拒做API工程师。


CgqCHl6pSFmAC5UzAAEwx63IBwE024.png


你知道webpack的作用是什么吗?


从官网上的描述我们其实不难理解,webpack的作用其实有以下几点:




  • 模块打包。可以将不同模块的文件打包整合在一起,并且保证它们之间的引用正确,执行有序。利用打包我们就可以在开发的时候根据我们自己的业务自由划分文件模块,保证项目结构的清晰和可读性。




  • 编译兼容。在前端的“上古时期”,手写一堆浏览器兼容代码一直是令前端工程师头皮发麻的事情,而在今天这个问题被大大的弱化了,通过webpackLoader机制,不仅仅可以帮助我们对代码做polyfill,还可以编译转换诸如.less, .vue, .jsx这类在浏览器无法识别的格式文件,让我们在开发的时候可以使用新特性和新语法做开发,提高开发效率。




  • 能力扩展。通过webpackPlugin机制,我们在实现模块化打包和编译兼容的基础上,可以进一步实现诸如按需加载,代码压缩等一系列功能,帮助我们进一步提高自动化程度,工程效率以及打包输出的质量。




说一下模块打包运行原理?


如果面试官问你Webpack是如何把这些模块合并到一起,并且保证其正常工作的,你是否了解呢?


首先我们应该简单了解一下webpack的整个打包流程:



  • 1、读取webpack的配置参数;

  • 2、启动webpack,创建Compiler对象并开始解析项目;

  • 3、从入口文件(entry)开始解析,并且找到其导入的依赖模块,递归遍历分析,形成依赖关系树;

  • 4、对不同文件类型的依赖模块文件使用对应的Loader进行编译,最终转为Javascript文件;

  • 5、整个过程中webpack会通过发布订阅模式,向外抛出一些hooks,而webpack的插件即可通过监听这些关键的事件节点,执行插件任务进而达到干预输出结果的目的。


其中文件的解析与构建是一个比较复杂的过程,在webpack源码中主要依赖于compilercompilation两个核心对象实现。


compiler对象是一个全局单例,他负责把控整个webpack打包的构建流程。
compilation对象是每一次构建的上下文对象,它包含了当次构建所需要的所有信息,每次热更新和重新构建,compiler都会重新生成一个新的compilation对象,负责此次更新的构建过程。


而每个模块间的依赖关系,则依赖于AST语法树。每个模块文件在通过Loader解析完成之后,会通过acorn库生成模块代码的AST语法树,通过语法树就可以分析这个模块是否还有依赖的模块,进而继续循环执行下一个模块的编译解析。


最终Webpack打包出来的bundle文件是一个IIFE的执行函数。


// webpack 5 打包的bundle文件内容

(() => { // webpackBootstrap
var __webpack_modules__ = ({
'file-A-path': ((modules) => { // ... })
'index-file-path': ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => { // ... })
})

// The module cache
var __webpack_module_cache__ = {};

// The require function
function __webpack_require__(moduleId) {
// Check if module is in cache
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
// Create a new module (and put it into the cache)
var module = __webpack_module_cache__[moduleId] = {
// no module.id needed
// no module.loaded needed
exports: {}
};

// Execute the module function
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);

// Return the exports of the module
return module.exports;
}

// startup
// Load entry module and return exports
// This entry module can't be inlined because the eval devtool is used.
var __webpack_exports__ = __webpack_require__("./src/index.js");
})


webpack4相比,webpack5打包出来的bundle做了相当的精简。在上面的打包demo中,整个立即执行函数里边只有三个变量和一个函数方法,__webpack_modules__存放了编译后的各个文件模块的JS内容,__webpack_module_cache__ 用来做模块缓存,__webpack_require__Webpack内部实现的一套依赖引入函数。最后一句则是代码运行的起点,从入口文件开始,启动整个项目。


其中值得一提的是__webpack_require__模块引入函数,我们在模块化开发的时候,通常会使用ES Module或者CommonJS规范导出/引入依赖模块,webpack打包编译的时候,会统一替换成自己的__webpack_require__来实现模块的引入和导出,从而实现模块缓存机制,以及抹平不同模块规范之间的一些差异性。


你知道sourceMap是什么吗?


提到sourceMap,很多小伙伴可能会立刻想到Webpack配置里边的devtool参数,以及对应的evaleval-cheap-source-map等等可选值以及它们的含义。除了知道不同参数之间的区别以及性能上的差异外,我们也可以一起了解一下sourceMap的实现方式。


sourceMap是一项将编译、打包、压缩后的代码映射回源代码的技术,由于打包压缩后的代码并没有阅读性可言,一旦在开发中报错或者遇到问题,直接在混淆代码中debug问题会带来非常糟糕的体验,sourceMap可以帮助我们快速定位到源代码的位置,提高我们的开发效率。sourceMap其实并不是Webpack特有的功能,而是Webpack支持sourceMap,像JQuery也支持souceMap


既然是一种源码的映射,那必然就需要有一份映射的文件,来标记混淆代码里对应的源码的位置,通常这份映射文件以.map结尾,里边的数据结构大概长这样:


{
"version" : 3, // Source Map版本
"file": "out.js", // 输出文件(可选)
"sourceRoot": "", // 源文件根目录(可选)
"sources": ["foo.js", "bar.js"], // 源文件列表
"sourcesContent": [null, null], // 源内容列表(可选,和源文件列表顺序一致)
"names": ["src", "maps", "are", "fun"], // mappings使用的符号名称列表
"mappings": "A,AAAB;;ABCDE;" // 带有编码映射数据的字符串
}


其中mappings数据有如下规则:



  • 生成文件中的一行的每个组用“;”分隔;

  • 每一段用“,”分隔;

  • 每个段由1、4或5个可变长度字段组成;


有了这份映射文件,我们只需要在我们的压缩代码的最末端加上这句注释,即可让sourceMap生效:


//# sourceURL=/path/to/file.js.map

有了这段注释后,浏览器就会通过sourceURL去获取这份映射文件,通过解释器解析后,实现源码和混淆代码之间的映射。因此sourceMap其实也是一项需要浏览器支持的技术。


如果我们仔细查看webpack打包出来的bundle文件,就可以发现在默认的development开发模式下,每个_webpack_modules__文件模块的代码最末端,都会加上//# sourceURL=webpack://file-path?,从而实现对sourceMap的支持。


sourceMap映射表的生成有一套较为复杂的规则,有兴趣的小伙伴可以看看以下文章,帮助理解soucrMap的原理实现:


Source Map的原理探究


Source Maps under the hood – VLQ, Base64 and Yoda


是否写过Loader?简单描述一下编写loader的思路?


从上面的打包代码我们其实可以知道,Webpack最后打包出来的成果是一份Javascript代码,实际上在Webpack内部默认也只能够处理JS模块代码,在打包过程中,会默认把所有遇到的文件都当作 JavaScript代码进行解析,因此当项目存在非JS类型文件时,我们需要先对其进行必要的转换,才能继续执行打包任务,这也是Loader机制存在的意义。


Loader的配置使用我们应该已经非常的熟悉:


// webpack.config.js
module.exports = {
// ...other config
module: {
rules: [
{
test: /^your-regExp$/,
use: [
{
loader: 'loader-name-A',
},
{
loader: 'loader-name-B',
}
]
},
]
}
}

通过配置可以看出,针对每个文件类型,loader是支持以数组的形式配置多个的,因此当Webpack在转换该文件类型的时候,会按顺序链式调用每一个loader,前一个loader返回的内容会作为下一个loader的入参。因此loader的开发需要遵循一些规范,比如返回值必须是标准的JS代码字符串,以保证下一个loader能够正常工作,同时在开发上需要严格遵循“单一职责”,只关心loader的输出以及对应的输出。


loader函数中的this上下文由webpack提供,可以通过this对象提供的相关属性,获取当前loader需要的各种信息数据,事实上,这个this指向了一个叫loaderContextloader-runner特有对象。有兴趣的小伙伴可以自行阅读源码。


module.exports = function(source) {
const content = doSomeThing2JsString(source);

// 如果 loader 配置了 options 对象,那么this.query将指向 options
const options = this.query;

// 可以用作解析其他模块路径的上下文
console.log('this.context');

/*
* this.callback 参数:
* error:Error | null,当 loader 出错时向外抛出一个 error
* content:String | Buffer,经过 loader 编译后需要导出的内容
* sourceMap:为方便调试生成的编译后内容的 source map
* ast:本次编译生成的 AST 静态语法树,之后执行的 loader 可以直接使用这个 AST,进而省去重复生成 AST 的过程
*/
this.callback(null, content);
// or return content;
}

更详细的开发文档可以直接查看官网的 Loader API


是否写过Plugin?简单描述一下编写plugin的思路?


如果说Loader负责文件转换,那么Plugin便是负责功能扩展。LoaderPlugin作为Webpack的两个重要组成部分,承担着两部分不同的职责。


上文已经说过,webpack基于发布订阅模式,在运行的生命周期中会广播出许多事件,插件通过监听这些事件,就可以在特定的阶段执行自己的插件任务,从而实现自己想要的功能。


既然基于发布订阅模式,那么知道Webpack到底提供了哪些事件钩子供插件开发者使用是非常重要的,上文提到过compilercompilationWebpack两个非常核心的对象,其中compiler暴露了和 Webpack整个生命周期相关的钩子(compiler-hooks),而compilation则暴露了与模块和依赖有关的粒度更小的事件钩子(Compilation Hooks)。


Webpack的事件机制基于webpack自己实现的一套Tapable事件流方案(github


// Tapable的简单使用
const { SyncHook } = require("tapable");

class Car {
constructor() {
// 在this.hooks中定义所有的钩子事件
this.hooks = {
accelerate: new SyncHook(["newSpeed"]),
brake: new SyncHook(),
calculateRoutes: new AsyncParallelHook(["source", "target", "routesList"])
};
}

/* ... */
}


const myCar = new Car();
// 通过调用tap方法即可增加一个消费者,订阅对应的钩子事件了
myCar.hooks.brake.tap("WarningLampPlugin", () => warningLamp.on());

Plugin的开发和开发Loader一样,需要遵循一些开发上的规范和原则:



  • 插件必须是一个函数或者是一个包含 apply 方法的对象,这样才能访问compiler实例;

  • 传给每个插件的 compilercompilation 对象都是同一个引用,若在一个插件中修改了它们身上的属性,会影响后面的插件;

  • 异步的事件需要在插件处理完任务时调用回调函数通知 Webpack 进入下一个流程,不然会卡住;


了解了以上这些内容,想要开发一个 Webpack Plugin,其实也并不困难。


class MyPlugin {
apply (compiler) {
// 找到合适的事件钩子,实现自己的插件功能
compiler.hooks.emit.tap('MyPlugin', compilation => {
// compilation: 当前打包构建流程的上下文
console.log(compilation);

// do something...
})
}
}

更详细的开发文档可以直接查看官网的 Plugin API


最后


本文也是结合一些优秀的文章和webpack本身的源码,大概地说了几个相对重要的概念和流程,其中的实现细节和设计思路还需要结合源码去阅读和慢慢理解。


Webpack作为一款优秀的打包工具,它改变了传统前端的开发模式,是现代化前端开发的基石。这样一个优秀的开源项目有许多优秀的设计思想和理念可以借鉴,我们自然也不应该仅仅停留在API的使用层面,尝试带着问题阅读源码,理解实现的流程和原理,也能让我们学到更多知识,理解得更加深刻,在项目中才能游刃有余的应用。




链接:https://juejin.cn/post/6943468761575849992

收起阅读 »

是什么让尤大选择放弃Webpack?面向未来的前端构建工具 Vite

前两天在知乎看到过一篇文章,大致意思是讲:字节跳动已经开始“弃用Webpack”,尝试在自研构建工具中使用类似Vite的ESmodule构建方式。 引起下方一大片焦虑: Webpack是不是要被取代了?现在学Vite就行了吧 Webpack还没学会,就又来新...
继续阅读 »

前两天在知乎看到过一篇文章,大致意思是讲:字节跳动已经开始“弃用Webpack”,尝试在自研构建工具中使用类似Vite的ESmodule构建方式。


引起下方一大片焦虑:



  • Webpack是不是要被取代了?现在学Vite就行了吧

  • Webpack还没学会,就又来新的了!


甚至有人搬出了去年尤大所发的一个动态:再也回不去Webpack了。


在这里插入图片描述



PS:最近的vite比较火,而且发布了2.0版本,vue的作者尤雨溪也是在极力推荐


全方位对比vite和webpack


webpack打包过程


1.识别入口文件


2.通过逐层识别模块依赖。(Commonjs、amd或者es6的import,webpack都会对其进行分析。来获取代码的依赖)


3.webpack做的就是分析代码。转换代码,编译代码,输出代码


4.最终形成打包后的代码


webpack打包原理


1.先逐级递归识别依赖,构建依赖图谱


2.将代码转化成AST抽象语法树


3.在AST阶段中去处理代码


4.把AST抽象语法树变成浏览器可以识别的代码, 然后输出



重点:这里需要递归识别依赖,构建依赖图谱。图谱对象就是类似下面这种



{ './app.js':
{ dependencies: { './test1.js': './test1.js' },
code:
'"use strict";\n\nvar _test = _interopRequireDefault(require("./test1.js"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n\nconsole.log(test
1);' },
'./test1.js':
{ dependencies: { './test2.js': './test2.js' },
code:
'"use strict";\n\nvar _test = _interopRequireDefault(require("./test2.js"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n\nconsole.log(\'th
is is test1.js \', _test["default"]);' },
'./test2.js':
{ dependencies: {},
code:
'"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n value: true\n});\nexports["default"] = void 0;\n\nfunction test2() {\n console.log(\'this is test2 \');\n}\n\nvar _default = tes
t2;\nexports["default"] = _default;' } }


在这里插入图片描述


Vite原理


当声明一个 script 标签类型为 module 时





浏览器就会像服务器发起一个GET


http://localhost:3000/src/main.js请求main.js文件:

// /src/main.js:
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')


浏览器请求到了main.js文件,检测到内部含有import引入的包,又会对其内部的 import 引用发起 HTTP 请求获取模块的内容文件



Vite 的主要功能就是通过劫持浏览器的这些请求,并在后端进行相应的处理将项目中使用的文件通过简单的分解与整合,然后再返回给浏览器,vite整个过程中没有对文件进行打包编译,所以其运行速度比原始的webpack开发编译速度快出许多!


webpack缺点一:缓慢的服务器启动


当冷启动开发服务器时,基于打包器的方式是在提供服务前去急切地抓取和构建你的整个应用。


Vite改进



  • Vite 通过在一开始将应用中的模块区分为 依赖 和 源码 两类,改进了开发服务器启动时间。

  • 依赖 大多为纯 JavaScript 并在开发时不会变动。一些较大的依赖(例如有上百个模块的组件库)处理的代价也很高。依赖也通常会以某些方式(例如 ESM 或者 CommonJS)被拆分到大量小模块中。

  • Vite 将会使用 esbuild 预构建依赖。Esbuild 使用 Go 编写,并且比以 JavaScript 编写的打包器预构建依赖快 10-100 倍。

  • 源码 通常包含一些并非直接是 JavaScript 的文件,需要转换(例如 JSX,CSS 或者 Vue/Svelte 组件),时常会被编辑。同时,并不是所有的源码都需要同时被加载。(例如基于路由拆分的代码模块)。

  • Vite 以 原生 ESM 方式服务源码。这实际上是让浏览器接管了打包程序的部分工作:Vite 只需要在浏览器请求源码时进行转换并按需提供源码。根据情景动态导入的代码,即只在当前屏幕上实际使用时才会被处理。


webpack缺点2:使用的是node.js去实现


在这里插入图片描述
Vite改进


Vite 将会使用 esbuild 预构建依赖。Esbuild 使用 Go 编写,并且比以 Node.js 编写的打包器预构建依赖快 10-100 倍。


webpack致命缺点3:热更新效率低下



  • 当基于打包器启动时,编辑文件后将重新构建文件本身。显然我们不应该重新构建整个包,因为这样更新速度会随着应用体积增长而直线下降。

  • 一些打包器的开发服务器将构建内容存入内存,这样它们只需要在文件更改时使模块图的一部分失活[1],但它也仍需要整个重新构建并重载页面。这样代价很高,并且重新加载页面会消除应用的当前状态,所以打包器支持了动态模块热重载(HMR):允许一个模块 “热替换” 它自己,而对页面其余部分没有影响。这大大改进了开发体验 - 然而,在实践中我们发现,即使是 HMR 更新速度也会随着应用规模的增长而显著下降。


Vite改进



  • 在 Vite 中,HMR 是在原生 ESM 上执行的。当编辑一个文件时,Vite 只需要精确地使已编辑的模块与其最近的 HMR 边界之间的链失效(大多数时候只需要模块本身),使 HMR 更新始终快速,无论应用的大小。

  • Vite 同时利用 HTTP 头来加速整个页面的重新加载(再次让浏览器为我们做更多事情):源码模块的请求会根据 304 Not Modified 进行协商缓存,而依赖模块请求则会通过 Cache-Control: max-age=31536000,immutable 进行强缓存,因此一旦被缓存它们将不需要再次请求。


Vite缺点1:生态,生态,生态不如webpack


wepback牛逼之处在于loader和plugin非常丰富,不过我认为生态只是时间问题,现在的vite,更像是当时刚出来的M1芯片Mac,我当时非常看好M1的Mac,毫不犹豫买了,现在也没什么问题


Vite缺点2:prod环境的构建,目前用的Rollup


原因在于esbuild对于css和代码分割不是很友好


Vite缺点3:还没有被大规模使用,很多问题或者诉求没有真正暴露出来


vite真正崛起那一天,是跟vue3有关系的,当vue3广泛开始使用在生产环境的时候,vite也就大概率意味着被大家慢慢开始接受了


总结


1.Vite,就像刚出来的M1芯片Mac,都说好,但是一开始买的人不多,担心生态问题,后面都说真香


2.相信vue3作者的大力支持下,vite即将大放异彩!


3.但是 Webpack 在现在的前端工程化中仍然扮演着非常重要的角色。


4.vite相关生态没有webpack完善,vite可以作为开发的辅助。



链接:https://juejin.cn/post/6975038104650383374

收起阅读 »

Vue3发布半年我不学,摸鱼爽歪歪,哎~就是玩儿

vue
是从 Vue 2 开始学基础还是直接学 Vue 3 ?尤雨溪给出的答案是:“直接学 Vue 3 就行了,基础概念是一模一样的。” 以上内容源引自最新一期的《程序员》期刊,原文链接为《直接学 Vue 3 吧 —— 对话 Vue.js 作者尤雨溪》。 前言 Vue...
继续阅读 »

是从 Vue 2 开始学基础还是直接学 Vue 3 ?尤雨溪给出的答案是:“直接学 Vue 3 就行了,基础概念是一模一样的。”


以上内容源引自最新一期的《程序员》期刊,原文链接为《直接学 Vue 3 吧 —— 对话 Vue.js 作者尤雨溪》


前言


Vue 3.0 出来之后,我一直在不断的尝试学习和接受新的概念。没办法,作为一个前端开发,并且也不是毕业于名校或就职于大厂,不断地学习,培养学习能力,才是我们这些普通前端开发的核心竞争力。


当然,有些同学抬杠,我专精一门技术,也能开发出自己的核心竞争力。好!!!有志气。但是多数同学,很难有这种意志力。如 CSS 大佬张鑫旭Canva 大佬老姚、可视化大佬月影大大、面试题大佬敖丙等等等等。这些大佬在一件事情上花费的精力,是需要极高的意志力和执行力才能做到的。我反正做不到(逃)。


学无止境!


一定要动手敲代码。仅仅学习而不实践,这种做法也不可取。


本文主要是介绍一些我学习 Vue 3.0 期间,看过的一些比较有用的资源,和大家分享一下,不喜勿喷,喷了我也学着 @尼克陈 顺着网线找到你家。


我与 Vue 3.0


其实一直都有在关注 Vue 3.0 相关的进度和新闻,不过真正学习是在它正式 release 后,2020 年 9 月我也发布了一篇文章《Vue 3.0 来了,我们该做些什么?》阐述了自己的看法,也制定了自己的学习计划。


其实,学习任何一门新技术的步骤都一样:


看文档 → 学习新语法 → 做小 demo → 做几个实战项目 → 看源码 → 整理心得并分享。


学习 Vue 3.0 亦是如此,虽然我这个人比较爱开玩笑,也爱写段子,标题取的也吊儿郎当,但是学习和行动起来我可不比别人差。


学习过程中看文档、做 demo,然后也一直在学习和分享 Vue3 的知识点,比如发布一些 Vue3 的教程:



也做了几个 Vue 3.0 实战的项目练手,之后发布到也开源了 GitHub 中,访问地址如下:



in GitHub : github.com/newbee-ltd


in Gitee : gitee.com/newbee-ltd



一个是 Vue3 版本的商城项目:


img


一个是 Vue3 版本的后台管理项目:


panban1 (1)


源码全部开放,后台 API 也有,都是很实用的项目。目前的反响还不错,得到了很多的正向反馈,这些免费的开源项目让大家有了一个不错的 Vue3 练手项目,顺利的完成了课程作业或者在简历里多了一份项目经验,因此也收到了很多感谢的话。


接下来就是学习过程中,我觉得非常有用的资源了,大家在学习 Vue 3 时可以参考和使用。


image-20210228175425067


Vue 3.0 相关技术栈



















































相关库名称在线地址 🔗
Vue 3.0 官方文档(英文)在线地址
Vue 3.0 中文文档在线地址 国内加速版
Composition-API手册在线地址
Vue 3.0 源码学习在线地址
Vue-Router 官方文档在线地址
Vuex 4.0Github
vue-devtoolsGithub(Vue3.0 需要使用最新版本)
Vite 源码学习线上地址
Vite 2.0 中文文档线上地址
Vue3 新动态线上地址

Vue3 新动态 这个仓库我经常看,里面有最新的 Vue 3 文章、仓库等等,都是中文的,作者应该是咱们的大兄弟,大家也可以关注一下。


更新 Vue 3.0 的开源 UI 组件库


Vue 2.0 时期,产生了不少好的开源组件库,这些组件库伴随着我们的成长,我们看看哪些组件库更新了 Vue 3.0 版本。


Element-plus


简介:大家想必也不陌生,它的 Vue 2.0 版本是 Element-UI,后经坤哥和他的小伙伴开发出了 Vue 3.0 版本的  Element-plus,确实很优秀,目前点赞数快破万了,持续关注。


仓库地址 🏠 :github.com/element-plu… ⭐ : 9.8k


文档地址 📖 :element-plus.gitee.io/#/zh-CN


开源项目 🔗 :



目前 Element-plus 的开源项目还不多,之前 Element-UI 相关开源项目,大大小小都在做 Element-plus 的适配。在此也感谢坤哥和他的小伙伴们,持续 Element 系列的维护,这对 Vue 生态是非常强大的贡献。


Ant Design of Vue


简介:它是最早一批做 Vue 3.0 适配的组件库, Antd 官方推荐的组件库。


仓库地址 🏠 :github.com/vueComponen… ⭐ : 14.8k


文档地址 📖 :antdv.com/docs/vue/in…


开源项目 🔗 :



他们的更新维护还是很积极的,最近一次更新实在 2021 年 2 月 27 号,可见这个组件库还是值得信赖的,有问题可以去 issue 提。


Vant


简介:国内移动端首屈一指的组件库,用过的都说好,个人已经在两个项目中使用过该组件库,也算是比较早支持 Vue 3.0 的框架,该有的都有。


仓库地址 🏠 :github.com/youzan/vant ⭐ : 16.9k


文档地址 📖 :vant-contrib.gitee.io/vant/v3/#/z…


开源项目 🔗 :



NutUI 3


简介:京东团队开发的移动端组件库,近期才升级到 Vue 3.0 版本,文章在此。虽然我没有使用过这个组件库,但是从他们的更新速度来看,比其他很多组件库要快,说明对待最近技术,还是有态度的。


仓库地址 🏠 :github.com/jdf2e/nutui ⭐ : 3.1k


文档地址 📖 :nutui.jd.com (看看这简短的域名,透露出壕的气息)


开源项目 🔗 :基本上还没有见到有公开的开源项目,如果有还望大家积极评论


链接:https://juejin.cn/post/6955129410705948702

收起阅读 »

iOS-使用SDCycleScrollView定制各种自定义样式的上下滚动的跑马灯

SDCycleScrollView的优点及实现技巧:1.利用UICollectionView的复用机制,只会创建屏幕可见个cell。2.如果是无限循环 ,会存在100*self.imagePathsGroup.count个item,第一次出现的位置在(100*...
继续阅读 »

SDCycleScrollView的优点及实现技巧:

1.利用UICollectionView的复用机制,只会创建屏幕可见个cell。
2.如果是无限循环 ,会存在100*self.imagePathsGroup.count个item,第一次出现的位置在(100*self.imagePathsGroup.count)/2的位置。
3.每次滚动到100*self.imagePathsGroup.count位置的item自动切换到(100*self.imagePathsGroup.count)/2的位置。
4.使用取余index % self.imagePathsGroup.count确定现在显示的imageView

缺点:

手动拖拽到最后、不会跳到初始位置

原因:

因为作者设置的100足够大、未对拖拽最后一个item做处理

解决方法:

同时监听NSTimer和拖拽,在(100 - 1)*self.imagePathsGroup.count和self.imagePathsGroup.count位置时实现切换到(100*self.imagePathsGroup.count)/2的位置

使用SDCycleScrollView制作各种自定义样式的上下滚动的跑马灯

效果图:


.m

@interface ViewController () <SDCycleScrollViewDelegate>
@end
@implementation ViewController
{
NSArray *_imagesURLStrings;
SDCycleScrollView *_customCellScrollViewDemo;
}

- (void)customCellScrollView {

// 如果要实现自定义cell的轮播图,必须先实现customCollectionViewCellClassForCycleScrollView:和 setupCustomCell:forIndex:代理方法

_customCellScrollViewDemo = [SDCycleScrollView cycleScrollViewWithFrame:CGRectMake(0, 820, w, 40) delegate:self placeholderImage:[UIImage imageNamed:@"placeholder"]];
_customCellScrollViewDemo.currentPageDotImage = [UIImage imageNamed:@"pageControlCurrentDot"];
_customCellScrollViewDemo.pageDotImage = [UIImage imageNamed:@"pageControlDot"];
_customCellScrollViewDemo.imageURLStringsGroup = imagesURLStrings;
_customCellScrollViewDemo.scrollDirection = UICollectionViewScrollDirectionVertical;
_customCellScrollViewDemo.showPageControl = NO;
[demoContainerView addSubview:_customCellScrollViewDemo];
}

// 不需要自定义轮播cell的请忽略下面的代理方法

// 如果要实现自定义cell的轮播图,必须先实现customCollectionViewCellClassForCycleScrollView:和setupCustomCell:forIndex:代理方法
- (Class)customCollectionViewCellClassForCycleScrollView:(SDCycleScrollView *)view
{
if (view != _customCellScrollViewDemo) {
return nil;
}
return [CustomCollectionViewCell class];
}

- (void)setupCustomCell:(UICollectionViewCell *)cell forIndex:(NSInteger)index cycleScrollView:(SDCycleScrollView *)view
{
CustomCollectionViewCell *myCell = (CustomCollectionViewCell *)cell;
//[myCell.imageView sd_setImageWithURL:_imagesURLStrings[index]];

NSArray *titleArray = @[@"新闻",
@"娱乐",
@"体育"];
NSArray *contentArray = @[@"新闻新闻新闻新闻新闻新闻新闻新闻新闻新闻新闻新闻",
@"娱乐娱乐娱乐娱乐娱乐娱乐娱乐娱乐娱乐娱乐",
@"体育体育体育体育体育体育体育体育体育体育体育体育"];
myCell.titleLabel.text = titleArray[index];
myCell.contentLabel.text = contentArray[index];
}

自定义cell-根据不同的cell定制各种自定义样式的上下滚动的跑马灯

.h
#import <UIKit/UIKit.h>

@interface CustomCollectionViewCell : UICollectionViewCell

@property (nonatomic, strong) UIImageView *imageView;
@property (nonatomic, strong) UILabel *titleLabel;
@property (nonatomic, strong) UILabel *contentLabel;

@end
.m
#import "CustomCollectionViewCell.h"
#import "UIView+SDExtension.h"

@implementation CustomCollectionViewCell

#pragma mark - 懒加载
- (UIImageView *)imageView {
if (!_imageView) {
_imageView = [UIImageView new];
_imageView.layer.borderColor = [[UIColor redColor] CGColor];
_imageView.layer.borderWidth = 0;
_imageView.hidden = YES;
}
return _imageView;
}
- (UILabel *)titleLabel {
if (!_titleLabel) {
_titleLabel = [[UILabel alloc]init];
_titleLabel.text = @"新闻";
_titleLabel.textColor = [UIColor redColor];
_titleLabel.numberOfLines = 0;
_titleLabel.textAlignment = NSTextAlignmentCenter;
_titleLabel.font = [UIFont systemFontOfSize:12];
_titleLabel.backgroundColor = [UIColor yellowColor];
_titleLabel.layer.masksToBounds = YES;
_titleLabel.layer.cornerRadius = 5;
_titleLabel.layer.borderColor = [UIColor redColor].CGColor;
_titleLabel.layer.borderWidth = 1.f;
}
return _titleLabel;
}
- (UILabel *)contentLabel {
if (!_contentLabel) {
_contentLabel = [[UILabel alloc]init];
_contentLabel.text = @"我是label的内容";
_contentLabel.textColor = [UIColor blackColor];
_contentLabel.numberOfLines = 0;
_contentLabel.font = [UIFont systemFontOfSize:12];
}
return _contentLabel;
}
#pragma mark - 页面初始化
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
self.contentView.backgroundColor = [UIColor whiteColor];
[self setupViews];
}
return self;
}

#pragma mark - 添加子控件
- (void)setupViews {
[self.contentView addSubview:self.imageView];
[self.contentView addSubview:self.titleLabel];
[self.contentView addSubview:self.contentLabel];
}

#pragma mark - 布局子控件
- (void)layoutSubviews {
[super layoutSubviews];
_imageView.frame = self.bounds;
_titleLabel.frame = CGRectMake(15, 10, 45, 20);
_contentLabel.frame = CGRectMake(15 + 45 + 15, 10, 200, 20);
}

实际情况自己可下载SDCycleScrollView自行研究。。。

转自:https://www.jianshu.com/p/641403879f7b

收起阅读 »

国内知名Wchat团队荣誉出品顶级IM通讯聊天系统

iOS
国内知名Wchat团队荣誉出品顶级IM通讯聊天系统团队言语在先:想低价购买者勿扰(团队是在国内首屈一指的通信公司离职后组建,低价购买者/代码代码贩子者/同行勿扰/)。想购买劣质低等产品者勿扰(行业鱼龙混杂,想购买类似低能协议xmpp者勿扰)。想购买由类似ope...
继续阅读 »



国内知名Wchat团队荣誉出品顶级IM通讯聊天系统



团队言语在先:

想低价购买者勿扰(团队是在国内首屈一指的通信公司离职后组建,低价购买者/代码代码贩子者/同行勿扰/)

。想购买劣质低等产品者勿扰(行业鱼龙混杂,想购买类似低能协议xmpp者勿扰)

。想购买由类似openfire第三方开源改造而来的所谓第三方通信server者勿扰

。想购买没有做任何安全加密场景者勿扰(随便一句api 一个接口就构成了红包收发/转账/密码设置等没有任何安全系数可言的低质产品)

。想购买非运营级别通信系统勿扰(到处呼喊:最稳定/真正可靠/大并发/真正安全!所有一切都需要实际架构支撑以及理论数值测验)

。想购买无保障/无支撑者勿扰(1W/4W/10W低质产品不可谓没有,必须做到:大并发支持合同保障/合作支持运维保障/在线人数支持架构保障)

。想购买消息丢包者勿扰(满天飞的所谓消息确认机制,最简单的测验既是前端支持消息收发demo测试环境,低质产品一秒收发百条消息必丢必崩,

别提秒发千条/万条,更低质产品可测验:同时发九张图片/根据数字12345678910发送出去,必丢!android vs ios)

。想购买大容量群uer者勿扰(随便宣传既是万人大群/几千大群/群组无限,小团队产品群组上线用户超过4000群消息体量不用很大手机前端必卡)

。最重要一点:口口声声说要运营很大的系统 却想出十几个money的人群勿扰,买产品做系统一要稳定二要长久用三要抛开运维烦恼,预算有限那就干脆

别买,买了几万的系统你一样后面用不起来会烂掉!

。产品体系包括:android ios server adminweb maintenance httpapi h5 webpc (支持server压测/前端消息收发压测/httpapi压测)

。。支持源码,但需要您拿去做一个伟大的系统出来!

。。团队产品目前国内没有同质化,客户集中在国外,有求高质量产品的个人或团队可通过以下方式联系到我们(低价者勿扰!)

。。。球球:383189941 q 513275129

。。。。产品不多介绍直接加我 测试产品更直接

。。。。。创新从未停止 更新不会终止 大陆唯一一家支持大并发保障/支持合同费用包含运维支撑的团队 

收起阅读 »

iOS第三方——JazzHands

JazzHands是UIKit一个简单的关键帧基础动画框架。可通过手势、scrollView,kvo或者ReactiveCocoa控制动画。JazzHands很适合用来创建很酷的引导页。Swift中的JazzHands想在Swift中使用Jazz Hands?...
继续阅读 »

JazzHands是UIKit一个简单的关键帧基础动画框架。可通过手势、scrollView,kvo或者ReactiveCocoa控制动画。JazzHands很适合用来创建很酷的引导页。


Swift中的JazzHands

想在Swift中使用Jazz Hands?可以试试RazzleDazzle。

安装

JazzHands可以通过CocoaPods安装,在Podfile中加入如下的一行:

pod "JazzHands"

你也可以把JazzHands文件夹的内容复制到工程中。

快速开始

首先,在UIViewController中加入JazzHands:

#import <IFTTTJazzHands.h>

现在创建一个Animator来管理UIViewController中所有的动画。

@property (nonatomic, strong) IFTTTAnimator *animator;

// later...

self.animator = [IFTTTAnimator new];

为你想要动画的view,创建一个animation。这儿有许多可以应用到view的animation。例如,我们使用IFTTTAlphaAnimation,可以使view淡入淡出。

IFTTTAlphaAnimation *alphaAnimation = [IFTTTAlphaAnimation animationWithView: viewThatYouWantToAnimate];

使用animator注册这个animation。

[self.animator addAnimation: alphaAnimation];

为animation添加一些keyframe关键帧。我们让这个view在times的30和60之间变淡(Let’s fade this view out between times 30 and 60)。

[alphaAnimation addKeyframeForTime:30 alpha:1.f];
[alphaAnimation addKeyframeForTime:60 alpha:0.f];

现在,让view动起来,要让animator知道what time it is。例如,把这个animation和UIScrollView绑定起来,在scroll的代理方法中来通知animator。

- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
[super scrollViewDidScroll:scrollView];
[self.animator animate:scrollView.contentOffset.x];
}

这样会产生的效果是,view在滚动位置的0到30之间时,view会淡入,变的可见。在滚动位置的30到60之间,view会淡出,变的不可见。而且在滚动位置大于60的时候会保持fade out。

动画的类型

Jazz Hands支持多种动画:

IFTTTAlphaAnimation 动画的是 alpha 属性 (创造的是淡入淡出的效果).
IFTTTRotationAnimation 动画的是旋转变换 (旋转效果).
IFTTTBackgroundColorAnimation 动画的是 backgroundColor 属性.
IFTTTCornerRadiusAnimation 动画的是 layer.cornerRadius 属性.
IFTTTHideAnimation 动画的是 hidden属性 (隐藏和展示view).
IFTTTScaleAnimation 应用一个缩放变换 (缩放尺寸).
IFTTTTranslationAnimation 应用一个平移变换 (平移view的位置).
IFTTTTransform3DAnimation 动画的是 layer.transform 属性 (是3D变换).
IFTTTTextColorAnimation 动画的是UILabel的 textColor 属性。
IFTTTFillColorAnimation 动画的是CAShapeLayer的fillColor属性。
IFTTTStrokeStartAnimation 动画的是CAShapeLayer的strokeStart属性。(does not work with IFTTTStrokeEndAnimation).
IFTTTStrokeEndAnimation 动画的是CAShapeLayer的strokeEnd属性。 (does not work with IFTTTStrokeStartAnimation).
IFTTTPathPositionAnimation 动画的是UIView的layer.position属性。
IFTTTConstraintConstantAnimation animates an AutoLayout constraint constant.
IFTTTConstraintMultiplierAnimation animates an AutoLayout constraint constant as a multiple of an attribute of another view (to offset or resize views based on another view’s size)
IFTTTScrollViewPageConstraintAnimation animates an AutoLayout constraint constant to place a view on a scroll view page (to position views on a scrollView using AutoLayout)
IFTTTFrameAnimation animates the frame property (moves and sizes views. Not compatible with AutoLayout).

更多例子

Easy Paging Scrollview Layouts in an AutoLayout World
JazzHands的IFTTTAnimatedPagingScrollViewController中的 keepView:onPage:方法,可以非常简单的在scroll view上布局分页。

调用keepView:onPages: 可以在多个pages上展示一个view,当其它view滚动的时候。

具体应用的例子

在开源项目coding/Coding-iOS中的IntroductionViewController有使用到,IntroductionViewController继承自IFTTTAnimatedPagingScrollViewController。

- (void)configureTipAndTitleViewAnimations{
for (int index = 0; index < self.numberOfPages; index++) {
NSString *viewKey = [self viewKeyForIndex:index];
UIView *iconView = [self.iconsDict objectForKey:viewKey];
UIView *tipView = [self.tipsDict objectForKey:viewKey];
if (iconView) {
if (index == 0) {//第一个页面
[self keepView:iconView onPages:@[@(index +1), @(index)] atTimes:@[@(index - 1), @(index)]];

[iconView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(kScreen_Height/7);
}];
}else{
[self keepView:iconView onPage:index];

[iconView mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerY.mas_equalTo(-kScreen_Height/6);//位置往上偏移
}];
}
IFTTTAlphaAnimation *iconAlphaAnimation = [IFTTTAlphaAnimation animationWithView:iconView];
[iconAlphaAnimation addKeyframeForTime:index -0.5 alpha:0.f];
[iconAlphaAnimation addKeyframeForTime:index alpha:1.f];
[iconAlphaAnimation addKeyframeForTime:index +0.5 alpha:0.f];
[self.animator addAnimation:iconAlphaAnimation];
}
if (tipView) {
[self keepView:tipView onPages:@[@(index +1), @(index), @(index-1)] atTimes:@[@(index - 1), @(index), @(index + 1)]];

IFTTTAlphaAnimation *tipAlphaAnimation = [IFTTTAlphaAnimation animationWithView:tipView];
[tipAlphaAnimation addKeyframeForTime:index -0.5 alpha:0.f];
[tipAlphaAnimation addKeyframeForTime:index alpha:1.f];
[tipAlphaAnimation addKeyframeForTime:index +0.5 alpha:0.f];
[self.animator addAnimation:tipAlphaAnimation];

[tipView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(iconView.mas_bottom).offset(kScaleFrom_iPhone5_Desgin(45));
}];
}
}
}

效果如下:


转自:https://blog.csdn.net/u014084081/article/details/53610215

收起阅读 »

网易换肤第二篇:本地换肤实现!

完整脑图: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 tess_two Android图片文字识别

ocr
先看效果图 我主要是识别截图,所以图片比较规范,识别率应该很高。 简介什么都不说了,直接看简单的用法吧 首先肯定是引入依赖了 dependencies { compile 'com.rmtheis:tess-two:6.2.0' } 简单的用法...
继续阅读 »


先看效果图


我主要是识别截图,所以图片比较规范,识别率应该很高。


简介什么都不说了,直接看简单的用法吧


首先肯定是引入依赖了


dependencies {
compile 'com.rmtheis:tess-two:6.2.0'
}

简单的用法其实就几行代码:


TessBaseAPI tessBaseAPI = new TessBaseAPI();
tessBaseAPI.init(DATAPATH, DEFAULT_LANGUAGE);//参数后面有说明。
tessBaseAPI.setImage(bitmap);
String text = tessBaseAPI.getUTF8Text();

就这样简单的把一个bitmap设置进去,就能识别到里面的文字并输出了。
但是真正用的时候还是遇到了点麻烦,虽然只是简单的识别。
主要是tessBaseAPI.init(DATAPATH, DEFAULT_LANGUAGE)这个方法容易出错。
先看一下这个方法的源码吧:


public boolean init(String datapath, String language) {
return init(datapath, language, OEM_DEFAULT);
}
/**
* Initializes the Tesseract engine with the specified language model(s). Returns
* true on success.
*
* @see #init(String, String)
*
* @param datapath the parent directory of tessdata ending in a forward
* slash
* @param language an ISO 639-3 string representing the language(s)
* @param ocrEngineMode the OCR engine mode to be set
* @return true on success
*/

public boolean init(String datapath, String language, int ocrEngineMode) {
if (datapath == null)
throw new IllegalArgumentException("Data path must not be null!");
if (!datapath.endsWith(File.separator))
datapath += File.separator;

File datapathFile = new File(datapath);
if (!datapathFile.exists())
throw new IllegalArgumentException("Data path does not exist!");

File tessdata = new File(datapath + "tessdata");
if (!tessdata.exists() || !tessdata.isDirectory())
throw new IllegalArgumentException("Data path must contain subfolder tessdata!");

//noinspection deprecation
if (ocrEngineMode != OEM_CUBE_ONLY) {
for (String languageCode : language.split("\\+")) {
if (!languageCode.startsWith("~")) {
File datafile = new File(tessdata + File.separator +
languageCode + ".traineddata");
if (!datafile.exists())
throw new IllegalArgumentException("Data file not found at " + datafile);
}
}
}

boolean success = nativeInitOem(mNativeData, datapath, language, ocrEngineMode);

if (success) {
mRecycled = false;
}

return success;
}

注意


从下面的方法中抛出的几个异常可以看出来,初始化的时候,第一个参数是个文件夹,而且这个文件夹中必须有一个tessdata的文件夹;而且这个文件夹中要有个文件叫做 第二个参数.traineddata 。具体的可以看下面代码里的注释。这些文件夹和文件没有的一定要创建好,不然会报错。


第二个参数.traineddata 是个什么文件呢?
这个是识别用到的语言库还是文字库什么的,按那个初始化方法的意思是哟啊放到SD卡中的。可以在下面的地址下载。我的demo里把这个文件放在了assets中,启动的时候复制到内存卡里。
https://github.com/tesseract-ocr/tessdata


chi_sim.traineddata应该是健体中文吧,我用的是这个。中英文都能识别。


代码


下面是主要代码:


import android.Manifest;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.support.annotation.NonNull;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import com.googlecode.tesseract.android.TessBaseAPI;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

public class MainActivity extends AppCompatActivity {

private static final String TAG = "MainActivity";
private Button btn;
private TextView tv;

/**
* TessBaseAPI初始化用到的第一个参数,是个目录。
*/

private static final String DATAPATH = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator;
/**
* 在DATAPATH中新建这个目录,TessBaseAPI初始化要求必须有这个目录。
*/

private static final String tessdata = DATAPATH + File.separator + "tessdata";
/**
* TessBaseAPI初始化测第二个参数,就是识别库的名字不要后缀名。
*/

private static final String DEFAULT_LANGUAGE = "chi_sim";
/**
* assets中的文件名
*/

private static final String DEFAULT_LANGUAGE_NAME = DEFAULT_LANGUAGE + ".traineddata";
/**
* 保存到SD卡中的完整文件名
*/

private static final String LANGUAGE_PATH = tessdata + File.separator + DEFAULT_LANGUAGE_NAME;

/**
* 权限请求值
*/

private static final int PERMISSION_REQUEST_CODE=0;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btn = (Button) findViewById(R.id.btn);
tv = (TextView) findViewById(R.id.tv);

if (Build.VERSION.SDK_INT >= 23) {
if (checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED ||
checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE}, PERMISSION_REQUEST_CODE);
}
}

//Android6.0之前安装时就能复制,6.0之后要先请求权限,所以6.0以上的这个方法无用。
copyToSD(LANGUAGE_PATH, DEFAULT_LANGUAGE_NAME);

btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new Thread(new Runnable() {
@Override
public void run() {
Log.i(TAG, "run: kaishi " + System.currentTimeMillis());

Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.quanbu);
Log.i(TAG, "run: bitmap " + System.currentTimeMillis());

TessBaseAPI tessBaseAPI = new TessBaseAPI();

tessBaseAPI.init(DATAPATH, DEFAULT_LANGUAGE);

tessBaseAPI.setImage(bitmap);
final String text = tessBaseAPI.getUTF8Text();
Log.i(TAG, "run: text " + System.currentTimeMillis() + text);
runOnUiThread(new Runnable() {
@Override
public void run() {
tv.setText(text);
}
});

tessBaseAPI.end();
}
}).start();
}
});

}

/**
* 将assets中的识别库复制到SD卡中
* @param path 要存放在SD卡中的 完整的文件名。这里是"/storage/emulated/0//tessdata/chi_sim.traineddata"
* @param name assets中的文件名 这里是 "chi_sim.traineddata"
*/

public void copyToSD(String path, String name) {
Log.i(TAG, "copyToSD: "+path);
Log.i(TAG, "copyToSD: "+name);

//如果存在就删掉
File f = new File(path);
if (f.exists()){
f.delete();
}
if (!f.exists()){
File p = new File(f.getParent());
if (!p.exists()){
p.mkdirs();
}
try {
f.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}

InputStream is=null;
OutputStream os=null;
try {
is = this.getAssets().open(name);
File file = new File(path);
os = new FileOutputStream(file);
byte[] bytes = new byte[2048];
int len = 0;
while ((len = is.read(bytes)) != -1) {
os.write(bytes, 0, len);
}
os.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (is != null)
is.close();
if (os != null)
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}

}

/**
* 请求到权限后在这里复制识别库
* @param requestCode
* @param permissions
* @param grantResults
*/

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
Log.i(TAG, "onRequestPermissionsResult: "+grantResults[0]);
switch (requestCode){
case PERMISSION_REQUEST_CODE:
if (grantResults.length>0&&grantResults[0]==PackageManager.PERMISSION_GRANTED){
Log.i(TAG, "onRequestPermissionsResult: copy");
copyToSD(LANGUAGE_PATH, DEFAULT_LANGUAGE_NAME);
}
break;
default:
break;
}
}
}



GitHub:https://github.com/rmtheis/tess-two

Demo的GitHub地址:https://github.com/wangyisll/TessTwoDemo

下载地址:tess-two

收起阅读 »

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
}
}
}
复制代码

使用过后代码非常的简洁而且也不需要再用 !! 或者定义一个新的变量,有兴趣的同学可以去看下源码。

收起阅读 »