注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

高效开发:分享 `extension` 有趣的用法

前言 extension 可以在不更改类或创建子类的情况下,向类添加扩展功能的一种方式。灵活使用 extension 对基础类进行扩展,对开发效率有显著提升。 举个栗子🌰,对 int 类型扩展 小轰在开发项目中碰到需求:将单位为分的数值转换成单位为元的字符串 ...
继续阅读 »

前言


extension 可以在不更改类或创建子类的情况下,向类添加扩展功能的一种方式。灵活使用 extension 对基础类进行扩展,对开发效率有显著提升。


举个栗子🌰,对 int 类型扩展


小轰在开发项目中碰到需求:将单位为分的数值转换成单位为元的字符串


/// 通常的写法,封装转换方法

///封装方法:金额转字符串 保留两位小数
String convertPointToUnit(int num){
return (num.toDouble() / 100).toStringAsFixed(2);
}

///使用
void main(){
int num = 100;
var result = convertPointToUnit(num);
print(result); //打印结果为 1.00
}

同样的功能,使用 extension 进行开发,会更加简洁,如下:


/// 使用 extension 对 int 类进行扩展,添加方法 moneyString
extension ExInt on int {
/// 金额转字符串 保留两位小数
/// 100 => 1.00
String get moneyString => (this.toDouble() / 100).toStringAsFixed(2);
}

import ../ExInt.dart;
///使用
void main(){
int num = 100;
print(num.moneyString);
}

扩展后,直接作为该类型的成员方法来被使用。extension 就像是基因赋值,直接将能力(方法)对宿主进行赠与。


各种场景的扩展演示



  • 对枚举进行扩展实现


enum FruitEnum { apple, banana }

extension ExFruitEnum on FruitEnum {
String get name {
switch (this) {
case FruitEnum.apple:
return "apple";
case FruitEnum.banana:
return "banana";
}
}
}

///字符串匹配枚举
FruitEnum generateFruit (String fruitType){
if(fruitType == FruitEnum.apple.name){
return FruitEnum.apple;
} else if(fruitType == FruitEnum.banana.name){
return FruitEnum.banana;
}
}


  • 扩展作用于泛型:


//扩展list的方法
extension ExList<T> on List<T> {
//扩展操作符
List<T> operator -() => reversed.toList();
//一个链表分割成两个
List<List<T>> split(int at) => <List<T>>[sublist(0, at), sublist(at)];
}


  • 扩展在 Widget 控件中的应用


我们会有类似的控件


Column(
children: <Widget>[
Container(
paddint: const EdgeInsets.all(10)
child: AWidget(),
),
Container(
paddint: const EdgeInsets.all(10)
child: BWidget(),
),
Container(
paddint: const EdgeInsets.all(10)
child: CWidget(),
),
]
)

代码中有很多的冗余对吧?我们用 extension 进行扩展一下:


extension ExWidget on Widget {
Widget paddingAll(double padding) {
return Container(
paddint: const EdgeInsets.all(padding)
child: this,
);
}
}

之后我们就可以改成:


Column(
children: <Widget>[
AWidget().paddingAll(10),
BWidget().paddingAll(10),
CWidget().paddingAll(10),
]
)

作者:李小轰_rex
链接:https://juejin.cn/post/7034439067269857294
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Android程序员如何从设计角度思考HTTPS

typora-root-url: img typora-copy-images-to: img 从设计角度思考HTTPS 我们了解了HTTP协议的内容后,明白HTTP存在很多安全隐患,所以后来推出了安全协议-HTTPS,我们不妨站在设计角度来设计一个安全的HT...
继续阅读 »
typora-root-url: img
typora-copy-images-to: img

从设计角度思考HTTPS


我们了解了HTTP协议的内容后,明白HTTP存在很多安全隐患,所以后来推出了安全协议-HTTPS,我们不妨站在设计角度来设计一个安全的HTTP连接协议,看看HTTP存在哪些问题,我们该如何设计能保证安全性,从而了解HTTPS的安全协议是如何保障的HTTP安全

首先我们需要考虑一下,实现HTTPS所谓的安全,我们需要保证那些地方的安全:


1.首先我们需要保证服务端和客户端之间发送的消息是安全的


2.其次我们要保证服务端和客户端之间的连接是安全的


3.最后我们还要保证服务端不会被其他的伪造客户端连接,并且通过此方式破解加密方式


服务端/客户端信息交互的安全


首先我们先来考虑一下,有什么方法可以保证客户端发送的消息给服务端并且服务端返回结果,这个过程是安全的,大概的过程如下:


image.png


这个时候我们最先想到的方案--加密,我们使用加密算法给数据加密了不就行了吗,那么该选择什么加密算法呢?开发过程中最常见的加密算法如:MD5、SHA1这样的摘要算法或者aes、des这样的对称加密算法,又或者rsa这样的非对称算法,看起来每一种都可以实现数据加密传输,但是我们不要忘记了,第三点中我们希望能保证其他客户端连接不会破解此种加密方式,要知道在互联网中,不是仅仅一台客户端和一个服务端交互,可以有无数台客户端同时与服务端交互,这个时候我们如果要防止别的客户端破解加密,看起来摘要算法这种不可逆的算法刚好合适,但是我们不要忘记了,客户端和服务端需要完成交互的,那么也就是说这个加密不能为不可逆算法,否则客户端也无法对服务端数据进行处理,服务端也无法处理客户端数据,那么只能是对称加密算法或者非对称加密算法能满足了,我们继续思考,如果是多台客户端同时连接服务端,如下图:


image.png


那么似乎哪一种加密都能满足,那么我们不禁有个问题,万一有黑客(恶意机器)拦截了我们的请求,并且充当了中间的传输者,我们的这两种加密算法还安全吗?如下图:


image.png


可以看到,我们的客户端和服务端中间被未知的恶意机器拦截转发了请求,那么我们之前的加密方式如果是直接传递的加密方式和密钥,如果是对称加密那么结局可想而知,对于中间机器来说,依然可以解密出客户端和服务端的消息,对于黑客来说依然是透明的,安全性仅仅比不加密强上一点点,完全不可以称之为可信任的安全协议,那么使用非对称加密呢?我们都知道非对称加密是一堆密钥,每一端持有自己的私钥,对外公开公钥,而公钥加密仅仅使用私钥才可以解密,这样即使有中间机器拦截,也仅仅能拿到客户端和服务端的公钥,但是我们不要忘记了,客户端应该是持有服务端的公钥,用公钥加密传输给服务端,服务端私钥解密,响应的过程即是客户端的私钥解密服务端持有的客户端公钥,中间机器即使拦截了双方的公钥,也无法解密双方公钥自身加密的信息,这样的话,客户端和服务端数据传输安全的问题似乎完美解决了


新隐患-公钥传输方式


刚刚我们经过对比,确定了使用公私钥方式的非对称加密来作为客户端-服务端传输的加密方式,看起来应该高枕无忧了,那么事实真的如此吗?其实和对称加密一样,非对称加密这样直接传输加密,也仅仅是提高了一点点安全性而已,如果遇到的黑客在拦截到客户端的请求后,将自身的公钥传递给服务端以及客户端,而将客户端/服务端的公钥持有会如何?是的,细极思恐,那样中间机器将拥有解密双端消息的能力!为什么会这样?试想一下,客户端使用所谓服务端的公钥加密消息,发送,被中间机器拦截后,这所谓的服务端公钥是中间机器的,那么私钥岂不是可以解密拿到明文信息?然后再伪装使用拦截到的真实的客户端的公钥加密,转发给服务端,同理,服务端的所谓客户端公钥加密对于中间机器完全形同虚设,那么这种问题如何解决呢?我们可不可以更换一种公钥传输方式,尽量绕开中间机器的拦截,保证安全性呢?


我们可以想下,大概有如下两种方法传输公钥:


1.服务端把公钥发送给每一个连接进来的客户端


2.将公钥放到一个地方(比如独立的服务器,或者文件系统),客户端需要获取公钥的时候,访问这个地方的公钥来和服务端进行匹配


而第一个方案,即我们刚刚推翻的方案,很明显会存在被拦截调包的可能,那么似乎我们只能使用第二个方案来传输公钥?那么我们不禁有个问题,即客户端是如何知道存放公钥的远程服务器地址以及认证加密方式,而且每次建立连接都要来获取一次,对服务器的抗压能力也有一定的考验?还有如何保证黑客等恶意访问的用户不能通过此种方式拿到公钥,所以安全也是个比较麻烦的问题


引入第三方CA机构


由于上述提到的问题,所以对于个人而言,如果在开发网站的同时,还要再花费大量金钱和精力在开发公钥服务上,是很不合理的,那么有木有专门做这个的公司,我们托管给这个公司帮我们完成,只需要付出金钱的代价就能体验到服务不可以吗?于是,专门负责证书认证的第三方CA机构出现了,我们只需要提前申请好对应的服务端信息,并且提交对应资料,付出报酬,CA就会给我们提供对应的服务端认证服务,大大减少我们的操作和复杂度,但是这个时候我们不禁又有个问题,CA机构能保证只有客户端拿到认证的证书,并且认证通过,拦截对应的非正常客户端吗?如果不能的话,那岂不是黑客也可以拿到认证?现在的问题开始朝着如何认证用户真伪方向发展了


验证证书有效性


其实想要解决认证的问题,我们可以从生活中寻找一些灵感,我们每个人都有一个唯一的id,证明身份,这样可以保证识别出id和对应的人,也能识别不法分子,那么,既然计算机来源于生活,设计出来的东西也应该遵循正常的逻辑,我们何不给每个证书设置类似id的唯一编号呢?当然计算机是死的,没办法简单的将机器和证书编号进行绑定,那么就需要设计一个符合逻辑的证书验证过程。我们不妨思考下,平时开发的软件为了识别被人篡改的软件,我们是如何做的,相信大脑里有个词会一闪而过,MD5/SHA1(签名)?没错,那么我们证书的认证可否按照这个思路设计?


现在我们假设,客户端拿到证书后,能够从证书上拿到公钥信息、证书签名hash和有效期等信息,也就是说证书内置了计算整个证书的签名hash值,如果此时我们根据客户端的签名算法进行一次加签计算,和证书默认计算好的hash比较,发现不一致,那么就说明证书被修改了,肯定不是第三方发布的正式证书,如果一致,说明证书是真实的,没有被篡改,我们可以尝试与服务端连接了,因为证书拿到了,也有了公钥,后续的就是加密通信的过程了


至此,似乎一个安全的加密https简陋的设计出来了,也似乎解决了这些安全问题,但是不得不提的一点是,我们上面有个很重要的一点,即存放证书的服务器一定要保证安全性,第三方机构算不算绝对安全呢?答案是否定的,因为在https至今的历史上,发生过第三方机构被黑客攻击成功,黑客使用的也是正版的证书的事件,只能说计算机的世界不存在绝对安全,而是相对来说,安全系数提高了太多


HTTPS认证过程


前面我们设计了简陋版的HTTPS,那么,我们接下来看看,正版的HTTPS大体认证过程是如何的,首先我们从申请证书开始:


image.png


可以看到,申请证书的时候,需要提供很多内容,其中域名、签名hash算法、加密算法是最重要的,通过这三项计算生成证书以及确定加密认证算法,并且在这个过程中还需要提供服务端自己的公钥,用来生成证书,CA机构使用自己的私钥加密证书,生成证书传递给服务端


2.证书申请拿到以后,客户端tcp三次握手连接(会携带一个随机数client-random),这个时候服务端将证书信息(包含过期时间、签名算法、当前证书的hash签名、服务端公钥、颁发证书机构等信息)传递给客户端,并且传递一个random随机数


3.客户端收到证书后,使用浏览器内置的CA认证,对证书的颁发机构逐个/逐层校验,确定证书来源正常,并且校验证书过期时间,确定是否可用,最后根据证书的签名算法,计算出对应的签名hash,和证书内置的签名hash比较,确定是否是未篡改的证书,完全认证通过后,证书认证环节结束


4.客户端生成随机对称密钥( pre-master ),将双端随机数组合通过证书的公钥(服务端的公钥)加密后,发送给服务端,服务端收到后,根据双方生成的随机数组合验证击进行http通信


以上就是HTTPS认证的大体流程,另外需要注意的是,HTTPS使用了签名算法(MD5/SHA256等)、对称加密以及非对称加密完成了整个交互过程,在认证过程中仅仅使用了签名算法和非对称加密保证建立通道的安全稳定,在通道建立过程中,维持了一个sessionid,用来防止频繁创建通道大量消耗资源,尽可能保证通道长期连接复用,并且我们需要知道一点,非对称加密虽然安全,但是相比较对称加密,加密解密步骤复杂导致时间会更久,所以HTTPS在建立通道以后,会选择双端协议使用对称加密来完成后续的数据交互,而上述提到的双方的随机对称密钥组合是用来在建立连接后的第一次交互的过程中,二次确认握手过程是否被篡改(客户端把Client.random + sever.random + pre-master组合后使用公钥加密,并且把握手消息根据证书的签名算法计算hash,发送给服务端确认握手过程是否被窜改),完成校验后,确定当前是安全连接后,双端之间就会使用约定好的对称加密算法进行数据加密解密传输,至此一个完整的HTTPS协议完成


相关视频


Android程序员中高级进阶学习/OkHttp原理分析


作者:传道士
链接:https://juejin.cn/post/7036654686371315725
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

做一个短链接系统需要考虑这么多

什么是短链接短链接顾名思义,就是一个比较短的链接(我好像说了个废话),我们平时看到的链接可能长这样:mp.weixin.qq.com/s?biz=MzU5M…又臭又长有没有(没错,这是我的WX公众号链接,可以关注一下),那如果我们需要将某个链接发在某个文章或者...
继续阅读 »



什么是短链接

短链接顾名思义,就是一个比较短的链接(我好像说了个废话),我们平时看到的链接可能长这样:

mp.weixin.qq.com/s?biz=MzU5M…

又臭又长有没有(没错,这是我的WX公众号链接,可以关注一下),那如果我们需要将某个链接发在某个文章或者推广给别人的时候,这么长看着也太不爽了,而短链接的出现就是用一个很短的URL来替代这个很长的家伙,当用户访问短链接的时候,会重定向到原来的链接。比如长下面这样:

sourl.cn/CsTkky

你如果平时有注意的话,各种商业短信上的链接也是会转成特别短的:

img

这个特别短的URL就是短链接。

为什么需要URL短链接

URL短链接用于为长URL创建较短的别名,我们称这些缩短的别名为“短链接”;当用户点击这些短链接时,会被重定向到原始URL;短链接在显示、打印、发送消息时可节省大量空间。

例如,如果我们通过sourl缩短以下URL:

juejin.cn/user/211951…

我们可以得到一个短链接:

sourl.cn/R99fbj

缩短的URL几乎是实际URL大小的三分之一。

URL缩写经常用于优化设备之间的链接,跟踪单个链接以分析受众,衡量广告活动的表现,或隐藏关联的原始URL。

如果你以前没有使用过sourl,可以尝试创建一个新的URL短链接,并花一些时间浏览一下他们的服务提供的各种选项。可以让你更好的理解这篇文章。

系统的要求和目标

在完成一个功能或者开发一个系统时,先确定系统的定位和要达到的目标是一个好的习惯,这可以让你在设计和开发过程中有更清晰的思路。

我们的短链接系统应满足以下要求:

功能要求:

  • 给定一个URL,我们的服务应该为其生成一个较短且唯一的别名,这叫做短链接,此链接应足够短,以便于复制和粘贴到应用程序中;

  • 当用户访问短链接时,我们的服务应该将他们重定向到原始链接;

  • 用户应该能够选择性地为他们的URL选择一个自定义的短链接;

  • 链接可以在指定时间跨度之后过期,用户应该能够指定过期时间。

非功能要求:

  • 系统必须高度可用。如果我们的服务关闭,所有URL重定向都将开始失败。

  • URL重定向的延迟情况应该足够小;

  • 短链接应该是不可猜测的。

扩展要求:

  • 支持分析和统计,例如短链接的访问次数;

  • 其他服务也应该可以通过RESTAPI访问我们的服务。

容量要求和限制

我们的系统将会有很大的访问量。会有对短链接的读取请求和创建短链接的写入请求。假设读写比例为100:1。

访问量预估:

假设我们每个月有5亿个新增短链接,读写比为100:1,我们可以预计在同一时间内有500亿重定向:

100 * 5亿 => 500亿

我们系统的QPS(每秒查询数量)是多少?每秒的新短链接为:

5亿/ (30天 * 24小时 * 3600 秒) ≈ 200 URLs/s

考虑到100:1读写比,每秒URL重定向将为:

100 * 200 URLs/s = 20000/s

存储预估:

假设我们将每个URL缩短请求(以及相关的缩短链接)存储5年。由于我们预计每个月将有5亿个新URL,因此我们预计存储的对象总数将为300亿:

5亿 * 5 年 * 12 月 = 300亿 

假设每个存储的对象大约有500个字节(这只是一个估算值)。我们将需要15TB的总存储:

300亿*500bytes≈15TB

带宽预估:

对于写请求,由于我们预计每秒有200个新的短链接创建,因此我们服务的总传入数据为每秒100KB:

200*500bytes≈100KB/s

对于读请求,预计每秒约有20,000个URL重定向,因此我们服务的总传出数据将为每秒10MB:

20000 * 500 bytes ≈10 MB/s

内存预估:

对于一些热门访问的URL为了提高访问速率,我们需要进行缓存,需要多少内存来存储它们?如果我们遵循二八原则,即20%的URL产生80%的流量,我们希望缓存这20%的热门URL。

由于我们每秒有20,000个请求,因此我们每天将收到17亿个请求:

20000 * 24 * 3600 ≈ 17亿

要缓存这些请求中的20%,我们需要170 GB的内存:

17亿 * 0.2 * 500bytes ≈ 170GB

这里需要注意的一件事是,由于将会有许多来自相同URL的重复请求,因此我们的实际内存使用量可能达不到170 GB。

整体来说,假设每月新增5亿个URL,读写比为100:1,我们的预估数据大概是下面这样:

类型预估数值
新增短链接200/s
短链接重定向20000/s
传入数据100KB/s
传出数据10 MB/s
存储5年容量15 TB
内存缓存容量170 GB

系统API设计

一旦我们最终确定了需求,就可以定义系统的API了,这里则是要明确定义我们的系统能提供什么服务。

我们可以使用REST API来公开我们服务的功能。以下是用于创建和删除URL的API的定义:

创建短链接接口

String createURL(api_dev_key, original_url, custom_alias=None, user_name=None, expire_date=None)
复制代码

参数列表:

api_dev_key:分配给注册用户的开发者密钥,可以根据该值对用户的创建短链接数量进行限制;

original_url:需要生成短链接的原始URL;

custom_alias :用户对于URL自定义的名称;

user_name :可以用在编码中的用户名;

expire_date :短链接的过期时间;

返回值:

成功生成短链接将返回短链接URL;否则,将返回错误代码。

删除短链接接口

String deleteURL(api_dev_key, url_key)
复制代码

其中url_key是表示要删除的短链接字符串;成功删除将返回delete success

如何发现和防止短链接被滥用?

恶意用户可以通过使用当前设计中的所有URL密钥来对我们进行攻击。为了防止滥用,我们可以通过用户的api_dev_key来限制用户。每个api_dev_key可以限制为每段时间创建一定数量的URL和重定向(可以根据开发者密钥设置不同的持续时间)。

数据模型设计

在开发之前完成数据模型的设计将有助于理解各个组件之间的数据流。

在我们短链接服务系统中的数据,存在以下特点:

  • 需要存储十亿条数据记录;

  • 存储的每个对象都很小(小于1K);

  • 除了存储哪个用户创建了URL之外,记录之间没有任何关系;

  • 我们的服务会有大量的读取请求。

我们需要创建两张表,一张用于存储短链接数据,一张用于存储用户数据;

img

应该使用怎样的数据库?

因为我们预计要存储数十亿行,并且不需要使用对象之间的关系-所以像mongoDB、Cassandra这样的NoSQL存储是更好的选择。选择NoSQL也更容易扩展。

基本系统设计与算法

现在需要解决的问题是如何为给定的URL生成一个简短且唯一的密钥。主要有两种解决方案:

  • 对原URL进行编码

  • 提前离线生成秘钥

对原URL编码

可以计算给定URL的唯一HASH值(例如,MD5或SHA256等)。然后可以对HASH进行编码以供显示。该编码可以是base36([a-z,0-9])base62([A-Z,a-z,0-9]),如果我们加上+/,就可以使用Base64编码。需要考虑的一个问题是短链接的长度应该是多少?6个、8个或10个字符?

使用Base64编码,6个字母的长密钥将产生64^6≈687亿个可能的字符串; 使用Base64编码,8个字母长的密钥将产生64^8≈281万亿个可能的字符串。

按照我们预估的数据,687亿对于我们来说足够了,所以可以选择6个字母。

如果我们使用MD5算法作为我们的HASH函数,它将产生一个128位的HASH值。在Base64编码之后,我们将得到一个超过21个字符的字符串(因为每个Base64字符编码6位HASH值)。

现在我们每个短链接只有6(或8)个字符的空间,那么我们将如何选择我们的密钥呢?

我们可以取前6(或8)个字母作为密钥,但是这样导致链接重复;要解决这个问题,我们可以从编码字符串中选择一些其他字符或交换一些字符。

我们的解决方案有以下问题:

解决办法:

我们可以将递增的序列号附加到每个输入URL以使其唯一,然后生成其散列。不过,我们不需要将此序列号存储在数据库中。此方法可能存在的问题是序列号不断增加会导致溢出。添加递增的序列号也会影响服务的性能。

另一种解决方案可以是将用户ID附加到输入URL。但是,如果用户尚未登录,我们将不得不要求用户选择一个唯一的key。即使这样也有可能有冲突,需要不断生成直到得到唯一的密钥。

离线生成秘钥

可以有一个独立的密钥生成服务,我们就叫它KGS(Key Generation Service),它预先生成随机的六个字母的字符串,并将它们存储在数据库中。每当我们想要生成短链接时,都去KGS获取一个已经生成的密钥并使用。这种方法更简单快捷。我们不仅不需要对URL进行编码,而且也不必担心重复或冲突。KGS将确保插入到数据库中的所有密钥都是唯一的。

会存在并发问题吗?

密钥一旦使用,就应该在数据库中进行标记,以确保不会再次使用。如果有多个服务器同时读取密钥,我们可能会遇到两个或多个服务器尝试从数据库读取相同密钥的情况。如何解决这个并发问题呢?

KGS可以使用两个表来存储密钥:一个用于尚未使用的密钥,一个用于所有已使用的密钥。

一旦KGS将密钥提供给其中一个服务器,它就可以将它们移动到已使用的秘钥表中;可以始终在内存中保留一些密钥,以便在服务器需要时快速提供它们。

为简单起见,一旦KGS将一些密钥加载到内存中,它就可以将它们移动到Used Key表中。这可确保每台服务器都获得唯一的密钥。

如果在将所有加载的密钥分配给某个服务器之前KGS重启或死亡,我们将浪费这些密钥,考虑到我们拥有的秘钥很多,这种情况也可以接受。

还必须确保KGS不将相同的密钥提供给多个服务器,因此,KGS将秘钥加载到内存和将秘钥移动到已使用表的动作需要时同步的,或者加锁,然后才能将秘钥提供给服务器。

KGS是否存在单点故障?

要解决KGS单点故障问题,我们可以使用KGS的备用副本。当主服务器死机时,备用服务器可以接管以生成和提供密钥。

每个应用服务器是否可以换成一些Key?

可以,这样可以减少对KGS的访问,不过,在这种情况下,如果应用服务器在使用所有密钥之前死亡,我们最终将丢失这些密钥。但是因为我们的秘钥数量很多,这点可以接受。

如何完成秘钥查找?

我们可以在数据库中查找密钥以获得完整的URL。如果它存在于数据库中,则向浏览器发回一个“HTTP302 Redirect”状态,将存储的URL传递到请求的Location字段中。如果密钥不在我们系统中,则发出HTTP 404 Not Found状态或将用户重定向回主页。

数据分区和复制

因为我们要存储十亿个URL数据,那么一个数据库节点在存储上可能不满足要求,并且单节点也不能支撑我们读取的要求。

因此,我们需要开发一种分区方案,将数据划分并存储到不同的数据库服务中。

基于范围分区:

我们可以根据短链接的第一个字母将URL存储在不同的分区中。因此,我们将所有以字母'A/a'开头的URL保存在一个分区中,将以字母‘B/b’开头的URL保存在另一个分区中,以此类推。这种方法称为基于范围的分区。我们甚至可以将某些不太频繁出现的字母合并到一个数据库分区中。

基于hash值分区:

在此方案中,我们对要存储的对象进行Hash计算。然后,我们根据Hash结果计算使用哪个分区。在我们的例子中,我们可以使用短链接的Hash值来确定存储数据对象的分区。

Hash函数会将URL随机分配到不同的分区中(例如,Hash函数总是可以将任何‘键’映射到[1…256]之间的一个数字,这个数字将表示我们在其中存储对象的分区。

这种方式有可能导致有些分区数据超载,可以使用一致性哈希算法解决。

缓存

对于频繁访问的热点URL我们可以进行缓存。缓存的方案可以使用现成的解决方案,比如使用memcached,Redis等,因此,应用服务器在查找数据库之前可以快速检查高速缓存是否具有所需的URL。

如果确定缓存容量?

可以从每天20%的流量开始,并根据客户端的使用模式调整所需的缓存服务器数量。如上所述,我们需要170 GB内存来缓存20%的日常流量。可以使用几个较小的服务器来存储所有这些热门URL。

选择哪种淘汰策略?

淘汰策略是指当缓存已满时,如果我们想用更热点的URL替换链接,我们该如何选择?

对于我们的系统来说,最近最少使用(LRU)是一个合理的策略。在此策略下,我们首先丢弃最近最少使用的URL;我们可以使用一个短链接或短链接的HASH值作为key的Hash Map或类似的数据结构来存储URL和访问次数。

如何更新缓存?

每当出现缓存未命中时,我们的服务器都会命中后端数据库。每次发生这种情况,我们都可以更新缓存并将新条目传递给所有缓存副本。每个副本都可以通过添加新条目来更新其缓存。如果副本已经有该条目,它可以简单地忽略它。

负载均衡

可以在系统中的三个位置添加负载均衡层:

  • 在客户端和应用程序服务器之间;

  • 在应用程序服务器和数据库服务器之间;

  • 在应用程序服务器和缓存服务器之间。

可以使用简单的循环调度方法,在后端服务器之间平均分配传入的请求。这种负载均衡方式实现起来很简单,并且不会带来任何开销。此方法的另一个好处是,如果服务器死机,负载均衡可以让其退出轮换,并停止向其发送任何流量。

循环调度的一个问题是没有考虑服务器过载情况。因此,如果服务器过载或速度慢,不会停止向该服务器发送新请求。要处理此问题,可以放置一个更智能的解决方案,定期查询后端服务器的负载并基于此调整流量。

数据清除策略

数据应该永远保留,还是应该被清除?如果达到用户指定的过期时间,短链接应该如何处理?

  • 持续扫描数据库,清除过期数据。

  • 懒惰删除策略

如果我们选择持续查询过期链接来删除,将会给数据库带来很大的压力;可以慢慢删除过期的链接,并进行懒惰的方式清理。服务确保只有过期的链接将被删除,尽管一些过期的链接可以在数据库保存更长时间,但永远不会返回给用户。

  • 每当用户尝试访问过期链接时,我们都可以删除该链接并向用户返回错误;

  • 单独的清理服务可以定期运行,从存储和缓存中删除过期的链接;

  • 此服务应该非常轻量级,并计划仅在预期用户流量较低时运行;

  • 我们可以为每个短链接设置默认的到期时间(例如两年);

  • 删除过期链接后,我们可以将密钥放回KGS的数据库中重复使用。

结语

以上就是开发一个短链接服务系统要做的方方面面,可能还存在一些小黑没有考虑到的地方,欢迎留言区交流!如果对你有一点点帮助,点个赞鼓励一下。

作者:小黑说Java
来源:https://juejin.cn/post/7034325565431611406

收起阅读 »

Python编程需要遵循的一些规则v2

Python编程需要遵循的一些规则v2使用 pylintpylint 是一个在 Python 源代码中查找 bug 的工具. 对于 C 和 C++ 这样的强类型静态语言来说, 这些 bug 通常由编译器来捕获. 由于 Python 的动态特性, 有些警告可能不...
继续阅读 »



Python编程需要遵循的一些规则v2

使用 pylint

pylint 是一个在 Python 源代码中查找 bug 的工具. 对于 C 和 C++ 这样的强类型静态语言来说, 这些 bug 通常由编译器来捕获. 由于 Python 的动态特性, 有些警告可能不对. 不过虚报的情况应该比较少. 确保对你的代码运行 pylint. 在 CI 流程中加入 pylint 检查的步骤. 抑制不准确的警告, 以便其他正确的警告可以暴露出来。

自底向上编程

自底向上编程(bottom up): 从最底层,依赖最少的地方开始设计结构及编写代码, 再编写调用这些代码的逻辑, 自底向上构造程序.

  • 采取自底向上的设计方式会让代码更少以及开发过程更加敏捷.

  • 自底向上的设计更容易产生符合单一责任原则(SRP) 的代码.

  • 组件之间的调用关系清晰, 组件更易复用, 更易编写单元测试案例.

如:需要编写调用外部系统 API 获取数据来完成业务逻辑的代码.

  • 应该先编写一个独立的模块将调用外部系统 API 获取数据的接口封装在一些函数中, 然后再编写如何调用这些函数 来完成业务逻辑.

  • 不可以先写业务逻辑, 然后在需要调用外部 API 时再去实现相关代码, 这会产生调用 API 的代码直 接耦合在业务逻辑中的代码.

防御式编程

使用 assert 语句确保程序处于的正确状态 不要过度使用 assert, 应该只用于确保核心的部分.

注意 assert 不能代替运行时的异常, 不要忘记 assert 语句可能会被解析器忽略.

assert 语句通常可用于以下场景:

  • 确保公共类或者函数被正确地调用 例如一个公共函数可以处理 list 或 dict 类型参数, 在函数开头使用 assert isinstance(param, (list, dict))确保函数接受的参数是 list 或 dict

  • assert 用于确保不变量. 防止需求改变时引起代码行为的改变

if target == x:
  run_x_code()
elif target == y:
  run_y_code()
else:
  run_z_code()

假设该代码上线时是正确的, target 只会是 x, y, z 三种情况, 但是稍后如果需求改变了, target 允许 w 的 情况出现. 当 target 为 w 时该代码就会错误地调用 run_z_code, 这通常会引起糟糕的后果.

  • 使用 assert 来确保不变量

assert target in (x, y, z)
if target == x:
  run_x_code()
elif target == y:
  run_y_code()
else:
  assert target == z
  run_z_code()

不使用 assert 的场景:

  • 不使用 assert 在校验用户输入的数据, 需要校验的情况下应该抛出异常

  • 不将 assert 用于允许正常失败的情况, 将 assert 用于检查不允许失败的情况.

  • 用户不应该直接看到 AssertionError, 如果用户可以看到, 将这种情况视为一个 BUG

避免使用 magic number

赋予特殊的常量一个名字, 避免重复地直接使用它们的字面值. 合适的时候使用枚举值 Enum.

使用常量在重构时只需要修改一个地方, 如果直接使用字面值在重构时将修改所有使用到的地方.

  • 建议

GRAVITATIONAL_CONSTANT = 9.81

def get_potential_energy(mass, height):
  return mass * height * GRAVITATIONAL_CONSTANT

class ConfigStatus:
  ENABLED = 1
  DISABLED = 0

Config.objects.filter(enabled=ConfigStatus.ENABLED)
  • 不建议

def get_potential_energy(mass, height):
  return mass * height * 9.81

# Django ORM
Config.objects.filter(enabled=1)

处理字典 key 不存在时的默认值

使用 dict.setdefault 或者 defaultdict

# group words by frequency
words = [(1, 'apple'), (2, 'banana'), (1, 'cat')]
frequency = {}

dict.setdefault

  • 建议

for freq, word in words:
  frequency.setdefault(freq, []).append(word)

或者使用 defaultdict

from collections import defaultdict

frequency = defaultdict(list)

for freq, word in words:
  frequency[freq].append(word)
  • 不建议

for freq, word in words:
  if freq not in frequency:
      frequency[freq] = []
  frequency[freq].append(word)

注意在 Python 3 中 map filter 返回的是生成器而不是列表, 在隋性计算方面有所区别

禁止使用 import *

原则上禁止避免使用 import *, 应该显式地列出每一个需要导入的模块

使用 import * 会污染当前命名空间的变量, 无法找到变量的定义是来哪个模块, 在被 import 的模块上的改动可 能会在预期外地影响到其它模块, 可能会引起难以排查的问题.

在某些必须需要使用或者是惯用法 from foo import * 的场景下, 应该在模块 foo 的末尾使用 all 控制被导出的变量.

# foo.py
CONST_VALUE = 1
class Apple:
  ...

__all__ = ("CONST_VALUE", "Apple")

# bar.py
# noinspection PyUnresolvedReferences
from foo import *

作者:未来现相
来源:https://mp.weixin.qq.com/s/QinR-bHolVlr0z8IyhCqfg

收起阅读 »

从零到一编写 IOC 容器

前言本文的编写主要是最近在使用 midway 编写后端应用,midway 的 IOC 控制反转能力跟我们平时常写的前端应用,例如 react、vue 这些单应用还是有蛮大区别的,所以促使我想一探究竟,这种类 Spring IOC 容器是如何用 JavaScri...
继续阅读 »




前言

本文的编写主要是最近在使用 midway 编写后端应用,midway 的 IOC 控制反转能力跟我们平时常写的前端应用,例如 react、vue 这些单应用还是有蛮大区别的,所以促使我想一探究竟,这种类 Spring IOC 容器是如何用 JavaScript 来实现的。为方便读者阅读,本文的组织结构依次为 TS 装饰器、Reflect Metadata、IOC 容器源码简单解读、以及自定义实现 IOC 容器。阅读完本文后,我希望你能有这样的感悟:元数据(metadata)和 装饰器(Decorator) 本是 ES 中两个独立的部分,但是结合它们, 竟然能实现 控制反转 这样的能力。本文的所有演示实例都已经上传到 github 仓库 ioc-container ,读者可以克隆下来进行调试运行。

辛苦整理良久,还望手动点赞鼓励~ 博客 github地址为:github.com/fengshi123/… ,汇总了作者的所有博客,欢迎关注及 star ~

一、TS 装饰器

1、类装饰器

(1)类型声明

type ClassDecorator = <TFunction extends Function>
(target: TFunction) => TFunction | void;
  • 参数:

    target: 类的构造器。

  • 返回:

如果类装饰器返回了一个值,它将会被用来代替原有的类构造器的声明。因此,类装饰器适合用于继承一个现有类并添加一些属性和方法。例如我们可以添加一个 toString 方法给所有的类来覆盖它原有的 toString 方法,以及增加一个新的属性 school,如下所示

type Consturctor = { new (...args: any[]): any };

function School<T extends Consturctor>(BaseClass: T) {
 // 新构造器继承原有的构造器,并且返回
 return class extends BaseClass {  
   // 新增属性 school
   public school = 'qinghua'
   // 重写方法 toString
   toString() {
     return JSON.stringify(this);
  }
};
}

@School
class Student {
 public name = 'tom';
 public age = 14;
}

console.log(new Student().toString())
// {"name":"tom","age":14,"school":"qinghua"}

但是存在一个问题:装饰器并没有类型保护,这意味着在类装饰器的构造函数中新增的属性,通过原有的类实例将报无法找到的错误,如下所示

type Consturctor = { new (...args: any[]): any };

function School<T extends Consturctor>(BaseClass: T){
 return class extends BaseClass {
   // 新增属性 school
   public school = 'qinghua'
};
}


@School
class Student{
 getSchool() {
   return this.school; // Property 'school' does not exist on type 'Student'
}
}

new Student().school  // Property 'school' does not exist on type 'Student'

这是 一个TypeScript的已知的缺陷。 目前我们能做的可以额外提供一个类用于提供类型信息,如下所示

type Consturctor = { new (...args: any[]): any };

function School<T extends Consturctor>(BaseClass: T){
 return class extends BaseClass {
   // 新增属性 school
   public school = 'qinghua'
};
}

// 新增一个类用于提供类型信息
class Base {
 school: string;
}

@School
class Student extends Base{
 getSchool() {
   return this.school;
}
}

new Student().school)

2、属性装饰器

(1)类型声明

type PropertyDecorator = (
target: Object,
 propertyKey: string | symbol
) => void;
复制代码
  • 参数:

    1. target: 对于静态成员来说是类的构造器,对于实例成员来说是类的原型链。

    2. propertyKey: 属性的名称。

  • 返回:

返回的结果将被忽略。

我们可以通过属性装饰器给属性添加对应的验证判断,如下所示

function NameObserve(target: Object, property: string): void {
 console.log('target:', target)
 console.log('property:', property)
 let _property = Symbol(property)
 Object.defineProperty(target, property, {
   set(val){
     if(val.length > 4){
       throw new Error('名称不能超过4位!')
    }
     this[_property] = val;
  },
   get: function() {
     return this[_property];
}
})
}

class Student {
 @NameObserve
 public name: string;  // target: Student {}   key: 'name'
}

const stu = new Student();
stu.name = 'jack'
console.log(stu.name); // jack
// stu.name = 'jack1'; // Error: 名称不能超过4位!

export default Student;

3、方法装饰器

(1)类型声明:

type MethodDecorator = <T>(
 target: Object,
 propertyKey: string | symbol,
 descriptor: TypedPropertyDescriptor<T>
) => TypedPropertyDescriptor<T> | void;
  • 参数:

    1. target: 对于静态成员来说是类的构造器,对于实例成员来说是类的原型链;

    2. propertyKey: 属性的名称;

    3. descriptor: 属性的描述器;

  • 返回: 如果返回了值,它会被用于替代属性的描述器。

方法装饰器不同于属性装饰器的地方在于 descriptor 参数。 通过这个参数我们可以修改方法原本的实现,添加一些共用逻辑。 例如我们可以给一些方法添加打印输入与输出的能力

function logger(target: Object, property: string, 
   descriptor: PropertyDescriptor): PropertyDescriptor | void {
 const origin = descriptor.value;
 console.log(descriptor)
 descriptor.value = function(...args: number[]){
   console.log('params:', ...args)
   const result = origin.call(this, ...args);
   console.log('result:', result);
   return result;
}
}

class Person {
 @logger
 add(x: number, y: number){
   return x + y;
}
}

const person = new Person();
const result = person.add(1, 2);
console.log('查看 result:', result) // 3

4、访问器装饰器

访问器装饰器总体上讲和方法装饰器很接近,唯一的区别在于描述器中有的 key 不同: 方法装饰器的描述器的 key 为:

  • value

  • writable

  • enumerable

  • configurable

访问器装饰器的描述器的key为:

  • get

  • set

  • enumerable

  • configurable

例如,我们可以对访问器进行统一更改:

function descDecorator(target: Object, property: string, 
   descriptor: PropertyDescriptor): PropertyDescriptor | void {
 const originalSet = descriptor.set;
 const originalGet = descriptor.get;
 descriptor.set = function(value: any){
   return originalSet.call(this, value)
}
 descriptor.get = function(): string{
   return 'name:' + originalGet.call(this)
}
}

class Person {
 private _name = 'tom';

 @descDecorator
 set name(value: string){
   this._name = value;
}

 get name(){
   return this._name;
}
}

const person = new Person();
person.name = ('tom');
console.log('查看:', person.name) // name:'tom'

5、参数装饰器

类型声明:

type ParameterDecorator = (
 target: Object,
 propertyKey: string | symbol,
 parameterIndex: number
) => void;
  • 参数:

    1. target: 对于静态成员来说是类的构造器,对于实例成员来说是类的原型链。

    2. propertyKey: 属性的名称(注意是方法的名称,而不是参数的名称)。

    3. parameterIndex: 参数在方法中所处的位置的下标。

  • 返回:

返回的值将会被忽略。

单独的参数装饰器能做的事情很有限,它一般都被用于记录可被其它装饰器使用的信息。

function ParamDecorator(target: Object, property: string, 
   paramIndex: number): void {
 console.log(property);
 console.log(paramIndex);
}

class Person {
 private name: string;

 public setNmae(@ParamDecorator school: string, name: string){  // setNmae 0
   this.name = school + '_' + name
}
}

6、执行时机

装饰器只在解释执行时应用一次,如下所示,这里的代码会在终端中打印 apply decorator,即便我们其实并没有使用类 A。

function f(C) {
 console.log('apply decorator')
 return C
}

@f
class A {}

// output: apply decorator

7、执行顺序

不同类型的装饰器的执行顺序是明确定义的:

  • 实例成员:参数装饰器 -> 方法/访问器/属性 装饰器

  • 静态成员:参数装饰器 -> 方法/访问器/属性 装饰器

  • 构造器:参数装饰器

  • 类装饰器

示例如下所示

function f(key: string): any {
 console.log("evaluate: ", key);
 return function () {
   console.log("call: ", key);
};
}

@f("Class Decorator")
class C {
 @f("Static Property")
 static prop?: number;

 @f("Static Method")
 static method(@f("Static Method Parameter") foo:any) {}

 constructor(@f("Constructor Parameter") foo:any) {}

 @f("Instance Method")
 method(@f("Instance Method Parameter") foo:any) {}

 @f("Instance Property")
 prop?: number;
}

/* 输出顺序如下
evaluate: Instance Method
evaluate: Instance Method Parameter
call: Instance Method Parameter
call: Instance Method
evaluate: Instance Property
call: Instance Property
evaluate: Static Property
call: Static Property
evaluate: Static Method
evaluate: Static Method Parameter
call: Static Method Parameter
call: Static Method
evaluate: Class Decorator
evaluate: Constructor Parameter
call: Constructor Parameter
call: Class Decorator
*/

我们从上注意到执行实例属性 prop 晚于实例方法 method 然而执行静态属性 static prop 早于静态方法static method。 这是因为对于属性/方法/访问器装饰器而言,执行顺序取决于声明它们的顺序。 然而,同一方法中不同参数的装饰器的执行顺序是相反的, 最后一个参数的装饰器会最先被执行。

function f(key: string): any {
 console.log("evaluate: ", key);
 return function () {
   console.log("call: ", key);
};
}

class C {
 method(
   @f("Parameter Foo") foo,
   @f("Parameter Bar") bar
) {}
}

/* 输出顺序如下
evaluate: Parameter Foo
evaluate: Parameter Bar
call: Parameter Bar
call: Parameter Foo
*/

8、多个装饰器组合

我们可以对同一目标应用多个装饰器。它们的组合顺序为:

  • 求值外层装饰器

  • 求值内层装饰器

  • 调用内层装饰器

  • 调用外层装饰器

如下示例所示

function f(key: string) {
 console.log("evaluate: ", key);
 return function () {
   console.log("call: ", key);
};
}

class C {
 @f("Outer Method")
 @f("Inner Method")
 method() {}
}

/* 输出顺序如下
evaluate: Outer Method
evaluate: Inner Method
call: Inner Method
call: Outer Method
*/

二、Reflect Metadata

1、背景

在 ES6 的规范当中,ES6 支持元编程,核心是因为提供了对 Proxy 和 Reflect 对象的支持。简单来说这个 API 的作用就是可以实现对变量操作的函数化,也就是反射。然而 ES6 的 Reflect 规范里面还缺失一个规范,那就是 Reflect Metadata。这会造成什么样的情境呢? 由于 JS/TS 现有的 装饰器更多的是存在于对函数或者属性进行一些操作,比如修改他们的值,代理变量,自动绑定 this 等等功能。但是却无法实现通过反射来获取究竟有哪些装饰器添加到这个类/方法上... 这就限制了 JS 中元编程的能力。 此时 Relfect Metadata 就派上用场了,可以通过装饰器来给类添加一些自定义的信息。然后通过反射将这些信息提取出来(当然你也可以通过反射来添加这些信息)。 综合一下, JS 中对 Reflect Metadata 的诉求,简单概括就是:

  • 其他 C#、Java、Pythone 语言已经有的高级功能,我 JS 也应该要有(诸如C# 和 Java 之类的语言支持将元数据添加到类型的属性或注释,以及用于读取元数据的反射API,而目前 JS 缺少这种能力)

  • 许多用例(组合/依赖注入,运行时类型断言,反射/镜像,测试)都希望能够以一致的方式向类中添加其他元数据。

  • 为了使各种工具和库能够推理出元数据,需要一种标准一致的方法;

  • 元数据不仅可以用在对象上,也可以通过相关捕获器用在 Proxy 上;

  • 对开发人员来说,定义新的元数据生成装饰器应该简洁易用;

2、使用

TypeScript 在 1.5+ 的版本已经支持 reflect-metadata,但是我们在使用的时候还需要额外进行安装,如下所示

  • npm i reflect-metadata --save

  • 在 tsconfig.json 里配置选项 emitDecoratorMetadata: true

关于 reflect-metadata 的基本使用 api 可以阅读 reflect-metadata 文档,其包含常见的增删改查基本功能,我们来看下其基本的使用示例,其中 Reflect.metadata 当作 Decorator 使用,当修饰类时,在类上添加元数据,当修饰类属性时,在类原型的属性上添加元数据,如:

import "reflect-metadata";

@Reflect.metadata('classMetaData', 'A')
class SomeClass {
 @Reflect.metadata('methodMetaData', 'B')
 public someMethod(): string {
   return 'hello someMethod';
}
}

console.log(Reflect.getMetadata('classMetaData', SomeClass)); // 'A'
console.log(Reflect.getMetadata('methodMetaData', new SomeClass(), 'someMethod')); // 'B

当然跟我们平时看到的 IOC 不同,我们进一步结合装饰器,如下所示,与前面的功能是一样的

import "reflect-metadata";

function classDecorator(): ClassDecorator {
 return target => {
   // 在类上定义元数据,key 为 `classMetaData`,value 为 `a`
   Reflect.defineMetadata('classMetaData', 'A', target);
};
}

function methodDecorator(): MethodDecorator {
 return (target, key, descriptor) => {
   // 在类的原型属性 'someMethod' 上定义元数据,key 为 `methodMetaData`,value 为 `b`
   Reflect.defineMetadata('methodMetaData', 'B', target, key);
};
}

@classDecorator()
class SomeClass {
 @methodDecorator()
 someMethod() {}
}

console.log(Reflect.getMetadata('classMetaData', SomeClass)); // 'A'
console.log(Reflect.getMetadata('methodMetaData', new SomeClass(), 'someMethod')); // 'B'

3、design:类型元数据

在 TS 中的 reflect-metadata 的功能是经过增强的,其添加 "design:type"、"design:paramtypes" 和 "design:returntype" 这 3 个类型相关的元数据

  • design:type 表示被装饰的对象是什么类型, 比如是字符串、数字、还是函数等;

  • design:paramtypes 表示被装饰对象的参数类型, 是一个表示类型的数组, 如果不是函数, 则没有该 key;

  • design:returntype 表示被装饰对象的返回值属性, 比如字符串、数字或函数等;

示例如下所示

import "reflect-metadata";

@Reflect.metadata('type', 'class')
class A {  
 constructor(
   public name: string,
   public age: number
) { }  

 @Reflect.metadata(undefined, undefined)  
 method(name: string, age: number):boolean {    
   return true  
}
}

 const t1 = Reflect.getMetadata('design:type', A.prototype, 'method')
 const t2 = Reflect.getMetadata('design:paramtypes', A.prototype, 'method')
 const t3 = Reflect.getMetadata('design:returntype', A.prototype, 'method')
 
 console.log(t1)  // [Function: Function]
 console.log(...t2) // [Function: String] [Function: Number]
 console.log(t3) // [Function: Boolean]

三、IOC 容器实现

1、源码解读

我们可以从 github 上克隆 midway 仓库的代码到本地,然后进行代码阅读以及 debug 调试。本篇博文我们主要想探究下 midway 的依赖注入合控制反转是如何实现的,其主要源码存在于两个目录:packages/core 和 packages/decorator,其中 packages/core 包含依赖注入的核心实现,加载对象的class,同步、异步创建对象实例化,对象的属性绑定等。 IOC 容器就像是一个对象池,管理着每个对象实例的信息(Class Definition),所以用户无需关心什么时候创建,当用户希望拿到对象的实例 (Object Instance) 时,可以直接拿到依赖对象的实例,容器会 自动将所有依赖的对象都自动实例化。packages/core 中主要有以下几种,分别处理不同的逻辑:

  • AppliationContext 基础容器,提供了基础的增加定义和根据定义获取对象实例的能力;

  • MidwayContainer 用的最多的容器,做了上层封装,通过 bind 函数能够方便的生成类定义,midway 从此类开始扩展;

  • RequestContainer 用于请求链路上的容器,会自动销毁对象并依赖另一个容器创建实例;

packages/decorator 包含装饰器 provide.ts、inject.ts 的实现,在midwayjs中是有一个装饰器管理类DecoratorManager, 用来管理 midwayjs 的所有装饰器:

  • @provide() 的作用是简化绑定,能被 IOC 容器自动扫描,并绑定定义到容器上,对应的逻辑是绑定对象定义;

  • @inject() 的作用是将容器中的定义实例化成一个对象,并且绑定到属性中,这样,在调用的时候就可以访问到该属性。

2、简单实现

2.1、装饰器 Provider

实现装饰器 Provider 类,作用为将对应类注册到 IOC 容器中。

import 'reflect-metadata'
import * as camelcase from 'camelcase'
import { class_key } from './constant'

// Provider 装饰的类,表示要注册到 IOC 容器中
export function Provider (identifier?: string, args?: Array<any>) {
 return function (target: any) {
   // 类注册的唯一标识符
   identifier = identifier ?? camelcase(target.name)

   Reflect.defineMetadata(class_key, {
     id: identifier,  // 唯一标识符
     args: args || [] // 实例化所需参数
  }, target)
   return target
}
}

2.2、装饰器 Inject

实现装饰器 Inject 类,作用为将对应的类注入到对应的地方。

import 'reflect-metadata'
import { props_key } from './constant'

export function Inject () {
 return function (target: any, targetKey: string) {
   // 注入对象
   const annotationTarget = target.constructor
   let props = {}
   // 同一个类,多个属性注入类
   if (Reflect.hasOwnMetadata(props_key, annotationTarget)) {
     props = Reflect.getMetadata(props_key, annotationTarget)
  }

   //@ts-ignore
   props[targetKey] = {
     value: targetKey
  }

   Reflect.defineMetadata(props_key, props, annotationTarget)
}
}

2.3、管理容器 Container

管理容器 Container 的实现,用于绑定实例信息并且在对应的地方获取它们。

import 'reflect-metadata'
import { props_key } from './constant'

export class Container {
 bindMap = new Map()

 // 绑定类信息
 bind(identifier: string, registerClass: any, constructorArgs: any[]) {
   this.bindMap.set(identifier, {registerClass, constructorArgs})
}

 // 获取实例,将实例绑定到需要注入的对象上
 get<T>(identifier: string): T {
   const target = this.bindMap.get(identifier)
   if (target) {
     const { registerClass, constructorArgs } = target
     // 等价于 const instance = new registerClass([...constructorArgs])
     const instance = Reflect.construct(registerClass, constructorArgs)

     const props = Reflect.getMetadata(props_key, registerClass)
     for (let prop in props) {
       const identifier = props[prop].value
       // 递归进行实例化获取 injected object
       instance[prop] = this.get(identifier)
    }
     return instance
  }
}
}

2.4、加载类文件 load

启动时扫描所有文件,获取文件导出的所有类,然后根据元数据进行绑定。

import * as fs from 'fs'
import { resolve } from 'path'
import { class_key } from './constant'

// 启动时扫描所有文件,获取定义的类,根据元数据进行绑定
export function load(container: any, path: string) {
 const list = fs.readdirSync(path)
 for (const file of list) {
   if (/\.ts$/.test(file)) {
     const exports = require(resolve(path, file))

     for (const m in exports) {
       const module = exports[m]
       if (typeof module === 'function') {
         const metadata = Reflect.getMetadata(class_key, module)
         // register
         if (metadata) {
           container.bind(metadata.id, module, metadata.args)
        }
      }
    }
  }
}
}

2.5、示例类

三个示例类如下所示

// class A
import { Provider } from "../provide";
import { Inject } from "../inject";
import B from './classB'
import C from './classC'

@Provider('a')
export default class A {
 @Inject()
 private b: B

 @Inject()
 c: C

 print () {
   this.c.print()
}
}

// class B
import { Provider } from '../provide'

@Provider('b', [10])
export default class B {
 n: number
 constructor (n: number) {
   this.n = n
}
}

// class C
import { Provider } from '../provide'

@Provider()
export default class C {
 print () {
   console.log('hello')
}
}

2.6、初始化

我们能从以下示例结果中看到,我们已经实现了一个基本的 IOC 容器能力。

import { Container } from './container'
import { load } from './load'
import { class_path } from './constant'

const init =  function () {

 const container = new Container()
 // 通过加载,会先执行装饰器(设置元数据),
 // 再由 container 统一管理元数据中,供后续使用
 load(container, class_path)
 const a:any = container.get('a') // A { b: B { n: 10 }, c: C {} }
 console.log(a);
 a.c.print() // hello
}

init()

总结

本文的依次从 TS 装饰器、Reflect Metadata、IOC 容器源码简单解读、以及自定义实现 IOC 容器四个部分由零到一编写自定义 IOC 容器,希望对你有所启发。本文的所有演示实例都已经上传到 github 仓库 ioc-container ,读者可以克隆下来进行调试运行。

作者:我是你的超级英雄
来源:https://juejin.cn/post/7036895697865555982

收起阅读 »

300行代码实现循环滚动控件

序言在业务中需要显示一个循环滚动的控件,内容可以循环滚动,可以自动滚动,手指触摸的时候会暂停。 由于目前的方案都是基于ViewPager或者RecycleView的。还需要实现Adapter,需要拦截各种事件。使用成本比较高。于是我就自定义了一个控件实现该功能...
继续阅读 »

序言

在业务中需要显示一个循环滚动的控件,内容可以循环滚动,可以自动滚动,手指触摸的时候会暂停。 由于目前的方案都是基于ViewPager或者RecycleView的。还需要实现Adapter,需要拦截各种事件。使用成本比较高。于是我就自定义了一个控件实现该功能,

使用

使用起来很简单。把需要显示的控件放置在其中就行。就和普通的HorizontalScrollView用法一样。 不过子控件必须要LoopLinearLayout 在这里插入图片描述

效果

  • 1.支持左右循环滚动
  • 2.支持自动滚动
  • 3.支持点击事件
  • 4.触摸暂停
  • 5.支持惯性滚动
  • 6.一共不到300行代码,逻辑简单易于扩展

在这里插入图片描述

原理

通过继承自HorizontalScrollView实现,重新onOverScrolled 和 scrollTo 方法在调用supper方法之前,对是否到达边界进行判断,如果到达就调用LoopLinearLayout.changeItemsToRight() 方法对内容重新摆放。

摆放使用的是 child.layout() 的方法,没有性能问题。摆放完成以后,对scrollX进行重新赋值。

需要注意的是在HorizontalScrollView中有一个负责惯性滚动的OverScroller 在这里插入图片描述 但是在调用其fling方法之前会设置maxX这导致无法滚动到控件内容之外。所以使用反射修改了这个类。拦截了fling方法 在这里插入图片描述

而动画的时长设置的是滚动一个LoopScrollView宽度需要的时间。还有就是无限循环的动画需要在 onDetachedFromWindow中移除,避免内存泄漏

源码

LoopLinearLayout

package com.example.myapplication;

import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;

import androidx.annotation.Nullable;

import java.util.List;

/**
* Created by zhuguohui
* Date: 2021/11/30
* Time: 10:46
* Desc:
*/
public class LoopLinearLayout extends LinearLayout {
public LoopLinearLayout(Context context) {
this(context, null);
}

public LoopLinearLayout(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}


public void changeItemsToRight(List<View> moveItems, int offset) {

int offset2 = 0;
for (int i = 0; i < getChildCount(); i++) {
View childAt = getChildAt(i);
if (!moveItems.contains(childAt)) {
MarginLayoutParams layoutParams = (MarginLayoutParams) childAt.getLayoutParams();
offset2 += childAt.getWidth() + layoutParams.leftMargin + layoutParams.rightMargin;
childAt.layout(childAt.getLeft() - offset, childAt.getTop(), childAt.getRight() - offset, childAt.getBottom());
}
}
for(View view:moveItems){
view.layout(view.getLeft()+offset2,view.getTop(),view.getRight()+offset2,view.getBottom());
}
}
public void changeItemsToLeft(List<View> moveItems, int offset) {

int offset2 = 0;
for (int i = 0; i < getChildCount(); i++) {
View childAt = getChildAt(i);
if (!moveItems.contains(childAt)) {
MarginLayoutParams layoutParams = (MarginLayoutParams) childAt.getLayoutParams();
offset2 += childAt.getWidth() + layoutParams.leftMargin + layoutParams.rightMargin;
childAt.layout(childAt.getLeft() + offset, childAt.getTop(), childAt.getRight() + offset, childAt.getBottom());
}
}
for(View view:moveItems){
view.layout(view.getLeft()-offset2,view.getTop(),view.getRight()-offset2,view.getBottom());
}
}


}

LoopScrollView

package com.example.myapplication;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.LinearInterpolator;
import android.widget.HorizontalScrollView;
import android.widget.OverScroller;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;

public class LoopScrollView extends HorizontalScrollView {

private LoopScroller loopScroller;
private ValueAnimator animator;

public LoopScrollView(Context context) {
this(context, null);
}

public LoopScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
setOverScrollMode(OVER_SCROLL_ALWAYS);
try {
@SuppressLint("DiscouragedPrivateApi")
Field field =HorizontalScrollView.class.getDeclaredField("mScroller");
field.setAccessible(true);
loopScroller = new LoopScroller(getContext());
field.set(this, loopScroller);

} catch (Exception e) {
e.printStackTrace();
}

}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
if(changed||animator==null){
buildAnimation();
}
}

private void buildAnimation() {
if(animator!=null){
animator.cancel();
animator=null;
}
animator = ValueAnimator.ofInt(getWidth() - getPaddingRight() - getPaddingLeft());
animator.setDuration(5*1000);
animator.setRepeatCount(-1);
animator.setInterpolator(new LinearInterpolator());
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
int lastValue;
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int value= (int) animation.getAnimatedValue();
int scrollByX=value-lastValue;
// Log.i("zzz","scroll by x="+scrollByX);
scrollByX=Math.max(0,scrollByX);
if(userUp) {
scrollBy(scrollByX, 0);
}
lastValue=value;
}
});
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
}

@Override
public void onAnimationStart(Animator animation) {
super.onAnimationStart(animation);
}

});
animator.start();
}

static class LoopScroller extends OverScroller{
public LoopScroller(Context context) {
super(context);
}

@Override
public void fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY, int overX, int overY) {
super.fling(startX, startY, velocityX, velocityY, Integer.MIN_VALUE,Integer.MAX_VALUE, minY, maxY, 0, overY);
}
}




@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if(animator!=null){
animator.cancel();
animator.removeAllListeners();
animator = null;
}
}

@Override
protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
if (userUp) {
//scroller再滚动
scrollX=loopScroller.getCurrX();
int detailX = scrollX - lastScrollX;
lastScrollX = scrollX;
if(detailX==0){
return;
}
scrollX = detailX + getScrollX();

}
int moveTo = moveItem(scrollX,clampedX);

super.onOverScrolled(moveTo, scrollY, false, clampedY);
}

boolean userUp = true;
int lastScrollX = 0;

@Override
public boolean onTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_UP) {
userUp = true;
lastScrollX = getScrollX();
} else {
userUp = false;
}
return super.onTouchEvent(ev);
}
@Override
public void scrollTo(int x, int y) {
int scrollTo = moveItem(x, false);
super.scrollTo(scrollTo, y);
}


private int moveItem(int scrollX, boolean clampedX) {

int toScrollX = scrollX;

if (getChildCount() > 0) {
if (!canScroll(scrollX,clampedX)) {
boolean toLeft=scrollX<=0;
int mWidth=getWidth()-getPaddingLeft()-getPaddingRight();
//无法向右滚动了,将屏幕外的item,移动到后面
List<View> needRemoveViewList = new ArrayList<>();
LoopLinearLayout group = (LoopLinearLayout) getChildAt(0);
int removeItemsWidth = 0;
boolean needRemove = false;
for (int i = group.getChildCount() - 1; i >= 0; i--) {
View itemView = group.getChildAt(i);
MarginLayoutParams params = (MarginLayoutParams) itemView.getLayoutParams();
if(toLeft){
int itemLeft = itemView.getLeft() - params.leftMargin;
if (itemLeft >= mWidth) {
//表示之后的控件都需要移除
needRemove = true;
}
}else{
int itemRight = itemView.getRight() + params.rightMargin;
if (itemRight <= scrollX) {
//表示之后的控件都需要移除
needRemove = true;
}
}

if (needRemove) {
int itemWidth = itemView.getWidth() + params.rightMargin + params.leftMargin;
removeItemsWidth += itemWidth;
needRemoveViewList.add(0,itemView);
}
needRemove=false;
}
if(!toLeft){
group.changeItemsToRight(needRemoveViewList,removeItemsWidth);
toScrollX -=removeItemsWidth;
}else{
group.changeItemsToLeft(needRemoveViewList,removeItemsWidth);
toScrollX +=removeItemsWidth;
}

}

}
return Math.max(0, toScrollX);
}

private boolean canScroll(int scrollX, boolean clampedX) {
if(scrollX<0){
return false;
}
if(scrollX==0&&clampedX){
//表示向左划不动了
return false;
}
View child = getChildAt(0);
if (child != null) {
int childWidth = child.getWidth();
return getWidth() + scrollX < childWidth + getPaddingLeft() + getPaddingRight();
}
return false;
}
}

最后所有的功能只依赖上述两个类,关于动画的时长写死在类中的,没有抽成方法。有需要的自己去改吧。


作者:solo_99
链接:https://juejin.cn/post/7036262704482353160
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

synchronized 的实现原理

synchronized 的使用 锁代码块(锁对象可指定,可为this、XXX.class、全局变量) 锁普通方法(锁对象是this,即该类实例本身) 锁静态方法(锁对象是该类,即XXX.class) 锁代码块 public class Sync { ...
继续阅读 »

synchronized 的使用



  • 锁代码块(锁对象可指定,可为this、XXX.class、全局变量)

  • 锁普通方法(锁对象是this,即该类实例本身)

  • 锁静态方法(锁对象是该类,即XXX.class)


锁代码块


public class Sync {

private int a = 0;

public void add(){
synchronized (this){
System.out.println("a values "+ ++a);
}
}

}

反编译之后的


public add()V
TRYCATCHBLOCK L0 L1 L2 null
TRYCATCHBLOCK L2 L3 L2 null
ALOAD 0
DUP
ASTORE 1
MONITORENTER
L0
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
NEW java/lang/StringBuilder
DUP
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
LDC "a values "
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
ALOAD 0
DUP
GETFIELD com/arrom/webview/Sync.a : I
ICONST_1
IADD
DUP_X1
PUTFIELD com/arrom/webview/Sync.a : I
INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
ALOAD 1
MONITOREXIT
L1
GOTO L4
L2
ASTORE 2
ALOAD 1
MONITOREXIT
L3
ALOAD 2
ATHROW
L4
RETURN
MAXSTACK = 5
MAXLOCALS = 3
}

由反编译结果可以看出:synchronized代码块主要是靠MONITORENTERMONITOREXIT这两个原语来实现同步的。当线程进入MONITORENTER获得执行代码的权利时,其他线程就不能执行里面的代码,直到锁Owner线程执行MONITOREXIT释放锁后,其他线程才可以竞争获取锁。


MONITORENTER

每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权.



  1. 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。

  2. 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.

  3. 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。


第2点就涉及到了可重入锁,意思就是说当一个线程已经获取一个锁时,它可以再获取无数次,从代码的角度上将就是有无数个相同的synchronized语句块嵌套在一起。在进入时,monitor的进入数+1;退出时就-1,直到为0的时候才可以被其他线程竞争获取。


MONITOREXIT

执行MONITOREXIT的线程必须是objectref所对应的monitor的所有者。


指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。


锁普通方法


public class Sync {

private int a = 0;

public synchronized void add(){
System.out.println("a values "+ ++a);
}

}

反编译之后并没有monitorenter和monitorexit,但是常量池中多了ACC_SYNCHRONIZED标示符。JVM就是根据该标示符来实现方法的同步的:


当方法调用时会检查方法的ACC_SYNCHRONIZED之后才能执行方法体,方法执行完后再释放monitor。


在方法执行期间,其他任何线程都无法再获得同一个monitor对象。这种方式与语句块没什么本质区别,都是通过竞争monitor的方式实现的。只不过这种方式是隐式的实现方法。


MONITORENTER和ACC_SYNCHRONIZED只是起标志作用,并无实质操作。


锁静态方法



private static int a = 0;

public synchronized static void add(){
System.out.println("a values "+ ++a);
}

常量池中用ACC_STATIC标志了这是一个静态方法,然后用ACC_SYNCHRONIZED标志位提醒线程去竞争monitor。由于静态方法是属于类级别的方法(即不用创建对象就可以被调用),所以这是一个类级别(XXX.class)的锁,即竞争某个类的monitor。


锁的竞争过程


image.png



  • (1)、多个线程请求锁,首先进入Contention List,它可以接纳所有请求线程,而且是一个后进先出(LIFO)的虚拟队列,通过结点Node和next指针构造。

  • (2)(3)、ContentionList会被线程并发访问,EntryList为了降低线程对ContentionList队尾的争用而构造出来。当Owner释放锁时,会从ContentionList中迁移线程到EntryList,并会指定EntryList中的某个线程(一般为Head结点)为Ready Thread,也就是说某个时刻最多只有一个线程正在竞争锁。

  • (4)、Owner并不是直接把锁交给OnDeck线程,而是将竞争锁的权利交给OnDeck(将锁释放了),然后让OnDeck自己去竞争。竞争成功后,OnDeck线程就变成Owner;否则继续留在EntryList的队头。

  • (5)(6)、当线程调用wait方法被阻塞时,进入WaitSet;当其他线程调用notifyAll()(notify())方法后,阻塞队列的(某个)线程就会进入EntryList中。


      处于ContetionList、EntryList、WaitSet的线程均处于阻塞状态。而线程被阻塞涉及到用户态与内核态的切换(Liunx),系统切换严重影响锁的性能。解决这个问题的办法就是自旋。自旋就是线程不断进行内部循环,即for循环什么也不做,防止线程wait()阻塞,在自旋过程中不断尝试获取锁,如果自旋期间,Owner刚好释放锁,此时自旋线程就可以去竞争锁。如果自旋了一段时间还没获取到锁,那没办法,只能调用wait()阻塞了。 

为什么自旋了一段时间后又调用wait()方法呢?因为自旋是要消耗CPU的,而且还有线程上下文切换,因为CPU还可以调度线程,只不过执行的是空的for循环罢了。 

对自旋锁周期的选择上,HotSpot认为最佳时间应是一个线程上下文切换的时间,但目前并没有做到。 

所以,synchronized是什么时候进行自旋的?答案是在进入ContetionList之前,因为它自旋一定时间后还没获取锁,最后它只好在ContetionList中阻塞等待了。


对象头


对象头(Object Header)包括两部分信息。


一部分用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,这部分数据的长度在32位和64位的虚拟机(暂 不考虑开启压缩指针的场景)中分别为32个和64个Bits,官方称它为“Mark Word”。


对象需要存储的运行时数据很多,其实已经超出了32、64位Bitmap结构所能记录的限度,但是对象头信息是与对象自身定义的数据无关的额 外存储成本,考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存储空间。例如在32位的HotSpot虚拟机 中对象未被锁定的状态下,Mark Word的32个Bits空间中的25Bits用于存储对象哈希码(HashCode),4Bits用于存储对象分代年龄,2Bits用于存储锁标志 位,1Bit固定为0,在其他状态(轻量级锁定、重量级锁定、GC标记、可偏向)下对象的存储内容如下表所示。


image.png


另外一部分是类型指针,即是对象指向它的类的元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说查找对象的元数据信息并不一定要经过对象本身。另外,如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中无法确定数组的大小。


为了减少锁释放带来的消耗,锁有一个升级的机制,从轻到重依次是:无锁状态 ——> 偏向锁 ——> 轻量级锁 ——>重量级锁。


偏向锁


在无其它线程与它竞争的情况下,持有偏向锁的线程永远也不需要同步。


它的加锁过程很简单:线程访问同步代码块时检查偏向锁中线程ID是否指向自己,如果是表明该线程已获得锁;否则,检测偏向锁标记是否为1,不是的话则CAS竞争锁,如果是就将对象头中线程ID指向自己。


当存在线程竞争锁时,偏向锁才会撤销,转而升级为轻量级锁。而这个撤销过程则需要有一个全局安全点(即这个时间点上没有正在执行的字节码)


image.png


在撤销锁的时候,栈中对象头的Mark Word要么偏向于其他线程,要么恢复到无锁或者轻量级锁。



  • 优点:加锁和解锁无需额外消耗

  • 缺点:锁进化时会带来额外锁撤销的消耗

  • 适用场景:只有一个线程访问同步代码块


轻量级锁


image.png



  • 优点:竞争的线程不阻塞,也就是不涉及到用户态与内核态的切换(Liunx),减少系统切换锁带来的开销

  • 缺点:如果长时间竞争不到锁,自旋会消耗CPU

  • 适用场景:追求响应时间、同步块执行速度非常快


重量级锁


它是传统意义上的锁,通过互斥量来实现同步,线程阻塞,等待Owner释放锁唤醒。




  • 优点:线程竞争不自旋,不消耗CPU




  • 缺点:线程阻塞,响应时间慢




  • 适用场景:追求吞吐量、同步块执行时间较长


作者:深圳_Arrom
链接:https://juejin.cn/post/7036354740342489118
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Jetpack-Lifecycle

1.AndroidX 的ComponentActivity 实现了LifecycleOwner接口,ComponentActivity 的子类会重写LifecycleOwner的接口方法,以便得到我们使用的lifecycle对象,lifecycle 是在Com...
继续阅读 »

图片.png


1.AndroidX 的ComponentActivity 实现了LifecycleOwner接口,ComponentActivity 的子类会重写LifecycleOwner的接口方法,以便得到我们使用的lifecycle对象,lifecycle 是在ComponentActivity 中创建的lifecycleRegistry对象;


2.使用的时候,通过lifecycleRegistry对象addObserver的方式注册LifecycleObserver,当生命周期变化的时候,会回调LifecycleObserver的onStateChanged, 里面的Lifecycle.Event能监听到当前Activity不同生命周期的变化


原理:


1.lifecycle注册的时候,会初始化observer的状态State,初始状态是 initial或者destroy, 将observer和state封装成一个类作为map的value值, observer作为key;


2.addObserver还有一个状态对齐,等会讲


3.当宿主activity或者fragment生命周期发生变化的时候,会分发当前的生命周期事件,转换成对应的mState,


3.1 和当前map中最早添加的observer的state进行比较,如果mState小于 state的值, 说明当前执行的是OnResume->onPause->onStop->onDestroy中的某一个环节, 遍历当前的map,将map的state向下更新为当前的生命周期状态,并调用observer的onStateChange方法;


3.2 和当前map中最后添加的observer的state进行比较,如果mState大于 state的值, 说明当前执行的是onCreate->onStart->onResume中的某一个环节, 遍历当前的map,将map的state向上更新为当前的生命周期状态,并调用observer的onStateChange方法;


3.3 状态对齐, 比如:当我们在生命周期的onStop方法中去addObserver时,此时添加到map中的state是inital状态, 实际上当前的生命周期是onStop,对应的是Created状态, 此时需要将map中小于Created的状态更新成Created状态,因为是upEvent, 所以回调的event会有onCreate


小结: 1.创建一个state保存到map; 2.等生命周期变化时,更新state的值,回调onStateChanged方法,达到监控生命周期的作用


作者:朱络嗒
链接:https://juejin.cn/post/7036552818668339208
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Glide数据输入输出

基础概念 在正式开始之前先明确一些概念 Glide输入: 我们日常在使用Glide的时候,通过load可以加载不同的资源类型例如文件,字符串等待。 我们把load的不同类型称为不同的输入。 Glide输出: Glide RequestManager提供了许多...
继续阅读 »

基础概念


在正式开始之前先明确一些概念


Glide输入: 我们日常在使用Glide的时候,通过load可以加载不同的资源类型例如文件,字符串等待。


requestManager多个load重载.png


我们把load的不同类型称为不同的输入。


Glide输出: Glide RequestManager提供了许多的as重载方法,


GlideAs方法.png


通过不同的as我们可以指定不同的输出类型。


ModelLoader: 是一个泛型接口,最直观的翻译是模型加载器。ModelLoader标记了它能够加载什么类型的数据,以及加载后返回什么样的数据类型。注意这里说说的返回的数据类型并不是我们想要的输出。ModelLoader定义如下


public interface ModelLoader<Model, Data> {
 class LoadData<Data> {
     //数据加载的key
   public final Key sourceKey;
   public final List<Key> alternateKeys;
     //获取数据的接口,对应获取不同类型的数据实现
   public final DataFetcher<Data> fetcher;

   public LoadData(@NonNull Key sourceKey, @NonNull DataFetcher<Data> fetcher) {
     this(sourceKey, Collections.<Key>emptyList(), fetcher);
  }

   public LoadData(@NonNull Key sourceKey, @NonNull List<Key> alternateKeys,
       @NonNull DataFetcher<Data> fetcher) {
     this.sourceKey = Preconditions.checkNotNull(sourceKey);
     this.alternateKeys = Preconditions.checkNotNull(alternateKeys);
     this.fetcher = Preconditions.checkNotNull(fetcher);
  }
}

   //创建LoadData 对象
 @Nullable
 LoadData<Data> buildLoadData(@NonNull Model model, int width, int height,
     @NonNull Options options);
   //判断当前的ModelLoader是否能够处理这个model
 boolean handles(@NonNull Model model);
}

DataFetcher: 用于进行数据加载,不同的类型有不同的DataFetcher


SourceGenerator远程数据加载过程


@Override
public boolean startNext() {
 //...
 boolean started = false;
 while (!started && hasNextModelLoader()) {
   loadData = helper.getLoadData().get(loadDataListIndex++);
   if (loadData != null
       && (helper.getDiskCacheStrategy().isDataCacheable(loadData.fetcher.getDataSource())
       || helper.hasLoadPath(loadData.fetcher.getDataClass()))) {
     started = true;
     loadData.fetcher.loadData(helper.getPriority(), this);
  }
}
 return started;
}

代码流程:


通过DecodeHelper获取LoadData,遍历每一个LoadData;


查看当前LoadData加载出来的数据能不能,转换成我们想要的输出数据,如果可以的话就是用当前loadData进行数据加载。


DecodeHelpe#getLoadData()


List<LoadData<?>> getLoadData() {
 if (!isLoadDataSet) {
   isLoadDataSet = true;
   loadData.clear();
     //此处的model就是我们 通过调用load传递进来的参数 即输入
   List<ModelLoader<Object, ?>> modelLoaders = glideContext.getRegistry().getModelLoaders(model);
   //noinspection ForLoopReplaceableByForEach to improve perf
   for (int i = 0, size = modelLoaders.size(); i < size; i++) {
     ModelLoader<Object, ?> modelLoader = modelLoaders.get(i);
     //通过modelLoader 构建loadData
     LoadData<?> current =
         modelLoader.buildLoadData(model, width, height, options);
     if (current != null) {
       loadData.add(current);
    }
  }
}
 return loadData;
}

ModelLoaderRegistry#getModelLoaders


getModelLoaders()实现的位置在ModelLoaderRegistry#getModelLoaders


public <A> List<ModelLoader<A, ?>> getModelLoaders(@NonNull A model) {
   //获取对应的modelLoader
 List<ModelLoader<A, ?>> modelLoaders = getModelLoadersForClass(getClass(model));
 int size = modelLoaders.size();
 boolean isEmpty = true;
 List<ModelLoader<A, ?>> filteredLoaders = Collections.emptyList();
 //noinspection ForLoopReplaceableByForEach to improve perf
 for (int i = 0; i < size; i++) {
   ModelLoader<A, ?> loader = modelLoaders.get(i);
     //判断对应的modelLoader是否有能力处理对应的model
   if (loader.handles(model)) {
     if (isEmpty) {
       filteredLoaders = new ArrayList<>(size - i);
       isEmpty = false;
    }
     filteredLoaders.add(loader);
  }
}
 return filteredLoaders;
}

getModelLoadersForClass主要是通过MultiModelLoaderFactory#build。然后MultiModelLoaderFactory会遍历所有已经注册的ModelLoader,只要当前的model是已经注册model的子类或者对应的实现,那么就会把对应的ModelLoader添加到待返回的集合中。


DecodeHelper#hasLoadPath


boolean hasLoadPath(Class<?> dataClass) {
   return getLoadPath(dataClass) != null;
}

<Data> LoadPath<Data, ?, Transcode> getLoadPath(Class<Data> dataClass) {
 return glideContext.getRegistry().getLoadPath(dataClass, resourceClass, transcodeClass);
}

可以看到hasLoadPath代码其实非常简单,就是获取一个LoadPath集合。获取的时候传递了三个参数 DataFetcher加载出来的数据类型dataClass,resourceClass ,transcodeClass


getLoadPath参数


对于resourceClass ,transcodeClass在DecodeHelper定义如下:


private Class<?> resourceClass;
private Class<Transcode> transcodeClass;

他们在init方法中进行初始化,经过层层代码的流转我们发现最终的参数初始化来自于RequestBuilder#obtainRequest


private Request obtainRequest(
     Target<TranscodeType> target,
     RequestListener<TranscodeType> targetListener,
     BaseRequestOptions<?> requestOptions,
     RequestCoordinator requestCoordinator,
     TransitionOptions<?, ? super TranscodeType> transitionOptions,
     Priority priority,
     int overrideWidth,
     int overrideHeight,
     Executor callbackExecutor) {
   return SingleRequest.obtain(
       context,
       glideContext,
       model,
       //该参数会在调用as系列方法后初始化,指向的是我们想要的输出类型。
       transcodeClass,
       //指向的是RequestBuilder 自身
       requestOptions,
       overrideWidth,
       overrideHeight,
       priority,
       target,
       targetListener,
       requestListeners,
       requestCoordinator,
       glideContext.getEngine(),
       transitionOptions.getTransitionFactory(),
       callbackExecutor);
}

而RequestOptions#getResourceClass返回的resourceClass默认情况下返回的是Object,而在asBitmap和asGifDrawable会做其它的转换。


private static final RequestOptions DECODE_TYPE_BITMAP = decodeTypeOf(Bitmap.class).lock();
private static final RequestOptions DECODE_TYPE_GIF = decodeTypeOf(GifDrawable.class).lock();

@NonNull
 @CheckResult
 public RequestBuilder<Bitmap> asBitmap() {
   return as(Bitmap.class).apply(DECODE_TYPE_BITMAP);
}
 public RequestBuilder<GifDrawable> asGif() {
   return as(GifDrawable.class).apply(DECODE_TYPE_GIF);
}

getLoadPath执行过程


getLoadPath最终会调用Registry#getLoadPath


@Nullable
public <Data, TResource, Transcode> LoadPath<Data, TResource, Transcode> getLoadPath(
   @NonNull Class<Data> dataClass, @NonNull Class<TResource> resourceClass,
   @NonNull Class<Transcode> transcodeClass) {
   //先获取DecodePath  
 List<DecodePath<Data, TResource, Transcode>> decodePaths =
       getDecodePaths(dataClass, resourceClass, transcodeClass);
   if (decodePaths.isEmpty()) {
     result = null;
  } else {
     result =
         new LoadPath<>(
             dataClass, resourceClass, transcodeClass, decodePaths, throwableListPool);
  }
   loadPathCache.put(dataClass, resourceClass, transcodeClass, result);
 return result;
}

private <Data, TResource, Transcode> List<DecodePath<Data, TResource, Transcode>> getDecodePaths(
     @NonNull Class<Data> dataClass, @NonNull Class<TResource> resourceClass,
     @NonNull Class<Transcode> transcodeClass) {
   List<DecodePath<Data, TResource, Transcode>> decodePaths = new ArrayList<>();
   //遍历所有资源解码器,获取能够解析当前输入dataClass的解码器
   List<Class<TResource>> registeredResourceClasses =
       decoderRegistry.getResourceClasses(dataClass, resourceClass);
   for (Class<TResource> registeredResourceClass : registeredResourceClasses) {
       //获取能够解析当前输入dataClass且将数据转变成我们想要的transcodeClass类型的转换类
     List<Class<Transcode>> registeredTranscodeClasses =
         transcoderRegistry.getTranscodeClasses(registeredResourceClass, transcodeClass);

     for (Class<Transcode> registeredTranscodeClass : registeredTranscodeClasses) {
//获取对应的所有解码器
       List<ResourceDecoder<Data, TResource>> decoders =
           decoderRegistry.getDecoders(dataClass, registeredResourceClass);
       //转换类
       ResourceTranscoder<TResource, Transcode> transcoder =
           transcoderRegistry.get(registeredResourceClass, registeredTranscodeClass);
       @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
       DecodePath<Data, TResource, Transcode> path =
           new DecodePath<>(dataClass, registeredResourceClass, registeredTranscodeClass,
               decoders, transcoder, throwableListPool);
       decodePaths.add(path);
    }
  }
   return decodePaths;
}

整个过程涉及到两个关键类LoadPath DecodePath。


LoadPath 由数据类型datacalss 和 DecodePath组成


DecodePath 由数据类型dataclass 解码器 ResourceDecoder 集合 和资源转换 ResourceTranscoder 构成。总体上而言 一个LoadPath的存在代表着可能存在一条路径能够将ModelLoader加载出来的data解码转换成我们指定的数据类型。


DocodeJob数据解码的过程


Glide DecodeJob 的工作过程我们知道SourceGenerator在数据加载完成之后如果允许缓存原始数据会再次执行SourceGenerator#startNext将加载的数据进行缓存,然后通过DataCacheGenerator从缓存文件中获取。最终获取数据成功后会调用DocodeJob#onDataFetcherReady


作者:小小小小小鹿
链接:https://juejin.cn/post/7036389645168410655
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

大厂面试Kafka,一定会问到的幂等性

01 幂等性如此重要 Kafka作为分布式MQ,大量用于分布式系统中,如消息推送系统、业务平台系统(如结算平台),就拿结算来说,业务方作为上游把数据打到结算平台,如果一份数据被计算、处理了多次,产生的后果将会特别严重。 02 哪些因素影响幂等性...
继续阅读 »

01 幂等性如此重要


Kafka作为分布式MQ,大量用于分布式系统中,如消息推送系统、业务平台系统(如结算平台),就拿结算来说,业务方作为上游把数据打到结算平台,如果一份数据被计算、处理了多次,产生的后果将会特别严重。

02 哪些因素影响幂等性


使用Kafka时,需要保证exactly-once语义。要知道在分布式系统中,出现网络分区是不可避免的,如果kafka broker 在回复ack时,出现网络故障或者是full gc导致ack timeout,producer将会重发,如何保证producer重试时不造成重复or乱序?又或者producer 挂了,新的producer并没有old producer的状态数据,这个时候如何保证幂等?即使Kafka 发送消息满足了幂等,consumer拉取到消息后,把消息交给线程池workers,workers线程对message的处理可能包含异步操作,又会出现以下情况:


  • 先commit,再执行业务逻辑:提交成功,处理失败 。造成丢失




  • 先执行业务逻辑,再commit:提交失败,执行成功。造成重复执行




  • 先执行业务逻辑,再commit:提交成功,异步执行fail。造成丢失




本文将针对以上问题作出讨论


03 Kafka保证发送幂等性


       针对以上的问题,kafka在0.11版新增了幂等型producer和事务型producer。前者解决了单会话幂等性等问题,后者解决了多会话幂等性。


单会话幂等性


为解决producer重试引起的乱序和重复。Kafka增加了pid和seq。Producer中每个RecordBatch都有一个单调递增的seq; Broker上每个tp也会维护pid-seq的映射,并且每Commit都会更新lastSeq。这样recordBatch到来时,broker会先检查RecordBatch再保存数据:如果batch中 baseSeq(第一条消息的seq)比Broker维护的序号(lastSeq)大1,则保存数据,否则不保存(inSequence方法)。

ProducerStateManager.scala


    private def maybeValidateAppend(producerEpoch: Short, firstSeq: Int, offset: Long): Unit = {

    validationType match {

    case ValidationType.None =>

    case ValidationType.EpochOnly =>

    checkProducerEpoch(producerEpoch, offset)

    case ValidationType.Full =>

    checkProducerEpoch(producerEpoch, offset)

    checkSequence(producerEpoch, firstSeq, offset)

    }

    }

    private def checkSequence(producerEpoch: Short, appendFirstSeq: Int, offset: Long): Unit = {

    if (producerEpoch != updatedEntry.producerEpoch) {

    if (appendFirstSeq != 0) {

    if (updatedEntry.producerEpoch != RecordBatch.NO_PRODUCER_EPOCH) {

    throw new OutOfOrderSequenceException(s"Invalid sequence number for new epoch at offset $offset in " +

    s"partition $topicPartition: $producerEpoch (request epoch), $appendFirstSeq (seq. number)")

    } else {

    throw new UnknownProducerIdException(s"Found no record of producerId=$producerId on the broker at offset $offset" +

    s"in partition $topicPartition. It is possible that the last message with the producerId=$producerId has " +

    "been removed due to hitting the retention limit.")

    }

    }

    } else {

    val currentLastSeq = if (!updatedEntry.isEmpty)

    updatedEntry.lastSeq

    else if (producerEpoch == currentEntry.producerEpoch)

    currentEntry.lastSeq

    else

    RecordBatch.NO_SEQUENCE

    if (currentLastSeq == RecordBatch.NO_SEQUENCE && appendFirstSeq != 0) {

ne throw mew UnknownProducerIdException(s"Local producer state matches expected epoch $producerEpoch " +

    s"for producerId=$producerId at offset $offset in partition $topicPartition, but the next expected " +

    "sequence number is not known.")

    } else if (!inSequence(currentLastSeq, appendFirstSeq)) {

    throw new OutOfOrderSequenceException(s"Out of order sequence number for producerId $producerId at " +

    s"offset $offset in partition $topicPartition: $appendFirstSeq (incoming seq. number), " +

    s"$currentLastSeq (current end sequence number)")

    }

    }

    }

    private def inSequence(lastSeq: Int, nextSeq: Int): Boolean = {

    nextSeq == lastSeq + 1L || (nextSeq == 0 && lastSeq == Int.MaxValue)

    }



引申:Kafka producer 对有序性做了哪些处理


假设我们有5个请求,batch1、batch2、batch3、batch4、batch5;如果只有batch2 ack failed,3、4、5都保存了,那2将会随下次batch重发而造成重复。我们可以设置max.in.flight.requests.per.connection=1(客户端在单个连接上能够发送的未响应请求的个数)来解决乱序,但降低了系统吞吐。
新版本kafka设置enable.idempotence=true后能够动态调整max-in-flight-request。正常情况下max.in.flight.requests.per.connection 大于1。当重试请求到来且时,batch 会根据 seq重新添加到队列的合适位置,并把max.in.flight.requests.per.connection设为1, 这样它 前面的 batch序号都比它小,只有前面的都发完了,它才能发。

    private void insertInSequenceOrder(Deque<ProducerBatch> deque, ProducerBatch batch) {

    // When we are requeing and have enabled idempotence, the reenqueued batch must always have a sequence.

    if (batch.baseSequence() == RecordBatch.NO_SEQUENCE)

    throw new IllegalStateException("Trying to re-enqueue a batch which doesn't have a sequence even " +

    "though idempotency is enabled.");

    if (transactionManager.nextBatchBySequence(batch.topicPartition) == null)

    throw new IllegalStateException("We are re-enqueueing a batch which is not tracked as part of the in flight " +

    "requests. batch.topicPartition: " + batch.topicPartition + "; batch.baseSequence: " + batch.baseSequence());

    ProducerBatch firstBatchInQueue = deque.peekFirst();

    if (firstBatchInQueue != null && firstBatchInQueue.hasSequence() && firstBatchInQueue.baseSequence() < batch.baseSequence()) {

    List<ProducerBatch> orderedBatches = new ArrayList<>();

    while (deque.peekFirst() != null && deque.peekFirst().hasSequence() && deque.peekFirst().baseSequence() < batch.baseSequence())

    orderedBatches.add(deque.pollFirst());

    log.debug("Reordered incoming batch with sequence {} for partition {}. It was placed in the queue at " +

    "position {}", batch.baseSequence(), batch.topicPartition, orderedBatches.size())

    deque.addFirst(batch);

    // Now we have to re insert the previously queued batches in the right order.

    for (int i = orderedBatches.size() - 1; i >= 0; --i) {

    deque.addFirst(orderedBatches.get(i));

    }

    // At this point, the incoming batch has been queued in the correct place according to its sequence.

    } else {

    deque.addFirst(batch);

    }

    }


多会话幂等性


在单会话幂等性中介绍,kafka通过引入pid和seq来实现单会话幂等性,但正是引入了pid,当应用重启时,新的producer并没有old producer的状态数据。可能重复保存。

Kafka事务通过隔离机制来实现多会话幂等性


kafka事务引入了transactionId 和Epoch,设置transactional.id后,一个transactionId只对应一个pid, 且Server 端会记录最新的 Epoch 值。这样有新的producer初始化时,会向TransactionCoordinator发送InitPIDRequest请求, TransactionCoordinator 已经有了这个 transactionId对应的 meta,会返回之前分配的 PID,并把 Epoch 自增 1 返回,这样当old
producer恢复过来请求操作时,将被认为是无效producer抛出异常。     如果没有开启事务,TransactionCoordinator会为新的producer返回new pid,这样就起不到隔离效果,因此无法实现多会话幂等。

    private def maybeValidateAppend(producerEpoch: Short, firstSeq: Int, offset: Long): Unit = {

    validationType match {

    case ValidationType.None =>

    case ValidationType.EpochOnly =>

    checkProducerEpoch(producerEpoch, offset)

    case ValidationType.Full => //开始事务,执行这个判断

    checkProducerEpoch(producerEpoch, offset)

    checkSequence(producerEpoch, firstSeq, offset)

    }

    }

    private def checkProducerEpoch(producerEpoch: Short, offset: Long): Unit = {

    if (producerEpoch < updatedEntry.producerEpoch) {

    throw new ProducerFencedException(s"Producer's epoch at offset $offset is no longer valid in " +

    s"partition $topicPartition: $producerEpoch (request epoch), ${updatedEntry.producerEpoch} (current epoch)")

    }

    }


04 Consumer端幂等性


如上所述,consumer拉取到消息后,把消息交给线程池workers,workers对message的handle可能包含异步操作,又会出现以下情况:


  • 先commit,再执行业务逻辑:提交成功,处理失败 。造成丢失




  • 先执行业务逻辑,再commit:提交失败,执行成功。造成重复执行




  • 先执行业务逻辑,再commit:提交成功,异步执行fail。造成丢失




对此我们常用的方法时,works取到消息后先执行如下code:


    if(cache.contain(msgId)){

    // cache中包含msgId,已经处理过

    continue;

    }else {

    lock.lock();

    cache.put(msgId,timeout);

    commitSync();

    lock.unLock();

    }

    // 后续完成所有操作后,删除cache中的msgId,只要msgId存在cache中,就认为已经处理过。Note:需要给cache设置有消息


作者:WeCoding
链接:https://juejin.cn/post/6844903909153062926
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Toast必须在UI(主)线程使用?

背景 依稀记得,从最开始干Android这一行就经常听到有人说:toast(吐司)不能在子线程调用显示,只能在UI(主)线程调用展示。 非常惭愧的是,我之前也这么认为,并且这个问题也一直没有深究。 直至前两天我的朋友 “林小海” 同学说toast不能在子线程中...
继续阅读 »

背景


依稀记得,从最开始干Android这一行就经常听到有人说:toast(吐司)不能在子线程调用显示,只能在UI(主)线程调用展示。


非常惭愧的是,我之前也这么认为,并且这个问题也一直没有深究。


直至前两天我的朋友 “林小海” 同学说toast不能在子线程中显示,这句话使我突然想起了点什么。


我觉得我有必要证明、并且纠正一下。


toast不能在子线程调用展示的结论真的是谬论~


疑点


前两天在说到这个toast的时候一瞬间对于只能在UI线程中调用展示的说法产生了两个疑点:




  1. 在子线程更新UI一般都会有以下报错提示:


    Only the original thread that created a view hierarchy can touch its views.


    但是,我们在子线程直接toast的话,报错的提示如下:


    Can't toast on a thread that has not called Looper.prepare()


    明显,两个报错信息是不一样的,从toast这条来看的话是指不能在一个没有调用Looper.prepare()的线程里面进行toast,字面意思上有说是不能在子线程更新UI吗?No,没有!这也就有了下面第2点的写法。




  2. 曾见过一种在子线程使用toast的用法如下(正是那时候没去深究这个问题):




        new Thread(new Runnable() {
@Override
public void run() {
Looper.prepare();
Toast.makeText(MainActivity.this.getApplicationContext(),"SHOW",Toast.LENGTH_SHORT).show();
Looper.loop();
}
}).start();

关于Looper这个东西,我想大家都很熟悉了,我就不多说looper这块了,下面主要分析一下为什么这样的写法就可以在子线程进行toast了呢?


并且Looper.loop()这个函数调用后是会阻塞轮循的,这种写法是会导致线程没有及时销毁,在toast完之后我特意给大家用如下代码展示一下这个线程的状态:


    Log.d("Wepon", "isAlive:"+t[0].isAlive());
Log.d("Wepon", "state:" + t[0].getState());

D/Wepon: isAlive:true
D/Wepon: state:RUNNABLE

可以看到,线程还活着,没有销毁掉。当然,这种代码里面如果想要退出looper的循环以达到线程可以正常销毁的话是可以使用looper.quit相关的函数的,但是这个调用quit的时机却是不好把握的。


下面将通过Toast相关的源码来分析一下为什么会出现上面的情况?


源码分析


Read the fuck source code.


1.分析Toast.makeText()方法


首先看我们的调用Toast.makeText,makeText这个函数的源码:


    // 这里一般是我们外部调用Toast.makeText(this, "xxxxx", Toast.LENGTH_SHORT)会进入的方法。
// 然后会调用下面的函数。
public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
return makeText(context, null, text, duration);
}

/**
* Make a standard toast to display using the specified looper.
* If looper is null, Looper.myLooper() is used. // 1. 注意这一句话
* @hide
*/
public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
@NonNull CharSequence text, @Duration int duration) {

// 2. 构造toast实例,有传入looper,此处looper为null
Toast result = new Toast(context, looper);

LayoutInflater inflate = (LayoutInflater)
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);
TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);
tv.setText(text);

result.mNextView = v;
result.mDuration = duration;

return result;
}

从上面的源码中看第1点注释,looper为null的时候会调用Looper.myLooper(),这个方法的作用是取我们线程里面的looper对象,这个调用是在Toast的构造函数里面发生的,看我们的Toast构造函数:


2.分析Toast构造函数


    /**
* Constructs an empty Toast object. If looper is null, Looper.myLooper() is used.
* @hide
*/
public Toast(@NonNull Context context, @Nullable Looper looper) {
mContext = context;
// 1.此处创建一个TN的实例,传入looper,接下来主要分析一下这个TN类
mTN = new TN(context.getPackageName(), looper);
mTN.mY = context.getResources().getDimensionPixelSize(
com.android.internal.R.dimen.toast_y_offset);
mTN.mGravity = context.getResources().getInteger(
com.android.internal.R.integer.config_toastDefaultGravity);
}

TN的构造函数如下,删除了一部分不重要代码:


TN(String packageName, @Nullable Looper looper) {
// .....
// ..... 省略部分源码,这
// .....

// 重点
// 2.判断looper == null,这里我们从上面传入的时候就是null,所以会进到里面去。
if (looper == null) {
// Use Looper.myLooper() if looper is not specified.
// 3.然后会调用Looper.myLooper这个函数,也就是会从ThreadLocal<Looper> sThreadLocal 去获取当前线程的looper。
// 如果ThreadLocal这个不太清楚的可以先去看看handler源码分析相关的内容了解一下。
looper = Looper.myLooper();
if (looper == null) {
// 4.这就是报错信息的根源点了!!
// 没有获取到当前线程的looper的话,就会抛出这个异常。
// 所以分析到这里,就可以明白为什么在子线程直接toast会抛出这个异常
// 而在子线程中创建了looper就不会抛出异常了。
throw new RuntimeException(
"Can't toast on a thread that has not called Looper.prepare()");
}
}
// 5.这里不重点讲toast是如何展示出来的源码了,主要都在TN这个类里面,
// Toast与TN中间有涉及aidl跨进程的调用,这些可以看看源码。
// 大致就是:我们的show方法实际是会往这个looper里面放入message的,
// looper.loop()会阻塞、轮循,
// 当looper里面有Message的时候会将message取出来,
// 然后会通过handler的handleMessage来处理。
mHandler = new Handler(looper, null) {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
// .... 省略代码
case SHOW: // 显示,与WindowManager有关,这部分源码不做说明了,可以自己看看,就在TN类里面。
case HIDE: // 隐藏
case CANCEL: // 取消
}
}
};
}

总结


从第1点可以看到会创建TN的实例,并传入looper,此时的looper还是null。


进入TN的构造函数可以看到会有looper是否为null的判断,并且在当looper为null时,会从当前线程去获取looper(第3点,Looper.myLooper()),如果还是获取不到,刚会抛出我们开头说的这个异常信息:Can't toast on a thread that has not called Looper.prepare()。


而有同学会误会只能在UI线程toast的原因是:UI(主)线程在刚创建的时候就有创建looper的实例了,在主线程toast的话,会通过Looper.myLooper()获取到对应的looper,所以不会抛出异常信息。


而不能直接在子线程程中toast的原因是:子线程中没有创建looper的话,去通过Looper.myLooper()获取到的为null,就会throw new RuntimeException(
"Can't toast on a thread that has not called Looper.prepare()");


另外,两个点说明一下:



  1. Looper.prepare() 是创建一个looper并通过ThreadLocal跟当前线程关联上,也就是通过sThreadLocal.set(new Looper(quitAllowed));

  2. Looper.loop()是开启轮循,有消息就会处理,没有的话就会阻塞。


综上,“Toast必须在UI(主)线程使用”这个说法是不对滴!,以后千万不要再说toast只能在UI线程显示啦.....


作者:ywp0919
链接:https://juejin.cn/post/6844904103538065422
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

手把手教你用Flutter搭建属于自己的个人博客

Flutter 2.0以来已经稳定支持web的开发,现在来教大家使用Flutter搭建一个个人的博客网站,使用Github提供的Actions、gh-pages服务,毕竟一时白票一时爽,一直白嫖一直爽。 1. 使用AndoridStuido创建一个Flutte...
继续阅读 »

Flutter 2.0以来已经稳定支持web的开发,现在来教大家使用Flutter搭建一个个人的博客网站,使用Github提供的Actions、gh-pages服务,毕竟一时白票一时爽,一直白嫖一直爽。


1. 使用AndoridStuido创建一个Flutter项目


Dingtalk_20210512195651.jpg


2. Github注册一个账号,并且创建一个Repository


Dingtalk_20210512195915.jpg


3. 上传创建的Flutter项目到这个Repository的master分支中


4. 获取Github的access token


新建一个access token
Dingtalk_20210512200242.jpg
保存token,等下要用


Dingtalk_20210512200516.jpg


5. 配置Actions secrets,name随便填写,value填入刚刚获取的token


Dingtalk_20210512200640.jpg


6.配置Actions


Dingtalk_20210512200843.jpg


Dingtalk_20210512201013.jpg


需要填写的规则


name: Flutter Web
on:
push:
branches:
- master
jobs:
build:
name: Build Web
env:
my_secret: ${{secrets.commit_secret}}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: subosito/flutter-action@v1
with:
channel: 'dev'
- run: flutter pub get
- run: flutter build web --release
- run: |
cd build/web
git init
git config --global user.email aaa
git config --global user.name bbb
git status
git remote add origin https://${{secrets.commit_secret}}@github.com/xxx/yyy.git
git checkout -b gh-pages
git add --all
git commit -m "update"
git push origin gh-pages -f

aaa-你的邮箱 bbb替-你的名称 xxx-你的git名字 yyy为-Repository名字


然后我们每次提交修改到master上时,Actions都会自动帮我们打包web到gh-pages分支上,完成Actions后,我们可以查看flutter构建的博客网站,一般网址为https://你的git名字.github.io/Repository名字/。


这里记得注意的是需要修改web目录下index.html中


<base href="/">
修改为Repository的名字
<base href="/flutter_blog/">

不然在打开网页的时候会找不到资源。


作者:gstory
链接:https://juejin.cn/post/6961399282872107015
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Python对象的浅拷贝与深拷贝

在讲我们深浅拷贝之前,我们需要先区分一下拷贝和赋值的概念。看下面的例子a = [1,2,3]赋值:b = a拷贝:b = a.copy()上面的两行代码究竟有什么不同呢?带着这个问题,继续 看了上面这张图,相信大家已经对直接赋值和拷贝有了一个比较清楚的认识...
继续阅读 »

在讲我们深浅拷贝之前,我们需要先区分一下拷贝和赋值的概念。看下面的例子

a = [1,2,3]

赋值:

b = a

拷贝:

b = a.copy()

上面的两行代码究竟有什么不同呢?带着这个问题,继续

Python对象的浅拷贝与深拷贝_递归



看了上面这张图,相信大家已经对直接赋值和拷贝有了一个比较清楚的认识。

直接赋值:复制一个对象的引用给新变量
拷贝:复制一个对象到新的内存地址空间,并且将新变量引用到复制后的对象

我们的深浅拷贝只是对于可变对象来讨论的。 不熟悉的朋友需要自己去了解可变对象与不可变对象哦。

1 对象的嵌套引用

a = { "list": [1,2,3] }

上面的代码,在内存中是什么样子的呢?请看下图:

Python对象的浅拷贝与深拷贝_递归_02



原来,在我们的嵌套对象中,子对象也是一个引用。

2 浅拷贝

Python对象的浅拷贝与深拷贝_python_03



如上图所示,我们就可以很好的理解什么叫做浅拷贝了。

浅拷贝:只拷贝父对象,不会拷贝对象的内部的子对象。内部的子对象指向的还是同一个引用

上面 的 a 和 c 是一个独立的对象,但他们的子对象还是指向统一对象

2.1 浅拷贝的方法

  • .copy()

a = {"list": [1,2,3] }
b = a.copy()
  • copy模块

import copy
a = {"list": [1,2,3] }
b = copy.copy(a)
  • 列表切片[:]

a = [1,2,3,[1,2,3]]
b = a[1:]
  • for循环

a = [1,2,3,[1,2,3]]
b = []
for i in a:
  b.append(i)

2.2 浅拷贝的影响

a = {"list":[1,2,3]}
b = a.copy()
a["list"].append(4)

print(a)
# {'list': [1, 2, 3, 4]}

print(b)
# {'list': [1, 2, 3, 4]}

在上面的例子中,我们明明只改变 a 的子对象,却发现 b 的子对象也跟着改变了。这样在我们的程序中也许会引发很多的BUG。

3 深拷贝

上面我们知道了什么是浅拷贝,那我们的深拷贝就更好理解了。

Python对象的浅拷贝与深拷贝_python_04



深拷贝:完全拷贝了父对象及其子对象,两者已经完成没有任何关联,是完全独立的。

import copy
a = {"list":[1,2,3]}
b = copy.deepcopy(a)
a["list"].append(4)

print(a)
# {'list': [1, 2, 3, 4]}

print(b)
# {'list': [1, 2, 3,]}

上面的例子中,我们再次修改 a 的子对象对 b 已经没有任何影响

4 手动实现一个深拷贝

主要采用递归的方法解决问题。判断拷贝的每一项子对象是否为引用对象。如果是就采用递归的方式将子对象进行复制。

def deepcopy(instance):
  if isinstance(instance, dict):
      return {k:deepcopy(v) for k,v in instance.items() }
   
  elif isinstance(instance, list):
      return [deepcopy(x) for x in instance]
   
  else:
      return instance

a = {"list": [1,2,3]}
b = deepcopy(a)

print(a)
# {'list': [1, 2, 3]}

print(b)
# {'list': [1, 2, 3]}

a["list"].append(4)
print(a)
# {'list': [1, 2, 3, 4]}

print(b)
# {'list': [1, 2, 3]}

创作不易,且读且珍惜。如有错漏还请海涵并联系作者修改,内容有参考,如有侵权,请联系作者删除。如果文章对您有帮助,还请动动小手,您的支持是我最大的动力。

作者:趣玩Python
来源:https://blog.51cto.com/u_14666251/4716452 收起阅读 »

手写迷你版Vue

手写迷你版Vue参考代码:github.com/57code/vue-…Vue响应式设计思路Vue响应式主要包含:数据响应式监听数据变化,并在视图中更新Vue2使用Object.defineProperty实现数据劫持Vu3使用Proxy实现数据劫持模板引擎提...
继续阅读 »




手写迷你版Vue

参考代码:github.com/57code/vue-…

Vue响应式设计思路

Vue响应式主要包含:

  • 数据响应式

  • 监听数据变化,并在视图中更新

  • Vue2使用Object.defineProperty实现数据劫持

  • Vu3使用Proxy实现数据劫持

  • 模板引擎

  • 提供描述视图的模板语法

  • 插值表达式{{}}

  • 指令 v-bind, v-on, v-model, v-for,v-if

  • 渲染

  • 将模板转换为html

  • 解析模板,生成vdom,把vdom渲染为普通dom

数据响应式原理

image.png

数据变化时能自动更新视图,就是数据响应式 Vue2使用Object.defineProperty实现数据变化的检测

原理解析

  • new Vue()⾸先执⾏初始化,对data执⾏响应化处理,这个过程发⽣在Observer

  • 同时对模板执⾏编译,找到其中动态绑定的数据,从data中获取并初始化视图,这个过程发⽣在

Compile

  • 同时定义⼀个更新函数和Watcher实例,将来对应数据变化时,Watcher会调⽤更新函数

  • 由于data的某个key在⼀个视图中可能出现多次,所以每个key都需要⼀个管家Dep来管理多个

Watcher

  • 将来data中数据⼀旦发⽣变化,会⾸先找到对应的Dep,通知所有Watcher执⾏更新函数

image.png

一些关键类说明

CVue:自定义Vue类 Observer:执⾏数据响应化(分辨数据是对象还是数组) Compile:编译模板,初始化视图,收集依赖(更新函数、 watcher创建) Watcher:执⾏更新函数(更新dom) Dep:管理多个Watcher实例,批量更新

涉及关键方法说明

observe: 遍历vm.data的所有属性,对其所有属性做响应式,会做简易判断,创建Observer实例进行真正响应式处理

html页面

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>cvue</title>
<script src="./cvue.js"></script>
</head>
<body>
<div id="app">
  <p>{{ count }}</p>
</div>

<script>
  const app = new CVue({
    el: '#app',
    data: {
      count: 0
    }
  })
  setInterval(() => {
    app.count +=1
  }, 1000);
</script>
</body>
</html>

CVue

  • 创建基本CVue构造函数:

  • 执⾏初始化,对data执⾏响应化处理

// 自定义Vue类
class CVue {
constructor(options) {
  this.$options = options
  this.$data = options.data

  // 响应化处理
  observe(this.$data)
}
}

// 数据响应式, 修改对象的getter,setter
function defineReactive(obj, key, val) {
// 递归处理,处理val是嵌套对象情况
observe(val)
Object.defineProperty(obj, key, {
  get() {
    return val
  },
  set(newVal) {
    if(val !== newVal) {
      console.log(`set ${key}:${newVal}, old is ${val}`)

      val = newVal
      // 继续进行响应式处理,处理newVal是对象情况
      observe(val)
    }
  }
})
}

// 遍历obj,对其所有属性做响应式
function observe(obj) {
// 只处理对象类型的
if(typeof obj !== 'object' || obj == null) {
  return
}
// 实例化Observe实例
new Observe(obj)
}

// 根据传入value的类型做相应的响应式处理
class Observe {
constructor(obj) {
  if(Array.isArray(obj)) {
    // TODO
  } else {
    // 对象
    this.walk(obj)
  }
}
walk(obj) {
  // 遍历obj所有属性,调用defineReactive进行响应化
  Object.keys(obj).forEach(key => defineReactive(obj, key, obj[key]))
}
}

为vm.$data做代理

方便实例上设置和获取数据

例如

原本应该是

vm.$data.count
vm.$data.count = 233

代理之后后,可以使用如下方式

vm.count
vm.count = 233

给vm.$data做代理

class CVue {
constructor(options) {
  // 省略
  // 响应化处理
  observe(this.$data)

  // 代理data上属性到实例上
  proxy(this)
}
}

// 把CVue实例上data对象的属性到代理到实例上
function proxy(vm) {
Object.keys(vm.$data).forEach(key => {
  Object.defineProperty(vm, key, {
    get() {
      // 实现 vm.count 取值
      return vm.$data[key]
    },
    set(newVal) {
      // 实现 vm.count = 123赋值
      vm.$data[key] = newVal
    }
  })
})
}

编译

image.png

初始化视图

根据节点类型进行编译
class CVue {
constructor(options) {
  // 省略。。
  // 2 代理data上属性到实例上
  proxy(this)

  // 3 编译
  new Compile(this, this.$options.el)
}
}

// 编译模板中vue语法,初始化视图,更新视图
class Compile {
constructor(vm, el) {
  this.$vm = vm
  this.$el = document.querySelector(el)

  if(this.$el) {
    this.complie(this.$el)
  }
}
// 编译
complie(el) {
  // 取出所有子节点
  const childNodes = el.childNodes
  // 遍历节点,进行初始化视图
  Array.from(childNodes).forEach(node => {
    if(this.isElement(node)) {
      // TODO
      console.log(`编译元素 ${node.nodeName}`)
    } else if(this.isInterpolation(node)) {
      console.log(`编译插值文本 ${node.nodeName}`)
    }
    // 递归编译,处理嵌套情况
    if(node.childNodes) {
      this.complie(node)
    }
  })
}
// 是元素节点
isElement(node) {
  return node.nodeType === 1
}
// 是插值表达式
isInterpolation(node) {
  return node.nodeType === 3
    && /\{\{(.*)\}\}/.test(node.textContent)
}
}
编译插值表达式
// 编译模板中vue语法,初始化视图,更新视图
class Compile {
complie(el) {
  Array.from(childNodes).forEach(node => {
    if(this.isElement(node)) {
      console.log(`编译元素 ${node.nodeName}`)
    } else if(this.isInterpolation(node)) {
      // console.log(`编译插值文本 ${node.textContent}`)
      this.complieText(node)
    }
    // 省略
  })
}
// 是插值表达式
isInterpolation(node) {
  return node.nodeType === 3
    && /\{\{(.*)\}\}/.test(node.textContent)
}
// 编译插值
complieText(node) {
  // RegExp.$1是isInterpolation()中/\{\{(.*)\}\}/匹配出来的组内容
  // 相等于{{ count }}中的count
  const exp = String(RegExp.$1).trim()
  node.textContent = this.$vm[exp]
}
}
编译元素节点和指令

需要取出指令和指令绑定值 使用数据更新视图

// 编译模板中vue语法,初始化视图,更新视图
class Compile {
complie(el) {
  Array.from(childNodes).forEach(node => {
    if(this.isElement(node)) {
      console.log(`编译元素 ${node.nodeName}`)
      this.complieElement(node)
    }
    // 省略
  })
}
// 是元素节点
isElement(node) {
  return node.nodeType === 1
}
// 编译元素
complieElement(node) {
  // 取出元素上属性
  const attrs = node.attributes
  Array.from(attrs).forEach(attr => {
    // c-text="count"中c-text是attr.name,count是attr.value
    const { name: attrName, value: exp } = attr
    if(this.isDirective(attrName)) {
      // 取出指令
      const dir = attrName.substring(2)
      this[dir] && this[dir](node, exp)
    }
  })
}
// 是指令
isDirective(attrName) {
  return attrName.startsWith('')
}
// 处理c-text文本指令
text(node, exp) {
  node.textContent = this.$vm[exp]
}
// 处理c-html指令
html(node, exp) {
  node.innerHTML = this.$vm[exp]
}
}

以上完成初次渲染,但是数据变化后,不会触发页面更新

依赖收集

视图中会⽤到data中某key,这称为依赖。 同⼀个key可能出现多次,每次出现都需要收集(⽤⼀个Watcher来维护维护他们的关系),此过程称为依赖收集。 多个Watcher需要⼀个Dep来管理,需要更新时由Dep统⼀通知。

image.png

  • data中的key和dep是一对一关系

  • 视图中key出现和Watcher关系,key出现一次就对应一个Watcher

  • dep和Watcher是一对多关系

实现思路

  • defineReactive中为每个key定义一个Dep实例

  • 编译阶段,初始化视图时读取key, 会创建Watcher实例

  • 由于读取过程中会触发key的getter方法,便可以把Watcher实例存储到key对应的Dep实例

  • 当key更新时,触发setter方法,取出对应的Dep实例Dep实例调用notiy方法通知所有Watcher更新

定义Watcher类

监听器,数据变化更新对应节点视图

// 创建Watcher监听器,负责更新视图
class Watcher {
// vm vue实例,依赖key,updateFn更新函数(编译阶段传递进来)
constructor(vm, key, updateFn) {
  this.$vm = vm
  this.$key = key
  this.$updateFn = updateFn
}
update() {
  // 调用更新函数,获取最新值传递进去
  this.$updateFn.call(this.$vm, this.$vm[this.$key])
}
}
修改Compile类中的更新函数,创建Watcher实例
class Complie {
// 省略。。。
// 编译插值
complieText(node) {
  // RegExp.$1是isInterpolation()中/\{\{(.*)\}\}/匹配出来的组内容
  // 相等于{{ count }}中的count
  const exp = String(RegExp.$1).trim()
  // node.textContent = this.$vm[exp]
  this.update(node, exp, 'text')
}
// 处理c-text文本指令
text(node, exp) {
  // node.textContent = this.$vm[exp]
  this.update(node, exp, 'text')
}
// 处理c-html指令
html(node, exp) {
  // node.innerHTML = this.$vm[exp]
  this.update(node, exp, 'html')
}
// 更新函数
update(node, exp, dir) {
  const fn = this[`${dir}Updater`]
  fn && fn(node, this.$vm[exp])

  // 创建监听器
  new Watcher(this.$vm, exp, function(newVal) {
    fn && fn(node, newVal)
  })
}
// 文本更新器
textUpdater(node, value) {
  node.textContent = value
}
// html更新器
htmlUpdater(node, value) {
  node.innerHTML = value
}
}
定义Dep类
  • data的一个属性对应一个Dep实例

  • 管理多个Watcher实例,通知所有Watcher实例更新

// 创建订阅器,每个Dep实例对应data中的一个属性
class Dep {
constructor() {
  this.deps = []
}
// 添加Watcher实例
addDep(dep) {
  this.deps.push(dep)
}
notify() {
  // 通知所有Wather更新视图
  this.deps.forEach(dep => dep.update())
}
}
创建Watcher时触发getter
class Watcher {
// vm vue实例,依赖key,updateFn更新函数(编译阶段传递进来)
constructor(vm, key, updateFn) {
  // 省略
  // 把Wather实例临时挂载在Dep.target上
  Dep.target = this
  // 获取一次属性,触发getter, 从Dep.target上获取Wather实例存放到Dep实例中
  this.$vm[key]
  // 添加后,重置Dep.target
  Dep.target = null
}
}
defineReactive中作依赖收集,创建Dep实例
function defineReactive(obj, key, val) {
// 递归处理,处理val是嵌套对象情况
observe(val)

const dep = new Dep()
Object.defineProperty(obj, key, {
  get() {
    Dep.target && dep.addDep(Dep.target)
    return val
  },
  set(newVal) {
    if(val !== newVal) {
      val = newVal
      // 继续进行响应式处理,处理newVal是对象情况
      observe(val)
      // 更新视图
      dep.notify()
    }
  }
})
}

监听事件指令@xxx

  • 在创建vue实例时,需要缓存methods到vue实例上

  • 编译阶段取出methods挂载到Compile实例上

  • 编译元素时

  • 识别出v-on指令时,进行事件的绑定

  • 识别出@属性时,进行事件绑定

  • 事件绑定:通过指令或者属性获取对应的函数,给元素新增事件监听,使用bind修改监听函数的this指向为组件实例

// 自定义Vue类
class CVue {
constructor(options) {
  this.$methods = options.methods
}
}

// 编译模板中vue语法,初始化视图,更新视图
class Compile {
constructor(vm, el) {
  this.$vm = vm
  this.$el = document.querySelector(el)
  this.$methods = vm.$methods
}

// 编译元素
complieElement(node) {
  // 取出元素上属性
  const attrs = node.attributes
  Array.from(attrs).forEach(attr => {
    // c-text="count"中c-text是attr.name,count是attr.value
    const { name: attrName, value: exp } = attr
    if(this.isDirective(attrName)) {
      // 省略。。。
      if(this.isEventListener(attrName)) {
        // v-on:click, subStr(5)即可截取到click
        const eventType = attrName.substring(5)
        this.bindEvent(eventType, node, exp)
      }
    } else if(this.isEventListener(attrName)) {
      // @click, subStr(1)即可截取到click
      const eventType = attrName.substring(1)
      this.bindEvent(eventType, node, exp)
    }
  })
}
// 是事件监听
isEventListener(attrName) {
  return attrName.startsWith('@') || attrName.startsWith('c-on')
}
// 绑定事件
bindEvent(eventType, node, exp) {
  // 取出表达式对应函数
  const method = this.$methods[exp]
  // 增加监听并修改this指向当前组件实例
  node.addEventListener(eventType, method.bind(this.$vm))
}
}

v-model双向绑定

实现v-model绑定input元素时的双向绑定功能

// 编译模板中vue语法,初始化视图,更新视图
class Compile {
// 省略...
// 处理c-model指令
model(node, exp) {
  // 渲染视图
  this.update(node, exp, 'model')
  // 监听input变化
  node.addEventListener('input', (e) => {
    const { value } = e.target
    // 更新数据,相当于this.username = 'mio'
    this.$vm[exp] = value
  })
}
// model更新器
modelUpdater(node, value) {
  node.value = value
}
}

数组响应式

  • 获取数组原型

  • 数组原型创建对象作为数组拦截器

  • 重写数组的7个方法

// 数组响应式
// 获取数组原型, 后面修改7个方法
const originProto = Array.prototype
// 创建对象做备份,修改响应式都是在备份的上进行,不影响原始数组方法
const arrayProto = Object.create(originProto)
// 拦截数组方法,在变更时发出通知
;['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(method => {
// 在备份的原型上做修改
arrayProto[method] = function() {
  // 调用原始操作
  originProto[method].apply(this, arguments)
  // 发出变更通知
  console.log(`method:${method} value:${Array.from(arguments)}`)
}
})

class Observe {
constructor(obj) {
  if(Array.isArray(obj)) {
    // 修改数组原型为自定义的
    obj.__proto__ = arrayProto
    this.observeArray(obj)
  } else {
    // 对象
    this.walk(obj)
  }
}
observeArray(items) {
  // 如果数组内部元素时对象,继续做响应化处理
  items.forEach(item => observe(item))
}
}

作者:LastStarDust
来源:https://juejin.cn/post/7036291383153393701

收起阅读 »

LRU缓存-keep-alive实现原理

相信大部分同学在日常需求开发中或多或少的会有需要一个组件状态被持久化、不被重新渲染的场景,熟悉 vue 的同学一定会想到 keep-alive 这个内置组件。 keep-alive 是 Vue.js 的一个 内置组件。它能够将不活动的组件实例保存在内存中,而不...
继续阅读 »



前言

相信大部分同学在日常需求开发中或多或少的会有需要一个组件状态被持久化、不被重新渲染的场景,熟悉 vue 的同学一定会想到 keep-alive 这个内置组件。

那么什么是 keep-alive 呢?

keep-alive 是 Vue.js 的一个 内置组件。它能够将不活动的组件实例保存在内存中,而不是直接将其销毁,它是一个抽象组件,不会被渲染到真实 DOM 中,也不会出现在父组件链中。简单的说,keep-alive用于保存组件的渲染状态,避免组件反复创建和渲染,有效提升系统性能。 keep-alivemax 属性,用于限制可以缓存多少组件实例,一旦这个数字达到了上限,在新实例被创建之前,已缓存组件中最久没有被访问的实例会被销毁掉,而这里所运用到的缓存机制就是 LRU 算法

LRU 缓存淘汰算法

LRU( least recently used)根据数据的历史记录来淘汰数据,重点在于保护最近被访问/使用过的数据,淘汰现阶段最久未被访问的数据

LRU的主体思想在于:如果数据最近被访问过,那么将来被访问的几率也更高

fifo对比lru原理

  1. 新数据插入到链表尾部;

  2. 每当缓存命中(即缓存数据被访问),则将数据移到链表尾部

  3. 当链表满的时候,将链表头部的数据丢弃。

实现LRU的数据结构

经典的 LRU 一般都使用 hashMap + 双向链表。考虑可能需要频繁删除一个元素,并将这个元素的前一个节点指向下一个节点,所以使用双链接最合适。并且它是按照结点最近被使用的时间顺序来存储的。 如果一个结点被访问了, 我们有理由相信它在接下来的一段时间被访问的概率要大于其它结点。

map.keys()

不过既然已经在 js 里都已经使用 Map 了,何不直接取用现成的迭代器获取下一个结点的 key 值(keys().next( )

// ./LRU.ts
export class LRUCache {
capacity: number; // 容量
cache: Map; // 缓存
constructor(capacity: number) {
  this.capacity = capacity;
  this.cache = new Map();
}
get(key: number): number {
  if (this.cache.has(key)) {
    let temp = this.cache.get(key) as number;
    //访问到的 key 若在缓存中,将其提前
    this.cache.delete(key);
    this.cache.set(key, temp);
    return temp;
  }
  return -1;
}
put(key: number, value: number): void {
  if (this.cache.has(key)) {
    this.cache.delete(key);
    //存在则删除,if 结束再提前
  } else if (this.cache.size >= this.capacity) {
    // 超过缓存长度,淘汰最近没使用的
    this.cache.delete(this.cache.keys().next().value);
    console.log(`refresh: key:${key} , value:${value}`)
  }
  this.cache.set(key, value);
}
toString(){
  console.log('capacity',this.capacity)
  console.table(this.cache)
}
}
// ./index.ts
import {LRUCache} from './lru'
const list = new LRUCache(4)
list.put(2,2)   // 2,剩余容量3
list.put(3,3)   // 3,剩余容量2
list.put(4,4)   // 4,剩余容量1
list.put(5,5)   // 5,已满   从头至尾         2-3-4-5
list.put(4,4)   // 入4,已存在 ——> 置队尾         2-3-5-4
list.put(1,1)   // 入1,不存在 ——> 删除队首 插入1 3-5-4-1
list.get(3)     // 获取3,刷新3——> 置队尾         5-4-1-3
list.toString()
// ./index.ts
import {LRUCache} from './lru'
const list = new LRUCache(4)

list.put(2,2)   // 2,剩余容量3
list.put(3,3)   // 3,剩余容量2
list.put(4,4)   // 4,剩余容量1
list.put(5,5)   // 5,已满   从头至尾 2-3-4-5
list.put(4,4)   // 入4,已存在 ——> 置队尾 2-3-5-4
list.put(1,1)   // 入1,不存在 ——> 删除队首 插入1 3-5-4-1
list.get(3)     // 获取3,刷新3——> 置队尾 5-4-1-3
list.toString()

结果如下: lru打印结果.jpg

vue 中 Keep-Alive

原理

  1. 使用 LRU 缓存机制进行缓存,max 限制缓存表的最大容量

  2. 根据设定的 include/exclude(如果有)进行条件匹配,决定是否缓存。不匹配,直接返回组件实例

  3. 根据组件 ID 和 tag 生成缓存 Key ,并在缓存对象中查找是否已缓存过该组件实例。如果存在,直接取出缓存值并更新该 key 在 this.keys 中的位置(更新 key 的位置是实现 LRU 置换策略的关键)

  4. 获取节点名称,或者根据节点 cid 等信息拼出当前 组件名称

  5. 获取 keep-alive 包裹着的第一个子组件对象及其组件名

源码分析

初始化 keepAlive 组件
const KeepAliveImpl: ComponentOptions = {
 name: `KeepAlive`,
 props: {
   include: [String, RegExp, Array],
   exclude: [String, RegExp, Array],
   max: [String, Number],
},
 setup(props: KeepAliveProps, { slots }: SetupContext) {
   // 初始化数据
   const cache: Cache = new Map();
   const keys: Keys = new Set();
   let current: VNode | null = null;
   // 当 props 上的 include 或者 exclude 变化时移除缓存
   watch(
    () => [props.include, props.exclude],
    ([include, exclude]) => {
     include && pruneCache((name) => matches(include, name));
     exclude && pruneCache((name) => !matches(exclude, name));
    },
    { flush: "post", deep: true }
  );
   // 缓存组件的子树 subTree
   let pendingCacheKey: CacheKey | null = null;
   const cacheSubtree = () => {
     // fix #1621, the pendingCacheKey could be 0
     if (pendingCacheKey != null) {
       cache.set(pendingCacheKey, getInnerChild(instance.subTree));
    }
  };
   // KeepAlive 组件的设计,本质上就是空间换时间。
   // 在 KeepAlive 组件内部,
   // 当组件渲染挂载和更新前都会缓存组件的渲染子树 subTree
   onMounted(cacheSubtree);
   onUpdated(cacheSubtree);
   onBeforeUnmount(() => {
   // 卸载缓存表里的所有组件和其中的子树...
  }
   return ()=>{
     // 返回 keepAlive 实例
  }
}
}

return ()=>{
 // 省略部分代码,以下是缓存逻辑
 pendingCacheKey = null
 const children = slots.default()
 let vnode = children[0]
 const comp = vnode.type as Component
 const name = getName(comp)
 const { include, exclude, max } = props
 // key 值是 KeepAlive 子节点创建时添加的,作为缓存节点的唯一标识
 const key = vnode.key == null ? comp : vnode.key
 // 通过 key 值获取缓存节点
 const cachedVNode = cache.get(key)
 if (cachedVNode) {
   // 缓存存在,则使用缓存装载数据
   vnode.el = cachedVNode.el
   vnode.component = cachedVNode.component
   if (vnode.transition) {
     // 递归更新子树上的 transition hooks
     setTransitionHooks(vnode, vnode.transition!)
  }
     // 阻止 vNode 节点作为新节点被挂载
     vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
     // 刷新key的优先级
     keys.delete(key)
     keys.add(key)
} else {
     keys.add(key)
     // 属性配置 max 值,删除最久不用的 key ,这很符合 LRU 的思想
     if (max && keys.size > parseInt(max as string, 10)) {
       pruneCacheEntry(keys.values().next().value)
    }
  }
   // 避免 vNode 被卸载
   vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
   current = vnode
   return vnode;
}
将组件移出缓存表
// 遍历缓存表
function pruneCache(filter?: (name: string) => boolean) {
 cache.forEach((vnode, key) => {
   const name = getComponentName(vnode.type as ConcreteComponent);
   if (name && (!filter || !filter(name))) {
     // !filter(name) 即 name 在 includes 或不在 excludes 中
     pruneCacheEntry(key);
  }
});
}
// 依据 key 值从缓存表中移除对应组件
function pruneCacheEntry(key: CacheKey) {
 const cached = cache.get(key) as VNode;
 if (!current || cached.type !== current.type) {
   /* 当前没有处在 activated 状态的组件
    * 或者当前处在 activated 组件不是要删除的 key 时
    * 卸载这个组件
  */
   unmount(cached); // unmount方法里同样包含了 resetShapeFlag
} else if (current) {
   // 当前组件在未来应该不再被 keepAlive 缓存
   // 虽然仍在 keepAlive 的容量中但是需要刷新当前组件的优先级
   resetShapeFlag(current);
   // resetShapeFlag
}
 cache.delete(key);
 keys.delete(key);
}
function resetShapeFlag(vnode: VNode) {
 let shapeFlag = vnode.shapeFlag; // shapeFlag 是 VNode 的标识
  // ... 清除组件的 shapeFlag
}

keep-alive案例

本部分将使用 vue 3.x 的新特性来模拟 keep-alive 的具体应用场景

在 index.vue 里我们引入了 CountUp 、timer 和 ColorRandom 三个带有状态的组件 在容量为 2 的 中包裹了一个动态组件

// index.vue
<script setup>
import { ref } from "vue"
import CountUp from '../components/CountUp.vue'
import ColorRandom from '../components/ColorRandom.vue'
import Timer from '../components/Timer.vue'
const tabs = ref([    // 组件列表
{
   title: "ColorPicker",
   comp: ColorRandom,
},
{
   title: "timer1",
   comp: Timer,
},
{
   title: "timer2",
   comp: Timer,
},
{
   title: "CountUp",
   comp: CountUp,
},
])
const currentTab = ref(tabs.value[0]) // tab 默认展示第一个组件
const tabSwitch = (tab) => {
 currentTab.value = tab
}
script>
<template>
 <div id="main-page">keep-alive demo belowdiv>
 <div class="tab-group">
   <button
   v-for="tab in tabs"
   :key="tab"
   :class="['tab-button', { active: currentTab === tab }]"
   @click="tabSwitch(tab)"
 >
   {{ tab.title }}
 button>
 div>
 <keep-alive max="2">
   
   <component
     v-if="currentTab"
     :is="currentTab.comp"
     :key="currentTab.title"
     :name="currentTab.title"
   />
 keep-alive>
template>

缓存状态

缓存流程如下:

缓存流程图

可以看到被包裹在 keep-alive 的动态组件缓存了前一个组件的状态。

通过观察 vue devtools 里节点的变化,可以看到此时 keepAlive 中包含了 ColorRandomTimer 两个组件,当前展示的组件会处在 activated 的状态,而其他被缓存的组件则处在 inactivated 的状态

如果我们注释了两个 keep-alive 会发现不管怎么切换组件,都只会重新渲染,并不会保留前次的状态
keepAlive-cache.gif

移除组件

移除流程如下:

移除流程图

为了验证组件是否在切换tab时能被成功卸载,在每个组件的 onUnmounted 中加上了 log

onUnmounted(()=>{
 console.log(`${props.name} 组件被卸载`)
})
  • 当缓存数据长度小于等于 max ,切换组件并不会卸载其他组件,就像上面在 vue devtools 里展示的一样,只会触发组件的 activateddeactivated 两个生命周期

  • 若此时缓存数据长度大于 max ,则会从缓存列表中删除优先级较低的,优先被淘汰的组件,对应的可以看到该组件 umounted 生命周期触发。

性能优化

使用 KeepAlive 后,被 KeepAlive 包裹的组件在经过第一次渲染后,的 vnode 以及 DOM 都会被缓存起来,然后再下一次再次渲染该组件的时候,直接从缓存中拿到对应的 vnode 和 DOM,然后渲染,并不需要再走一次组件初始化,render 和 patch 等一系列流程,减少了 script 的执行时间,性能更好。

总结

Vue 内部将 DOM 节点抽象成了一个个的 VNode 节点,keep-alive 组件的缓存也是基于 VNode 节点的而不是直接存储 DOM 结构。它将满足条件( include 与 exclude )的组件在 cache 对象中缓存起来,在需要重新渲染的时候再将 vnode 节点从 cache 对象中取出并渲染。

具体缓存过程如下:

  1. 声明有序集合 keys 作为缓存容器,存入组件的唯一 key 值

  2. 在缓存容器 keys 中,越靠前的 key 值意味着被访问的越少也越优先被淘汰

  3. 渲染函数执行时,若命中缓存时,则从 keys 中删除当前命中的 key,并往 keys 末尾追加 key 值,刷新该 key 的优先级

  4. 未命中缓存时,则 keys 追加缓存数据 key 值,若此时缓存数据长度大于 max 最大值,则删除最旧的数据

  5. 当触发 beforeMount/update 生命周期,缓存当前 activated 组件的子树的数据


参考

作者:政采云前端团队
来源:https://juejin.cn/post/7036483610920091656

收起阅读 »

Android 关键字高亮

前言项目中经常会遇到需要对关键字加特殊色值显示,不管是搜索内容还是列表关键字展示,对于特殊文字或者词组高亮是一种很常见的需求,Android 没有自带这样的工具或者组件提供,但是我们可以自己实现一个这样的工具类,用到的地方直接调用就好了。文字高亮所谓文字高亮,...
继续阅读 »

前言

项目中经常会遇到需要对关键字加特殊色值显示,不管是搜索内容还是列表关键字展示,对于特殊文字或者词组高亮是一种很常见的需求,Android 没有自带这样的工具或者组件提供,但是我们可以自己实现一个这样的工具类,用到的地方直接调用就好了。

文字高亮

所谓文字高亮,其实就是针对某个字符做特殊颜色显示,下面列举几种常见的实现方式

一、通过加载Html标签,显示高亮

Android 的TextView 可以加载带Html标签的段落,方法:

textView.setText(Html.fromHtml("<font color='red' size='24'>Hello World</font>"));

那么要高亮显示关键字,就可以这样实现,把需要高亮显示的关键字,通过这样的方式,组合起来就好了,例如:

textView.setText(“这是我的第一个安卓项目” + Html.fromHtml("<font color='red' size='24'>Hello World</font>"));

二、通过SpannableString来实现文本高亮

先来简单了解下SpannableString

SpannableString的基本使用代码示例:

//设置Url地址连接
private void addUrlSpan() {
SpannableString spanString = new SpannableString("超链接");
URLSpan span = new URLSpan("tel:0123456789");
spanString.setSpan(span, 0, 3, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
tv.append(spanString);
}

//设置字体背景的颜色
private void addBackColorSpan() {
SpannableString spanString = new SpannableString("文字背景颜色");
BackgroundColorSpan span = new BackgroundColorSpan(Color.YELLOW);
spanString.setSpan(span, 0, 3, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
tv.append(spanString);
}
//设置字体的颜色
private void addForeColorSpan() {
SpannableString spanString = new SpannableString("文字前景颜色");
ForegroundColorSpan span = new ForegroundColorSpan(Color.BLUE);
spanString.setSpan(span, 0, 3, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
tv.append(spanString);
}
//设置字体的大小
private void addFontSpan() {
SpannableString spanString = new SpannableString("36号字体");
AbsoluteSizeSpan span = new AbsoluteSizeSpan(36);
spanString.setSpan(span, 0, 5, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
tv.append(spanString);
}

以上是比较常用的,还有其他例如设置字体加粗,下划线,删除线等,都可以实现

我们这里主要用到给字体设置背景色,通过正则表达式匹配关键字,设置段落中匹配到的关键字高亮

/***
* 指定关键字高亮 字符串整体高亮
* @param originString 原字符串
* @param keyWords 高亮字符串
* @param highLightColor 高亮色值
* @return 高亮后的字符串
*/
public static SpannableString getHighLightWord(String originString, String keyWords, int highLightColor) {
SpannableString originSpannableString = new SpannableString(originString);
if (!TextUtils.isEmpty(keyWords)) {
Pattern pattern = Pattern.compile(keyWords);
Matcher matcher = pattern.matcher(originSpannableString);
while (matcher.find()) {
int startIndex = matcher.start();
int endIndex = matcher.end();
originSpannableString.setSpan(new ForegroundColorSpan(highLightColor), startIndex, endIndex, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
return originSpannableString;
}

在扩展一下,可以支持关键字,关键词拆分显示

类似:测试1234测1234试(测试为高亮字,实现测试/测/试分别高亮)

/***
* 指定关键字高亮 支持分段高亮
* @param originString
* @param keyWords
* @param highLightColor
* @return
*/
public static SpannableString getHighLightWords(String originString, String keyWords, int highLightColor) {
SpannableString originSpannableString = new SpannableString(originString);
if (!TextUtils.isEmpty(keyWords)) {
for (int i = 0; i < keyWords.length(); i++) {
Pattern p = Pattern.compile(String.valueOf(keyWords.charAt(i)));
Matcher m = p.matcher(originSpannableString);
while (m.find()) {
int start = m.start();
int end = m.end();
originSpannableString.setSpan(new ForegroundColorSpan(highLightColor), start, end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
}
return originSpannableString;
}

字符可以,那么数组呢,是不是也可以实现了?

/***
* 指定关键字数组高亮
* @param originString 原字符串
* @param keyWords 高亮字符串数组
* @param highLightColor 高亮色值
* @return 高亮后的字符串
*/
public static SpannableString getHighLightWordsArray(String originString, String[] keyWords, int highLightColor) {
SpannableString originSpannableString = new SpannableString(originString);
if (keyWords != null && keyWords.length > 0) {
for (int i = 0; i < keyWords.length; i++) {
Pattern p = Pattern.compile(keyWords[i]);
Matcher m = p.matcher(originSpannableString);
while (m.find()) {
int start = m.start();
int end = m.end();
originSpannableString.setSpan(new ForegroundColorSpan(highLightColor), start, end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
}
return originSpannableString;
}

总结

这样不管来什么需求,是不是都可以满足了,随便产品经理提,要什么给什么

收起阅读 »

聊一聊Android开发利器之adb

学无止境,有一技旁身,至少不至于孤陋寡闻。adb的全称为Android Debug Bridge,通过adb可以方便我们调试Android程序。作为移动端开发相关的同学,掌握所需要的adb操作命令是非常必须的,就把平时工作中用得相对比较多的adb命令做个梳理。...
继续阅读 »

学无止境,有一技旁身,至少不至于孤陋寡闻。

adb的全称为Android Debug Bridge,通过adb可以方便我们调试Android程序。作为移动端开发相关的同学,掌握所需要的adb操作命令是非常必须的,就把平时工作中用得相对比较多的adb命令做个梳理。(日常中把adb操作命令搭配shell alias使用起来更方便)

ADB常用命令

1.启动/停止adb server命令

adb start-server  //启动命令
adb kill-server //停止命令

2. 通过adb查看设备相关信息

  1. 查询已连接设备/模拟器
    adb devices
  2. 查看手机型号
    adb shell getprop ro.product.model
  3. 查看电池状况
    adb shell dumpsys battery
  4. 查看屏幕分辨率
    adb shell wm size
  5. 查看屏幕密度
    adb shell wm density
  6. 查看显示屏参数
    adb shell dumpsys window displays
  7. 查看Android系统版本
    adb shell getprop ro.build.version.release
  8. 查看CPU信息
    adb shell cat /proc/cpuinfo
  9. 查看手机CPU架构
    adb shell getprop ro.product.cpu.abi
  10. 查看内存信息
    adb shell cat /proc/meminfo

3. 通过adb连接设备命令

adb [-d|-e|-s ]
如果只有一个设备/模拟器连接时,可以省略掉 [-d|-e|-s ] 这一部分,直接使用 adb即可 。 如果有多个设备/模拟器连接,则需要为命令指定目标设备。

参数含义
-d指定当前唯一通过 USB 连接的 Android 设备为命令目标
-e指定当前唯一运行的模拟器为命令目标
-s <serialNumber>指定相应 serialNumber 号的设备/模拟器为命令目标
在多个设备/模拟器连接的情况下较常用的是-s参数,serialNumber 可以通过adb devices命令获取。如:
$ adb devices
List of devices attached
cfxxxxxx device
emulator-5554 device
10.xxx.xxx.x:5555 device

输出里的 cfxxxxxxemulator-5554 和 10.xxx.xxx.x:5555 即为 serialNumber。 比如这时想指定 cfxxxxxx 这个设备来运行 adb 命令 获取屏幕分辨率:

adb -s cfxxxxxx shell wm size

安装应用:

adb -s cfxxxxxx install hello.apk

遇到多设备/模拟器的情况均使用这几个参数为命令指定目标设备。

4. 通过adb在设备上操作应用相关

  1. 安装 APK

    adb install [-rtsdg] <apk_path>

    参数:
    adb install 后面可以跟一些可选参数来控制安装 APK 的行为,可用参数及含义如下:

    参数含义
    -r允许覆盖安装
    -t允许安装 AndroidManifest.xml 里 application 指定 android:testOnly="true" 的应用
    -s将应用安装到 sdcard
    -d允许降级覆盖安装
    -g授予所有运行时权限
  2. 卸载应用

    adb uninstall [-k] <packagename>

    <packagename> 表示应用的包名,-k 参数可选,表示卸载应用但保留数据和缓存目录。

    adb uninstall com.vic.dynamicview
  3. 强制停止应用

    adb shell am force-stop <packagename>

    命令示例:

    adb shell am force-stop com.vic.dynamicview
  4. 调起对应的Activity

    adb shell am start [options] <INTENT>

    例如:

    adb shell am start -n com.vic.dynamicview/.MainActivity --es "params" "hello, world"

    表示调起 com.vic.dynamicview/.MainActivity 并传给它 string 数据键值对 params - hello, world。

  5. 查看前台 Activity

    adb shell dumpsys activity activities | grep ResumedActivity

    查看activity堆栈信息: adb shell dumpsys activity

    ACTIVITY MANAGER PENDING INTENTS (adb shell dumpsys activity intents)
    ...
    ACTIVITY MANAGER BROADCAST STATE (adb shell dumpsys activity broadcasts)
    ...
    ACTIVITY MANAGER CONTENT PROVIDERS (adb shell dumpsys activity providers)
    ...
    ACTIVITY MANAGER SERVICES (adb shell dumpsys activity services)
    ...
    ACTIVITY MANAGER ACTIVITIES (adb shell dumpsys activity activities)
    ...
    ACTIVITY MANAGER RUNNING PROCESSES (adb shell dumpsys activity processes)
    ...
  6. 打开系统设置:
    adb shell am start -n com.android.settings/com.android.settings.Settings

  7. 打开开发者选项:
    adb shell am start -a com.android.settings.APPLICATION_DEVELOPMENT_SETTINGS

  8. 进入WiFi设置
    adb shell am start -a android.settings.WIRELESS_SETTINGS

  9. 重启系统
    adb reboot

5. 通过adb操作日志相关

  1. logcathelp帮助信息
    adb logcat --help 可以查看logcat帮助信息
    adb logcat 命令格式: adb logcat [选项] [过滤项], 其中 选项 和 过滤项 在 中括号 [] 中, 说明这是可选的;

  2. 输出日志信息到文件:
    ">"输出 :
    ">" 后面跟着要输出的日志文件, 可以将 logcat 日志输出到文件中, 使用 adb logcat > log 命令, 使用 more log 命令查看日志信息;
    如:adb logcat > ~/logdebug.log

  3. 输出指定标签内容:
    "-s"选项 : 设置默认的过滤器, 如 我们想要输出 "System.out" 标签的信息, 就可以使用 adb logcat -s System.out 命令;

  4. 清空日志缓存信息:
    使用 adb logcat -c 命令, 可以将之前的日志信息清空, 重新开始输出日志信息;

  5. 输出缓存日志:
    使用 adb logcat -d 命令, 输出命令, 之后退出命令, 不会进行阻塞;

  6. 输出最近的日志:
    使用 adb logcat -t 5 命令, 可以输出最近的5行日志, 并且不会阻塞;

  7. 日志过滤:
    注意:在windows上不能使用grep关键字,可以用findstr代替grep.

    • 过滤固定字符串:
      adb logcat | grep logtag
      adb logcat | grep -i logtag #忽略大小写。
      adb logcat | grep logtag > ~/result.log #将过滤后的日志输出到文件
      adb logcat | grep --color=auto -i logtag #设置匹配字符串颜色。

    • 使用正则表达式匹配
      adb logcat | grep "^..Activity"

ADB其他命令

1. 清除应用数据与缓存

adb shell pm clear <packagename>

<packagename> 表示应用名包,这条命令的效果相当于在设置里的应用信息界面点击了「清除缓存」和「清除数据」。

adb shell pm clear com.xxx.xxx

2. 与应用交互操作

主要是使用 am <command> 命令,常用的 <command> 如下:

command用途
start [options] <INTENT>启动 <INTENT> 指定的 Activity
startservice [options] <INTENT>启动 <INTENT> 指定的 Service
broadcast [options] <INTENT>发送 <INTENT> 指定的广播
force-stop <packagename>停止 <packagename> 相关的进程

<INTENT> 参数很灵活,和写 Android 程序时代码里的 Intent 相对应。

用于决定 intent 对象的选项如下:

参数含义
-a <ACTION>指定 action,比如 android.intent.action.VIEW
-c <CATEGORY>指定 category,比如 android.intent.category.APP_CONTACTS
-n <COMPONENT>指定完整 component 名,用于明确指定启动哪个 Activity,如 com.example.app/.ExampleActivity

<INTENT> 里还能带数据,就像写代码时的 Bundle 一样:

参数含义
--esn <EXTRA_KEY>null 值(只有 key 名)
-e--es <EXTRA_KEY> <EXTRA_STRING_VALUE>`
--ez <EXTRA_KEY> <EXTRA_BOOLEAN_VALUE>boolean 值
--ei <EXTRA_KEY> <EXTRA_INT_VALUE>integer 值
--el <EXTRA_KEY> <EXTRA_LONG_VALUE>long 值
--ef <EXTRA_KEY> <EXTRA_FLOAT_VALUE>float 值
--eu <EXTRA_KEY> <EXTRA_URI_VALUE>URI
--ecn <EXTRA_KEY> <EXTRA_COMPONENT_NAME_VALUE>component name
--eia <EXTRA_KEY> <EXTRA_INT_VALUE>[,<EXTRA_INT_VALUE...]integer 数组
--ela <EXTRA_KEY> <EXTRA_LONG_VALUE>[,<EXTRA_LONG_VALUE...]long 数组
  1. 调起Activity

    adb shell am start [options] <INTENT>

    例如:

    adb shell am start -n com.cc.test/.MainActivity --es "params" "hello, world"

    表示调起 com.cc.test/.MainActivity 并传给它 string 数据键值对 params - hello, world。

  2. 调起Service

    adb shell am startservice [options] <INTENT>

    例如:

    adb shell am startservice -n com.tencent.mm/.plugin.accountsync.model.AccountAuthenticatorService
  3. 发送广播

    adb shell am broadcast [options] <INTENT>

    可以向所有组件广播,也可以只向指定组件广播。 例如,向所有组件广播 BOOT_COMPLETED:

    adb shell am broadcast -a android.intent.action.BOOT_COMPLETED

    又例如,只向 com.cc.test/.BootCompletedReceiver 广播 BOOT_COMPLETED:

    adb shell am broadcast -a android.intent.action.BOOT_COMPLETED -n com.cc.test/.BootCompletedReceiver
  4. 撤销应用程序的权限

    1. 向应用授予权限。只能授予应用程序声明的可选权限
    adb shell pm grant <packagename> <PACKAGE_PERMISSION>

    例如:adb -d shell pm grant packageName android.permission.BATTERY_STATS

    1. 取消应用授权
    adb shell pm revoke <packagename> <PACKAGE_PERMISSION>

3. 模拟按键/输入

Usage: input [<source>] <command> [<arg>...]

The sources are:
mouse
keyboard
joystick
touchnavigation
touchpad
trackball
stylus
dpad
gesture
touchscreen
gamepad

The commands and default sources are:
text <string> (Default: touchscreen)
keyevent [--longpress] <key code number or name> ... (Default: keyboard)
tap <x> <y> (Default: touchscreen)
swipe <x1> <y1> <x2> <y2> [duration(ms)] (Default: touchscreen)
press (Default: trackball)
roll <dx> <dy> (Default: trackball)

比如模拟点击://在屏幕上点击坐标点x=50 y=250的位置。

adb shell input tap 50 250

结合shell alias使用adb

shell终端的别名只是命令的简写,有类似键盘快捷键的效果。如果你经常执行某个长长的命令,可以给它起一个简短的化名。使用alias命令列出所有定义的别名。
你可以在~/.bashrc(.zshrc)文件中直接定义别名如alias logRunActivity="adb shell dumpsys activity activities | grep 'Run*'",也可以新创建一个文件如.byterc, 然后在当前shell对应的文件中.bashrc或者.zshrc 中增加source ~/.byterc,重新source配置,使得配置生效,即可使别名全局生效。使用别名可以节省时间、提高工作效率。

如何添加别名alias

下面在MAC环境采用新建文件形式添加别名,步骤如下:

  1. 新建.byterc 文件
    • 如果已经新建,直接打开
      open ~/.byterc
    • 没有新建,则新建后打开
      新建: touch ~/.byterc
      打开:open ~/.byterc
  2. 在.zshrc中添加source ~/.byterc
  3. 在打开的.byterc文件中定义别名
    alias logRunActivity="adb shell dumpsys activity activities | grep 'Run*'"
    Android同学应该知道作用就是查看当前设备运行的Activity信息
  4. 重新source配置,使得配置生效
    $ source ~/.byterc
    如果不是新建文件,直接使用.bashrc或者.zshrc ,直接source对应的配置即可,如:$ source ~/.zshrc .
  5. 此时在命令行中直接执行logRunActivity 即可查看当前设备运行的Activity信息。

注意: 可使用$ alias查看当前有设置哪些别名操作。


收起阅读 »

Swift 中的 Self & Self.Type & self

iOS
Swift 中的 Self & Self.Type & self这是我参与11月更文挑战的第13天,活动详情查看:2021最后一次更文挑战你可能在写代码的时候已经用过很多次 self 这个关键词了,但是你有没有想过什么是 self 呢?今天我们...
继续阅读 »

Swift 中的 Self & Self.Type & self

这是我参与11月更文挑战的第13天,活动详情查看:2021最后一次更文挑战


你可能在写代码的时候已经用过很多次 self 这个关键词了,但是你有没有想过什么是 self 呢?今天我们就来看看:

  • 什么是 self、Self 和 Self.Type?
  • 都在什么情况下使用?

self

这个大家用的比较多了,self 通常用于当你需要引用你当前所在范围内的对象时。所以,例如,如果在 Rocket 的实例方法中使用 self,在这种情况下,self 将是该 Rocket 的实例。这个很好理解~

struct Rocket {
    func launch() {
        print("10 秒内发射 \(self)")
    }
}

let rocket = Rocket()
rocket.launch() //10 秒内发射 Rocket()

但是,如果要在类方法或静态方法中使用 self,该怎么办?在这种情况下,self 不能作为对实例的引用,因为没有实例,而 self 具有当前类型的值。这是因为静态方法和类方法存在于类型本身而不是实例上。

class Dog {
    class func bark() {
        print("\(self) 汪汪汪!")
    }
}

Dog.bark() //Dog 汪汪汪!


struct Cat {
    static func meow() {
        print("\(self) 喵喵喵!")
    }
}

Cat.meow() // Cat 喵喵喵!


元类型

还有个需要注意的地方。所有的值都应该有一个类型,包括 self。就像上面提到的,静态和类方法存在于类型上,所以在这种情况下,self 就拥有了一种类型:Self.Type。比如:Dog.Type 就保存所有 Dog 的类型值。

包含其他类型的类型称为元类型

有点绕哈,简单来说,元类型 Dog.Type 不仅可以保存 Dog 类型的值,还可以保存它的所有子类的值。比如下面这个例子,其中 Labrador 是 Dog 的一个子类。

class Dog {
    class func bark() {
        print("\(self) 汪汪汪!")
    }
}

class Labrador: Dog {

}

Labrador.bark() //Labrador 汪汪汪!

如果你想将 type 本身当做一个属性,或者将其传递到函数中,那么你也可以将 type 本身作为值使用。这时候,就可以这样用:Type.self。

let dogType: Dog.Type = Labrador.self

func saySomething(dog: Dog.Type) {
    print("\(dog) 汪汪汪!")
}

saySomething(dog: dogType) // Labrador 汪汪汪!


Self

最后,就是大写 s 开头的 Self。在创建工厂方法或从协议方法返回具体类型时,非常的有用:

struct Rocket {
    func launch() {
        print("10 秒内发射 \(self)")
    }
}

extension Rocket {
    static func makeRocket() -> Self {
        return Rocket()
    }
}

protocol Factory {
    func make() -> Self
}

extension Rocket: Factory {
    func make() -> Rocket {
        return Rocket()
    }
}

收起阅读 »

iOS小技能:快速创建OEM项目app

iOS
iOS小技能:快速创建OEM项目app这是我参与11月更文挑战的第29天,活动详情查看:2021最后一次更文挑战。引言贴牌生产(英语:Original Equipment Manufacturer, OEM)因采购方可提供品牌和授权,允许制造方生产贴有该品牌的...
继续阅读 »

iOS小技能:快速创建OEM项目app

这是我参与11月更文挑战的第29天,活动详情查看:2021最后一次更文挑战

引言

贴牌生产(英语:Original Equipment Manufacturer, OEM)

因采购方可提供品牌和授权,允许制造方生产贴有该品牌的产品,所以俗称“贴牌生产”。

需求背景: SAAS平台级应用系统为一个特大商户,提供专属OEM项目,在原有通用app的基础上进行定制化开发

例如去掉开屏广告,删除部分模块,保留核心模块。更换专属app icon以及主题色

I 上架资料

  1. 用户协议及隐私政策
  2. App版本、 审核测试账号信息
  3. icon、名称、套装 ID(bundle identifier)
  4. 关键词:
  5. app描述:
  6. 技术支持网址使用:

kunnan.blog.csdn.net/article/det…

II 开发小细节

  1. 更换基础配置信息,比如消息推送证书、第三方SDK的ApiKey、启动图、用户协议及隐私政策。
  2. 接口修改:比如登录接口新增SysId请求字段用于区分新旧版、修改域名(备案信息)
  3. 废弃开屏广告pod 'GDTMobSDK' ,'4.13.26'

1.1 更换高德定位SDK的apiKey

    NSString *AMapKey = @"";
[AMapServices sharedServices].apiKey = AMapKey;


1.2 更新消息推送证书和极光的appKey

  1. Mac 上的“钥匙串访问”创建证书签名请求 (CSR)

a. 启动位于 /Applications/Utilities 中的“钥匙串访问”。

b. 选取“钥匙串访问”>“证书助理”>“从证书颁发机构请求证书”。

c. 在“证书助理”对话框中,在“用户电子邮件地址”栏位中输入电子邮件地址。

d. 在“常用名称”栏位中,输入密钥的名称 (例如,Gita Kumar Dev Key)。

e. 将“CA 电子邮件地址”栏位留空。

f. 选取“存储到磁盘”,然后点按“继续”。

help.apple.com/developer-a…

在这里插入图片描述

  1. 从developer.apple.com 后台找到对应的Identifiers创建消息推送证书,并双击aps.cer安装到本地Mac,然后从钥匙串导出P12的正式上传到极光后台。

docs.jiguang.cn//jpush/clie…在这里插入图片描述

  1. 更换appKey(极光平台应用的唯一标识)
        [JPUSHService setupWithOption:launchOptions appKey:@""
channel:@"App Store"
apsForProduction:YES
advertisingIdentifier:nil];


http://www.jiguang.cn/accounts/lo…

1.3 更换Bugly的APPId

    [Bugly startWithAppId:@""];//异常上报


1.4 app启动的新版本提示

更换appid

    [self checkTheVersionWithappid:@""];


检查版本

在这里插入图片描述


- (void)checkTheVersionWithappid:(NSString*)appid{


[QCTNetworkHelper getWithUrl:[NSString stringWithFormat:@"http://itunes.apple.com/cn/lookup?id=%@",appid] params:nil successBlock:^(NSDictionary *result) {
if ([[result objectForKey:@"results"] isKindOfClass:[NSArray class]]) {
NSArray *tempArr = [result objectForKey:@"results"];
if (tempArr.count) {


NSString *versionStr =[[tempArr objectAtIndex:0] valueForKey:@"version"];
NSString *appStoreVersion = [versionStr stringByReplacingOccurrencesOfString:@"." withString:@""] ;
if (appStoreVersion.length==2) {
appStoreVersion = [appStoreVersion stringByAppendingString:@"0"];
}else if (appStoreVersion.length==1){
appStoreVersion = [appStoreVersion stringByAppendingString:@"00"];
}

NSDictionary *infoDic=[[NSBundle mainBundle] infoDictionary];
NSString* currentVersion = [[infoDic valueForKey:@"CFBundleShortVersionString"] stringByReplacingOccurrencesOfString:@"." withString:@""];

currentVersion = [currentVersion stringByReplacingOccurrencesOfString:@"." withString:@""];
if (currentVersion.length==2) {
currentVersion = [currentVersion stringByAppendingString:@"0"];
}else if (currentVersion.length==1){
currentVersion = [currentVersion stringByAppendingString:@"00"];
}



NSLog(@"currentVersion: %@",currentVersion);


if([self compareVesionWithServerVersion:versionStr]){



UIAlertController *alertController = [UIAlertController alertControllerWithTitle:[NSString stringWithFormat:@"%@%@",QCTLocal(@"Discover_a_new_version"),versionStr] message:QCTLocal(@"Whethertoupdate") preferredStyle:UIAlertControllerStyleAlert];
// "Illtalkaboutitlater"= "稍后再说";
// "Update now" = "立即去更新";
// "Unupdate"= "取消更新";

[alertController addAction:[UIAlertAction actionWithTitle:QCTLocal(@"Illtalkaboutitlater") style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
NSLog(@"取消更新");
}]];
[alertController addAction:[UIAlertAction actionWithTitle:QCTLocal(@"Updatenow") style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"itms-apps://itunes.apple.com/app/id%@",appid]];
if (@available(iOS 10.0, *)) {
[[UIApplication sharedApplication] openURL:url options:@{} completionHandler:^(BOOL success) {
}];
} else {
// Fallback on earlier vesions
[[UIApplication sharedApplication] openURL:url];
}
}]];
[[QCT_Common getCurrentVC] presentViewController:alertController animated:YES completion:nil];
}
}
}
} failureBlock:^(NSError *error) {
NSLog(@"检查版本错误: %@",error);
}];
}


see also

更多内容请关注 #小程序:iOS逆向,只为你呈现有价值的信息,专注于移动端技术研究领域;更多服务和咨询请关注#公众号:iOS逆向

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

收起阅读 »

objc_msgsend(中)方法动态决议

iOS
引入在学习本文之前我们应该了解objc_msgsend消息快速查找(上) objc_msgsend(中)消息慢速查找 当快速消息查找和消息慢速查找都也找不到imp时,苹果系统后续是怎么处理的我们一起来学习! 方法动态决议主要做了哪些事情?准...
继续阅读 »


引入

在学习本文之前我们应该了解

当快速消息查找和消息慢速查找都也找不到imp时,苹果系统后续是怎么处理的我们一起来学习! 方法动态决议主要做了哪些事情?

准备工作

resolveMethod_locked动态方法决议

1.png

  • 赋值imp = forward_imp

  • 做了个单例判断动态控制执行流程根据behavior方法只执行一次。

2.png

对象方法的动态决议

3.png

类方法的动态决议

3.png

lookUpImpOrForwardTryCache

4.png

cache_getImp

5.png

  • 苹果给与一次动态方法决议的机会来挽救APP
  • 如果是类请用resolveInstanceMethod
  • 如果是元类请用resolveClassMethod

如果都没有处理那么imp = forward_imp ,const IMP forward_imp = (IMP)_objc_msgForward_impcache;

_objc_msgForward_impcache探究

6.png

  • __objc_forward_handler主要看这个函数处理

__objc_forward_handler

7.png

代码案例分析

   int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LGTeacher *p = [LGTeacher alloc];
        [t sayHappy];
[LGTeacher saygood];
    }
    return 0;
}


崩溃信息

2021-11-28 22:36:39.223567+0800 KCObjcBuild[12626:762145] +[LGTeacher sayHappy]: unrecognized selector sent to class 0x100008310

2021-11-28 22:36:39.226012+0800 KCObjcBuild[12626:762145] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '+[LGTeacher sayHappy]: unrecognized selector sent to class 0x100008310'

复制代码

动态方法决议处理对象方法找不到

代码动态决议处理imp修复崩溃

@implementation LGTeacher

-(void)text{
    NSLog(@"%s", __func__ );
}

+(void)say777{

    NSLog(@"%s", __func__ );
}

// 对象方法动态决议

+(BOOL**)resolveInstanceMethod:(SEL)sel{

    if (sel == @selector(sayHappy)) {

        IMP imp =class_getMethodImplementation(self, @selector(text));
        Method m = class_getInstanceMethod(self, @selector(text));
        const char * type = method_getTypeEncoding(m);
        return** class_addMethod(self, sel, imp, type);
    }
    return [super resolveInstanceMethod:sel];

}

//类方法动态决议

+ (BOOL)resolveClassMethod:(SEL)sel{
    if (sel == @selector(saygood)) {
        IMP  imp7 = class_getMethodImplementation(objc_getMetaClass("LGTeacher"), @selector(say777));
        Method m  = class_getInstanceMethod(objc_getMetaClass("LGTeacher"), @selector(say777));
        const char type = method_getTypeEncoding(m);
        return class_addMethod(objc_getMetaClass("LGTeacher"), sel, imp7, type);
    }

    return [super resolveClassMethod:sel];

}

@end


运行打印信息

2021-11-29 16:30:46.403671+0800 KCObjcBuild[27071:213498] -[LGTeacher text]

2021-11-29 16:30:46.404186+0800 KCObjcBuild[27071:213498] +[LGTeacher say777]

  • 找不到imp我们动态添加一个imp ,但这样处理太麻烦了。
  • 实例方法方法查找流程 类->父类->NSObject->nil
  • 类方法查找流程 元类->父类->根元类-NsObject->nil

最终都会找到NSobject.我们可以在NSObject统一处理 所以我们可以给NSObject创建个分类

@implementation NSObject (Xu)
+(BOOL)resolveInstanceMethod:(SEL)sel{
if (@selector(sayHello) == sel) {
NSLog(@"--进入%@--",NSStringFromSelector(sel));
IMP imp = class_getMethodImplementation(self , @selector(sayHello2));
Method meth = class_getInstanceMethod(self , @selector(sayHello2));
const char * type = method_getTypeEncoding(meth);
return class_addMethod(self ,sel, imp, type);;

}else if (@selector(test) == sel){
NSLog(@"--进入%@--",NSStringFromSelector(sel));
IMP imp = class_getMethodImplementation(object_getClass([self class]), @selector(newTest));
Method meth = class_getClassMethod(object_getClass([self class]) , @selector(newTest));
const char * type = method_getTypeEncoding(meth);
return class_addMethod(object_getClass([self class]) ,sel, imp, type);;
}
return NO;
}

- (void)sayHello2{
NSLog(@"--%s---",__func__);
}

+(void)newTest{
NSLog(@"--%s---",__func__);
}

@end


实例方法是类方法调用,系统都自动调用了resolveInstanceMethod方法,和上面探究的吻合。 动态方法决议优点

  • 可以统一处理方法崩溃的问题,出现方法崩溃可以上报服务器,或者跳转到首页
  • 如果项目中是不同的模块你可以根据命名不同,进行业务的区别
  • 这种方式叫切面编程熟成AOP

方法动态决议流程图

9.png

问题

  • resolveInstanceMethod为什么调用两次?
  • 统一处理方案怎么处理判断问题,可能是对象方法崩溃也可能是类方法崩溃,怎么处理?
  • 动态方法决议后苹果后续就没有处理了吗?

链接:https://juejin.cn/post/7035965819955707935
收起阅读 »

Android静态代码扫描效率优化与实践(下)

前面分析了如何获取差异文件以及增量扫描的原理,分析的重点还是侧重在Lint工具本身的实现机制上。接下来分析,在Gradle中如何实现一个增量扫描任务。大家知道,通过执行./gradlew lint命令来执行Lint静态代码检测任务。创建一个新的Android工...
继续阅读 »


Android静态代码扫描效率优化与实践(下)
Lint增量扫描Gradle任务实现

前面分析了如何获取差异文件以及增量扫描的原理,分析的重点还是侧重在Lint工具本身的实现机制上。接下来分析,在Gradle中如何实现一个增量扫描任务。大家知道,通过执行./gradlew lint命令来执行Lint静态代码检测任务。创建一个新的Android工程,在Gradle任务列表中可以在Verification这个组下面找到几个Lint任务,如下所示:

Android静态代码扫描效率优化与实践_美团_12

这几个任务就是 Android Gradle插件在加载的时候默认创建的。分别对应于以下几个Task:

  • lint->LintGlobalTask:由TaskManager创建;

  • lintDebug、lintRelease、lintVitalRelease->LintPerVariantTask:由ApplicationTaskManager或者LibraryTaskManager创建,其中lintVitalRelease只在release下生成。

所以,在Android Gradle 插件中,应用于Lint的任务分别为LintGlobalTask和LintPerVariantTask。他们的区别是前者执行的是扫描所有Variant,后者执行只针对单独的Variant。而我们的增量扫描任务其实是跟Variant无关的,因为我们会把所有差异文件都收集到。无论是LintGlobalTask或者是LintPerVariantTask,都继承自LintBaseTask。最终的扫描任务在LintGradleExecution的runLint方法中执行,这个类位于lint-gradle-26.1.1中,前面提到这个库是基于Lint的API针对Gradle任务做的一些封装。

/** Runs lint on the given variant and returns the set of warnings */
  private Pair, LintBaseline> runLint(
          @Nullable Variant variant,
          @NonNull VariantInputs variantInputs,
          boolean report, boolean isAndroid) {
      IssueRegistry registry = createIssueRegistry(isAndroid);
      LintCliFlags flags = new LintCliFlags();
      LintGradleClient client =
              new LintGradleClient(
                      descriptor.getGradlePluginVersion(),
                      registry,
                      flags,
                      descriptor.getProject(),
                      descriptor.getSdkHome(),
                      variant,
                      variantInputs,
                      descriptor.getBuildTools(),
                      isAndroid);
      boolean fatalOnly = descriptor.isFatalOnly();
      if (fatalOnly) {
          flags.setFatalOnly(true);
      }
      LintOptions lintOptions = descriptor.getLintOptions();
      if (lintOptions != null) {
          syncOptions(
                  lintOptions,
                  client,
                  flags,
                  variant,
                  descriptor.getProject(),
                  descriptor.getReportsDir(),
                  report,
                  fatalOnly);
      } else {
          // Set up some default reporters
          flags.getReporters().add(Reporter.createTextReporter(client, flags, null,
                  new PrintWriter(System.out, true), false));
          File html = validateOutputFile(createOutputPath(descriptor.getProject(), null, ".html",
                  null, flags.isFatalOnly()));
          File xml = validateOutputFile(createOutputPath(descriptor.getProject(), null, DOT_XML,
                  null, flags.isFatalOnly()));
          try {
              flags.getReporters().add(Reporter.createHtmlReporter(client, html, flags));
              flags.getReporters().add(Reporter.createXmlReporter(client, xml, false));
          } catch (IOException e) {
              throw new GradleException(e.getMessage(), e);
          }
      }
      if (!report || fatalOnly) {
          flags.setQuiet(true);
      }
      flags.setWriteBaselineIfMissing(report && !fatalOnly);

      Pair, LintBaseline> warnings;
      try {
          warnings = client.run(registry);
      } catch (IOException e) {
          throw new GradleException("Invalid arguments.", e);
      }

      if (report && client.haveErrors() && flags.isSetExitCode()) {
          abort(client, warnings.getFirst(), isAndroid);
      }

      return warnings;
  }

我们在这个方法中看到了warnings = client.run(registry),这就是Lint扫描得到的结果集。总结一下这个方法中做了哪些准备工作用于Lint扫描:

  1. 创建IssueRegistry,包含了Lint内建的BuiltinIssueRegistry;

  2. 创建LintCliFlags;

  3. 创建LintGradleClient,这里面传入了一大堆参数,都是从Gradle Android 插件的运行环境中获得;

  4. 同步LintOptions,这一步是将我们在build.gralde中配置的一些Lint相关的DSL属性,同步设置给LintCliFlags,给真正的Lint 扫描核心库使用;

  5. 执行Client的Run方法,开始扫描。

扫描的过程上面的原理部分已经分析了,现在我们思考一下如何构造增量扫描的任务。我们已经分析到扫描的关键点是client.run(registry),所以我们需要构造一个Client来执行扫描。一个想法是通过反射来获取Client的各个参数,当然这个思路是可行的,我们也验证过实现了一个用反射方式构造的Client。但是反射这种方式有个问题是丢失了从Gradle任务执行到调用Lint API开始扫描这一过程中做的其他事情,侵入性比较高,所以我们最终采用继承LintBaseTask自行实现增量扫描任务的方式。

FindBugs扫描简介

FindBugs是一个静态分析工具,它检查类或者JAR 文件,通过Apache的 BCEL 库来分析Class,将字节码与一组缺陷模式进行对比以发现问题。FindBugs自身定义了一套缺陷模式,目前的版本3.0.1内置了总计300多种缺陷,详细可参考 官方文档 。FindBugs作为一个扫描的工具集,可以非常灵活的集成在各种编译工具中。接下来,我们主要分析在Gradle中FindBugs的相关内容。

Gradle FindBugs任务属性分析

在Gradle的内置任务中,有一个FindBugs的Task,我们看一下 官方文档 对Gradle属性的描述。

选几个比较重要的属性介绍:

  • Classes

该属性表示我们要分析的Class文件集合,通常我们会把编译结果的Class目录用于扫描。
  • Classpath

分析目标集合中的Class需要用到的所有相关的Classes路径,但是并不会分析它们自身,只用于扫描。
  • Effort

包含MIN,Default,MAX,级别越高,分析得越严谨越耗时。
  • findbugsClasspath

Finbugs库相关的依赖路径,用于配置扫描的引擎库。
  • reportLevel

报告级别,分为Low,Medium,High。如果为Low,所有Bug都报告,如果为High,仅报告High优先级。
  • Reports

扫描结果存放路径。

通过以上属性解释,不难发现要FindBugs增量扫描,只需要指定Classes的文件集合就可以了。

FindBugs任务增量扫描分析

在做增量扫描任务之前,我们先来看一下FindBugs IDEA插件是如何进行单个文件扫描的。

Android静态代码扫描效率优化与实践_Android教程_13

我们选择Analyze Current File对当前文件进行扫描,扫描结果如下所示:

Android静态代码扫描效率优化与实践_Android教程_14

可以看到确实只扫描了一个文件。那么扫描到底使用了哪些输入数据呢,我们可以通过扫描结果的提示清楚看到:

Android静态代码扫描效率优化与实践_美团_15

这里我们能看到很多有用的信息:

  • 源码目录列表,包含了工程中的Java目录,res目录,以及编译过程中生成的一些类目录;

  • 需要分析的目标Class集合,为编译后的Build目录下的当前Java文件对应的Class文件;

  • Aux Classpath Entries,表示分析上面的目标文件需要用到的类路径。

所以,根据IDEA的扫描结果来看,我们在做增量扫描的时候需要解决上面这几个属性的获取。在前面我们分析的属性是Gradle在FindBugs lib的基础上,定义的一套对应的Task属性。真正的Finbugs属性我们可以通过 官方文档 或者源码中查到。

配置AuxClasspath

前文提到,ClassPath是用来分析目标文件需要用到的相关依赖Class,但本身并不会被分析,所以我们需要尽可能全的找到所有的依赖库,否则在扫描的时候会报依赖的类库找不到。

FileCollection buildClasses = project.fileTree(dir: "${project.buildDir}/intermediates/classes/${variant.flavorName}/${variant.buildType.name}",includes: classIncludes)

FileCollection targetClasspath = project.files()
GradleUtils.collectDepProject(project, variant).each { targetProject ->
  GradleUtils.getAndroidVariants(targetProject).each { targetVariant ->
      if (targetVariant.name.capitalize().equalsIgnoreCase(variant.name.capitalize())) {
          targetClasspath += targetVariant.javaCompile.classpath
      }
  }
}

classpath = variant.javaCompile.classpath + targetClasspath + buildClasses
FindBugs增量扫描误报优化

对于增量文件扫描,参与的少数文件扫描在某些模式规则上可能会出现误判,但是全量扫描不会有问题,因为参与分析的目标文件是全集。举一个例子:

class A {
public static String buildTime = "";
....
}

静态变量buildTime会被认为应该加上Final,但是其实其他类会对这个变量赋值。如果单独扫描类A文件,就会报缺陷BUG_TYPE_MS_SHOULD_BE_FINAL。我们通过FindBugs-IDEA插件来扫描验证,也同样会有一样的问题。要解决此类问题,需要找到谁依赖了类A,并且一同参与扫描,同时也需要找出类A依赖了哪些文件,简单来说:需要找出与类A有直接关联的类。为了解决这个问题,我们通过ASM来找出相关的依赖,具体如下:

void findAllScanClasses(ConfigurableFileTree allClass) {
  allScanFiles = [] as HashSet
  String buildClassDir = "${project.buildDir}/$FINDBUGS_ANALYSIS_DIR/$FINDBUGS_ANALYSIS_DIR_ORIGIN"

  Set moduleClassFiles = allClass.files
  for (File file : moduleClassFiles) {
      String[] splitPath = file.absolutePath.split("$FINDBUGS_ANALYSIS_DIR/$FINDBUGS_ANALYSIS_DIR_ORIGIN/")
      if (splitPath.length > 1) {
          String className = getFileNameNoFlag(splitPath[1],'.')
          String innerClassPrefix = ""
          if (className.contains('$')) {
              innerClassPrefix = className.split('\\$')[0]
          }
          if (diffClassNamePath.contains(className) || diffClassNamePath.contains(innerClassPrefix)) {
              allScanFiles.add(file)
          } else {
              Iterable classToResolve = new ArrayList()
              classToResolve.add(file.absolutePath)
              Set dependencyClasses = Dependencies.findClassDependencies(project, new ClassAcceptor(), buildClassDir, classToResolve)
              for (File dependencyClass : dependencyClasses) {
                  if (diffClassNamePath.contains(getPackagePathName(dependencyClass))) {
                      allScanFiles.add(file)
                      break
                  }
              }
          }
      }
  }
}

通过以上方式,我们可以解决一些增量扫描时出现的误报情况,相比IDEA工具,我们更进一步降低了扫描部分文件的误报率。

CheckStyle增量扫描

相比而言,CheckStyle的增量扫描就比较简单了。CheckStyle对源码扫描,根据[ 官方文档]各个属性的描述,我们发现只要指定Source属性的值就可以指定扫描的目标文件。

void configureIncrementScanSource() {
  boolean isCheckPR = false
  DiffFileFinder diffFileFinder

  if (project.hasProperty(CodeDetectorExtension.CHECK_PR)) {
      isCheckPR = project.getProperties().get(CodeDetectorExtension.CHECK_PR)
  }

  if (isCheckPR) {
      diffFileFinder = new DiffFileFinderHelper.PRDiffFileFinder()
  } else {
      diffFileFinder = new DiffFileFinderHelper.LocalDiffFileFinder()
  }

  source diffFileFinder.findDiffFiles(project)

  if (getSource().isEmpty()) {
      println '没有找到差异java文件,跳过checkStyle检测'
  }
}

优化结果数据

经过全量扫描和增量扫描的优化,我们整个扫描效率得到了很大提升,一次PR构建扫描效率整体提升50%+。优化数据如下:

Android静态代码扫描效率优化与实践_Android教程_16

落地与沉淀

扫描工具通用性

解决了扫描效率问题,我们想怎么让更多的工程能低成本的使用这个扫描插件。对于一个已经存在的工程,如果没有使用过静态代码扫描,我们希望在接入扫描插件后续新增的代码能够保证其经过增量扫描没有问题。而老的存量代码,由于代码量过大增量扫描并没有效率上的优势,我们希望可以使用全量扫描逐步解决存量代码存在的问题。同时,为了配置工具的灵活,也提供配置来让接入方自己决定选择接入哪些工具。这样可以让扫描工具同时覆盖到新老项目,保证其通用。所以,要同时支持配置使用增量或者全量扫描任务,并且提供灵活的选择接入哪些扫描工具

扫描完整性保证

前面提到过,在FindBugs增量扫描可能会出现因为参与分析的目标文件集不全导致的某类匹配规则误报,所以在保证扫描效率的同时,也要保证扫描的完整性和准确性。我们的策略是以增量扫描为主,全量扫描为辅,PR提交使用增量扫描提高效率,在CI配置Daily Build使用全量扫描保证扫描完整和不遗漏

我们在自己的项目中实践配置如下:

apply plugin: 'code-detector'

codeDetector {
  // 配置静态代码检测报告的存放位置
  reportRelativePath = rootProject.file('reports')

  /**
    * 远程仓库地址,用于配置提交pr时增量检测
    */
  upstreamGitUrl = "ssh://git@xxxxxxxx.git"

  checkStyleConfig {
      /**
        * 开启或关闭 CheckStyle 检测
        * 开启:true
        * 关闭:false
        */
      enable = true
      /**
        * 出错后是否要终止检查
        * 终止:false
        * 不终止:true。配置成不终止的话 CheckStyleTask 不会失败,也不会拷贝错误报告
        */
      ignoreFailures = false
      /**
        * 是否在日志中展示违规信息
        * 显示:true
        * 不显示:false
        */
      showViolations = true
      /**
        * 统一配置自定义的 checkstyle.xml 和 checkstyle.xsl 的 uri
        * 配置路径为:
        *     "${checkStyleUri}/checkstyle.xml"
        *     "${checkStyleUri}/checkstyle.xsl"
        *
        * 默认为 null,使用 CodeDetector 中的默认配置
        */
      checkStyleUri = rootProject.file('codequality/checkstyle')
  }

  findBugsConfig {
      /**
        * 开启或关闭 Findbugs 检测
        * 开启:true
        * 关闭:false
        */
      enable = true
      /**
        * 可选项,设置分析工作的等级,默认值为 max
        * min, default, or max. max 分析更严谨,报告的 bug 更多. min 略微少些
        */
      effort = "max"
      /**
        * 可选项,默认值为 high
        * low, medium, high. 如果是 low 的话,那么报告所有的 bug
        */
      reportLevel = "high"
      /**
        * 统一配置自定义的 findbugs_include.xml 和 findbugs_exclude.xml 的 uri
        * 配置路径为:
        *     "${findBugsUri}/findbugs_include.xml"
        *     "${findBugsUri}/findbugs_exclude.xml"
        * 默认为 null,使用 CodeDetector 中的默认配置
        */
      findBugsUri = rootProject.file('codequality/findbugs')
  }

  lintConfig {

      /**
        * 开启或关闭 lint 检测
        * 开启:true
        * 关闭:false
        */
      enable = true

      /**
        * 统一配置自定义的 lint.xml 和 retrolambda_lint.xml 的 uri
        * 配置路径为:
        *     "${lintConfigUri}/lint.xml"
        *     "${lintConfigUri}/retrolambda_lint.xml"
        * 默认为 null,使用 CodeDetector 中的默认配置
        */
      lintConfigUri = rootProject.file('codequality/lint')
  }
}

我们希望扫描插件可以灵活指定增量扫描还是全量扫描以应对不同的使用场景,比如已存在项目的接入、新项目的接入、打包时的检测等。

执行脚本示例:

./gradlew ":${appModuleName}:assemble${ultimateVariantName}" -PdetectorEnable=true -PcheckStyleIncrement=true -PlintIncrement=true -PfindBugsIncrement=true -PcheckPR=${checkPR} -PsourceCommitHash=${sourceCommitHash} -PtargetBranch=${targetBranch} --stacktrace

希望一次任务可以暴露所有扫描工具发现的问题,当某一个工具扫描到问题后不终止任务,如果是本地运行在发现问题后可以自动打开浏览器方便查看问题原因。

def finalizedTaskArray = [lintTask,checkStyleTask,findbugsTask]
checkCodeTask.finalizedBy finalizedTaskArray

"open ${reportPath}".execute()

为了保证提交的PR不会引起打包问题影响包的交付,在PR时触发的任务实际为打包任务,我们将静态代码扫描任务挂接在打包任务中。由于我们的项目是多Flavor构建,在CI上我们将触发多个Job同时执行对应Flavor的增量扫描和打包任务。同时为了保证代码扫描的完整性,我们在真正的打包Job上执行全量扫描。

总结与展望

本文主要介绍了在静态代码扫描优化方面的一些思路与实践,并重点探讨了对Lint、FindBugs、CheckStyle增量扫描的一些尝试。通过对扫描插件的优化,我们在代码扫描的效率上得到了提升,同时在实践过程中我们也积累了自定义Lint检测规则的方案,未来我们将配合基础设施标准化建设,结合静态扫描插件制定一些标准化检测规则来更好的保证我们的代码规范以及质量。

参考资料

作者简介

鸿耀,美团餐饮生态技术团队研发工程师。

作者:美团技术团队 · 鸿耀
来源:https://blog.51cto.com/u_15197658/2768467

收起阅读 »

Android静态代码扫描效率优化与实践(上)

背景与问题思考与策略思考一:现有插件包含的扫描工具是否都是必需的?为了验证扫描工具的必要性,我们关心以下一些维度:经过以上的对比分析我们发现,工具的诞生都能针对性解决某一领域问题。CheckStyle的扫描速度快效率高,对代码风格和圈复杂度支持友好;FindB...
继续阅读 »



小伙伴们,美美又来推荐干货文章啦~本文为美团研发同学实战经验,主要介绍Android静态扫描工具Lint、CheckStyle、FindBugs在扫描效率优化上的一些探索和实践,希望大家喜欢鸭。

背景与问题

DevOps实践中,我们在CI(Continuous Integration)持续集成过程主要包含了代码提交、静态检测、单元测试、编译打包环节。其中静态代码检测可以在编码规范,代码缺陷,性能等问题上提前预知,从而保证项目的交付质量。Android项目常用的静态扫描工具包括CheckStyle、Lint、FindBugs等,为降低接入成本,美团内部孵化了静态代码扫描插件,集合了以上常用的扫描工具。项目初期引入集团内部基建时我们接入了代码扫描插件,在PR(Pull Request)流程中借助Jenkins插件来触发自动化构建,从而达到监控代码质量的目的。初期单次构建耗时平均在1~2min左右,对研发效率影响甚少。但是随着时间推移,代码量随业务倍增,项目也开始使用Flavor来满足复杂的需求,这使得我们的单次PR构建达到了8~9min左右,其中静态代码扫描的时长约占50%,持续集成效率不高,对我们的研发效率带来了挑战。

思考与策略

针对以上的背景和问题,我们思考以下几个问题:

思考一:现有插件包含的扫描工具是否都是必需的?

扫描工具对比

为了验证扫描工具的必要性,我们关心以下一些维度:

  • 扫码侧重点,对比各个工具分别能针对解决什么类型的问题;

  • 内置规则种类,列举各个工具提供的能力覆盖范围;

  • 扫描对象,对比各个工具针对什么样的文件类型扫描;

  • 原理简介,简单介绍各个工具的扫描原理;

  • 优缺点,简单对比各个工具扫描效率、扩展性、定制性、全面性上的表现。

Android静态代码扫描效率优化与实践_美团_03

注:FindBugs只支持Java1.0~1.8,已经被SpotBugs替代。鉴于部分老项目并没有迁移到Java8,目前我们并没有使用SpotBugs代替FindBugs的原因如下,详情参考 官方文档
Android静态代码扫描效率优化与实践_美团_04
同时,SpotBugs的作者也在 讨论是否让SpotBugs支持老的Java版本,结论是不提供支持。

经过以上的对比分析我们发现,工具的诞生都能针对性解决某一领域问题。CheckStyle的扫描速度快效率高,对代码风格和圈复杂度支持友好;FindBugs针对Java代码潜在问题,能帮助我们发现编码上的一些错误实践以及部分安全问题和性能问题;Lint是官方深度定制,功能极其强大,且可定制性和扩展性以及全面性都表现良好。所以综合考虑,针对思考一,我们的结论是整合三种扫描工具,充分利用每一个工具的领域特性。

思考二:是否可以优化扫描过程?

既然选择了整合这几种工具,我们面临的挑战是整合工具后扫描效率的问题,首先来分析目前的插件到底耗时在哪里。

静态代码扫描耗时分析

Android项目的构建依赖Gradle工具,一次构建过程实际上是执行所有的Gradle Task。由于Gradle的特性,在构建时各个Module都需要执行CheckStyle、FindBugs、Lint相关的Task。对于Android来说,Task的数量还与其构建变体Variant有关,其中Variant = Flavor * BuildType。所以一个Module执行的相关任务可以由以下公式来描述:Flavor * BuildType (Lint,CheckStyle,Findbugs),其中为笛卡尔积。如下图所示:

Android静态代码扫描效率优化与实践_美团_05

可以看到,一次构建全量扫描执行的Task跟Varint个数正相关。对于现有工程的任务,我们可以看一下目前各个任务的耗时情况:(以实际开发中某一次扫描为例)

Android静态代码扫描效率优化与实践_Android开发_06

通过对Task耗时排序,主要的耗时体现在FindBugs和Lint对每一个Module的扫描任务上,CheckStyle任务并不占主要影响。整体来看,除了工具本身的扫描时间外,耗时主要分为多Module多Variant带来的任务数量耗时。

优化思路分析

对于工具本身的扫描时间,一方面受工具自身扫描算法和检测规则的影响,另一方面也跟扫描的文件数量相关。针对源码类型的工具比如CheckStyle和Lint,需要经过词法分析、语法分析生成抽象语法树,再遍历抽象语法树跟定义的检测规则去匹配;而针对字节码文件的工具FindBugs,需要先编译源码成Class文件,再通过BCEL分析字节码指令并与探测器规则匹配。如果要在工具本身算法上去寻找优化点,代价比较大也不一定能找到有效思路,投入产出比不高,所以我们把精力放在减少Module和Variant带来的影响上。

从上面的耗时分析可以知道,Module和Variant数直接影响任务数量, 一次PR提交的场景是多样的,比如多Module多Variant都有修改,所以要考虑这些都修改的场景。先分析一个Module多Variant的场景,考虑到不同的Variant下源代码有一定差异,并且FindBugs扫描针对的是Class文件,不同的Variant都需要编译后才能扫描,直接对多Variant做处理比较复杂。我们可以简化问题,用以空间换时间的方式,在提交PR的时候根据Variant用不同的Jenkins Job来执行每一个Variant的扫描任务。所以接下来的问题就转变为如何优化在扫描单个Variant的时候多Module任务带来的耗时。

对于Module数而言,我们可以将其抽取成组件,拆分到独立仓库,将扫描任务拆分到各自仓库的变动时期,以aar的形式集成到主项目来减少Module带来的任务数。那对于剩下的Module如何优化呢?无论是哪一种工具,都是对其输入文件进行处理,CheckStyle对Java源代码文件处理,FindBugs对Java字节码文件处理,如果我们可以通过一次任务收集到所有Module的源码文件和编译后的字节码文件,我们就可以减少多Module的任务了。所以对于全量扫描,我们的主要目标是来解决如何一次性收集所有Module的目标文件

思考三:是否支持增量扫描?

上面的优化思路都是基于全量扫描的,解决的是多Module多Variant带来的任务数量耗时。前面提到,工具本身的扫描时间也跟扫描的文件数量有关,那么是否可以从扫描的文件数量来入手呢?考虑平时的开发场景,提交PR时只是部分文件修改,我们没必要把那些没修改过的存量文件再参与扫描,而只针对修改的增量文件扫描,这样能很大程度降低无效扫描带来的效率问题。有了思路,那么我们考虑以下几个问题:

  • 如何收集增量文件,包括源码文件和Class文件?

  • 现在业界是否有增量扫描的方案,可行性如何,是否适用我们现状?

  • 各个扫描工具如何来支持增量文件的扫描?

根据上面的分析与思考路径,接下来我们详细介绍如何解决上述问题。

优化探索与实践

全量扫描优化

搜集所有Module目标文件集

获取所有Module目标文件集,首先要找出哪些Module参与了扫描。一个Module工程在Gradle构建系统中被描述为一个“Project”,那么我们只需要找出主工程依赖的所有Project即可。由于依赖配置的多样性,我们可以选择在某些Variant下依赖不同的Module,所以获取参与一次构建时与当前Variant相关的Project对象,我们可以用如下方式:

static Set collectDepProject(Project project, BaseVariant variant, Set result = null) {
if (result == null) {
  result = new HashSet<>()
}
Set taskSet = variant.javaCompiler.taskDependencies.getDependencies(variant.javaCompiler)
taskSet.each { Task task ->
  if (task.project != project && hasAndroidPlugin(task.project)) {
    result.add(task.project)
    BaseVariant childVariant = getVariant(task.project)
    if (childVariant.name == variant.name || "${variant.flavorName}${childVariant.buildType.name}".toLowerCase() == variant.name.toLowerCase()) {
      collectDepProject(task.project, childVariant, result)
    }
  }
}
return result
}

目前文件集分为两类,一类是源码文件,另一类是字节码文件,分别可以如下处理:

projectSet.each { targetProject ->
if (targetProject.plugins.hasPlugin(CodeDetectorPlugin) && GradleUtils.hasAndroidPlugin(targetProject)) {
  GradleUtils.getAndroidExtension(targetProject).sourceSets.all { AndroidSourceSet sourceSet ->
    if (!sourceSet.name.startsWith("test") && !sourceSet.name.startsWith(SdkConstants.FD_TEST)) {
      source sourceSet.java.srcDirs
    }
  }
}
}

注:上面的Source是CheckStyle Task的属性,用其来指定扫描的文件集合;

// 排除掉一些模板代码class文件
static final Collection defaultExcludes = (androidDataBindingExcludes + androidExcludes + butterKnifeExcludes + dagger2Excludes).asImmutable()

List allClassesFileTree = new ArrayList<>()
ConfigurableFileTree currentProjectClassesDir = project.fileTree(dir: variant.javaCompile.destinationDir, excludes: defaultExcludes)
allClassesFileTree.add(currentProjectClassesDir)
GradleUtils.collectDepProject(project, variant).each { targetProject ->
if (targetProject.plugins.hasPlugin(CodeDetectorPlugin) && GradleUtils.hasAndroidPlugin(targetProject)) {
  // 可能有的工程没有Flavor只有buildType
    GradleUtils.getAndroidVariants(targetProject).each { BaseVariant targetProjectVariant ->
    if (targetProjectVariant.name == variant.name || "${targetProjectVariant.name}".toLowerCase() == variant.buildType.name.toLowerCase()) {
        allClassesFileTree.add(targetProject.fileTree(dir: targetProjectVariant.javaCompile.destinationDir, excludes: defaultExcludes))
    }
  }
}
}

注:收集到字节码文件集后,可以用通过FindBugsTask 的 Class 属性指定扫描,后文会详细介绍FindBugs Task相关属性。

对于Lint工具而言,相应的Lint Task并没有相关属性可以指定扫描文件,所以在全量扫描上,我们暂时没有针对Lint做优化。

全量扫描优化数据

通过对CheckStyle和FindBugs全量扫描的优化,我们将整体扫描时间由原来的9min降低到了5min左右。

Android静态代码扫描效率优化与实践_美团_07

增量扫描优化

由前面的思考分析我们知道,并不是所有的文件每次都需要参与扫描,所以我们可以通过增量扫描的方式来提高扫描效率。

增量扫描技术调研

在做具体技术方案之前,我们先调研一下业界的现有方案,调研如下:

Android静态代码扫描效率优化与实践_Android教程_08

针对Lint,我们可以借鉴现有实现思路,同时深入分析扫描原理,在3.x版本上寻找出增量扫描的解决方案。对于CheckStyle和FindBugs,我们需要了解工具的相关配置参数,为其指定特定的差异文件集合。

注:业界有一些增量扫描的案例,例如 diff_cover,此工具主要是对单元测试整体覆盖率的检测,以增量代码覆盖率作为一个指标来衡量项目的质量,但是这跟我们的静态代码分析的需求不太符合。它有一个比较好的思路是找出差异的代码行来分析覆盖率,粒度比较细。但是对于静态代码扫描,仅仅的差异行不足以完成上下文的语义分析,尤其是针对FindBugs这类需要分析字节码的工具,获取的差异行还需要经过编译成Class文件才能进行分析,方案并不可取。

寻找增量修改文件

增量扫描的第一步是获取待扫描的目标文件。我们可以通过git diff命令来获取差异文件,值得注意的是对于删除的文件和重命名的文件需要忽略,我们更关心新增和修改的文件,并且只需要获取差异文件的路径就好了。举个例子:git diff --name-only --diff-filter=dr commitHash1 commitHash2,以上命令意思是对比两次提交记录的差异文件并获取路径,过滤删除和重命名的文件。对于寻找本地仓库的差异文件上面的命令已经足够了,但是对于PR的情况还有一些复杂,需要对比本地代码与远程仓库目标分支的差异。集团的代码管理工具在Jenkins上有相应的插件,该插件默认提供了几个参数,我们需要用到以下两个:

  • ${targetBranch}:需要合入代码的目标分支地址;

  • ${sourceCommitHash}:需要提交的代码hash值。

通过这两个参数执行以下一系列命令来获取与远程目标分支的差异文件。

git remote add upstream ${upstreamGitUrl}
git fetch upstream ${targetBranch}
git diff --name-only --diff-filter=dr $sourceCommitHash upstream/$targetBranch
  1. 配置远程分支别名为UpStream,其中upstreamGitUrl可以在插件提供的配置属性中设置;

  2. 获取远程目标分支的更新;

  3. 比较分支差异获取文件路径。

通过以上方式,我们找到了增量修改文件集。

Lint扫描原理分析

在分析Lint增量扫描原理之前,先介绍一下Lint扫描的工作流程:

Android静态代码扫描效率优化与实践_Android教程_09

App Source Files

项目中的源文件,包括Java、XML、资源文件、proGuard等。

lint.xml

用于配置希望排除的任何 Lint 检查以及自定义问题严重级别,一般各个项目都会根据自身项目情况自定义的lint.xml来排除一些检查项。

lint Tool

一套完整的扫描工具用于对Android的代码结构进行分析,可以通过命令行、IDEA、Gradle命令三种方式运行lint工具。

lint Output

Lint扫描的输出结果。

从上面可以看出,Lint Tool就像一个加工厂,对投入进来的原料(源代码)进行加工处理(各种检测器分析),得到最终的产品(扫描结果)。Lint Tool作为一个扫描工具集,有多种使用方式。Android为我们提供了三种运行方式,分别是命令行、IDEA、Gradle任务。这三种方式最终都殊途同归,通过LintDriver来实现扫描。如下图所示:

Android静态代码扫描效率优化与实践_美团_10

为了方便查看源码,新建一个工程,在build.gradle脚本中,添加如下依赖:

compile 'com.android.tools.build:gradle:3.1.1'
compile 'com.android.tools.lint:lint-gradle:26.1.1'

我们可以得到如下所示的依赖:

Android静态代码扫描效率优化与实践_Android教程_11

lint-api-26.1.1

Lint工具集的一个封装,实现了一组API接口,用于启动Lint。

lint-checks-26.1.1

一组内建的检测器,用于对这种描述好Issue进行分析处理。

lint-26.1.1

可以看做是依赖上面两个jar形成的一个基于命令行的封装接口形成的脚手架工程,我们的命令行、Gradle任务都是继承自这个jar包中相关类来做的实现。

lint-gradle-26.1.1

可以看做是针对Gradle任务这种运行方式,基于lint-26.1.1做了一些封装类。

lint-gradle-api-26.1.1

真正Gradle Lint任务在执行时调用的入口。

在理解清楚了以上几个jar的关系和作用之后,我们可以发现Lint的核心库其实是前三个依赖。后面两个其实是基于脚手架,对Gradle这种运行方式做的封装。最核心的逻辑在LintDriver的Analyze方法中。

fun analyze() {

  ...省略部分代码...

   for (project in projects) {
       fireEvent(EventType.REGISTERED_PROJECT, project = project)
  }
   registerCustomDetectors(projects)

  ...省略部分代码...

   try {
       for (project in projects) {
           phase = 1

           val main = request.getMainProject(project)

           // The set of available detectors varies between projects
           computeDetectors(project)

           if (applicableDetectors.isEmpty()) {
               // No detectors enabled in this project: skip it
               continue
          }

           checkProject(project, main)
           if (isCanceled) {
               break
          }

           runExtraPhases(project, main)
      }
  } catch (throwable: Throwable) {
       // Process canceled etc
       if (!handleDetectorError(null, this, throwable)) {
           cancel()
      }
  }
  ...省略部分代码...
}

主要是以下三个重要步骤:

registerCustomDetectors(projects)

Lint为我们提供了许多内建的检测器,除此之外我们还可以自定义一些检测器,这些都需要注册进Lint工具用于对目标文件进行扫描。这个方法主要做以下几件事情:

  1. 遍历每一个Project和它的依赖Library工程,通过client.findRuleJars来找出自定义的jar包;

  2. 通过client.findGlobalRuleJars找出全局的自定义jar包,可以作用于每一个Android工程;

  3. 从找到的jarFiles列表中,解析出自定义的规则,并与内建的Registry一起合并为CompositeIssueRegistry;需要注意的是,自定义的Lint的jar包存放位置是build/intermediaters/lint目录,如果是需要每一个工程都生效,则存放位置为~/.android/lint/

computeDetectors(project)

这一步主要用来收集当前工程所有可用的检测器。

checkProject(project, main)

接下来这一步是最为关键的一步。在此方法中,调用runFileDetectors来进行文件扫描。Lint支持的扫描文件类型很多,因为是官方支持,所以针对Android工程支持的比较友好。一次Lint任务运行时,Lint的扫描范围主要由Scope来描述。具体表现在:

fun infer(projects: Collection?): EnumSet {
          if (projects == null || projects.isEmpty()) {
              return Scope.ALL
          }

          // Infer the scope
          var scope = EnumSet.noneOf(Scope::class.java)
          for (project in projects) {
              val subset = project.subset
              if (subset != null) {
                  for (file in subset) {
                      val name = file.name
                      if (name == ANDROID_MANIFEST_XML) {
                          scope.add(MANIFEST)
                      } else if (name.endsWith(DOT_XML)) {
                          scope.add(RESOURCE_FILE)
                      } else if (name.endsWith(DOT_JAVA) || name.endsWith(DOT_KT)) {
                          scope.add(JAVA_FILE)
                      } else if (name.endsWith(DOT_CLASS)) {
                          scope.add(CLASS_FILE)
                      } else if (name.endsWith(DOT_GRADLE)) {
                          scope.add(GRADLE_FILE)
                      } else if (name == OLD_PROGUARD_FILE || name == FN_PROJECT_PROGUARD_FILE) {
                          scope.add(PROGUARD_FILE)
                      } else if (name.endsWith(DOT_PROPERTIES)) {
                          scope.add(PROPERTY_FILE)
                      } else if (name.endsWith(DOT_PNG)) {
                          scope.add(BINARY_RESOURCE_FILE)
                      } else if (name == RES_FOLDER || file.parent == RES_FOLDER) {
                          scope.add(ALL_RESOURCE_FILES)
                          scope.add(RESOURCE_FILE)
                          scope.add(BINARY_RESOURCE_FILE)
                          scope.add(RESOURCE_FOLDER)
                      }
                  }
              } else {
                  // Specified a full project: just use the full project scope
                  scope = Scope.ALL
                  break
              }
          }
}

可以看到,如果Project的Subset为Null,Scope就为Scope.ALL,表示本次扫描会针对能检测的所有范围,相应地在扫描时也会用到所有全部的Detector来扫描文件;

如果Project的Subset不为Null,就遍历Subset的集合,找出Subset中的文件分别对应哪些范围。其实到这里我们已经可以知道,Subset就是我们增量扫描的突破点。接下来我们看一下runFileDetectors:

if(scope.contains(Scope.JAVA_FILE)||scope.contains(Scope.ALL_JAVA_FILES)){
val checks = union(scopeDetectors[Scope.JAVA_FILE],scopeDetectors[Scope.ALL_JAVA_FILES])
if (checks != null && !checks.isEmpty()) {
  val files = project.subset
  if (files != null) {
    checkIndividualJavaFiles(project, main, checks, files)
  } else {
    val sourceFolders = project.javaSourceFolders
    val testFolders = if (scope.contains(Scope.TEST_SOURCES))
    project.testSourceFolders
    else
    emptyList ()
    val generatedFolders = if (isCheckGeneratedSources)
    project.generatedSourceFolders
    else
    emptyList ()
    checkJava(project, main, sourceFolders, testFolders, generatedFolders, checks)
  }
}
}

这里更加明确,如果project.subset不为空,就对单独的Java文件扫描,否则,就对源码文件和测试目录以及自动生成的代码目录进行扫描。整个runFileDetectors的扫描顺序入下:

  1. Scope.MANIFEST

  2. Scope.ALL_RESOURCE_FILES)|| scope.contains(Scope.RESOURCE_FILE) ||

    scope.contains(Scope.RESOURCE_FOLDER) || scope.contains(Scope.BINARY_RESOURCE_FILE)

  3. scope.contains(Scope.JAVA_FILE) || scope.contains(Scope.ALL_JAVA_FILES)

  4. scope.contains(Scope.CLASS_FILE) || scope.contains(Scope.ALL_CLASS_FILES) ||

    scope.contains(Scope.JAVA_LIBRARIES)

  5. scope.contains(Scope.GRADLE_FILE)

  6. scope.contains(Scope.OTHER)

  7. scope.contains(Scope.PROGUARD_FILE)

  8. scope.contains(Scope.PROPERTY_FILE)

官方文档的描述顺序一致。

现在我们已经知道,增量扫描的突破点其实是需要构造project.subset对象。

    /**
    * Adds the given file to the list of files which should be checked in this
    * project. If no files are added, the whole project will be checked.
    *
    * @param file the file to be checked
    */
  public void addFile(@NonNull File file) {
      if (files == null) {
          files = new ArrayList<>();
      }
      files.add(file);
  }

  /**
    * The list of files to be checked in this project. If null, the whole
    * project should be checked.
    *
    * @return the subset of files to be checked, or null for the whole project
    */
  @Nullable
  public List getSubset() {
      return files;
  }

注释也很明确的说明了只要Files不为Null,就会扫描指定文件,否则扫描整个工程。

Android静态代码扫描效率优化与实践(下)

作者:美团技术团队 · 鸿耀
来源:https://blog.51cto.com/u_15197658/2768467

收起阅读 »

Python内存驻留机制

驻留下面举例介绍python中的驻留机制。 python内存驻留知道结果是什么吗?下面是执行结果:TrueFalseTrueTrue整型驻留执行结果:FalseTrueTrueTrue因为启动时,Python 将一个 -5~256 之间整数列表预加载(缓存)到...
继续阅读 »



字符串驻留机制在许多面向对象编程语言中都支持,比如Java、python、Ruby、PHP等,它是一种数据缓存机制,对不可变数据类型使用同一个内存地址,有效的节省了空间,本文主要介绍Python的内存驻留机制。

驻留

字符串驻留就是每个字符串只有一个副本,多个对象共享该副本,驻留只针对不可变数据类型,比如字符串,布尔值,数字等。在这些固定数据类型处理中,使用驻留可以有效节省时间和空间,当然在驻留池中创建或者插入新的内容会消耗一定的时间。

下面举例介绍python中的驻留机制。
python内存驻留

在Python对象及内存管理机制一文中介绍了python的参数传递以及以及内存管理机制,来看下面一段代码:

l1 = [1, 2, 3, 4]
l2 = [1, 2, 3, 4]
l3 = l2
print(l1 == l2)
print(l1 is l2)
print(l2 == l3)
print(l2 is l3)

知道结果是什么吗?下面是执行结果:

True
False
True
True

l1和l2内容相同,却指向了不同的内存地址,l2和l3之间使用等号赋值,所以指向了同一个对象。因为列表是可变对象,每创建一个列表,都会重新分配内存,列表对象是没有“内存驻留”机制的。下面来看不可变数据类型的驻留机制。

整型驻留

Jupyter或者控制台交互环境中执行下面代码:

a1 = 300
b1 = 300
c1 = b1
print(a1 is b1)
print(c1 is b1)

a2 = 200
b2 = 200
c2 = b2
print(a2 is b2)
print(c2 is b2)

执行结果:

False
True
True
True

可以发现a1和b1指向了不同的地址,a2和b2指向了相同的地址,这是为什么呢?

因为启动时,Python 将一个 -5~256 之间整数列表预加载(缓存)到内存中,我们在这个范围内创建一个整数对象时,python会自动引用缓存的对象,不会创建新的整数对象。

浮点型不支持:

a = 1.0
b = 1.0
print(a is b)
print(a == b)

# 结果
# False
# True

如果上面的代码在非交互环境,也就是将代码作为python脚本运行的结果是什么呢?(运行环境为python3.7)

True
True
True
True
True
True

全为True,没有明确的限定临界值,都进行了驻留操作。这是因为使用不同的环境时,代码的优化方式不同。

字符串驻留

Jupyter或者控制台交互环境中:

  • 满足标识符命名规范的字符串都会被驻留,长度不限。

  • 空字符串会驻留

  • 使用乘法得到的字符串且满足标识符命名规范的字符串:长度小于等于20会驻留(peephole优化),Python 3.7改为4096(AST优化器)。

  • 长度为1的特殊字符(ASCII 字符中的)会驻留

  • 空元组或者只有一个元素且元素范围为-5~256的元组会驻留

满足标识符命名规范的字符:

a = 'Hello World'
b = 'Hello World'
print(a is b)

a = 'Hello_World'
b = 'Hello_World'
print(a is b)

结果:

False
True

乘法获取字符串(运行环境为python3.7)

a = 'aa'*50
b = 'aa'*50
print(a is b)

a = 'aa'*5000
b = 'aa'*5000
print(a is b)

结果:

True
False

在非交互环境中:

  • 默认字符串都会驻留

  • 使用乘法运算得到的字符串与在控制台相同

  • 元组类型(元组内数据为不可变数据类型)会驻留

  • 函数、类、变量、参数等的名称以及关键字都会驻留

注意:字符串是在编译时进行驻留,也就是说,如果字符串的值不能在编译时进行计算,将不会驻留。比如下面的例子:

letter = 'd'
a = 'Hello World'
b = 'Hello World'
c = 'Hello Worl' + 'd'
d = 'Hello Worl' + letter
e = " ".join(['Hello','World'])

print(id(a))
print(id(b))
print(id(c))
print(id(d))
print(id(e))

在交互环境执行结果如下:

1696903309168
1696903310128
1696903269296
1696902074160
1696903282800

都指向不同的内存。

python 3.7 非交互环境执行结果:

1426394439728
1426394439728
1426394439728
1426394571504
1426394571440

发现d和e指向不同的内存,因为d和e不是在编译时计算的,而是在运行时计算的。前面的a = 'aa'*50是在编译时计算的。

强行驻留

除了上面介绍的python默认的驻留外,可以使用sys模块中的intern()函数来指定驻留内容

import sys
letter_d = 'd'
a = sys.intern('Hello World')
b = sys.intern('Hello World')
c = sys.intern('Hello Worl' + 'd')
d = sys.intern('Hello Worl' + letter)
e = sys.intern(" ".join(['Hello','World']))

print(id(a))
print(id(b))
print(id(c))
print(id(d))
print(id(e))

结果:

1940593568304
1940593568304
1940593568304
1940593568304
1940593568304

使用intern()后,都指向了相同的地址。

总结

本文主要介绍了python的内存驻留,内存驻留是python优化的一种策略,注意不同运行环境下优化策略不一样,不同的python版本也不相同。注意字符串是在编译时进行驻留。

作者:测试开发小记
来源:https://blog.51cto.com/u_15441270/4714515

收起阅读 »

安卓客服云集成机器人欢迎语

1.会话分配 给APP渠道指定全天机器人2.机器人欢迎语打开,并指定一个菜单3.代码部分/** * 保存欢迎语到本地 */ public void saveMessage(){ Message message = Messa...
继续阅读 »
1.会话分配 给APP渠道指定全天机器人


2.机器人欢迎语打开,并指定一个菜单


3.代码部分

/**
* 保存欢迎语到本地
*/
public void saveMessage(){
Message message = Message.createReceiveMessage(Message.Type.TXT);
String str = Preferences.getInstance().getRobotWelcome();
EMTextMessageBody body = null;
if(!isRobotMenu(str)){
body = new EMTextMessageBody(str);
}else{
try{
body = new EMTextMessageBody("");
JSONObject msgtype = new JSONObject(str);
message.setAttribute("msgtype",msgtype);
}catch (Exception e){
Log.e("RobotMenu","onError:"+e.getMessage());
}
}
message.setFrom(toChatUsername);
message.addBody(body);
message.setMsgTime(System.currentTimeMillis());
message.setStatus(Message.Status.SUCCESS);
message.setMsgId(UUID.randomUUID().toString());

ChatClient.getInstance().chatManager().saveMessage(message);
messageList.refresh();
}

/**
* 判断机器人欢迎语是否是菜单类型
*
* @return
*/
private boolean isRobotMenu(String str) {
try {
JSONObject json = new JSONObject(str);
JSONObject obj = json.getJSONObject("choice");
} catch (Exception e) {
return false;
}
return true;
}

public void getNewRobotWelcome(String toChatUsername, MessageList messageList) {
this.toChatUsername=toChatUsername;
new Thread(new Runnable() {
@Override
public void run() {
OkHttpClient okHttpClient = new OkHttpClient();
//需要替换为自己的参数
String tenantid = "67386";//需要替换为自己的tenantid(管理员模式->账户->账户信息->租户ID一栏)
String orgname = "1473190314068186";//appkey # 前半部分
String appname = "kefuchannelapp67386";//appkey # 后半部分
String username = toChatUsername;//IM 服务号
String token = ChatClient.getInstance().accessToken();//用户token
// String url = "http://kefu.easemob.com/v1/webimplugin/tenants/robots/welcome?channelType=easemob&originType=app&tenantId=" + tenantid + "&orgName=" + orgname + "&appName=" + appname + "&userName=" + username + "&token=" + token;
String url = "https://kefu.easemob.com/v1/webimplugin/tenants/robots/welcome?channelType=easemob&originType=app&tenantId=95739&orgName=1404210708092119&appName=kefuchannelapp95739&userName=kefuchannelimid_548067&token=YWMtamWiyuByEeuml5FFEs_ewo740PDfnhHrjuLfDWx-sxgBU8F64G8R65kl_RFfGcMJAwMAAAF6iaJgxwBPGgDFrFY27hqYUwtUP5mDC0wRg1jcOkfkyEVs38cgDdmEQw";
Request request = new Request.Builder().url(url).get().build();
try {
Response response = okHttpClient.newCall(request).execute();
String result = response.body().string();
JSONObject obj = new JSONObject(result);
Log.e("newwelcome----", obj.getJSONObject("entity").getString("greetingText"));
int type = obj.getJSONObject("entity").getInt("greetingTextType");
final String rob_welcome = obj.getJSONObject("entity").getString("greetingText");
//type0代表是文字消息的机器人欢迎语
//type1代表是菜单消息的机器人欢迎语
if (type == 0) {
//把解析拿到的string保存在本地
Preferences.getInstance().setRobotWelcome(rob_welcome);

} else if (type == 1) {
final String str = rob_welcome.replaceAll("&amp;quot;", "\"");
JSONObject json = new JSONObject(str);
JSONObject ext = json.getJSONObject("ext");
final JSONObject msgtype = ext.getJSONObject("msgtype");
//把解析拿到的string保存在本地
Preferences.getInstance().setRobotWelcome(msgtype.toString());
}
} catch (JSONException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();

ChatClient.getInstance().chatManager().getCurrentSessionId(toChatUsername, new ValueCallBack() {
@Override
public void onSuccess(String value) {
Log.e("TAG value:", value);
//当返回value不为空时,则返回的当前会话的会话ID,也就是说会话正在咨询中,不需要发送欢迎语
if (value.isEmpty()) {//
saveMessage();
}
}

@Override
public void onError(int error, String errorMsg) {

}
});
}





收起阅读 »

统一路由,让小程序跳转更智能

我们在小程序开发及运营过程中,不可避免的需要进行页面之间的跳转。如果使用小程序自带的路由功能来实现这个功能,是非常简单的,如: // 根据不同的场景选择 navigateTo、redirectTo、switchTab 等 wx.navigateTo({ u...
继续阅读 »

我们在小程序开发及运营过程中,不可避免的需要进行页面之间的跳转。如果使用小程序自带的路由功能来实现这个功能,是非常简单的,如:


// 根据不同的场景选择 navigateTo、redirectTo、switchTab 等
wx.navigateTo({
url: "pages/somepage?id=1",
success: function (res) {},
});

但这里面存在几个问题:



  • 需要代码里面写死或者运营人员维护小程序页面的长长的具体路径,这显然是很不友好的

  • 需要知道页面是否为 tabbar 页面(switchTab)

  • 如果某个页面在 tabbar 和非 tabbar 页面之间发生了变化,或路径因为重构、主包瘦身等各种原因发生变化,原来的代码就会报错导致无法运行

  • navigateBack 不支持传参


为了解决以上问题,我们在项目中实现了一套基于命令别名(cmd)的统一路由跳转方式(以下称为统一路由),很好解决了遇到的实际问题,统一路由特点如下:



  • 页面别名声明使用注释方式,不侵入业务代码

  • 页面可以存在多个别名,方便新老版本页面的流量切换

  • 路由内自动判断是否 tabbar 页面,自行处理跳转及传参,业务代码无需关心

  • 支持纯 js api 的页面跳转及需要用户点击的任意类型跳转(如联系客服、打开小程序等等)

  • 对于页面栈中存在相同页面时,可以自动返回并根据参数是否相同决定是否需要刷新页面,可有效减少页面栈层级,规避小程序 10 层限制


实现思路


step1. 资源描述约定


小程序内的跳转类操作存在以下几种



  1. js api 直接可以操作的内部页面间跳转(wx.navigateTo、wx.navigateBack、wx.redirectTo、wx.reLaunch、wx.switchTab)

  2. js api 直接可以操作的打开微信原生功能的跳转(扫码、拨打电话等)

  3. 需要借助点击操作的跳转(如打开小程序及客服等需要 open-type 配合的场景 )


针对这三类操作,我们使用常见的 URL(统一资源定位系统)方式描述不同的待跳转资源



  1. 内部页面


https://host?cmd=${pagename}&param1=a  // 打开普通页面并传参,标准的H5容器也算在普通页面内


  1. 微信原生 API


https://host?cmd=nativeAPI&API=makePhoneCall&phoneNumber=123456  // 拨打电话
https://host?cmd=nativeAPI&API=scanCode&callback=scanCallback // 扫码并执行回调


  1. 需要借助按钮 open-type 的微信原生能力


https://host?cmd=nativeButtonAPI&openType=contact  // 在线客服


  1. 打开另一个小程序


https://host?cmd=miniProgram&appId=wx637bb****&path=pages/order/index&version=trial&uid=${uid} 


小程序跳转需要携带更多的参数,所以做了cmd的区分,这里实际会解析成 nativeButtonAPI 运行



step2. 在页面内定义需要的数据


在每个页面的顶部添加注释,注意 cmd 不能重复,支持多个 cmd。为了方便后续解析,我们的注释大体上遵循 JSDoc 注释规范


// pages/detail/index.tsx

/**
* @cmd detail, newdetail
* @description 详情
* @param skuid {number} skuid
*/

step3. 在编译阶段扫描并生成配置文件


根据入口文件的页面定义,匹配出需要的注释部分,使用 doctrine 解析需要的数据,解析后的数据如下:


// config/router.config.ts
export default {
index: {
description: "首页", // 页面描述
path: "/pages/index/index", // 真实路径
isTabbar: true, // 是否tabbar页面
ensureLogin: false, // 是否需要强制登录
},
detail: {
description: "详情",
path: "/pages/detail/index",
isTabbar: false,
ensureLogin: true,
},
};

这里顺便可以使用 param 等生成详细的页面名称及入参文档,提供给其他研发或运营同学使用。


step4. 资源描述解析为标准数据


根据上面的资源描述约定及扫描得到的配置文件,我们可以将其转换为方便在小程序内解析的数据定义,基本格式如下


{
origin: 'https://host?cmd=detail&skuid=1', // 原始数据
parsed: {
type: 'PAGE', // 类型,PAGE,NATIVE_API,NATIVE_BUTTON_API,UNKNOW
data: {
path: 'pages/detail/index', // 实际的页面路径,如果type是PAGE则会解析出此字段
action: undefined, // 动作,scanCode,makePhoneCall,openType,miniprogram ……。如果type是NATIVE_API,NATIVE_BUTTON_API,则会解析出此字段
params: {
skuid: '1' // 需要携带的参数
}
}
}
}

step5. 根据标准数据执行对应逻辑


由于我们的项目使用的是 Taro 框架,以下伪代码都是以 Taro 为例。


// utils/router.ts

// 用于解析原始链接为标准数据
const parseURL = (origin) => {
// balabala,一顿操作格式化成上文的数据
const data = {
...
};
return data;
};

// 执行除 NATIVE_BUTTON_API 之外的跳转
const routeURL = (origin) => {
const parsedData = parseURL(origin)
const {parsed: {type, data}} = parsedData

switch(type){
case 'PAGE':
...
break;
case 'NATIVE_API':
...
break;
case 'UNKNOW':
...
break;
}
};

export default {
parseURL,
routeURL,
};

对于需要点击的类型,我们需要借助 UI 组件实现


// components/router.tsx

import router from "/utils/router";
import { Button } from "@tarojs/components";
import Taro, { Component, eventCenter } from "@tarojs/taro";

export default class Router extends Component {
componentWillMount() {
const { path } = this.props;
const data = router.parseURL(path);
const { parsed, origin } = data;
const openType =
(parsed &&
parsed.data &&
parsed.data.params &&
parsed.data.params.openType) ||
false;
this.setState({
parsed,
openType,
});
}

// 点击事件
async handleClick(parsed, origin) {
// 点击执行动作
let {
type,
data: { action, params },
} = parsed;
if (!type) {
return;
}

// 内部页面
if (["PAGE", "CMD_UNKNOW"].includes(type)) {
console.log(`CMD_NATIVE_PAGE 参数:`, origin, options);
router.routeURL(origin);
return;
}

// 拨打电话、扫码等原生API
if (["NATIVE_API"].includes(type) && action) {
if (action === "makePhoneCall") {
let { phoneNumber = "" } = params;
if (!phoneNumber || phoneNumber.replace(/\s/g, "") == "") {
Taro.showToast({
icon: "none",
title: "未查询到号码,无法呼叫哦~",
});
return;
}
}

let res = await Taro[action]({ ...params });

// 扫码事件,需要在扫码完成后发送全局广播,业务内自行处理
if (action === "scanCode" && params.callback) {
let eventName = `${params.callback}_event`;
eventCenter.trigger(eventName, res);
}
}

// 打开小程序
if (
["NATIVE_BUTTON_API"].includes(type) &&
["miniprogram"].includes(action)
) {
await Taro.navigateToMiniProgram({
...params,
});
}
}

render() {
const { parsed, openType, origin } = this.state;

return (
<Button
onClick={this.handleClick.bind(this, parsed, origin)}
hoverClass="none"
openType={openType}
>
{this.props.children}
</Button>
);
}
}

在具体业务中使用


// pages/index/index.tsx
import router from "/utils/router";
import Router from "/components/router";

// js方式直接跳转
router.routeURL('https://host?cmd=detail&skuid=1')

// UI组件方式
...
render(){
return <Router path='https://host?cmd=detail&skuid=1'></Router>
}
...

当然这里面可以附加你自己需要的功能,比如:增加跳转方式控制、数据处理、埋点、加锁防连续点击,相对来说并不复杂。甚至你还可以顺手实现一下上面提到的 navigateBack 传参。


结语


上文的思考及实现过程比较简单,纯属抛砖引玉,欢迎大家交流互动。


作者:胖纳特
链接:https://juejin.cn/post/6930899487250448398

收起阅读 »

系统学习iOS动画 —— 渐变动画

iOS
系统学习iOS动画 —— 渐变动画这是我参与11月更文挑战的第22天,活动详情查看:2021最后一次更文挑战这个是希望达成的效果,主要就是下面字体的渐变动画以及右拉手势动画:先创建需要的控件:class ViewController: UIViewContro...
继续阅读 »

系统学习iOS动画 —— 渐变动画

这是我参与11月更文挑战的第22天,活动详情查看:2021最后一次更文挑战

这个是希望达成的效果,主要就是下面字体的渐变动画以及右拉手势动画:

请添加图片描述

先创建需要的控件:

class ViewController: UIViewController {
let timeLabel = UILabel()

override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
view.addSubview(timeLabel)
view.backgroundColor = .gray
timeLabel.text = "9:42"
timeLabel.font = UIFont.systemFont(ofSize: 72)
timeLabel.textColor = .white
timeLabel.frame = CGRect(x: 0, y: 100, width: timeLabel.intrinsicContentSize.width, height: timeLabel.intrinsicContentSize.height)
timeLabel.center.x = view.center.x

}


}

然后创建一个文件,然后写一个继承自UIView的类来编写动画的界面。

import UIKit
import QuartzCore

class AnimatedMaskLabel: UIView {

}

CAGradientLayer是CALayer的另一个子类,专门用于渐变的图层。这里创建一个CAGradientLayer来做渐变。这里

  • startPoint和endPoint定义了渐变的方向及其起点和终点
  • Colors是渐变的颜色数组
  • location: 每个渐变点的位置,范围 0 - 1 ,默认为0。

let gradientLayer: CAGradientLayer = {
let gradientLayer = CAGradientLayer()

// Configure the gradient here
gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.5)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.5)
let colors = [
UIColor.yellow.cgColor,
UIColor.green.cgColor,
UIColor.orange.cgColor,
UIColor.cyan.cgColor,
UIColor.red.cgColor,
UIColor.yellow.cgColor
]
gradientLayer.colors = colors

let locations: [NSNumber] = [
0.0, 0.0, 0.0, 0.0, 0.0, 0.25
]
gradientLayer.locations = locations

return gradientLayer
}()


在layoutSubviews里面为gradient设置frame,这里设置宽度为三个屏幕宽度大小来让动画看起来更加顺滑。

 override func layoutSubviews() {
gradientLayer.frame = CGRect(
x: -bounds.size.width,
y: bounds.origin.y,
width: 3 * bounds.size.width,
height: bounds.size.height)
}

接着需要声明一个text,当text被赋值的时候,将文本渲染为图像,然后使用该图像在渐变图层上创建蒙版。

 var text: String! {
didSet {
setNeedsDisplay()

let image = UIGraphicsImageRenderer(size: bounds.size)
.image { _ in
text.draw(in: bounds, withAttributes: textAttributes)
}

let maskLayer = CALayer()
maskLayer.backgroundColor = UIColor.clear.cgColor
maskLayer.frame = bounds.offsetBy(dx: bounds.size.width, dy: 0)
maskLayer.contents = image.cgImage

gradientLayer.mask = maskLayer
}
}

这里还需要为文本创建一个文本属性

  let textAttributes: [NSAttributedString.Key: Any] = {
let style = NSMutableParagraphStyle()
style.alignment = .center
return [
.font: UIFont(
name: "HelveticaNeue-Thin",
size: 28.0)!,
.paragraphStyle: style
]
}()

最后在didMoveToWindow中添加gradientLayer为自身子view并且为gradientLayer添加动画。

  override func didMoveToWindow() {
super.didMoveToWindow()
layer.addSublayer(gradientLayer)

let gradientAnimation = CABasicAnimation(keyPath: "locations")
gradientAnimation.fromValue = [0.0, 0.0, 0.0, 0.0, 0.0, 0.25]
gradientAnimation.toValue = [0.65, 0.8, 0.85, 0.9, 0.95, 1.0]
gradientAnimation.duration = 3.0
gradientAnimation.repeatCount = Float.infinity

gradientLayer.add(gradientAnimation, forKey: nil)
}

接下来在viewController中添加这个view。 声明一个animateLabel

    let animateLabel = AnimatedMaskLabel()

之后在viewDidLoad里面添加animateLabel在子view并且设置好各属性,这样animateLabel就有一个渐变动画了。

view.addSubview(animateLabel)
animateLabel.frame = CGRect(x: 0, y: UIScreen.main.bounds.size.height - 200, width: 200, height: 40)
animateLabel.center.x = view.center.x
animateLabel.backgroundColor = .clear
animateLabel.text = "Slide to reveal"

接下来为animateLabel添加滑动手势,这里设置滑动方向为向右滑动。

   let swipeGesture = UISwipeGestureRecognizer(target: self, action: #selector(handleSlide))
swipeGesture.direction = .right
animateLabel.addGestureRecognizer(swipeGesture)


然后在响应方法里面添加动画,这里先创建一个临时变量并且让其在屏幕外面,然后第一次动画的时候让timeLabel上移,animateLabel下移,然后让image跑到屏幕中间。完了之后在创建一个动画让timeLabel和animateLabel复原,把image移动到屏幕外,然后把image移除掉。

  @objc func handleSlide() {
// reveal the meme upon successful slide
let image = UIImageView(image: UIImage(named: "meme"))
image.center = view.center
image.center.x += view.bounds.size.width
view.addSubview(image)

UIView.animate(withDuration: 0.33, delay: 0.0,
animations: {
self.timeLabel.center.y -= 200.0
self.animateLabel.center.y += 200.0
image.center.x -= self.view.bounds.size.width
},
completion: nil
)

UIView.animate(withDuration: 0.33, delay: 1.0,
animations: {
self.timeLabel.center.y += 200.0
self.animateLabel.center.y -= 200.0
image.center.x += self.view.bounds.size.width
},
completion: {_ in
image.removeFromSuperview()
}
)
}

这样动画就完成了,完整代码:

import UIKit

class ViewController: UIViewController {
let timeLabel = UILabel()
let animateLabel = AnimatedMaskLabel()

override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
view.addSubview(timeLabel)
view.addSubview(animateLabel)

view.backgroundColor = .gray
timeLabel.text = "9:42"
timeLabel.font = UIFont.systemFont(ofSize: 72)
timeLabel.textColor = .white
timeLabel.frame = CGRect(x: 0, y: 100, width: timeLabel.intrinsicContentSize.width, height: timeLabel.intrinsicContentSize.height)
timeLabel.center.x = view.center.x


animateLabel.frame = CGRect(x: 0, y: UIScreen.main.bounds.size.height - 200, width: 200, height: 40)
animateLabel.center.x = view.center.x
animateLabel.backgroundColor = .clear
animateLabel.text = "Slide to reveal"
let swipeGesture = UISwipeGestureRecognizer(target: self, action: #selector(handleSlide))
swipeGesture.direction = .right
animateLabel.addGestureRecognizer(swipeGesture)

}

@objc func handleSlide() {
// reveal the meme upon successful slide
let image = UIImageView(image: UIImage(named: "meme"))
image.center = view.center
image.center.x += view.bounds.size.width
view.addSubview(image)

UIView.animate(withDuration: 0.33, delay: 0.0,
animations: {
self.timeLabel.center.y -= 200.0
self.animateLabel.center.y += 200.0
image.center.x -= self.view.bounds.size.width
},
completion: nil
)

UIView.animate(withDuration: 0.33, delay: 1.0,
animations: {
self.timeLabel.center.y += 200.0
self.animateLabel.center.y -= 200.0
image.center.x += self.view.bounds.size.width
},
completion: {_ in
image.removeFromSuperview()
}
)
}

}


import UIKit
import QuartzCore


class AnimatedMaskLabel: UIView {

let gradientLayer: CAGradientLayer = {
let gradientLayer = CAGradientLayer()

// Configure the gradient here
gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.5)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.5)
let colors = [
UIColor.yellow.cgColor,
UIColor.green.cgColor,
UIColor.orange.cgColor,
UIColor.cyan.cgColor,
UIColor.red.cgColor,
UIColor.yellow.cgColor
]
gradientLayer.colors = colors

let locations: [NSNumber] = [
0.0, 0.0, 0.0, 0.0, 0.0, 0.25
]
gradientLayer.locations = locations

return gradientLayer
}()

var text: String! {
didSet {
setNeedsDisplay()

let image = UIGraphicsImageRenderer(size: bounds.size)
.image { _ in
text.draw(in: bounds, withAttributes: textAttributes)
}

let maskLayer = CALayer()
maskLayer.backgroundColor = UIColor.clear.cgColor
maskLayer.frame = bounds.offsetBy(dx: bounds.size.width, dy: 0)
maskLayer.contents = image.cgImage

gradientLayer.mask = maskLayer
}
}

let textAttributes: [NSAttributedString.Key: Any] = {
let style = NSMutableParagraphStyle()
style.alignment = .center
return [
.font: UIFont(
name: "HelveticaNeue-Thin",
size: 28.0)!,
.paragraphStyle: style
]
}()

override func layoutSubviews() {
layer.borderColor = UIColor.green.cgColor
gradientLayer.frame = CGRect(
x: -bounds.size.width,
y: bounds.origin.y,
width: 3 * bounds.size.width,
height: bounds.size.height)
}

override func didMoveToWindow() {
super.didMoveToWindow()
layer.addSublayer(gradientLayer)

let gradientAnimation = CABasicAnimation(keyPath: "locations")
gradientAnimation.fromValue = [0.0, 0.0, 0.0, 0.0, 0.0, 0.25]
gradientAnimation.toValue = [0.65, 0.8, 0.85, 0.9, 0.95, 1.0]
gradientAnimation.duration = 3.0
gradientAnimation.repeatCount = Float.infinity

gradientLayer.add(gradientAnimation, forKey: nil)
}
}



收起阅读 »

iOS中加载xib

iOS
iOS中加载xib「这是我参与11月更文挑战的第27天,活动详情查看:2021最后一次更文挑战」关于 xib 或 storyboard共同点都用来描述软件界面都用 interface builder 工具来编辑本质都是转换成代码去创建控件不同点xib是轻量级的...
继续阅读 »

iOS中加载xib

「这是我参与11月更文挑战的第27天,活动详情查看:2021最后一次更文挑战

关于 xib 或 storyboard

  • 共同点
    • 都用来描述软件界面
    • 都用 interface builder 工具来编辑
    • 本质都是转换成代码去创建控件
  • 不同点
    • xib是轻量级的,用来描述局部UI界面
    • storyboard是重量级的,用来描述整个软件的多个界面,并且能够展示多个界面的跳转关系

加载xib

xib 文件在编译的后会变成 nib 文件

11975486-4f7dfbf345c0bff5.png

  • 第一种加载方式
    NSArray * xibArray = [[NSBundle mainBundle]loadNibNamed:NSStringFromClass(self) owner:nil options:nil] ;
    return xibArray[0];

  • 第二种加载方式
    UINib *nib = [UINib nibWithNibName:NSStringFromClass(self) bundle:nil];
    NSArray *xibArray = [nib instantiateWithOwner:nil options:nil];
    return xibArray[0];

    xibArray中log打印 log.png

控制器加载xib

  1. 首先需要对 xib 文件进行一些处理,打开 xib 文件

  2. 点击 "File‘s Owner",设置 Class 为 xxxViewControler 点击

  3. 右键 "Files‘s Owner",里面有个默认的IBOutlet变量view,看一下后面有没有做关联,如果没有就拉到下面的View和视图做个关联

    Files‘s Owner与View做关联

  • 第一种加载方式,传入指定的 xib(如CustomViewController)

    CustomViewController *custom = [[CustomViewController alloc]initWithNibName:@"CustomViewController" bundle:nil];

  • 第二种加载方式,不指定 xib

    CustomViewController *custom = [[CustomViewController alloc]initWithNibName:nil bundle:nil];

    • 第一步:寻找有没有和控制器类名同名的xib,如果有就去加载(XXViewController.xib)

      控制器类名同名的xib.png

    • 第二步:寻找有没有和控制器类名同名但是不带Controller的xib,如果有就去加载(XXView.xib)

      11975486-e40e19dd11cafbc5.png

    • 第三步:如果没有找到合适的 xib,就会创建一个 view(白色View,为系统自己创建的)


xib自定义控件与代码自定义的区别

这是自定义的一个 view,我们通过不同的初始化方式去判断它的执行方法

#import "CustomViw.h"
@implementation CustomViw
- (instancetype)init{
self = [super init];
if (self) {
NSLog(@"%s",__func__);
}
return self;
}

- (instancetype)initWithFrame:(CGRect)frame{
if (self = [super initWithFrame:frame]) {
NSLog(@"%s",__func__);
}
return self;
}

- (instancetype)initWithCoder:(NSCoder *)aDecoder{

if (self = [super initWithCoder:aDecoder]) {
}
NSLog(@"%s",__func__);
return self;
}

- (void)awakeFromNib{
[super awakeFromNib];
NSLog(@"%s",__func__);
}
@end

  • 通过 init 方法初始化自定义控件

    @implementation ViewController
    - (void)viewDidLoad {
    [super viewDidLoad];
    CustomViw *customView = [[CustomViw alloc] init];
    }
    @end

    log:

    通过init方法初始化自定义控件log打印.png

  • 通过加载 xib 方法初始化自定义控件

    @implementation ViewController
    - (void)viewDidLoad {
    [super viewDidLoad];
    CustomViw *customView = [[[NSBundle mainBundle]loadNibNamed:NSStringFromClass([CustomViw class]) owner:nil options:nil] lastObject];
    }
    @end

    log(打印三次是因为CustomViw的xib文件里有三个View) 通过加载xib方法初始化自定义控件log打印.png

小结:

  • 通过代码初始化自定义控件是不会自动加载xib的,它会执行 initWithFrame 和 init
  • 通过加载 xib 初始化自定义控件,仅仅执行 initWithCoder 和 awakeFromNib,如果要通过代码修改 xib 的内容,一般建议放在 awakeFromNib 方法内

控件封装

一般封装一个控件,为了让开发者方便使用,通常会在自定义的控件中编写俩个方法初始化方法,这样不管是通过 init 还是加载xib都可以实现相同的效果

#import "CustomViw.h"
@implementation CustomViw

- (instancetype)initWithFrame:(CGRect)frame{
if (self = [super initWithFrame:frame]) {
[self setup];
}
return self;
}

- (void)awakeFromNib{
[super awakeFromNib];
[self setup];
}

- (void)setup{
[self setBackgroundColor:[UIColor redColor]];
}
@end

收起阅读 »

iOS中的Storyboard

iOS
iOS中的Storyboard「这是我参与11月更文挑战的第28天,活动详情查看:2021最后一次更文挑战」关于StoryboardStoryboard 是最先在 iOS5 中引入的一项新特性,它的出现使得开发人员大幅缩减构建App用户界面所需的时间关于Sto...
继续阅读 »


iOS中的Storyboard

「这是我参与11月更文挑战的第28天,活动详情查看:2021最后一次更文挑战

关于Storyboard

Storyboard 是最先在 iOS5 中引入的一项新特性,它的出现使得开发人员大幅缩减构建App用户界面所需的时间

关于Storyboard的加载方式

  • 一般在新建工程后,我们便可以看到Xcode会默认加载 Storyboard,但是在实际开发中,我们更常用的是自己新建 Storyboard,所以,这里主要讲手动创建控制器时,加载 Storyboard 的方式

  • 通常在新建的项目中,我们首先要将Xcode加载 Storyboard 去掉

    这里写图片描述

  • 关于 Storyboard 创建控制器

    第一种:

    这里写图片描述

    self.window = [[UIWindow alloc]initWithFrame:[UIScreen mainScreen].bounds];
    UIStoryboard *sb = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
    UIViewController *vc = [sb instantiateInitialViewController];
    self.window.rootViewController = vc;
    [self.window makeKeyAndVisible];

    第二种:

    这里写图片描述

    self.window = [[UIWindow alloc]initWithFrame:[UIScreen mainScreen].bounds];
    UIStoryboard *sb = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
    UIViewController *vc = [sb instantiateViewControllerWithIdentifier:@"WAKAKA"];
    self.window.rootViewController = vc;
    [self.window makeKeyAndVisible];


关于UIStoryboardSegue

在 Storyboard 中,用来描述界面跳转的线,都属于 UIStoryboardSegue 的对象(简称:Segue

这里写图片描述

Segue的属性

  • 唯一标识(identifier
  • 来源控制器(sourceViewController
  • 目标控制器(destinationViewController

Segue的类型

  • 自动型(点击某控件,不需要进行某些判断可直接跳转的)

    这里写图片描述

  • 手动型(点击某控件,需要进行某些判断才跳转的) 这里写图片描述

  • 手动设置 Segue 需要设置

    这里写图片描述

    使用 perform 方法执行对应的 Segue

    //根据Identifier去storyboard中找到对应的线,之后建立一个storyboard的对象
    [self performSegueWithIdentifier:@"showinfo" sender:nil];

    如果需要做传值或跳转到不同的UI,需要在这个方法里代码实现

    - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{
    //比较唯一标识
    if ([segue.identifier isEqualToString:@"showInfo"]) {
    //来源控制器
    UINavigationController *nvc = segue.sourceViewController;
    //目的控制器
    ListViewController *vc = segue.destinationViewController;
    vc.info = @
    "show";
    }
    }

    链接:https://juejin.cn/post/7035408728509644814
收起阅读 »

iOS小技能: 图片的平铺和拉伸、图片的加载方式、内容模式

iOS
iOS小技能: 图片的平铺和拉伸、图片的加载方式、内容模式这是我参与11月更文挑战的第28天,活动详情查看:2021最后一次更文挑战。引言例子:按照比例显示图片全部内容,并自动适应高度I 图片的平铺和拉伸 #import "UIImage+ResizableI...
继续阅读 »

iOS小技能: 图片的平铺和拉伸、图片的加载方式、内容模式

这是我参与11月更文挑战的第28天,活动详情查看:2021最后一次更文挑战

引言

例子:按照比例显示图片全部内容,并自动适应高度

I 图片的平铺和拉伸


#import "UIImage+ResizableImage.h"

@implementation UIImage (ResizableImage)


+ (UIImage*)resizableImageWithName:(NSString *)name {
NSLog(@"%s--%@",__func__,name);
UIImage *image = [UIImage imageNamed:name];
//裁剪图片方式一:
//Creates and returns a new image object with the specified cap values.
/*right cap is calculated as width - leftCapWidth - 1
bottom cap is calculated as height - topCapWidth - 1
*/

return [image stretchableImageWithLeftCapWidth:image.size.width*0.5 topCapHeight:image.size.height*0.5];
//方式二:
// CGFloat top = image.size.width*0.5f-1;
// CGFloat left = image.size.height*0.5f-1;
// UIEdgeInsets insets = UIEdgeInsetsMake(top, left, top, left);
// UIImage *capImage = [image resizableImageWithCapInsets:insets resizingMode:UIImageResizingModeTile];
//
}




/**
CGFloat top = 0; // 顶端盖高度
CGFloat bottom = 0 ; // 底端盖高度
CGFloat left = 0; // 左端盖宽度
CGFloat right = 0; // 右端盖宽度

// UIImageResizingModeStretch:拉伸模式,通过拉伸UIEdgeInsets指定的矩形区域来填充图片
// UIImageResizingModeTile:平铺模式,通过重复显示UIEdgeInsets指定的矩形区域来填充图片


@param img <#img description#>
@param top <#top description#>
@param left <#left description#>
@param bottom <#bottom description#>
@param right <#right description#>
@return <#return value description#>
*/

- (UIImage *) resizeImage:(UIImage *) img WithTop:(CGFloat) top WithLeft:(CGFloat) left WithBottom:(CGFloat) bottom WithRight:(CGFloat) right
{
UIImage * resizeImg = [img resizableImageWithCapInsets:UIEdgeInsetsMake(self.size.height * top, self.size.width * left, self.size.height * bottom, self.size.width * right) resizingMode:UIImageResizingModeStretch];

return resizeImg;
}



//返回一个可拉伸的图片
- (UIImage *)resizeWithImageName:(NSString *)name
{
UIImage *normal = [UIImage imageNamed:name];

// CGFloat w = normal.size.width * 0.5f ;
// CGFloat h = normal.size.height *0.5f ;

CGFloat w = normal.size.width*0.8;
CGFloat h = normal.size.height*0.8;
//传入上下左右不需要拉升的编剧,只拉伸中间部分
return [normal resizableImageWithCapInsets:UIEdgeInsetsMake(h, w, h, w)];

// [normal resizableImageWithCapInsets:UIEdgeInsetsMake(<#CGFloat top#>, <#CGFloat left#>, <#CGFloat bottom#>, <#CGFloat right#>)]

// 1 = width - leftCapWidth - right
// 1 = height - topCapHeight - bottom

//传入上下左右不需要拉升的编剧,只拉伸中间部分,并且传入模式(平铺/拉伸)
// [normal :<#(UIEdgeInsets)#> resizingMode:<#(UIImageResizingMode)#>]

//只用传入左边和顶部不需要拉伸的位置,系统会算出右边和底部不需要拉升的位置。并且中间有1X1的点用于拉伸或者平铺
// 1 = width - leftCapWidth - right
// 1 = height - topCapHeight - bottom
// return [normal stretchableImageWithLeftCapWidth:w topCapHeight:h];
}




@end


II 图片的加载方式

优先选择3x图像,而不是2x图像时使用initWithContentsOfFile

 NSString *path = [[NSBundle mainBundle] pathForResource:@"smallcat" ofType:@"png"];
UIImage *image = [[UIImage alloc]initWithContentsOfFile:path];
// 在ipone5 s、iphone6和iphone6 plus都是优先加载@3x的图片,如果没有@3x的图片,就优先加载@2x的图片



  • 优先加载@2x的图片
  • [UIImage imageNamed:@"smallcat"]

iphone5s和iphone6优先加载@2x的图片,iphone6 plus是加载@3x的图片。

加载图片注意点:如果图片比较小,并且使用非常频繁,可以使用imageName:(eg icon),如果图片比较大,并且使用比较少,可以使用imageWithContentsOfFile:(eg 引导页 相册)。 imageName:

  • 1、当对象销毁的时候,图片占用的内存不会随着一起销毁,内存由系统来管理,程序员不可控制
  • 2、加载的图片,占用的内存非常大
  • 3、相同的图片不会被重复加载到内存

imageWithContentsOfFile:

  • 1、当对象销毁的时候,图片占用的内存会随着一起销毁
  • 2、加载的图片占用的内存较小

3、相同的图片如果被多次加载就会占据多个内存空间

III 内容模式

首先了解下图片的内容模式

3.1 内容模式

  • UIViewContentModeScaleToFill

拉伸图片至填充整个UIImageView,图片的显示尺寸会和imageVew的尺寸一样 。

This will scale the image inside the image view to fill the entire boundaries of the image view.

  • UIViewContentModeScaleAspectFit

图片的显示尺寸不能超过imageView尺寸大小

This will make sure the image inside the image view will have the right aspect ratio and fits inside the image view’s boundaries.

  • UIViewContentModeScaleAspectFill

按照图片的原来宽高比进行缩放(展示图片最中间的内容),配合使用 tmpView.layer.masksToBounds = YES;

This will makes sure the image inside the image view will have the right aspect ratio and fills the entire boundaries of the image view. For this value to work properly, make sure that you have set the clipsToBounds property of the image view to YES.

  • UIViewContentModeScaleToFill : 直接拉伸图片至填充整个imageView

划重点:

  1. UIViewContentModeScaleAspectFit : 按照图片的原来宽高比进行缩放(一定要看到整张图片)

使用场景:信用卡图片的展示

在这里插入图片描述

  1. UIViewContentModeScaleAspectFill : 按照图片的原来宽高比进行缩放(只能图片最中间的内容)

引导页通常采用UIViewContentModeScaleAspectFill


// 内容模式
self.contentMode = UIViewContentModeScaleAspectFill;
// 超出边框的内容都剪掉
self.clipsToBounds = YES;




3.2 例子:商品详情页的实现

  • [商品详情页(按照图片原宽高比例显示图片全部内容,并自动适应高度)

](kunnan.blog.csdn.net/article/det…)

  • 背景图片的拉伸(中间空白的小矩形设置为可拉伸,保证有形状的地方不进行缩放)
- (void)awakeFromNib
{
[super awakeFromNib];
// 拉伸
// self.backgroundView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"bg_dealcell"]];
// 平铺
// self.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:@"bg_dealcell"]];


[self setAutoresizingMask:UIViewAutoresizingNone];



}




- (void)drawRect:(CGRect)rect
{
// 平铺
// [[UIImage imageNamed:@"bg_dealcell"] drawAsPatternInRect:rect];
// 拉伸
[[UIImage imageNamed:@"bg_dealcell"] drawInRect:rect];
}



背景图片的拉伸(中间空白的小矩形设置为可拉伸,保证有形状的地方不进行缩放)
UIImage *resizableImage = [image resizableImageWithCapInsets:UIEdgeInsetsMake(heightForLeftORRight, widthForTopORBottom, heightForLeftORRight, widthForTopORBottom)];

see also

只为你呈现有价值的信息,专注于移动端技术研究领域;更多服务和咨询请关注#公众号:iOS逆向

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

收起阅读 »

图解-元宇宙(MetaVerse)

目录 1、前言 2、元宇宙是什么 3、生态技术图谱 1、前言 近日,全球互联网巨头Facebook宣布改名为Meta(Meta为元宇宙MetaVerse的前缀),一时间,基于技术创新且未来空间广阔的“元宇宙”再次成为科技界最关心的话题。 2、元宇宙是什么 元...
继续阅读 »

目录


1、前言


2、元宇宙是什么


3、生态技术图谱




1、前言


近日,全球互联网巨头Facebook宣布改名为Meta(Meta为元宇宙MetaVerse的前缀),一时间,基于技术创新且未来空间广阔的“元宇宙”再次成为科技界最关心的话题。


2、元宇宙是什么


元宇宙(Metaverse)一词,诞生于1992年的科幻小说《雪崩》,小说描绘了一个庞大的虚拟现实世界,在这里,人们用数字化身来控制,并相互竞争以提高自己的地位,到现在看来,描述的还是超前的未来世界。


Metaverse是由Meta和Verse组成,Meta表示超越,verse是宇宙universe的意思,合起来通常表示,互联网的下一个阶段,由AR,VR,3D等技术支持的虚拟现实的网络世界。


3、生态技术图谱


元宇宙生态包含了从技术基础到各种支持系统的项目,生态图谱极其庞大。



图片来源:悦财经





作者:Meng
链接:https://juejin.cn/post/7032217003486085133
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

必学必知的自定义View基础

前言自定义View原理是Android开发者必须了解的基础;在了解自定义View之前,你需要有一定的知识储备;本文将全面解析关于自定义View中的所有知识基础。目录1. 视图定义即日常说的View,具体表现为显示在屏幕上的各种视图控件,如TextView、Li...
继续阅读 »

前言

  • 自定义View原理是Android开发者必须了解的基础;
  • 在了解自定义View之前,你需要有一定的知识储备;
  • 本文将全面解析关于自定义View中的所有知识基础。

目录

示意图


1. 视图定义

即日常说的View,具体表现为显示在屏幕上的各种视图控件,如TextView、LinearLayout等。


2. 视图分类

视图View主要分为两类:

  • 单一视图:即一个View、不包含子View,如TextView
  • 视图组,即多个View组成的ViewGroup、包含子View,如LinearLayout

Android中的UI组件都由View、ViewGroup共同组成。


3. 视图类简介

  • 视图的核心类是:View类
  • View类是Android中各种组件的基类,如View是ViewGroup基类
  • View的构造函数:共有4个,具体如下:

自定义View必须重写至少一个构造函数:

// 构造函数1
// 调用场景:View是在Java代码里面new的
public CarsonView(Context context) {
super(context);
}

// 构造函数2
// 调用场景:View是在.xml里声明的
// 自定义属性是从AttributeSet参数传进来的
public CarsonView(Context context, AttributeSet attrs) {
super(context, attrs);
}

// 构造函数3
// 应用场景:View有style属性时
// 一般是在第二个构造函数里主动调用;不会自动调用
public CarsonView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}

// 构造函数4
// 应用场景:View有style属性时、API21之后才使用
// 一般是在第二个构造函数里主动调用;不会自动调用
public CarsonView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}

更加具体的使用请看:深入理解View的构造函数和 理解View的构造函数


4. 视图结构

  • 对于包含子View的视图组(ViewGroup),结构是树形结构
  • ViewGroup下可能有多个ViewGroup或View,如下图:

这里需要特别注意的是:在View的绘制过程中,永远都是从View树结构的根节点开始(即从树的顶端开始),一层一层、一个个分支地自上而下遍历进行(即树形递归),最终计算整个View树中各个View,从而最终确定整个View树的相关属性。


5. Android坐标系

Android的坐标系定义为:

  • 屏幕的左上角为坐标原点
  • 向右为x轴增大方向
  • 向下为y轴增大方向

具体如下图:

注:区别于一般的数学坐标系

两者坐标系的区别


6. View位置(坐标)描述

视图的位置由四个顶点决定,如图1-3所示的A、B、C、D。

视图的位置是相对于父控件而言的,四个顶点的位置描述分别由四个与父控件相关的值决定:

  • 顶部(Top):视图上边界到父控件上边界的距离;
  • 左边(Left):视图左边界到父控件左边界的距离;
  • 右边(Right):视图右边界到父控件左边界的距离;
  • 底部(Bottom):视图下边界到父控件上边界的距离。

具体如图1-4所示。

可根据视图位置的左上顶点、右下顶点进行记忆:

  • 顶部(Top):视图左上顶点到父控件上边界的距离;
  • 左边(Left):视图左上顶点到父控件左边界的距离;
  • 右边(Right):视图右下顶点到父控件左边界的距离;
  • 底部(Bottom):视图右下顶点到父控件上边界的距离。

7. 位置获取方式

视图的位置获取是通过View.getXXX()方法进行获取。

获取顶部距离(Top):getTop()
获取左边距离(Left):getLeft()
获取右边距离(Right):getRight()
获取底部距离(Bottom):getBottom()
  • 与MotionEvent中 get() getRaw()的区别
//get() :触摸点相对于其所在组件坐标系的坐标
event.getX();
event.getY();

//getRaw() :触摸点相对于屏幕默认坐标系的坐标
event.getRawX();
event.getRawY();

具体如下图:

get() 和 getRaw() 的区别


8. 角度(angle)& 弧度(radian)

  • 自定义View实际上是将一些简单的形状通过计算,从而组合到一起形成的效果。

这会涉及到画布的相关操作(旋转)、正余弦函数计算等,即会涉及到角度(angle)与弧度(radian)的相关知识。

  • 角度和弧度都是描述角的一种度量单位,区别如下图::

角度和弧度区别

在默认的屏幕坐标系中角度增大方向为顺时针。

屏幕坐标系角度增大方向

注:在常见的数学坐标系中角度增大方向为逆时针


9. 颜色相关

Android中的颜色相关内容包括颜色模式,创建颜色的方式,以及颜色的混合模式等。

9.1 颜色模式

Android支持的颜色模式主要包括:

  • ARGB8888:四通道高精度(32位)
  • ARGB4444:四通道低精度(16位)
  • RGB565:Android屏幕默认模式(16位)
  • Alpha8:仅有透明通道(8位)

这里需要特别注意的是:

  • 字母:表示通道类型;
  • 数值:表示该类型用多少位二进制来描述;
  • 示例说明:ARGB8888,表示有四个通道(ARGB);每个对应的通道均用8位来描述。

以ARGB8888为例介绍颜色定义:

ARGB88888

9.2 颜色定义

主要分为xml定义 / java定义。

/**
* 定义方式1:xml
* 在/res/values/color.xml文件中定义
*/
<?xml version="1.0" encoding="utf-8"?>
<resources>
//定义了红色(没有alpha(透明)通道)
<color name="red">#ff0000</color>
//定义了蓝色(没有alpha(透明)通道)
<color name="green">#00ff00</color>
</resources>

// 在xml文件中以”#“开头定义颜色,后面跟十六进制的值,有如下几种定义方式:
#f00 //低精度 - 不带透明通道红色
#af00 //低精度 - 带透明通道红色
#ff0000 //高精度 - 不带透明通道红色
#aaff0000 //高精度 - 带透明通道红色

/**
* 定义方式2:Java
*/
// 使用Color类定义颜色
int color = Color.GRAY; //灰色

// Color类使用ARGB值表示
int color = Color.argb(127, 255, 0, 0); //半透明红色
int color = 0xaaff0000; //带有透明度的红色

9.3 颜色引用

主要分为xml定义 / java定义。

/**
* 引用方式1:xml
*/
// 1. 在style文件中引用
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="colorPrimary">@color/red</item>
</style>
// 2. 在layout文件中引用
android:background="@color/red"
// 3. 在layout文件中创建并使用颜色
android:background="#ff0000"

/**
* 引用方式2:Java
*/
//方法1
int color = getResources().getColor(R.color.mycolor);

//方法2(API 23及以上)
int color = getColor(R.color.myColor);

9.4 取色工具

  • 颜色都是用RGB值定义的,而我们一般是无法直观的知道自己需要颜色的值,需要借用取色工具直接从图片或者其他地方获取颜色的RGB值。
  • 有时候一些简单的颜色选取就不用去麻烦UI了,开发者自己去选取效率更高
  • 这里,取色工具我强推Markman:一款设计师用于标注的工具,主要用于尺寸标注、字体大小标注、颜色标注,而且使用简单。本人强烈推荐!




收起阅读 »

如何美化checkbox

前言 对于前端开发人员,checkbox应该是经常见到的东西。利用checkbox的checked属性,我们可以做出很多精彩的效果,之前还用checkbox来做动画暂停。前几天还看到外国大佬使用 checkbok做游戏:http://www.bryanbrau...
继续阅读 »

前言


对于前端开发人员,checkbox应该是经常见到的东西。利用checkbox的checked属性,我们可以做出很多精彩的效果,之前还用checkbox来做动画暂停。前几天还看到外国大佬使用 checkbok做游戏:http://www.bryanbraun.com/2021/09/21/… ,真的是佩服的五体投地,不过对于我这种菜鸡选手,还是只能实现一些简单的东西。对于下面的这个switch按钮,大家应该非常熟悉了,同样的在这个效果上还衍生出了各种华丽花哨的效果,例如暗黑模式的切换。一生万,掌握了一,万!还不是手到擒来。


image-20211128233027149


推荐大家看看codepen上的这个仓库:文章封面的效果,也是从这里录制的!
tql

codepen.io/oliviale/pe…


image-20211128235343983


标签


这里使用for将label和input捆绑


<input type="checkbox" id="toggle" />
<label for="toggle"></label>

同时设置input不可见


input {
display: none;
}

美化label


遇到checkbox的美化问题,基本上都是考虑用美化labl替代美化input。


设置背景颜色,宽高,以及圆角


.switch {
  display: inline-block;
  display:relative;
  width: 40px;
  height: 20px;
  background-color: rgba(0, 0, 0, 0.25);
  border-radius: 20px;
}


最终的效果如下:


image-20211128233616100


切换的圆


在label上会有一个圆,一开始是在左边的,效果如下,其实这个只需要利用伪元素+positon定位,就可以实现了。


image-20211128233732168


这是postion:absolute,同时将位置定位在top1px,left1px。同时设置圆角。


      .switch:after {
      content: "";
      position: absolute;
      width: 18px;
      height: 18px;
      border-radius: 18px;
      background-color: white;
      top: 1px;
      left: 1px;
      transition: all 0.3s;
    }

checked+小球右移动


这里点击之后圆会跑到右边,这里有两种实现方案


1.仍然通过定位


当checkbox处于checked状态,会设置top,left,bottom,right。这里将top,left设置为auto是必须的,这种的好处就是,不需要考虑label的宽度。


  input[type="checkbox"]:checked + .switch:after {
      top: auto;
      left: auto;
      bottom: 1px ;
      right: 1px ;
    }

当然知道label的宽度可以直接,设置top和left


top: 1px;
left: 21px;

2.translateX


*transform: translateX(20px)*

美化切换后的label


加上背景色


input[type="checkbox"]:checked + .switch {
background-color: #7983ff;
}

效果:


switch


后记


看上去本文是一篇介绍一个checkbox美化的效果,其实是一篇告诉你如何美化checkbox的文章,最终的思想就是依赖for的捆绑效果,美化label来达到最终的效果。


作者:半夏的故事
链接:https://juejin.cn/post/7035650204829220877

收起阅读 »

CoordinatorLayout与AppBarLayout。置顶悬停,二级悬停,类似京东、淘宝等二级悬停。

类似京东、淘宝等二级悬停。 参考+实践 一、惯例先上效果图 二、GitHub 代码地址,欢迎指正https://github.com/MNXP/SlideTop 三、XML布局主要用到的控件 1、PullRefreshLayout (借用这位大神的ht...
继续阅读 »

类似京东、淘宝等二级悬停。
参考+实践




一、惯例先上效果图


效果图


二、GitHub

三、XML布局主要用到的控件


  1、PullRefreshLayout (借用这位大神的https://github.com/genius158/PullRefreshLayout)
2、CoordinatorLayout
3、AppBarLayout

四、实现

1、布局的实现



  需要注意的几点:
1)AppBarLayout 设置 behavior 需要自己定义,为以后拦截事件用
app:layout_behavior=".weight.MyBehavior"
2)AppBarLayout 第一个子view,就是需要滑动消失的布局,设置
app:layout_scrollFlags="scroll|exitUntilCollapsed"
scroll 滚动,exitUntilCollapsed 可以在置顶后有阴影效果
3)最外层RecyclerView(也可以是各种带滑动的view,也可以是ViewPager实现分页) 设置
app:layout_behavior="@string/appbar_scrolling_view_behavior"


xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

android:layout_width="match_parent"
android:layout_height="60dp"
android:background="#ffffff"
android:orientation="vertical">

android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="置顶滑动"
android:textColor="@color/black"
android:textSize="20sp" />
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_alignParentBottom="true"
android:background="#dddddd"/>


android:id="@+id/swipe_refresh_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:prl_pullDownMaxDistance="300dp"
app:prl_twinkEnable="true">

android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/home_top_view"
android:orientation="vertical">

android:id="@+id/app_bar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#ffffff"
app:layout_behavior=".weight.MyBehavior">

android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
android:layout_width="match_parent"
android:layout_height="150dp"
android:src="@mipmap/home_c"/>
android:id="@+id/top_img_rv"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>


android:id="@+id/home_tab_container_layout"
android:layout_width="match_parent"
android:layout_height="55dp"
android:gravity="center"
android:orientation="horizontal">
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:layout_marginLeft="20dp"
android:textSize="15sp"
android:textColor="#222222"
android:text="悬停标题"/>

android:id="@+id/filter_layout"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_marginEnd="20dp"
android:scaleType="fitXY"
android:src="@mipmap/home_icon" />




android:id="@+id/bottom_img_rv"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />








2、首先解决PullRefreshLayout 与 CoordinatorLayout(依靠AppBarLayout来处理)滑动冲突


通过AppBarLayout监听addOnOffsetChangedListener获取CoordinatorLayout是否滑动到顶部,
设置PullRefreshLayout是否可以上拉刷新

   // 记录AppBar滚动距离
appBarLayout.addOnOffsetChangedListener(View::setTag);
homeRefreshLayout.setOnTargetScrollCheckListener(new PullRefreshLayout.OnTargetScrollCheckListener() {
@Override
public boolean onScrollUpAbleCheck() {
// 根据AppBar滚动的距离来设置RefreshLayout是否可以下拉刷新
int appbarOffset = ((appBarLayout.getTag() instanceof Integer)) ? (int) appBarLayout.getTag() : 0;
return appbarOffset != 0;
}

@Override
public boolean onScrollDownAbleCheck() {
return true;
}
});

3、启用AppBarLayout滑动(不设置也可以,但有的时候会滑动有问题)


注意📢:要在数据加载之后设置,不然不起作用

CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) appBarLayout.getLayoutParams();
MyBehavior behavior = (MyBehavior) params.getBehavior();
try {
if (behavior!=null){
behavior.setDragCallback(new AppBarLayout.Behavior.DragCallback() {
@Override
public boolean canDrag(@NonNull AppBarLayout appBarLayout) {
isFirstData = true;
//为了启用折叠工具栏的滚动
return true;
}
});
}

} catch (Exception e) {

}

4、自以为很完美了┭┮﹏┭┮,但是遇到一个小问题


  问题?
滑动置顶之后,滑动下面的recyclerview,使得recyclerview不是显示第一个item,松开手,
然后向下滑动“悬停标题”,发现可以向下滑动,🤩,是bug的味道。如下图

BUG的味道


下面就开始解决


 解决思路就是根据下面的RecyclerView滑动,设置AppBarLayout是否可以滑动,
(1) 设置监听RecyclerView第一个完整item
(2) 根据Position来设置behavior.setCanMove(position<1);
(3) MyBehavior实现是否可以滑动
上代码

// (1)设置监听RecyclerView第一个完整item
bottomRv.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if (newState == SCROLL_STATE_IDLE) {
if (bottomRv != null && bottomRv.getLayoutManager() instanceof LinearLayoutManager) {
LinearLayoutManager layoutManager = (LinearLayoutManager) bottomRv.getLayoutManager();
if (layoutManager != null) {
// 根据滑动item设置置顶是否可以滑动
int firstCompletelyVisible = layoutManager.findFirstCompletelyVisibleItemPosition();
initAppbar(firstCompletelyVisible);
}
}
}
}
});
//根据Position来设置behavior.setCanMove(position<1);
private boolean isFirstData;
private int oldPosition = -2;
public void initAppbar(int position) {
if (oldPosition == position){
return;
}
oldPosition = position;
CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) appBarLayout.getLayoutParams();
MyBehavior behavior = (MyBehavior) params.getBehavior();
try {
if (behavior!=null){
if (position == -1){
behavior.setDragCallback(new AppBarLayout.Behavior.DragCallback() {
@Override
public boolean canDrag(@NonNull AppBarLayout appBarLayout) {
isFirstData = true;
//为了启用折叠工具栏的滚动
return true;
}
});
}else {
// 置顶后,如果recyclerview不是第一个item,禁止工具栏滑动
behavior.setCanMove(position<1);
}
}

} catch (Exception e) {
}
}
// (3) MyBehavior实现是否可以滑动
@Override
public boolean onInterceptTouchEvent(@NonNull CoordinatorLayout parent, @NonNull AppBarLayout child, @NonNull MotionEvent ev) {
if (!canMove && ev.getAction() == MotionEvent.ACTION_DOWN){
return false;
}
return super.onInterceptTouchEvent(parent, child, ev);
}

public void setCanMove(boolean canMove){
this.canMove = canMove;
}

5、完整代码


Activity代码

  public class MainActivity extends AppCompatActivity {
private RecyclerView topRv;
private RecyclerView bottomRv;
private AppBarLayout appBarLayout;
private PullRefreshLayout homeRefreshLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
appBarLayout = findViewById(R.id.app_bar_layout);
homeRefreshLayout = findViewById(R.id.swipe_refresh_layout);
topRv = findViewById(R.id.top_img_rv);
bottomRv = findViewById(R.id.bottom_img_rv);
topRv.setLayoutManager(new LinearLayoutManager(this));
bottomRv.setLayoutManager(new LinearLayoutManager(this));
initView();
initData();
}
private void initView() {
StoreHouseHeader header = new StoreHouseHeader(this);
header.setPadding(0, 20, 0, 20);
header.initWithString("XIANGPAN");
header.setTextColor(0xFF222222);

homeRefreshLayout.setHeaderView(header);

homeRefreshLayout.setOnRefreshListener(new PullRefreshLayout.OnRefreshListenerAdapter() {
@Override
public void onRefresh() {
initData();
checkHandler.sendEmptyMessageDelayed(0,2000);
}
});
// 记录AppBar滚动距离
appBarLayout.addOnOffsetChangedListener(View::setTag);

homeRefreshLayout.setOnTargetScrollCheckListener(new PullRefreshLayout.OnTargetScrollCheckListener() {
@Override
public boolean onScrollUpAbleCheck() {
// 根据AppBar滚动的距离来设置RefreshLayout是否可以下拉刷新
int appbarOffset = ((appBarLayout.getTag() instanceof Integer)) ? (int) appBarLayout.getTag() : 0;
return appbarOffset != 0;
}

@Override
public boolean onScrollDownAbleCheck() {
return true;
}
});

}
private void initData() {
initTop();
initBottom();
if (!isFirstData) {
initAppbar(-1);
}

appBarLayout.setExpanded(true, false);
}

private void initBottom() {
PhotoAdapter bottomAdapter = new PhotoAdapter();
bottomRv.setAdapter(bottomAdapter);
bottomAdapter.setDataList(10);
bottomRv.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if (newState == SCROLL_STATE_IDLE) {
if (bottomRv != null && bottomRv.getLayoutManager() instanceof LinearLayoutManager) {
LinearLayoutManager layoutManager = (LinearLayoutManager) bottomRv.getLayoutManager();
if (layoutManager != null) {
// 根据滑动item设置置顶是否可以滑动
int firstCompletelyVisible = layoutManager.findFirstCompletelyVisibleItemPosition();
initAppbar(firstCompletelyVisible);
}
}
}
}
});
}

private void initTop() {
PhotoAdapter topAdapter = new PhotoAdapter();
topRv.setAdapter(topAdapter);
topAdapter.setDataList(4);
}
private boolean isFirstData;
private int oldPosition = -2;
public void initAppbar(int position) {
if (oldPosition == position){
return;
}
oldPosition = position;
CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) appBarLayout.getLayoutParams();
MyBehavior behavior = (MyBehavior) params.getBehavior();
try {
if (behavior!=null){
if (position == -1){
behavior.setDragCallback(new AppBarLayout.Behavior.DragCallback() {
@Override
public boolean canDrag(@NonNull AppBarLayout appBarLayout) {
isFirstData = true;
//为了启用折叠工具栏的滚动
return true;
}
});
}else {
// 置顶后,如果recyclerview不是第一个item,禁止工具栏滑动
behavior.setCanMove(position<1);
}
}

} catch (Exception e) {

}

}
public Handler checkHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
//模拟网络请求结束,去除刷新效果
if (homeRefreshLayout != null) {
homeRefreshLayout.refreshComplete();
}
}
};
}

MyBehavior代码

public class MyBehavior extends AppBarLayout.Behavior {


private boolean canMove = true;

public MyBehavior() {

}

public MyBehavior(Context context, AttributeSet attrs) {

super(context, attrs);
}

@Override
public boolean onInterceptTouchEvent(@NonNull CoordinatorLayout parent, @NonNull AppBarLayout child, @NonNull MotionEvent ev) {
if (!canMove && ev.getAction() == MotionEvent.ACTION_DOWN){
return false;
}
return super.onInterceptTouchEvent(parent, child, ev);
}

public void setCanMove(boolean canMove){
this.canMove = canMove;
}

public boolean isCanMove() {
return canMove;
}
}



以上就是全部内容,待完善,以后会更新。如有建议和意见,请及时沟通。


作者:_xiangpan
链接:https://juejin.cn/post/7018453099794825252
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

Metaverse 已经到来:5 家公司正在构建我们的虚拟现实未来

如果你相信 Facebook,未来就是一个虚拟现实的“元宇宙”。这家上个月更名为 Meta的科技巨头计划今年投资100 亿美元来开发支持增强现实和虚拟现实的产品——机械手、高科技 VR 眼镜和复杂的软件应用程序,仅举几例。分析师预计该公司至少要花费 500 亿...
继续阅读 »

如果你相信 Facebook,未来就是一个虚拟现实的“元宇宙”

这家上个月更名为 Meta的科技巨头计划今年投资100 亿美元来开发支持增强现实虚拟现实的产品——机械手高科技 VR 眼镜和复杂的软件应用程序,仅举几例。分析师预计该公司至少要花费 500 亿美元来实现其对虚拟现实未来的承诺。

但 Meta 远非唯一的玩家。事实上,六家其他公司已经在构建将成为下一代虚拟交互的硬件和软件——华尔街认为这是一个价值 1 万亿美元的市场。这些公司包括谷歌、微软、苹果、Valve 和其他开发工作和通信产品的公司。随着投资者涌入市场,规模较小的初创公司可能会加入他们的行列。

“元宇宙是真实的,华尔街正在寻找赢家,”韦德布什分析师丹艾夫斯在一份报告中说。

在 Facebook 试图在元领域打上烙印时,这些公司的产品将不得不与之抗衡。

谷歌

Google Cardboard 可能是历史上最成功的 VR 项目。2014 年,当时世界上最大的科技公司要求数百万人用一块硬纸板将智能手机绑在脸上。谷歌表示,它出货了“数千万”可折叠耳机,谷歌 Cardboard应用程序的下载量超过 1.6 亿次。这不是最高分辨率或高科技的体验,但该策略帮助向数百万学生有抱负的开发人员介绍了虚拟现实。

它还帮助谷歌摆脱了之前的增强现实实验Glass今天,增强现实眼镜作为企业业务的工具进行销售,但当它推出时,谷歌的期望值很高。字面意思是:谷歌创始人谢尔盖·布林 (Sergey Brin) 从飞机上跳下时宣布了 1,500 美元的产品。

玻璃本质上是智能手机的内脏,在非处方眼镜框架上安装了一个小型摄像头。该项目失败了,但在催生了无数模因之前就失败了。 

微软

微软于 2015 年发布Hololens混合现实眼镜。一年后,微软并没有用营销炒作充斥消费者市场,而是悄悄推出了 Hololens 作为工业制造工具,面向选定的企业集团。价值 3,000 美元的商业套件附带专业版 Windows,具有额外的安全功能和软件以帮助应用程序开发。第二次迭代于 2019 年首次亮相,价格稍贵,但拥有更好的相机和镜头卡口,可实现更精确的操作,并提供更广泛的软件功能,包括工业应用。 

目前Hololens用户包括像肯沃斯,三得利和丰田,它使用耳机,以加快培养和汽车修理重量级人物,根据微软

苹果

如果你相信传言,苹果一直在释放的风口浪尖AR眼镜多年这家 iPhone 制造商于 2017 年在 iOS 11 中发布了ARKit,这是为 Apple 设备创建增强现实应用程序的开发者框架。 据科技网站The Information报道,Apple 在 2019 年举行了一次 1000 人的会议,讨论了 iPhone 上的 AR 和两个潜力未来的产品,N421 智能眼镜和 N301 VR 耳机分析师现在推测,苹果正准备在 2022 年及以后发布 AR 产品。   

阀门

Valve 的Index耳机可以说是市场上最强大的消费虚拟现实产品。高分辨率屏幕流畅,控制器在虚拟现实和游戏环境中提供无与伦比的控制。该索引还与 Value 的Steam视频游戏市场集成,这意味着该设备已经堆满了兼容的内容。

它也很贵而且很笨拙。完整的 Index VR 套件的价格接近 1,000 美元,要正常运行,耳机需要多条电缆和传感器。Valve 继续创新和试验沉浸式虚拟现实耳机。分析师预计,这家总部位于贝尔维尤的游戏公司将很快发布一款独立耳机,与 Facebook 的Oculus Quest 2展开竞争

魔法飞跃

尽管虚拟现实的想法部分受到科幻小说的启发,但 Big Tech 对 AR 和 VR 未来的现代愿景直接受到 Magic Leap 的启发。该公司成立于 2010 年,2014 年从谷歌和芯片制造商高通等公司筹集了超过 5 亿美元 2015 年,该公司发布了一段令人惊叹的视频,旨在展示该产品的技术。但是怀疑论者质疑这项技术,最终的产品遭到了抨击

最初的 Magic Leap 耳机是在设计和广告等创意协作行业销售的。

Magic Leap于 2018 年推出了一款精致的 AR 设备,筹集了更多资金,并计划在 2022 年初发布Magic Leap 2。该公司还计划瞄准国防、医疗保健和工业制造。

收起阅读 »

HashMap有何特别之处,为什么java面试从不缺席?

涉及知识点 看过java面试经验分享的小伙伴或者经历过准备过校招面试的小伙伴应该都曾经被Hashmap给支配过,即使是社招HashMap也仍然是高频考点,那么究竟为什么大家都喜欢问HashMap,其中包含了哪些知识点? 首先从生产的角度来说,HashMap是...
继续阅读 »

涉及知识点


看过java面试经验分享的小伙伴或者经历过准备过校招面试的小伙伴应该都曾经被Hashmap给支配过,即使是社招HashMap也仍然是高频考点,那么究竟为什么大家都喜欢问HashMap,其中包含了哪些知识点?



  • 首先从生产的角度来说,HashMap是我们在生产过程中最常用的集合之一,如果完全不懂它的原理,很难发挥出它的优势甚至会造成线上bug

  • 从数据结构和算法的角度,HashMap涉及到了数组,链表,红黑树,以及三者相互转化的过程,以及位运算等丰富的数据结构和算法内容。


所以HashMap就成为了面试的高频考点,这些过程都清楚,说明数据结构和算法的基础不会太差。


HashMap基本知识


JDK1.8之前数据结构是数组+链表
JDK1.8以后采用数组+链表+红黑树


image.png


其中,当数组元素超过64个且数组单个位置存储的链表元素个数超过8个时,会进化成红黑树,红黑树的引进时为了加快查找效率


同样,当数组单个位置存储的链表元素个数少于6个时,又会退化成链表


HashMap重要的类属性


其实JDK的源码都有非常详细的注释,但还是翻译一下吧,如下:



  • 初始容量大小为16


/**
* The default initial capacity - MUST be a power of two.
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16


  • 最大容量为2^30


/**
* The maximum capacity, used if a higher value is implicitly specified
* by either of the constructors with arguments.
* MUST be a power of two <= 1<<30.
*/
static final int MAXIMUM_CAPACITY = 1 << 30;


  • 默认的负载因子为0.75


负载因子之所以选择是基于时间和空间平衡的结果选择的参数,时间和空间的平衡是指既不浪费太多的空间,又不用频繁地进行扩容


/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;


  • 链表元素数量进化成红黑树的阈值为8,数组元素大于64时,链表元素超过8就会进化为红黑树


/**
* The bin count threshold for using a tree rather than list for a
* bin. Bins are converted to trees when adding an element to a
* bin with at least this many nodes. The value must be greater
* than 2 and should be at least 8 to mesh with assumptions in
* tree removal about conversion back to plain bins upon
* shrinkage.
*/
static final int TREEIFY_THRESHOLD = 8;

至于为什么选8,在之前有一段很长的注释里面有说明,可以看一下原文如下:


 * Because TreeNodes are about twice the size of regular nodes, we
* use them only when bins contain enough nodes to warrant use
* (see TREEIFY_THRESHOLD). And when they become too small (due to
* removal or resizing) they are converted back to plain bins. In
* usages with well-distributed user hashCodes, tree bins are
* rarely used. Ideally, under random hashCodes, the frequency of
* nodes in bins follows a Poisson distribution
* (http://en.wikipedia.org/wiki/Poisson_distribution) with a
* parameter of about 0.5 on average for the default resizing
* threshold of 0.75, although with a large variance because of
* resizing granularity. Ignoring variance, the expected
* occurrences of list size k are (exp(-0.5) * pow(0.5, k) /
* factorial(k)). The first values are:
*
* 0: 0.60653066
* 1: 0.30326533
* 2: 0.07581633
* 3: 0.01263606
* 4: 0.00157952
* 5: 0.00015795
* 6: 0.00001316
* 7: 0.00000094
* 8: 0.00000006
* more: less than 1 in ten million

大概意思就是:如果 hashCode的分布离散良好的话,那么红黑树是很少会被用到的,因为各个值都均匀分布,很少出现链表很长的情况。在理想情况下,链表长度符合泊松分布,各个长度的命中概率依次递减,注释中给我们展示了1-8长度的具体命中概率,当长度为8的时候,概率概率仅为0.00000006,这么小的概率,HashMap的红黑树转换几乎不会发生



  • 红黑树元素少于6个就退化成链表


至于为什么是6个不是7个是为了避免频繁在树与链表之间变换,如果是7,加一个元素就需要进化成红黑树,删一个元素就需要退化成链表


/**
* The bin count threshold for untreeifying a (split) bin during a
* resize operation. Should be less than TREEIFY_THRESHOLD, and at
* most 6 to mesh with shrinkage detection under removal.
*/
static final int UNTREEIFY_THRESHOLD = 6;


  • 链表树能进化成树时最小的数组长度,只有数组长度打到这个值链表才可能进化成树


/**
* The smallest table capacity for which bins may be treeified.
* (Otherwise the table is resized if too many nodes in a bin.)
* Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
* between resizing and treeification thresholds.
*/
static final int MIN_TREEIFY_CAPACITY = 64;

重要的内部类


链表Node<K,V>


定义的链表Node类,next有木有很熟悉,单向链表
/**
* Basic hash bin node, used for most entries. (See below for
* TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
*/
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;

Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}

public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }

public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}

public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}

public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}

红黑树TreeNode<K,V>


/**
* Entry for Tree bins. Extends LinkedHashMap.Entry (which in turn
* extends Node) so can be used as extension of either regular or
* linked node.
*/
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}

/**
* Returns root of tree containing this node.
*/
final TreeNode<K,V> root() {
for (TreeNode<K,V> r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}

HashMap的put方法


实际上调用的是putVal方法


/**
* Associates the specified value with the specified key in this map.
* If the map previously contained a mapping for the key, the old
* value is replaced.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
* (A <tt>null</tt> return can also indicate that the map
* previously associated <tt>null</tt> with <tt>key</tt>.)
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

接下来看看putVal方法


/**
* Implements Map.put and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}

putVal的流程如下:


HashMap的put方法执行过程.png


HashMap的resize扩容方法(重要)


先翻译一下这个方法的官方注释:
初始化或者将原来的容量翻倍,如果为空,则分配初始容量,否则,进行容量翻倍,原来存在的元素要么留在原来的位置上,要么在新的数组中向后移动原来数组容量的大小


/**
* Initializes or doubles table size. If null, allocates in
* accord with initial capacity target held in field threshold.
* Otherwise, because we are using power-of-two expansion, the
* elements from each bin must either stay at same index, or move
* with a power of two offset in the new table.
*
* @return the table
*/
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
//数组原来不为空
if (oldCap > 0) {
// 超过最大值就不再扩充了,就只好随你碰撞去吧
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 没超过最大值,就扩充为原来的2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
//初始化,申明HashMap的时候指定了初始容量
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
//初始化,申明HashMap的时候没有指定初始容量
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 计算新的resize上限
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;

//扩容之后的新数组
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}

整个扩容的流程如下:


HashMap的resize过程.png


链表重新整理的过程


可能会将原来一个链表拆分为两个链表,判断当前链表元素是否需要移动到新的链表的依据是:
计算(e.hash & oldCap) == 0?
true,不需要移动到另一个链表,我们用头结点为loHead的链表将这些元素串起来
false,元素需要移动到另一个新的链表,我们用头结点为hiHead的链表将这些元素串起来
(tips:其实lo和hi是low和high的缩写,这里用的是两个双指针来进行链表的拆分整理)
以数组从容量为16扩容到32为例,位于下标为1的链表扩容之后如图所示:一部分链表仍然保留在下标为1的位置,另一部分则迁移至下标为1+16的位置。


HashMap扩容链表重新整理.png
在链表重新整理的过程中,同一个链表的元素的相对顺序不会被改变


红黑树重新整理的过程


看了一下TreeNode的继承关系,TreeNdoe也是继承Node的子类,红黑树重新整理的过程也和链表相似,因为它其实也维护了一个链表的结构
拆分可能会将原来一个红黑树拆分为两个链表,或者一个红黑树一个链表,判断当前链表元素是否需要移动到新的链表的依据是:
计算(e.hash & oldCap) == 0?
true,不需要移动,仍然在原来的位置,我们用头结点为loHead的链表将这些元素串起来,如果元素的个数不小于6,则继续维护成红黑树,否则为链表
false,元素需要移动到另一个新的位置,我们用头结点为hiHead的链表将这些元素串起来,如果元素的个数不小于6,则继续维护成红黑树,否则为链表


HashMap的TreeNode.png


HashMap线程安全性


HashMap中存在的线程安全问题:


1.HashMap在读取Hash槽首元素的时候读取的是工作内存中引用所指向的对象,并发情况下,其他线程修改的值并不能被及时读取到。


2.HashMap在插入新元素的时候,主要会进行两次判断:


2.1 第一次是根据键的hash判断当前hash槽是否被占用,如果没有就放入当前插入对象。并发情况下,如果A线程判断该槽未被占用,在执行写入操作时时间片耗尽。此时线程B也执行获取hash(恰巧和A线程的对象发生hash碰撞)判断该槽未被占用,继而直接插入该对象。然后A线程被CPU重新调度继续执行写入操作,就会将线程B的数据覆盖。(注:此处也有可见性问题)


2.2 第二次是同一个hash槽内,因为HashMap特性是保持key值唯一,所以会判断当前欲插入key是否存在,存在就会覆盖。与上文类似,并发情况下,如果线程A判断最后一个节点仍未发现重复key那么会把以当前对象构建节点挂在链表或者红黑树上,如果线程B在A判断操作和写操作之间,进行了判断和写操作,也会发生数据覆盖。


除此之外扩容也会发生类似的并发问题。还有size的问题,感觉其实高并发情况下这个size的准确性可以让步性能。


参考文章


【1】tech.meituan.com/2016/06/24/…

【2】blog.csdn.net/weixin_3143…


作者:ForeverKobe
链接:https://juejin.cn/post/7033392478753390629
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

为什么 JakeWharton 建议:App 只要用到一个 Activity ?

安卓开发大神级人物 JakeWharton 前不久在接受采访时提出一个颇具争议而又没有给出原因的建议:一个 App 只需要一个 Activity ,你可以使用 Fragments,只是别用 Fragments 回退栈。 针对这一言论,有关 JakeWharto...
继续阅读 »

安卓开发大神级人物 JakeWharton 前不久在接受采访时提出一个颇具争议而又没有给出原因的建议:一个 App 只需要一个 Activity ,你可以使用 Fragments,只是别用 Fragments 回退栈。


针对这一言论,有关 JakeWharton 建议的背后原因的一个提问迅速在 Reddit 国外网站的安卓开发频道引发热评。



众多安卓开发人员纷纷提出自己的见解。其中获赞最高的一条,甚至得到 JakeWharton 本人的亲自赞评。



我们来看看这条回答都提到了哪些内容,对 Activity 和 Fragment 之间的爱恨情仇有何独到的见解,凭什么能得到 JakeWharton 本尊的青睐有加。




因为 Activity 是一个程序入口。你可以将其视为 app 的一个 main 函数。站在用户的立场上,通常你进入 app 的方式可能包括以下几种:




  • launcher 桌面程序(main 函数入口);




  • 来自参数化 main 函数入口的通知栏,并且导航到 app 的指定位置;




  • 如果你做的是一个相机应用,那么需要处理图片请求的 intents;




  • 如果你做的是一个社交产品,那么需要处理 share 请求的 intents;




差不多类似这些场景。


但是,如果你真的不用分享和来自应用的 intents 的话,并且唯一的程序入口就是 launcher 桌面,别为每一个页面创建一个新的入口。这样做其实没有意义。为什么没有意义?因为这种场景下,进程死掉后 launcher 能够启动任何你应用中的 Activity 页面。


Fragments 是处理生命周期事件的视图控制器,并且非常不错。然而,Fragments 回退栈简直垃圾;回退栈变化监听器总是不正常地被调用( 1 次 transaction 三次调用?),并且不告诉你调用什么,而在恢复事务时也不知道哪些 fragments 是可用的。


你可以给事务添加 tag 标签,然后从栈中弹出操作,但是仅仅是一个 main -> Events -> Details(id=123) 的操作流程就相当繁琐了。


同样的,一旦你将一个 Fragment 放进回退栈中,我个人不知道它的生命周期开始做什么。我曾经遇到过一个后台中的 fragment 被调用四次 onCreateView() 方法,我甚至不知道究竟怎么了。而没有位于回退栈中的 Fragments 是可以被预见的。它们的动画支持有点古怪,但至少它们还能使用。


所以如果你想知道哪些 Fragments 是你能够操作的并且哪些 views 是你正在展示的并且能够在你自己的导航状态控制之中,那么你应该自己处理导航操作。把“应用逻辑”抽象化到一个 presenter(亦枫注:MVP 模式)中听起来来很棒,但是你是不是脱离了应用视图层里面的真实情况?



但是单一 activity 的优势是什么?



更简单的生命周期处理(例如,当 app 进入后台时,你只需要处理 onStop 方法),更少错误空间,和更多控制。同样的,你可以移动视图层外面的导航状态到 domain 层,或者至少到 presenter 中。不需要太多 view.navigateToDetail(songId) 之类的东西,你只需要在你的 presenter 或者 ViewModel 或者无论哪些时髦的用法中使用 backstack.goTo(SongKey.create(songId)) 就行。借助一个合适的库,当你到了 onResume 时它会自动将这些导航调用加入队列,并且不会致使 fragment 事务发生崩溃,非常得好。


尽管 Google 给出的案例也在用 commitAllowingStateLoss(),我有使用 commitNow() 的动画爱好。在我看来,单个 activity 能够看得见的好处就是,页面间共享 views 的能力,取代通过使用 <include 标签在 18 个布局文件重复视图。其他当然是更简单的导航操作。




以上便是深得 JakeWharton 大神心意的一条回答。话虽如此,但是系统 Fragment 存在的未解之谜或者说出乎你意料的坑实在太多。如果一定要在多 activity 部分 fragments 和单 activity 多 fragments 之间选择的话,我想不只是我,很多人还是毫不犹豫地选择前者。


更多讨论内容,参见:


http://www.reddit.com/r/androidde…


作者:亦枫
链接:https://juejin.cn/post/6844903521209286663
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

termux 安卓神器

今年春节在家的时候,手头没有电脑,但是想用电脑写下代码,于是乎我找到这一款termux神器,可以把安卓手机当作一台小型的服务器来使用。利用5年前已经淘汰的安卓手机,插上适配器,这样我就可以无休止的跑我的脚本了。termux 安装在termux官网上看到最新的版...
继续阅读 »

今年春节在家的时候,手头没有电脑,但是想用电脑写下代码,于是乎我找到这一款termux神器,可以把安卓手机当作一台小型的服务器来使用。利用5年前已经淘汰的安卓手机,插上适配器,这样我就可以无休止的跑我的脚本了。

termux 安装

在termux官网上看到最新的版本,必须要安装在Android 7以上的手机,我的魅族手机经过我的一番折腾只能升级到安卓6,不能安装最新的termux,还好旧的版本0.73支持Android 5以上。  安装包下载后,就是常规的apk安装了。

termux 环境配置

termux支持sshd,所有我们不用在旧手机上进行操作,我们可以在自己新安卓手机上通过ssh连接到旧手机上,前提是termux要启动sshd.

#安装openssh
pkg install openssh
#安装后,启动sshd
sshd
#设置用户密码
passwd
#登陆用户名
whoami
#查看局域网内ip
ifconfig|grep inet

我们在另外一台手机上安卓juicessh,一款安卓的ssh客户端,由于termux的sshd端口跟我们平时使用的22端是不一样的,所以在ssh到termux采用的是8022端口,通过刚才我们查看的用户名登陆,输入我们刚刚设置的密码,就能完成ssh手机登陆了。

现在我们就能愉快的采用termux进行学习了。

vim 练习

#安装vim
pkg install vim
vim hello.txt

 这就跟我们配置在服务器用vim一样,我们就可以练习下vim的快捷键

安装nodejs

#安装nodejs
pkg install nodejs
node -v
pkg install npm
npm -v

安装软件的时候,会有点慢,但是千万不要切换镜像,因为我们的版本比较旧,国内的源没有搬运旧的,所以切换后会导致安装软件出现各自问题,这是卸载安装5次的体验。清华源网站已经提示了镜像仅适用于 Android 7.0 (API 24) 及以上版本,旧版本系统使用本镜像可能导致程序错误。

安装好nodejs后,我们可以做更多有趣的事情,运行node脚本,node server服务器,个人网站等等,甚至可以做一些定时任务,比如定时发天气预告邮件等。


作者:chiv
链接:https://juejin.cn/post/6930531076607737870
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

能让你更早下班的Python垃圾回收机制

人生苦短,只谈风月,谈什么垃圾回收。能让你更早下班的Python垃圾回收机制_内存空间据说上图是某语言的垃圾回收机制。。。我们写过C语言、C++的朋友都知道,我们的C语言是没有垃圾回收这种说法的。手动分配、释放内存都需要我们的程序员自己完成。不管是“内存泄漏”...
继续阅读 »



人生苦短,只谈风月,谈什么垃圾回收。

能让你更早下班的Python垃圾回收机制_内存空间

能让你更早下班的Python垃圾回收机制_内存空间

据说上图是某语言的垃圾回收机制。。。

我们写过C语言、C++的朋友都知道,我们的C语言是没有垃圾回收这种说法的。手动分配、释放内存都需要我们的程序员自己完成。不管是“内存泄漏” 还是野指针都是让开发者非常头疼的问题。所以C语言开发这个讨论得最多的话题就是内存管理了。但是对于其他高级语言来说,例如Java、C#、Python等高级语言,已经具备了垃圾回收机制。这样可以屏蔽内存管理的复杂性,使开发者可以更好的关注核心的业务逻辑。

对我们的Python开发者来说,我们可以当甩手掌柜。不用操心它怎么回收程序运行过程中产生的垃圾。但是这毕竟是一门语言的内心功法,难道我们甘愿一辈子做一个API调参侠吗?

1.什么是垃圾?

当我们的Python解释器在执行到定义变量的语法时,会申请内存空间来存放变量的值,而内存的容量是有限的,这就涉及到变量值所占用内存空间的回收问题。

当一个对象或者说变量没有用了,就会被当做“垃圾“。那什么样的变量是没有用的呢?

a = 10000

当解释器执行到上面这里的时候,会划分一块内存来存储 10000 这个值。此时的 10000 是被变量 a 引用的

a = 30000

当我们修改这个变量的值时,又划分了一块内存来存 30000 这个值,此时变量a引用的值是30000。

这个时候,我们的 10000 已经没有变量引用它了,我们也可以说它变成了垃圾,但是他依旧占着刚才给他的内存。那我们的解释器,就要把这块内存地盘收回来。

2.内存泄露和内存溢出

上面我们了解了什么是程序运行过程中的“垃圾”,那如果,产生了垃圾,我们不去处理,会产生什么样的后果呢?试想一下,如果你家从不丢垃圾,产生的垃圾就堆在家里会怎么呢?

  1. 家里堆满垃圾,有个美女想当你对象,但是已经没有空间给她住了。

  2. 你还能住,但是家里的垃圾很占地方,而且很浪费空间,慢慢的,总有一天你的家里会堆满垃圾

上面的结果其实就是计算机里面让所有程序员都闻风丧胆的问题,内存溢出和内存泄露,轻则导致程序运行速度减慢,重则导致程序崩溃。

内存溢出:程序在申请内存时,没有足够的内存空间供其使用,出现 out of memory

内存泄露:程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光

3.引用计数

前面我们提到过垃圾的产生的是因为,对象没有再被其他变量引用了。那么,我们的解释器究竟是怎么知道一个对象还有没有被引用的呢?

答案就是:引用计数。python内部通过引用计数机制来统计一个对象被引用的次数。当这个数变成0的时候,就说明这个对象没有被引用了。这个时候它就变成了“垃圾”。

这个引用计数又是何方神圣呢?让我们看看代码

text = "hello,world"

上面的一行代码做了哪些工作呢?

  • 创建字符串对象:它的值是hello,world

  • 开辟内存空间:在对象进行实例化的时候,解释器会为对象分配一段内存地址空间。把这个对象的结构体存储在这段内存地址空间中。

我们再来看看这个对象的结构体

typedef struct_object {
int ob_refcnt;
struct_typeobject *ob_type;
} PyObject;

熟悉c语言或者c++的朋友,看到这个应该特别熟悉,他就是结构体。这是因为我们Python官方的解释器是CPython,它底层调用了很多的c类库与接口。所以一些底层的数据是通过结构体进行存储的。看不懂的朋友也没有关系。

这里,我们只需要关注一个参数:ob_refcnt

这个参数非常神奇,它记录了这个对象的被变量引用的次数。所以上面 hello,world 这个对象的引用计数就是 1,因为现在只有text这个变量引用了它。

3.1 变量初始化赋值:

text = "hello,world"

能让你更早下班的Python垃圾回收机制_内存空间_02

3.2 变量引用传递:

new_text = text

能让你更早下班的Python垃圾回收机制_垃圾回收_03

3.3 删除第一个变量:

del text

能让你更早下班的Python垃圾回收机制_垃圾回收机制_04

3.4 删除第二个变量:

del new_text

能让你更早下班的Python垃圾回收机制_垃圾回收_05

此时 “hello,world” 对象的引用计数为:0,被当成了垃圾。下一步,就该被我们的垃圾回收器给收走了。

能让你更早下班的Python垃圾回收机制_python_06

4.引用计数如何变化

上面我们了解了什么是引用计数。那这个参数什么时候会发生变化呢?

4.1 引用计数加一的情况

  • 对象被创建

a = "hello,world"
  • 对象被别的变量引用(赋值给一个变量)

b = a
  • 对象被作为元素,放在容器中(比如被当作元素放在列表中)

list = []
list.append(a)
  • 对象作为参数传递给函数

func(a)

4.2 引用计数减一

  • 对象的引用变量被显示销毁

del a
  • 对象的引用变量赋值引用其他对象

a = "hello, Python"   # a的原来的引用对象:a = "hello,world"
  • 对象从容器中被移除,或者容器被销毁(例:对象从列表中被移除,或者列表被销毁)

del list
list.remove(a)
  • 一个引用离开了它的作用域

func():
  a = "hello,world"
  return

func() # 函数执行结束以后,函数作用域里面的局部变量a会被释放

4.3 查看对象的引用计数

如果要查看对象的引用计数,可以通过内置模块 sys 提供的 getrefcount 方法去查看。

import sys
a = "hello,world"
print(sys.getrefcount(a))

注意:当使用某个引用作为参数,传递给 getrefcount() 时,参数实际上创建了一个临时的引用。因此,getrefcount() 所得到的结果,会比期望的多 1

5.垃圾回收机制

其实Python的垃圾回收机制,我们前面已经说得差不多了。

Python通过引用计数的方法来说实现垃圾回收,当一个对象的引用计数为0的时候,就进行垃圾回收。但是如果只使用引用计数也是有点问题的。所以,python又引进了标记-清除和分代收集两种机制。

Python采用的是引用计数机制为主,标记-清除和分代收集两种机制为辅的策略。

前面的引用计数我们已经了解了,那这个标记-清除跟分代收集又是什么呢?

5.1 引用计数机制缺点

Python语言默认采用的垃圾收集机制是“引用计数法 ”,该算法最早George E. Collins在1960的时候首次提出,50年后的今天,该算法依然被很多编程语言使用。

引用计数法:每个对象维护一个 ob_refcnt 字段,用来记录该对象当前被引用的次数,每当新的引用指向该对象时,它的引用计数ob_refcnt加1,每当该对象的引用失效时计数ob_refcnt减1,一旦对象的引用计数为0,该对象立即被回收,对象占用的内存空间将被释放。

缺点:

  1. 需要额外的空间维护引用计数

  2. 无法解决循环引用问题

什么是循环引用问题?看看下面的例子

a = {"key":"a"}  # 字典对象a的引用计数:1
b = {"key":"b"} # 字典对象b的引用计数:1

a["b"] = b # 字典对象b的引用计数:2
b["a"] = a # 字典对象a的引用计数:2

del a # 字典对象a的引用计数:1
del b # 字典对象b的引用计数:1

看上面的例子,明明两个变量都删除了,但是这两个对象却没有得到释放。原因是他们的引用计数都没有减少到0。而我们垃圾回收机制只有当引用计数为0的时候才会释放对象。这是一个无法解决的致命问题。这两个对象始终不会被销毁,这样就会导致内存泄漏。

那怎么解决这个问题呢?这个时候 标记-清除 就排上了用场。标记清除可以处理这种循环引用的情况。

5.2 标记-清除策略

Python采用了标记-清除策略,解决容器对象可能产生的循环引用问题。

该策略在进行垃圾回收时分成了两步,分别是:

  • 标记阶段,遍历所有的对象,如果是可达的(reachable),也就是还有对象引用它,那么就标记该对象为可达;

  • 清除阶段,再次遍历对象,如果发现某个对象没有标记为可达,则就将其回收

这里简单介绍一下标记-清除策略的流程

能让你更早下班的Python垃圾回收机制_内存空间_07

可达(活动)对象:从root集合节点有(通过链式引用)路径达到的对象节点

不可达(非活动)对象:从root集合节点没有(通过链式引用)路径到达的对象节点

流程:

  1. 首先,从root集合节点出发,沿着有向边遍历所有的对象节点

  2. 对每个对象分别标记可达对象还是不可达对象

  3. 再次遍历所有节点,对所有标记为不可达的对象进行垃圾回收、销毁。

标记-清除是一种周期性策略,相当于是一个定时任务,每隔一段时间进行一次扫描。
并且标记-清除工作时会暂停整个应用程序,等待标记清除结束后才会恢复应用程序的运行。

5.3 分代回收策略

分代回收建立标记清除的基础之上,因为我们的标记-清除策略会将我们的程序阻塞。为了减少应用程序暂停的时间,Python 通过“分代回收”(Generational Collection)策略。以空间换时间的方法提高垃圾回收效率。

分代的垃圾收集技术是在上个世纪 80 年代初发展起来的一种垃圾收集机制。

简单来说就是:对象存在时间越长,越可能不是垃圾,应该越少去收集

Python 将内存根据对象的存活时间划分为不同的集合,每个集合称为一个代,Python 将内存分为了 3“代”,分别为年轻代(第 0 代)、中年代(第 1 代)、老年代(第 2 代)。

那什么时候会触发分代回收呢?

import gc

print(gc.get_threshold())
# (700, 10, 10)
# 上面这个是默认的回收策略的阈值

# 也可以自己设置回收策略的阈值
gc.set_threshold(500, 5, 5)
  • 700:表示当分配对象的个数达到700时,进行一次0代回收

  • 10:当进行10次0代回收以后触发一次1代回收

  • 10:当进行10次1代回收以后触发一次2代回收

能让你更早下班的Python垃圾回收机制_垃圾回收_08

5.4 gc模块

  • gc.get_count():获取当前自动执行垃圾回收的计数器,返回一个长度为3的列表

  • gc.get_threshold():获取gc模块中自动执行垃圾回收的频率,默认是(700, 10, 10)

  • gc.set_threshold(threshold0[,threshold1,threshold2]):设置自动执行垃圾回收的频率

  • gc.disable():python3默认开启gc机制,可以使用该方法手动关闭gc机制

  • gc.collect():手动调用垃圾回收机制回收垃圾

其实,既然我们选择了python,性能就不是最重要的了。我相信大部分的python工程师甚至都还没遇到过性能问题,因为现在的机器性能可以弥补。而对于内存管理与垃圾回收,python提供了甩手掌柜的方式让我们更关注业务层,这不是更加符合人生苦短,我用python的理念么。如果我还需要像C++那样小心翼翼的进行内存的管理,那我为什么还要用python呢?咱不就是图他的便利嘛。所以,放心去干吧。越早下班越好!

创作不易,且读且珍惜。如有错漏还请海涵并联系作者修改,内容有参考,如有侵权,请联系作者删除。如果文章对您有帮助,还请动动小手,您的支持是我最大的动力。


作者: 趣玩Python
来源:https://blog.51cto.com/u_14666251/4674779

收起阅读 »

跨域问题及常见解决方法

1.出现跨域问题是因为浏览器的同源策列限制,下面是MDN文档对浏览器同源策略的描述,简单来说就是:同源策略会阻止一个域的javascript脚本和另外一个域的内容进行交互。所谓同源(即指在同一个域)就是两个页面具有相同的协议(protocol),主机(host...
继续阅读 »



1.出现跨域问题是因为浏览器的同源策列限制,下面是MDN文档对浏览器同源策略的描述,简单来说就是:

同源策略会阻止一个域的javascript脚本和另外一个域的内容进行交互。所谓同源(即指在同一个域)就是两个页面具有相同的协议(protocol),主机(host)和端口号(port)

浏览器的同源策略

同源策略是一个重要的安全策略,它用于限制一个origin的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。

同源的定义

如果两个 URL 的 protocol、port (en-US) (如果有指定的话)和 host 都相同的话,则这两个 URL 是同源。这个方案也被称为“协议/主机/端口元组”,或者直接是 “元组”。(“元组” 是指一组项目构成的整体,双重/三重/四重/五重/等的通用形式)。

2.四种常见解决跨域的方法:

一,CORS:

跨域资源共享,它允许浏览器向非同源服务器,发出XMLHttpRequest请求。它对一般请求和非一般请求的处理方式不同: 1、一般跨域请求(对服务器没有要求):只需服务器端设置Access-Control-Allow-Origin 2、非一般跨域请求(比如要请求时要携带cookie):前后端都需要设置。

一般跨域请求服务器设置代码: (1)Node.JS

const http = require('http');
const server = http.createServer();
const qs = require('querystring');

server.on('request', function(req, res) {
   var postData = '';
   // 数据块接收中
   req.addListener('data', function(chunk) {
       postData += chunk;
  });
   // 数据接收完毕
   req.addListener('end', function() {
       postData = qs.parse(postData);
       // 跨域后台设置
       res.writeHead(200, {
           'Access-Control-Allow-Credentials': 'true',     // 后端允许发送Cookie
           'Access-Control-Allow-Origin': 'http://www.example.com',    // 允许访问的域(协议+域名+端口)  
      });
       res.end(JSON.stringify(postData));
  });
});
server.listen('8080');
console.log('running at port 8080...');

复制代码

(2)PHP

<?php
header("Access-Control-Allow-Origin:*");
复制代码

如果要发送Cookie,Access-Control-Allow-Origin就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie也无法读取服务器域名下的Cookie。

前端请求携带cookie代码:

(1)原生JavaScript

const xhr = new XMLHttpRequest(); 
// 前端设置是否带cookie
xhr.withCredentials = true;
};
复制代码

(2)axios

axios.defaults.withCredentials = true
复制代码

二,JSONP

JSONP 只支持get请求,不支持post请求。 核心思想:网页通过添加一个<scriot>标签,向服务器请求 JSON 数据,服务器收到请求后,将数据放在一个指定名字的回调函数的参数位置传回来。

原生JavaScript代码:

<script src="http://example.php?callback=getData"></script>
// 向服务器发出请求,请求参数callback是下面定义的函数名字

// 处理服务器返回回调函数的数据
<script type="text/javascript">
   function getData(res)
  {
       console.log(res.data)
  }
</script>
复制代码

三,设置document.domain

因为浏览器是通过document.domain属性来检查两个页面是否同源,因此只要通过设置相同的document.domain,两个页面就可以共享Cookie(此方案仅限主域相同,子域不同的跨域应用场景。)

// 两个页面都设置
document.domain = 'test.com';
复制代码

四,跨文档通信 API:window.postMessage()

调用postMessage方法实现父窗口向子窗口发消息(子窗口同样可以通过该方法发送消息给父窗口)

var openWindow = window.open('http://test2.com', 'title');

// 父窗口向子窗口发消息(第一个参数代表发送的内容,第二个参数代表接收消息窗口的url)
openWindow.postMessage('Nice to meet you!', 'http://test2.com');
//调用message事件,监听对方发送的消息

// 监听 message 消息
window.addEventListener('message', function (e) {
 console.log(e.source); // e.source 发送消息的窗口
 console.log(e.origin); // e.origin 消息发向的网址
 console.log(e.data);   // e.data   发送的消息
},false);


作者:玩具大兵
来源:https://juejin.cn/post/7035562059152490526

收起阅读 »

TypeScript 原始类型、函数、接口、类、泛型 基础总结

原始数据类型原始数据类型包括:BooleanStringNumberNullundefined类型声明是TS非常重要的一个特点,通过类型声明可以指定TS中变量、参数、形参的类型。Boolean 类型let boolean: boolean = truebool...
继续阅读 »



原始数据类型

原始数据类型包括:

  • Boolean

  • String

  • Number

  • Null

  • undefined

类型声明是TS非常重要的一个特点,通过类型声明可以指定TS中变量、参数、形参的类型。

  • Boolean 类型

    let boolean: boolean = true
    boolean = false
    boolean = null
    // bollean = 123 报错不可以将数字 123 赋值给 boolean类型的变量
  • Number 类型

    //ES6 Number 类型 新增支持2进制和8进制
    let num: number = 123
    num = 0b1111
  • String 类型

    let str1: string = 'hello TS'
    let sre2: string = `模板字符串也支持使用 ${str1}`]
  • Null 和 Undefined

    let n: null = null
    let u: undefined = undefined
    n = undefined
    u = null
    // undefined 和 null 是所有类型的子类型 所以可以赋值给number类型的变量
    let num: number = 123
    num = undefined
    num = null

any 类型

any 表示队变量没有任何显示,编译器失去了对 TS 的检测功能与 JS 无异(不建议使用)。

let notSure: any = 4
// any类型可以随意赋值
notSure = `任意模板字符串`
notSure = true
notSure = null

// 当 notSure 为any 类型时,在any类型上访问任何属性和调用方法都是允许的, 很有可能出现错误
notSure.name // 现在调用name属性是允许的,但很明显我们定义的notSure没有name这个属性,下面的调用sayName方法也是如此
notSure.sayName()

array 类型

// 数组类型,可以指定数组的类型和使用数组的方法和属性
let arrOfNumbers: number[] = [1, 2, 3]
console.log(arrOfNumbers.length);

arrOfNumbers.push(4)

tuple 元组类型

// 元组类型  元组就是固定长度,类型的数组  
// 类型和长度必须一致
let u: [string, number] = ['12', 12]
// let U: [string, number] = ['12', 12, true]   报错信息为:不能将类型“[string, number, boolean]”分配给类型“[string, number]”。源具有 3 个元素,但目标仅允许 2 个。

// 也可以使用数组的方法,如下所示push一个值给元组u
u.push(33)

Interface 接口

  • 对对象的形状(shape)进行描述

  • Duck Typing(鸭子类型)

interface Person {
   // readonly id 表示只读属性的id不可以修改
   readonly id: number;
   name: string;
   age: number;
   // weight? 表示可选属性,可以选用也可以不选用
   weight?: number
}

let host: Person = {
   id: 1,
   name: 'host',
   age: 20,
   weight: 70
}

//host.id = 2 报错 提示信息为:无法分配到 "id" ,因为它是只读属性

function 函数类型

// 方式一:函数声明的写法   z 为可选参数 ,
function add1 (x: number, y: number, z?: number): number {
   if (typeof z === 'number') {
       return x + y + z
  } else {
       return x + y
  }
}
// 需要注意的是:可选参数必须置于所有必选参数之后,否则会报错

add1(1, 2, 3)

// 方式二:函数表达式
const add2 = (x: number, y: number, z?: number): number => {
   if (typeof z === 'number') {
       return x + y + z
  } else {
       return x + y
  }
}


// 使用interface接口 描述函数类型
interface ISum {
  (x: number, y: number, z?: number): number
}
let add3: ISum = add1

值的注意的是:可选参数必须置于所有必选参数之后,否则会报错,如下图展示的错误案例所示:·

image-20211120152558241

类型推论

当定义变量时没有指定类型,编译器会自动推论第一次赋的值为默认类型

let s = 'str'
// s = 12 本句将会报错,提示为:不能将类型“number”分配给类型“string”

联合类型

使用| 分隔可选类型

let StringOrNumber: string | number
StringOrNumber = 123
StringOrNumber = '111'

类型断言

使用 as 关键字进行类型断言

function getLength (rod: number | string): number {
   const str = rod as string
   //这里我们可以用 as 关键字,告诉typescript 编译器,你没法判断我的代码,但是我本人很清楚,这里我就把它看作是一个 string,你可以给他用 string 的方法。
   if (str.length) {
       return str.length
  } else {
       const num = rod as number
       return num.toString().length
  }
}

类型守卫

// 4.类型守卫 type guard     typescript 在不同的条件分支里面,智能的缩小了范围
function getLength2 (rod: number | string): number {
   if (typeof rod === 'string') {
       return rod.length
  } else {
       // else 里面的rod 会自动默认为number类型
       return rod.toString().length
       
  }
}

Class 类

面向对象编程的三大特点:

  • 封装(Encapsulation):将对数据的操作细节隐藏起来,只暴露对外的接口。外界调用端不需要(也不可能)知道细节,就能通过对外提供的接口来访问该对象,

  • 继承(Inheritance):子类继承父类,子类除了拥有父类的所有特性外,还有一些更具体的特性。

  • 多态(Polymorphism):由继承而产生了相关的不同的类,对同一个方法可以有不同的响应。

话不多少,看代码:

class Animal {
   readonly name: string
   constructor(name: string) {
       this.name = name
       console.log(this.run())
  }
   // private run ():私有的   protected run () 受保护的
   run () {
       return `${this.name} is running`
  }

}
const animal = new Animal('elephant')
// console.log(animal.name)
animal.run() //elephant is running

// 继承
class Dog extends Animal {
   age: number
   constructor(name, age) {
       super(name)
       console.log(this.name)
       this.age = age
  }
   bark () {
       console.log(`这只在叫的狗狗叫${this.name},它今年${this.age}岁了`)
  }
}
const dog = new Dog('旺财', 5)
dog.run() // 旺财 is running
dog.bark() // 这只在叫的狗狗叫旺财,它今年5岁了


// 多态
class Cat extends Animal {
   static catAge = 2

   constructor(name) {
       super(name)
       console.log(this.name) // 布丁
  }
   run () {
       return 'Meow,' + super.run()
  }
}
const cat = new Cat('布丁')
console.log(cat.run()) // Meow,布丁 is running
console.log(Cat.catAge) // 2

class中还提供了readonly关键字,readonly为只读属性,在调用的时候不能修改。如下所示:

image-20211124155826105

类成员修饰符

  • public修饰的属性或方法是公有的,可以在任何地方被访问到,默认所有的属性和方法都是public的。

  • private修饰的属性或方法是私有的,不能在声明它的类的外部访问。

    上述示例代码中,在父类Animalrun 方法身上加上private修饰符之后就会产生如下图的报错信息:

    image-20211124154129377

  • protected 修饰的属性或方法是受保护的,它和private类似,区别在于它在子类中也是可以访问的。

上述示例代码中,在父类Animalrun 方法身上加上protected修饰符之后就会产生如下图的报错信息:

image-20211124153823828

接口和类

类可以使用 implements来实现接口。

// interface可以用来抽象验证类的方法和方法
interface Person {
   Speak (trigger: boolean): void;
}
interface Teenagers {
   Young (sge: number): void
}
// 接口之间的继承
interface PersonAndTeenagers extends Teenagers {
   Speak (trigger: boolean): void;
}

// implements 实现接口
class Boy implements Person {
   Speak (mouth: boolean) { }
}

// class Girl implements Person, Teenagers 和 class Girl implements PersonAndTeenagers 作用相同
class Girl implements PersonAndTeenagers {
   Speak (mouth: boolean) { }
   Young (sge: number) { }
}

enum枚举

枚举成员会被赋值为从 0 开始递增的数字,同时也会对枚举值到枚举名进行反向映射:

enum Color {
   red = 'red',
   blue = 'blue',
   yellow = 'yellow',
   green = 'green'
}
// 常量枚举
const enum Color {
   red = 'red',
   blue = 'blue',
   yellow = 'yellow',
   green = 'green'
}
console.log(Color.red) // 0
// 反向映射
console.log(Color[0]) // red

const value = 'red'
if (value === Color.red) {
   console.log('Go Go Go ')
  • 常量枚举经过编译后形成的js文件如下:

image-20211124182628644

  • 非常量枚举经过编译器编译之后的js文件如下:

image-20211124182633413

Generics 泛型

泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。

  • 约束泛型

  • 类与泛型

  • 接口与泛

示例代码如下:

function echo (arg) {
   return arg
}

let result1 = echo(123) // 参数传递123后result1 的类型为any

// 泛型
function echo2<T> (arg: T): T {
   return arg
}
let result2 = echo2(123)  // 加上泛型之后 参数传递 123后result2的类型为number

function swap<T, U> (tuple: [T, U]): [U, T] {
   return [tuple[1], tuple[0]]
}
console.log(swap(['hero', 123]))//[ 123, 'hero' ]


// 约束泛型
interface IWithLength {
   length: number
}

function echoWithLength<T extends IWithLength> (arg: T): T {
   console.log(arg.length)
   return arg
}

const str = echoWithLength('123')
const obj = echoWithLength({ length: 3, name: 'Tom' })
const arr = echoWithLength([1, 2, 3, 4])


// 类与泛型
class Queue<T> {
   private data = []
   push (item: T) {
       return this.data.push(item)
  }
   pop (): T {
       return this.data.shift()
  }
}
const queue = new Queue<number>()

queue.push(1)
console.log(queue.pop().toFixed())// 1


// 接口与泛型
interface KeyPair<T, U> {
   key: T
   value: U
}
let kp1: KeyPair<string, number> = { key: 'str', value: 123 }
let kp2: KeyPair<number, string> = { key: 123, value: 'str' }
let arr2: Array<string> = ['1', '2'] // 使用 Array<string> 等价于 interface Array<T>

类型别名 type-alias

类型别名,就是给类型起一个别名,让它可以更方便的被重用。

let sum: (x: number, y: string) => number
const result1 = sum(1, '2')
// 将(x: number, y: string) => number类型取一个别名 为 PlusType
type PlusType = (x: number, y: string) => number
let sum2: PlusType
const result2 = sum2(2, '2')

type StrOrNum = string | number
let result3: StrOrNum = 123
result3 = '123'

字面量

let Name: 'name' = 'name'
// Name = '123' //报错信息:不能将类型“"123"”分配给类型“"name"”
let age: 19 = 19

type Directions = 'Up' | 'Down' | 'Left' | 'Right'
let up: Directions = 'Up'

交叉类型

// 交叉类型  使用 ‘&’ 符号进行类型的扩展
interface IName {
   name: string
}

type IPerson = IName & { age: number }
let person: IPerson = { name: 'Tom', age: 19 }

内置类型

  • 全局对象

// global objects 全局对象
const a: Array<string> = ['123', '456']
const time = new Date()
time.getTime()
const reg = /abc/ // 此时reg为RegExp类型
reg.test('abc')
  • build-in object 内置对象

    Math.pow(2, 2) //返回 2 的 2次幂。
    console.log(Math.pow(2, 2)) // 4
  • DOM and BOM

    // document 对象,返回的是一个 HTMLElement
    let body: HTMLElement = document.body
    // document 上面的query 方法,返回的是一个 nodeList 类型
    let allLis = document.querySelectorAll('li')

    //当然添加事件也是很重要的一部分,document 上面有 addEventListener 方法,注意这个回调函数,因为类型推断,这里面的 e 事件对象也自动获得了类型,这里是个 mouseEvent 类型,因为点击是一个鼠标事件,现在我们可以方便的使用 e 上面的方法和属性。
    document.addEventListener('click', (e) => {
       e.preventDefault()
    })
  • utility types实用类型

    interface IPerson2 {
       name: string
       age: number
    }
    let viking: IPerson2 = { name: 'viking', age: 20 }
    // partial,它可以把传入的类型都变成可选
    type IPartial = Partial<IPerson>
    let viking2: IPartial = {} // Partial 将IPerson 中的类型变成了可选类型 所以 viking2 可以等于一个空对象

    // Omit,它返回的类型可以忽略传入类型的某个属性
    type IOmit = Omit<IPerson2, 'name'> // 忽略name属性
    let viking3: IOmit = { age: 20 }

如果加上name属性将会报错:不能将类型“{ age: number; name: string; }”分配给类型“IOmit”。对象文字可以只指定已知属性,并且“name”不在类型“IOmit”中。


作者:不一213
来源:https://juejin.cn/post/7035563509882552334

收起阅读 »

神奇的交叉观察器 - IntersectionObserver

1. 背景网页开发时,不管是在移动端,还是PC端,都有个很重要的概念,叫做动态懒加载,适用于一些图片资源(或者数据)特别多的场景中,这个时候,我们常常需要了解某个元素是否进入了“视口”(viewport),即用户能不能看到它。 传统的实现方法是,监听到scro...
继续阅读 »

1. 背景

网页开发时,不管是在移动端,还是PC端,都有个很重要的概念,叫做动态懒加载,适用于一些图片资源(或者数据)特别多的场景中,这个时候,我们常常需要了解某个元素是否进入了“视口”(viewport),即用户能不能看到它。


传统的实现方法是,监听到scroll事件或者使用setInterval来判断,调用目标元素的getBoundingClientRect()方法,得到它对应于视口左上角的坐标,再判断是否在视口之内。这种方法的缺点是,由于scroll事件触发频率高,计算量很大,如果不做防抖节流的话,很容易造成性能问题,而setInterval由于其有间歇期,也会出现体验问题。


所以在几年前,Chrome率先提供了一个新的API,就是IntersectionObserver,它可以用来自动监听元素是否进入了设备的可视区域之内,而不需要频繁的计算来做这个判断。由于可见(visible)的本质是,目标元素与视口产生一个交叉区,所以这个 API 叫做”交叉观察器”。


2. 兼容性

由于这个api问世已经很多年了,所以对浏览器的支持性还是不错的,完全可以上生产环境,点击这里可以看看当前浏览器对于IntersectionObserver的支持性:


111111.png

3. 用法

API的调用非常简单:


const io = new IntersectionObserver(callback, option);

上面代码中,IntersectionObserver 是浏览器原生提供的构造函数,接受两个参数:



  • callback:可见性发现变化时的回调函数
  • option:配置对象(可选)。

构造函数的返回值是一个观察器实例。实例一共有4个方法:



  • observe:开始监听特定元素
  • unobserve:停止监听特定元素
  • disconnect:关闭监听工作
  • takeRecords:返回所有观察目标的对象数组

3.1 observe

该方法需要接收一个target参数,值是Element类型,用来指定被监听的目标元素


// 获取元素
const target = document.getElementById("dom");

// 开始观察
io.observe(target);

3.2 unobserve

该方法需要接收一个target参数,值是Element类型,用来指定停止监听的目标元素


// 获取元素
const target = document.getElementById("dom");

// 停止观察
io.unobserve(target);

3.3 disconnect

该方法不需要接收参数,用来关闭观察器


// 关闭观察器
io.disconnect();

3.4 takeRecords

该方法不需要接收参数,返回所有被观察的对象,返回值是一个数组


// 获取被观察元素
const observerList = io.takeRecords();

注意:

observe方法的参数是一个 DOM 节点,如果需要观察多个节点,就要多次调用这个方法:


// 开始观察多个元素
io.observe(domA);
io.observe(domB);
io.observe(domC);

4. callback 参数

目标元素的可见性变化时,就会调用观察器的回调函数callback


callback一般会触发两次。一次是目标元素刚刚进入视口,另一次是完全离开视口。


const io = new IntersectionObserver((changes, observer) => {
console.log(changes);
console.log(observer);
});

上面代码中,callback函数的参数接收两个参数changesobserver



  • changes:这是一个数组,每个成员都是一个被观察对象。举例来说,如果同时有两个被观察的对象的可见性发生变化,那么changes数组里面就会打印出两个元素,如果只观察一个元素,我们打印changes[0]就能获取到被观察对象
  • observer: 这是一个对象,返回我们在实例中传入的第二个参数option(如果没传,则返回默认值)

5. IntersectionObserverEntry 对象

上面提到的changes数组中的每一项都是一个IntersectionObserverEntry 对象(下文简称io对象),对象提供目标元素的信息,一共有八个属性,我们打印这个对象:


// 创建实例
const io = new IntersectionObserver(changes => {
changes.forEach(change => {
console.log(change);
});
});

// 获取元素
const target = document.getElementById("dom");

// 开始监听
io.observe(target);

运行上面代码,并且改变dom的可见性,这时控制台可以看到一个对象:


555.png

每个属性的含义如下:



  • boundingClientRect:目标元素的矩形区域的信息
  • intersectionRatio:目标元素的可见比例,即intersectionRectboundingClientRect的比例,完全可见时为1,完全不可见时小于等于0
  • intersectionRect:目标元素与视口(或根元素)的交叉区域的信息
  • isIntersecting: 布尔值,目标元素与交集观察者的根节点是否相交
  • isVisible: 布尔值,目标元素是否可见(该属性还在试验阶段,不建议在生产环境中使用)
  • rootBounds:根元素的矩形区域的信息,getBoundingClientRect()方法的返回值,如果没有根元素(即直接相对于视口滚动),则返回null
  • target:被观察的目标元素,是一个 DOM 节点对象
  • time:可见性发生变化的时间,是一个高精度时间戳,单位为毫秒

6. 应用


  1. 预加载(滚动加载,翻页加载,无限加载)
  2. 懒加载(后加载、惰性加载)
  3. 其它

7. 注意点

IntersectionObserver API 是异步的,不随着目标元素的滚动同步触发。


规格写明,IntersectionObserver的实现,应该采用requestIdleCallback(),即只有线程空闲下来,才会执行观察器。这意味着,这个观察器的优先级非常低,只在其他任务执行完,浏览器有了空闲才会执行。


8. 参考链接


作者:三年没洗澡
来源:https://juejin.cn/post/7035490578015977480

收起阅读 »

美团外卖iOS多端复用的推动、支撑与思考

iOS
前言美团外卖2013年11月开始起步,随后高速发展,不断刷新多项行业记录。截止至2018年5月19日,日订单量峰值已超过2000万,是全球规模最大的外卖平台。业务的快速发展对技术支撑提出了更高的要求:为线上用户提供高稳定的服务体验,保障全链路业务和系统高可用运...
继续阅读 »



前言

美团外卖2013年11月开始起步,随后高速发展,不断刷新多项行业记录。截止至2018年5月19日,日订单量峰值已超过2000万,是全球规模最大的外卖平台。业务的快速发展对技术支撑提出了更高的要求:为线上用户提供高稳定的服务体验,保障全链路业务和系统高可用运行的同时,要提升多入口业务的研发速度,推进App系统架构的合理演化,进一步提升跨部门跨地域团队之间的协作效率。

而另一方面随着用户数与订单数的高速增长,美团外卖逐渐有了流量平台的特征,兄弟业务纷纷尝试接入美团外卖进行推广和发布,期望提供统一标准化服务平台。因此,基础能力标准化,推进多端复用,同时输出成熟稳定的技术服务平台,一直是我们技术团队追求的核心目标。

多端复用的端

这里的“端”有两层意思:

  • 其一是相同业务的多入口

美团外卖在iOS下的业务入口有三个,『美团外卖』App、『美团』App的外卖频道、『大众点评』App的外卖频道。

值得一提的是:由于用户画像与产品策略差异,『大众点评』外卖频道与『美团』外卖频道和『美团外卖』虽经历技术栈融合,但业务形态区别较大,暂不考虑上层业务的复用,故这篇文章主要介绍美团系两大入口的复用。

在2015年外卖C端合并之前,美团系的两大入口由两个不同的团队研发,虽然用户感知的交互界面几乎相同,但功能实现层面的代码风格和技术栈都存在较大差异,同一需求需要在两端重复开发显然不合理。所以,我们的目标是相同功能,只需要写一次代码,做一次估时,其他端只需做少量的适配工作。

  • 其二是指平台上各个业务线

外卖不同兄弟业务线都依赖外卖基础业务,包括但不限于:地图定位、登录绑定、网络通道、异常处理、工具UI等。考虑到标准化的范畴,这些基础能力也是需要多端复用的。

img

图1 美团外卖的多端复用的目标

关于组件化

提到多端复用,不免与组件化产生联系,可以说组件化是多端复用的必要条件之一。大多数公司口中的“组件化”仅仅做到代码分库,使用Cocoapods的Podfile来管理,再在主工程把各个子库的版本号聚合起来。但是能设计一套合理的分层架构,理清依赖关系,并有一整套工具链支撑组件发版与集成的相对较少。否则组件化只会导致包体积增大,开发效率变慢,依赖关系复杂等副作用。

整体思路

A. 多端复用概念图

img

图2 多端复用概念图

多端复用的目标形态其实很好理解,就是将原有主工程中的代码抽出独立组件(Pods),然后各自工程使用Podfile依赖所需的独立组件,独立组件再通过podspec间接依赖其他独立组件。

B. 准备工作

确认多端所依赖的基层库是一致的,这里的基层库包括开源库与公司内的技术栈。

iOS中常用开源库(网络、图片、布局)每个功能基本都有一个库业界垄断,这一点是iOS相对于Android的优势。公司内也存在一些对开源库二次开发或自行研发的基础库,即技术栈。不同的大组之间技术栈可能存在一定差异。如需要复用的端之间存在差异,则需要重构使得技术栈统一。(这里建议重构,不建议适配,因为如果做的不够彻底,后续很大可能需要填坑。)

就美团而言,美团平台与点评平台作为公司两大App,历史积淀厚重。自2015年底合并以来,为了共建和沉淀公共服务,减少重复造轮子,提升研发效率,对上层业务方提供统一标准的高稳定基础能力,两大平台的底层技术栈也在不断融合。而美团外卖作为较早实践独立App,同时也是依托于两大平台App的大业务方,在外卖C端合并后的1年内,我们也做了大量底层技术栈统一的必要工作。

C. 方案选型

在演进式设计与计划式设计中的抉择。

演进式设计指随着系统的开发而做设计变更,而计划式设计是指在开发之前完全指定系统架构的设计。演进的设计,同样需要遵循架构设计的基本准则,它与计划的设计唯一的区别是设计的目标。演进的设计提倡满足客户现有的需求;而计划的设计则需要考虑未来的功能扩展。演进的设计推崇尽快地实现,追求快速确定解决方案,快速编码以及快速实现;而计划的设计则需要考虑计划的周密性,架构的完整性并保证开发过程的有条不紊。

美团外卖iOS客户端,在多端复用的立项初期面临着多个关键点:频道入口与独立应用的复用,外卖平台的搭建,兄弟业务的接入,点评外卖的协作,以及架构迁移不影响现有业务的开发等等,因此权衡后我们使用“演进式架构为主,计划式架构为辅”的设计方案。不强求历史代码一下达到终极完美架构,而是循序渐进一步一个脚印,满足现有需求的同时并保留一定的扩展性。

演进式架构推动复用

术语解释

  • Waimai:特指『美团外卖』App,泛指那些独立App形式的业务入口,一般为project。

  • Channel:特指『美团』App中的外卖频道,泛指那些以频道或者Tab形式集成在主App内的业务入口,一般为Pods。

  • Special:指将Waimai中的业务代码与原有工程分离出来,让业务代码成为一个Pods的形态。

  • 下沉:即下沉到下层,这里的“下层”指架构的基层,一般为平台层或通用层。“下沉”指将不同上层库中的代码统一并移动到下层的基层库中。

在这里先贴出动态的架构演进过程,让大家有一个宏观的概念,后续再对不同节点的经历做进一步描述。

图3 演进式架构动态图

原始复用架构

如图4所示,在过去一两年,因为技术栈等原因我们只能采用比较保守的代码复用方案。将独立业务或工具类代码沉淀为一个个“Kit”,也就是粒度较小的组件。此时分层的概念还比较模糊,并且以往的工程因历史包袱导致耦合严重、逻辑复杂,在将UGC业务剥离后发现其他的业务代码无法轻易的抽出。(此时的代码复用率只有2.4%。)

鉴于之前的准备工作已经完成,多端基础库已经一致,于是我们不再采取保守策略,丰富了一些组件化通信、解耦与过渡的手段,在分层架构上开始发力。

img

图4 原始复用架构

业务复用探索

在技术栈已统一,基础层已对齐的背景下,我们挑选外卖核心业务之一的Store(即商家容器)开始了在业务复用上的探索。如图5所示,大致可以理解为“二合一,一分三”的思路,我们从代码风格和开发思路上对两边的Store业务进行对齐,在此过程中顺势将业务类与技术(功能)类的代码分离,一些通用Domain也随之分离。随着一个个组件的拆分,我们的整体复用度有明显提升,但开发效率却意外的受到了影响。多库开发在版本的发布与集成中增加了很多人工操作:依赖冲突、lock文件冲突等问题都阻碍了我们的开发效率进一步提升,而这就是之前“关于组件化”中提到的副作用。

于是我们将自动发版与自动集成提上了日程。自动集成是将“组件开发完毕到功能合入工程主体打出测试包”之间的一系列操作自动化完成。在这之前必须完成一些前期铺垫工作——壳工程分离。img

图5 商家容器下沉时期

壳工程分离

如图6所示,壳工程顾名思义就是将原来的project中的代码全部拆出去,得到一个空壳,仅仅保留一些工程配置选项和依赖库管理文件。

为什么说壳工程是自动集成的必要条件之一?

因为自动集成涉及版本号自增,需要机器修改工程配置类文件。如果在创建二进制的过程中有新业务PR合入,会造成commit树分叉大概率产生冲突导致集成失败。抽出壳工程之后,我们的壳只关心配置选项修改(很少),与依赖版本号的变化。业务代码的正常PR流程转移到了各自的业务组件git中,以此来杜绝人工与机器的冲突。

img

图6 壳工程分离

壳工程分离的意义主要有如下几点:

  • 让职能更加明确,之前的综合层身兼数职过于繁重。

  • 为自动集成铺路,避免业务PR与机器冲突。

  • 提升效率,后续Pods往Pods移动代码比proj往Pods移动代码更快。

  • 『美团外卖』向『美团』开发环境靠齐,降低适配成本。

img

图7 壳工程分离阶段图

图7的第一张图到第二张图就是上文提到的壳工程分离,将“Waimai”所有的业务代码打包抽出,移动到过渡仓库Special,让原先的“Waimai”成为壳。

第二张图到第三张图是Pods库的内部消化。

前一阶段相当于简单粗暴的物理代码移动,后一阶段是对Pods内整块代码的梳理与分库。

内部消化对齐

在前文“多端复用概念图”的部分我们提到过,所谓的复用是让多端的project以Pods的方式接入统一的代码。我们兼容考虑保留一端代码完整性,降低回接成本,决定分Subpods使用阶段性合入达到平滑迁移。

img

图8 代码下沉方案

图8描述了多端相同模块内的代码具体是如何统一的。此时因为已经完成了壳工程分离,所以业务代码都在“Special”这样的过渡仓库中。

“Special”和“Channel”两端的模块统一大致可分为三步:平移 → 下沉 → 回接。(前提是此模块的业务上已经确定是完全一致。)

平移阶段是保留其中一端“Special”代码的完整性,以自上而下的平移方式将代码文件拷贝到另一端“Channel”中。此时前者不受任何影响,后者的代码因为新文件拷贝和原有代码存在重复。此时将旧文件重命名,并深度优先遍历新文件的依赖关系补齐文件,最终使得编译通过。然后将旧文件中的部分差异代码加到新文件中做好一定的差异化管理,最后删除旧文件。

下沉阶段是将“Channel”处理后的代码解耦并独立出来,移动到下层的Pods或下层的SubPods。此时这里的代码是既支持“Special”也支持“Channel”的。

回接阶段是让“Special”以Pods依赖的形式引用之前下沉的模块,引用后删除平移前的代码文件。(如果是在版本的间隙完成固然最好,否则需要考虑平移前的代码文件在这段时间的diff。)

实际操作中很难在有限时间内处理完一个完整的模块(例如订单模块)下沉到Pods再回接。于是选择将大模块分成一个个子模块,这些子模块平滑的下沉到SubPods,然后“Special”也只引用这个统一后的SubPods,待一个模块完全下沉完毕再拆出独立的Pods。

再总结下大量代码下沉时如何保证风险可控:

  • 联合PM,先进行业务梳理,特殊差异要标注出来。

  • 使用OClint的提前扫描依赖,做到心中有数,精准估时。

  • 以“Special”的代码风格为基准,“Channel”在对齐时仅做加法不做减法。

  • “Channel”对齐工作不影响“Special”,并且回接时工作量很小。

  • 分迭代包,QA资源提前协调。

中间件层级压平

经过前面的“内部消化”,Channel和Special中的过渡代码逐渐被分发到合适的组件,如图9所示,Special只剩下AppOnly,Channel也只剩下ChannelOnly。于是Special消亡,Channel变成打包工程。

AppOnly和ChannelOnly 与其他业务组件层级压平。上层只留下两个打包工程。

img

图9 中间件层级压平

平台层建设

如图10所示,下层是外卖基础库,WaimaiKit包含众多细分后的平台能力,Domain为通用模型,XunfeiKit为对智能语音二次开发,CTKit为对CoreText渲染框架的二次开发。

针对平台适配层而言,在差异化收敛与依赖关系梳理方面发挥重要角色,这两点在下问的“衍生问题解决中”会有详细解释。

外卖基础库加上平台适配层,整体构成了我们的外卖平台层(这是逻辑结构不是物理结构),提供了60余项通用能力,支持无差异调用。

img

图10 外卖平台层的建设

多端通用架构

此时我们把基层组件与开源组件梳理并补充上,达到多端通用架构,到这里可以说真正达到了多端复用的目标。

img

图11 多端通用架构完成

由上层不同的打包工程来控制实际需要的组件。除去两个打包工程和两个Only组件,下面的组件都已达到多端复用。对比下“Waimai”与“Channel”的业务架构图中两个黑色圆圈的部分。

img

图12 “Waimai”的业务架构

img

图13 “Channel”的业务架构

衍生问题解决

差异问题

A.需求本身的差异

三种解决策略:

  • 对于文案、数值、等一两行代码的差异我们使用 运行时宏(动态获取proj-identifier)或预编译宏(custome define)直接在方法中进行if else判断。

  • 对于方法实现的不同 使用Glue(胶水层),protocol提供相同的方法声明,用来给外部调用,在不同的载体中写不同的方法实现。

  • 对于较大差异例如两边WebView容器不一样,我们建多个文件采用文件级预编译,可预编译常规.m文件或者Category。(例如WMWebViewManeger_wm.m&WMWebViewManeger_mt.m、UITableView+WMEstimated.m&UITableView+MTEstimated.m)

进一步优化策略:

用上述三种策略虽然完成差异化管理,但差异代码散落在不同组件内难以收敛,不便于管理。有了平台适配层之后,我们将差异化判断收敛到适配层内部,对上层提供无差异调用。组件开发者在开发中不用考虑宿主差异,直接调用用通用接口。差异的判断或者后续优化在接口内部处理外部不感知。

图14给出了一个平台适配层提供通用接口修改后的例子。

img

图14 平台适配层接口示例

B.多端节奏差异

实际场景中除了需求的差异还有可能出现多端进版节奏的差异,这类差异问题我们使用分支管理模型解决。

前提条件既然要多端复用了,那需求的大方向还是会希望多端统一。一般较多的场景是:多端中A端功能最少,B端功能基本算是是A端的超集。(没有绝对的超集,A端也会有较少的差异点。)在外卖的业务中,“Channel”就是这个功能较少的一端,“Waimai”基本是“Channel”的超集。

两端的差异大致分为了这5大类9小类:

  1. 需求两端相同(1.1、提测上线时间基本相同;1.2、“Waimai”比“Channel”早3天提测 ;1.3、“Waimai”比“Channel”晚3天提测)。

  2. 需求“Waimai”先进版,“Channel”下一版进 (2.1、频道下一版就上;2.2、频道下两版本后再上)。

  3. 需求“Waimai”先进版,“Channel”不需要。

  4. 需求“Channel”先进版,“Waimai”下一版进(4.1、需要改动通用部分;4.2、只改动“ChannelOnly”的部分)。

  5. 需求“Channel”先进版,“Waimai”不需要(只改动“ChannelOnly”的部分)。

img

图15 最复杂场景下的分支模型

也不用过多纠结,图15是最复杂的场景,实际场合中很难遇到,目前的我们的业务只遇到1和2两个大类,最多2条线。

编译问题

以往的开发方式初次全量编译5分钟左右,之后就是差量编译很快。但是抽成组件后,随着部分子库版本的切换间接的增加了pod install的次数,此时高频率的3分钟、5分钟会让人难以接受。

于是在这个节点我们采用了全二进制依赖的方式,目标是在日常开发中直接引用编译后的产物减少编译时间。

img

图16 使用二进制的依赖方式

如图所示三个.a就是三个subPods,分了三种Configuration:

  1. debug/ 下是 deubg 设置编译的 x64 armv7 arm64。

  2. release/ 下是 release 设置编译的 armv7 arm64。

  3. dailybuild/ 下是 release + TEST=1编译的 armv7 arm64。

  4. 默认(在文件夹外的.a)是 debug x64 + release armv7 + release arm64。

这里有一个问题需要解决,即引用二进制带来的弊端,显而易见的就是将编译期的问题带到了运行期。某个宏修改了,但是编译完的二进制代码不感知这种改动,并且依赖版本不匹配的话,原本的方法缺失编译错误,就会带到运行期发生崩溃。解决此类问题的方法也很简单,就是在所有的打包工程中都配置了打包自动切换源码。二进制仅仅用来在开发中获得更高的效率,一旦打提测包或者发布包都会使用全源码重新编译一遍。关于切源码与切二进制是由环境变量控制拉取不同的podspec源。

并且在开发中我们支持源码与二进制的混合开发模式,我们给某个binary_pod修饰的依赖库加上标签,或者使用.patch文件,控制特定的库拉源码。一般情况下,开发者将与自己当前需求相关联的库拉源码便于Debug,不关联的库拉二进制跳过编译。

依赖问题

如图17所示,外卖有多个业务组件,公司也有很多基础Kit,不同业务组件或多或少会依赖几个Kit,所以极易形成网状依赖的局面。而且依赖的版本号可能不一致,易出现依赖冲突,一旦遇到依赖冲突需要对某一组件进行修改再重新发版来解决,很影响效率。解决方式是使用平台适配层来统一维护一套依赖库版本号,上层业务组件仅仅关心平台适配层的版本。

img

图17 平台适配层统一维护依赖

当然为了避免引入平台适配层而增加过多无用依赖的问题,我们将一些依赖较多且使用频度不高的Kit抽出subPods,支持可选的方式引入,例如IM组件。

再者就是pod install 时依赖分析慢的问题。对于壳工程而言,这是所有依赖库汇聚的地方,依赖关系写法若不科学极易在analyzing dependency中耗费大量时间。Cocoapods的依赖分析用的是Molinillo算法,链接中介绍了这个算法的实现方式,是一个具有前向检察的回溯算法。这个算法本身是没有问题的,依赖层级深只要依赖写的合理也可以达到秒开。但是如果对依赖树叶子节点的版本号控制不够严密,或中间出现了循环依赖的情况,会导致回溯算法重复执行了很多压栈和出栈操作耗费时间。美团针对此类问题的做法是维护一套“去依赖的podspec源”,这个源中的dependency节点被清空了(下图中间)。实际的所需依赖的全集在壳工程Podfile里平铺,统一维护。这么做的好处是将之前的树状依赖(下图左)压平成一层(下图右)。

img

图18 依赖数的压平

效率问题

前面我们提到了自动集成,这里展示下具体的使用方式。美团发布工程组自行研发了一套HyperLoop发版集成平台。当某个组件在创建二进制之前可自行选择集成的目标,如果多端复用了,那只需要在发版创建二进制的同时勾选多个集成的目标。发版后会自行进行一系列检查与测试,最终将代码合入主工程(修改对应壳工程的依赖版本号)。

img

图19 HyperLoop自动发版自动集成

img

图20 主工程commit message的变化

以上是“Waimai”的commit对比图。第一张图是以往的开发方式,能看出工程配置的commit与业务的commit交错堆砌。第二张图是进行壳工程分离后的commit,能看出每条message都是改了某个依赖库的版本号。第三张图是使用自动集成后的commit,能看出每条message都是画风统一且机器串行提交的。

这里又衍生出另一个问题,当我们用壳工程引Pods的方式替代了project集中式开发之后,我们的代码修改散落到了不同的组件库内。想看下主工程6.5.0版本和6.4.0版本的diff时只能看到所有依赖库版本号的diff,想看commit和code diff时必须挨个去组件库查看,在三轮提测期间这样类似的操作每天都会重复多次,很不效率。

于是我们开发了atomic diff的工具,主要原理是调git stash的接口得到版本号diff,再通过版本号和对应的仓库地址深度遍历commit,再深度遍历commit对应的文件,最后汇总,得到整体的代码diff。

img

图21 atomic diff汇总后的commit message

整套工具链对多端复用的支撑

上文中已经提到了一些自动化工具,这里整理下我们工具链的全景图。

img

图22 整套工具链

  1. 在准备阶段,我们会用OClint工具对compile_command.json文件进行处理,对将要修改的组件提前扫描依赖。

  2. 在依赖库拉取时,我们有binary_pod.rb脚本里通过对源的控制达到二进制与去依赖的效果,美团发布工程组维护了一套ios-re-sankuai.com的源用于存储remove dependency的podspec.json文件。

  3. 在依赖同步时,会通过sync_podfile定时同步主工程最新Podfile文件,来对依赖库全集的版本号进行维护。

  4. 在开发阶段,我们使用Podfile.patch工具一键对二进制/源码、远端/本地代码进行切换。

  5. 在引用本地代码开发时,子库的版本号我们不太关心,只关心主工程的版本号,我们使用beforePod和AfterPod脚本进行依赖过滤以防止依赖冲突。

  6. 在代码提交时,我们使用git squash对多条相同message的commit进行挤压。

  7. 在创建PR时,以往需要一些网页端手动操作,填写大量Reviewers,现在我们使用MTPR工具一键完成,或者根据个人喜好使用Chrome插件。

  8. 在功能合入master之前,会有一些jenkins的job进行检测。

  9. 在发版阶段,使用Hyperloop系统,一键发版操作简便。

  10. 在发版之后,可选择自动集成和联合集成的方式来打包,打包产物会自动上传到美团的“抢鲜”内测平台。

  11. 在问题跟踪时,如果需要查看主工程各个版本号间的commit message和code diff,我们有atomic diff工具深度遍历各个仓库并汇总结果。

感想总结

  • 多端复用之后对PM-RD-QA都有较大的变化,我们代码复用率由最初的2.4%*达到了*84.1%,让更多的PM投入到了新需求的吞吐中,但研发效率提升增大了QA的工作量。一个大的尝试需要RD不断与PM和QA保持沟通,选择三方都能接受的最优方案。

  • 分清主次关系,技术架构等最终是为了支撑业务,如果一个架构设计的美如画天衣无缝,但是落实到自己的业务中确不能发挥理想效果,或引来抱怨一片,那这就是个失败的设计。并且在实际开发中技术类代码修改尽量选择版本间隙合入,如果与业务开发的同学产生冲突时,都要给业务同学让路,不能影响原本的版本迭代速度。

  • 时刻对 “不合理” 和 “重复劳动”保持敏感。新增一个埋点常量要去改一下平台再发个版是否成本太大?一处订单状态的需求为什么要修改首页的Kit?实际开发中遇到别扭的地方多增加一些思考而不是硬着头皮过去,并且手动重复两次以上的操作就要思考有没有自动化的替代方案。

  • 一旦决定要做,在一些关键节点决不能手软。例如某个节点为了不Block别人,加班不可避免。在大量代码改动时也不用过于紧张,有提前预估,有Case自测,还有QA的三轮回归来保障,保持专注,放手去做就好。

作者简介

尚先,美团资深工程师。2015年加入美团,目前作为美团外卖iOS端平台化虚拟小组组长,主要负责业务架构、持续集成和工程化相关工作,致力于提升研发效率与协作效率。


作者:美团技术团队
来源:https://juejin.cn/post/6844903629753679886

收起阅读 »

女儿拿着小天才电话手表问我App启动流程(下)

接 女儿拿着小天才电话手表问我App启动流程(上) 第四关:ActivityThread闪亮登场刚才说到由Zygote进行fork进程,并返回新进程的pid。其实这过程中也实例化ActivityThread对象。一起看看是怎么实现的: //RuntimeIni...
继续阅读 »

女儿拿着小天才电话手表问我App启动流程(上)


第四关:ActivityThread闪亮登场

刚才说到由Zygote进行fork进程,并返回新进程的pid。其实这过程中也实例化ActivityThread对象。一起看看是怎么实现的:


//RuntimeInit.java
protected static Runnable findStaticMain(String className, String[] argv,
ClassLoader classLoader) {
Class<?> cl;

try {
cl = Class.forName(className, true, classLoader);
} catch (ClassNotFoundException ex) {
throw new RuntimeException(
"Missing class when invoking static main " + className,
ex);
}

Method m;
try {
m = cl.getMethod("main", new Class[] { String[].class });
} catch (NoSuchMethodException ex) {
throw new RuntimeException(
"Missing static main on " + className, ex);
} catch (SecurityException ex) {
throw new RuntimeException(
"Problem getting static main on " + className, ex);
}
//...
return new MethodAndArgsCaller(m, argv);
}

原来是反射!通过反射调用了ActivityThread 的 main 方法。ActivityThread大家应该都很熟悉了,代表了Android的主线程,而main方法也是app的主入口。这不对上了!新建进程的时候就调用了,可不是主入口嘛。来看看这个主入口。


public static void main(String[] args) {
//...
Looper.prepareMainLooper();

ActivityThread thread = new ActivityThread();
thread.attach(false, startSeq);

//...

if (false) {
Looper.myLooper().setMessageLogging(new
LogPrinter(Log.DEBUG, "ActivityThread"));
}
//...
Looper.loop();

throw new RuntimeException("Main thread loop unexpectedly exited");
}

main方法主要创建了ActivityThread,创建了主线程的Looper对象,并开始loop循环。除了这些,还要告诉AMS,我醒啦,进程创建好了!也就是上述代码中的attach方法,最后会转到AMSattachApplicationLocked方法,一起看看这个方法干了啥:


//ActivitymanagerService.java
private final boolean attachApplicationLocked(IApplicationThread thread,
int pid, int callingUid, long startSeq) {
//...
ProcessRecord app;
//...
thread.bindApplication(processName, appInfo, providers, null, profilerInfo,
null, null, null, testMode,
mBinderTransactionTrackingEnabled, enableTrackAllocation,
isRestrictedBackupMode || !normalMode, app.isPersistent(),
new Configuration(app.getWindowProcessController().getConfiguration()),
app.compat, getCommonServicesLocked(app.isolated),
mCoreSettingsObserver.getCoreSettingsLocked(),
buildSerial, autofillOptions, contentCaptureOptions);
//...
app.makeActive(thread, mProcessStats);

//...
// See if the top visible activity is waiting to run in this process...
if (normalMode) {
try {
didSomething = mAtmInternal.attachApplication(app.getWindowProcessController());
} catch (Exception e) {
Slog.wtf(TAG, "Exception thrown launching activities in " + app, e);
badApp = true;
}
}
//...
}

//ProcessRecord.java
public void makeActive(IApplicationThread _thread, ProcessStatsService tracker) {
//...
thread = _thread;
mWindowProcessController.setThread(thread);
}

这里主要做了三件事:



  • bindApplication方法,主要用来启动Application。
  • makeActive方法,设定WindowProcessController里面的线程,也就是上文中说过判断进程是否存在所用到的。
  • attachApplication方法,启动根Activity。

第五关:创建Application

接着上面看,按照我们所熟知的,应用启动后,应该就是启动Applicaiton,启动Activity。看看是不是怎么回事:


    //ActivityThread#ApplicationThread
public final void bindApplication(String processName, ApplicationInfo appInfo,
List<ProviderInfo> providers, ComponentName instrumentationName,
ProfilerInfo profilerInfo, Bundle instrumentationArgs,
IInstrumentationWatcher instrumentationWatcher,
IUiAutomationConnection instrumentationUiConnection, int debugMode,
boolean enableBinderTracking, boolean trackAllocation,
boolean isRestrictedBackupMode, boolean persistent, Configuration config,
CompatibilityInfo compatInfo, Map services, Bundle coreSettings,
String buildSerial, AutofillOptions autofillOptions,
ContentCaptureOptions contentCaptureOptions) {
AppBindData data = new AppBindData();
data.processName = processName;
data.appInfo = appInfo;
data.providers = providers;
data.instrumentationName = instrumentationName;
data.instrumentationArgs = instrumentationArgs;
data.instrumentationWatcher = instrumentationWatcher;
data.instrumentationUiAutomationConnection = instrumentationUiConnection;
data.debugMode = debugMode;
data.enableBinderTracking = enableBinderTracking;
data.trackAllocation = trackAllocation;
data.restrictedBackupMode = isRestrictedBackupMode;
data.persistent = persistent;
data.config = config;
data.compatInfo = compatInfo;
data.initProfilerInfo = profilerInfo;
data.buildSerial = buildSerial;
data.autofillOptions = autofillOptions;
data.contentCaptureOptions = contentCaptureOptions;
sendMessage(H.BIND_APPLICATION, data);
}

public void handleMessage(Message msg) {
if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
switch (msg.what) {
case BIND_APPLICATION:
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "bindApplication");
AppBindData data = (AppBindData)msg.obj;
handleBindApplication(data);
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
break;
}
}

复制代码

可以看到这里有个H,H是主线程的一个Handler类,用于处理需要主线程处理的各类消息,包括BIND_SERVICE,LOW_MEMORY,DUMP_HEAP等等。接着看handleBindApplication:


private void handleBindApplication(AppBindData data) {
//...
try {
final ClassLoader cl = instrContext.getClassLoader();
mInstrumentation = (Instrumentation)
cl.loadClass(data.instrumentationName.getClassName()).newInstance();
}
//...
Application app;
final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskWrites();
final StrictMode.ThreadPolicy writesAllowedPolicy = StrictMode.getThreadPolicy();
try {
// If the app is being launched for full backup or restore, bring it up in
// a restricted environment with the base application class.
app = data.info.makeApplication(data.restrictedBackupMode, null);
mInitialApplication = app;
// don't bring up providers in restricted mode; they may depend on the
// app's custom Application class
if (!data.restrictedBackupMode) {
if (!ArrayUtils.isEmpty(data.providers)) {
installContentProviders(app, data.providers);
}
}

// Do this after providers, since instrumentation tests generally start their
// test thread at this point, and we don't want that racing.
try {
mInstrumentation.onCreate(data.instrumentationArgs);
}
//...
try {
mInstrumentation.callApplicationOnCreate(app);
} catch (Exception e) {
if (!mInstrumentation.onException(app, e)) {
throw new RuntimeException(
"Unable to create application " + app.getClass().getName()
+ ": " + e.toString(), e);
}
}
}
//...
}

这里信息量就多了,一点点的看:



  • 首先,创建了Instrumentation,也就是上文一开始startActivity的第一步。每个应用程序都有一个Instrumentation,用于管理这个进程,比如要创建Activity的时候,首先就会执行到这个类里面。
  • makeApplication方法,创建了Application,终于到这一步了。最终会走到newApplication方法,执行Application的attach方法。

public Application newApplication(ClassLoader cl, String className, Context context)
throws InstantiationException, IllegalAccessException,
ClassNotFoundException {
Application app = getFactory(context.getPackageName())
.instantiateApplication(cl, className);
app.attach(context);
return app;
}

attach方法有了,onCreate方法又是何时调用的呢?马上来了:


instrumentation.callApplicationOnCreate(app);

public void callApplicationOnCreate(Application app) {
app.onCreate();
}

也就是创建Application->attach->onCreate调用顺序。


等等,在onCreate之前还有一句重要的代码:


installContentProviders

这里就是启动Provider的相关代码了,具体逻辑就不分析了。


第六关:启动Activity

说完bindApplication,该说说后续了,上文第五关说到,bindApplication方法之后执行的是attachApplication方法,最终会执行到ActivityThread的handleLaunchActivity方法:


public Activity handleLaunchActivity(ActivityClientRecord r,
PendingTransactionActions pendingActions, Intent customIntent) {
//...
WindowManagerGlobal.initialize();
//...
final Activity a = performLaunchActivity(r, customIntent);
//...
return a;
}

首先,初始化了WindowManagerGlobal,这是个啥呢? 没错,就是WindowManagerService了,也为后续窗口显示等作了准备。


继续看performLaunchActivity:


private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
//创建ContextImpl
ContextImpl appContext = createBaseContextForActivity(r);
Activity activity = null;
try {
java.lang.ClassLoader cl = appContext.getClassLoader();
//创建Activity
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
}

try {
if (activity != null) {
//完成activity的一些重要数据的初始化
activity.attach(appContext, this, getInstrumentation(), r.token,
r.ident, app, r.intent, r.activityInfo, title, r.parent,
r.embeddedID, r.lastNonConfigurationInstances, config,
r.referrer, r.voiceInteractor, window, r.configCallback,
r.assistToken);

if (customIntent != null) {
activity.mIntent = customIntent;
}

//设置activity的主题
int theme = r.activityInfo.getThemeResource();
if (theme != 0) {
activity.setTheme(theme);
}

//调用activity的onCreate方法
if (r.isPersistable()) {
mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
} else {
mInstrumentation.callActivityOnCreate(activity, r.state);
}
}
}

return activity;
}

哇,终于看到onCreate方法了。稳住,还是一步步看看这段代码。


首先,创建了ContextImpl对象,ContextImpl可能有的朋友不知道是啥,ContextImpl继承自Context,其实就是我们平时用的上下文。有的同学可能表示,这不对啊,获取上下文明明获取的是Context对象。来一起跟随源码看看。


//Activity.java
Context mBase;

@Override
public Executor getMainExecutor() {
return mBase.getMainExecutor();
}

@Override
public Context getApplicationContext() {
return mBase.getApplicationContext();
}

这里可以看到,我们平时用的上下文就是这个mBase,那么找到这个mBase是啥就行了:


protected void attachBaseContext(Context base) {
if (mBase != null) {
throw new IllegalStateException("Base context already set");
}
mBase = base;
}

//一层层往上找

final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor) {

attachBaseContext(context);

mWindow = new PhoneWindow(this, window, activityConfigCallback);
mWindow.setWindowControllerCallback(this);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
mWindow.getLayoutInflater().setPrivateFactory(this);
if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {
mWindow.setSoftInputMode(info.softInputMode);
}


}

这不就是,,,刚才一开始performLaunchActivity方法里面的attach吗?太巧了,所以这个ContextImpl就是我们平时所用的上下文。


顺便看看attach还干了啥?新建了PhoneWindow,建立自己和Window的关联,并设置了setSoftInputMode等等。


ContextImpl创建完之后,会通过类加载器创建Activity的对象,然后设置好activity的主题,最后调用了activity的onCreate方法。


总结

再一起捋一遍App的启动流程:



  • Launcher被调用点击事件,转到Instrumentation类的startActivity方法。
  • Instrumentation通过跨进程通信告诉AMS要启动应用的需求。
  • AMS反馈Launcher,让Launcher进入Paused状态
  • Launcher进入Paused状态,AMS转到ZygoteProcess类,并通过socket与Zygote通信,告知Zygote需要新建进程。
  • Zygote fork进程,并调用ActivityThread的main方法,也就是app的入口。
  • ActivityThread的main方法新建了ActivityThread实例,并新建了Looper实例,开始loop循环。
  • 同时ActivityThread也告知AMS,进程创建完毕,开始创建Application,Provider,并调用Applicaiton的attach,onCreate方法。
  • 最后就是创建上下文,通过类加载器加载Activity,调用Activity的onCreate方法。

至此,应用启动完毕。


当然,分析源码的目的一直都不是为了学知识而学,而是理解了这些基础,我们才能更好的解决问题。 学习了App的启动流程,我们可以再思考下一些之前没理解透的问题,比如启动优化


分析启动过程,其实可以优化启动速度的地方有三个地方:



  • Application的attach方法,MultiDexApplication会在方法里面会去执行MultiDex逻辑。所以这里可以进行MultiDex优化,比如今日头条方案就是单独启动一个进程的activity去加载MultiDex。
  • Application的onCreate方法,大量三方库的初始化都在这里进行,所以我们可以开启线程池,懒加载等等。把每个启动任务进行区分,哪些可以子线程运行,哪些有先后顺序。
  • Activity的onCreate方法,同样进行线程处理,懒加载。或者预创建Activity,提前类加载等等。

最后希望各位老铁都能有一个乖巧可爱漂亮的女儿/儿子。😊


附件

fork使用多线程
今日头条启动优化
app启动流程分析


作者:积木zz
来源:https://juejin.cn/post/6867744083809419277

收起阅读 »

女儿拿着小天才电话手表问我App启动流程(上)

首先,new一个女儿,var mDdaughter = new 女儿("6岁",“漂亮可爱”,“健康乖巧”,“最喜欢玩小天才电话手表和她的爸爸”)“爸爸爸爸,你说我玩的这个小天才电话手表怎么这么厉害,随便点一下这个小图片,这个应用就冒出来了,就可以听儿歌了。好...
继续阅读 »



前言

首先,new一个女儿,

var mDdaughter = new 女儿("6岁",“漂亮可爱”,“健康乖巧”,“最喜欢玩小天才电话手表和她的爸爸”)

好了,女儿有了,有一天,女儿问我:

“爸爸爸爸,你说我玩的这个小天才电话手表怎么这么厉害,随便点一下这个小图片,这个应用就冒出来了,就可以听儿歌了。好神奇啊。”

我心里一惊: img

小天才电话手表的系统就是Android,所以这不就是。。面试官常考的应用启动流程嘛!
女儿也要来面试我了吗!😭
好了,既然女儿问了,那就答吧。
但是,对付这个小小的0经验面试官,我该咋说呢?

解答小小面试官

女儿,你可以把手表里面想象成一个幼儿园,里面有一个老师,一个班长,一个班干部,以及一大堆小朋友。

  • 一个老师:Z老师(Zygote进程)

  • 一个班长:小A(ActivityManagerService)

  • 一个班干部:小L(Launcher桌面应用)

  • 一大堆小朋友:所有应用,包括音乐小朋友,聊天小朋友,日历小朋友等等。

img

应用启动过程就像一个小朋友被叫醒一样,开机之后呢,Z老师会依次叫醒班长和班干部(SystemServer#ActivityManagerService,Launcher),小L醒了之后就会去了解手表里有哪些小朋友,长什么样(icon,name),家庭信息(包名,androidmanifest)等等,然后一个个把小朋友的照片(icon)贴到自己的身上。比如有音乐小朋友,聊天小朋友,日历小朋友,其实也就是你手表上这个桌面啦。

这时候你要点开一个音乐小朋友呢(startActivity),小L就会通知班长小A(Binder),小A知道了之后,让小L自己休息下(Paused),然后就去找Z老师了。Z老师就负责叫音乐小朋友起床了(fork进程,启动ActivityThread),音乐小朋友起来后就又找小A带她去洗脸刷牙(启动ApplicationThread,Activity),都弄完了就可以进行各种表演了,唱歌啊,跳舞啊。

不是很明白啊?我们一起聊个天你就懂了,假如我是Launcher

img

img

女儿似懂非懂的给我点了一个赞👍,爸爸你真棒。

十五年后

mDdaughter.grow(15)
mDdaughter.study("Android")

过了十五年,女儿已经21岁了,正在学习Android,考虑要不要女从父业。

这天,她一脸疑惑的来找我: “爸,这个app启动到底是怎么个流程啊,我看了好久还是不大明白,要不你再跟我详细讲一遍吧?” “好嘞,别担心,我这次详细跟你说说”

解答Android程序媛

还记得我小时候跟你说过的故事吗,Android系统就像一个幼儿园,有一个大朋友叫Launcher,身上会贴很多其他小朋友的名片。这个Launcher就是我们的桌面了,它通过PackageManagerService获知了系统里所有应用的信息,并展示了出来,当然它本身也是一个应用。

通过点击一个应用图标,也就是触发了点击事件,最后会执行到startActivity方法。这里也就和启动Activity步骤重合上了。

那么这个startActivity干了啥?是怎么通过重重关卡唤醒这个应用的?

首先,介绍下系统中那些重要的成员,他们在app启动流程中都担任了重要的角色.

系统成员介绍

  • init进程,Android系统启动后,Zygote并不是第一个进程,而是linux的根进程init进程,然后init进程才会启动Zygote进程。

  • Zygote进程,所有android进程的父进程,当然也包括SystemServer进程

  • SystemServer进程,正如名字一样,系统服务进程,负责系统中大大小小的事物,为此也是启动了三员大将(ActivityManagerService,PackageManagerService,WindowManagerService)以及binder线程池。

  • ActivityManagerService,主要负责系统中四大组件的启动、切换、调度及应用进程的管理和调度等工作,对于一些进程的启动,都会通过Binder通信机制传递给AMS,再处理给Zygote。

  • PackageManagerService,主要负责应用包的一些操作,比如安装,卸载,解析AndroidManifest.xml,扫描文件信息等等。

  • WindowManagerService,主要负责窗口相关的一些服务,比如窗口的启动,添加,删除等。

  • Launcher,桌面应用,也是属于应用,也有自己的Activity,一开机就会默认启动,通过设置Intent.CATEGORY_HOME的Category隐式启动。

搞清楚这些成员,就跟随我一起看看怎么过五关斩六将,最终启动了一个App。

第一关:跨进程通信,告诉系统我的需求

首先,要告诉系统,我Launcher要启动一个应用了,调用Activity.startActivityForResult方法,最终会转到mInstrumentation.execStartActivity方法。 由于Launcher自己处在一个单独的进程,所以它需要跨进程告诉系统服务我要启动App的需求。 找到要通知的Service,名叫ActivityTaskManagerService,然后使用AIDL,通过Binder与他进行通信。

这里的简单说下ActivityTaskManagerService(简称ATMS)。原来这些通信工作都是属于ActivityManagerService,现在分了一部分工作给到ATMS,主要包括四大组件的调度工作。也是由SystemServer进程直接启动的,相关源码可见ActivityManagerService.Lifecycle.startService方法,感兴趣朋友可以自己看看。

接着说跨进程通信,相关代码如下:

//Instrumentation.java
int result = ActivityTaskManager.getService()
  .startActivity(whoThread, who.getBasePackageName(), intent,
                  intent.resolveTypeIfNeeded(who.getContentResolver()),
                  token, target != null ? target.mEmbeddedID : null,
                  requestCode, 0, null, options);


//ActivityTaskManager.java            
public static IActivityTaskManager getService() {
   return IActivityTaskManagerSingleton.get();
}
private static final Singleton<IActivityTaskManager> IActivityTaskManagerSingleton =
   new Singleton<IActivityTaskManager>() {
   @Override
   protected IActivityTaskManager create() {
       final IBinder b = ServiceManager.getService(Context.ACTIVITY_TASK_SERVICE);
       return IActivityTaskManager.Stub.asInterface(b);
  }
};

//ActivityTaskManagerService.java
public class ActivityTaskManagerService extends IActivityTaskManager.Stub

   public static final class Lifecycle extends SystemService {
       private final ActivityTaskManagerService mService;

       public Lifecycle(Context context) {
           super(context);
           mService = new ActivityTaskManagerService(context);
      }

       @Override
       public void onStart() {
           publishBinderService(Context.ACTIVITY_TASK_SERVICE, mService);
           mService.start();
      }
  }

startActivity我们都很熟悉,平时启动Activity都会使用,启动应用也是从这个方法开始的,也会同样带上intent信息,表示要启动的是哪个Activity。

另外要注意的一点是,startActivity之后有个checkStartActivityResult方法,这个方法是用作检查启动Activity的结果。当启动Activity失败的时候,就会通过这个方法抛出异常,比如有我们常见的问题:未在AndroidManifest.xml注册。

public static void checkStartActivityResult(int res, Object intent) {
   switch (res) {
       case ActivityManager.START_INTENT_NOT_RESOLVED:
       case ActivityManager.START_CLASS_NOT_FOUND:
           if (intent instanceof Intent && ((Intent)intent).getComponent() != null)
               throw new ActivityNotFoundException(
               "Unable to find explicit activity class "
               + ((Intent)intent).getComponent().toShortString()
               + "; have you declared this activity in your AndroidManifest.xml?");
           throw new ActivityNotFoundException(
               "No Activity found to handle " + intent);
       case ActivityManager.START_PERMISSION_DENIED:
           throw new SecurityException("Not allowed to start activity "
                                       + intent);
       case ActivityManager.START_FORWARD_AND_REQUEST_CONFLICT:
           throw new AndroidRuntimeException(
               "FORWARD_RESULT_FLAG used while also requesting a result");
       case ActivityManager.START_NOT_ACTIVITY:
           throw new IllegalArgumentException(
               "PendingIntent is not an activity");
           //...
  }
}

第二关:通知Launcher可以休息了

ATMS收到要启动的消息后,就会通知上一个应用,也就是Launcher可以休息会了,进入Paused状态。

//ActivityStack.java

private boolean resumeTopActivityInnerLocked(ActivityRecord prev, ActivityOptions options) {
   //...
   ActivityRecord next = topRunningActivityLocked(true /* focusableOnly */);
   //...
   boolean pausing = getDisplay().pauseBackStacks(userLeaving, next, false);
   if (mResumedActivity != null) {
       if (DEBUG_STATES) Slog.d(TAG_STATES,
                                "resumeTopActivityLocked: Pausing " + mResumedActivity);
       pausing |= startPausingLocked(userLeaving, false, next, false);
  }
   //...

   if (next.attachedToProcess()) {
       //应用已经启动
       try {
           //...
           transaction.setLifecycleStateRequest(
               ResumeActivityItem.obtain(next.app.getReportedProcState(),
                                         getDisplay().mDisplayContent.isNextTransitionForward()));
           mService.getLifecycleManager().scheduleTransaction(transaction);
           //...
      } catch (Exception e) {
           //...
           mStackSupervisor.startSpecificActivityLocked(next, true, false);
           return true;
      }
       //...
       // From this point on, if something goes wrong there is no way
       // to recover the activity.
       try {
           next.completeResumeLocked();
      } catch (Exception e) {
           // If any exception gets thrown, toss away this
           // activity and try the next one.
           Slog.w(TAG, "Exception thrown during resume of " + next, e);
           requestFinishActivityLocked(next.appToken, Activity.RESULT_CANCELED, null,
                                       "resume-exception", true);
           return true;
      }
  } else {
       //冷启动流程
       mStackSupervisor.startSpecificActivityLocked(next, true, true);
  }        
}

这里有两个类没有见过:

  • ActivityStack,是Activity的栈管理,相当于我们平时项目里面自己写的Activity管理类,用于管理Activity的状态啊,如栈出栈顺序等等。

  • ActivityRecord,代表具体的某一个Activity,存放了该Activity的各种信息。

startPausingLocked方法就是让上一个应用,这里也就是Launcher进入Paused状态。 然后就会判断应用是否启动,如果已经启动了,就会走ResumeActivityItem的方法,看这个名字,结合应用已经启动的前提,是不是已经猜到了它是干吗的?没错,这个就是用来控制Activity的onResume生命周期方法的,不仅是onResume还有onStart方法,具体可见ActivityThread的handleResumeActivity方法源码。

如果应用没启动就会接着走到startSpecificActivityLocked方法,接着看。

第三关:是否已启动进程,否则创建进程

Launcher进入Paused之后,ActivityTaskManagerService就会判断要打开的这个应用进程是否已经启动,如果已经启动,则直接启动Activity即可,这也就是应用内的启动Activity流程。如果进程没有启动,则需要创建进程。

这里有两个问题:

  • 怎么判断应用进程是否存在呢?如果一个应用已经启动了,会在ATMS里面保存一个WindowProcessController信息,这个信息包括processName和uid,uid则是应用程序的id,可以通过applicationInfo.uid获取。processName则是进程名,一般为程序包名。所以判断是否存在应用进程,则是根据processName和uid去判断是否有对应的WindowProcessController,并且WindowProcessController里面的线程不为空。代码如下:

//ActivityStackSupervisor.java
void startSpecificActivityLocked(ActivityRecord r, boolean andResume, boolean checkConfig) {
   // Is this activity's application already running?
   final WindowProcessController wpc =
       mService.getProcessController(r.processName, r.info.applicationInfo.uid);

   boolean knownToBeDead = false;
   if (wpc != null && wpc.hasThread()) {
       //应用进程存在
       try {
           realStartActivityLocked(r, wpc, andResume, checkConfig);
           return;
      }
  }
}

//WindowProcessController.java
IApplicationThread getThread() {
   return mThread;
}

boolean hasThread() {
   return mThread != null;
}
  • 还有个问题就是怎么创建进程?还记得Z老师吗?对,就是Zygote进程。之前说了他是所有进程的父进程,所以就要通知Zygote去fork一个新的进程,服务于这个应用。

//ZygoteProcess.java
private Process.ProcessStartResult attemptUsapSendArgsAndGetResult(
   ZygoteState zygoteState, String msgStr)
   throws ZygoteStartFailedEx, IOException {
   try (LocalSocket usapSessionSocket = zygoteState.getUsapSessionSocket()) {
       final BufferedWriter usapWriter =
           new BufferedWriter(
           new OutputStreamWriter(usapSessionSocket.getOutputStream()),
           Zygote.SOCKET_BUFFER_SIZE);
       final DataInputStream usapReader =
           new DataInputStream(usapSessionSocket.getInputStream());

       usapWriter.write(msgStr);
       usapWriter.flush();

       Process.ProcessStartResult result = new Process.ProcessStartResult();
       result.pid = usapReader.readInt();
       // USAPs can't be used to spawn processes that need wrappers.
       result.usingWrapper = false;

       if (result.pid >= 0) {
           return result;
      } else {
           throw new ZygoteStartFailedEx("USAP specialization failed");
      }
  }
}

可以看到,这里其实是通过socket和Zygote进行通信,BufferedWriter用于读取和接收消息。这里将要新建进程的消息传递给Zygote,由Zygote进行fork进程,并返回新进程的pid。

可能又会有人问了?fork是啥?为啥这里又变成socket进行IPC通信,而不是Bindler了?

  • 首先,fork()是一个方法,是类Unix操作系统上创建进程的主要方法。用于创建子进程(等同于当前进程的副本)。

  • 那为什么fork的时候不用Binder而用socket了呢?主要是因为fork不允许存在多线程,Binder通讯偏偏就是多线程。

问题总是在不断产生,总有好奇的朋友会接着问,为什么fork不允许存在多线程?

收起阅读 »

黑科技,Python 脚本帮你找出微信上删除你好友的人

查看被删的微信好友原理就是新建群组,如果加不进来就是被删好友了(不要在群组里讲话,别人是看不见的)用的是微信网页版的接口查询结果可能会引起一些心理上的不适,请小心使用..(逃还有些小问题:结果好像有疏漏一小部分,原因不明..最终会遗留下一个只有自己的群组,需要...
继续阅读 »



查看被删的微信好友

原理就是新建群组,如果加不进来就是被删好友了(不要在群组里讲话,别人是看不见的)

用的是微信网页版的接口

查询结果可能会引起一些心理上的不适,请小心使用..(逃

还有些小问题:

结果好像有疏漏一小部分,原因不明..

最终会遗留下一个只有自己的群组,需要手工删一下

没试过被拉黑的情况

新手步骤 Mac 上步骤:

  1. 在 Mac 上操作,下载代码文件wdf.py

  2. 打开 Terminal 输入:python +空格,然后拖动刚才下载的 wdf.py 到 Terminal 后回车。格式: python wdf.py

  3. 接下来按步骤操作即可;

代码如下:

#!/usr/bin/env python
# coding=utf-8

import os
import urllib, urllib2
import re
import cookielib
import time
import xml.dom.minidom
import json
import sys
import math

DEBUG = False

MAX_GROUP_NUM = 35 # 每组人数

QRImagePath = os.getcwd() + '/qrcode.jpg'

tip = 0
uuid = ''

base_uri = ''
redirect_uri = ''

skey = ''
wxsid = ''
wxuin = ''
pass_ticket = ''
deviceId = 'e000000000000000'

BaseRequest = {}

ContactList = []
My = []

def getUUID():
global uuid

url = 'https://login.weixin.qq.com/jslogin'
params = {
'appid': 'wx782c26e4c19acffb',
'fun': 'new',
'lang': 'zh_CN',
'_': int(time.time()),
}

request = urllib2.Request(url = url, data = urllib.urlencode(params))
response = urllib2.urlopen(request)
data = response.read()

# print data

# window.QRLogin.code = 200; window.QRLogin.uuid = "oZwt_bFfRg==";
regx = r'window.QRLogin.code = (\d+); window.QRLogin.uuid = "(\S+?)"'
pm = re.search(regx, data)

code = pm.group(1)
uuid = pm.group(2)

if code == '200':
return True

return False

def showQRImage():
global tip

url = 'https://login.weixin.qq.com/qrcode/' + uuid
params = {
't': 'webwx',
'_': int(time.time()),
}

request = urllib2.Request(url = url, data = urllib.urlencode(params))
response = urllib2.urlopen(request)

tip = 1

f = open(QRImagePath, 'wb')
f.write(response.read())
f.close()

if sys.platform.find('darwin') >= 0:
os.system('open %s' % QRImagePath)
elif sys.platform.find('linux') >= 0:
os.system('xdg-open %s' % QRImagePath)
else:
os.system('call %s' % QRImagePath)

print '请使用微信扫描二维码以登录'

def waitForLogin():
global tip, base_uri, redirect_uri

url = 'https://login.weixin.qq.com/cgi-bin/mmwebwx-bin/login?tip=%s&uuid=%s&_=%s' % (tip, uuid, int(time.time()))

request = urllib2.Request(url = url)
response = urllib2.urlopen(request)
data = response.read()

# print data

# window.code=500;
regx = r'window.code=(\d+);'
pm = re.search(regx, data)

code = pm.group(1)

if code == '201': #已扫描
print '成功扫描,请在手机上点击确认以登录'
tip = 0
elif code == '200': #已登录
print '正在登录...'
regx = r'window.redirect_uri="(\S+?)";'
pm = re.search(regx, data)
redirect_uri = pm.group(1) + '&fun=new'
base_uri = redirect_uri[:redirect_uri.rfind('/')]
elif code == '408': #超时
pass
# elif code == '400' or code == '500':

return code

def login():
global skey, wxsid, wxuin, pass_ticket, BaseRequest

request = urllib2.Request(url = redirect_uri)
response = urllib2.urlopen(request)
data = response.read()

# print data

'''

0
OK
xxx
xxx
xxx
xxx
1

'''

doc = xml.dom.minidom.parseString(data)
root = doc.documentElement

for node in root.childNodes:
if node.nodeName == 'skey':
skey = node.childNodes[0].data
elif node.nodeName == 'wxsid':
wxsid = node.childNodes[0].data
elif node.nodeName == 'wxuin':
wxuin = node.childNodes[0].data
elif node.nodeName == 'pass_ticket':
pass_ticket = node.childNodes[0].data

# print 'skey: %s, wxsid: %s, wxuin: %s, pass_ticket: %s' % (skey, wxsid, wxuin, pass_ticket)

if skey == '' or wxsid == '' or wxuin == '' or pass_ticket == '':
return False

BaseRequest = {
'Uin': int(wxuin),
'Sid': wxsid,
'Skey': skey,
'DeviceID': deviceId,
}

return True

def webwxinit():

url = base_uri + '/webwxinit?pass_ticket=%s&skey=%s&r=%s' % (pass_ticket, skey, int(time.time()))
params = {
'BaseRequest': BaseRequest
}

request = urllib2.Request(url = url, data = json.dumps(params))
request.add_header('ContentType', 'application/json; charset=UTF-8')
response = urllib2.urlopen(request)
data = response.read()

if DEBUG == True:
f = open(os.getcwd() + '/webwxinit.json', 'wb')
f.write(data)
f.close()

# print data

global ContactList, My
dic = json.loads(data)
ContactList = dic['ContactList']
My = dic['User']

ErrMsg = dic['BaseResponse']['ErrMsg']
if len(ErrMsg) > 0:
print ErrMsg

Ret = dic['BaseResponse']['Ret']
if Ret != 0:
return False

return True

def webwxgetcontact():

url = base_uri + '/webwxgetcontact?pass_ticket=%s&skey=%s&r=%s' % (pass_ticket, skey, int(time.time()))

request = urllib2.Request(url = url)
request.add_header('ContentType', 'application/json; charset=UTF-8')
response = urllib2.urlopen(request)
data = response.read()

if DEBUG == True:
f = open(os.getcwd() + '/webwxgetcontact.json', 'wb')
f.write(data)
f.close()

# print data

dic = json.loads(data)
MemberList = dic['MemberList']

# 倒序遍历,不然删除的时候出问题..
SpecialUsers = ['newsapp', 'fmessage', 'filehelper', 'weibo', 'qqmail', 'fmessage', 'tmessage', 'qmessage', 'qqsync', 'floatbottle', 'lbsapp', 'shakeapp', 'medianote', 'qqfriend', 'readerapp', 'blogapp', 'facebookapp', 'masssendapp', 'meishiapp', 'feedsapp', 'voip', 'blogappweixin', 'weixin', 'brandsessionholder', 'weixinreminder', 'wxid_novlwrv3lqwv11', 'gh_22b87fa7cb3c', 'officialaccounts', 'notification_messages', 'wxid_novlwrv3lqwv11', 'gh_22b87fa7cb3c', 'wxitil', 'userexperience_alarm', 'notification_messages']
for i in xrange(len(MemberList) - 1, -1, -1):
Member = MemberList[i]
if Member['VerifyFlag'] & 8 != 0: # 公众号/服务号
MemberList.remove(Member)
elif Member['UserName'] in SpecialUsers: # 特殊账号
MemberList.remove(Member)
elif Member['UserName'].find('@@') != -1: # 群聊
MemberList.remove(Member)
elif Member['UserName'] == My['UserName']: # 自己
MemberList.remove(Member)

return MemberList

def createChatroom(UserNames):
MemberList = []
for UserName in UserNames:
MemberList.append({'UserName': UserName})


url = base_uri + '/webwxcreatechatroom?pass_ticket=%s&r=%s' % (pass_ticket, int(time.time()))
params = {
'BaseRequest': BaseRequest,
'MemberCount': len(MemberList),
'MemberList': MemberList,
'Topic': '',
}

request = urllib2.Request(url = url, data = json.dumps(params))
request.add_header('ContentType', 'application/json; charset=UTF-8')
response = urllib2.urlopen(request)
data = response.read()

# print data

dic = json.loads(data)
ChatRoomName = dic['ChatRoomName']
MemberList = dic['MemberList']
DeletedList = []
for Member in MemberList:
if Member['MemberStatus'] == 4: #被对方删除了
DeletedList.append(Member['UserName'])

ErrMsg = dic['BaseResponse']['ErrMsg']
if len(ErrMsg) > 0:
print ErrMsg

return (ChatRoomName, DeletedList)

def deleteMember(ChatRoomName, UserNames):
url = base_uri + '/webwxupdatechatroom?fun=delmember&pass_ticket=%s' % (pass_ticket)
params = {
'BaseRequest': BaseRequest,
'ChatRoomName': ChatRoomName,
'DelMemberList': ','.join(UserNames),
}

request = urllib2.Request(url = url, data = json.dumps(params))
request.add_header('ContentType', 'application/json; charset=UTF-8')
response = urllib2.urlopen(request)
data = response.read()

# print data

dic = json.loads(data)
ErrMsg = dic['BaseResponse']['ErrMsg']
if len(ErrMsg) > 0:
print ErrMsg

Ret = dic['BaseResponse']['Ret']
if Ret != 0:
return False

return True

def addMember(ChatRoomName, UserNames):
url = base_uri + '/webwxupdatechatroom?fun=addmember&pass_ticket=%s' % (pass_ticket)
params = {
'BaseRequest': BaseRequest,
'ChatRoomName': ChatRoomName,
'AddMemberList': ','.join(UserNames),
}

request = urllib2.Request(url = url, data = json.dumps(params))
request.add_header('ContentType', 'application/json; charset=UTF-8')
response = urllib2.urlopen(request)
data = response.read()

# print data

dic = json.loads(data)
MemberList = dic['MemberList']
DeletedList = []
for Member in MemberList:
if Member['MemberStatus'] == 4: #被对方删除了
DeletedList.append(Member['UserName'])

ErrMsg = dic['BaseResponse']['ErrMsg']
if len(ErrMsg) > 0:
print ErrMsg

return DeletedList

def main():

opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookielib.CookieJar()))
urllib2.install_opener(opener)

if getUUID() == False:
print '获取uuid失败'
return

showQRImage()
time.sleep(1)

while waitForLogin() != '200':
pass

os.remove(QRImagePath)

if login() == False:
print '登录失败'
return

if webwxinit() == False:
print '初始化失败'
return

MemberList = webwxgetcontact()

MemberCount = len(MemberList)
print '通讯录共%s位好友' % MemberCount

ChatRoomName = ''
result = []
for i in xrange(0, int(math.ceil(MemberCount / float(MAX_GROUP_NUM)))):
UserNames = []
NickNames = []
DeletedList = ''
for j in xrange(0, MAX_GROUP_NUM):
if i * MAX_GROUP_NUM + j >= MemberCount:
break

Member = MemberList[i * MAX_GROUP_NUM + j]
UserNames.append(Member['UserName'])
NickNames.append(Member['NickName'].encode('utf-8'))
                       
print '第%s组...' % (i + 1)
print ', '.join(NickNames)
print '回车键继续...'
raw_input()

# 新建群组/添加成员
if ChatRoomName == '':
(ChatRoomName, DeletedList) = createChatroom(UserNames)
else:
DeletedList = addMember(ChatRoomName, UserNames)

DeletedCount = len(DeletedList)
if DeletedCount > 0:
result += DeletedList

print '找到%s个被删好友' % DeletedCount
# raw_input()

# 删除成员
deleteMember(ChatRoomName, UserNames)

# todo 删除群组


resultNames = []
for Member in MemberList:
if Member['UserName'] in result:
NickName = Member['NickName']
if Member['RemarkName'] != '':
NickName += '(%s)' % Member['RemarkName']
resultNames.append(NickName.encode('utf-8'))

print '---------- 被删除的好友列表 ----------'
print '\n'.join(resultNames)
print '-----------------------------------'

# windows下编码问题修复
# http://blog.csdn.net/heyuxuanzee/article/details/8442718
class UnicodeStreamFilter:  
def __init__(self, target):  
self.target = target  
self.encoding = 'utf-8'  
self.errors = 'replace'  
self.encode_to = self.target.encoding  
def write(self, s):  
if type(s) == str:  
s = s.decode('utf-8')  
s = s.encode(self.encode_to, self.errors).decode(self.encode_to)  
self.target.write(s)  
 
if sys.stdout.encoding == 'cp936':  
sys.stdout = UnicodeStreamFilter(sys.stdout)

if __name__ == '__main__' :

print '本程序的查询结果可能会引起一些心理上的不适,请小心使用...'
print '回车键继续...'
raw_input()

main()

print '回车键结束'
raw_input()

作者: 0x5e(github id)

来源:https://juejin.cn/post/6844903425629487112

收起阅读 »

js打包时间缩短90%,bundleless生产环境实践总结

最近尝试将bundleless的构建结果直接用到了线上生产环境,因为bundleless只会编译代码,不会打包,因此构建速度极快,同比bundle模式时间缩短了90%以上。得益于大部分浏览器都已经支持了http2和浏览器的es module,对于我们没有强兼容...
继续阅读 »




最近尝试将bundleless的构建结果直接用到了线上生产环境,因为bundleless只会编译代码,不会打包,因此构建速度极快,同比bundle模式时间缩短了90%以上。得益于大部分浏览器都已经支持了http2和浏览器的es module,对于我们没有强兼容场景的中后台系统,将bundleless构建结果应用于线上是有可能的。本文主要介绍一下,本人在使用bundleless构建工具实践中遇到的问题。

  • 起源

  • 结合snowpack实践

  • snowpack的Streaming Imports

  • 性能比较

  • 总结

  • 附录snowpack和vite的对比


本文原文来自我的博客: github.com/fortheallli…

一、起源

1.1 从http2谈起

以前因为http1.x不支持多路服用, HTTP 1.x 中,如果想并发多个请求,必须使用多个 TCP 链接,且浏览器为了控制资源,还会对单个域名有 6-8个的TCP链接请求限制.因此我们需要做的就是将同域的一些静态资源比如js等,做一个资源合并,将多次请求不同的js文件,合并成单次请求一个合并后的大js文件。这就是webpack等bundle工具的由来。

而从http2开始,实现了TCP链接的多路复用,因此同域名下不再有请求并发数的限制,我们可以同时请求同域名的多个资源,这个并发数可以很大,比如并发10,50,100个请求同时去请求同一个服务下的多个资源。

因为http2实现了多路复用,因此一定程度上,将多个静态文件打包到一起,从而减少请求次数,就不是必须的

主流浏览器对http2的支持情况如下:

Lark20210825-203949

除了IE以外,大部分浏览器对http2的支持性都很好,因为我的项目不需要兼容IE,同时也不需要兼容低版本浏览器,不需要考虑不支持http2的场景。(这也是我们能将不bundle的代码用于线上生产环境的前提之一)

1.2 浏览器esm

对于es modules,我们并不陌生,什么是es modules也不是本文的重点,一些流行的打包构建工具比如babel、webpack等早就支持es modules。

我们来看一个最简单的es modules的写法:

//main.js
import a from 'a.js'
console.log(a)

//a.js
export let  a = 1

上述的es modules就是我们经常在项目中使用的es modules,这种es modules,在支持es6的浏览器中是可以直接使用的。

我们来举一个例子,直接在浏览器中使用es modules

<html  lang="en">
   <body>
       <div id="container">my name is {name}</div>
       <script type="module">
          import Vue from 'https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.esm.browser.js'
          new Vue({
            el: '#container',
            data:{
               name: 'Bob'
            }
          })
       </script>
   </body>
</html>

上述的代码中我们直接可以运行,我们根据script的type="module"可以判断浏览器支不支持es modules,如果不支持,该script里面的内容就不会运行。

首先我们来看主流浏览器对于ES modules的支持情况:

Lark20201119-151747

从上图可以看出来,主流的Edge, Chrome, Safari, and Firefox (+60)等浏览器都已经开始支持es modules。

同样的因为我们的中后台项目不需要强兼容,因此不需要兼容不支持esm的浏览器(这也是我们能将不bundle的代码用于线上生产环境的前提之二)。

1.3 小结

浏览器对于http2和esm的支持,使得我们可以减少模块的合并,以及减少对于js模块化的处理。

  • 如果浏览器支持http2,那么一定程度上,我们不需要合并静态资源

  • 如果浏览器支持esm,那么我们就不需要通过构建工具去维护复杂的模块依赖和加载关系。

这两点正是webpack等打包工具在bundle的时候所做的事情。浏览器对于http2和esm的支持使得我们减少bundle代码的场景。

二、结合snowpack实践

我们比较了snowpack和vite,最后选择采用了snowpack(选型的原因以及snowpack和vite的对比看最后附录),本章节讲讲如何结合snowpack构建工具,构建出不打包形式的线上代码。

2.1 snowpack的基础用法

我们的中后台项目是react和typescript编写的,我们可以直接使用snowpack相应的模版:

npx create-snowpack-app myproject --template @snowpack/app-template-react-typescript

snowpack构建工具内置了tsc,可以处理tsx等后缀的文件。上述就完成了项目初始化。

2.2 前端路由处理

前端路由我们直接使用react-router或者vue-router等,需要注意的时,如果是在开发环境,那么必须要指定在snowpack.config.mjs配置文件,在刷新时让匹配到前端路由:

snowpack.config.mjs
...
 routes: [{ match: 'routes', src: '.*', dest: '/index.html' }],
...

类似的配置跟webpack devserver等一样,使其在后端路由404的时候,获取前端静态文件,从而执行前端路由匹配。

2.3 css、jpg等模块的处理

在snowpack中同样也自带了对css和image等文件的处理。

  • css

以sass为例,

snowpack.config.mjs

plugins: [
    '@snowpack/plugin-sass',
    {
      /* see options below */
    },
  ],

只需要在配置中增加一个sass插件就能让snowpack支持sass文件,此外,snowpack也同样支持css module。.module.css或者.module.scss命名的文件就默认开启了css module。此外,css最后的结果都是通过编译成js模块,通过动态创建style标签,插入到body中的。

//index.module.css文件
.container{
   padding: 20px;
}

snowpack构建处理后的css.proxy.js文件为:

export let code = "._container_24xje_1 {\n  padding: 20px;\n}";
let json = {"container":"_container_24xje_1"};
export default json;

// [snowpack] add styles to the page (skip if no document exists)
if (typeof document !== 'undefined') {
 const styleEl = document.createElement("style");
 const codeEl = document.createTextNode(code);
 styleEl.type = 'text/css';

 styleEl.appendChild(codeEl);
 document.head.appendChild(styleEl);
}

上述的例子中我们可以看到。最后css的构建结果是一段js代码。在body中动态插入了style标签,就可以让原始的css样式在系统中生效。

  • jpg,png,svg等

如果处理的是图片类型,那么snowpack同样会将图片编译成js.

//logo.svg.proxy.js
export default "../dist/assets/logo.svg";

snowpack没有对图片做任何的处理,只是把图片的地址,包含到了一个js模块文件导出地址中。值得注意的是在浏览器es module中,import 动作类似一个get请求,import from可以是一个图片地址,浏览器es module自身可以处理图片等形式。因此在.js文件结尾的模块中,export 的可以是一个图片。

snowpack3.5.0以下的版本在使用css module的时候会丢失hash,需要升级到最新版本。

2.4 按需加载处理

snowpack默认是不打包的。只对每一个文件都做一些简单的模块处理(将非js模块转化成js模块)和语法处理,因此天然支持按需加载,snowpack支持React.lazy的写法,在react的项目中,只要正常使用React.Lazy就能实现按需加载。

2.5 文件hash处理

在最后构建完成后,在发布构建结果的时候,为了处理缓存,常见的就是跟静态文件增加hash,snowpack也提供了插件机制,插件会处理snowpack构建前的所有文件的内容,做为content转入到插件中,经过插件的处理转换后得到新的content.

可以通过snowpack-files-hash插件来实现给文件增加hash。

2.6 公用esm模块托管

snowpack对于项目构建的bundleless的代码可以直接跑在线上,在bundless的构建结果中,我们想进一步减少构建结果文件大小。以bundleless的方式构建的代码,默认在处理三方npm包依赖的时候,虽然不会打包,snowpack对项目中node_modules中的依赖重新编译成esm形式,然后放在一个新的静态目录下。因此最后构建的代码包含了两个部分:

项目本身的代码,将node_modules中的依赖处理成esm后的静态文件

其中node_modules中的依赖处理成esm后的静态文件,可以以cdn或者其他服务形式来托管。这样我们每次都不需要在构建的时候处理node_modules中的依赖。在项目本身的代码中,如果引用了npm包,只需要将其指向一个cdn地址即可。这样处理后的,构建的代码就变成:

只有项目本身的代码(项目中对于三方插件的引入,直接使用三方插件的cdn地址)

进一步想,如果我们使用了托管所有npm包(es module形式)的cdn地址之后,那么在本地开发或者线上构建的过程中,我们甚至不需要去维护本地的node_modules目录,以及yarn-lock或者package-lock文件。我们需要做的,仅仅是一个map文件进行版本管理。保存项目中的npm包名和该包相对应的cdn地址。

比如:

//config.map.json
{
 "react": "https://cdn.skypack.dev/react@17.0.2",
 "react-dom": "https://cdn.skypack.dev/react-dom@17.0.2",
}

通过这个map文件,不管是在开发还是线上,只要把:

import React from 'react'

替换成

import React from "https://cdn.skypack.dev/react@17.0.2"

就能让代码在开发环境或者生产环境中跑起来。如此简化之后,我们不论在开发环境还是生产环境都不需要在本地维护node_modules相关的文件,进一步可以减少打包时间。同时包管理也更加清晰,仅仅是一个简单的json文件,一对固定意义的key/value,简单纯粹

我们提到了一个托管了的npm包的有es module形式的cdn服务,上述以skypack为例,这对比托管了npm包cjs形式的cdn服务unpkg,两者的区别就是,unpkg所托管的npm包,大部分是cjs形式的,cjs形式的npm包,是不能直接用于浏览器的esm中的。skypack所做的事情就是将大部分npm包从cjs形式转化成esm的形式,然后存储和托管esm形式的结果。

三、snowpack的Streaming Imports

在2.7中我们提到了在dev开发环境使用了skypack,那么本地不需要node_modules,甚至不需要yarn-lock和package-lock等文件,只需要一个json文件,简单的、纯粹的,只有一对固定意义的key/value。在snowpack3.x就提供了这么一个功能,称之为Streaming Imports。

3.1 snowpack和skypack

在snowpack3.x在dev环境支持skypack:

// snowpack.config.mjs
export default {
 packageOptions: {
   source: 'remote',
},
};

如此,在dev的webserver过程中,就是直接下载skypack中相应的esm形式的npm包,放在最后的结果中,而不需要在本地做一个cjs到esm的转换。这样做有几点好处:

  • 速度快: 不需要npm install一个npm包,然后在对其进行build转化成esm,Streaming Imports可以直接从一个cdn地址直接下载esm形式的依赖

  • 安全:业务代码中不需要处理公共npm包cjs到esm的转化,业务代码和三方依赖分离,三方依赖交给skypack处理

3.2 依赖控制

Streaming Imports自身也实现了一套简单的依赖管理,有点类似go mod。是通过一个叫snowpack.deps.json文件来实现的。跟我们在2.7中提到的一样,如果使用托管cdn,那么本地的pack-lock和yarn-lock,甚至node_modules是不需要存在的,只需要一个简单纯粹的json文件,而snowpack中就是通过snowpack.deps.json来实现包的依赖管理的。

我们安装一个npm包时,我们以安装ramda为例:

npx snowpack ramda

在snowpack.deps.json中会生成:

{
 "dependencies": {
   "ramda": "^0.27.1",
},
 "lock": {
   "ramda#^0.27.1": "ramda@v0.27.1-3ePaNsppsnXYRcaNcaWn",
}
}

安装过程的命令行如下所示:

飞书20210831-211844

从上图可以看出来,通过npx snowpack安装的依赖是从skypack cdn直接请求的。

特别的,如果项目需要支持typescript,那么我们需要将相应的npm包的声明文件types下载到本地,skypack同样也支持声明文件的下载,只需要在snowpack的配置文件中增加:

// snowpack.config.mjs
export default {
 packageOptions: {
   source: 'remote',
   types:true //增加type=true
},
};

snowpack会把types文件下载到本地的.snowpack目录下,因此在tsc编译的时候需要指定types的查找路径,在tsconfig.json中增加:

//tsconfig.json
"paths": {
     "*":[".snowpack/types/*"]
  },

3.3 build环境

snowpack的Streaming Imports,在dev可以正常工作,dev的webserver中在请求npm包的时候会将请求代理到skypack,但是在build环境的时候,还是需要其他处理的,在我们的项目中,在build的时候可以用一个插件snowpack-plugin-skypack-replacer,将build后的代码引入npm包的时候,指向skypack。

build后的线上代码举例如下:

import * as __SNOWPACK_ENV__ from '../_snowpack/env.271340c8a413.js';
import.meta.env = __SNOWPACK_ENV__;

import ReactDOM from "https://cdn.skypack.dev/react-dom@^17.0.2";
import App from "./App.e1841499eb35.js";
import React from "https://cdn.skypack.dev/react@^17.0.2";
import "./index.css.proxy.9c7da16f4b6e.js";

const start = async () => {
 await ReactDOM.render(/* @__PURE__ */ React.createElement(App, null), document.getElementById("root"));
};
start();
if (undefined /* [snowpack] import.meta.hot */ ) {
 undefined /* [snowpack] import.meta.hot */ .accept();
}

从上述可以看出,build之后的代码,通过插件将:

import React from 'react'
//替换成了
import React from "https://cdn.skypack.dev/react@^17.0.2";

四、性能比较

4.1 lighthouse对比

简单的使用lighthouse来对比bundleless和bundle两种不同构建方式网页的性能。

  • bundleless的前端简单性能测试:

img

  • bundle的前端性能测试:

img

对比发现,这里两个网站都是同一套代码,相同的部署环境,一套是构建的时候是bundleless,利用浏览器的esm,另一个是传统的bundle模式,发现性能上并没有明显的区别,至少bundleless简单的性能测试方面没有明显差距。

4.2构建时间对比

bundleless构建用于线上,主要是减少了构建的时间,我们传统的bundle的代码,一次编译打包等可能需要几分钟甚至十几分钟。在我的项目中,bundleless的构建只需要4秒。

飞书20210901-165311

同一个项目,用webpack构建bundle的情况下需要60秒左右。

4.3构建产物体积对比

bundleless构建出的产物,一般来说也只有bundle情况下的1/10.这里不一一举例。

五、总结

在没有强兼容性的场景,特别是中后台系统,bundleless的代码直接跑在线上,是一种可以尝试的方案,上线的时间会缩短90%,不过也有一些问题需要解决,首先需要保证托管esm资源的CDN服务的稳定性,且要保障被托管的esm资源在浏览器运行不会出现异常。我们运行了一些常见的npm包,发现并没有异常情况,不过后续需要更多的测试。

六、附录:snowpack和vite的对比

6.1 相同点

snowpack和vite都是bundleless的构建工具,都利用了浏览器的es module来减少对静态文件的打包,从而减少热更新的时间,从而提高开发体验。原理都是将本地安装的依赖重新编译成esm形式,然后放在本地服务的静态目录下。snowpack和vite有很多相似点

  • 在dev环境都将本地的依赖进行二次处理,对于本地node_module目录下的npm包,通过其他构建工具转换成esm。然后将所有转换后的esm文件放在本地服务的静态目录下

  • 都支持css、png等等静态文件,不需要安装其他插件。特别对于css,都默认支持css module

  • 默认都支持jsx,tsx,ts等扩展名的文件

  • 框架无关,都支持react、vue等主流前端框架,不过vite对于vue的支持性是最好的。

6.2 不同点

dev构建: snowpack和vite其实大同小异,在dev环境都可以将本地node_modules中npm包,通过esinstall等编译到本地server的静态目录。不同的是在dev环境

  • snowpack是通过rollup来将node_modules的包,重新进行esm形式的编译

  • vite则是通过esbuild来将node_modules的包,重新进行esm形式的编译

因此dev开发环境来看,vite的速度要相对快一些,因为一个npm包只会重新编译一次,因此dev环境速度影响不大,只是在初始化项目冷启动的时候时间有一些误差,此外snowpack支持Streaming Imports,可以在dev环境直接用托管在cdn上的esm形式的npm包,因此dev环境性能差别不大。

build构建:

在生产环境build的时候,vite是不支持unbundle的,在bundle模式下,vite选择采用的是rollup,通过rollup来打包出线上环境运行的静态文件。vite官方支持且仅支持rollup,这样一定程度上可以保持一致性,但是不容易解耦,从而结合非rollup构建工具来打包。而snowpack默认就是unbundle的,这种unbundle的默认形式,对构建工具就没有要求,对于线上环境,即可以使用rollup,也可以使用webpack,甚至可以选择不打包,直接使用unbundle。

可以用两个表格来总结如上的结论:

dev开发环境:

产品dev环境构建工具
snowpackrollup(或者使用Streaming imports)
viteesbuild

build生产环境:

产品build构建工具
snowpack1.unbundle(esbuild) 2.rollup 3.webpack...
viterollup(且不支持unbundle)

6.3 snowpack支持Streaming Imports

Streaming Imports是一个新特性,他允许用户,不管是生产环境还是开发环境,都不需要在本地使用npm/yarn来维护一个lock文件,从而下载应用中所使用的npm包到本地的node_module目录下。通过使用Streaming Imports,可以维护一个map文件,该map文件中的key是包名,value直接指向托管该npm包esm文件形式的cdn服务器的地址。

6.4 vite的一些优点

vite相对于snowpack有一下几个优点,不过个人以为都不算特别有用的一些优点。

  • 多页面支持,除了根目录的root/index.html外,还支持其他根目录以外的页面,比如nest/index.html

  • 对于css预处理器支持更好(这点个人没发现)

  • 支持css代码的code-splitting

  • 优化了异步请求多个chunk文件(不分场景可以同步进行,从而一定程度下减少请求总时间)

6.5 总结

如果想在生产环境使用unbundle,那么vite是不行的,vite对于线上环境的build,是必须要打包的。vite优化的只是开发环境。而snowpack默认就是unbundle的,因此可以作为前提在生产环境使用unbundle.此外,snowpack的Streaming Imports提供了一套完整的本地map的包管理,不需要将npm包安装到本地,很方便我们在线上和线下使用cdn托管公共库。


作者:yuxiaoliang
来源:https://juejin.cn/post/7034484346874986533

收起阅读 »

Android 手把手带你搭建一个组件化项目架构

🔥 一、组件化作为一个单工程撸到底的开发人员,想试着将项目进行组件化改造,说动就动。毕竟技术都是写出来的,看着文章感觉懂了,但是实际开发中还是能遇到各种各样的问题,开始搞起来。💥 1.1 为什么使用组件化一直使用单工程撸到底,项目越来越大导致出现了不少的问题:...
继续阅读 »



🔥 一、组件化

作为一个单工程撸到底的开发人员,想试着将项目进行组件化改造,说动就动。毕竟技术都是写出来的,看着文章感觉懂了,但是实际开发中还是能遇到各种各样的问题,开始搞起来。

💥 1.1 为什么使用组件化

一直使用单工程撸到底,项目越来越大导致出现了不少的问题:

  • 查找问题慢:定位问题,需要在多个代码混合的模块中寻找和跳转。

  • 开发维护成本增加:避免代码的改动影响其它业务的功能,导致开发和维护成本不断增加。

  • 编译时间长:项目工程越大,编译完整代码所花费的时间越长。

  • 开发效率低:多人协作开发时,开发风格不一,又很难将业务完全分割,大家互相影响,导致开发效率低下。

  • 代码复用性差:写过的代码很难抽离出来再次利用。

💥 1.2 模块化与组件化

🌀 1.2.1 模块

一个程序按照其功能做拆分,分成相互独立的模块,以便于每个模块只包含与其功能相关的内容,比如登录模块首页模块等等。

🌀 1.2.2 组件

组件指的是单一的功能组件,如登录组件视频组件支付组件 等,每个组件都可以以一个单独的 module 开发,并且可以单独抽出来作为 SDK 对外发布使用。可以说往往一个模块包含了一个或多个组件。

💥 1.3 组件化的优势

组件化基于可重用的目的,将应用拆分成多个独立组件,以减少耦合:

  • 加快编译速度:每个业务功能都是一个单独的工程,可独立编译运行,拆分后代码量较少,编译自然变快。

  • 解耦:通过关注点分离的形式,将App分离成多个模块,每个模块都是一个组件。

  • 提高开发效率:多人开发中,每个组件模块由单人负责,降低了开发之间沟通的成本,减少因代码风格不一而产生的相互影响。

  • 代码复用:类似我们引用的第三方库,可以将基础组件或功能组件剥离。在新项目微调或直接使用。

💥 1.4 组件化需要解决的问题

  • 组件分层:怎么将一个项目分成多个组件、组件间的依赖关系是怎么样的?

  • 组件单独运行和集成调试:组件是如何独立运行和集成调试的?

  • 组件间通信:主项目与组件、组件与组件之间如何通信就变成关键?

🔥 二、组件分层

组件依赖关系是上层依赖下层,修改频率是上层高于下层。先上一张图:

img

💥 2.1 基础组件

基础公共模块,最底层的库:

  • 封装公用的基础组件;

  • 网络访问框架、图片加载框架等主流的第三方库;

  • 各种第三方SDK。

💥 2.2 common组件(lib_common)

  • 支撑业务组件、功能组件的基础(BaseActivity/BaseFragment等基础能力;

  • 依赖基础组件层;

  • 业务组件、功能组件所需的基础能力只需要依赖common组件即可获得。

💥 2.3 功能组件

  • 依赖基础组件层;

  • 对一些公用的功能业务进行封装与实现;

  • 业务组件可以在library和application之间切换,但是最后打包时必须是library ;

💥 2.4 业务组件

  • 可直接依赖基础组件层;同时也能依赖公用的一些功能组件;

  • 各组件之间不存在依赖关系,通过路由进行通信;

  • 业务组件可以在library和application之间切换,但是最后打包时必须是library ;

💥 2.5 主工程(app)

  • 只依赖各业务组件;

  • 除了一些全局的配置和主Activity之外,不包含任何业务代码,是应用的入口;

💥 2.6 完成后项目

img

这只是个大概,并不是说必须这样,可以按照自己的方式来。比如:你觉得基础组件比较多导致project里面的项目太多,那么你可以创建一个lib_base,然在lib_base里面再创建其他基础组件即可。

🔥 三、组件单独调试

💥 3.1 创建组件(收藏)

img

  • library和application之间切换:选择第一项。

  • 始终是library:选择第二项

这样尽可能的减少变动项,当然这仅仅是个建议,看个人习惯吧。

因为咱们创建的是一个module,所以在AndridManifest中添加android:exported="true"属性可直接构建一个APK。下面咱们看看如何生成不同的工程类型。

💥 3.2 动态配置组件的工程类型

在 AndroidStudio 开发 Android 项目时,使用的是 Gradle 来构建,具体来说使用的是 Android Gradle 插件来构建,Android Gradle 中提供了三种插件,在开发中可以通过配置不同的插件来构建不同的工程。

🌀 3.2.1 build.gradle(module)

//构建后输出一个 APK 安装包
apply plugin: 'com.android.application'
//构建后输出 ARR 包
apply plugin: 'com.android.library'
//配置一个 Android Test 工程
apply plugin: 'com.android.test'

独立调试:设置为 Application 插件。

集成调试:设置为 Library 插件。

🌀 3.2.2 设置gradle.properties

img

isDebug = true 独立调试

🌀 3.2.3 动态配制插件(build.gradle)

//注意gradle.properties中的数据类型都是String类型,使用其他数据类型需要自行转换
if(isDebug.toBoolean()){
   //构建后输出一个 APK 安装包
   apply plugin: 'com.android.application'
}else{
   //构建后输出 ARR 包
   apply plugin: 'com.android.library'
}

💥 3.3 动态配置组件的 ApplicationId 和 AndroidManifest 文件

  • 一个 APP 是只有一个 ApplicationId ,所以在单独调试集成调试组件的 ApplicationId 应该是不同的。

  • 单独调试时也是需要有一个启动页,当集成调试时主工程和组件的AndroidManifest文件合并会产生多个启动页。

根据上面动态配制插件的经验,我们也需要在build.gradle中动态配制ApplicationId 和 AndroidManifest 文件。

🌀 3.3.1 准备两个不同路径的 AndroidManifest 文件

img

有什么不同?咱们一起看看具体内容。

🌀 3.3.2 src/main/debug/AndroidManifest

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
   package="com.scc.module.collect">

   <application
       android:allowBackup="true"
       android:icon="@mipmap/ic_launcher"
       android:label="@string/app_name"
       android:roundIcon="@mipmap/ic_launcher_round"
       android:supportsRtl="true"
       android:theme="@style/Theme.SccMall">
       <activity android:name=".CollectActivity"
           android:exported="true">
           <intent-filter>
               <action android:name="android.intent.action.MAIN" />

               <category android:name="android.intent.category.LAUNCHER" />
           </intent-filter>
       </activity>
   </application>

</manifest>

🌀 3.3.3 src/main/debug/AndroidManifest

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
   package="com.scc.module.collect">
   <application
       android:allowBackup="true"
       android:supportsRtl="true"
       >
       <activity android:name=".CollectActivity"/>
   </application>

</manifest>

🌀 3.3.4 动态配制(build.gradle)

defaultConfig {
   if(isDebug.toBoolean()){
       //独立调试的时候才能设置applicationId
       applicationId "com.scc.module.collect"
  }
}
sourceSets {
   main {
       if (isDebug.toBoolean()) {
           //独立调试
           manifest.srcFile 'src/main/debug/AndroidManifest.xml'
      } else {
           //集成调试
           manifest.srcFile 'src/main/AndroidManifest.xml'
      }
  }
}

💥 3.4 实现效果

🌀 3.4.1 独立调试

isDebug = true

img

🌀 3.4.2 集成调试

isDebug = false

img

🔥 四、Gradle配置统一管理

💥 4.1 config.gradle

当我们需要进行插件版本、依赖库版本升级时,项目多的话改起来很麻烦,这时就需要我们对Gradle配置统一管理。如下:

img

具体内容

ext{
   //组件独立调试开关, 每次更改值后要同步工程
   isDebug = true
   android = [
           // 编译 SDK 版本
           compileSdkVersion: 31,
           // 最低兼容 Android 版本
           minSdkVersion   : 21,
           // 最高兼容 Android 版本
           targetSdkVersion : 31,
           // 当前版本编号
           versionCode     : 1,
           // 当前版本信息
           versionName     : "1.0.0"
  ]
   applicationid = [
           app:"com.scc.sccmall",
           main:"com.scc.module.main",
           webview:"com.scc.module.webview",
           login:"com.scc.module.login",
           collect:"com.scc.module.collect"
  ]
   dependencies = [
           "appcompat"         :'androidx.appcompat:appcompat:1.2.0',
           "material"         :'com.google.android.material:material:1.3.0',
           "constraintlayout" :'androidx.constraintlayout:constraintlayout:2.0.1',
           "livedata"         :'androidx.lifecycle:lifecycle-livedata:2.4.0',
           "viewmodel"         :'androidx.lifecycle:lifecycle-viewmodel:2.4.0',
           "legacyv4"         :'androidx.legacy:legacy-support-v4:1.0.0',
           "splashscreen"     :'androidx.core:core-splashscreen:1.0.0-alpha01'
  ]
   libARouter= 'com.alibaba:arouter-api:1.5.2'
   libARouterCompiler = 'com.alibaba:arouter-compiler:1.5.2'
   libGson = 'com.google.code.gson:gson:2.8.9'
}

💥 4.2 添加配制文件build.gradle(project)

apply from:"config.gradle"

💥 4.3 其他组件使用

//build.gradle
//注意gradle.properties中的数据类型都是String类型,使用其他数据类型需要自行转换
if(isDebug.toBoolean()){
   //构建后输出一个 APK 安装包
   apply plugin: 'com.android.application'
}else{
   //构建后输出 ARR 包
   apply plugin: 'com.android.library'
}
android {
   compileSdkVersion 31

   defaultConfig {
       if(isDebug.toBoolean()){
           //独立调试的时候才能设置applicationId
           applicationId "com.scc.module.collect"
      }
       minSdkVersion 21
       targetSdkVersion 31
       versionCode 1
       versionName "1.0"

       testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
  }

   buildTypes {
       release {
           minifyEnabled false
           proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
      }
  }
   sourceSets {
       main {
           if (isDebug.toBoolean()) {
               //独立调试
               manifest.srcFile 'src/main/debug/AndroidManifest.xml'
          } else {
               //集成调试
               manifest.srcFile 'src/main/AndroidManifest.xml'
          }
      }
  }
   compileOptions {
       sourceCompatibility JavaVersion.VERSION_1_8
       targetCompatibility JavaVersion.VERSION_1_8
  }
}

dependencies {
//   implementation root.dependencies.appcompat
//   implementation root.dependencies.material
//   implementation root.dependencies.constraintlayout
//   implementation root.dependencies.livedata
//   implementation root.dependencies.viewmodel
//   implementation root.dependencies.legacyv4
//   implementation root.dependencies.splashscreen
//   implementation root.libARouter
   //上面内容在lib_common中已经添加咱们直接依赖lib_common
   implementation project(':lib_common')

   testImplementation 'junit:junit:4.+'
   androidTestImplementation 'androidx.test.ext:junit:1.1.2'
   androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}

🔥 五、组件间界面跳转(ARouter)

💥 5.1 介绍

Android 中的界面跳转那是相当简单,但是在组件化开发中,由于不同组件式没有相互依赖的,所以不可以直接访问彼此的类,这时候就没办法通过显式的方式实现了。

所以在这里咱们采取更加灵活的一种方式,使用 Alibaba 开源的 ARouter 来实现。

一个用于帮助 Android App 进行组件化改造的框架 —— 支持模块间的路由、通信、解耦

文档介绍的蛮详细的,感兴趣的可以自己实践一下。这里做个简单的使用。

💥 5.2 使用

🌀 5.2.1 添加依赖

先在统一的config.gradle添加版本等信息

ext{
  ...
   libARouter= 'com.alibaba:arouter-api:1.5.2'
   libARouterCompiler = 'com.alibaba:arouter-compiler:1.5.2'
}

因为所有的功能组件和业务组件都依赖lib_common,那么咱们先从lib_common开始配制

lib_common

dependencies {
   api root.libARouter
  ...
}

其他组件(如collect)

android {
   defaultConfig {
      ...
       javaCompileOptions {
           annotationProcessorOptions {
               arguments = [AROUTER_MODULE_NAME: project.getName()]
               //如果项目内有多个annotationProcessor,则修改为以下设置
               //arguments += [AROUTER_MODULE_NAME: project.getName()]
          }
      }
  }
}

dependencies {
   //arouter-compiler的注解依赖需要所有使用 ARouter 的 module 都添加依赖
   annotationProcessor root.libARouterCompiler
  ...
}

🌀 5.2.2 添加注解

你要跳转的Activity

// 在支持路由的页面上添加注解(必选)
// 这里的路径需要注意的是至少需要有两级,/xx/xx
@Route(path = "/collect/CollectActivity")
public class CollectActivity extends AppCompatActivity {
   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_collect);
  }
}

🌀 5.2.3 初始化SDK(主项目Application)

public class App extends BaseApplication {
   @Override
   public void onCreate() {
       super.onCreate();
       if (isDebug()) {           // 这两行必须写在init之前,否则这些配置在init过程中将无效
           ARouter.openLog();     // 打印日志
           ARouter.openDebug();   // 开启调试模式(如果在InstantRun模式下运行,必须开启调试模式!线上版本需要关闭,否则有安全风险)
      }
       ARouter.init(this); // 尽可能早,推荐在Application中初始化
  }
   private boolean isDebug() {
       return BuildConfig.DEBUG;
  }
}

💥 5.3 发起路由操作

🌀 5.3.1 应用内简单的跳转

ARouter.getInstance().build("/collect/CollectActivity").navigation();

这里是用module_main的HomeFragment跳转至module_collect的CollectActivity界面,两个module中不存在依赖关系。"/collect/CollectActivity"在上面已注册就不多描述了。

效果如下:

img

🌀 5.3.2 跳转并携带参数

这里是用module_main的MineFragment的Adapter跳转至module_webview的WebViewActivity界面,两个module中同样不存在依赖关系。

启动方

ARouter.getInstance().build("/webview/WebViewActivity")
  .withString("url", bean.getUrl())
  .withString("content",bean.getName())
  .navigation();

这里传了两个参数urlname到WebViewActivity,下面咱们看看WebViewActivity怎么接收。

接收方

//为每一个参数声明一个字段,并使用 @Autowired 标注
//URL中不能传递Parcelable类型数据,通过ARouter api可以传递Parcelable对象
//添加注解(必选)
@Route(path = "/webview/WebViewActivity")
public class WebViewActivity extends BaseActivity<ActivityWebviewBinding, WebViewViewModel> {
   //发送方和接收方定义的key名称相同则无需处理
   @Autowired
   public String url;
   //通过name来映射URL中的不同参数
   //发送方定义key为content,我们用title来接收
   @Autowired(name = "content")
   public String title;

   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       //注入参数和服务(这里用到@Autowired所以要设置)
       //不使用自动注入,可不写,如CollectActivity没接收参数就没有设置
       ARouter.getInstance().inject(this);
       binding.btnBoom.setText(String.format("%s,你来啦", title));
       //加载链接
       initWebView(binding.wbAbout, url);
  }
}

上效果图:

搞定,更多高级玩法可自行探索。

🌀 5.3.3 小记(ARouter目标不存在)

W/ARouter::: ARouter::There is no route match the path

这里出现个小问题,配置注释都好好的,但是发送发无论如何都找不到设置好的Activity。尝试方案:

  • Clean Project

  • Rebuild Project

  • 在下图也能找到ARouter内容。

后来修改Activity名称好了。

img

🔥 六、组件间通信(数据传递)

界面跳转搞定了,那么数据传递怎么办,我在module_main中使用悬浮窗,但是需要判断这个用户是否已登录,再执行后续逻辑,这个要怎么办?这里我们可以采用 接口 + ARouter 的方式来解决。

在这里可以添加一个 componentbase 模块,这个模块被所有的组件依赖

这里我们通过 module_main组件 中调用 module_login组件 中的方法来获取登录状态这个场景来演示。

💥 6.1 通过依赖注入解耦:服务管理(一) 暴露服务

🌀 6.1.1 创建 componentbase 模块(lib)

img

🌀 6.1.2 创建接口并继承IProvider

注意:接口必须继承IProvider,是为了使用ARouter的实现注入。

img

🌀 6.1.3 在module_login组件中实现接口

lib_common

所有业务组件和功能组件都依赖lib_common,所以咱们直接在lib_common添加依赖即可

dependencies {
  ...
   api project(":lib_componentbase")
}

module_login

dependencies {
  ...
   implementation project(':lib_common')
}

实现接口

//实现接口
@Route(path = "/login/AccountServiceImpl")
public class AccountServiceImpl implements IAccountService {
   @Override
   public boolean isLogin() {
       MLog.e("AccountServiceImpl.isLogin");
       return true;
  }

   @Override
   public String getAccountId() {
       MLog.e("AccountServiceImpl.getAccountId");
       return "1000";
  }

   @Override
   public void init(Context context) {

  }
}

img

💥 6.2 通过依赖注入解耦:服务管理(二) 发现服务

🌀 6.2.1 在module_main中调用调用是否已登入

public class HomeFragment extends BaseFragment<FragmentHomeBinding> {
   @Autowired
   IAccountService accountService;
   @Override
   public void onViewCreated(@NonNull @NotNull View view, @Nullable @org.jetbrains.annotations.Nullable Bundle savedInstanceState) {
       super.onViewCreated(view, savedInstanceState);
       ARouter.getInstance().inject(this);
       binding.frgmentHomeFab.setOnClickListener(new View.OnClickListener() {
           @Override
           public void onClick(View v) {
               MLog.e("Login:"+accountService.isLogin());
               MLog.e("AccountId:"+accountService.getAccountId());

          }
      });
  }
}

img 运行结果:

E/-SCC-: AccountServiceImpl.isLogin
E/-SCC-: Login:true
E/-SCC-: AccountServiceImpl.getAccountId
E/-SCC-: AccountId:1000

🔥 七、总结

本文介绍了组件化、组件分层、解决了组件的独立调试、集成调试、页面跳转、组件通信等。

其实会了这些后你基本可以搭建自己的组件化项目了。其实最大的问题还是分组分层、组件划分。这个就需要根据你的实际情况来设置。

本项目比较糙,后面会慢慢完善。比如添加Gilde、添加MMVK、添加Room等。

项目传送门

💥 相关推荐

Android OkHttp+Retrofit+Rxjava+Hilt实现网络请求框架

💥 参考与感谢

“终于懂了” 系列:Android组件化,全面掌握!

Android 组件化最佳实践

手把手带你搭建一个优秀的Android项目架构


作者:Android帅次
来源:https://juejin.cn/post/7033954652315975688


收起阅读 »

就业寒冬,从拉勾招聘看Python就业前景

事情的起源是这样的,某个风和日丽的下午... 习惯性的打开知乎准备划下水,看到一个问题刚好邀请回答于是就萌生了采集下某招聘网站Python岗位招聘的信息,看一下目前的薪水和岗位分布,说干就干。Chrome浏览器右键检查查看network,找到链接https:/...
继续阅读 »



1.数据采集

事情的起源是这样的,某个风和日丽的下午... 习惯性的打开知乎准备划下水,看到一个问题刚好邀请回答

img

于是就萌生了采集下某招聘网站Python岗位招聘的信息,看一下目前的薪水和岗位分布,说干就干。

先说下数据采集过程中遇到的问题,首先请求头是一定要伪装的,否则第一步就会给你弹出你的请求太频繁,请稍后再试,其次网站具有多重反爬策略,解决方案是每次先获取session然后更新我们的session进行抓取,最后拿到了想要的数据。

Chrome浏览器右键检查查看network,找到链接https://www.lagou.com/jobs/positionAjax.json?needAddtionalResult=false

img

可以看到返回的数据正是页面的Python招聘详情,于是我直接打开发现直接提示{"status":false,"msg":"您操作太频繁,请稍后再访问","clientIp":"124.77.161.207","state":2402},机智的我察觉到事情并没有那么简单

img

真正的较量才刚刚开始,我们先来分析下请求的报文,

img

img

可以看到请求是以post的方式传递的,同时传递了参数

datas = {
          'first': 'false',
          'pn': x,
          'kd': 'python',
      }

同时不难发现每次点击下一页都会同时发送一条get请求

这里我点了两次,出现两条get请求

img

经过探索,发现这个get请求和我们post请求是一致的,那么问题就简单许多,整理一下思路

img

关键词:python 搜索范围:全国 数据时效:2019.05.05

#!/usr/bin/env python3.4
# encoding: utf-8
"""
Created on 19-5-05
@title: ''
@author: Xusl
"""
import json
import requests
import xlwt
import time

# 获取存储职位信息的json对象,遍历获得公司名、福利待遇、工作地点、学历要求、工作类型、发布时间、职位名称、薪资、工作年限
def get_json(url, datas):
  my_headers = {
      "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.119 Safari/537.36",
      "Referer": "https://www.lagou.com/jobs/list_Python?city=%E5%85%A8%E5%9B%BD&cl=false&fromSearch=true&labelWords=&suginput=",
      "Content-Type": "application/x-www-form-urlencoded;charset = UTF-8"
  }
  time.sleep(5)
  ses = requests.session()   # 获取session
  ses.headers.update(my_headers) # 更新
  ses.get("https://www.lagou.com/jobs/list_python?city=%E5%85%A8%E5%9B%BD&cl=false&fromSearch=true&labelWords=&suginput=")
  content = ses.post(url=url, data=datas)
  result = content.json()
  info = result['content']['positionResult']['result']
  info_list = []
  for job in info:
      information = []
      information.append(job['positionId']) # 岗位对应ID
      information.append(job['city']) # 岗位对应城市
      information.append(job['companyFullName']) # 公司全名
      information.append(job['companyLabelList']) # 福利待遇
      information.append(job['district']) # 工作地点
      information.append(job['education']) # 学历要求
      information.append(job['firstType']) # 工作类型
      information.append(job['formatCreateTime']) # 发布时间
      information.append(job['positionName']) # 职位名称
      information.append(job['salary']) # 薪资
      information.append(job['workYear']) # 工作年限
      info_list.append(information)
      # 将列表对象进行json格式的编码转换,其中indent参数设置缩进值为2
      # print(json.dumps(info_list, ensure_ascii=False, indent=2))
  # print(info_list)
  return info_list

def main():
  page = int(input('请输入你要抓取的页码总数:'))
  # kd = input('请输入你要抓取的职位关键字:')
  # city = input('请输入你要抓取的城市:')

  info_result = []
  title = ['岗位id', '城市', '公司全名', '福利待遇', '工作地点', '学历要求', '工作类型', '发布时间', '职位名称', '薪资', '工作年限']
  info_result.append(title)
  for x in range(1, page+1):
      url = 'https://www.lagou.com/jobs/positionAjax.json?needAddtionalResult=false'
      datas = {
          'first': 'false',
          'pn': x,
          'kd': 'python',
      }
      try:
          info = get_json(url, datas)
          info_result = info_result + info
          print("第%s页正常采集" % x)
      except Exception as msg:
          print("第%s页出现问题" % x)
       
      # 创建workbook,即excel
      workbook = xlwt.Workbook(encoding='utf-8')
      # 创建表,第二参数用于确认同一个cell单元是否可以重设值
      worksheet = workbook.add_sheet('lagouzp', cell_overwrite_ok=True)
      for i, row in enumerate(info_result):
          # print(row)
          for j, col in enumerate(row):
              # print(col)
              worksheet.write(i, j, col)
      workbook.save('lagouzp.xls')

if __name__ == '__main__':
  main()

日志记录

img

当然存储于excel当然是不够的,之前一直用matplotlib做数据可视化,这次换个新东西pyecharts

2.了解pyecharts

pyecharts是一款将python与echarts结合的强大的数据可视化工具,包含多种图表

  • Bar(柱状图/条形图)

  • Bar3D(3D 柱状图)

  • Boxplot(箱形图)

  • EffectScatter(带有涟漪特效动画的散点图)

  • Funnel(漏斗图)

  • Gauge(仪表盘)

  • Geo(地理坐标系)

  • Graph(关系图)

  • HeatMap(热力图)

  • Kline(K线图)

  • Line(折线/面积图)

  • Line3D(3D 折线图)

  • Liquid(水球图)

  • Map(地图)

  • Parallel(平行坐标系)

  • Pie(饼图)

  • Polar(极坐标系)

  • Radar(雷达图)

  • Sankey(桑基图)

  • Scatter(散点图)

  • Scatter3D(3D 散点图)

  • ThemeRiver(主题河流图)

  • WordCloud(词云图)

用户自定义

  • Grid 类:并行显示多张图

  • Overlap 类:结合不同类型图表叠加画在同张图上

  • Page 类:同一网页按顺序展示多图

  • Timeline 类:提供时间线轮播多张图

另外需要注意的是从版本0.3.2 开始,为了缩减项目本身的体积以及维持 pyecharts 项目的轻量化运行,pyecharts 将不再自带地图 js 文件。如用户需要用到地图图表(Geo、Map),可自行安装对应的地图文件包。

  1. 全球国家地图: echarts-countries-pypkg (1.9MB): 世界地图和 213 个国家,包括中国地图

  2. 中国省级地图: echarts-china-provinces-pypkg (730KB):23 个省,5 个自治区

  3. 中国市级地图: echarts-china-cities-pypkg (3.8MB):370 个中国城市

也可以使用命令进行安装

pip install echarts-countries-pypkg
pip install echarts-china-provinces-pypkg
pip install echarts-china-cities-pypkg

3.数据可视化(代码+展示)

  • 各城市招聘数量

from pyecharts import Bar

city_nms_top10 = ['北京', '上海', '深圳', '成都', '杭州', '广州', '武汉', '南京', '苏州', '郑州', '天津', '西安', '东莞', '珠海', '合肥', '厦门', '宁波','南宁', '重庆', '佛山', '大连', '哈尔滨', '长沙', '福州', '中山']
city_nums_top10 = [149, 95, 77, 22, 17, 17, 16, 13, 7, 5, 4, 4, 3, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1]

bar = Bar("Python岗位", "各城市数量")
bar.add("数量", city_nms, city_nums, is_more_utils=True)
# bar.print_echarts_options() # 该行只为了打印配置项,方便调试时使用
bar.render('Python岗位各城市数量.html')  # 生成本地 HTML 文件

img

  • 地图分布展示(这个场景意义不大,不过多分析)

from pyecharts import Geo

city_datas = [('北京', 149), ('上海', 95), ('深圳', 77), ('成都', 22), ('杭州', 17), ('广州', 17), ('武汉', 16), ('南京', 13), ('苏州', 7), ('郑州', 5), ('天津', 4), ('西安', 4), ('东莞', 3), ('珠海', 2), ('合肥', 2), ('厦门', 2), ('宁波', 1), ('南宁', 1), ('重庆', 1), ('佛山', 1), ('大连', 1), ('哈尔滨', 1), ('长沙', 1), ('福州', 1), ('中山', 1)]
geo = Geo("Python岗位城市分布地图", "数据来源拉勾", title_color="#fff",
          title_pos="center", width=1200,
          height=600, background_color='#404a59')
attr, value = geo.cast(city_datas)
geo.add("", attr, value, visual_range=[0, 200], visual_text_color="#fff",symbol_size=15, is_visualmap=True)
geo.render("Python岗位城市分布地图_scatter.html")
geo = Geo("Python岗位城市分布地图", "数据来源拉勾", title_color="#fff",
          title_pos="center", width=1200,
          height=600, background_color='#404a59')
attr, value = geo.cast(city_datas)
geo.add("", attr, value, type="heatmap", visual_range=[0,10],visual_text_color="#fff",symbol_size=15,is_visualmap=True)
geo.render("Python岗位城市分布地图_heatmap.html")

img

img

  • 各个城市招聘情况

from pyecharts import Pie

city_nms_top10 = ['北京', '上海', '深圳', '成都', '广州', '杭州', '武汉', '南京', '苏州', '郑州']
city_nums_top10 = [149, 95, 77, 22, 17, 17, 16, 13, 7, 5]
pie = Pie()
pie.add("", city_nms_top10, city_nums_top10, is_label_show=True)
# pie.show_config()
pie.render('Python岗位各城市分布饼图.html')

img

北上深的岗位明显碾压其它城市,这也反映出为什么越来越多的it从业人员毕业以后相继奔赴一线城市,除了一线城市的薪资高于二三线这个因素外,还有一个最重要的原因供需关系,因为一线岗位多,可选择性也就比较高,反观二三线的局面,很有可能你跳个几次槽,发现同行业能呆的公司都待过了...

  • 薪资范围

    img

由此可见,python的岗位薪资多数在10k~20k,想从事Python行业的可以把工作年限和薪资结合起来参考一下。

  • 学历要求 + 工作年限

    img

从工作年限来看,1-3年或者3-5年工作经验的招聘比较多,而应届生和一年以下的寥寥无几,对实习生实在不太友好,学历也普遍要求本科,多数公司都很重视入职人员学历这点毋容置疑,虽然学历不代表一切,但是对于一个企业来说,想要短时间内判断一个人的能力,最快速有效的方法无疑是从学历入手。学历第一关,面试第二关。

但是,这不代表学历不高的人就没有好的出路,现在的大学生越来越多,找工作也越来越难,竞争越来越激烈,即使具备高学历,也不能保证你一定可以找到满意的工作,天道酬勤,特别是it这个行业,知识的迭代,比其他行业来的更频密。不断学习,拓展自己学习的广度和深度,才是最正确的决定。

就业寒冬来临,我们需要的是理性客观的看待,而不是盲目地悲观或乐观。从以上数据分析,如果爱好Python,仍旧可以入坑,不过要注意一个标签有工作经验,就算没有工作经验,自己在学习Python的过程中一定要尝试独立去做一个完整的项目,爬虫也好,数据分析也好,亦或者是开发,都要尝试独立去做一套系统,在这个过程中培养自己思考和解决问题的能力。持续不断的学习,才是对自己未来最好的投资,也是度过寒冬最正确的姿势。


作者:一只写程序的猿
来源:https://juejin.cn/post/6844903837698883597

收起阅读 »

环信广纳人才,base北京,欢迎大家踊跃跳槽/推荐~~ps:双休不加班

1、高级Android开发工程师:1. 3年及以上Android开发经验,具有成熟Android APP产品开发经验者优先;2. 熟练掌握Android SDK,Java,设计模式,http,多线程编程者优先;3. 有NDK开发经验优先;4. 熟悉Androi...
继续阅读 »

1、高级Android开发工程师:
1. 3年及以上Android开发经验,具有成熟Android APP产品开发经验者优先;
2. 熟练掌握Android SDK,Java,设计模式,http,多线程编程者优先;
3. 有NDK开发经验优先;
4. 熟悉Android Framwork,插件开发,有APP架构设计优先;
5. 有SDK开发经验优先;6.有IM开发经验优先


2、iOS开发工程师:
1. 3年及以上iOS开发经验,具有成熟iOS APP产品开发经验者优先;
2. 熟悉iOS框架以及各种特性,深刻理解常用设计模式, 熟练使用网络、多线程、数据库等客户端开发技术;
3. 扎实的Objective-C或Swift语言基础;
4. 分析问题和解决问题的能力强,有大规模代码的阅读和修改经验者优先;
5. 有较好的学习能力和沟通能力,有创新能力和责任感,对移动端产品有浓厚的兴趣;

3、前端工程师:
1. 计算机及相关专业本科及以上学历,至少3年以上前端开发工作经验;
2. 有丰富的Web前端开发经验,熟悉HTML5开发,浏览器渲染原理,熟悉React框架;
3. 工作认真负责,乐观开朗,有较强的逻辑分析、问题排查能力,善于团队合作;
4. 良好软件工程思想,良好的编程能力和编程习惯;
5. 熟悉HTTP、WebSocket等协议;
6. 有针对海外开发者产品经验的优先考虑;
7. 有SDK开发经验者优先考虑;

4、高级SDK跨平台开发工程师(Flutter/Electron/RN/Unity/Unreal):
1. 熟练使用 Java script/C#/Dart 其中至少一种以上开发语言。
2. 熟悉使用 C++,有多语言混合开发经验。
3. 有过 Android/iOS/Windows/macOS 其中至少一种原生平台应用的开发经验。
4. 使用过跨平台框架,有框架和原生混合开发经验。例如:Electron/ Unity/Flutter 其中的一种或者多种。
5. 有即时通讯相关的开发经验属于加分项。
6. 有跨平台框架的插件,中间件或者 SDK 开发经验属于加分项。
7. 本科及以上学历, 有两年以上的工作经验。

5、中高级后台工程师(Erlang/Go/C++):
1. 3年以上软件工程师工作经验,有Erlang,Go,C++经验或感兴趣优先;
2. 大型通讯软件,通讯协议开发经验优先;
3. 计算机科学、自动化、通讯等相关专业本科以上学历;
4. 熟悉TCP/IP,HTTP、WebSocket协议;
5. 熟悉SQL、Kafka、Redis;6.熟悉Linux操作系统;

6、中高级Java工程师:
1. 3年以上大型互联网分布式产品或网络软件设计经验;
2. 强大的需求分析能力与编码能力;
3. 精通Java语言,精通异步编程、多线程编程;
4. 精通Spring、Spring Boot、Sp;

7、产品经理:
1. 5年以上to B产品规划与设计经验,3年及以上互联网产品工作经验,计算机或相关专业本科以上学历,有IaaS、Paas、中台方向相关经验者优先;
2. 具备良好的需求分析和产品设计能力,熟练使用Axure/Sketch等产品原型工具;
3. 具备丰富的PaaS平台相关经验,理解PaaS、SaaS等架构逻辑,对业界领先的云平台有一定研究;
4. 对产品的发展趋势有敏锐的洞察力和创新意识,重视细节与用户体验,对用户使用流程、交互流程敏感;
5. 2年以上B端产品经验;6.有研发经验可以作为加分项;

8、初级测试和高级自动化测试工程师:
1. 计算机本科及以上学历;
2. 2年以上自动化测试经验;
3. 熟悉mysql或者相关数据库,能熟练编写sql脚本优先;
4. 熟悉Java\\Python和Linux操作系统;
5. 熟悉自动化测试,熟悉robot framework\\Appium\\Selenium等测试框架,有多个大型实际项目自动化测试经验者优先;
6. 良好的表达沟通能力、细致、责任心、团队精神;7.具备追求卓越的质量观念,并有志成为测试领域的高端人才。

9.运维工程师:
1. 有3年以上互联网系统运维工作经验。
2. 熟悉Linux操作系统的管理维护(部署、配置和调优),熟练使用Linux Shell或Python或者golang编程语言。
3. 熟悉Docker以及相关容器技术,使用过docker-compose、mesos 等容器编排工具,进行过开发维护经验者优先。
4. 熟悉Prometheus等常用监控软件。
5. 熟练使用nginx, redis, kafka 等常用软件,并进行调优。
6. 熟悉TiDB或者CockroachDB优先。
7. 热爱运维工作,能承受压力,具备较强的问题分析和解决能力。
8. 积极主动、责任心强,良好的沟通能力和团队协作精神。


10、高级产品市场经理
岗位职责:
1.负责环信即时通讯云的价值挖掘、内容组织并推动将产品及价值传递给用户,主要产出物为:产品技术及解决方案干货内容;客户案例;产品彩页;产品PPT;市场软文;市场活动演讲PPT;官网内容;市场活动产品资料等
2.行业研究,分析行业及竞争对手动向,为产品团队提出产品定位建议
3.做为公司产品对外接口, 不定期参加公司对外直播公开课演讲以及行业会议演讲


任职资格:
1.有技术背景,懂得如何与研发团队、销售及用户沟通和协作;
2.熟悉通讯云领域SaaS、PaaS产品,理解并挖掘相关产品核心价值;
3.沟通能力强,写作能力强;
4.有IM通讯云领域市场分析,行业分析,顾问咨询经验者优先;
5.有产品经理或项目经理经验者优先;
6.本科以上学历;
7.5年以上工作经验;

--------------------------------

以上岗位如有意向,可私信我,咱们内推搞起来!

成功推荐朋友入职可获奖励5000~20000元,欢迎大家踊跃推荐!

收起阅读 »

iOS集成

IM 和 客服 并存开发指南—iOS篇 ...
继续阅读 »




IM 和 客服 并存开发指南—iOS篇











 如果觉得哪里描述的不清晰,可评论内指出,会不定期更新。


 一、SDK 介绍

      HelpDesk.framework 为 客服SDK(带实时音视频)

      HelpDeskLite.framework 为 客服SDK(不带实时音视频)

      Hyphenate.framework 为 IM SDK(带实时音视频)

      HyphenateLite.framework 为 IM SDK(不带实时音视频)

      环信客服SDK 基于 IM SDK 3.x , 如果同时集成 客服 和 IM,只需要在初始化、登录、登出操作时使用客服SDK 提供的相应API,IM 的其他API均不受影响。

      UI 部分集成需要分别导入 HelpDeskUI 和 IM demo 中的UI文件(也可以自定义UI)。 下面详细介绍IM 和 客服共存的开发步骤。

二、注意事项

      1、开发过程中,初始化、登录和登出,务必只使用客服访客端SDK的API。

      2、需要联系商务开通客服长连接。

           不开通长连接,会出现用户长时间(一天或几天)不使用app,再打开app会无法正常使用im相关功能的问题,报错信息一般是User is not login。

      3、IM SDK 和客服SDK 都包括了模拟器的CPU 架构,在上传到app store时需要剔除模拟器的CPU 架构,保留  armv7、arm64,参考文档:上传appstore以及打包ipa注意事项。 

三、资源准备

      到环信官网下载客服访客端的开源的商城Demo源码 + SDK,下载链接:http://www.easemob.com/download/cs  选  择“iOS SDK”下载(如下图)。

      

下载客服.png



      到环信官网下载IM的开源的Demo源码 + SDK ,下载链接:http://www.easemob.com/download/im 选择 iOS SDK(如下图)。

      

下载IM.png




下载的 IM SDK+Demo 和 客服SDK+Demo 中都有 IM 的
Hyphenate.framework 或 HyphenateLite.framework,为了保持版本的匹配,我们只使用 IM Demo 中的
UI, 而不使用 IM SDK 中 的 Hyphenate.framework 或 HyphenateLite.framework 文件。

四、集成步骤

      1、阅读客服访客端SDK集成文档,集成客服,地址:http://docs.easemob.com/cs/300visitoraccess/iossdk。 

      2、阅读 IM 的集成文档,地址:http://docs-im.easemob.com/im/ios/sdk/prepare 

      3、将 IM Demo 中的 UI 文件按照自己的需求分模块导入到工程中

      4、将 IM 的 UI 所依赖的第三方库集成到项目中(IM集成文档内有说明)

      5、在pch文件中引入 EMHeaders.h 

          #ifdef __OBJC__ 

            //包含实时音视频功能 

            #import  

            // 若不包含实时音视频,则替换为 

            // #import  

            #import "HelpDeskUI.h" 

            #import "EMHeaders.h" 

         #endif

      6、由于HelpDeskUI 和 IM UI 中都使用了 第三方库,如果工程中出现三方库重复的问题,可将重复文件删除,如果部分接口已经升级或弃用可自行升级、调整。

提供的兼容Demo介绍:

     1、Demo集成了初始化sdk、登录、退出登录、IM单聊、联系客服的简单功能,处理了第三方库冲突的问题。

     2、pch文件中的appkey等信息需要换成开发者自己的。

     3、Demo源码下载地址: https://pan.baidu.com/s/1v1TUl-fqJNLQrtsJfWYGzw 

         提取码: kukb 
收起阅读 »