Coding and Learning

oubindo的技术与生活博客


  • Home

  • Archives

Android插件化系列二:资源与打包流程

Posted on 2019-09-14

Android插件化系列二: 资源与打包流程

好的朋友们,新的一周开始了,让我们继续来学习插件化的知识吧。先回顾一下系列文章架构
Android插件化文章框架

根据我的行文思路,本篇文章讲解资源和App打包的一些知识。算是插件化系列的第二篇基础文章。阅读完本文后,你应该会了解:

  • 资源id的组成,
  • R.java的秘密
  • App打包流程

资源这一部分将会先从大家的直观印象切入,逐步的加大深度。然后我会结合前半部分资源的铺垫讲解App的打包流程。大家如果阅读完以后发现,咦,这一点我还真不知道,那本文也算是有点意义了。因为本篇依然属于插件化的基础知识文章,所以还是不会讲到插件化,但是后面讲到插件化的时候会引用到本篇文章的部分知识。从另外个角度来说,本篇文章也是一篇知识比较自成一体的文章。OK,那咱们开始吧。

资源与R.java

先做一点准备工作,我们建一个工程,这个工程下面有三个module,App和我们自建的modulea,moduleb。这三个module的依赖关系是app->modulea->moduleb。然后我们在每个module里面放一点资源,比如string之类的,这里我在modulea中放了一个String叫testA, 在moduleb中放了一个String叫testB。然后我们会发现在每个module的build/generated/source/r(这个文件夹跟gradle版本有关系,3.5以后文件夹有变更)下面出现了R.java文件,这个就是android打包过程中借助于aapt工具生成的资源id目录。

然后我们分别打开主模块和modulea的R.java。下面是主模块和普通模块的R.java文件中的id示例。

1
2
3
4
5
6
7
// 主模块app中的R.java
public static final int testA = 0x7f0b002a;
public static final int testB=0x7f0b002b;

// modulea中的R.java
public static int testA = 0x7f15002b;
public static int testB = 0x7f15002c;

大家可以看到:

  • 为什么资源组成都以0x7f开头?
  • 为什么主模块(application module)资源有final修饰,非主模块(library module,后面也称库模块)都不是final的
  • 为什么同一个资源,不同模块产生的R.java中的资源id值是不统一的

为什么会这样呢?我们将在后面讲解这些内容并在最后给出结论

1.资源Id的组成

我们先看看资源Id的组成。大家都知道,资源id是一个资源的唯一标识。那么问题来了,这么多的module,这么多的资源种类,甚至还有Android自带的资源,资源id为什么不会重复呢?秘诀就在资源id的组成上面。

packageId: 前两位是packageId,相当于一个命名空间,主要用来区分不同的包空间(不是不同的module)。目前来看,在编译app的时候,至少会遇到两个包空间:android系统资源包和咱们自己的App资源包。大家可以观察R.java文件,可以看到部分是以0x01开头的,部分是以0x7f开头的。以0x01开头的就是系统已经内置的资源id,以0x7f开头的是咱们自己添加的app资源id。

typeId:typeId是指资源的类型id,我们知道android资源有animator、anim、color、drawable、layout,string等等,typeId就是拿来区分不同的资源类型。

entryId:entryId是指每一个资源在其所属的资源类型中所出现的次序。注意,不同类型的资源的Entry ID有可能是相同的,但是由于它们的类型不同,我们仍然可以通过其资源ID来区别开来。

通过资源id的三个区块的划分,在编译期间,同一个资源在普通的apk中只会属于一个package,一个type,只拥有一个次序,所以一个资源的id是不会和别的资源重复的。当然这只是正常情况下,要是我们有部分资源没有参与打包呢?比如说我们要说的插件化,插件化是要下发一个插件,插件中当然也有资源,这部分资源是没有经过统一的编译的,那么就可能存在和宿主(插件要下发到的App)资源冲突的情况。比如你已经给梁山排好了108将,每个人都有一个称号,但是从山下又来了一个“及时雨”宋江,那岂不是同时存在两个及时雨了,听谁的呢?梁山就会大乱,app也是如此。

为了避免这种情况,插件的资源id通常会采用0x02 - 0x7e之间的数值,避免和宿主资源冲突。至于怎么做到的,等后面的文章再聊~

2.资源id的使用

我们通常会在编码的时候使用类似于R.layout.xxxx一类的引用,这些引用就是R.java文件中的字段。并且我们在主模块和library模块中使用这些id的时候,好像并没有什么区别,那么这两者中的id真的是毫无区别吗?

我们先看看在主模块中和库模块中分别去使用id的区别。我们分别在app模块和modulea模块中分别建一个Activity。每个Activity中有一段这样的代码,大家应该都比较熟悉

1
2
3
4
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}

然后我们点击Android Studio的Tools->kotlin->show kotlin bytecode直接看这个类的字节码。当然直接看字节码还是比较难,我们再点面板上的decompile,把它解析成java代码。然后我们就会发现,有点细微的区别。

1
2
3
4
5
6
7
8
9
10
11
// 主模块的代码
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.setContentView(-1300009);
}

// 库模块的代码
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.setContentView(layout.activity_module_a);
}

大家可以看到,主模块中的R.layout.xxx完全是作为常量,直接内联进了代码中。而库模块中的R.layout.xxx, 依然是作为变量引用到了代码中。这个规律在编译期间也是存在的。这个规律和前面对R.java中的字段的处理是一致的,也即是说,

  • 主模块中的R.java中的字段以final修饰,以常量形式存在。
  • 库模块中的R.java中的字段不以final修饰,以变量形式被项目中的代码所引用。

3.资源的合并

通常apk中的资源来源主要是3个,具体可以参考官网:

  • 主资源(main source set):比如src/main/res
  • 编译变量(Build variant source set): 比如src/demoDebug/res
  • 库文件依赖(libraries): 也就是我们引进的aar。

一个资源通常会使用它的文件名作为标识,也就是说,相同resource type(anim/drawable/string等)和相同resource qualifier(比如hdpi, value中的语言等)下相同文件名的资源,系统会认为他是唯一的。那么单一module下可能就会有相同的资源存在,比如有多个主资源集。那么当出现这种冲突的情况的时候,系统会怎么处理呢?系统会进行合并,低优先级的资源会被覆盖掉。

覆盖的优先级如下:
build variant > build type > product flavor > main source set > library dependences

举个栗子,如果我们主资源集下有两个资源: res/layout/a.xml, res/layout/b.xml, build type文件夹下面有res/layout/a.xml。那么最后打包生成的apk中的res/layout/a.xml来自于build type, res/layout/b.xml来自于main source set。

除了单一module不同文件夹下的资源覆盖,不同module间也会有资源覆盖。比如app模块依赖了modulea,两个module中都有一个资源文件res/layout/a.xml,那么最后编译的apk中的res/layout/a.xml一定是app模块下定义的那个。

资源合并有什么实际意义呢?我个人认为通过资源合并可以实现更高级别的自适应打包。比如说,我们可以为不同的product flavor去设置不同的资源,比如页面xml,这样,只要改一下product flavor就能打出不一样的包,实现更高级别的自适应。

4.R文件的生成

上面讲了资源id的一些机制,接下来我们来探讨一下R文件的生成机制。这里的规律是基于gradle 3.1.2。

首先我们先看一下数量上的规律,还是以我们上面的例子为例。三个module的依赖关系是app->modulea->moduleb。modulea中有个string叫testA,moduleb中有个string叫testB。最后我们发现app模块下面有三个R文件。

并且发现plugindemo(也即是App模块)下面的R文件里包含了我们在modulea和moduleb中定义的string。

通过上面的例子可以给出结论,用一个图可以说明。

1.数量的规律:一个module被编译的时候,会生成当前module的R文件,并且该module依赖的module或者aar也会在当前module生成R文件。这种依赖关系不同于gradle里面的implementation依赖传递,implementation是跨级不能传递,但是R文件的生成是跨级可以传递的。所以,
module的R文件数 = 依赖的module/aar数量 + 1(自身的R文件)

举个例子,A模块依赖了B模块,同时也依赖了fresco,那么他生成的R文件有几个呢?答案是三个,B模块,fresco,和自身的R文件。

2.生成顺序的规律,三个模块的依赖关系是app->modulea->moduleb。生成R文件的顺序是从底层到上层,逐层生成。也就是说先生成moduleb的,再生成modulea的,再生成app模块的。

3.资源的规律:上层模块会把所依赖的模块的R文件merge进去。比如app模块并没有testA和testB这两个string,但是app的R文件却包含了这两个资源的id。这就是因为上层的模块把下层模块的资源给merge进去了。

5.总结

讲完了这些规律,我们就可以回答小节一开头提出的三个问题了。

1.为什么资源id都以0x7f开头?
因为这些资源都是应用包的资源,统一是0x7f开头

2.为什么主模块(application module)资源有final修饰,非主模块(library module)都不是final的?
比较早的aapt的版本生成的非主模块的资源id确实都是final修饰的,这样会带来一个问题,这些资源id全部内联到代码中,一旦新增或者删除,修改了资源,资源id就会有变化,所有的代码都需要重新编译,造成严重的编译耗时。后来改为主模块final常量方式内联,非主模块引用方式,这样等按照从下到上编译到App模块的时候,所有的资源id都已经确定了,底层模块的资源只需要通过引用就能拿到自己对应的id,而修改(新增,删除,修改)了资源之后,也只需要重新生成R文件就好了。编译耗时大大减少。

3.为什么同一个资源,不同模块产生的R.java中的资源id值是不统一的?
因为资源id只是表示资源的次序,而不是别的跟资源本身绑定的属性。当到了不同的模块以后,参与编译的资源变多了,那次序肯定会改变。资源id也就改变了。并且子模块的资源id只是引用形式存在于代码中,id具体是什么值并不是很care。

不知道大家看完这些,有没有什么收获呢?

6.补充知识

不知道大家有没有用过ButterKnife这个依赖注入框架,这个框架最核心的使用场景就是使用注解进行依赖注入。比如

1
@BindView(R.id.user) EditText username;

大家应该常见这种用法,那么,这里有没有什么玄机呢?我们上面讲到了,非主模块中资源id是变量,没有final修饰。但是注解大家都知道,传入的参数必须是final常量。这样的话岂不是相悖了吗?

其实上面的两个结论都没有错。Butterknife针对这种情况做了一个骚处理。他直接copy了一份module中的R.java,搞了个R2.java,把R.java中所有的资源id全部改为final的,这样就能在注解中使用了。等到真正使用的时候,再进行替换,使用真正的主模块的生成的资源id。

具体可以参考R.java、R2.java 是时候懂了

App打包

打包流程这一块我会先讲述基本流程,然后会补充一些关于打包流程的应用的扩展知识。

1.打包流程

先来一张打包流程图。

1.打包资源文件,生成R.java文件
这一过程主要是aapt对res和asset文件夹,AndroidManifest.xml,android库(aar,jar)等的资源文件进行处理。先检查AndroidManifest.xml的合法性,然后编译res与asserts目录下的资源并生成resource.arsc文件,再生成R文件。除了assets和res/raw资源被原封不动地打包进APK之外,其它的资源都会被编译或者处理,大部分文本格式的XML资源文件会被编译成二进制格式的XML资源文件。除了assets资源之外,其他的资源都会在R文件中被赋予一个资源ID。也就是说,R文件中只会存在id,真正的资源存在于resource.arsc中,resource.arsc相当于一个资源索引表,资源id是key,value是资源路径。我们使用drawable-xdpi或者drawable-xxdpi这些不同分辨率的图片的时候,就是依靠resource.arsc根据设备的分辨率选择不同的图片。

2.处理aidl文件,生成相应的.java文件
这一步就是我们代码中的aidl的文件被生成java文件。

3.编译工程源码,生成相应的class文件
R文件,aidl生成的java文件和我们工程中的源代码被javac工具编译成了class文件。

4.转换所有的class文件,生成classes.dex文件
Android系统的dalvik虚拟机的可执行文件为dex格式,程序运行所需的classes.dex文件就是在这一步生成的,使用的工具为dx,dx工具主要的工作是将java字节码转换为dalvik字节码、压缩常量池、消除冗余信息等。
这里在生成dex的时候,就会遇到65536的问题。一个DEX文件中的method个数采用使用原生类型short来索引文件的方法,也就是4个字节共计最多表达65536个method。所以当method数过多的时候,就必须使用multidex。

5.打包生成apk
把所有的dex文件打包为一个apk文件。

6.对apk文件进行签名
apk需要签名才能在手机上安装。平时我们测试主要是使用了一个debug.keystore对apk进行签名。正式发布时需要提供一个符合android开发文档中要求的签名文件。比如jarsigner和APK Signature Scheme v2。

7.对签名后的apk进行对齐处理
一步需要使用的工具为zipalign,它位于android-sdk/tools目录,源码位于android系统资源的build/tools/zipalign目录,它的主要工作是将apk包进行对齐处理,使apk包中的所有资源文件举例文件起始偏移为4字节的整数倍,这样通过内存映射访问apk时的速度会更快。为什么快呢?如果每个资源的开始位置上都是一个资源之后的4n字节,那么访问下一个资源就不用遍历,直接跳到4字节之后即可。

8.混淆proguard:proguard主要的目的是混淆代码,保护应用源代码。次要的功能还有移除无用类等,优化字节码,缩小包体积。

  • 压缩(Shrink):检测并移除代码中无用的类、字段、方法和特性(Attribute)
  • 优化(Optimize):字节码进行优化,移除无用的指令。
  • 混淆(Obfuscate):使用a、b、c、d这样简短而无意义的名称,对垒、字段和方法进行重命名。
  • 预检测(Preveirfy):在Java平台对处理后的代码进行预检测,确保加载class文件是可执行的。

2.一些技术点

1.资源去重,极致缩包
前面我们讲到了proguard的功能是混淆代码和缩减体积。但是proguard是不能处理资源文件的。为了解决资源文件的混淆问题,微信推出了AndResGuard。使用AndResGuard可以更加缩减包体积。

除了AndResGuard之外,我们还会遇到资源被重复使用的问题,识别重复资源很简单,只要计算一下md5就行了。并且我们在resources.arsc中可以拿到所有的资源,那么我们就可以对resources.arsc中的所有资源进行处理,根据md5进行去重,把使用了相同资源的资源id都指向同一个资源,把多余的资源删除掉,再回写入resources.arsc就好了。当然,这里面还是有挺多学问的。

2.Transform
Transform是Android gradle plugin提供给开发者的一套API,允许开发者在编译之后,dex之前对class进行修改。开发者可以通过AppExtension或者LibraryExtension进行注册Transform。多个transform会形成一条链。上一个Transform的输出是下一个Transform的输入,因此,Transform的顺序也很重要。

既然有了这个Transform,就意味着我们有机会去操作java的字节码。网上常见的处理字节码的框架有AspectJ, Javasist, ASM。可以利用这些工具进行字节码插桩。这样可以把一些不能耦合在业务代码中的代码在字节码阶段给merge进去。

3.多渠道打包
Android和iOS不一样的是市场和渠道众多,为了区分和统计不同的渠道包的效果,需要有一种方法来标记他们。大家可能会想到使用productFlavor,但是这样的话要打多少包就需要build多少次,耗时非常长。

现在比较好的方案是在apk进行v2签名的时候在签名块中写入一些信息,这样更快更安全。详情可以参考Android美团多渠道打包Walle集成

3.总结

本小节讲解了打包过程,和利用打包机制可以做的一些技术点。打包过程如果能学的透彻的话,还是能给android开发带来很多的可能性。

参考文章:
罗升阳 Android应用程序资源的编译和打包过程分析

Android美团多渠道打包Walle集成

Android插件化系列一:开篇前言,Binder机制,ClassLoader

Posted on 2019-09-07

Android插件化系列一: 开篇前言,Binder机制,ClassLoader

系列前言

Hello,美好的一周又开始啦,让我们开始愉快的学习吧。

从今天开始,我会花较多的时间来跟大家一起学习Android插件化。这一篇文章是Android插件化的启动篇。

Android插件化是之前几年里的一个很火的技术概念。从2012年开始就有人在研究这门技术。从粗糙的AndroidDynamicLoader框架,到第一代的DroidPlugin等,继而发展到第二代的VirtualApk,Replugin等,再到现如今的VirtualApp,Atlas。插件化在国内逐渐的发展和完善,却也在近几年出现了RN等替代品以后慢慢会走向弱势。

尽管插件化技术的研究热潮已经过去,但是这门技术本身还是有着大量的技术实践,对于我们了解Android机制很有帮助。所以从这篇文章开始我会写一系列的文章,加上自己对插件化的实践,最后会去从源码角度分析几个优秀的插件化库,形成一套完整的插件化的理论体系。

下面是插件化的技术框架,也是我这个系列文章的行文思路,

Android插件化文章框架

一. Binder机制

网上分析Binder机制的文章已经很多了,在这篇文章里,我不会去讲解Binder的使用,而是会去讲解清楚Binder的设计思路,设计原理和对于插件化的使用。

为什么需要Binder

首先我们知道,Android是基于Linux内核开发的。对于Linux来说,操作系统为一个二进制可执行文件创建了一个载有该文件自己的栈,堆、数据映射以及共享库的内存片段,还为其分配特殊的内部管理结构。这就是一个进程。操作系统必须提供公平的使用机制,使得每个进程能正常的开始,执行和终结。

这样呢,就引入了一个问题。一个进程能不能去操作别的进程的数据呢?我们可以想一下,这是绝对不能出现的,尤其是系统级的进程,如果被别的进程影响了可能会造成整个系统的崩塌。所以我们很自然的想到,我们应该把进程隔离起来,linux也是这样做的,它的虚拟内存机制为每个进程分配连续的内存空间,进程只能操作自己的虚拟内存空间。同时,还必须满足进程之间保持通信的能力,毕竟团结力量大,单凭单个进程的独立运作是不能撑起操作系统的功能需求的。

为了解决这个问题,Linux引进了用户空间User Space和内核空间Kernel Space的区别。用户空间要想访问内核空间,唯一方式就是系统调用。内核空间通过接口把应用程序请求传给内核处理后返回给应用程序。同时,用户空间进程如果想升级为内核空间进程,需要进行安全检查。

补充知识:系统调用主要通过两个方法:

  • copy_from_user():将用户空间的数据拷贝到内核空间

  • copy_to_user():将内核空间的数据拷贝到用户空间

linux系统调用

以上就是linux系统的跨进程通信机制。而我们马上要说的Binder,就是跨进程的一种方式

Binder模型

Binder是一种进程间通信(IPC)方式,Android常见的进程中通信方式有文件共享,Bundle,AIDL,Messenger,ContentProvider,Socket。其中AIDL,Messenger,ContentProvider都是基于Binder。Linux系统通过Binder驱动来管理Binder机制。

Binder的实现基于mmap()系统调用,只用拷贝一次,比常规文件页操作更快。微信开源的MMKV等也是基于此。有兴趣的可以了解一下。

首先Binder机制有四个参与者,Server,Client两个进程,ServiceManager,Binder驱动(内核空间)。其中ServiceManager和Binder驱动都是系统实现的,而Server和Client是需要开发者自己实现的。四者之中只有Binder驱动是运行在内核空间的。

Binder机制

这里的ServiceManager作为Manager,承担着Binder通信的建立,Binder的注册和传递的能力。Service负责创建Binder,并为他起一个字符形式的名字,然后把Binder和名字通过通过Binder驱动,借助于ServiceManager自带的Binder向ServiceManager注册。注意这里,因为Service和ServiceManager也是跨进程通信需要Binder,ServerManager是自带Binder的,所以相对ServiceManager来说Service也就相当于Client了。

Service注册了这个Binder以后,Client就能通过名字获得Binder的引用了。这里的跨进程通信双方就变成了Client和ServiceManager,然后ServiceManager从Binder表取出Binder的引用返给Client,这样的话如果有多个Client的话,多次返回引用就行了,但是事实上引用的都是放在ServiceManager中的Service。

当Client经过Binder驱动跟Service通信的时候,往往需要获取到Service的某个对象object。这时候为了安全考虑,Binder会把object的代理对象proxyobject返回,这个对象拥有一模一样的方法,但是没有具体能力,只负责接收参数传给真正的object使用。

所以完整的Binder通信过程是

binder模型

OK,跨进程通信就讲清楚了。接下来我们讲讲插件化中的Binder。

Binder与插件化

首先,我们先回顾一下Activity的启动过程,Instrumentation调用了ActivityManagerNative,这个AMN是我们的本地对象,然后AMN调用getDefault拿到了ActivityManagerProxy,这个人AMP就是AMS在本地的代理。相当于binder模型中的Client,而这个AMP继承的是IActivityManager,拥有四大组件的所有需要AMS参与的方法。本地通过调用这个AMP方法来间接地调用AMS。这样,我们就调用到了AMS启动了Activity。

那么,AMS如何与Client进行通信呢?现在我们通过Launcher启动了Activity,肯定要告诉Launcher “没你什么事了,你洗洗睡吧”。大家可以看到,这里双方的角色就发生了改变,AMS需要去发消息,承担Client的角色,而Launcher这时候作为Service提供服务。而这次通信同样也是使用的Binder机制。AMS这边保存了一个ApplicationThreadProxy对象,这个对象就是Launcher的ApplicationThread的代理。AMS通过ATP给App发消息,App通过ApplicationThread处理。

Activity启动流程

以上,就是Binder机制在Android中的运用,我们后面会通过hook这个过程实现插件化。

总结

看了这么多,可能还是很多朋友不懂Binder。我也是这样,很长一段时间都不知道Binder到底指的是啥。后来我看到了这样一种定义:

  • 从进程间通信的角度看,Binder 是一种进程间通信的机制;

  • 从 Server 进程的角度看,Binder 指的是 Server 中的 Binder 实体对象;

  • 从 Client 进程的角度看,Binder 指的是对 Binder 代理对象,是 Binder 实体对象的一个远程代理

  • 从传输过程的角度看,Binder 是一个可以跨进程传输的对象;Binder 驱动会对这个跨越进程边界的对象对一点点特殊处理,自动完成代理对象和本地对象之间的转换。

二. ClassLoader

双亲委托模型

Java中默认有三种ClassLoader。分别是:

  • BootStrap ClassLoader:启动类加载器,最顶层的加载器。主要负责加载JDK中的核心类。在JVM启动后也随着启动,并构造Ext ClassLoader和App ClassLoader。
  • Extension ClassLoader:扩展类加载器,负责加载Java的扩展类库。
  • App ClassLoader:系统类加载器,负责加载应用程序的所有jar和class文件。
  • 自定义ClassLoader:需要继承自ClassLoader类。

ClassLoader默认使用双亲委托模型来搜索类。每个ClassLoader都有一个父类的引用。当ClassLoader需要加载某个类时,先判断是否加载过,如果加载过就返回Class对象。否则交给他的父类去加载,继续判断是否加载过。这样 层层判断,就到了最顶层的BootStrap ClassLoader来试图加载。如果连最顶层的Bootstrap ClassLoader都没加载过,那就加载。如果加载失败,就转交给子ClassLoader,层层加载,直到最底层。如果还不能加载的话那就只能抛出异常了。

通过这种双亲委托模型,好处是:

  • 更高效,父类加载一次就可以避免了子类多次重复加载
  • 更安全,避免了外界伪造java核心类。

Android中的ClassLoader

android从5.0开始使用art虚拟机,这种虚拟机在程序运行时也需要ClassLoader将类加载到内存中,但是与java不同的是,java虚拟机通过读取class字节码来加载,但是art则是通过dex字节码来加载。这是一种优化,可以合并多个class文件为一个classes.dex文件。

android一共有三种类加载器:

  • BootClassLoader:父类构造器

  • PathClassLoader:一般是加载指定路径/data/app中的apk,也就是安装到手机中的apk。所以一般作为默认的加载器。

  • DexClassLoader:从包含classes.dex的jar或者apk中,加载类的加载器,可用于动态加载。

看PathClassLoader和DexClassLoader源码,都是继承自BaseDexClassLoader。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
}


public class PathClassLoader extends BaseDexClassLoader {

public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent); //见下文
}
}

public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList;

public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(parent); //见下文
//收集dex文件和Native动态库【见小节3.2】
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
}

我们可以看到PathClassLoader的两个参数都为null,表明只能接受固定的dex文件,而这个文件是只能在安装后出现的。而DexClassLoader中optimizedDirectory,和librarySearchPath都是可以自己定义的,说明我们可以传入一个jar或者apk包,保证解压缩后是一个dex文件就可以操作了。因此,我们通常使用DexClassLoader来进行插件化和热修复。

可以看到,BaseDexClassLoader有一个相当重要的过程就是初始化DexPathList。初始化DexPathList的过程主要是收集dexElements和nativeLibraryPathElements。一个Classloader可以包含多个dex文件,每个dex文件被封装到一个Element对象。这element对象在初始化和热修复逻辑中是相当重要的。当查找某个类时,会遍历dexElements,如果找到就返回,否则继续遍历。所以当多个dex中有相同的类,只会加载前面的dex中的类。下面是这段逻辑的具体实现

1
2
3
4
5
6
7
8
9
10
11
12
13
public Class findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
//找到目标类,则直接返回
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
return null;
}

总结

我们先是讲解了Java中类加载的双亲委托机制,然后介绍了Android中的几种ClassLoader,从源码角度介绍了两种ClassLoader加载机制的不同。以后的插件化实践中,我们会经常用到DexClassLoader。

参考资料

Android跨进程通信:图文详解 Binder机制 原理

写给 Android 应用工程师的 Binder 原理剖析

深入理解Android中的ClassLoader

Android类加载器ClassLoader

一篇文章深入gradle(上篇):依赖机制

Posted on 2019-09-05

一篇文章深入gradle(上篇):依赖机制

Hello,各位朋友们,小笨鸟又和你们见面啦。不同于网上泛泛而谈的入门文章只停留在“怎么用”的层次,本篇文章从源码角度去理解gradle。当然,如果你没有看过我的前一篇文章《一篇文章基本看懂gradle》,还是建议你先看一下,本篇文章的很多知识点会在上篇文章的基础上展开。

本篇文章会比较深入,需要费一些脑筋和精力才能理解。建议跟着我的行文思路一步一步理解。本文的篇幅会比较长。阅读大概需要花费半个小时。阅读本文将会了解:

  • gradle构建Lifecycle
  • gradle Extension和Plugin
  • gradle依赖实现,Configuration

其中gradle依赖将会是本文的重点。本来之前还想把artifact也讲解到,但是发现篇幅已经很长了,所以就一篇文章拆成两篇吧。

gradle构建Lifecycle

Lifecycle的概念我们在上一篇文章讲Project的时候也讲到过。在这篇文章中,我们会再简单讲一讲作为引入。

正如官网所说,gradle构建流程总共为三个阶段:

  • 初始化阶段:在这个阶段中settings.gradle是主角,gradle会把settings.gradle中的配置代理给Settings类。主要负责的是判断哪些module需要参与到构建中,然后根据这些module的设置初始化他们的delegate对象Project。注意,Project对象是在初始化阶段生成的。
  • 配置阶段:配置阶段的任务是执行各项目下的build.gradle,完成Project对象的配置,并且构造Task任务依赖关系图TaskExectionGraph以便在执行阶段按照依赖关系执行Task。
  • 执行阶段:执行阶段会根据你命令行输入的Task,按照依赖关系通过调用gradle taskname进行执行。

值得一提的是,我们可以通过多种方式对project构建阶段进行监控和处理。如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 以下为Project的方法
afterEvaluate(closure),afterEvaluate(action)
beforeEvaluate(closure),beforeEvaluate(action)

// 以下为gradle提供的生命周期回调
afterProject(closure),afterProject(action)
beforeProject(closure),beforeProject(action)
buildFinished(closure),buildFinished(action)
projectsEvaluated(closure),projectsEvaluated(action)
projectsLoaded(closure),projectsLoaded(action)
settingsEvaluated(closure),settingsEvaluated(action)
addBuildListener(buildListener)
addListener(listener)
addProjectEvaluationListener(listener)

// Task也有这种方法
afterTask​(Closure closure)
beforeTask(Closure closure)
//任务准备好后调用
whenReady(Closure closure)

那么知道这样的构建流程我们可以怎么使用呢?我们可以进行监控,或者是动态的根据需要去控制project和task的构建执行。比如为了加快编译速度,我们去掉一些测试的task。就可以这样写

1
2
3
4
5
6
7
8
9
gradle.taskGraph.whenReady {
tasks.each { task ->
if (task.name.contains("Test")) {
task.enabled = false
} else if (task.name == "mockableAndroidJar") {
task.enabled = false
}
}
}

Extension和Plugin

Extension和Plugin都是我们日常开发中经常有讲到的东西。上一篇文章中我们讲到了build.gradle中的闭包都是有一个Delegate代理的,这个代理对象可以接受闭包中的参数传递给自己来使用,那么这个代理是啥呢?其实就是我们这里要说的Extension。

1.Extension通俗来讲其实就是一个普通的java bean。里面存放一些参数。使用需要借助于ExtensionContainer来进行创建。举个栗子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 先定义一个实体类
public class Message {
String message = "empty"
String greeter = "none"
}

// 接下来在gradle文件中去使用project.extensions(也就是ExtensionContainer)来进行创建。
def extension = project.extensions.create("cyMessage", Message)

// 再写个task来进行验证
project.task('sendMessage') {
doLast {
println "${extension.message} from ${extension.greeter}"
println project.cyMessage.message + "from ${extension.greeter}"
}
}

2.Plugin插件
讲完了Extension,我们可能就会疑惑了。因为我们在build.gradle中并没有看到这些javabean和extension添加的操作啊,那么这些代码是在哪里写的呢?这时候我们就要引入Plugin的概念了,也就是你看到的

1
apply plugin 'com.android.application'

这个玩意了。这个就是Android Application的插件。android相关的Extension都是定义在这个插件中的。
有些同学可能对插件不是很理解,为什么需要这个东西。其实大家可以思考一下,gradle只是一个通用的构建工具。在他上层可能有各种应用,比如java,比如Android,甚至可能是未来的鸿蒙。那么这些应用对gradle肯定会有不同的扩展,又肯定不能把这些扩展直接放在gradle中。所以在上层添加插件就是个好选择,需要什么扩展就选什么插件。
Android开发中常见的插件有三种:

1
2
3
apply plugin 'com.android.application'   // AppPlugin,  主module才能引用
apply plugin 'com.android.library' // LibraryPlugin, 普通的android module引用
apply plugin 'java' // java module的引用

插件的其它知识不是本篇文章的重点,网上这方面的文章很多,大家可以自行学习。我们根据现在掌握的知识就要去探索gradle更深层次的知识啦。

gradle依赖实现

这两篇文章截止到现在,gradle构建相关的流程我们基本都走通了。但是有个很重要的组件我们没有去分析。就是依赖管理。我们前一篇文章也讲到了,依赖管理是gradle的一个很重要的特性,方便我们进行代码复用。那么我们这一部分就专门讲一讲依赖管理到底是怎么实现的。

首先我们还是看看基本的代码

1
2
3
4
5
6
7
8
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation 'com.android.support:appcompat-v7:26.1.0'
api 'com.android.support:appcompat-v7:26.1.0'
implementation project(':test')
androidTestImplementation 'com.android.support.test:runner:1.0.1'
annotationProcessor 'com.jakewharton:butterknife-compiler:8.4.0'
}

我们可以看到,一般有三种三方库类型,第一种是二进制文件,fileTree指向这个文件夹下的所有jar文件,第二种是三方库的string坐标形式,第三种是project的形式。
当然,我们也会知道,代码中的implementation和api两种引用方式是有区别的。implement的依赖是不能传递的,但是api是可以的。

我们在之前的研发中,可能没有去考虑深层次的这三种三方库类型,两种引用方式的原因。当我们按照上一篇中所说的代理模式去DependencyHandler中找implement, api等相关方法的时候,却发现,好像并没有定义这些方法,那么这是怎么回事呢?如果没有定义,是不是可以随便定义一种引用方式呢?带着问题我们往下面看

1.源码阅读姿势

首先我们还是先介绍一下阅读gradle源码的方式。我尝试了很多种方式,发现还是Android studio阅读起来最舒服,然后找到了一种方法。就是可以建一个gradle的demo,然后建一个module,把除了build.gradle以外的东西全部删掉。然后拷贝下面的代码进去。这样就能看到源码了

1
2
3
4
5
6
apply plugin 'java'

dependencies {
compile gradleApi()
compile 'xxxx' // 填入你gradle的版本
}

2.源码分析

2.1 methodmissing

首先我们先介绍一个groovy语言的特性:methodmissing。大家可以参考官网
简单来说就是当我们预先在一个类中定义一个methodmissing方法。然后在这个类的对象上调用之前没有定义过的方法时,这个方法就会降级(fallback)到它所定义的methodmissing方法上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class GORM {

def dynamicMethods = [...] // an array of dynamic methods that use regex

def methodMissing(String name, args) {
def method = dynamicMethods.find { it.match(name) }
if(method) {
GORM.metaClass."$name" = { Object[] varArgs ->
method.invoke(delegate, name, varArgs)
}
return method.invoke(delegate,name, args)
}
else throw new MissingMethodException(name, delegate, args)
}
}

assert new GORM().methodA(1) == resultA

如图,当我们调用methodA时,因为这个方法没有定义,就会转到methodmissing方法上,并且会把这个方法的名字methodA和它的参数一起传到methodmissing,这样如果dynamicMethod里面有定义methodA的话,这个方法就能执行了。这就是methodmissing的妙用。

为什么需要这种机制呢?我理解这还是为了扩展性。dependencies是gradle自身的功能。它不能完全的总括所有上层应用可能会有的引用方式。每种插件都可能增加引用的方式,为了扩展性考虑,必须采用这种methodmissing的特性,把这些引用交给插件处理。比如Android的implement, api, annotationProcessor等。

2.2 Configuration

在往下面讲解前,我们先了解一下Configuration的一些知识。
按照官网所说:

Every dependency declared for a Gradle project applies to a specific scope. For example some dependencies should be used for compiling source code whereas others only need to be available at runtime. Gradle represents the scope of a dependency with the help of a Configuration. Every configuration can be identified by a unique name.

也就是说,Configuration定义了依赖在编译和运行时候的不同范围, 每个Configuration都有name来区分。比如android常见的两种依赖方式implementation和api。
。

2.3 依赖的识别

gradle中使用MethodMixIn这个接口来实现methodmissing的能力。

1
2
3
4
// MethodMixIn
public interface MethodMixIn {
MethodAccess getAdditionalMethods();
}

1
2
3
4
5
6
7
8
9
10
11
12
13
public interface MethodAccess {
/**
* Returns true when this object is known to have a method with the given name that accepts the given arguments.
*
* <p>Note that not every method is known. Some methods may require an attempt invoke it in order for them to be discovered.</p>
*/
boolean hasMethod(String name, Object... arguments);

/**
* Invokes the method with the given name and arguments.
*/
DynamicInvokeResult tryInvokeMethod(String name, Object... arguments);
}

可以看到这里的methodmissing主要是在找不到这个方法的时候去返回一个MethodAccess,MethodAccess中去判断是否存在以及动态执行这个method。

接下来我们看DependencyHandler的实现类DefaultDependencyHandler。这个类实现了MethodMixIn接口,返回的是一个DynamicAddDependencyMethods对象。

1
2
3
4
5
6
7
8
9
// DefaultDependencyHandler.java
public DefaultDependencyHandler(...) {
...
this.dynamicMethods = new DynamicAddDependencyMethods(configurationContainer, new DefaultDependencyHandler.DirectDependencyAdder());
}

public MethodAccess getAdditionalMethods() {
return this.dynamicMethods;
}

所以其实就是返回了一个DynamicAddDependencyMethods去加以判断。那么毫无疑问要在这个类中进行判断和执行具体方法。接下来我们看看这个类中是怎么处理的。

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
27
28
29
30
31
32
33
34
35
DynamicAddDependencyMethods(ConfigurationContainer configurationContainer, DynamicAddDependencyMethods.DependencyAdder dependencyAdder) {
this.configurationContainer = configurationContainer;
this.dependencyAdder = dependencyAdder;
}

public boolean hasMethod(String name, Object... arguments) {
return arguments.length != 0 && this.configurationContainer.findByName(name) != null;
}

public DynamicInvokeResult tryInvokeMethod(String name, Object... arguments) {
if (arguments.length == 0) {
return DynamicInvokeResult.notFound();
} else {
Configuration configuration = (Configuration)this.configurationContainer.findByName(name);
if (configuration == null) {
return DynamicInvokeResult.notFound();
} else {
List<?> normalizedArgs = CollectionUtils.flattenCollections(arguments);
if (normalizedArgs.size() == 2 && normalizedArgs.get(1) instanceof Closure) {
return DynamicInvokeResult.found(this.dependencyAdder.add(configuration, normalizedArgs.get(0), (Closure)normalizedArgs.get(1)));
} else if (normalizedArgs.size() == 1) {
return DynamicInvokeResult.found(this.dependencyAdder.add(configuration, normalizedArgs.get(0), (Closure)null));
} else {
Iterator var5 = normalizedArgs.iterator();

while(var5.hasNext()) {
Object arg = var5.next();
this.dependencyAdder.add(configuration, arg, (Closure)null);
}

return DynamicInvokeResult.found();
}
}
}
}

可以看到这个类的两个要点:
1.判断Configuration有无:通过外部传入的ConfigurationContainer来判断是否存在这个方法。这样我们可以联想到,这个ConfigurationContainer肯定是每个平台Plugin自己传入的,必须是已定义的才能使用。比如android就添加了implementation, api等。如果你想查看Configuration在gradle源码中的初始化和配置,可以查看VariantDependencies这个类。

2.执行方法:真正的执行方法会根据参数来判断,比如我们常见的一个参数的引用形式,还有一个参数+一个闭包的形式,比如

1
2
3
compile('com.zhyea:ar4j:1.0') {
exclude module: 'cglib' //by artifact name
}

这种类型的引用。当在ConfigurationContainer中找到了这个引用方式(以下都称Configuration)时,就会返回一个DynamicInvokeResult。具体这个类的作用我们后面再看,我们先看他们都做了一个

1
this.dependencyAdder.add(configuration, arg, (Closure)null);

的操作,这个操作是做了些什么呢,我们继续往下跟就会发现,其实还是调用了DefaultDependencyHandler的doAdd方法。

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
// DefaultDependencyHandler.java
private class DirectDependencyAdder implements DependencyAdder<Dependency> {
private DirectDependencyAdder() {
}

public Dependency add(Configuration configuration, Object dependencyNotation, @Nullable Closure configureAction) {
return DefaultDependencyHandler.this.doAdd(configuration, dependencyNotation, configureAction);
}
}

private Dependency doAdd(Configuration configuration, Object dependencyNotation, Closure configureClosure) {
if (dependencyNotation instanceof Configuration) {
Configuration other = (Configuration)dependencyNotation;
if (!this.configurationContainer.contains(other)) {
throw new UnsupportedOperationException("Currently you can only declare dependencies on configurations from the same project.");
} else {
configuration.extendsFrom(new Configuration[]{other});
return null;
}
} else {
Dependency dependency = this.create(dependencyNotation, configureClosure);
configuration.getDependencies().add(dependency);
return dependency;
}
}

可以看到,这里会先判断dependencyNotation是否是Configuration,如果存在的话,就让当前的configuration继承dependencyNotation,也就是所有添加到dependencyNotation的依赖都会添加到configuration中。

这里可能有些朋友就会疑惑了,为啥还要对dependencyNotation判断呢?这个主要是为了处理嵌套的情况。比如implementation project(path: ‘:projectA’, configuration: ‘configA’)这种类型的引用。有兴趣可以看看上面的CollectionUtils.flattenCollections(arguments)方法。

总结一下,这个过程就是借助gradle的MethodMixIn接口,将所有未定义的引用方法转到getAdditionalMethods方法上来,在这个方法里面判断Configuration是否存在,如果存在的话就生成Dependency。

2.4 依赖的创建

可以看到上面过程的最后,是DefaultDependencyHandler调用了create方法创建出了一个Dependency。我们继续来分析创建Dependency的过程。

1
2
3
4
5
6
7
8
9
10
11
12
// DefaultDependencyHandler.java
public Dependency create(Object dependencyNotation, Closure configureClosure) {
Dependency dependency = this.dependencyFactory.createDependency(dependencyNotation);
return (Dependency)ConfigureUtil.configure(configureClosure, dependency);
}

// DefaultDependencyFactory.java
public Dependency createDependency(Object dependencyNotation) {
Dependency dependency = (Dependency)this.dependencyNotationParser.parseNotation(dependencyNotation);
this.injectServices(dependency);
return dependency;
}

可以看到最终是调用了dependencyNotationParser来parse这个dependencyNotation。而这里的dependencyNotationParser其实就是DependencyNotationParser这个类。

1
2
3
4
5
6
7
8
9
10
11
public class DependencyNotationParser {
public static NotationParser<Object, Dependency> parser(Instantiator instantiator, DefaultProjectDependencyFactory dependencyFactory, ClassPathRegistry classPathRegistry, FileLookup fileLookup, RuntimeShadedJarFactory runtimeShadedJarFactory, CurrentGradleInstallation currentGradleInstallation, Interner<String> stringInterner) {
return NotationParserBuilder.toType(Dependency.class)
.fromCharSequence(new DependencyStringNotationConverter(instantiator, DefaultExternalModuleDependency.class, stringInterner))
.converter(new DependencyMapNotationConverter(instantiator, DefaultExternalModuleDependency.class))
.fromType(FileCollection.class, new DependencyFilesNotationConverter(instantiator))
.fromType(Project.class, new DependencyProjectNotationConverter(dependencyFactory))
.fromType(ClassPathNotation.class, new DependencyClassPathNotationConverter(instantiator, classPathRegistry, fileLookup.getFileResolver(), runtimeShadedJarFactory, currentGradleInstallation))
.invalidNotationMessage("Comprehensive documentation on dependency notations is available in DSL reference for DependencyHandler type.").toComposite();
}
}

从里面我们看到了FileCollection,Project,ClassPathNotation三个类,是不是感觉和我们的三种三方库资源形式很对应?其实这三种资源形式的解析就是用这三个类进行的。DependencyNotationParser就是整合了这些转换器,成为一个综合的转换器。其中,

  • DependencyFilesNotationConverter将FileCollection解析为SelfResolvingDependency,也就是implementation fileTree(include: [‘*.jar’], dir: ‘libs’)这种形式。
  • DependencyProjectNotationConverter将Project解析为ProjectDependency。也就是implementation project(‘:projectA’)
  • DependencyClassPathNotationConverter将ClassPathNotation转成SelfResolvingDependency。也就是implementation ‘xxx’这种。

这三种方式具体的解析方法大家可以自行阅读源码,不是本文重点。所以除了Project会被解析为ProjectDependency以外,其他的都是SelfResolvingDependency。其实ProjectDependency是SelfResolvingDependency的子类。他们的关系可以从SelfResolvingDependency的代码注释中看出。

2.5 ProjectDependency

接下来讲讲ProjectDependency.一个常见的Project引用如下:

1
implementation project(‘:projectA’)

这里的implementation我们已经知道是插件添加的扩展,不是gradle自带的。那project呢?这个就是gradle自带的了。delegate是DependencyHandler的project方法。

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
27
// DefaultDependencyHandler.java
public Dependency project(Map<String, ?> notation) {
return this.dependencyFactory.createProjectDependencyFromMap(this.projectFinder, notation);
}

// DefaultDependencyFactory.java
public ProjectDependency createProjectDependencyFromMap(ProjectFinder projectFinder, Map<? extends String, ? extends Object> map) {
return this.projectDependencyFactory.createFromMap(projectFinder, map);
}

// ProjectDependencyFactory.java
public ProjectDependency createFromMap(ProjectFinder projectFinder, Map<? extends String, ?> map) {
return (ProjectDependency)NotationParserBuilder.toType(ProjectDependency.class).converter(new ProjectDependencyFactory.ProjectDependencyMapNotationConverter(projectFinder, this.factory)).toComposite().parseNotation(map);
}

// ProjectDependencyMapNotationConverter.java
protected ProjectDependency parseMap(@MapKey("path") String path, @Optional @MapKey("configuration") String configuration) {
return this.factory.create(this.projectFinder.getProject(path), configuration);
}

// DefaultProjectDependencyFactory.java
public ProjectDependency create(ProjectInternal project, String configuration) {
DefaultProjectDependency projectDependency = (DefaultProjectDependency)this.instantiator.newInstance(DefaultProjectDependency.class, new Object[]{project, configuration, this.projectAccessListener, this.buildProjectDependencies});
projectDependency.setAttributesFactory(this.attributesFactory);
projectDependency.setCapabilityNotationParser(this.capabilityNotationParser);
return projectDependency;
}

我们可以看到,传入的project最终传递给了ProjectDependencyMapNotationConverter。先去查找这个project,然后通过factory去create ProjectDependency对象,当然这里也有考虑到Configuration的影响, 最终是产生了一个DefaultProjectDependency。这就是ProjectDependency的产生过程。

2.6 依赖的体现

看到这里,大家可以已经理解了不同的依赖的解析方式,但是可能还是不理解依赖到底是一个什么东西。其实依赖库并不是依赖三方库的源代码,而是依赖三方库的产物,产物又是通过一系列的Task执行产生的。也就是说,projectA依赖projectB,那么A就拥有了对于B的产物的所有权。关于产物我们等后面再介绍。先了解一下Configuration对于产物有一些什么支持。
我们看Configuration的代码, 可以发现他继承了FileCollection接口,而FileCollection又继承了Buildable接口。这个接口有啥用呢,用处大得很。先看官网介绍

Buildable表示着很多Task对象生成的产物。它里面只有一个方法。

1
2
3
public interface Buildable {
TaskDependency getBuildDependencies();
}

我们看看DefaultProjectDependency的实现。

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
27
28
29
// DefaultProjectDependency.java
public TaskDependencyInternal getBuildDependencies() {
return new DefaultProjectDependency.TaskDependencyImpl();
}

private class TaskDependencyImpl extends AbstractTaskDependency {
private TaskDependencyImpl() {
}

public void visitDependencies(TaskDependencyResolveContext context) {
if (DefaultProjectDependency.this.buildProjectDependencies) {
DefaultProjectDependency.this.projectAccessListener.beforeResolvingProjectDependency(DefaultProjectDependency.this.dependencyProject);
Configuration configuration = DefaultProjectDependency.this.findProjectConfiguration();
context.add(configuration);
context.add(configuration.getAllArtifacts());
}
}
}

public Configuration findProjectConfiguration() {
ConfigurationContainer dependencyConfigurations = this.getDependencyProject().getConfigurations();
String declaredConfiguration = this.getTargetConfiguration();
Configuration selectedConfiguration = dependencyConfigurations.getByName((String)GUtil.elvis(declaredConfiguration, "default"));
if (!selectedConfiguration.isCanBeConsumed()) {
throw new ConfigurationNotConsumableException(this.dependencyProject.getDisplayName(), selectedConfiguration.getName());
} else {
return selectedConfiguration;
}
}

我们可以看到,其实就是在解析每个依赖的时候,如果指定了ConfigurationContainer中声明好的Configuration,比如implementation, api等,那就返回这个Configuration,否则就返回default。拿到这个Configuration之后,做了这个操作

1
2
context.add(configuration);
context.add(configuration.getAllArtifacts());

这里的context是一个TaskDependencyResolveContext,它的add方法可以添加能contribute tasks to the result的对象,比如Task,TaskDependencies,Buildable等,这些类型都能为产生结果贡献Task(前面我们说到了就是靠Task产生产物的嘛)。

这里context把configuration和configuration.getAllArtifacts()加入,都是作为Buildable而加入。区别是configuration.getAllArtifacts()获取的是DefaultPublishArtifactSet对象。接下来看看DefaultPublishArtifactSet是怎么实现Buildable的方法的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public TaskDependency getBuildDependencies() {
return this.builtBy; // 这里的builtBy是下面的ArtifactsTaskDependency对象
}

private class ArtifactsTaskDependency extends AbstractTaskDependency {
private ArtifactsTaskDependency() {
}

public void visitDependencies(TaskDependencyResolveContext context) {
Iterator var2 = DefaultPublishArtifactSet.this.iterator();

while(var2.hasNext()) {
PublishArtifact publishArtifact = (PublishArtifact)var2.next();
context.add(publishArtifact);
}

}
}

我们发现果然和DefaultPublishArtifactSet的名字一样,是作为set把里面包含的PublishArtifact对象逐个的放入context中。

2.7 总结

我们在这一小节中分析了不同的依赖方式的区别,告诉了大家Configuration是什么东西,也告诉了大家依赖到底是怎么产生和起作用的。这样大家在日常开发中就更能知其所以然了。总结一下就是说

  • implementation等都是通过methodmissing机制,被插件解析成不同的Configuration,所以要预定义。
  • 我们不同的依赖声明,将会被不同的转化器Parser进行转换。project依赖会被转换为ProjectDependency,其余的会被解析成可自解析的SelfResolvingDependencies。
  • project依赖最终是Task和产物artifact的依赖。

那么Task和产物又是一种什么关系呢?这个就涉及到更深层次了。本文篇幅有限,放在下一篇再分析吧。

总结

本文主要是从Extension和Plugin引入,主要讲解了gradle依赖的原理和解析机制,然后抛下了一个疑问:产物artifacts是怎么和依赖扯上关系的呢?这个问题我们等下一篇《一篇文章深入gradle(下篇):Artifacts》再解答

一篇文章基本看懂gradle

Posted on 2019-08-30

一篇文章基本看懂gradle

Hi,大家好啊。笨鸟之旅已经很久都没有更新了,感谢大家这么久以来还把我留在列表里。这么久以来不更新的原因主要是我本人进入了一个迷茫期,对于工作和生活都提不起兴致来。加上每天工作的时间太长,一回家就瘫在床上了。这也导致了很久之前的文章计划一直都搁浅,做的计划一次一次的delay。于是生活进入了一个负循环。但是生活还是要向上看,还是要坚持去努力做点事情,努力去变得更好,所以我又来了。以后一定会努力的学习,努力的发文。

这段时间来学习了gradle,也体会到了gradle从初步理解到基本熟悉,再到深入源码这样一个过程中的一些曲折。这篇文章主要是gradle的基础知识篇。看完这篇文章,你可以:

  • 清楚gradle的定义和解决的痛点
  • 基本理解Android gradle的运作机制
  • 基本理解gradle的大部分语法
  • 学会基本的groovy开发

如果你想关注gradle更深入的一些知识,请继续关注后续gradle文章。

what is gradle?

先来看一段维基百科上对于gradle的解释。

Gradle是一个基于Apache Ant和Apache Maven概念的项目自动化构建工具。它使用一种基于Groovy的特定领域语言来声明项目设置,而不是传统的XML。当前其支持的语言限于Java、Groovy和Scala,计划未来将支持更多的语言。

可能刚接触gradle的同学都不是很了解gradle的这个定义。可能就只会跟着网上的教程copy一点配置,但是不理解这些配置背后的原理。那么怎么来理解这句话呢,我们可以把握到三个要点:首先,它是一种构建工具,其次,gradle是基于maven概念的,最后,使用groovy这种语言来声明。要理解这几句话,我们先考虑几个场景。

1.渠道管理:国内手机市场有大大小小数十个,大的手机厂商也有五六个,每个厂商可能又有不同的定制rom。如果我们要为不同市场和厂商进行适配,那就需要写这样的代码

1
2
3
4
5
if(isHuawei) {
// dosomething
} else if(isOppo) {
// dosomething
}

这样的话,繁琐不说,对单个手机而言大量的无用代码被编译进apk中,包体积和运行速度都会受影响。为了解决这个问题,gradle引进了productFlavor和buildType的能力,能根据情况来进行打包。所以说他是一个自动化构建工具。可以看官方文档

2.依赖管理:我们通常会在项目中引入各种三方库进行代码复用。比如,直接手动把jar或者aar copy到项目中,然后添加依赖。这种方法缺陷很明显,首先配置和删除流程很繁琐,其次,同一个jar可能会被多个项目所引用,导致不知不觉就copy了多个jar。最后,版本管理艰难。为了解决这个问题,gradle是基于maven仓库,配置和删除的时候仅需要对仓库的坐标进行操作,所有的库都会被gradle统一管理,大多数情况下每个库只会有一个版本存在于项目中,并且每个库只会有一个副本存在于项目中。

所以gradle其实不是什么神秘的东西,只是基于某种语言(groovy, java, kotlin)的一种构建工具而已。只要我们大概掌握了基本的用法和他的内部原理,日常工作中就会知道自己网上搜到的命令是什么意思啦。skr~

小试牛刀-android中的gradle

咱们先看看日常工作中经常用到的几个gradle文件。可以看到主要有有三个文件:
1.build.gradle
根文件下放的通常放的是针对整个工程的通用配置,每个module下面的build.gradle文件是针对每个module自身的配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
buildscript {
ext.kotlin_version = '1.2.71'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.2.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {
repositories {
google()
jcenter()
}
}

这是一个默认的配置,我们可以看到有buildscript,allprojects,repositories,dependencies几个配置项,这些配置项是干嘛的呢,很多的同学在刚学gradle的时候都是一脸懵逼的。这些其实是gradle的一种特定的语法,我们称之为DSL(domain-specific language)。可以参考官网。这里可以看到allprojects代理的是每个project,可以理解成我们的每个module,也就是对我们所写的每个module的配置。buildscript主要配置的是打包相关的东西,比如gradle版本,gradle插件版本等,这些都是针对构建工具自己的配置。repositories,dependencies是三方库的仓库和坐标。所以根目录的build.gradle相当于是整体的配置。

而module下的build.gradle主要是android,dependencies等配置项。

1
2
3
4
5
6
7
8
apply plugin: 'com.android.application'

android{
...
}
dependencies{
...
}

可能有些同学会感到奇怪,为啥我们在官网没有看到android这个配置项呢?这个主要是因为它并不是gradle的DSL,某种意义上说应该算是android特有的,是通过Android的插件’com.android.application’带进来的配置项。我们如果把第一行删掉,就会发现android{}这个配置项找不到了。

所以,我们可以发现,build.gradle里面的配置项,要么是gradle自带的,要么是各种插件定义的。有不认识的配置项,就去官网查询一下就好了,授人以鱼不如授人以渔嘛。我们后面也会讲解到引进插件的方式和怎么定义插件和配置项。

2.settings.gradle
这个文件主要是决定每个module是否参与构建。我们可以这样去理解,settings.gradle相当于是每个module的开关,关上了这个module就不能使用了,别的依赖到它的module也都会出问题。

3.gradle.properties
这里主要是增加和修改一些可以在构建过程中直接使用的参数。不只是可以添加自定义参数,还可以修改系统的参数哦~

总结一下,就是说根目录下有一个build.gradle,处理整个工程的配置项,根目录下的settings.gradle配置整个工程中参与构建的module,每个module自己有一个build.gradle,处理自己模块的配置。这就是android构建的一个大概情况。当然,看了这一部分肯定还是不懂怎么去写的,接下来我们走进代码层面。

groovy-学gradle的密钥

gradle可以使用groovy,kotlin,java等语言进行书写,但是groovy相对来说是目前比较流行的gradle配置方式,下面我们讲解一点groovy基础。不讲太多,够用就行。

1.字符串

groovy的字符串分为两种java.lang.String和groovy.lang.GString。其中单引号和三引号是String类型的。双引号是GString类型的。支持占位插值操作。和kotlin一样,groovy的插值操作也是用${}或者$来标示,${}用于一般替代字串或者表达式,$主要用于A.B的形式中。

1
2
3
4
5
6
7
8
9
10
def number = 1 
def eagerGString = "value == ${number}"
def lazyGString = "value == ${ -> number }"

println eagerGString
println lazyGString

number = 2
println eagerGString
println lazyGString

2.字符Character

Groovy没有明确的Character。但是可以强行声明。

1
2
3
4
5
6
7
8
char c1 = 'A' 
assert c1 instanceof Character

def c2 = 'B' as char
assert c2 instanceof Character

def c3 = (char)'C'
assert c3 instanceof Character

4.List

Groovy的列表和python的很像。支持动态扩展,支持放置多种数据。使用方法支持def和直接定义。还可以像python那样索引

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
27
28
29
30
//List中存储任意类型
def heterogeneous = [1, "a", true]

//判断List默认类型
def arrayList = [1, 2, 3]
assert arrayList instanceof java.util.ArrayList

//使用as强转类型
def linkedList = [2, 3, 4] as LinkedList
assert linkedList instanceof java.util.LinkedList

//定义指定类型List
LinkedList otherLinked = [3, 4, 5]
assert otherLinked instanceof java.util.LinkedList

// 像python一样索引
assert letters[1] == 'b'
//负数下标则从右向左index
assert letters[-1] == 'd'
assert letters[-2] == 'c'
//指定item赋值判断
letters[2] = 'C'
assert letters[2] == 'C'
//给List追加item
letters << 'e'
assert letters[ 4] == 'e'
assert letters[-1] == 'e'
//获取一段List子集
assert letters[1, 3] == ['b', 'd']
assert letters[2..4] == ['C', 'd', 'e']

5.Map

1
2
3
4
5
//定义一个Map
def colors = [red: '#FF0000', green: '#00FF00', blue: '#0000FF']
//获取一些指定key的value进行判断操作
assert colors['red'] == '#FF0000'
assert colors.green == '#00FF00'

6.运算符

  • **: 次方运算符。
  • ?.:安全占位符。和kotlin一样避免空指针异常。
  • .@:直接域访问操作符。因为Groovy自动支持属性getter方法,但有时候我们有一个自己写的特殊getter方法,当不想调用这个特殊的getter方法则可以用直接域访问操作符。这点跟kotlin的
  • .&:方法指针操作符,因为闭包可以被作为一个方法的参数,如果想让一个方法作为另一个方法的参数则可以将一个方法当成一个闭包作为另一个方法的参数。
  • ?::二目运算符。与kotlin中的类似。
  • *.展开运算符,一个集合使用展开运算符可以得到一个元素为原集合各个元素执行后面指定方法所得值的集合。
1
2
3
4
5
6
cars = [
new Car(make: 'Peugeot', model: '508'),
null,
new Car(make: 'Renault', model: 'Clio')]
assert cars*.make == ['Peugeot', null, 'Renault']
assert null*.make == null

7.闭包
groovy里比较重要的是闭包的概念。官方定义是“Groovy中的闭包是一个开放,匿名的代码块,可以接受参数,返回值并分配给变量”。
其实闭包跟kotlin的lambda函数很像,都是先定义后执行。但是又有一些细微的区别。接下来我们细讲讲gradle的闭包。

闭包是可以用作方法参数的代码块,Groovy的闭包更象是一个代码块或者方法指针,代码在某处被定义然后在其后的调用处执行。一个闭包实际上就是一个Closure类型的实例。写法和kotlin的lambda函数很像。

我们常见的闭包是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//最基本的闭包
{ item++ }
//使用->将参数与代码分离
{item -> item++ }
//使用隐含参数it
{ println it }
//使用显示的名为参数
{ name -> println name }

// 调用方法
a.call()
a()

// Groovy的闭包支持最后一个参数为不定长可变长度的参数。
def multiConcat = { int n, String... args ->
args.join('')*n
}

大家要注意,如果我们单纯的只是写成 a = { item++ }, 这只是定义了一个闭包,是不能运行的。必须调用a.call()才能运行出来。所以大家可以理解了,闭包就是一段代码块而已。当我们有需要的时候,可以去运行它,这么一想是不是和lambda函数很像?

如果你看了官网,你会发现有一些这样的说法,

什么叫做delegate?这里涉及到闭包内部的三种对象。

  • this 对应于定义闭包的那个类,如果在内部类中定义,指向的是内部类
  • owenr 对应于定义闭包的那个类或者闭包,如果在闭包中定义,对应闭包,否则同this一致
  • delegate 默认是和owner一致,或者自定义delegate指向

this和owner都比较好理解。我们可以用闭包的getxxx方法获取

1
2
3
def thisObject = closure.getThisObject()
def ownerObject = closure.getOwner()
def delegate = closure.getDelegate()

重头戏还是delegate这个对象。闭包可以设置delegate对象,设置delegate的意义就是将闭包和一个具体的对象关联起来。
我们先来看个例子,这里以自定义android闭包为例。

1
2
3
4
5
6
7
8
9
10
11
android {
compileSdkVersion 25
buildToolsVersion "25.0.2"

defaultConfig {
minSdkVersion 15
targetSdkVersion 25
versionCode 1
versionName "1.0"
}
}

这个闭包对应的实体类是两个。

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# Android.groovy
class Android {
private int mCompileSdkVersion
private String mBuildToolsVersion
private ProductFlavor mProductFlavor

Android() {
this.mProductFlavor = new ProductFlavor()
}

void compileSdkVersion(int compileSdkVersion) {
this.mCompileSdkVersion = compileSdkVersion
}

void buildToolsVersion(String buildToolsVersion) {
this.mBuildToolsVersion = buildToolsVersion
}

void defaultConfig(Closure closure) {
closure.setDelegate(mProductFlavor)
closure.setResolveStrategy(Closure.DELEGATE_FIRST)
closure.call()
}

@Override
String toString() {
return "Android{" +
"mCompileSdkVersion=" + mCompileSdkVersion +
", mBuildToolsVersion='" + mBuildToolsVersion + '\'' +
", mProductFlavor=" + mProductFlavor +
'}'
}
}

# ProductFlavor.groovy
class ProductFlavor {
private int mVersionCode
private String mVersionName
private int mMinSdkVersion
private int mTargetSdkVersion

def versionCode(int versionCode) {
mVersionCode = versionCode
}

def versionName(String versionName) {
mVersionName = versionName
}

def minSdkVersion(int minSdkVersion) {
mMinSdkVersion = minSdkVersion
}


def targetSdkVersion(int targetSdkVersion) {
mTargetSdkVersion = targetSdkVersion
}

@Override
String toString() {
return "ProductFlavor{" +
"mVersionCode=" + mVersionCode +
", mVersionName='" + mVersionName + '\'' +
", mMinSdkVersion=" + mMinSdkVersion +
", mTargetSdkVersion=" + mTargetSdkVersion +
'}'
}
}

然后定义的时候就写成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//闭包定义
def android = {
compileSdkVersion 25
buildToolsVersion "25.0.2"
defaultConfig {
minSdkVersion 15
targetSdkVersion 25
versionCode 1
versionName "1.0"
}
}

//调用
Android bean = new Android()
android.delegate = bean
android.call()
println bean.toString()

//打印结果
Android{mCompileSdkVersion=25, mBuildToolsVersion='25.0.2', mProductFlavor=ProductFlavor{mVersionCode=1, mVersionName='1.0', mMinSdkVersion=15, mTargetSdkVersion=25}}

这样就能将闭包中声明的值,赋给两个对象Android和ProductFlavor来处理了。

上面官网的图里,说ScriptHandler被设置成buildscript的delegate。意思就是buildscript定义的参数被ScriptHandler拿来使用了。大家有兴趣的可以去看看ScriptHandler的源码~

Project与Task-gradle构建体系

上面我们讲完了基本的用法,大家可能懂gradle的配置和写法了。但是可能还是不懂gradle的构建体系到底是怎么样的。这里我们就要深入进gradle的构建体系Project和Task了。下面的东西看着就要动动脑筋了。

1.Task
Task是gradle脚本中的最小可执行单元。类图如下:

值得注意的是因为Gradle构建脚本默认的名字是build.gradle,当在shell中执行gradle命令时,Gradle会去当前目录下寻找名字是build.gradle的文件。所以只有定义在build.gradle中的Task才是有效的。

可以通过三种方式来声明task。我们可以根据自己的项目需要去定义Task。比如自定义task接管gradle的编译过程

1
2
3
4
5
6
7
8
9
10
11
12
13
task myTask2 << {
println "doLast in task2"
}

//采用 Project.task(String name) 方法来创建
project.task("myTask3").doLast {
println "doLast in task3"
}

//采用 TaskContainer.create(String name) 方法来创建
project.tasks.create("myTask4").doLast {
println "doLast in task4"
}

TaskContianer 是用来管理所有的 Task 实例集合的,可以通过 Project.getTasks() 来获取 TaskContainer 实例。
常见接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
findByPath(path: String): Task
getByPath(path: String): Task
getByName(name: String): Task
withType(type: Class): TaskCollection
matching(condition: Closure): TaskCollection

//创建task
create(name: String): Task
create(name: String, configure: Closure): Task
create(name: String, type: Class): Task
create(options: Map<String, ?>): Task
create(options: Map<String, ?>, configure: Closure): Task

//当task被加入到TaskContainer时的监听
whenTaskAdded(action: Closure)

Gradle支持增量编译。了解过编译profile文件的朋友都知道,里面有大量的task都是up-to-date。那么这种up-to-date是什么意思呢。Gradle的Task会把每次运行的结果缓存下来,当下次运行时,会检查一个task的输入输出有没有变更。如果没有变更就是up-to-date,跳过编译。

2.Project
先从Project对象讲起,Project是与Gradle交互的主接口。android开发中最为我们所熟悉的就是build.gradle文件,这个文件与Project是一对一的关系,build.gradle文件是project对象的委托,脚本中的配置都是对应着Project的Api。Gradle构建进程启动的时候会根据build.gradle去实例化Project类。也就是说,构建的时候,每个build.gradle文件会生成一个Project对象,这个对象负责当前module的构建。

Project本质上是包含多个Task的容器,所有的Task存在TaskContainer中。我们从名字可以看出

可以看到dependencies, configuration, allprojects, subprojects, beforeEvaluate, afterEvaluate这些都是我们常见的配置项,在build.gradle文件中接收一个闭包Closure。

好了,现在我们已经聊了build.gradle了,但是大家都知道,我们项目中还有一个settings.gradle呢,这个是拿来干嘛的呢?这就要说到Project的Lifecycle了,也就是Gradle构建Project的步骤,看官网原文:

  • Create a Settings instance for the build.
  • Evaluate the settings.gradle script, if present, against the Settings object to configure it.
  • Use the configured Settings object to create the hierarchy of Project instances.
  • Finally, evaluate each Project by executing its build.gradle file, if present, against the project. The projects are evaluated in breadth-wise order(宽度搜索), such that a project is evaluated before its child projects. This order can be overridden by calling Project.evaluationDependsOnChildren() or by adding an explicit evaluation dependency using Project.evaluationDependsOn(java.lang.String).

也就是说,Project对象依赖Settings对象的构建。我们常在settings.gradle文件中配置需要引入的module,就是这个原因。

3.Property
看完了build.gradle和settings.gradle,接下来我们讲讲gradle.properties。这个文件存放的键值对形式的属性,这些属性能被项目中的gradle脚本使用ext.xxx所访问。

我们也可以使用Properties类来动态创建属性文件。如:

1
2
3
def defaultProps = new Properties()
defaultProps.setProperty("debuggable", 'true')
defaultProps.setProperty("groupId", GROUP)

并且属性可以继承,在一个项目中定义的属性可以自动被子项目继承。所以在哪个子项目都可以使用project.ext.xxx访问。不同子项目间采用通用的配置插件来配置

1
apply from: rootProject.file('library.gradle')

总结

通过上面的学习,大家应该已经了解了gradle的基本配置,写法和比较浅显的内部原理了。因为篇幅原因,深入的内容我们放在下一篇。敬请期待《一篇文章深入gradle》

我是Android笨鸟之旅,一个陪着你慢慢变强的公众号。

参考:
官网
[Android Gradle] 搞定Groovy闭包这一篇就够了

C++自学笔记

Posted on 2019-01-04

C++自学笔记

一. 基础

1.变量类型:在C的基础上添加了wchar_t。

2.变量声明:变量和函数声明只在编译时有意义,在程序连接时编译器需要实际的变量声明也就是变量定义。并且声明可以多次,但是变量定义只能一次。

extern修饰变量:用于提供一个全局变量的引用,全局变量对所有的程序文件都是可见的。当您有多个文件且定义了一个可以在其他文件中使用的全局变量或函数时,可以在其他文件中使用 extern 来得到已定义的变量或函数的引用。可以这么理解,extern 是用来在另一个文件中声明一个全局变量或函数。extern 修饰符通常用于当有两个或多个文件共享相同的全局变量或函数的时候

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// a.cpp
#include <iostream>
int count ;
extern void write_extern();
int main()
{
count = 5;
write_extern();
}

// b.cpp
#include <iostream>
extern int count;
void write_extern(void)
{
std::cout << "Count is " << count << std::endl;
}

static:存储类指示编译器在程序的生命周期内保持局部变量的存在,而不需要在每次它进入和离开作用域时进行创建和销毁。因此,使用 static 修饰局部变量可以在函数调用之间保持局部变量的值。static 修饰符也可以应用于全局变量。当 static 修饰全局变量时,会使变量的作用域限制在声明它的文件内。

3.类型限定符:

const const 类型的对象在程序执行期间不能被修改改变。
volatile 修饰符 volatile 告诉编译器不需要优化volatile声明的变量,让程序可以直接从内存中读取变量。对于一般的变量编译器会对变量进行优化,将内存中的变量值放在寄存器中以加快读写效率。
restrict 由 restrict 修饰的指针是唯一一种访问它所指向的对象的方式。只有 C99 增加了新的类型限定符 restrict。

4.函数

默认函数:您可以为参数列表中后边的每一个参数指定默认值。当调用函数时,如果实际参数的值留空,则使用这个默认值。注意:这里的默认参数必须放在普通参数之后

1
2
3
4
5
6
int sum(int a, int b=20)
{
int result;
result = a + b;
return (result);
}

函数的三种传值方式:

  • 传值调用:把参数的实际值复制给函数的形式参数。在这种情况下,修改函数内的形式参数对实际参数没有影响。
  • 指针调用把参数的地址复制给形式参数。在函数内,该地址用于访问调用中要用到的实际参数。这意味着,修改形式参数会影响实际参数。
  • 引用调用:把参数的引用复制给形式参数。在函数内,该引用用于访问调用中要用到的实际参数。这意味着,修改形式参数会影响实际参数。

5.字符串

C风格: char a[10] = “hello”;

C++风格: string a = “s d” + “ ds”;

二. 指针与数据结构

1.指针的声明,赋值和取值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int main ()
{
int var = 20; // 实际变量的声明
int *ip; // 指针变量的声明
ip = &var; // 在指针变量中存储 var 的地址
cout << "Value of var variable: ";
cout << var << endl;

// 输出在指针变量中存储的地址
cout << "Address stored in ip variable: ";
cout << ip << endl;

// 访问指针中地址的值
cout << "Value of *ip variable: ";
cout << *ip << endl;
return 0;
}

在变量声明的时候,如果没有确切的地址可以赋值,为指针变量赋一个 NULL 值是一个良好的编程习惯。

2.在函数参数列表中的两种表示方法:

1
2
3
void swap(int &x, int &y)  // 引用传值,这里&x表示的是传入的参数是一个值,可以直接使用,或者用&取出这个参数的指针

void swap_2(int *x, int *y) // 指针传值,这里*x代表传入的参数是一个指针,可以直接使用,或者用*取出这个参数的值

3.指针可以进行数学比较,毕竟都是指针。

4.指针数组:

1
2
int *ptr[MAX]  // 指针数组
int (*ptr)[MAX] // 数组指针

5.函数中返回指针

1
int * function()

引用

引用很容易与指针混淆,它们之间有三个主要的不同:

  • 不存在空引用。引用必须连接到一块合法的内存。
  • 一旦引用被初始化为一个对象,就不能被指向到另一个对象。指针可以在任何时候指向到另一个对象。
  • 引用必须在创建时被初始化。指针可以在任何时间被初始化。

结构

结构变量用.来引出结构中的成员,结构变量指针用->来引出。

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
using namespace std;
void printBook( struct Books *book );

struct Books
{
char title[50];
char author[50];
char subject[100];
int book_id;
};

int main( )
{
Books Book1; // 定义结构体类型 Books 的变量 Book1
Books Book2; // 定义结构体类型 Books 的变量 Book2

// Book1 详述
strcpy( Book1.title, "C++ 教程");
strcpy( Book1.author, "Runoob");
strcpy( Book1.subject, "编程语言");
Book1.book_id = 12345;

// Book2 详述
strcpy( Book2.title, "CSS 教程");
strcpy( Book2.author, "Runoob");
strcpy( Book2.subject, "前端技术");
Book2.book_id = 12346;

// 通过传 Book1 的地址来输出 Book1 信息
printBook( &Book1 );

// 通过传 Book2 的地址来输出 Book2 信息
printBook( &Book2 );

return 0;
}
// 该函数以结构指针作为参数
void printBook( struct Books *book )
{
cout << "书标题 : " << book->title <<endl;
cout << "书作者 : " << book->author <<endl;
cout << "书类目 : " << book->subject <<endl;
cout << "书 ID : " << book->book_id <<endl;
}

三. 类和对象

1.继承:C++类定义及使用与Java十分相似,但是引入了继承方式的区别, 规则是:

public 继承:基类 public 成员,protected 成员,private 成员的访问属性在派生类中分别变成:public, protected, private

protected 继承:基类 public 成员,protected 成员,private 成员的访问属性在派生类中分别变成:protected, protected, private

private 继承:基类 public 成员,protected 成员,private 成员的访问属性在派生类中分别变成:private, private, private

看个例子

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include<iostream>
#include<assert.h>
using namespace std;
class A{
public:
int a;
A(){
a1 = 1;
a2 = 2;
a3 = 3;
a = 4;
}
void fun(){
cout << a << endl; //正确
cout << a1 << endl; //正确
cout << a2 << endl; //正确
cout << a3 << endl; //正确
}
public:
int a1;
protected:
int a2;
private:
int a3;
};
class B : protected A{
public:
int a;
B(int i){
A();
a = i;
}
void fun(){
cout << a << endl; //正确,public成员。
cout << a1 << endl; //正确,基类的public成员,在派生类中变成了protected,可以被派生类访问。
cout << a2 << endl; //正确,基类的protected成员,在派生类中还是protected,可以被派生类访问。
cout << a3 << endl; //错误,基类的private成员不能被派生类访问。
}
};
int main(){
B b(10);
cout << b.a << endl; //正确。public成员
cout << b.a1 << endl; //错误,protected成员不能在类外访问。
cout << b.a2 << endl; //错误,protected成员不能在类外访问。
cout << b.a3 << endl; //错误,private成员不能在类外访问。
system("pause");
return 0;
}

类中变量和函数声明也要使用这三个修饰符。并且默认是private。

2.C++同时引入了析构函数,用于在删除对象的时候运行。C++类会自动控制类的空间的申请和释放。所以析构函数就是在这个时候调用。

1
2
Line::Line(double len);
~Line:Line();

3.拷贝构造函数

拷贝构造函数是一种特殊的构造函数,它在创建对象时,是使用同一类中之前创建的对象来初始化新创建的对象。拷贝构造函数通常用于:

  • 通过使用另一个同类型的对象来初始化新创建的对象。
  • 复制对象把它作为参数传递给函数。
  • 复制对象,并从函数返回这个对象。

如果在类中没有定义拷贝构造函数,编译器会自行定义一个。如果类带有指针变量,并有动态内存分配,则它必须有一个拷贝构造函数。拷贝构造函数的最常见形式如下:

1
2
classname (const classname &obj) {    // 构造函数的主体 
}

比如接下来的例子给出了三者组合的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 成员函数定义,包括构造函数
Line::Line(int len)
{
cout << "调用构造函数" << endl;
// 为指针分配内存
ptr = new int;
*ptr = len;
}

Line::Line(const Line &obj)
{
cout << "调用拷贝构造函数并为指针 ptr 分配内存" << endl;
ptr = new int;
*ptr = *obj.ptr; // 拷贝值
}

Line::~Line(void)
{
cout << "释放内存" << endl;
delete ptr;
}

4.指向类的指针需要使用成员访问运算符->,就像指向结构的指针一样使用。

5.类的静态成员数据:

与Java不同的地方是不能把静态成员在类的所有对象中是共享的。如果不存在其他的初始化语句,在创建第一个对象时,所有的静态数据都会被初始化为零。我们不能把静态成员的初始化放置在类的定义中,但是可以在类的外部通过使用范围解析运算符 :: 来重新声明静态变量从而对它进行初始化,函数也是如此:

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <iostream>

using namespace std;

class Box
{
public:
static int objectCount;
// 构造函数定义
Box(double l=2.0, double b=2.0, double h=2.0)
{
cout <<"Constructor called." << endl;
length = l;
breadth = b;
height = h;
// 每次创建对象时增加 1
objectCount++;
}
double Volume()
{
return length * breadth * height;
}
static int getCount()
{
return objectCount;
}
private:
double length; // 长度
double breadth; // 宽度
double height; // 高度
};

// 初始化类 Box 的静态成员
int Box::objectCount = 0;

int main(void)
{

// 在创建对象之前输出对象的总数
cout << "Inital Stage Count: " << Box::getCount() << endl;

Box Box1(3.3, 1.2, 1.5); // 声明 box1
Box Box2(8.5, 6.0, 2.0); // 声明 box2

// 在创建对象之后输出对象的总数
cout << "Final Stage Count: " << Box::getCount() << endl;

return 0;
}

6.运算符重载

重载的运算符是带有特殊名称的函数,函数名是由关键字 operator 和其后要重载的运算符符号构成的。与其他函数一样,重载运算符有一个返回类型和一个参数列表。

1
Box operator+(const Box&);

7.虚函数

虚函数 是在基类中使用关键字 virtual 声明的函数。在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数。也就是说,虚函数的调用不是在编译时刻确定的,而是在运行时刻确定的

我们想要的是在程序中任意点可以根据所调用的对象类型来选择调用的函数,这种操作被称为动态链接,或后期绑定。

虚函数有点类似于Java中的可以重写的函数。Java之中可以重写的函数都是运行时候确定的,

如果不想给一个虚函数给出实现,就定为纯需函数。纯虚函数有点像接口的函数

1
virtual double getVolume() = 0;

8.接口和抽象类

如果类中至少有一个函数被声明为纯虚函数,则这个类就是抽象类。纯虚函数是通过在声明中使用 “= 0” 来指定的。

四.高级

1.文件操作

文件操作头文件

  • ofstream:输出文件流
  • ifstream:输入文件流
  • fstream:文件流,拥有输入输出的功能

2.在从文件读取信息或者向文件写入信息之前,必须先打开文件。ofstream 和 fstream 对象都可以用来打开文件进行写操作,如果只需要打开文件进行读操作,则使用 ifstream 对象。

下面是 open() 函数的标准语法,open() 函数是 fstream、ifstream 和 ofstream 对象的一个成员。

1
void open(const char *filename, ios::openmode mode);

这里的openmode指明了打开的模式

模式标志 描述
ios::app 追加模式。所有写入都追加到文件末尾。
ios::ate 文件打开后定位到文件末尾。
ios::in 打开文件用于读取。
ios::out 打开文件用于写入。
ios::trunc 如果该文件已经存在,其内容将在打开文件之前被截断,即把文件长度设为 0。

打开以后使用 >> 和 << 进行数据的写入与读出。与cout cin类似。

2. C++动态内存

栈:在函数内部声明的所有变量都将占用栈内存。

堆:这是程序中未使用的内存,在程序运行时可用于动态分配内存。

通常使用new和delete运算符来申请和删除内存。

如:

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
using namespace std;

int main ()
{
double* pvalue = NULL; // 初始化为 null 的指针
pvalue = new double; // 为变量请求内存
*pvalue = 29494.99; // 在分配的地址存储值
cout << "Value of pvalue : " << *pvalue << endl;
delete pvalue; // 释放内存
return 0;
}

3.模板

C++模板和Java模板语法很像。

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
27
28
29
30
31
// 方法模板
template <typename T>
int compare(const T &left, const T &right)
{
if (left < right)
{
return -1;
}
if (right < left)
{
return 1;
}
return 0;
}

// 类模板
template <class T>
class Stack
{
private:
vector<T> elems; // 元素

public:
void push(T const &); // 入栈
void pop(); // 出栈
T top() const; // 返回栈顶元素
bool empty() const
{ // 如果为空则返回真。
return elems.empty();
}
};

4.线程

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
27
28
29
30
31
32
33
34
#include <iostream>
#include <cstdlib>
#include <pthread.h>

using namespace std;

#define NUM_THREADS 5

void *PrintHello(void *threadid)
{
// 对传入的参数进行强制类型转换,由无类型指针变为整形数指针,然后再读取
int tid = *((int*)threadid);
cout << "Hello Runoob! 线程 ID, " << tid << endl;
pthread_exit(NULL);
}

int main ()
{
pthread_t threads[NUM_THREADS];
int indexes[NUM_THREADS];// 用数组来保存i的值
int rc;
int i;
for( i=0; i < NUM_THREADS; i++ ){
cout << "main() : 创建线程, " << i << endl;
indexes[i] = i; //先保存i的值
// 传入的时候必须强制转换为void* 类型,即无类型指针
rc = pthread_create(&threads[i], NULL,
PrintHello, (void *)&(indexes[i]));
if (rc){
cout << "Error:无法创建线程," << rc << endl;
}
}
pthread_exit(NULL);
}

也谈谈我的2018

Posted on 2019-01-02 | In 年度总结

也谈谈我的2018

年轻的时候喜欢写一些文章,喜欢铺垫一些比较煽情的气氛,喜欢总结一些看似很有道理的经验。越到年纪大了,越觉得生活就是很平平淡淡的事。尤其是进入社会以来,除去工作以外生活所给的反馈越来越少。更让我加深了这种感慨。有时候就容易感叹:生活真是索然无趣啊。

于是就想着求变。想着2018年没有做好的事情,2019年要用更大的激情和努力来完成,哪怕并没有达到自己年初的预期,想来也总算有个盼头。就像小的时候盼着过年,读书的时候盼着高考一样,进入社会了总不能盼着早点退休吧。咳咳。

我以为回顾起我的2018,会像某些朋友一样:这是xxxx的一年,也是xxxx的一年。但是对我来说,2018年过的有点太快了。快到我没有好好体会就结束了。毕业论文,答辩,适应工作。我现在只能想起这几个词。毕业论文选择了做一点偏研究性的工作,想体会一下真正的科研人员们的快乐,提升一下自己的研究能力,然后那段时间就是疯狂的看论文,想idea。结果终究还是有点准备不足,就算花了很多的时间还是没有达到比较好的效果。但是也推翻了自己对自己的一种固有观念:不喜欢搞科研。人生还是有无限可能的。不想自己给自己设限。从此以后,应该会去追求更多的可能性和多样性吧。

临到毕业的时候想着出国去做做义工旅行,困于囊中羞涩打消了计划。其实内心里还是很想去的。想去老挝的琅勃拉邦,过一段时间日出而作日落而息的生活。远离手机和电脑,多点对自己人生的思考。这一点其实也是对自己大学的不满。感觉大学期间的自己浑浑噩噩,很多事情都没有做到过自己能力范围内的最好,与自己的预期相差甚远。加权没有进入前十,没有好好利用图书馆,没有去做一件让自己离开大学也会念念不忘的大事。甚至于自己拿来谋生的技能,自己也不算做的很好,学习精力分散,杂而不精。更重要的是,内心变得十分的浮躁,越来越少的思考,经常直接接受不用消化的信息和知识。总想有段时间能进行反思总结,结果没能去成。没去成也就罢了吧。自己慢慢的去改正吧。这半年来也已经慢慢改了不少了。

进入工作以来,自己进入了一个适应期。如何跟同事沟通,如何完成上级分配的任务,如何摆正自己的心态,如何规划自己的未来道路,如何更快更好的提升自己。十分感谢导师和leader对我的帮助。我感觉慢慢适应了自己的工作,也对自己的未来慢慢的有了一些规划和安排。进入头条感觉很幸运,同时也感觉很有挑战。每天都下班很晚,周末还经常加班。大家都是抱着对项目的期待在努力着。对于我来说,必须自己粗心,做事不细致不严谨的毛病,慢慢成为一个踏实靠谱,能独当一面的人。过几天我的试用期就结束了,希望不要被裁掉。哈哈。也希望新的一年,自己能尽快成长,进步的速度大于头发脱落的速度。

这一年里感情方面跟女朋友关系也算磨合的更有默契了。两个人都对对方有了更深的了解,所以对两个人在一起的状态也有了更多的思考。两个人性格都偏温和,平时也没啥吵闹,凡事商量着来。尽量去礼让对方。分手几次后,争吵的更少了,也算是带来了一些益处。

按照我司OKR的评价标准评价自己的2018年,我打个6.5分。各方面都有较高的努力空间。未来可期。

2019年,对于自己的期望是把现在的一些能做的事做到最好。思想上能摒弃一些无益的糟粕,兴趣爱好能有一些拿得出手的成就。事业上成长的足够快,生活上更有规律更健康,感情上更成熟稳定。我也立个okr吧,明年今天复验,完不成的话,就罚自己吃公司健康餐一个月。

2019年 OKR:

  1. github上有一两个长久维护的库,能跟着自己的技术成长的那种。阅读至少20个优秀开源库源码
  2. 按照自定的计划继续坚持健身。今年的目标是体重达到65kg或体脂率达到13,身材匀称。
  3. 学完《吉他自学三月通》,滑板学会ollie和一些中低级动作
  4. 更深入的学习MMA。项目空闲后每周周末一次实战。
  5. 每月看一本专业无关的闲书。
  6. 早睡早起,养成为革命持续奋斗的作息习惯。持续养生,不做地中海!

2019,冲鸭!

自控力

Posted on 2018-09-30 | In 书籍

自控力

这一周终于把拖了很久没看的自控力看完了。用一句话来归纳自己的所得,可以总结为:感觉自己学习到了什么,又好像啥都没学到,又好像见识到了什么,但是对自己来说又有好多的争议点。下面我会针对自己的一些摘录,提供一些自己的思考,由于是第一次这样去做,可能还是不够完善。

摘录及有得

是非判断对自控力的影响:
1.当说到孰是孰非时,我们都能毫不费力地作出符合道德标准的选择。我们只想让自己感觉良好,而这就为自己的胡作非为开了绿灯。
2.只要我们的思想中存在正反两方,好的行为就总是允许我们做一点坏事
3.不要把支持目标实现的行为误认为是目标本身。不是说你做了一件和你目标一致的事情,你就不会再面临危险了。注意观察一下,你是否因为认为某些积极的行为值得称赞,就忘了自己实际的目标是什么。

当我们把事情分割成正反两方时候,我们就很容易为了一点好处而搭上另外一些代价。或者说是更心安理得的去接收一些对我们自控有害的东西。比如标注了低糖的可乐总是会成功的俘获减肥的我们。减肥期的人本来就不应该喝可乐,低糖成功的降低了我们的注意力。

关注过程对自控力的影响:
1.虽然这个观点不符合我们对“完成目标”的看法,但关注进步确实会让我们离成功越来越远。这不是说进步本身是个问题,问题在于进步给我们带来的感觉。更进一步说,问题是我们不能坚持自己的目标,而会听从自己的感觉。进步可以激励人,甚至可以提高未来的自控力,但前提是,你要把自己的行动当做努力完成目标的证据。
2.不要把支持目标实现的行为误认为是目标本身。不是说你做了一件和你目标一致的事情,你就不会再面临危险了。注意观察一下,你是否因为认为某些积极的行为值得称赞,就忘了自己实际的目标是什么。
3.虽然这个观点不符合我们对“完成目标”的看法,但关注进步确实会让我们离成功越来越远。这不是说进步本身是个问题,问题在于进步给我们带来的感觉。更进一步说,问题是我们不能坚持自己的目标,而会听从自己的感觉。进步可以激励人,甚至可以提高未来的自控力,但前提是,你要把自己的行动当做努力完成目标的证据。

当我们要实现某个目标时,如果计划比较长远,可以分解成一步一步的小台阶,关注自己在每个小台阶上得到的进步,这样会让我们更有攀登高峰的决心。但是我们同样要有意识,我们的小进步并不是目标本身,我们依然要以长远为导向

道德光环带来的影响:
1.这种“道德许可”的形式为你对诱惑说“好的”提供了充足的理由。当我们想获得放纵许可的时候,我们会寻找任何一个美德的暗示,为自己放弃抵抗作辩护。
2.只要使你放纵的东西和使你觉得品德高尚的东西同时出现,就会产生光环效应。比如,研究人员发现,出于慈善目的购买巧克力的人,会吃更多的巧克力来奖励自己的善行

给自己带上一层道德光环,就容易放任自己做一些坏事

多巴胺与奖励系统对自控力的影响:
1.我们会发现,在追求幸福的时候,我们可不能相信大脑指引的方向。我们还会发现,神经营销学这个全新领域是如何利用这个原理来操控我们的大脑、为我们制造欲望的,以及我们如何才能抵抗这种欲望。
2.奖励系统是怎么迫使我们行动的呢?当大脑发现获得奖励的机会时,它就释放出叫做多巴胺的神经递质。多巴胺会告诉大脑其他的部分它们需要注意什么,怎样才能让贪婪的我们得手
3.克努森证明了,多巴胺控制的是行动,而不是快乐。奖励的承诺保证了被试者成功地行动,从而获得奖励。当奖励系统活跃的时候,他们感受到的是期待,而不是快乐。
4.现代科技“及时行乐”的特点,加上原始的激励系统,就让我们成了多巴胺的奴隶,从此欲罢不能。我们中的一些人应该还记得那种狂按电话答录机按钮、查收新消息的刺激感。后来,我们又通过调制解调器连上了美国在线,希望电脑会告诉我们:“你收到了新邮件!”好吧,我们现在有了Facebook、Twitter、电子邮件和短信息——这就是精神病专家罗伯特·希斯设计的自我刺激设备的现代版
5.难以预料的,经常变化的奖励比通常的奖励更有吸引力
6.奖励的承诺有很大的力量,它会让我们继续追求那些不会带给我们快乐的东西,会让我们消费那些不会带来满足感,只会带来更多痛苦的东西。追求奖励是多巴胺的主要目标,所以,即便你经历的事物和原本承诺的并不相符,它也不会给你释放“停下来”的信号。
7.奖励系统也很重要:快感缺乏的人认为生活就是一系列的习惯,他们没有对满足感的期待。他们可以吃东西、购物、社交,甚至有性生活,但不会期待从中获得快乐。当他们不再需要快感的时候,他们就失去了动力。如果你想不出任何一件让你感觉良好的事,你就很难从床上爬起来做事。这种毫无欲望的状态耗尽了希望,也夺走了很多人的生命。

很多时候我们都是被奖励系统驱使着。比如游戏,冲动的犯罪,当我们每次被奖励系统驱使着的时候,只要质问一下自己:这真的会让我快乐吗?应该就能更好的自控。但是这奖励系统又太过于重要,是一个人追求幸福不能缺少的东西,不然人生就会毫无干劲

压力对自控力的影响:
1.为什么压力会带来欲望呢?因为这是大脑援救任务的一部分。此前,我们看到了压力是如何引发应激反应的。应激反应是身体内部相互协调的一系列变化,让你能在面临危险的时候保护自己。但人脑不仅仅会保护人的生命,它也想维持人的心情。所以,当你感到压力时,你的大脑就会指引着你,让你去做它认为能带给你快乐的事情。
2.有效和无效的策略最主要的区别是什么?真正能缓解压力的不是释放多巴胺或依赖奖励的承诺,而是增加大脑中改善情绪的化学物质,
3.美国心理学家协会的调查发现,最有效的解压方法包括:锻炼或参加体育活动、祈祷或参加宗教活动、阅读、听音乐、与家人朋友相处、按摩、外出散步、冥想或做瑜伽,以及培养有创意的爱好。
4.我们有时候产生强烈的不适感,或者我们并不知道这是为什么。即使我们意识不到这种恐惧,它还是会让我们立即作出回应,对抗自己的无力感。我们会去寻找保护伞,寻找任何能让自己觉得安全、有力量、得到安慰的东西。
5.屈服会让你对自己失望,会让你想做一些改善心情的事。那么,最廉价、最快捷的改善心情的方法是什么?往往是做导致你情绪低落的事。

延迟满足感:
1.经济学家称之为“延迟折扣”。也就是说,等待奖励的时间越长,奖励对你来说价值越低。很小的延迟就能大幅降低你感知到的价值。
2.对那些想延迟快感的人来说,这是个好消息。只要你能创造一点距离,就会让拒绝变得容易起来。比如,一项研究发现,把糖果罐放在桌子的抽屉里,而不是直接放在桌上,会让办公室职员少吃1/3的糖。
3.想获得一个冷静明智的头脑,我们就需要在所有诱惑面前安排10分钟的等待时间。如果10分钟后你仍旧想要,你就可以拥有它。

当我们确实想要打破自己的计划的时候,延迟十分钟思考一下。在制定计划的时候,也要考虑到让自己和诱惑的距离远一点。

总是期待未来:
1.如果我们真的指望未来的自己能这么崇高,我们确实可以相信,未来的自己能做好所有的事。但更典型的情况是,当我们到了未来,理想中“未来的自己”却不见了,最后作决定的还是毫无改变的曾经的自己。即便我们现在已经失去了自控力,我们仍然愚蠢地希望未来的自己不会面临冲突。
2.向未来的自己描述一下自己现在将要做什么,有助于你实现长期目标。你对未来的自己有什么希望?你觉得自己会变成什么样?你也可以想象未来的自己回头看现在的自己。未来的自己会因为现在的自己做了什么而表示感激?

把希望和努力放在现在,而把享受留给明天

社会对我们的控制力的影响:
1.实际在很大程度上,那些我们通常认为受自控力影响的行为,也会受社会控制力的影响。我们愿意相信,我们的决定不会受他人的影响,我们为自己的独立和自由意志感到自豪。
2.在考虑如何作出选择时,我们经常想象自己是别人评估的对象。研究发现,这为人们自控提供了强大的精神支持。预想自己实现目标(比如戒烟或献血)后会非常自豪的人,更有可能坚持到底并获得成功,预想自己的行为会受到谴责也很有效。
3.为了让自豪感发挥作用,我们必须认为别人都在监视自己,或我们有机会向别人报告自己的成功。市场研究人员发现,人们在公开场合更愿意购买绿色产品,比他们私下网购时买得多。
4.当人们试着不去想某件事时,反而会比没有控制自己的思维时想得更多,比自己有意去想的时候还要多。这个效应在人处于紧张、疲劳或烦乱状态时最为严重。韦格纳把这个效应称为“讽刺性反弹”(
5.避免讽刺性反弹 怎么才能找到摆脱这种困境的方法呢?韦格纳提出了一种对抗讽刺性反弹的方法。这个方法本身就很有讽刺意味——这个方法就是放弃自控。当人们不再试图控制那些不希望出现的想法和情绪时,它们也就不会再来烦你了。大脑激活研究证实,一旦允许研究对象把压抑的想法表达出来,这个想法就不太容易被激活了,因此进入意识的可能性也变小了。这件事说起来有点矛盾——允许你去想一件事,反而会减少你想起它的可能性。

这个点也很新颖。但是也确实是这样的。

你所不知道的Method Handles

Posted on 2018-09-30 | In android

Method Handles

Method Hanldes是在Java 7引入的概念。全限定名是java.lang.invoke.MethodHandles。在这篇文章中,我们将学会如何创建,使用MethodHandles及它的原理。

1.介绍

Method Handles的引入是为了与已经存在的java.lang.reflect API相配合。他们分别是为了解决不同的问题而出现的。从性能角度上说,MethodHandle api要比反射快很多因为访问检查在创建的时候就已经完成了,而不是像反射一样等到运行时候才检查。但同时,Method Handles比反射更难用,因为没有列举类中成员,获取属性访问标志之类的机制。
另外,MethodHandles可以操作方法,更改方法参数的类型和他们的顺序。而反射则没有这些功能。
从以上角度看,反射更通用,但是安全性更差,因为可以在不授权的情况下使用反射对象。而method Handles遵从了分享者的能力。所以method handle是一种更低级的发现,适配和调用方法的方式,唯一的优点就是更快。所以反射更适合主流Java开发者,而method handle更适用于对编译和运行性能有要求的人。

2.使用

1.要使用method handle,首先需要得到Lookup。这是创造方法,构造函数,属性的method handles的工厂类。

1
2
3
4
// public方法的Lookup
MethodHandles.Lookup publicLookup = MethodHandles.publicLookup();
// 所有方法的Lookup
MethodHandles.Lookup lookup = MethodHandles.lookup();

2.要创建MethodHandle,lookup需要一个定义了它的类型的MethodType对象。这里的类型包括了传入参数的类型,和最后返回的类型,要一一对应。第一个是返回类型,如果没有返回值就是Void.class, 后面是可变的传入参数的类型。

a MethodType represents the arguments and return type accepted and returned by a method handle or passed and expected by a method handle caller.

例如

1
2
// 接收数组,返回一个List对象
MethodType mt = MethodType.methodType(List.class, Object[].class);

3.查找MethodHandle
Lookup之所以叫Lookup自然是因为他们有查找MethodHandle的能力。先看看他的方法。
屏幕快照 2018-10-19 上午10.19.20.png

4.接下来就可以进行查找并调用了

1
2
3
4
MethodType mt = MethodType.methodType(String.class, char.class, char.class);
MethodHandle replaceMH = publicLookup.findVirtual(String.class, "replace", mt);
 
String output = (String) replaceMH.invoke("jovo", Character.valueOf('o'), 'a');

5.方法调用细则:
有三种方法可以调用方法invoke(), invokeWithArugments()和invokeExact(),当我们使用invoke时,我们必须固定arguments的数目。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// invoke使用
MethodType mt = MethodType.methodType(String.class, char.class, char.class);
MethodHandle replaceMH = publicLookup.findVirtual(String.class, "replace", mt);
String output = (String) replaceMH.invoke("jovo", Character.valueOf('o'), 'a');

// invokeWithArguments使用
MethodType mt = MethodType.methodType(List.class, Object[].class);
MethodHandle asList = publicLookup.findStatic(Arrays.class, "asList", mt);
List<Integer> list = (List<Integer>) asList.invokeWithArguments(1,2);

// invokeExact
MethodType mt = MethodType.methodType(int.class, int.class, int.class);
MethodHandle sumMH = lookup.findStatic(Integer.class, "sum", mt);
int sum = (int) sumMH.invokeExact(1, 11);

具体的区别是:

与invokeExact方法不同,invoke方法允许更加松散的调用方式。它会尝试在调用的时候进行返回值和参数类型的转换工作。这是通过MethodHandle类的asType方法来完成的,asType方法的作用是把当前方法句柄适配到新的MethodType上面,并产生一个新的方法句柄。当方法句柄在调用时的类型与其声明的类型完全一致的时候,调用invoke方法等于调用invokeExact方法;否则,invoke方法会先调用asType方法来尝试适配到调用时的类型。如果适配成功,则可以继续调用。否则会抛出相关的异常。这种灵活的适配机制,使invoke方法成为在绝大多数情况下都应该使用的方法句柄调用方式。

参考:
https://www.baeldung.com/java-method-handles
https://www.cnblogs.com/night-wind/p/4405564.html

从零到一完成一个健身App之二:路由框架设计(未完成)

Posted on 2018-09-19 | In 从零到一健身App

从零到一完成一个健身App之二:路由框架设计

1.路由框架的相关背景

路由框架是组件化大背景下出现的一种成熟方案。

相信有过组件化开发经验的同学都会知道,如果我们想在Module A中打开Module B中的Activity,这时候的Activity是找不到引用的。显式跳转是行不通的。另外,在很多时候我们都需要后台来动态确定我们需要跳转到什么页面。比如常见的分享码等。除此之外,H5页面越来越多,而H5页面是没办法通过startActivity跳到原生页面的。所以在这种情况下需要定义一种更适合组件化的,更灵活的路由方式。

为了找到这种方式,我们经历了以下过程

  1. 隐式意图Activity跳转:依赖于Manifest文件的修改,并且参数不方便传递。使用了startActivity之后就无法插手任何环节了,就无法在跳转失败的时候降级。
  2. 基于事件,广播或EventBus,这种情况下跳转流不容易监控,而且在跳转复杂的情况下接入维护成本较高
  3. 调用一个固定的方法:这种情况侵入性太强,所有Activity都要实现,改造起来困难,难于扩展。

所以我们需要一个优秀的路由框架,要能够实现:

  • 通过与后台一起定义schema,可以达到按后台所需跳转到特定页面的需求,也可以和H5页面统一跳转方式
  • 代码侵入性弱,调用方便。
  • 接入方简单易用。
  • 灵活,能针对特定需求进行特定的处理。比如我们常见的登录判断和权限检查等。

具体到实现方面,要能做到:

  • 路由注册采用apt注解式自动生成,避免手动管理
  • 参数依赖注入,自动保存,不再需要手动写onSaveInstance、onCreate(SaveInstace)、onNewIntent(Intent)、getQueryParamer等
  • 能动态拦截和动态替换

2.现有框架调研

当前比较知名的路由框架有:阿里的ARouter,美团的WMRouter,ActivityRouter.

2.1 ARouter

ARouter有多个优势:
1.直接解析URL路由,解析参数并赋值到对应目标字段的页面中;
2.支持多模块项目;
3.支持InstantRun;
4.拦截器策略,允许自定义;
5.提供IoC容器,控制反转;
6.映射关系自动注册;
7.灵活的降级策略.

赶紧学习一下这些都是怎么做到的。先看一个示例

1
2
3
4
@Route(path = "/test/activity2")
public class Test2Activity extends AppCompatActivity

ARouter.getInstance().build("/test/activity2")..withString("key1", "value1").navigation();

再看看ARouter的架构。
37b723fb660fdfcf7cdc09c194c88a8073d8272d.png

其中Compiler中三个处理器,分别是:
Route Processor:处理路径路由
Interceptor Processor:处理拦截器
Autowire Processor:进行自动装配

API中Launcher是用户可以调用的api所在的地方
Service是将功能和组件封装成的接口,对外开放。等到时候分析源码的时候要注意一下是怎么做到的。
Templete是用于SDK编译器生成映射文件时候提供的模板。
更下层的ware House: 存储了ARouter在运行期间加载的一些配置文件以及映射关系
Thread则是提供了线程池,因为存在多个拦截器的时候以及跳转过程中都是需要异步执行的
Class工具则是用于解决不同类型APK的兼容问题的。
再下一层就是Logistics Center,从名字上翻译就是物流中心,整个SDK的流转以及内部调用最终都会下沉到这一层, 也按功能模块划分。

一.如何实现解析URL路由,解析参数并赋值到目标字段中?
ARouter采用的是APT技术,通过定义的annotation来解析到相应的path。这里会遇到的第一个问题是找到处理注解的时机。运行期处理注解会大量的运用反射。所以要在编译期处理被注解的类。至于如何区分,就在于这里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* 用来指明注解的访问范围
* 1.源码级注解SOURCE,该类型的注解信息会留在.java源码中,
* 源码编译后,注解信息会被丢弃,不会保留在编译好的.class文件中;
* 2.编译时注解CLASS,注解信息会保留在.java源码里和.class文件中,
* 在执行的时候,会被Java虚拟机丢弃不回家再到虚拟机中;
* 3.运行时注解RUNTIME,java源码里,.class文件中和Java虚拟机在运行期也保留注解信息,
* 可通过反射读取
*/
@Retention(RUNTIME)
//是一个ElementType类型的数组,用来指定注解所使用的对象范围
@Target(value = FIELD)
public @interface Add {
float ele1() default 0f;
float ele2() default 0f;
}

从零到一完成一个健身App之一:开篇词和项目概述

Posted on 2018-09-17 | In 从零到一健身App

从零到一完成一个健身App

一.开篇词

我的计划是用一年半到两年的时间完成一个健身App。

1.目的:我是一个喜欢接受新技术的人。但是我又是一个很懒的人,这种懒惰体现在自己对于技术可能很多时候只限于使用的地步,而没有去深挖技术内部原理和实现细节,并且对于App的设计也缺乏一定的思考。现在自己已经在公司工作两个月了,从公司的项目中开阔了眼界。但是苦于没有地方练手。所以想找个App把自己学到的应用上来。目前来看,自己对于Android较为深入的技术都缺乏认知,都是需要强补的。这也是我想从头开始设计一个好的应用的原因。

2.项目现状与预期:这个应用是我为自己这种健身爱好者量身打造的应用,也打算部分参考keep和抖音的设计。之前未毕业前就有开始过这个项目的开发,但是当时因为毕业而搁置了。现在再拿出来看,当时的代码写的非常的傻逼。于是打算推翻重来。预期是:

  • 学会使用主流框架,如fresco,Architecture Component,RxJava等。
  • 结合组件化和插件化,热修复等技术
  • 对于一些较为简单的工具,如路由框架,能自行编写
  • 关注性能优化
  • 在项目的一些模块中能接触到深度学习。做一个AI赋能的App

3.项目计划:因为现在在公司项目中确实压力比较大,10,10,大小周。目前来看,自己能用在这个项目上的时间是早上的八点半到十点。晚上的十点四十到十一点四十。周末也有时间可以搞一搞。考虑到自己可能需要学习很多的知识,而且自己平时的时间也不一定能放到这个上面来,预计需要一年半到两年的时间里完成这个项目。在这两年里,我会每两周根据自己的项目情况完成一篇博客。因为是自己的项目,代码中不会出现公司的代码,所有的代码都是自己学习后产出的。也不希望有人将我这项目用于商业目的。

4.专栏适合的人群:

  • 有一定Android开发经验,处于Android基础和进阶之间的人
  • 想通过一个完整项目接触到当前互联网公司里最火热的技术的人
  • 作为Android开发,想接触到深度学习等在Android中应用的人
  • 适用于想一步一步提升自己各方面Android技术的人

二。项目概述

1.项目将会采用的总体架构:

  • MVVM和Architecture Component:相信大家都知道有MVC,MVP,和MVVM三种架构,MVC就不说了,MVP在很长的一段时间里都是主流,但是已经体验过MVVM的我不可能再用MVP了-_- 。常用的MVVM框架有databinding,但是databinding会带来业务代码耦合进xml中的问题,扩展性也差。我决定使用google的Architecture Component。谷爹出品,值得信赖,而且还挺好用的,很多数据问题都得到了解决。

  • RxJava:Rxjava作为一个成熟的库,事件流和函数式编程的思想深入人心

  • okhttp+retrofit:这两个框架是当今大厂的主流网络框架,也是学习网络相关的重要入口,我会在基本使用的基础上去挖掘相关的值得学习和有亮点的地方。
  • fresco:这也是最强大和性能最好的图片框架了。
  • 插件化+组件化+热修复:这三个是类似的,但是又有一些区别。我倾向于每种技术都自己实现一个框架,其中组件化的路由框架我是一定要写的,代码量并不多。其他的就等到时候再看,如果有时间就自己写一个。
  • AOP技术:主要是用来进行日志和自动埋点等。目前对这块不是特别熟悉,等到时候再考虑吧。
  • JNA:由于之前没怎么接触过JNI技术,对相关的技术都不是很了解。所以在这个项目中想加入一些特效相关的处理,这也是我的兴趣所在。不可避免的要使用到native方法。因此打算使用JNA这个库
  • 深度学习框架:目前暂定ncnn,腾讯优图出品的开源深度学习框架,看评测是要强于目前所有已知开源框架。
  • 数据库:因为Realm的一些坑,数据库框架打算采用GreenDao。
  • 下载库:打算自己实现一个比较有意思的库。目前公司的项目有,自己可以吸收其精华,去掉自己不需要的部分。

再次声明:本项目中不会有公司项目的源码,都是自己完成。

12

oubindo

17 posts
8 categories
12 tags
© 2019 oubindo
Powered by Hexo
|
Theme — NexT.Muse v5.1.4