0%

对于算法的复杂度,一直是懵懂状态。索性这次将这段时间听到看到的一些内容整理出来,以求准确清晰的理解它。

听吴军老师的音频,才了解在计算机发展初期,对于算法好坏的评判标准科学家也是不清楚的,1965年哈特马尼斯(Juris Hartmanis)和斯坦恩斯(Richard Stearns)提出了算法复杂度的概念(二人后来因此获得了图灵奖),计算机科学家们开始考虑一个公平的、一致的评判算法好坏的方法。不过,最早将复杂度严格量化衡量的是著名计算机科学家、算法分析之父 高德纳(Don Knuth) 。今天全世界计算机领域都以高德纳的思想为准。

高德纳的思想主要包括这三个部分:

  1. 在比较算法的快慢时,需要考虑数据量特别特别大,大到近乎无穷大时的情况。为什么要比大数的情况,而不比小数的情况呢?因为计算机的发明就是为了处理大量数据的,而且数据越处理越多。

  2. 决定算法快慢的因素虽然可能很多,但是所有的因素都可以被分为两类。第一类是不随数据量变化的因素,第二类是随着数量变化的。

    • 比如说,有一种大小排序的算法,它的运算次数是10倍的N平方,其中N是要排序的数字的数量。前面的那个10倍是个常数,和N的大小显然没有关系,10个数排序是如此,一亿个数排序时也是如此。而后面的N平方自然和N有关系了。高德纳讲,我们在研究算法时,不必考虑前面那个不变的常数,它是10倍,还是1倍,或者是100倍,只需要看后面那个变化的因素即可。

    • 因为N这个数趋近于无穷大时,前面那个不变的常数的影响是微乎其微的,算法的速度主要由后面一个因素决定。比如,当N=10的时候,N平方就是100;N=100,N平方就是1万;N=1万,N平方就是一亿……总之,N平方这个因子的变化非常快。更广泛地讲,任何随着N变化的因素,通常会造成量级的差异。量级就如同芝麻和西瓜的差异,西瓜和地球的差异。100个芝麻是无法和一个西瓜去对比的。

  3. 如果两种算法 在量级上相当 ,在计算机科学里,就认为它们是一样好的,也就是计算机科学家并不关心三五倍的差别,这就好比1粒芝麻和5粒芝麻都是芝麻量级的东西,大家就不要比了。只有当科学家们不关心几倍的差异后,才可能集中精力考虑量级的差异。也就是说,计算机科学家要尽可能地去捡西瓜。事实上在计算机科学领域,如果谁说自己把目前最好的算法速度提升了一倍,这种论文是无法发表的。

在了解了高德纳提出的思想后,对于算法复杂度关心的点以及评判标准才有了“最正宗”的理解。关心的是大量数据的问题,排除掉不变的因素,且只关注量级的差别,这其实是一个理想的环境,这种假设是非常有意义的。吴军老师说“回顾下科学发展的历史,那些能总结出理论的人要做的第一件事,就是多虑掉所有次要的因素,构建一个理想的环境(或者虚拟的环境),其目的是找到真正的主要矛盾,先把主要问题解决了再说。这些人的思维方式,比普通人是高出一大截的。我们都同意艺术上要有想象力,其实在科学上也是如此,抽象的想象力很重要。”

在有了上面这段内容作为基础后,下面对时间复杂度和空间复杂度的讲述会简单很多。

1. 时间复杂度

一句话描述就是:一个算法在问题规模不断增大时对应的时间增长曲线。
看完下面这两张图,你大概能理解问题规模的增大以及时间增长曲线之间的关系。

algorithm-time-complexity-summary

algorithm-time-complexity-problem-size

从第二张图可以直观的反应出指数、立方和平方级别同线性对数级别的时间增长曲线不在一个量级上。所以你在写代码时,到底选择怎样的时间曲线增长量级是不是有底了?这里其实也印证了高德纳思想的第1和第3点,那第2点排除掉不变的因素怎么理解,这需要用到数学公式了。

在数学上定义: 存在常数 c,使得当 N >= c 时 T(N) <= f(N),表示为 T(n) = O(f(n)) 。
算法需要执行的运算次数 用输入大小n的函数表示,即 T(n)。如下图示:

algorithm-time-complexity-math

当 N >= 2 的时候,f(n) = n^2 总是大于 T(n) = n + 2 的,于是我们说 f(n) 的增长速度是大于或者等于 T(n) 的,也说 f(n) 是 T(n) 的上界,可以表示为 T(n) = O(f(n))。

因为f(n) 的增长速度是大于或者等于 T(n) 的,即T(n) = O(f(n)),所以我们可以用 f(n) 的增长速度来度量 T(n) 的增长速度,所以我们说这个算法的时间复杂度是 O(f(n))。其实就是我们通常所说的大O推倒法,几条重要的准则如下:
1.用常数1来取代运行时间中所有加法常数。
2.修改后的运行次数函数中,只保留最高阶项。
3.如果最高阶项存在且不是1,则去除与这个项相乘的常数。

至此,时间复杂度讲述完毕,没有复杂的描述,利用几张图和一个数学公式推倒就能理解好。另外还需要掌握时间复杂度分析的基本策略:从内向外分析,从最深层开始分析。如果遇到函数调用,要深入函数进行分析。

2. 空间复杂度

空间复杂度讨论的是算法所需要的辅助存储空间,其中输入数据所占用的具体空间取决于问题本身, 与算法无关。记作:
S(n) = O(f(n))

若算法执行时间时所需要的辅助空间相对于输入数据量而言是一个常数,则称这个算法为原地工作, 辅助空间为O(1)。

将一维数组a中的n个数据逆序存放到原数组中, 下面是两种算法:

[算法1]
S(n) = O(n)

1
for(i = 0; i < n; i++)    b[i] = a[n - i - 1];   for(i = 0; i < n; i++)    a[i] = b[i]  

[算法2]
S(n) = O(1)

1
for(i=0; i < n/2; i++){    t = a[i];    a[i] = a[n - i - 1];    a[n - i - 1] = t;   }
  • 算法1的空间复杂度为O(n), 需要一个大小为n的辅助数组b
  • 算法2的空间复杂度为O(1), 仅需要一个变量t, 与问题规模n无关

算法的空间复杂度与时间复杂度合称为算法的复杂度。面对不同的算法如何选择主要就从这两个方面去考虑,理想情况是一个算法的时间与空间复杂度都小,但这是很难做到的,面对不同的情况要具体问题具体分析: 是以时间换空间, 还是以空间换时间。

参考:

  1. (数据结构)十分钟从零搞定时间复杂度(计算算法的时间复杂度)
  2. 如何理解算法时间复杂度的表示法O(n²)、O(n)、O(1)、O(nlogn)等?
  3. 算法(一)时间复杂度
  4. 算法的性能分析
  5. 算法之基本排序算法

Android群英传

1、怎样区别Dalvik与ART的异同?
2、怎样理解Android UI界面架构图?
3、怎样理解requestWindowFeature的设置时机?
4、怎样理解当创建一个Canvas对象时,要传入一个bitmap对象?
5、怎样做好UI模板?
6、怎样理解getScrollX()和getScrollY()?
7、怎样自定义一个简单的scrollView和viewPager,理清思路?
8、怎样理解事件拦截中的时间传递和事件处理?
9、怎样设置ListView为空时的样式?
10、怎样判断ListView是否滚到最后一行?
11、怎样判断ListView的滚动方向?
12、怎样获取ActionBar的高度?
13、怎样理解Android坐标系中的各种方法?
14、怎样使用MarginLayoutParams?
15、怎样理解scrollTo和scrollBy参数正负值的含义?
16、怎样理解canvas的4个方法?
17、怎样理解颜色矩阵?
18、怎样理解图形变换矩阵?
19、怎样实现自定义刮刮乐效果?
20、怎样理解SurfaceView和View的异同和使用场景?
21、怎样绘制正弦曲线?
22、怎么理解Android任务栈?
23、怎样实现在真机上检测布局层级?

图解HTTP

大概去年5月份的时候,从同事桌上看到这本书,随手一翻觉得里面的插图很有意思,留下了深刻的印象。这几天花了几个小时的时间看完之后,收获颇多。总得来说,这是一本偏基础的书,阅读起来没什么困难,对我而言,主要是理顺了一些之前模棱两可的概念。
TCP/IP协议族按层次分别是:应用层、传输层、网络层和数据链路层。层次化之后,设计也变得相对简单了。处于应用层上的应用可以只考虑分派给自己的任务,而不需要弄清对方在地球上哪个地方、对方的传输路线是怎样的、是否能确保传输送达等问题。简单来说就是职责单一化了,可以只关心自己应该关心的内容。

应用层:应用层决定了向用户提供应用服务时通信的活动。
TCP/IP协议族内预存了各类通用的应用服务。比如,FTP(File Transfer Protocol,文件传输协议)和DNS(Domain Name System,域名系统)服务就是其中两类。HTTP协议也处于该层。

传输层:传输层对上层应用层,提供处于网络连接中的两台计算机之间的数据传输。在传输层有两个性质不同的协议:TCP(Transmission Control Protocol,传输控制协议)和UDP(User Data Protocol,用户数据报协议)。

网络层:网络层用来处理在网络上流动的数据包。数据包是网络传输的最小数据单位。该层规定了通过怎样的路径(所谓的传输路线)到达对方计算机,并把数据包传送给对方。与对方计算机之间通过多台计算机或网络设备进行传输时,网络层所起的作用就是在众多的选项内选择一条传输路线。

链路层:用来处理连接网络的硬件部分。包括控制操作系统、硬件的设备驱动、NIC(Network Interface Card,网络适配器,即网卡),及光纤等物理可见部分(还包括连接器等一切传输媒介)。硬件上的范畴均在链路层的作用范围之内。

经过作者的讲述后,对四层的职责,分别干了些什么是不是有了很明确的了解。反正经过这段文字之后,我是有种顿悟的感觉。

当输入一个网址后接着发生了什么?这段描述或许会给你一个清晰的认知流程:“利用TCP/IP协议族进行网络通信时,会通过分层顺序与对方进行通信。发送端从应用层往下走,接收端则往应用层往上走。我们用HTTP举例来说明,首先作为发送端的客户端在应用层(HTTP协议)发出一个想看某个Web页面的HTTP请求。接着,为了传输方便,在传输层(TCP协议)把从应用层处收到的数据(HTTP请求报文)进行分割,并在各个报文上打上标记序号及端口号后转发给网络层。在网络层(IP协议),增加作为通信目的地的MAC地址后转发给链路层。这样一来,发往网络的通信请求就准备齐全了。接收端的服务器在链路层接收到数据,按序往上层发送,一直到应用层。当传输到应用层,才能算真正接收到由客户端发送过来的HTTP请求。发送端在层与层之间传输数据时,每经过一层时必定会被打上一个该层所属的首部信息。反之,接收端在层与层传输数据时,每经过一层时会把对应的首部消去。 ”下面图示更能清晰明了说明问题:

getScrollX

在HTTP协议中,状态码的职责是当客户端向服务器端发送请求时,描述返回的请求结果。借助状态码,用户可以知道服务器端是正常处理了请求,还是出现了错误。数字中的第一位指定了响应类别,详细如下图所示:

getScrollX

HTTP首部字段大致分为四类:通用首部字段,请求首部字段,响应首部字段,实体首部字段。
getScrollX
getScrollX
getScrollX
getScrollX

在这些首部字段中,有很多值得关注。

例如no-cache字段,使用no-cache指令的目的是为了防止从缓存中返回过期的资源。客户端发送的请求中如果包含no-cache指令,则表示客户端将不会接收缓存过的响应。于是,“中间”的缓存服务器必须把客户端请求转发给源服务器。如果服务器返回的响应中包含no-cache指令,那么缓存服务器不能对资源进行缓存。源服务器以后也将不再对缓存服务器请求中提出的资源有效性进行确认,且禁止其对响应资源进行缓存操作。

例如首部字段If-Modified-Since,属附带条件之一,它会告知服务器若If-Modified-Since字段值早于资源的更新时间,则希望能处理该请求。而在指定If-Modified-Since字段值的日期时间之后,如果请求的资源都没有过更新,则返回状态码304 Not Modified的响应。If-Modified-Since用于确认代理或客户端拥有的本地资源的有效性。获取资源的更新日期时间,可通过确认首部字段Last-Modified来确定。

例如首部字段Expire,s会将资源失效的日期告知客户端。缓存服务器在接收到含有首部字段Expires的响应后,会以缓存来应答请求,在Expires字段值指定的时间之前,响应的副本会一直被保存。当超过指定的时间后,缓存服务器在请求发送过来时,会转向源服务器请求资源。源服务器不希望缓存服务器对资源缓存时,最好在Expires字段内写入与首部字段Date相同的时间值。但是,当首部字段Cache-Control有指定max-age指令时,比起首部字段Expires,会优先处理max-age指令。

书籍的结尾章节主要讨论的是HTTP的缺点、性能、安全问题,这里不再总结。关于网络的知识总结,可以参考我写的一篇日志:Android之网络通信整理

总得来说,这本书的内容简单、阅读起来无障碍,主要用于疏通知识点,值得一阅。

step1: native项目准备工作

在app module下的build.gradle文件的dependencies中添加React Native 依赖:

1
compile "com.facebook.react:react-native:+"

com.android.support:appcompat的版本号用23.0.1,同时修改compileSdkVersion和buildToolsVersion

1
2
3
4
5
6
7
8
9
10
11
12
compileSdkVersion 23
buildToolsVersion "23.0.1"
defaultConfig {
applicationId "com.quncao.testsize"
minSdkVersion 16
targetSdkVersion 22
versionCode 1
versionName "1.0"
ndk{
abiFilters "armeabi-v7a", "x86"
}
}

修改清单文件,增加如下内容:

1
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />

step2: 将android工程改造成react-native工程

引入或者创建package.json文件

  • 可以通过命令npm init创建,也可以复制一个空RN工程中的package.json

引入或者创建node_modules文件

  • 可以通过命令npm install --save react react-native创建,也可以复制一个空RN工程中的node_modules文件夹

引入或者创建.flowconfig文件

  • 可以通过curl -o .flowconfig https://raw.githubusercontent.com/facebook/react-native/master/.flowconfig创建,也可以复制一个空RN工程中的.flowconfig文件

引入或者创建index.android.js文件

在项目根目录的build.gradle中(注意:不是app模块中的build.gradle文件)添加依赖

1
2
3
4
5
6
7
8
9
10
11
allprojects {
repositories {
mavenLocal()
jcenter()
maven {
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
url "$projectDir/../node_modules/react-native/android"
// url "$rootDir/../node_modules/react-native/android"
}
}
}

这里需要注意url的值,上面是指向了当前工程中的node_modules文件夹,而$rootDir/../node_modules/react-native/android这句会引用默认的react-native的版本,通常会是0.21.0

step3: 改造Applicatioin

实现ReactApplication类,并修改清单文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MyApplication extends Application implements ReactApplication {
private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
@Override
public boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}

@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage()
);
}
};

@Override
public ReactNativeHost getReactNativeHost() {
return mReactNativeHost;
}
}

step4: 编写Activity,返回RN页面

1
2
3
4
5
6
7
8
9
10
11
12
package com.quncao.testsize;

import com.facebook.react.ReactActivity;

public class SettingActivity extends ReactActivity {

@Override
protected String getMainComponentName() {
return "TestSize";
}

}

这里需要注意getMainComponentName的值和index.android.js这个文件中的registerComponent值保持一致

Demo工程:链接: https://pan.baidu.com/s/1eSL4P7k 密码: ggav

一万小时天才理论

断断续续看完了这本书,印象最深的一个词是髓鞘质,然后就是感觉这本书有点啰嗦,大量的采访素材堆砌。总得来说用一句话就能概括本书:【激情+伯乐+精深练习一万小时 = 天才】。当然还是有些细节部分是很出彩的,至少在我没接触这本书之前,对于精深练习这个词是没有任何概念的。

本书传递的一个价值观可能是:我们口中的“天才”其实就处在我们的周围,或者是你的邻居,或者是你镇上的某个同学,在经过上面公式的“摧残”后成名。请务必不要说什么一夜成名,哪有什么一夜成名的事情发生,鬼知道别人经历的什么。

关于精深练习的部分,作者用的笔墨是最多的,就是想要告诉读者:朝着既定目标挣扎前进,挑战自己的能力极限,不断犯错,汲取经验,不断犯错,汲取经验,然后你以为自己快要成功的时候,成功却与你失之交臂,此时应该给自己一些掌声,生活哪有随随便便的成功,当然会一波好几折。其实此时有个东西在潜移默化的进化着,是的,它是髓鞘质,通过你不断重复的练习和刺激,形成了一个绝缘的神经回路。当你的髓鞘质生长到足够强大,其实就是练习的次数足够多的时候,在你的领域距离“专家”就越近。

为什么目标明确、重视错误的练习如此有效呢?因为构建一条好的神经回路,最佳的方法就是开启电流,处理错误,然后重启,就这样一遍遍重复这个过程。努力拼搏不是无关紧要的过程,而是生理上的必经之路。所以人才和技能的故事就是髓鞘质的故事,看到这里你是不是有点失望呢?

精深练习需要时间,时刻保持激情就显示尤为重要。当然在你精深练习的时候,没个好的教练怎么能行呢,所以赶紧去找个伯乐吧,他会在自身拥有了强大知识库和深厚的认识后,灵活的根据你的自身情况调整施教策略。所以想成为天才的你准备好了吗?

下面是摘录内容:

第一部分 精深

  • 谁也不能随随便便成功,它来自彻底的自我管理和毅力。——哈佛图书馆训言

  • 精深练习是建立在一个悖论之上的:朝着既定目标挣扎前进,挑战自己的能力极限,不断犯错,会让你更聪明。类似的说法是,做那些不得不放慢节奏的事情,犯错并加以改正——就像爬冰山,刚开始的时候会滑倒,会跌跌撞撞,最后不知不觉中就变得敏捷自如。

  • 林克的训练器可以让飞行员的练习更加深入:停下,犹豫,犯错,并从中吸取经验。在林克训练器里坐上几小时,飞行员可以在操作面板上“起飞”、“降落”十几次。可以俯冲、失速,再调节恢复正常,可以在那个挑战自己能力极限的最佳位置上停留数小时,而在真正的飞机上是不可能冒这个险的。

  • 那些用林克训练器训练出来的航空队飞行员并非比那些失事的飞行员更勇敢、更聪明,而只是多了一个精深练习的机会。

  • 我一直认为,除了傻子,人在智力上差别不大,不同的只是热情和努力。——查尔斯·达尔文

  • 目标明确的练习能够将学习速度提高10倍,这听起来就像那个神话故事,一小把种子长成了一根有魔力的长藤,长藤通往成才的天堂。

  • 神经元完成的每一个动作都非常迅速,就在开关的一开一合之间。”菲尔茨谈及突触时说,“但是一开一关不是我们学习大多数事物的方法。弹好钢琴,下好象棋,打好棒球都非一日之功,但都是髓鞘质所擅长的。

  • 人才和技能的故事就是髓鞘质的故事。

  • 问:为什么目标明确、重视错误的练习如此有效呢?答:因为构建一条好的神经回路,最佳的方法就是开启电流,处理错误,然后重启,就这样一遍遍重复这个过程。努力拼搏不是无关紧要的过程,而是生理上的必经之路。

  • 犯错绝不是可有可无的——从神经学的角度来说,这是必须的:要想使技能回路达到最佳状态,必须先找到次佳位置;你必须犯错误,并关注这些错误;你得慢慢地锻炼自己的回路。你还必须持续开启那个回路(练习)以保持髓鞘质运作正常。毕竟,髓鞘质是活体组织。

  • 任何领域的任何专家都要经过10000小时专心致志地练习。

  • 它意味着所有的技能都可以通过同一套基本生理机制获得,并且进一步指出该生理机制有其生理极限,无人能幸免。

  • 精深练习×一万小时=世界级技能

  • 哇塞效应(Holy Shit Effect,缩写HSE),意思是当人才突然从天而降时,人们产生的一种强烈而复杂的情感:怀疑、崇拜以及妒忌(顺序不分先后)。

  • 哇塞效应是那种人们在看到跟自己一样的普通人拥有盖世才华时产生的感受。

  • 看见邻街那个傻孩子突然成了著名摇滚乐队的主吉他手,或是看见自家孩子对微分学熟练到令人费解时,心底产生的那种讶异,同时还有点隐隐作痛。

  • 秘技第一式:组块化

  • 精深练习就是构建神经回路,并使之裹上绝缘体。

  • 为什么放慢节奏如此有效呢?首先,放慢练习节奏使你更加关注错误,每一次都提高了精确度——而对于髓鞘质的生长而言,精确就是一切。

  • 第二,放慢练习给了练习者更重要的东西:技能的形状和节奏。

  • 重复练习是无可替代且千金难求的,然而,要补充解释几点。常规练习是练得越多越好,但是,精深练习并不适用这道等式。投入更多的时间练习是有用的,但前提是你必须处于最佳位置。更重要的是,一个人每天能进行多久的精深练习似乎是有普遍限制的。

  • 练习就是每天弹奏同样的音符,人类基本的姿态——为一个想法努力,为你渴望的伟大成就努力争取,然后感觉它与你失之交臂。

  • 正是这种感觉,精深练习不是简单的挣扎,而是有目的的奋斗:选定目标、努力争取、评估差距、回到初始步骤。而且那种感觉同其他东西一样,是可以习得的。髓鞘质的其中一个进化优势是,它能够使任何回路绝缘化,甚至是那些一开始我们不喜欢的。在草山,指导员看着学生们体会精深练习的感觉。起初他们并不喜欢。但是很快学生们开始忍受,甚至享受这样的体验。

第二部分 激情

  • 精深练习需要时间

  • 这就是激情的工作原理,是那些让我们意识到“我就想成为那样的人”的时刻。人们通常认为激情是一种内心品质。但是我探访的人才温床越多,就越坚信激情首先来自外部世界。在人才温床,正确的蝴蝶拍打翅膀卷起了人才飓风。

  • 他看到这个图表时震惊了。“我不敢相信自己的眼睛。”他说。进步不取决于任何可衡量的天赋或者性格,而是一个微不足道的念头。一个小小的念头却产生了无穷的力量,孩子甚至在学习开始之前就打定了主意,这决定了进步的快慢。不同孩子之间的表现有着惊人的差异。同样的练习时间,给出长期承诺的那组孩子表现得比给出短期承诺的孩子好4倍。

  • 每个信号都与身份和群体有关。每个信号都相当于一盏闪烁的灯,指引人们前进:那些人在做超级有意义的事情。总之,每个信号都针对未来的归属感。

  • 人们都会因为自己与一个成就非凡的群体有联系而感到心潮澎湃。

  • “如果身处轻松愉快的环境,我们自然而然就会停止努力。”巴特克斯说,“为什么会这样呢?如果人们发现环境很艰苦,马上受到激励。一座管理完善的豪华网球学校给了学员一种豪华未来的体验——学习动机当然也就被压抑了,那是无法避免的。”

  • 丧父或者丧母是一个原始信号:生活不再安全。你不必成为心理学家,也会发现由于缺乏安全感而激发出来的能量储备;你也不必是达尔文主义理论家,也会看到这种反应是如何进化而来的。这个信号可以改变孩子与世界的关系,重新给自己定位,激发和引导自己的大脑去解决危险,处理生命中的可能性。正如基思·西门顿(Keith Simonton)在《天才起源》(Origins of Genius)中提到的,对于父母死亡,“如此不利的事件造就了人格的健全发展,剽悍到使他们足以克服拦在成功道路上的种种障碍和挫折。”

  • 艾森斯塔德名单上的超级名人并非天赋异禀,并非人类中的统计异常值,而是统治所有人的普遍原则的逻辑外延:(1)才能需要精深练习;(2)精深练习需要充分的能量;(3)某些信号会触发巨大能量的迸发。

  • 也不是说,在一个大家庭里出生较晚就必然跑得快,更不是说有位家长在孩子年幼时过世就必然使其能够当上英国首相。但这确实说明,跑得快如同其他才能,受到一堆主要因素的影响,超越基因这个因素,直接相关的是对激励信号强烈的潜意识反应,这种激励信号提供了精深练习所需的能量,从而促进髓鞘质生长。

  • 短短几句话,他成功地点燃了排他性(“我只知道,它很适合汤姆·索亚……我看一千个孩子里面都没有……”)和稀缺性(“男孩子每天都可以刷围栏吗?……波莉姨妈对这围栏的要求太可怕了”)这些原始信号。他的手势以及其他身体语言传达着同样的讯息“盯着他看了一番”以及“一会走远看看效果,随意地在某个地方加上一刷子,又评论一下粉刷效果”——仿佛是在从事最最重要的工作。

  • 精深练习需要深入认真的工作以及热情的劲头。当你开始练习,你不是“打”网球;你在挣扎、在反抗,你需要集中精力,然后慢慢进步。我们的学习像婴儿的蹒跚学步。赞其勤奋的语句之所以有效,是因为它直达学习的核心,而想要点燃激情,没有比这更强大的了。

第三部分 伯乐

  • 他们的个性——核心才能的回路——更像农民;如汉斯·詹森那样认真细致的髓鞘质培育者。他们脚踏实地,而且严格自律。拥有广泛而且深刻的知识框架。他们将之应用于培养逐步提升的技能回路,却不掌控最终结果。詹森无法回答我的问题,因为这个问题的核心不符合逻辑。仅仅看见两棵幼苗,就能知道哪一棵会长得更高,这可能吗?唯一的答案是,判断孰优孰劣还为时尚早,他们都还在成长。

  • 优势一:知识矩阵——伯乐的杀手锏

  • 这些深厚的认识,并非与生俱来。同任何技能一样,那是随着时间,通过激情和精深练习逐渐掌握的。[插图]伟大的教练绝不能一蹴而就。我遇到的多数教练拥有类似的人生轨迹:他们曾经在各自的领域意气风发,却不幸受挫,但是他们能够积极寻找原因。

  • 优势二:洞察力——鹰的视力

  • 优势三:简明的指示——神奇的教鞭

  • 优势四:气质与诚信——不可阻挡的魅力

  • 诸如踢足球、写作、喜剧表演这样的技能,需要建立灵活性神经回路,即需要学习者在脑中形成数千条回路,这样就可以轻松地找到一条回路,可以让他绕过变化无常的障碍物。拉小提琴、打高尔夫、体操、花样滑冰这些技能,需要建立一致性神经回路,全靠扎实的技术基础,重现一场完美表演所遵循的基本法则。这就是为什么自学的高尔夫球员、滑冰运动员,还有体操运动员达到世界级水平的鲜有案例,而自学成才的小说家、喜剧演员,还有足球运动员却层出不穷的原因。

后记 一万小时的世界

  • 但是实际上,正如所有的精深练习,一个人首先要克服那种“大事化小,小事化了”的自然倾向——这在企业中尤其困难。

译者后记

  • 所以,这不是为那些天生有异象的人写的书,而是送给那些没被上帝的骰子掷中的人的一份补偿。

在Android中Activity和Service是应用程序的核心组件,它们以松藕合的方式组合在一起构成了一个完整的应用程序,首先这得益于framework层提供了一套完整的机制来协助应用程序启动这些Activity和Service,其次得益于系统提供Binder机制实现进程间通信。这篇日志目的主要是梳理启动一个Activity的核心流程,涉及到哪些进程等。阅读之前需要先了解ActivityManagerServic、ActivityStack、ApplicationThread、ActivityThread以及Binder等概念才不至于一头雾水。

ActivityRecord:描述了一个Activity所需要的全部信息。
TaskRecord:用来记录Activity的任务栈。
ActivityStack:用于管理一系列的TaskRecord。
ActivityManagerServic:Android系统中最核心的系统服务,主要负责四大组件的启动、切换和调度,以及应用进程的管理和调度工作。
ActivityThread:应用程序的主线程,负责主线程Handler的启动以及四大组件的调度和管理。
ApplicationThread:ActivityThread的内部类,继承自IApplicationThread.Stub,用于其它进程与它所属应用进程的交互。
Binder:Android平台上用于实现IPC的一种机制。

Activity启动过程的大致步骤

  1. 无论是通过Launcher来启动Activity,还是通过Activity内部调用startActivity接口来启动新的Activity,都通过Binder进程间通信进入到ActivityManagerService进程中,并且调用ActivityManagerService.startActivity接口;

  2. ActivityManagerService调用ActivityStack.startActivityMayWait来做准备要启动的Activity的相关信息;

  3. ActivityStack通知ApplicationThread要进行Activity启动调度了,这里的ApplicationThread代表的是调用ActivityManagerService.startActivity接口的进程,对于通过点击应用程序图标的情景来说,这个进程就是Launcher了,而对于通过在Activity内部调用startActivity的情景来说,这个进程就是这个Activity所在的进程了;

  4. ApplicationThread不执行真正的启动操作,它通过调用ActivityManagerService.activityPaused接口进入到ActivityManagerService进程中,看看是否需要创建新的进程来启动Activity;

  5. 对于通过点击应用程序图标来启动Activity的情景来说,ActivityManagerService在这一步中,会调用startProcessLocked来创建一个新的进程,而对于通过在Activity内部调用startActivity来启动新的Activity来说,这一步是不需要执行的,因为新的Activity就在原来的Activity所在的进程中进行启动;

  6. ActivityManagerService调用ApplicationThread.scheduleLaunchActivity接口,通知相应的进程执行启动Activity的操作;

  7. ApplicationThread把这个启动Activity的操作转发给ActivityThread,ActivityThread通过ClassLoader导入相应的Activity类,然后把它启动起来。

7.0系统上AMS启动Activity的核心流程

8.0系统上AMS启动Activity的核心流程

从Launcher启动一个应用程序

下面以启动MainActivity为例来说明Android应用程序的启动过程。从Launcher启动一个应用程序的时序图如下:
从Launcher启动一个应用程序

整个应用程序的启动过程要执行很多步骤,但是整体来看,主要分为以下五个阶段:
1、Step1 - Step 11:Launcher通过Binder进程间通信机制通知ActivityManagerService,它要启动一个Activity

2、Step 12 - Step 16:ActivityManagerService通过Binder进程间通信机制通知Launcher进入Paused状态;

3、Step 17 - Step 24:Launcher通过Binder进程间通信机制通知ActivityManagerService,它已经准备就绪进入Paused状态,于是ActivityManagerService就创建一个新的进程,用来启动一个ActivityThread实例,即将要启动的Activity就是在这个ActivityThread实例中运行;

4、Step 25 - Step 27:ActivityThread通过Binder进程间通信机制将一个ApplicationThread类型的Binder对象传递给ActivityManagerService,以便以后ActivityManagerService能够通过这个Binder对象和它进行通信;

5、Step 28 - Step 35:ActivityManagerService通过Binder进程间通信机制通知ActivityThread,现在一切准备就绪,它可以真正执行Activity的启动操作了。

具体的Step步骤详情可参考:http://blog.csdn.net/luoshengyang/article/details/6689748

ActivityManagerService启动Activity的大致过程

Android应用程序框架层中的ActivityManagerService启动Activity的过程

在上面图中,ActivityManagerService和ActivityStack位于同一个进程(SystemServer进程),而ApplicationThread和ActivityThread位于另一个进程中(应用进程)。其中ActivityManagerService负责管理Activity的生命周期,它还借助ActivityStack把所有的Activity按照后进先出的顺序放在一个堆栈中;对每一个应用程序而言都有一个ActivityThread表示应用程序的主线程,而每一个ActivityThread都包含有一个ApplicationThread实例,它是一个Binder对象,负责和其它进程(如SystemServer进程)进行通信。

一点感想


参考:

  1. Android应用程序的Activity启动过程简要介绍和学习计划
  2. Android应用程序启动过程源代码分析
  3. Android应用程序内部启动Activity过程(startActivity)的源代码分析
  4. 任务与后退栈
  5. activity
  6. 那两年炼就的Android内功修养
  7. Android艺术探索 第9章 四大组件的工作过程
  8. 写给 Android 应用工程师的 Binder 原理剖析

最后感谢大神中的大神罗升阳的博客

1. Java基本数据类型及其封装类

简单类型 字节数 封装器
void void
boolean 1/8 Boolean
byte 1 Byte
char 2 Character
short 2 Short
int 4 Integer
long 8 Long
float 4 Float
double 8 Double

2. Switch能否用string做参数

在switch(args)中,args只能是一个整数表达式或者枚举常量Enum,整数表达式可以是int基本类型或Integer包装类型,由于byte,short,char都可以隐含转换为int,所以这些类型及其包装类型也是可以的。显然long、float、double类型不符合switch的语法规定,并且不能被隐式转换成int类型,所以它们不能作用于swtich语句中。注意:String类型是Java7开始支持的。

3. Object有哪些公用方法

方法名 解释
equals() 测试的是两个对象是否相等
hashCode() 返回当前对象的哈希值
toString() 返回对象的String代表串
clone() 进行对象拷贝
getClass() 返回和当前对象相关的Class对象
notify()、notifyall()、wait() 都是用来对给定对象进行线程同步的
finalize() 当GC检测到没有引用到此对象时调用

4. equals 与 == 的区别

Object类中的equals方法和 “==” 是一样的 没有区别,比较内存中存放的对象的堆内存地址,通俗一点说就是比较两个对象是否为同一对象;而String、Integer等一些类是重写了equals方法,加入了自己所需的 “逻辑比较”,才使得equals和 “==” 不同,一个呈现的是逻辑上是否相等,一个呈现的是是否为同一对象。所以当我们创建类时自动继承了Object的equals方法,要想实现不同的等于比较必须重写equals方法。以下是Object类中的equals源码:

1
2
3
public boolean equals(Object obj) {
return (this == obj);
}

测试代码如下所示:

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
package com.leeeyou.test.basic;

/**
* Created by leeeyou on 2017/4/13.
*/
public class TestEquals {
public static void main(String[] args) {
// -128 ~ 127 之间
Integer i1 = -129;
Integer i2 = -129;

System.out.println(i1 == i2); //false, 但在 -128 ~ 127 之间是true
System.out.println(i1.intValue());
System.out.println(i1.equals(i2));//true
System.out.println("---");

Float f1 = .5f;
Float f2 = .5f;
System.out.println(f1 == f2); //false
System.out.println(Float.floatToIntBits(f1));
System.out.println(f1.equals(f2));//true
System.out.println("---");

//Object类中的equals方法和"=="是一样的,比较内存中存放的对象的(堆)内存地址
Object o1 = new Object();
Object o2 = new Object();
Object o3 = o1;
System.out.println(o1.equals(o2));//false
System.out.println(o1 == o2);//false
System.out.println(o1 == o3);//true
System.out.println(o1.equals(o3));
}
}
1
2
3
4
5
6
7
8
9
10
11
12
false
-129
true
---
false
1056964608
true
---
false
false
true
true

5. 改写equals为什么要改写hashCode

如果不这样的话,就会违反Object.hashCode的通用约定,从而导致该类无法结合所有基于散列的集合一起正常运作,这样的集合包括HashMap、HashSet和HashTable。

下面是约定的内容,摘自Obejct规范[Java SE6]:

  • 在应用程序执行期间,只要对象的equals方法的比较操作所用到的信息没有被修改,那么对这同一个对象调用多次,hashCode方法都必须始终如一地返回同一个整数。在同一个应用程序的多次执行过程中,每次执行所返回的整数可以不一致。

  • 如果两个对象根据equals(Object)方法比较是相等的,那么调用这两个对象中任意一个对象的hashCode方法都必须产生同样的整数结果。即保证在equals相同的情况下hashcode值必定相同。

  • 如果两个对象根据equals(Object)方法比较是不相等的,那么调用这两个对象中任意一个对象的hashCode方法,则不一定要产生不同的整数结果。但是程序员应该知道,给不相等的对象产生截然不同的整数结果,有可能提高散列表的性能。

参考:Effective java 中文版(第2版)

6. Java的四种引用

6.1. 强引用(StrongReference)

强引用是使用最普遍的引用。如果一个对象具有强引用那GC绝不会回收它。当内存空间不足,JVM宁愿抛出OutOfMemoryError使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。

Tips:强引用其实也就是我们平时 A a = new A() 这个意思。

6.2. 软引用(SoftReference)

如果一个对象只具有软引用,则内存空间足够GC就不会回收它;如果内存空间不足了,就会回收这些对象的内存。软引用可用来实现内存敏感的高速缓存。

软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被GC回收,JVM就会把这个软引用加入到与之关联的引用队列中。

6.3. 弱引用(WeakReference)

弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。 在GC线程扫描它所管辖的内存区域时,一旦发现弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过由于 GC是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。

6.4. 虚引用(PhantomReference)

与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被GC回收。虚引用主要用来跟踪对象被GC回收。

虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。 当GC准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。

1
2
ReferenceQueue queue = new ReferenceQueue ();
PhantomReference pr = new PhantomReference (object, queue);

程序员可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否为将要被垃圾回收。如果发现某个虚引用已经被加入到引用队列,就可以在所引用对象的内存被回收之前采取必要的行动。

Tips:利用软引用和弱引用解决OOM问题。
用一个HashMap来保存图片的路径和相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM会自动回收这些缓存图片对象所占用的空间,从而有效地避免了OOM的问题

Tips:更多关于对象引用的知识,参考 第4讲 | 强引用、软引用、弱引用、幻象引用有什么区别?

7. ArrayList、LinkedList、Vector的区别

下面这张是Collection框架图,主要包含List、Set和Queue集合。
Collection框架图

参考 第8讲 | 对比Vector、ArrayList、LinkedList有何区别?

8. HashMap和ConcurrentHashMap的区别

下面这张是Map集合框架图。
Map集合框架图
参考 第9讲 | 对比Hashtable、HashMap、TreeMap有什么不同?
参考 第10讲 | 如何保证集合是线程安全的? ConcurrentHashMap如何实现高效地线程安全?

9. String、StringBuffer与StringBuilder的区别

String 是 Java 语言非常基础和重要的类,提供了构造和管理字符串的各种基本逻辑。它是典型的 Immutable 类,被声明成为 final class,所有属性也都是 final 的。也由于它的不可变性,类似拼接、裁剪字符串等动作,都会产生新的 String 对象。由于字符串操作的普遍性,所以相关操作的效率往往对应用性能有明显影响。

StringBuffer 是为解决上面提到拼接产生太多中间对象的问题而提供的一个类,它是 Java 1.5 中新增的,我们可以用 append 或者 add 方法,把字符串添加到已有序列的末尾或者指定位置。StringBuffer 本质是一个线程安全的可修改字符序列,它保证了线程安全,也随之带来了额外的性能开销,所以除非有线程安全的需要,不然还是推荐使用它的后继者,也就是 StringBuilder。

StringBuilder 在能力上和 StringBuffer 没有本质区别,但是它去掉了线程安全的部分,有效减小了开销,是绝大部分情况下进行字符串拼接的首选。

Tips:更多知识,参考 第5讲 | String、StringBuffer、StringBuilder有什么区别?

10. try catch finally

在不抛出异常的情况下,程序执行完try里面的代码块之后,该方法并不会立即结束,而是继续试图去寻找该方法有没有finally的代码块。

如果没有finally代码块,整个方法在执行完try代码块后返回相应的值来结束整个方法。
如果有finally代码块,此时程序执行到try代码块里的return语句之时并不会立即执行return,而是先去执行finally代码块里的代码。

若finally代码块里没有return或没有能够终止程序的代码,程序将在执行完finally代码块代码之后再返回try代码块执行return语句来结束整个方法。
若finally代码块里有return或含有能够终止程序的代码,方法将在执行完finally之后被结束,不再跳回try代码块执行return。

在抛出异常的情况下,原理也是和上面的一样的,你把上面说到的try换成catch去理解就OK了。

11. Excption与Error包结构

Throwable

参考 第2讲 | Exception和Error有什么区别?

12. 锁的等级

12.1. 分类

类锁:在代码中的方法上加了static和synchronized的锁,或者synchronized(xxx.class)的代码段。
对象锁:在代码中的方法上加上synchronized锁或者synchronized(this)的代码段,二者的加锁方法构成竞争关系,同一时刻只能有一个方法能执行。
私有锁:在类内部声明一个私有属性如private Object lock,在需要加锁的代码段synchronized(lock)。

类锁和对象锁不会产生竞争,二者的加锁方法不会相互影响。
私有锁和对象锁也不会产生竞争,二者的加锁方法不会相互影响。

12.2. synchronized和lock区别

Lock是Java SE5中新引入的用于提高性能的类,类似的还有Atomic类。

1
2
3
4
5
6
7
Lock l = ...;
l.lock();
try {
// access the resource protected by this lock
} finally {
l.unlock();
}

需要考虑两个因素:1 互斥方法体的大小问题。只互斥那些绝对必须互斥的部分 ,但实际情况中,被互斥部分可能非常大,相对于进入和退出互斥的时间比来说,Lock提升的互斥速度优势基本就湮灭了; 2 synchronized的可读性高,维护成本较低,通常是开发人员的惯用手法。建议以synchronized入手,只有在性能调优时或者 有明确的证据表明 在同步下synchronized遇到性能瓶颈才替换为Lock这种做法具有实际意义。

Tips:更多知识,参考 第15讲 | synchronized和ReentrantLock有什么区别呢?

13. 生产者消费者模式

下面这段代码的实现方式比较复杂,需要人为控制轮询和条件判断。

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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
package com.day12.proCus;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
* 解决了"重复消费和未消费"的问题(if改成while),同时利用JDK5.0新特性Lock和Condition 处理了同步时上锁解锁和等待唤醒的的问题
* Lock最大的一个特点是:"可以支持多个相关的 Condition 对象",也就是说一把锁上可以绑定多个"状态条件"对象,详见API
*
* @author kongbei
*/
public class ProducerCustomerDemo2 {
public static void main(String[] args) {
Res2 r = new Res2();

Producer2 p = new Producer2(r);
Customer2 c = new Customer2(r);

Thread t1 = new Thread(p);
Thread t2 = new Thread(c);
Thread t3 = new Thread(p);
Thread t4 = new Thread(c);

t1.start();
t2.start();
t3.start();
t4.start();
}
}

class Producer2 implements Runnable {
private Res2 r;

Producer2(Res2 r) {
this.r = r;
}

@Override
public void run() {
r.set("iPhone5s");
}
}

class Customer2 implements Runnable {
private Res2 r;

Customer2(Res2 r) {
this.r = r;
}

@Override
public void run() {
r.print();
}
}

class Res2 {
private String name;
private int count = 5588;
private boolean flag = false;//当前仓库是不是满的

private Lock lock = new ReentrantLock();
private Condition conditon_pro = lock.newCondition();
private Condition condition_cus = lock.newCondition();

public void set(String name) {
while (true) {
lock.lock(); //上锁
while (flag) {
try {
conditon_pro.await(); //让生产线程等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.name = name + "..编号" + count++;
System.out.println(Thread.currentThread().getName() + "- 生产 -" + this.name);
flag = true;
condition_cus.signal(); //唤醒消费线程
lock.unlock();//解锁
}
}

public void print() {
while (true) {
lock.lock(); //上锁
while (!flag) {
try {
condition_cus.await();//让消费线程等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(" " + Thread.currentThread().getName() + "-消费-" + name);
flag = false;
conditon_pro.signal();//唤醒生成线程
lock.unlock();//解锁
}
}
}

下面利用 BlockingQueue 来实现,由于其提供的等待机制,我们可以少操心很多协调工作。

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
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class ConsumerProducer {
public static final String EXIT_MSG = "Good bye!";

public static void main(String[] args) {
// 使用较小的队列,以更好地在输出中展示其影响
BlockingQueue<String> queue = new ArrayBlockingQueue<>(3);
Producer producer = new Producer(queue);
Consumer consumer = new Consumer(queue);
new Thread(producer).start();
new Thread(consumer).start();
}


static class Producer implements Runnable {
private BlockingQueue<String> queue;

public Producer(BlockingQueue<String> q) {
this.queue = q;
}

@Override
public void run() {
for (int i = 0; i < 20; i++) {
try {
Thread.sleep(5L);
String msg = "Message" + i;
System.out.println("Produced new item: " + msg);
queue.put(msg);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

try {
System.out.println("Time to say good bye!");
queue.put(EXIT_MSG);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

static class Consumer implements Runnable {
private BlockingQueue<String> queue;

public Consumer(BlockingQueue<String> q) {
this.queue = q;
}

@Override
public void run() {
try {
String msg;
while (!EXIT_MSG.equalsIgnoreCase((msg = queue.take()))) {
System.out.println(" Consumed item: " + msg);
Thread.sleep(10L);
}
System.out.println("Got exit message, bye!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

Tips:更多知识,参考 第20讲 | 并发包中的ConcurrentLinkedQueue和LinkedBlockingQueue有什么区别?

14. 写一个死锁程序

出现原因:同步中嵌套同步

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
package com.day11.deadLock;


public class DeadLock {
  public static void main(String[] args) {
    Thread t1 = new Thread(new DeadLock2(false));
    Thread t2 = new Thread(new DeadLock2(true));
    t1.start();
    t2.start();
  }
}


class DeadLock2 implements Runnable{
  boolean flag = true;
  DeadLock2(boolean flag){
    this.flag = flag;
  }
  @Override
  public void run() {
    if(flag){
     synchronized (LockObject.lo1) {
       System.out.println("if lo1");
       synchronized (LockObject.lo2) {
         System.out.println("if lo2");
       }
     }
    }else{
     synchronized (LockObject.lo2) {
       System.out.println("else lo2");
       synchronized (LockObject.lo1) {
         System.out.println("else lo1");
       }
     }
    }
  }
}


class LockObject {
  static LockObject lo1 = new LockObject();
  static LockObject lo2 = new LockObject();
}

Tips:更多知识,参考 第18讲 | 什么情况下Java程序会产生死锁?如何定位、修复?

15. ThreadLocal

ThreadLocal是一个数据结构,用于隔离其他线程,提供线程内的局部变量;这种变量在线程的生命周期内起作用,能减少同一个线程内多个函数或者组件之间一些公共变量传递的复杂度。

先看看JDK8的ThreadLocal-set/get方法的源码:

libcore/ojluni/src/main/java/java/lang/ThreadLocal.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
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//拿到当前线程的ThreadLocalMap,实际上就是Thread类的threadLocals属性
ThreadLocalMap map = this.getMap(t);
if (map != null)
//key是ThreadLocal本身
map.set(this, value);
else
createMap(t, value);
}

public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
//在ThreadLocalMap中找到对应的Entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
//得到Entry的value
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}

ThreadLocal.ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

可以发现,每个线程中都有一个ThreadLocalMap数据结构。当执行set方法时,其值是保存在当前线程的threadLocals变量中;而执行get方法中是从当前线程的threadLocals变量获取。通过下面的例子可以更好的理解:

1
2
3
4
5
6
7
ThreadLocal<String> cityName = new ThreadLocal();

在Thread1中cityName.set("深圳");
在Thread2中cityName.set("北京");

Thread1 - ThreadLocalMap1(key,value) - key就是ThreadLocal实例,value就是深圳
Thread2 - ThreadLocalMap2(key,value) - key就是ThreadLocal实例,value就是北京

总结一下ThreadLocal的设计思路:每个Thread维护一个ThreadLocalMap映射表,这个映射表的key是ThreadLocal实例本身,value是真正需要存储的Object。所以在Thread1中set的值,对Thread2来说是摸不到的,而且在Thread2中重新set的话,也不会影响到Thread1中的值,保证了线程之间不会相互干扰。

15.1 Android中ThreadLocal唯一性验证

结合Android中的Handler机制中的Looper是如何保证线程唯一性的,下面代码分别在主线程和子线程中反射获取sThreadLocal和mQueue,然后观察它们是否一致。

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
/**
* 验证ThreadLocal全局唯一性,分别在主线程和子线程中反射获取sThreadLocal和mQueue,然后观察它们是否一致
*
* 期望:sThreadLocal一致,完全是同一个实例对象,因为它是static final的,隶属于类
* 而mQueue是final的,是跟线程实例对象对应的,不同线程是不同的实例对象
*/
private fun verifyThreadLocal() {
Toast.makeText(this, "请查看logcat", Toast.LENGTH_SHORT).show()

val looperClazz = Class.forName("android.os.Looper")

// 在主线程中反射获取sThreadLocal和mQueue对象
Looper.getMainLooper()?.also {
Handler(it).post {
val threadLocalField = looperClazz.getDeclaredField("sThreadLocal")
val mqField = looperClazz.getDeclaredField("mQueue")
threadLocalField.isAccessible = true
mqField.isAccessible = true
val threadLocalObj = threadLocalField.get(null) // 获取静态类型的实例对象
val mqObj = mqField.get(it)

val msg =
"ThreadName [${Thread.currentThread().name}], sThreadLocal is [${threadLocalObj}], mQueue is [${mqObj}]"
Log.e(TAG, msg)
}
}

// 在子线程中反射获取sThreadLocal和mQueue对象
Thread {
Looper.prepare() // 子线程中prepare looper
Looper.myLooper()?.also {
Handler(it).post { // 在子线程中创建Handler并post msg
val threadLocalField = looperClazz.getDeclaredField("sThreadLocal")
val mqField = looperClazz.getDeclaredField("mQueue")
threadLocalField.isAccessible = true
mqField.isAccessible = true
val threadLocalObj = threadLocalField.get(null) // 获取静态类型的实例对象
val mqObj = mqField.get(it)

val msg =
"ThreadName [${Thread.currentThread().name}], sThreadLocal is [${threadLocalObj}], mQueue is [${mqObj}]"
Log.e(TAG, msg)
}
}
Looper.loop() // 开启子线程的loop循环
}.start()
}

输出的日志如下所示,可以看到不管是主线程还是子线程,sThreadLocal是同一个实例对象,而mQueue是跟线程实例对象对应的,不同线程是不同的实例对象。

1
2
2021-12-16 15:12:38.550 8100-8100/com.leeeyou123.samplehandler E/MainActivity: ThreadName [main], sThreadLocal is [java.lang.ThreadLocal@8a18755], mQueue is [android.os.MessageQueue@74fba5b]
2021-12-16 15:12:38.550 8100-11638/com.leeeyou123.samplehandler E/MainActivity: ThreadName [Thread-12], sThreadLocal is [java.lang.ThreadLocal@8a18755], mQueue is [android.os.MessageQueue@51c42f8]

15.2. ThreadLocalMap

从名字上看可以猜到它也是一个类似HashMap的数据结构,但是在ThreadLocal中并没实现Map接口。在ThreadLoalMap中初始化一个大小16的Entry数组,Entry对象用来保存每一个key-value键值对,只不过这里的key永远都是ThreadLocal对象,是不是很神奇,通过ThreadLocal对象的set方法,结果把ThreadLocal对象自己当做key,放进了ThreadLoalMap中。

ThreadLoalMap的Entry继承了WeakReference,和HashMap很大的区别是,Entry中没有next字段,所以就不存在链表的情况了。

ThreadLocalMap是使用ThreadLocal的弱引用作为Key的,源码如下所示:

libcore/ojluni/src/main/java/java/lang/ThreadLocal.java

1
2
3
4
5
6
7
8
9
10
11
12
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
...
}

hash冲突

没有链表结构,那发生hash冲突了怎么办?先看看ThreadLoalMap中插入一个key-value的实现:

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
private void set(ThreadLocal<?> key, Object value) {
//拿到Entry数组
Entry[] tab = table;
int len = tab.length;
//通过ThreadLocal对象的hash值计算出下标
int i = key.threadLocalHashCode & (len-1);

//循环数组,起点是i所在的位置
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();

//位置i已经有Entry对象了,如果这个Entry对象的key正好是即将设置的key,那么重新设置Entry中的value
if (k == key) {
e.value = value;
return;
}

//位置i的key是null,则替换掉陈旧的Entry
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}

//当前i的位置是null,正在就初始化一个Entry放在i位置上
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}

每个ThreadLocal对象都有一个hash值threadLocalHashCode,每初始化一个ThreadLocal对象,hash值就增加一个固定的大小0x61c88647(即十进制1640531527)。上面的set代码展示了插入过程中的定位逻辑,在get的时候,也会根据ThreadLocal对象的hash值,定位到table中的位置,然后判断该位置Entry对象中的key是否和get的key一致,如果不一致,就判断下一个位置。

15.3. ThreadLocal和Synchronized

一个是锁机制进行时间换空间,一个是存储拷贝进行空间换时间。

16. 守护线程和非守护线程的区别以及用法

调用时机:在启动线程前调用。
特点一:开启后,和前台线程共同抢夺CPU执行权并运行。
特点二:结束时,当所有前台线程都结束后,后台线程会自动结束(JAVA VM退出)。

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
package com.day12.deamonThread;

public class DeamonThreadDemo {
public static void main(String[] args) {
DeamonThread dt = new DeamonThread();
Thread t1 = new Thread(dt);
Thread t2 = new Thread(dt);

t1.setDaemon(true);//设置为后台线程
t2.setDaemon(true);

t1.start();
t2.start();

for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + ".." + i);
}
}
}

class DeamonThread implements Runnable {
@Override
public void run() {
while (true) {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + ".." + i);
}
}
}
}

17. ThreadPool用法与优势

17.1. 优势

降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
提高响应速度:当任务到达时,任务可以不需要等到线程创建就能立即执行。
提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

17.2. newCachedThreadPool

创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
只有非核心线程;线程数量不固定的线程池;有超时机制,超过60s,闲置线程就会被回收;适合执行大量的耗时较少的任务。

17.3. newFixedThreadPool

创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
只有核心线程;线程数量固定的线程池;线程处于空闲状态,不会被回收。

17.4. newScheduledThreadPool

创建一个定长线程池,支持定时及周期性任务执行。
核心线程有固定数,非核心线程没有限制的线程池;非核心线程闲置时会被回收;主要用于执行定时任务和具有固定周期的重复任务。

17.5. newSingleThreadExecutor

创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
只有一个核心线程;确保所有的任务都在同一个线程中按顺序执行;其意义在于统一外界任务到一个线程中,使得这些任务之间不需要处理线程同步问题。

Tips:更多知识,参考 第21讲 | Java并发类库提供的线程池有哪几种? 分别有什么特点?

18. Java IO与NIO

IO NIO
面向流 面向缓冲
阻塞IO 非阻塞IO
选择器

18.1. 面向流 vs 面向缓冲

Java NIO和IO之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。

Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外它不能前后移动流中的数据,如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。

Java NIO的缓冲导向方法略有不同,数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性,但还需要检查是否该缓冲区中包含所有您需要处理的数据,而且需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。

18.2. 阻塞 vs 非阻塞

Java IO的各种流是阻塞的。这意味着当一个线程调用read()或write()时,该线程被阻塞直到有一些数据被读取或数据完全写入。该线程在此期间不能再干任何事情了。

Java NIO的非阻塞模式,一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。

18.3. 事件监听器

Java NIO的选择器(Selector)允许一个单独的线程来监视多个输入通道(Channel),我们在Selector上声明我们关心哪一个Channel的什么事件, Selector会监控这些Channels,并在事件发生时通知我们。

Tips:更多知识,参考 第11讲 | Java提供了哪些IO方式? NIO如何实现多路复用?

19. 接口和抽象类的区别

Tips:更多知识,参考 第13讲 | 谈谈接口和抽象类有什么区别?

20. 静态类和非静态类的区别

静态类不能实例化,非静态类在使用时必须要实例化。

静态类中不能创建非静态的方法,即静态类中只能创建静态方法,但在非静态类中可以调用静态方法。

静态内部类可以有静态成员(方法,属性),而非静态内部类则不能有静态成员(方法,属性)。

静态内部类只能够访问外部类的静态成员,而非静态内部类则可以访问外部类的所有成员(方法,属性)。

实例化一个非静态的内部类的方法 :

1
2
3
4
5
a.先生成一个外部类对象实例
OutClassTest oc1 = new OutClassTest();

b.通过外部类的对象实例生成内部类对象
OutClassTest.InnerClass no_static_inner = oc1.new InnerClass();

实例化一个静态内部类的方法 :

1
2
3
4
5
6
a.不依赖于外部类的实例,直接实例化内部类对象
OutClassTest.InnerStaticClass inner = new OutClassTest.InnerStaticClass();

b.调用内部静态类的方法或静态变量,通过类名直接调用
OutClassTest.InnerStaticClass.static_value
OutClassTest.InnerStaticClass.getMessage()

21. 父类、子类实例化顺序

当实例化子类对象时,首先要加载父类的class文件进内存,静态代码块是随着类的创建而执行,所以父类静态代码块最先被执行,子类class文件再被加载,同理静态代码块被先执行;实例化子类对象要先调用父类的构造方法,而调用父类构造方法前会先执行父类的非静态代码块。

例如:子类Sub继承父类Parent,Parent a = new Sub(); 则父类B构造函数、父类B静态代码块、父类B非静态代码块、子类A构造函数、子类A静态代码块、子类A非静态代码块 执行的先后顺序是?

父类静态代码块->子类静态代码块->父类非静态代码块->父类构造函数->子类非静态代码块->子类构造函数。

1
2
3
4
5
6
ParentCodeBlock 静态代码块
SubBlockParent 静态代码块
ParentCodeBlock 非静态代码块
ParentCodeBlock 构造函数
SubBlockParent 非静态代码块
SubBlockParent 构造函数

22. OOP和AOP的区别

OOP(面向对象编程)针对业务处理过程的实体及其属性和行为进行封装,以获得更加清晰的逻辑单元划分。

AOP则是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合性的隔离效果。AOP可以通过 预编译方式和运行期动态代理 实现在不修改源代码的情况下给程序动态统一添加功能的一种技术,把散落在程序中的公共部分提取出来做成切面类,这样的好处在于代码的可重用,一旦涉及到该功能的需求发生变化,只要修改该代码就行,否则你要到处修改,如果只要修改1、2处那还可以接受,万一有1000处呢?

23. 依赖注入和控制反转的区别

依赖注入,就其广义而言即是通过 “注入” 的方式来获得依赖。我们知道,A对象依赖于B对象,等价于A对象内部存在对B对象的 “调用”,而前提是A对象内部拿到了B对象的引用。B对象的引用的来源无非有以下几种:A对象内部创建(无论是作为字段还是作为临时变量)、构造器注入、属性注入、方法注入。后面三种方式统称为“依赖注入”,而第一种方式我也生造了一个名词,称为 “依赖内生” 。二者根本的差异即在于:我所依赖的对象的创建工作是否由我自己来完成。

控制反转跟依赖倒置都是一种编程思想,依赖倒置着眼于调用的形式,而控制反转则着眼于程序流程的控制权。一般来说,程序的控制权属于Client,而一旦控制权交到server,就叫控制反转。比如你去下馆子,你是Client餐馆是server。你点菜,餐馆负责做菜,程序流程的控制权属于Client;而如果你去自助餐厅,程序流程的控制权就转到server了,也就是控制反转。

1. view的绘制过程

首先还是有必要谈下view的绘制过程,这里不得不提ViewRoot,ViewRoot对应ViewRootImpl类,是连接WindowManager和DecorView的纽带。View的三大流程均是通过ViewRoot来完成的。

同时View的绘制流程从ViewRoot的performTraversals开始,经过measure,layout,draw三个过程才能最终将一个View绘制出来。简单来说measure用来测量View的宽和高,layout用来确定View在父容器中放置的位置,而draw则负责将View绘制在屏幕上。

总体绘制流程如下:

  • measure过程决定了View的宽/高,measure完成以后,可以通过getMeasureWidth和getMeasureHeight方法来获取到View测量后的宽/高,在几乎所有情况下它都等同于View的最终宽高,但是特殊情况下后面讨论。
  • layout过程决定了view的四个顶点的坐标和实际的view的宽高,完成以后,可以通过getTop,getBottom,getLeft,getRight来拿到view的四个顶点的位置,并可以通过getWidth和getHeight方法来拿到view的最终宽高。
  • draw过程则决定了view的显示,只有draw方法完成以后view的内容才能呈现在屏幕上。

上面提及的特殊情况如下:

  • 重写view的onLayout方法,就能使测量宽高不等于最终宽高
  • 另一种情况,view需要多次measure才能确定自己的测量宽高,在前几次的测量过程中,其得出的测量宽高有可能和最终宽高不一致,但最终来说,测量宽高还是和最终宽高一致

2. measure过程

几乎所有情况下view的测量大小和最终大小相等

2.1. View的measure

需重写onMeasure同时设置wrap_content时自身的大小,否则在布局中wrap_content相当于match_parent。同时考虑paddig的影响。

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
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int resultWidth;
int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
if (modeWidth == MeasureSpec.EXACTLY) {
resultWidth = sizeWidth;
} else {
resultWidth = mBitmap.getWidth() + getPaddingLeft() + getPaddingRight();
if (modeWidth == MeasureSpec.AT_MOST) {
resultWidth = Math.min(resultWidth, sizeWidth);
}
}
int resultHeight;
int modeHeight = MeasureSpec.getMode(heightMeasureSpec);
int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
if (modeHeight == MeasureSpec.EXACTLY) {
resultHeight = sizeHeight;
} else {
resultHeight = mBitmap.getHeight() + getPaddingTop() + getPaddingBottom();
if (modeHeight == MeasureSpec.AT_MOST) {
resultHeight = Math.min(resultHeight, sizeHeight);
}
}
setMeasuredDimension(resultWidth, resultHeight);
}

2.2. ViewGroup的measure

1 由于不同的viewgroup子类有不同的布局特性,所以交由子类实现测量细节
2 measureChildren();
3 自定义ViewGroup时,需要考虑测量以及布局子View,同时需要考虑ViewGroup的margin

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
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int parentDesireWidth = 0;
int parentDesireHeight = 0;
if (getChildCount() > 0) {
for (int i = 0; i < getChildCount(); i++) {
View childAt = getChildAt(i);
CustomLayoutParams layoutParams = (CustomLayoutParams) childAt.getLayoutParams();
measureChildWithMargins(childAt, widthMeasureSpec, 0, heightMeasureSpec, 0);
parentDesireWidth += childAt.getMeasuredWidth()
+ layoutParams.leftMargin
+ layoutParams.rightMargin;
parentDesireHeight += childAt.getMeasuredHeight()
+ layoutParams.topMargin
+ layoutParams.bottomMargin;
}
parentDesireWidth += getPaddingLeft() + getPaddingRight();
parentDesireHeight += getPaddingTop() + getPaddingBottom();
parentDesireWidth = Math.max(parentDesireWidth, getSuggestedMinimumWidth());
parentDesireHeight = Math.max(parentDesireHeight, getSuggestedMinimumHeight());
setMeasuredDimension(resolveSize(parentDesireWidth, widthMeasureSpec),
resolveSize(parentDesireHeight, heightMeasureSpec));
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int parentPaddingTop = getPaddingTop();
int parentPaddingLeft = getPaddingLeft();
if (getChildCount() > 0) {
int mutilHeight = 0;
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
CustomLayoutParams childLayoutParams = (CustomLayoutParams) child.getLayoutParams();
child.layout(parentPaddingLeft + childLayoutParams.leftMargin,
mutilHeight + parentPaddingTop + childLayoutParams.topMargin,
child.getMeasuredWidth() + parentPaddingLeft + childLayoutParams.leftMargin,
child.getMeasuredHeight() + mutilHeight + parentPaddingTop + childLayoutParams.topMargin);
mutilHeight += child.getMeasuredHeight()
+ childLayoutParams.topMargin
+ childLayoutParams.bottomMargin;
}
}
}

此时需要注意的是在处理margin时,可以自己定义一个CustomLayoutParams类继承LayoutParams,此后在onMeasure和onLayout中都强制转换成CustomLayoutParams。

2.3. view的mearsure过程和activity的生命周期不是同步执行的!

1 onWindowFocusChanged()
2 view.post(Runnable)
3 ViewTreeObserver

3. layout过程

1 通过setFrame设定view的l、t、r、b;四点确定,则view在父容器中的位置也确定了
2 调用onLayout,是父容器确定子容器的位置
3 onLayout具体实现同样和具体布局有关,所以view和viewgroup均没有真正实现onlayout

4. draw过程

1 绘制背景 background.draw(canvas)
2 绘制自己 onDraw
3 绘制children dispatchDraw
4 绘制装饰 onDrawScrollBars
5 setWillNotDraw设置为true,系统会进行相应优化。view默认不启用,viewgroup默认启用

5. 自定义View须知

1 处理好滑动冲突
2 处理好线程和动画,当view不可见时停止线程和动画
3 不要在View中使用handler,用post
4 让view支持padding和wrap_content

6. 自定义view实践

以下是自己过去两年内写过的关于自定义控件的日志,方便以后查阅。每当Android提供的控件不能满足你的需求时,首先你应该想想是否可以在现有控件的基础上修改一下来达到你的目的,而不是盲目地直接重写View或ViewGroup类,你可以提供不同的接口方法来修改你复合控件中的各类元素。

Android之开发中必备的坐标体系知识

Android之Paint的使用总结
Android之Drawable、Bitmap、Canvas、Paint之间区别
Android之自定义控件之画笔
Android之绘图基础Path类
Android之图像的色彩变换

Android艺术探索 第3章 View的事件体系
Android艺术探索 第4章 View的工作原理
Android艺术探索 第5章 理解RemoteViews

一级标题示例

二级标题

这里是二级标题正文这里是二级标题正文这里是二级标题正文这里是二级标题正文这里是二级标题正文这里是二级标题正文这里是二级标题正文这里是二级标题正文这里是二级标题正文这里是二级标题正文这里是二级标题正文

这里是正文这里是正文这里是正文这里是正文这里是正文这里是正文这里是正文这里是正文这里是正文这里是二级标题正文这里是二级标题正文这里是二级标题正文这里是二级标题正文这里是二级标题正文这里是二级标题正文

getScrollX

加载代码示例

1
2
3
4
5
6
package com.example.leeeyou.fixmyproblem;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapShader;

注释用法示例

有两个方法可以设置控件的上下左右图标,分别是:
第一个方法:setCompoundDrawablesWithIntrinsicBounds(Drawable left, Drawable top, Drawable right, Drawable bottom)
第二种方法:setCompoundDrawables(Drawable left, Drawable top, Drawable right, Drawable bottom)

它们的区别是使用第二种方法之前必须已经setBound(Rect)了,api原文如下

Sets the Drawables (if any) to appear to the left of, above, to the right of, and below the text. Use null if you do not want a Drawable there. The Drawables must already have had setBounds(Rect) called.

引用网址示例

场景:官方比赛数据页签 - 淘汰赛和积分赛数据 → viewpager(水平) + scrollView(垂直) + HorizontalScrollView(水平) / ListView(垂直)
分析:这里的冲突在于水平事件里面嵌套垂直,再嵌套水平和垂直事件
解决:采用内部拦截法配合getParent().requestDisallowInterceptTouchEvent(false);

这篇日志也记录了Android艺术探索 第3章 View的事件体系

表格示例

Tables Are Cool
col 3 is right-aligned $1600
col 2 is centered $12
zebra stripes are neat $1
dog bird cat
foo foo foo
bar bar bar
baz baz baz

代办和清单

  • 表示未完成
  • 表示已完成

1. ImageView的background和src的不同

如果两个属性同时存在,用户会看到 src 属性中设置的背景. 但同时 background 设置的背景也存在, 只是被 src 属性挡住了,在后面. src 等于是前景, background 等于是背景.background 会根据 ImageView 组件给定的长宽进行拉伸, 而 src 就存放的是原图的大小, 不会进行拉伸。src 是图片内容(前景), bg 是背景, 可以同时使用.此外: scaleType 只对 src 起作用,比如在 ImageView 中就可以用 android:scaleType 控制图片的缩放方式 ; bg 可设置透明度.

2. Glide加载圆形图和圆角图

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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
package com.example.leeeyou.fixmyproblem;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RectF;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.widget.ImageView;

import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation;

public class Problem02_GlideActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_glide);

String imageUrl = "http://7xptzi.com1.z0.glb.clouddn.com/19%E4%BA%91%E8%AF%BE%E5%A0%82_06.png";

ImageView img01 = (ImageView) findViewById(R.id.img01);
ImageView img02 = (ImageView) findViewById(R.id.img02);
ImageView img03 = (ImageView) findViewById(R.id.img03);
//Glide.with(this).load(imageUrl).thumbnail(0.1f).into(img01);

// play gif
Glide.with(this).load(R.mipmap.pkq).into(img01);

// round image
Glide.with(this)
.load(imageUrl)
.crossFade()
.transform(new GlideRoundTransform(this))
.into(img02);

// circle image
Glide.with(this)
.load(imageUrl)
.crossFade()
.transform(new GlideCircleTransform(this))
.into(img03);

}

public class GlideCircleTransform extends BitmapTransformation {

public GlideCircleTransform(Context context) {
super(context);
}

@Override
protected Bitmap transform(BitmapPool pool, Bitmap toTransform, int outWidth, int outHeight) {
return circleCrop(pool, toTransform);
}

private Bitmap circleCrop(BitmapPool pool, Bitmap source) {
if (source == null) return null;

int size = Math.min(source.getWidth(), source.getHeight());
int x = (source.getWidth() - size) / 2;
int y = (source.getHeight() - size) / 2;

Bitmap squared = Bitmap.createBitmap(source, x, y, size, size);

Bitmap result = pool.get(size, size, Bitmap.Config.ARGB_8888);
if (result == null) {
result = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
}

Canvas canvas = new Canvas(result);
Paint paint = new Paint();

paint.setShader(new BitmapShader(squared, BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP));
paint.setAntiAlias(true);

float radius = size / 2f;
canvas.drawCircle(radius, radius, radius, paint);

return result;
}

@Override
public String getId() {
return getClass().getName();
}
}

public class GlideRoundTransform extends BitmapTransformation {

private float radius = 0f;

public GlideRoundTransform(Context context) {
this(context, 4);
}

public GlideRoundTransform(Context context, int dp) {
super(context);
this.radius = Resources.getSystem().getDisplayMetrics().density * dp;
}

@Override
protected Bitmap transform(BitmapPool pool, Bitmap toTransform, int outWidth, int outHeight) {
return roundCrop(pool, toTransform);
}

private Bitmap roundCrop(BitmapPool pool, Bitmap source) {
if (source == null) return null;

Bitmap result = pool.get(source.getWidth(), source.getHeight(), Bitmap.Config.ARGB_8888);
if (result == null) {
result = Bitmap.createBitmap(source.getWidth(), source.getHeight(), Bitmap.Config.ARGB_8888);
}

Canvas canvas = new Canvas(result);
Paint paint = new Paint();
paint.setShader(new BitmapShader(source, BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP));
paint.setAntiAlias(true);
RectF rectF = new RectF(0f, 0f, source.getWidth(), source.getHeight());
canvas.drawRoundRect(rectF, radius, radius, paint);
return result;
}

@Override
public String getId() {
return getClass().getName();
}
}

}

3. setCompoundDrawablesWithIntrinsicBounds的优势

有两个方法可以设置控件的上下左右图标,分别是:
第一个方法:setCompoundDrawablesWithIntrinsicBounds(Drawable left, Drawable top, Drawable right, Drawable bottom)
第二种方法:setCompoundDrawables(Drawable left, Drawable top, Drawable right, Drawable bottom)

它们的区别是使用第二种方法之前必须已经setBound(Rect)了,api原文如下

Sets the Drawables (if any) to appear to the left of, above, to the right of, and below the text. Use null if you do not want a Drawable there. The Drawables must already have had setBounds(Rect) called.

所以:
如果想手动设置大小的话就要用setCompoundDrawables,事先要给Drawable设置setBounds;
如果按照原有比例大小显示图片就使用setCompoundDrawablesWithIntrinsicBounds

4. 滑动事件冲突的处理

场景:官方比赛数据页签 - 淘汰赛和积分赛数据 → viewpager(水平) + scrollView(垂直) + HorizontalScrollView(水平) / ListView(垂直)
分析:这里的冲突在于水平事件里面嵌套垂直,再嵌套水平和垂直事件
解决:采用内部拦截法配合getParent().requestDisallowInterceptTouchEvent(false);

效果图如下:

这篇日志也记录了Android艺术探索 第3章 View的事件体系

5. shape资源整理

android:shape=[“rectangle” | “oval” | “line” | “ring”]
shape的形状,默认为矩形,可以设置为矩形(rectangle)、椭圆形(oval)、线性形状(line)、环形(ring)下面的属性只有在android:shape=”ring时可用:
android:innerRadius 尺寸,内环的半径。
android:innerRadiusRatio 浮点型,以环的宽度比率来表示内环的半径,例如,如果android:innerRadiusRatio,表示内环半径等于环的宽度除以5,这个值是可以被覆盖的,默认为9.
android:thickness 尺寸,环的厚度
android:thicknessRatio 浮点型,以环的宽度比率来表示环的厚度,例如,如果android:thicknessRatio=”2”, 那么环的厚度就等于环的宽度除以2。这个值是可以被android:thickness覆盖的,默认值是3.
android:useLevel boolean值,如果当做是LevelListDrawable使用时值为true,否则为false.

corners
android:radius 整型半径
android:topLeftRadius 整型左上角半径
android:topRightRadius 整型右上角半径
android:bottomLeftRadius 整型左下角半径
android:bottomRightRadius 整型右下角半径

渐变色
android:startColor 颜色值 起始颜色
android:endColor 颜色值结束颜色
android:centerColor 整型渐变中间颜色,即开始颜色与结束颜色之间的颜色
android:angle 整型渐变角度(PS:当angle=0时,渐变色是从左向右。 然后逆时针方向转,当angle=90时为从下往上。angle必须为45的整数倍)
android:type [“linear” | “radial” | “sweep”] 渐变类型(取值:linear、radial、sweep)
linear 线性渐变,这是默认设置
radial 放射性渐变,以开始色为中心。
sweep 扫描线式的渐变。
android:useLevel [“true” | “false”]如果要使用LevelListDrawable对象,就要设置为true。设置为true无渐变。false有渐变色
android:gradientRadius 整型渐变色半径.当 android:type=”radial” 时才使用。单独使用 android:type=”radial”会报错。
android:centerX 整型渐变中心X点坐标的相对位置
android:centerY 整型渐变中心Y点坐标的相对位置

描边
android:width 整型描边的宽度
android:color 颜色值描边的颜色
android:dashWidth 整型表示描边的样式是虚线的宽度, 值为0时,表示为实线。值大于0则为虚线。
android:dashGap 整型表示描边为虚线时,虚线之间的间隔 即“ - - - - ”

6. 记录bug:Manifest merger failed error

由于build.gradle文件中的defaultConfig中的信息和其他module中的不同。检查minSdkVersion、targetSdkVersion是否和其他module一致

7. 整理ripple

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/txt_ed4d4d"><!-- 波纹颜色 -->

<!-- 定义一个带圆角的背景 -->
<item>
<shape android:shape="rectangle">
<!-- 正常状态下的颜色 -->
<solid android:color="@color/txt_blue" />

<stroke
android:width="2dp"
android:color="@color/txt_ed4d4d" />

<corners
android:bottomLeftRadius="0dp"
android:bottomRightRadius="0dp"
android:topLeftRadius="8dp"
android:topRightRadius="8dp" />

</shape>
</item>

</ripple>

8. 整理fragment universalAdapter notify不成功的原因

nofityDataSetChange不成功的原因:
1、数据源没有更新,调用notifyDataSetChanged无效。
2、数据源更新了,但是它指向新的引用,调用notifyDataSetChanged无效。
3、数据源更新了,但是adpter没有收到消息通知,无法动态更新列表。

由于在Fragment中调用notifyDataSetChange老是不成功,所以决定好好的研究研究为何UniversalAdapter会引起此问题。

其实好好的理解上面三条之后,解决ListView刷新不显示的问题,肯定妥妥的。

总结起来就是:数据源的引用一定不能变,但是塞到数据源中的数据一定要更新。牢牢地掌握好这一条之后,ListView刷新不显示的问题就迎刃而解了。

解决“数据源的引用一定不能变”的问题,可以提前创建一个List对象, mData = new ArrayList<>(); 之后一直对这个mData倒腾即可,不要再引入新的数据源,也就是不要再改变mData的引用地址。

解决“但是塞到数据源中的数据一定要更新”的问题,可以通过更新数据完成,万一数据源就是没有更新,又想要塞到数据源中,可以通过
mData.clear();
mData.addAll(data);
方法实现。

这次bug的产生就是由于没有事先创建一个mData的对象,直接将入参赋值给mData,导致执行
mData.clear();
mData.addAll(data);
时,先清空了数据集,然后将空数据集添加到了mData中,此时再去调用notifyDataSetChanged()时,其实生效了,但是数据为0,当然就展示了空白页。

同时需要再次调用setAdapter()方法。

通过这次的bug解决,自己认识到遇到问题时,最好追根究底的去解决它。因为你一定会不止一次遇到这个问题。当你彻底理解和掌握了之后,下次再遇到同样的问题时,显然就不是问题了。

解决方法截图:


9. 记录bug:recyclerView在fragment中 notifyDataSetChange不起作用的问题

现象:在创建一口价时,activity中放置了两个fragment,选择图片时,跳转到另外一个界面,返回时,又重新创建了fragment
分析:由于采用ViewPager放置两个fragment,在每次切换的是否都创建了新的fragment,导致每次选好照片之后回到viewpager时,又重新创建了新的fragment,所以notifyDataSetChange无效。
解决:两个fragment用全局变量记录保存,不要每次创建新的。
后续:在adapter.getlist.size 返回0时,就应该联想到是否fragment被重新创建了。以此记录

10. EventBus粘性事件

EventBus默认支持一条事件总线,通常是通过getDefault()方法获取EventBus实例,但也能通过直接new EventBus这种最简单的方式获取多条事件总线,彼此之间完全分开。例子见com.example.leeeyou.fixmyproblem.Problem05_EventBusActivity

EventBus#Register()其实只做了三件事:

  1. 查找订阅者所有的订阅事件
  2. 将订阅事件作为key,所有订阅了此订阅事件的订阅者作为value存放进subscriptionsByEventType
  3. 将订阅者作为key,订阅者的所有订阅事件作为value存放进typesBySubscriber

EventBus#Post()也只做了三件事:

  1. 根据订阅事件在subscriptionsByEventType中查找相应的订阅者
  2. 分发订阅者的订阅事件调用线程
  3. 通过反射调用订阅者的订阅事件

粘性事件:发送事件之后再订阅该事件也能收到该事件,跟粘性广播类似。简单来说就是能够收到订阅之前发送的消息。

11. 事件传递机制

① 假设最高层View叫OuterLayout,中间层View叫InnerLayout,最底层View叫MyVIew。调用顺序是这样的(假设各个函数返回的都是false)
OuterLayout.onInterceptTouchEvent->InnerLayout.onInterceptTouchEvent->MyView.onTouchEvent->InnerLayout.onTouchEvent->OuterLayout.onTouchEvent。

② 内部拦截法,子控件拦截父控件事件

③ 注意点
一个view一旦拦截一个某个事件,当前事件所在的完整事件序列将都会由这个view去处理,反应在真实的代码中,就是一旦view拦截了down事件,那么此后的move和up事件都将不调用onInterceptTouchEvent,而直接由它处理,这就也意味着在onInterceptTouchEvent处理事件是不合适的,因为有可能来了事件,却直接跳过onInterceptTouchEvent方法。这个也意味着,一旦一个ViewGroup没有拦截ACTION_DOWN,那么这个事件序列的其他Action,它都将收不到,所以在处理ACTION_DOWN的时候,尤其需要谨慎。

④ 注意点
onTouchEvent中是要判断MotionEvent的Action,因为一次点击操作就会调用两次onTouchEvent方法,一次是ACTION_DOWN,一次是ACTION_UP,如果手滑一下,还会有若干个ACTION_MOVE

⑤问题:TextView的onTouchEvent的返回值也是True吗?
是的,那为什么点在TextView上面还是能触发它的父视图的onTouchEvent,理论上应该是TextView消耗掉这次的事件,不回传。理论上确实是这样,但是因为TextView的clickable和longClickable属性都是false,当这两个属性都为false的时候,是不会消耗事件的,所以TextView不会消耗事件,这也就可以解释为什么把一个TextView放在一个Button上面,然后点击TextView还是能触发Button的点击事件

⑥问题:view的enable状态和onTouchEvent之间的关系
它们之间没有关系,只有clickable状态才对onTouchEvent有影响的,还有一点 ,设置 view的enable为false确实也会把view的clickable设成false,但是设置view的onclickListener就又把view的clickable变成了true,所以最后的解决方案就是把那两行代码换下先后顺序,问题就迎刃而解了。

⑦问题:onTouchListener OnTouchEvent OnClickListener
onTouchListener是在onTouch方法中生效,而且onTouch要先于onTouchEvent,就是说一旦设置了onTouchListener并且最后onTouch方法返回了True,那onTouchEvent将不会再被执行。而onClickListener和onTouchEvent有些关系,onTouchEvent的默认实现里会调用onClickListener的onClick方法,如果重写了onTouchEvent,因为onClickListener接受不到ACTION_DOWN和ACTION_UP,那么再设置onClickListener也就不会再生效了,这个时候的单击或者长按处理只能在onTouchEvent中自己处理。

12. patch

为什么叫9patch呢?Patch的中文意思是”片,块”的意思,那这里按中文的意思来说就是9片或9块.因此可想而知这个图片会被分为9片,如下图片所示:

工具栏中的Show patches选中,中间紫色的区域就是拉伸区域。
工具栏中的Show content选中,看见蓝色的区域,这片区域就是显示内容的区域;比如说:这个图片宽有30px,我们把下面的那一条线的横向的第20px到25px画上了黑点,那么这个图片设置成某个组件的背景后,这个组件的paddingleft就会设置成20dp,paddingRight就会设置成5dp,如果再在布局文件里面设置这两个值,那个这里画的黑点就不起作用了。

工具栏中的Show bad patches选中,可能会出现下图效果:

其中被选中的3块不符合要求。这里它是根据什么来判断这个绘制的结果不符合要求呢?怎么就认为这三块不符合要求呢?它是根据左侧的黑色的小点所对过来的部分里面的每一个像素点的颜色是否一样。如果像素存在差异,当背景变大的时候就有两种颜色要被重复的绘制,系统就不知道到底绘制这两种颜色哪一种多一点,因此这个工具建议被拉伸的区域只能选择一种颜色。

如果对结果要求比较高的,右下角坐标显示区域就起作用了。

13. Fragment的使用总结

1、replace和add方式的区别

replace 是先remove掉相同containerViewId的所有fragment,然后在add当前的这个fragment。

Replace an existing fragment that was added to a container. This is essentially the same as calling remove(Fragment) for all currently added fragments that were added with the same containerViewId and then add(int, Fragment, String) with the same arguments given here.

add 是把一个fragment添加到一个容器 container 里。

Add a fragment to the activity state. This fragment may optionally also have its view (if Fragment.onCreateView returns non-null) into a container view of the activity.

而至于返回键,这个跟事务有关,跟使用add还是replace没有任何关系。

2、采用replace时的生命周期流转

加载fragment
09-09 17:17:40.906 E: LifeCycle01Fragment onAttach
09-09 17:17:40.906 E: LifeCycle01Fragment onCreate
09-09 17:17:40.911 E: LifeCycle01Fragment onCreateView
09-09 17:17:40.912 E: LifeCycle01Fragment onViewCreated
09-09 17:17:40.912 E: LifeCycle01Fragment onActivityCreated
09-09 17:17:40.912 E: LifeCycle01Fragment onStart
09-09 17:17:40.912 E: LifeCycle01Fragment onResume

屏幕灭掉
09-09 16:37:30.910 E: LifeCycle01Fragment onPause
09-09 16:37:30.953 E: LifeCycle01Fragment onSaveInstanceState
09-09 16:37:30.953 E: LifeCycle01Fragment onStop

点亮屏幕
09-09 16:38:05.967 E: LifeCycle01Fragment onStart
09-09 16:38:05.976 E: LifeCycle01Fragment onResume

切换到其他的fragment
09-09 16:38:30.691 E: LifeCycle01Fragment onPause
09-09 16:38:30.691 E: LifeCycle01Fragment onStop
09-09 16:38:30.691 E: LifeCycle01Fragment onDestroyView
09-09 16:38:30.691 E: LifeCycle01Fragment onDestroy
09-09 16:38:30.691 E: LifeCycle01Fragment onDetach

切换回本身(相当于重新加载fragment)
09-09 17:19:19.368 E: LifeCycle01Fragment onAttach
09-09 17:19:19.368 E: LifeCycle01Fragment onCreate
09-09 17:19:19.371 E: LifeCycle01Fragment onCreateView
09-09 17:19:19.372 E: LifeCycle01Fragment onViewCreated
09-09 17:19:19.372 E: LifeCycle01Fragment onActivityCreated
09-09 17:19:19.372 E: LifeCycle01Fragment onStart
09-09 17:19:19.372 E: LifeCycle01Fragment onResume

回到桌面
09-09 16:39:49.689 E: LifeCycle01Fragment onPause
09-09 16:39:49.803 E: LifeCycle01Fragment onSaveInstanceState
09-09 16:39:49.803 E: LifeCycle01Fragment onStop

回到应用
09-09 16:40:10.743 E: LifeCycle01Fragment onStart
09-09 16:40:10.743 E: LifeCycle01Fragment onResume

退出应用
09-09 16:44:06.357 E: LifeCycle01Fragment onPause
09-09 16:44:06.662 E: LifeCycle01Fragment onStop
09-09 16:44:06.663 E: LifeCycle01Fragment onDestroyView
09-09 16:44:06.663 E: LifeCycle01Fragment onDestroy
09-09 16:44:06.663 E: LifeCycle01Fragment onDetach

采用replace方式的生命周期相对比较简单,因为replace的机制是remove掉相同containerViewId的fragment,再重新加载一把fragment。

3、采用add - show - hide方式的生命周期流转

加载fragment
09-09 17:14:04.440 E: LifeCycle01Fragment onAttach
09-09 17:14:04.440 E: LifeCycle01Fragment onCreate
09-09 17:14:04.445 E: LifeCycle01Fragment onCreateView
09-09 17:14:04.446 E: LifeCycle01Fragment onViewCreated
09-09 17:14:04.446 E: LifeCycle01Fragment onActivityCreated
09-09 17:14:04.446 E: LifeCycle01Fragment onStart
09-09 17:14:04.446 E: LifeCycle01Fragment onResume

屏幕灭掉
09-09 16:37:30.910 E: LifeCycle01Fragment onPause
09-09 16:37:30.953 E: LifeCycle01Fragment onSaveInstanceState
09-09 16:37:30.953 E: LifeCycle01Fragment onStop

点亮屏幕
09-09 16:38:05.967 E: LifeCycle01Fragment onStart
09-09 16:38:05.976 E: LifeCycle01Fragment onResume

切换到其他的fragment
没有执行到生命周期方法

切回本身(addToBackStack的情况下)
09-09 17:14:53.309 E: LifeCycle01Fragment onPause
09-09 17:14:53.309 E: LifeCycle01Fragment onStop
09-09 17:14:53.309 E: LifeCycle01Fragment onDestroyView
09-09 17:14:53.310 E: LifeCycle01Fragment onDestroy
09-09 17:14:53.310 E: LifeCycle01Fragment onDetach

回到桌面
09-09 17:15:50.753 E: LifeCycle01Fragment onPause
09-09 17:15:50.855 E: LifeCycle01Fragment onSaveInstanceState
09-09 17:15:50.855 E: LifeCycle01Fragment onStop

回到应用
09-09 17:16:12.903 E: LifeCycle01Fragment onStart
09-09 17:16:12.903 E: LifeCycle01Fragment onResume

退出应用
09-09 17:16:30.815 E: LifeCycle01Fragment onPause
09-09 17:16:30.815 E: LifeCycle01Fragment onStop
09-09 17:16:30.815 E: LifeCycle01Fragment onDestroyView
09-09 17:16:30.816 E: LifeCycle01Fragment onDestroy
09-09 17:16:30.816 E: LifeCycle01Fragment onDetach

这种方式有个值得注意的地方是切换到其他framgent时,并没有执行生命周期。❓

与Activity生命周期的对比

对于replace和add方式的选择,官方文档解释说:replace()这个方法只是在上一个Fragment不再需要时采用的简便方法。正确的切换方式是add(),切换时hide(),add()另一个Fragment;再次切换时,只需hide()当前,show()另一个。这样就能做到多个Fragment切换不重新实例化。

fragment的生命周期与activity的生命周期的一个关键区别就在于:fragment的生命周期方法是由托管acitivity而不是操作系统调用的。操作系统无从知晓activity用来管理视图的fragment。fragment的使用是activity自己内部的使用。

4、addToBackStack
对于是否要加transaction.addToBackStack(null);也就是将Fragment加入到回退栈。官方的说法是取决于你是否要在回退的时候显示上一个Fragment。

14. Math中对于小数的处理

09-11 10:18:14.193 I: Math.ceil(109.82934) = 110.0
09-11 10:18:14.193 I: Math.floor(109.82934) = 109.0
09-11 10:18:14.193 I: Math.round(109.82934) = 110
09-11 10:18:14.194 I: DecimalFormat 0.00 会四舍五入小数部分, 109.82964 约等于 109.83
09-11 10:18:14.194 I: DecimalFormat #.00 会四舍五入小数部分, 109.82964 约等于 109.83
09-11 10:18:14.195 I: String format %.2f 会四舍五入小数部分, 109.82964 约等于 109.83
09-11 10:18:14.196 I: BigDecimal 3位小数 BigDecimal.ROUND_HALF_UP 会四舍五入小数部分, 109.82964 约等于 109.830
09-11 10:18:14.196 I: BigDecimal 3位小数 BigDecimal.ROUND_DOWN 直接舍弃小数部分, 109.82964 约等于 109.829
09-11 10:18:14.209 I: float型转json : {“name”:”Jack”,”score”:0.01}

15. 开发经验整理

① 对于图片的处理,务必考虑使用缩略图
② 类似订单状态和货架状态的需求,在有很多个状态的情况下,在开发之前最好整理各个状态对应的操作和文案;同时尽量做成一个Activity,避免跳转的时候跳转多界面的处理,同时对外的入参最好也只依赖一个orderId或者goodsId之类的,不要传递完整的对象,因为有可能其他模块提供不了完整对象。
③ 订单的多个状态下,写了多个activity去匹配,这样的缺点是当有消息推送时,不能根据一个orderId准确的知道需要跳转到哪个界面去。

16. ExpandableListView

① 更改箭头的位置
调用setGroupIndicator(null);可以隐藏指示器

可以在getGroupView中根据isExpanded来动态显示上箭头和下箭头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
public View getGroupView(final int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) {

...

if (!isExpanded) {
Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), R.mipmap.icon_arrow_down_grey);
BitmapDrawable bitmapDrawable = new BitmapDrawable(context.getResources(), bitmap);
groupViewHolder.tv_game_schedule_num.setCompoundDrawablesWithIntrinsicBounds(null, null, bitmapDrawable, null);
} else {
Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), R.mipmap.icon_arrow_up_grey);
BitmapDrawable bitmapDrawable = new BitmapDrawable(context.getResources(), bitmap);
groupViewHolder.tv_game_schedule_num.setCompoundDrawablesWithIntrinsicBounds(null, null, bitmapDrawable, null);
}

return convertView;
}

② BaseExpandableListAdapter点击时,影响其他item的问题
问题:造成此问题的原因是setGroupViewListener方法中先去获取了ExpandableListView当前的收起/展开状态,然后根据此状态再去调用collapseGroup或expandGroup,最后立即通过ExpandableListView.isGroupExpanded获取此时的收起/展开状态,此时的状态是不准确的。

解决方案:在调用collapseGroup或expandGroup方法之后,监听onGroupExpanded和onGroupCollapsed方法,在其中调用notifyDataSetChanged();去刷新界面,然后在getGroupView中根据isExpanded去动态改变UI的上箭头还是下箭头。

③ BaseExpandableListAdapter notifyDataSetChange()的问题
问题:调用notifyDataSetChange()无效

分析:开始以为是notifyDataSetChange没有刷新造成的,后台debug跟进到Adapter的getGroupView时,发现调用了getGroupView方法,也就是系统API执行了刷新操作,只是getGroupView中自己的逻辑处理有问题。当前如果是没有数据的情况,getGroupView展示的是item_no_data_layout布局,但是如果有数据进来,调用notifyDataSetChange时,会执行到getGroupView,此时会复用item_no_data_layout布局,但是此布局并不是我们的数据展示布局,所以要再加一层判断,如viewHolder==null,需要再加载一次item_offical_game布局到convertView

解决方案:双层判断viewHolder==null,如果为null,需要再加载一次item_offical_game布局到convertView

17. JSONArray ,JSONObject , json相互转换

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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
/**
* jsonobject , jsonarray , json 互转
*/
public class Problem10_JSONArray_Activity extends BaseActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_jsonarray);

jsonStringTojsonObj();
jsonArrayTojsonObj();
jsonObjTojsonObj();
listobjTojsonArray();
}

private void listobjTojsonArray() {
List<String> imageList = new ArrayList<>();
imageList.add("http://quncao-app.b0.upaiyun.com/51c1e24b6b821c916f732c2aa5b1dc9f.jpg");
imageList.add("http://quncao-app.b0.upaiyun.com/8ad331112d30453d2ace4c31903c5c55.jpg");
imageList.add("http://quncao-app.b0.upaiyun.com/839f7a8407047bab2d7a721c114912df.jpg");
imageList.add("http://quncao-app.b0.upaiyun.com/369ca74245ebc32d512eae501e7de807.jpg");

try {
JSONArray jsonArray = new JSONArray(new Gson().toJson(imageList));

Log.e("com.jsonarray", "listobjTojsonArray Gson ---- " + jsonArray.toString());

JSONArray jsonArray2 = new JSONArray(imageList);
Log.e("com.jsonarray", "listobjTojsonArray 直接转换 ---- " + jsonArray2.toString());

Log.e("com.jsonarray", "-------------------- ");
} catch (JSONException e) {
e.printStackTrace();
}
}

private void jsonObjTojsonObj() {
JSONArray jsonArray = new JSONArray();
jsonArray.put("http://quncao-app.b0.upaiyun.com/51c1e24b6b821c916f732c2aa5b1dc9f.jpg");
jsonArray.put("http://quncao-app.b0.upaiyun.com/8ad331112d30453d2ace4c31903c5c55.jpg");
jsonArray.put("http://quncao-app.b0.upaiyun.com/839f7a8407047bab2d7a721c114912df.jpg");
jsonArray.put("http://quncao-app.b0.upaiyun.com/369ca74245ebc32d512eae501e7de807.jpg");

JSONObject jsonObject = new JSONObject();
try {
jsonObject.put("imageList", jsonArray);
jsonObject.put("age", 28);
jsonObject.put("nickName", "Jack");
} catch (JSONException e) {
e.printStackTrace();
}

//直接调用toString可以将JSONObject转换成json字符串
Log.e("com.jsonarray", "jsonObjTojsonObj ---- " + jsonObject.toString());

Log.e("com.jsonarray", "jsonObjTojsonObj Gson ---- " + new Gson().toJson(jsonObject));

Log.e("com.jsonarray", "-------------------- ");
}

private void jsonArrayTojsonObj() {
JSONArray jsonArray = new JSONArray();
jsonArray.put("http://quncao-app.b0.upaiyun.com/51c1e24b6b821c916f732c2aa5b1dc9f.jpg");
jsonArray.put("http://quncao-app.b0.upaiyun.com/8ad331112d30453d2ace4c31903c5c55.jpg");
jsonArray.put("http://quncao-app.b0.upaiyun.com/839f7a8407047bab2d7a721c114912df.jpg");
jsonArray.put("http://quncao-app.b0.upaiyun.com/369ca74245ebc32d512eae501e7de807.jpg");

//直接调用toString可以将JSONArray转换成json字符串
Log.e("com.jsonarray", "jsonArrayTojsonObj ---- " + jsonArray.toString());

Log.e("com.jsonarray", "-------------------- ");
}

private void jsonStringTojsonObj() {
String imageObject = "{\"imageList\":[{\"id\": 292,\"imageUrl\": \"http://quncao-app.b0.upaiyun.com/51c1e24b6b821c916f732c2aa5b1dc9f.jpg\"},{\"id\": 289,\"imageUrl\": \"http://quncao-app.b0.upaiyun.com/8ad331112d30453d2ace4c31903c5c55.jpg\"},{\"id\": 288,\"imageUrl\": \"http://quncao-app.b0.upaiyun.com/839f7a8407047bab2d7a721c114912df.jpg\"},{\"id\": 291,\"imageUrl\": \"http://quncao-app.b0.upaiyun.com/369ca74245ebc32d512eae501e7de807.jpg\"}]}";
FixedPrice fixedPrice = new Gson().fromJson(imageObject, FixedPrice.class);
Log.e("com.jsonarray", "jsonStringTojsonObj ---- " + fixedPrice.toString());

String imageArray = "[{\"id\": 292,\"imageUrl\": \"http://quncao-app.b0.upaiyun.com/51c1e24b6b821c916f732c2aa5b1dc9f.jpg\"},{\"id\": 289,\"imageUrl\": \"http://quncao-app.b0.upaiyun.com/8ad331112d30453d2ace4c31903c5c55.jpg\"},{\"id\": 288,\"imageUrl\": \"http://quncao-app.b0.upaiyun.com/839f7a8407047bab2d7a721c114912df.jpg\"},{\"id\": 291,\"imageUrl\": \"http://quncao-app.b0.upaiyun.com/369ca74245ebc32d512eae501e7de807.jpg\"}]";
List<Image> imageList = new Gson().fromJson(imageArray, new TypeToken<List<Image>>() {
}.getType());

Log.e("com.jsonarray", "jsonStringTojsonObj gson fromJson ---- " + fixedPrice.toString());
Log.e("com.jsonarray", "jsonStringTojsonObj TypeToken ---- " + imageList.toString());

Log.e("com.jsonarray", "-------------------- ");
}

class Image {
public int id;
public String imageUrl;

@Override
public String toString() {
return new Gson().toJson(this);
}
}

class FixedPrice {
public List<Image> imageList;

@Override
public String toString() {
return new Gson().toJson(this);
}
}

}

18. 记录bug:Fragment already active

现象
反复进入退出进入退出某一个fragment界面导致
分析
在 Fragment 没有被添加到 FragmentManager 之前,我们可以通过 Fragment.setArguments() 来设置参数,并在 Fragment 中,使用 getArguments() 来取得参数。在 Fragment 被添加到 FragmentManager 后,一旦被使用,我们再次调用 setArguments() 将会导致 java.lang.IllegalStateException: Fragment already active 异常。
解决
1、可以在add()方法时候,先判断currentFragment.isAdded();
2、可以使用setter和getter Fragment的属性方法进行数据的存储和获取;

19. 记录bug:调用fragment的replace加载显示异常的问题

现象
现有FragmentA和FragmentB,当前显示FragmentA,用fragmentTransaction调用replace加载FragmentB时,可能会出现加载不成功的情况,显示的还是FragmentA
分析
使用replace会带来一个问题,FragmentA在replace后会被销毁,会调用其生命周期函数(onDestoryView()、onPause()、onDestory())。如果频繁地replace Fragment会不断创建新实例并销毁旧的,无法重用。经过多次切换后,会导致Fragment上的View无法加载的问题,此时就会出现点击切换图标,还是显示FragmentA
解决
可以利用add()方法配合show()和hide()来弥补replace带来的低效问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

private void hideFragments(FragmentTransaction transaction) {
if (indexFragment != null) {
transaction.hide(indexFragment);
}
if (newVenueFragment != null) {
transaction.hide(newVenueFragment);
}
if (sportVenueFragment != null) {
transaction.hide(sportVenueFragment);
}
if (messageMainFragment != null) {
transaction.hide(messageMainFragment);
}
if (mineFragment != null) {
transaction.hide(mineFragment);
}
}

//再调用如下方法来显示你要替换的fragment
fragmentTransaction.show(indexFragment);

20. ScrollView中嵌套ListView时,ListView抢夺焦点问题

ListView嵌入到可滚动的控件中时,ListView会抢夺页面的焦点。对于此类问题的终极解决方案是:使用属性 descendantFocusabilit
android:descendantFocusability属性共有三个取值
1 beforeDescendants:viewgroup 会优先其子类控件而获取到焦点
2 afterDescendants: viewgroup 只有当其子类控件不需要获取焦点时才获取焦点
3 blocksDescendants:viewgroup 会覆盖子类控件而直接获得焦点

1
2
3
4
5
6
7
<com.quncao.pulltorefreshlib.PullToRefreshScrollView
android:id="@+id/pullToRefreshScrollView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#e8e8e8"
android:descendantFocusability="blocksDescendants"
android:orientation="vertical">

21. ScrollView MATCH_PARENT 无法填满屏幕

在ScrollView中嵌套一个RelativeLayout,并设置MATCH_PARENT给RelativeLayout。此时想在屏幕底部放置一个Button会出现无法正确的固定到底部。
通过设置ScrollView的android:fillViewport为true可以解决此问题。

当ScrollView未设置fillViewport=“true”时, 里面的元素(比如LinearLayout)会按照wrap_content来计算(不论它是否设了”match_parent”),而如果LinearLayout的元素设置了match_parent,那么也是不管用的。因为LinearLayout依赖里面的元素,而里面的元素又依赖LinearLayout,这样自相矛盾。所以里面元素设置了match_parent,也会当做wrap_content来计算。

22. 判断RecyclerView是否可以垂直滚动

1
2
3
4

RecyclerView.canScrollVertically(1)的值表示是否能向上滚动,false表示已经滚动到底部
RecyclerView.canScrollVertically(-1)的值表示是否能向下滚动,false表示已经滚动到顶部

下面这段代码是在PtrFrameLayout中嵌套RecyclerView的例子

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

ptrFrame.setPtrHandler(new PtrHandler() {
@Override
public void onRefreshBegin(PtrFrameLayout frame) {
onRefreshBefore();

fetchDataFromServer();
}

@Override
public boolean checkCanDoRefresh(PtrFrameLayout frame, View content, View header) {
// 默认实现,根据实际情况做改动

return !mRecyclerView.canScrollVertically(-1);
}
});

23. LoaderManager的使用

1.Loader特性:
(1).对于每个Activity或者Fragment都可用
(2).提供异步加载数据
(3).监视数据资源,当内容改变时重新更新
(4).当配置改变时,自动重新连接最新的cursor,故不需要重新查询数据

2.Loader相关类接口
(1).LoaderManager
对于每个activity或者fragment只存在一个与之相关的LoaderManager对象,该LoaderManager对象可以存在多个可供管理loader对象。
(2).LoaderManager.LoaderCallbacks
LoaderManager.LoaderCallbacks是个回掉接口,用于客户端与LoaderManager的交互,loader对象就是在其接口的onCreateLoader()方法中得到,在使用时需要覆盖其方法。
(3).CursorLoader
CursorLoader是AsyncTaskLoader的子类,通过它可以查询ContentResolver并返回一个Cursor对象,并使用该cursor对象在后台线程执行查询操作,以不至于会阻塞主线程,从一个内容提供者去异步加载数据是CursorLoader对象最大用处。

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

...

public class GalleryFragment extends Fragment implements LoaderManager.LoaderCallbacks<Cursor> {
private GalleryAdapter mGalleryAdapter;
private ImageLoader imageLoader;

...

/**
* 切换目录
*
* @param dirId 目录id
* @param dirName 目录名
*/
public void switchDir(int dirId, String dirName) {
Bundle bundle = new Bundle();
bundle.putInt("dirId", dirId);
getLoaderManager().restartLoader(0, bundle, this);
title.setText(dirName);
}

@Override
public void onStop() {
super.onStop();
imageLoader.stop();
}

@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
String select = null;
if (args != null) {
dirId = args.getInt("dirId", 0);
if (dirId != 0) {
select = String.format("%s == %s", MediaStore.Images.Media.BUCKET_ID, dirId);
}
}

Uri baseUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;

return new CursorLoader(getActivity(),
baseUri,
new String[]{MediaStore.Images.Media._ID, MediaStore.Images.Media.DATA,},
select,
null,
MediaStore.Images.Media.DATE_TAKEN + " DESC"
);
}

@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
mGalleryAdapter.swapCursor(data);
}

@Override
public void onLoaderReset(Loader<Cursor> loader) {
mGalleryAdapter.swapCursor(null);
}


}


24. 不透明度16进制值

不透明度 16进制值
100% FF
95% F2
90% E6
85% D9
80% CC
75% BF
70% B3
65% A6
60% 99
55% 8C
50% 80
45% 73
40% 66
35% 59
30% 4D
25% 40
20% 33
15% 26
10% 1A
5% 0D
0% 00

25. 髙扇入低扇出

在软件设计中,扇入和扇出的概念是指应用程序模块之间的层次调用情况。按照结构化设计方法,一个应用程序是由多个功能相对独立的模块所组成。扇入:是指直接调用该模块的上级模块的个数。扇入大表示模块的复用程序高。

扇出:是指该模块直接调用的下级模块的个数。扇出大表示模块的复杂度高,需要控制和协调过多的下级模块;但扇出过小(例如总是1)也不好。扇出过大一般是因为缺乏中间层次,应该适当增加中间层次的模块。扇出太小时可以把下级模块进一步分解成若干个子功能模块,或者合并到它的上级模块中去。

设计良好的软件结构,通常顶层扇出比较大,中间扇出小,底层模块则有大扇入。

26. What-How-Why

  • What - “What is it?” 你要搞清楚某个东东是【什么】样子的?有【什么】用处?有【什么】特性?有【什么】语法?
  • How - “How to do?” 你要搞清楚某个东西,其内部是【如何】运作的?【如何】实现的?
  • Why - 就是搞清楚某个东西【为什么】设计成这样?【为什么】不是另外的样子?这样的设计有什么讲究?

27. 网络框架应具备的功能

27.1. OkHttp

OkHttp3升级实践与之前2.0对比

  • 提供一个合适的辅助类
  • 基本的get,post请求
  • 基本的同步和异步请求
  • 下载文件以及进度回调
    • 对于超过1MB的响应body,应使用流的方式来处理body。
  • 取消某个网络请求
  • OkHttp参数配置
  • 配置日志打印拦截器
  • 配置网络连接拦截器
  • 参考 https://github.com/hongyangAndroid/okhttp-utils
  • 单例方式提供OkHttpClient

27.2. Retrofit

  • 使用原生的Call类型处理返回结果
  • 使用RxJava方式处理返回结果
  • 解决Https访问的问题,SSLSocketFactory
  • 解决直接返回String类型数据的问题,StringConverterFactory http://www.jianshu.com/p/308f3c54abdd
  • 配置统一的OkHttpClient给Retrofit
  • 处理先后访问两个网络请求的情况[需求:获取某人的某个项目下所有关注者自己的所有项目列表]
  • 下载jar包
  • 配置网络连接拦截器
  • 对返回结果再封装