注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

CSS mask 实现鼠标跟随镂空效果

偶然在某思看到这样一个问题,如何使一个div的部分区域变透明而其他部分模糊掉?,最后实现效果是这样的 进一步,还能实现任意形状的镂空效果 鼠标经过的地方清晰可见,其他地方则是模糊的。 可能一开始无从下手,不要急,可以先从简单的、类似的效果开始,一步一步尝试...
继续阅读 »

偶然在某思看到这样一个问题,如何使一个div的部分区域变透明而其他部分模糊掉?,最后实现效果是这样的


237330258-6181fcdb471cf


进一步,还能实现任意形状的镂空效果


Kapture 2021-11-20 at 13.44.26


鼠标经过的地方清晰可见,其他地方则是模糊的。


可能一开始无从下手,不要急,可以先从简单的、类似的效果开始,一步一步尝试,一起看看吧。


一、普通半透明的效果


比如平时开发中碰到更多的可能是一个半透明的效果,有点类似于探照灯(鼠标外面的地方是半透明遮罩,看起来会暗一点)。如下:


image-20211117200548416


那先从这种效果开始吧,假设有这样一个布局:


<div class="wrap" id="img">
<img class="prew" src="https://tva1.sinaimg.cn/large/008i3skNgy1gubr2sbyqdj60xa0m6tey02.jpg">
</div>

那么如何绘制一个镂空的圆呢?先介绍一种方法


其实很简单,只需要一个足够大的投影就可以了,原理如下


image-20211117195737723


这里可以用伪元素::before来绘制,结构更加精简。用代码实现就是


.wrap::before{
content:'';
position: absolute;
width: 100px;
height: 100px;
border-radius: 50%;
left: 50%;
top: 50%;
transform: translate(-50%,-50%); /*默认居中*/
box-shadow: 0 0 0 999vw rgba(0, 0, 0, .5); /*足够大的投影*/
}

可以得到这样的效果


image-20211117200548416


二、借助 CSS 变量传递鼠标位置


按照以往的经验,可能会在 js 中直接修改元素的 style 属性,类似这样


img.addEventListener('mousemove', (ev) => {
img.style.left = '...';
img.style.top = '...';
})

但是这样交互与业务逻辑混杂在一起,不利于后期维护。其实,我们只需要鼠标的坐标,在 CSS 中也能完全实现跟随的效果。


这里借助 CSS 变量,那一切就好办了!假设鼠标的坐标是 [--x,--y](范围是[0, 1]),那么遮罩的坐标就可以使用 calc计算了


.wrap::before{
left: calc(var(--x) * 100%);
top: calc(var(--y) * 100%);
}

然后鼠标坐标的获取可以使用 JS 来计算,也比较容易,如下


img.addEventListener('mousemove', (ev) => {
img.style.setProperty('--x', ev.offsetX / ev.target.offsetWidth);
img.style.setProperty('--y', ev.offsetY / ev.target.offsetHeight);
})

这样,半透明效果的镂空效果就完成了


Kapture 2021-11-17 at 20.26.27


完整代码可以访问: backdrop-shadow (codepen.io)


三、渐变也能实现半透明的效果


除了上述阴影扩展的方式,CSS 径向渐变也能实现这样的效果


绘制一个从透明到半透明的渐变,如下


.wrap::before{
content: '';
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
background: radial-gradient( circle at center, transparent 50px, rgba(0,0,0,.5) 51px);
}

可以得到这样的效果


image-20211117200548416


然后,把鼠标坐标映射上去就可以了。从这里就可以看出 CSS 变量的好处,无需修改 JS,只需要在CSS中修改渐变中心点的位置就可以实现了


.wrap::before{
background: radial-gradient( circle at calc(var(--x) * 100% ) calc(var(--y) * 100% ), transparent 50px, rgba(0,0,0,.5) 51px);
}

Kapture 2021-11-18 at 19.51.30


四、背景模糊的效果尝试


CSS 中有一个专门针对背景(元素后面区域)的属性:backdrop-filter。使用方式和 filter完全一致!


backdrop-filter: blur(10px);

下面是 MDN 中的一个示意效果


image-20211119191341911


backdrop-filter是让当前元素所在区域后面的内容模糊,要想看到效果,需要元素本身半透明或者完全透明;而filter是让当前元素自身模糊。有兴趣的可以查看这篇文章: CSS backdrop-filter简介与苹果iOS毛玻璃效果 « 张鑫旭-鑫空间-鑫生活 (zhangxinxu.com)


需要注意的是,这种模糊与背景的半透明度没有任何关系,哪怕元素本身是透明的,仍然会有效果。例如下面是去除背景后的效果 ,整块都是模糊的


image-20211119193956128


如果直接运用到上面的例子会怎么样呢?


1. 阴影实现


在上面第一个例子中添加 backdrop-filter


.wrap::before{
content:'';
position: absolute;
width: 100px;
height: 100px;
border-radius: 50%;
left: 50%;
top: 50%;
transform: translate(-50%,-50%); /*默认居中*/
box-shadow: 0 0 0 999vw rgba(0, 0, 0, .5); /*足够大的投影*/
backdrop-filter: blur(5px)
}

得到效果如下


Kapture 2021-11-19 at 19.20.57


可以看到圆形区域是模糊的,正好和希望的效果相反。其实也好理解,只有圆形区域才是真实的结构,外面都是阴影,所以最后作用的范围也只有圆形部分


2. 渐变实现


现在在第二个例子中添加 backdrop-filter


.wrap::before{
content: '';
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
background: radial-gradient( circle at calc(var(--x) * 100% ) calc(var(--y) * 100% ), transparent 50px, rgba(0,0,0,.5) 51px);
backdrop-filter: blur(5px)
}

效果如下


Kapture 2021-11-19 at 19.31.22


已经全部都模糊了,只是圆形区域外暗一些。由于::before的尺寸占据整个容器,所以整个背后都变模糊了,圆形外部比较暗是因为半透明渐变的影响。


总之还是不能满足我们的需求,需要寻求新的解决方式。


五、CSS MASK 实现镂空


与其说是让圆形区域不模糊,还不如说是把那块区域给镂空了。就好比之前是一整块磨砂玻璃,然后通过 CSS MASK 打了一个圆孔,这样透过圆孔看到后面肯定是清晰的。


可以对第二个例子稍作修改,通过径向渐变绘制一个透明圆,剩余部分都是纯色的遮罩层,示意如下


image-20211120113029155


用代码实现就是


.wrap::before{
content: '';
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
-webkit-mask: radial-gradient( circle at calc(var(--x, .5) * 100% ) calc(var(--y, .5) * 100% ), transparent 50px, #000 51px);
background: rgba(0,0,0,.3);
backdrop-filter: blur(5px)
}

这样就实现了文章开头的效果


237330258-6181fcdb471cf


完整代码可以查看:backdrop-mask (codepen.io)


六、CSS MASK COMPOSITE 实现更丰富的镂空效果


除了使用径向渐变绘制遮罩层以外,还可以通过 CSS MASK COMPOSITE(遮罩合成)的方式来实现。标准关键值如下(firefox支持):


/* Keyword values */
mask-composite: add; /* 叠加(默认) */
mask-composite: subtract; /* 减去,排除掉上层的区域 */
mask-composite: intersect; /* 相交,只显示重合的地方 */
mask-composite: exclude; /* 排除,只显示不重合的地方 */

遮罩合成是什么意思呢?可以类比 photoshop 中的形状合成,几乎是一一对应的


image-20211120123004278


-webkit-mask-composite 与标准下的值有所不同,属性值非常多,如下(chorme 、safari 支持)


-webkit-mask-composite: clear; /*清除,不显示任何遮罩*/
-webkit-mask-composite: copy; /*只显示上方遮罩,不显示下方遮罩*/
-webkit-mask-composite: source-over;
-webkit-mask-composite: source-in; /*只显示重合的地方*/
-webkit-mask-composite: source-out; /*只显示上方遮罩,重合的地方不显示*/
-webkit-mask-composite: source-atop;
-webkit-mask-composite: destination-over;
-webkit-mask-composite: destination-in; /*只显示重合的地方*/
-webkit-mask-composite: destination-out;/*只显示下方遮罩,重合的地方不显示*/
-webkit-mask-composite: destination-atop;
-webkit-mask-composite: xor; /*只显示不重合的地方*/

是不是一脸懵?这里做了一个对应的效果图,如果不太熟练,使用的时候知道有这样一个功能,然后对着找就行了


image-20211120130421281


回到这里,可以绘制一整块背景和一个圆形背景,然后通过遮罩合成排除(mask-composite: exclude)打一个孔就行了,实现如下


.wrap::before{
content: '';
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
-webkit-mask: url("data:image/svg+xml,%3Csvg width='50' height='50' viewBox='0 0 50 50' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle cx='25' cy='25' r='25' fill='%23C4C4C4'/%3E%3C/svg%3E"), linear-gradient(red, red);
-webkit-mask-size: 50px, 100%;
-webkit-mask-repeat: no-repeat;
-webkit-mask-position: calc(var(--x, .5) * 100% + var(--x, .5) * 100px - 50px ) calc(var(--y, .5) * 100% + var(--y, .5) * 100px - 50px ), 0;
-webkit-mask-composite: xor; /*只显示不重合的地方, chorem 、safari 支持*/
mask-composite: exclude; /* 排除,只显示不重合的地方, firefox 支持 */
background: rgba(0,0,0,.3);
backdrop-filter: blur(5px)
}

需要注意-webkit-mask-position中的计算,这样也能很好的实现这个效果


237330258-6181fcdb471cf


完整代码可以查看:backdrop-mask-composite (codepen.io)


你可能已经发现,上述例子中的圆是通过 svg 绘制的,还用到了遮罩合成,看着好像更加繁琐了。其实呢,这是一种更加万能的解决方式,可以带来无限的可能性。比如我需要一个星星⭐️的镂空效果,很简单,先通过一个绘制软件画一个


image-20211120131056453


然后把这段 svg 代码转义一下,这里推荐使用张鑫旭老师的SVG在线压缩合并工具


image-20211120131335734


替换到刚才的例子中就可以了


.wrap::before{
content: '';
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
-webkit-mask: url("data:image/svg+xml,%3Csvg width='96' height='91' viewBox='0 0 96 91' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M48 0l11.226 34.55h36.327l-29.39 21.352L77.39 90.45 48 69.098 18.61 90.451 29.837 55.9.447 34.55h36.327L48 0z' fill='%23C4C4C4'/%3E%3C/svg%3E"), linear-gradient(red, red);
-webkit-mask-size: 50px, 100%;
-webkit-mask-repeat: no-repeat;
-webkit-mask-position: calc(var(--x, .5) * 100% + var(--x, .5) * 100px - 50px ) calc(var(--y, .5) * 100% + var(--y, .5) * 100px - 50px ), 0;
-webkit-mask-composite: xor; /*只显示不重合的地方, chorem 、safari 支持*/
mask-composite: exclude; /* 排除,只显示不重合的地方, firefox 支持 */
background: rgba(0,0,0,.3);
backdrop-filter: blur(5px)
}

星星镂空实现效果如下


Kapture 2021-11-20 at 13.35.28


完整代码可以查看:backdrop-star (codepen.io)


再比如一个心形❤,实现效果如下


Kapture 2021-11-20 at 13.44.26


完整代码可以查看:backdrop-heart (codepen.io)


只有想不到,没有做不到



作者:XboxYan
链接:https://juejin.cn/post/7033188994641100831
收起阅读 »

Unable to extract the trust manager on Android10Platform 完美解决

Unable to extract the trust manager on Android10Platform网上有大致有两种解决方案,但都不靠谱。产生这个异常的根本原因是:builder.sslSocketFactory(sslContext.getSoc...
继续阅读 »

Unable to extract the trust manager on Android10Platform

网上有大致有两种解决方案,但都不靠谱。产生这个异常的根本原因是:

builder.sslSocketFactory(sslContext.getSocketFactory());
这个方式已经过时了,需要新的方式,如下:



final X509TrustManager trustManager = new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {

}

@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {

}

@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
};
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new X509TrustManager[]{trustManager}, new SecureRandom());

OkHttpClient.Builder builder = new OkHttpClient().newBuilder()
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(15,TimeUnit.SECONDS)
.addInterceptor(logInterceptor)
.sslSocketFactory(sslContext.getSocketFactory(),trustManager)
.hostnameVerifier(new HostnameVerifier() {

@Override
public boolean verify(String hostname, SSLSession session) {

return true;
}
});

收起阅读 »

面试官:一千万数据,怎么快速查询?

sql
前言 面试官: 来说说,一千万的数据,你是怎么查询的?B哥:直接分页查询,使用limit分页。面试官:有实操过吗?B哥:肯定有呀 此刻献上一首《凉凉》 也许有些人没遇过上千万数据量的表,也不清楚查询上千万数据量的时候会发生什么。 今天就来带大家实操一下,这次...
继续阅读 »

前言



  • 面试官: 来说说,一千万的数据,你是怎么查询的?
  • B哥:直接分页查询,使用limit分页。
  • 面试官:有实操过吗?
  • B哥:肯定有呀

此刻献上一首《凉凉》


也许有些人没遇过上千万数据量的表,也不清楚查询上千万数据量的时候会发生什么。


今天就来带大家实操一下,这次是基于MySQL 5.7.26做测试


准备数据


没有一千万的数据怎么办?


创建呗


代码创建一千万?那是不可能的,太慢了,可能真的要跑一天。可以采用数据库脚本执行速度快很多。


创建表

CREATE TABLE `user_operation_log`  (
  `id` int(11NOT NULL AUTO_INCREMENT,
  `user_id` varchar(64CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `ip` varchar(20CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `op_data` varchar(255CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `attr1` varchar(255CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `attr2` varchar(255CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `attr3` varchar(255CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `attr4` varchar(255CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `attr5` varchar(255CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `attr6` varchar(255CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `attr7` varchar(255CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `attr8` varchar(255CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `attr9` varchar(255CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `attr10` varchar(255CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `attr11` varchar(255CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `attr12` varchar(255CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`USING BTREE
ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

创建数据脚本

采用批量插入,效率会快很多,而且每1000条数就commit,数据量太大,也会导致批量插入效率慢


DELIMITER ;;
CREATE PROCEDURE batch_insert_log()
BEGIN
  DECLARE i INT DEFAULT 1;
  DECLARE userId INT DEFAULT 10000000;
 set @execSql = 'INSERT INTO `test`.`user_operation_log`(`user_id`, `ip`, `op_data`, `attr1`, `attr2`, `attr3`, `attr4`, `attr5`, `attr6`, `attr7`, `attr8`, `attr9`, `attr10`, `attr11`, `attr12`) VALUES';
 set @execData = '';
  WHILE i<=10000000 DO
   set @attr = "'测试很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长的属性'";
  set @execData = concat(@execData"(", userId + i, ", '10.0.69.175', '用户登录操作'"","@attr","@attr","@attr","@attr","@attr","@attr","@attr","@attr","@attr","@attr","@attr","@attr")");
  if i % 1000 = 0
  then
     set @stmtSql = concat(@execSql@execData,";");
    prepare stmt from @stmtSql;
    execute stmt;
    DEALLOCATE prepare stmt;
    commit;
    set @execData = "";
   else
     set @execData = concat(@execData",");
   end if;
  SET i=i+1;
  END WHILE;

END;;
DELIMITER ;

开始测试



哥的电脑配置比较低:win10 标压渣渣i5 读写约500MB的SSD



由于配置低,本次测试只准备了3148000条数据,占用了磁盘5G(还没建索引的情况下),跑了38min,电脑配置好的同学,可以插入多点数据测试


SELECT count(1FROM `user_operation_log`

返回结果:3148000


三次查询时间分别为:



  • 14060 ms
  • 13755 ms
  • 13447 ms

普通分页查询


MySQL 支持 LIMIT 语句来选取指定的条数数据, Oracle 可以使用 ROWNUM 来选取。


MySQL分页查询语法如下:


SELECT * FROM table LIMIT [offset,] rows | rows OFFSET offset


  • 第一个参数指定第一个返回记录行的偏移量
  • 第二个参数指定返回记录行的最大数目

下面我们开始测试查询结果:


SELECT * FROM `user_operation_log` LIMIT 1000010

查询3次时间分别为:



  • 59 ms
  • 49 ms
  • 50 ms

这样看起来速度还行,不过是本地数据库,速度自然快点。


换个角度来测试


相同偏移量,不同数据量

SELECT * FROM `user_operation_log` LIMIT 1000010
SELECT * FROM `user_operation_log` LIMIT 10000100
SELECT * FROM `user_operation_log` LIMIT 100001000
SELECT * FROM `user_operation_log` LIMIT 1000010000
SELECT * FROM `user_operation_log` LIMIT 10000100000
SELECT * FROM `user_operation_log` LIMIT 100001000000

查询时间如下:

















































数量 第一次 第二次 第三次
10条 53ms 52ms 47ms
100条 50ms 60ms 55ms
1000条 61ms 74ms 60ms
10000条 164ms 180ms 217ms
100000条 1609ms 1741ms 1764ms
1000000条 16219ms 16889ms 17081ms

从上面结果可以得出结束:数据量越大,花费时间越长


相同数据量,不同偏移量

SELECT * FROM `user_operation_log` LIMIT 100100
SELECT * FROM `user_operation_log` LIMIT 1000100
SELECT * FROM `user_operation_log` LIMIT 10000100
SELECT * FROM `user_operation_log` LIMIT 100000100
SELECT * FROM `user_operation_log` LIMIT 1000000100










































偏移量 第一次 第二次 第三次
100 36ms 40ms 36ms
1000 31ms 38ms 32ms
10000 53ms 48ms 51ms
100000 622ms 576ms 627ms
1000000 4891ms 5076ms 4856ms

从上面结果可以得出结束:偏移量越大,花费时间越长


SELECT * FROM `user_operation_log` LIMIT 100100
SELECT idattr FROM `user_operation_log` LIMIT 100100

如何优化


既然我们经过上面一番的折腾,也得出了结论,针对上面两个问题:偏移大、数据量大,我们分别着手优化


优化偏移量大问题


采用子查询方式

我们可以先定位偏移位置的 id,然后再查询数据


SELECT * FROM `user_operation_log` LIMIT 100000010

SELECT id FROM `user_operation_log` LIMIT 10000001

SELECT * FROM `user_operation_log` WHERE id >= (SELECT id FROM `user_operation_log` LIMIT 10000001LIMIT 10

查询结果如下:































sql 花费时间
第一条 4818ms
第二条(无索引情况下) 4329ms
第二条(有索引情况下) 199ms
第三条(无索引情况下) 4319ms
第三条(有索引情况下) 201ms

从上面结果得出结论:



  • 第一条花费的时间最大,第三条比第一条稍微好点
  • 子查询使用索引速度更快

缺点:只适用于id递增的情况


id非递增的情况可以使用以下写法,但这种缺点是分页查询只能放在子查询里面


注意:某些 mysql 版本不支持在 in 子句中使用 limit,所以采用了多个嵌套select


SELECT * FROM `user_operation_log` WHERE id IN (SELECT t.id FROM (SELECT id FROM `user_operation_log` LIMIT 100000010AS t)

采用 id 限定方式

这种方法要求更高些,id必须是连续递增,而且还得计算id的范围,然后使用 between,sql如下


SELECT * FROM `user_operation_log` WHERE id between 1000000 AND 1000100 LIMIT 100

SELECT * FROM `user_operation_log` WHERE id >= 1000000 LIMIT 100

查询结果如下:



















sql 花费时间
第一条 22ms
第二条 21ms

从结果可以看出这种方式非常快


注意:这里的 LIMIT 是限制了条数,没有采用偏移量


优化数据量大问题


返回结果的数据量也会直接影响速度


SELECT * FROM `user_operation_log` LIMIT 11000000

SELECT id FROM `user_operation_log` LIMIT 11000000

SELECT id, user_id, ip, op_data, attr1, attr2, attr3, attr4, attr5, attr6, attr7, attr8, attr9, attr10, attr11, attr12 FROM `user_operation_log` LIMIT 11000000

查询结果如下:























sql 花费时间
第一条 15676ms
第二条 7298ms
第三条 15960ms

从结果可以看出减少不需要的列,查询效率也可以得到明显提升


第一条和第三条查询速度差不多,这时候你肯定会吐槽,那我还写那么多字段干啥呢,直接 * 不就完事了


注意本人的 MySQL 服务器和客户端是在同一台机器上,所以查询数据相差不多,有条件的同学可以测测客户端与MySQL分开


SELECT * 它不香吗?

在这里顺便补充一下为什么要禁止 SELECT *。难道简单无脑,它不香吗?


主要两点:



  1. 用 "SELECT * " 数据库需要解析更多的对象、字段、权限、属性等相关内容,在 SQL 语句复杂,硬解析较多的情况下,会对数据库造成沉重的负担。
  2. 增大网络开销,* 有时会误带上如log、IconMD5之类的无用且大文本字段,数据传输size会几何增涨。特别是MySQL和应用程序不在同一台机器,这种开销非常明显。

结束


最后还是希望大家自己去实操一下,肯定还可以收获更多,欢迎留言!!


创建脚本我给你正好了,你还在等什么!!!


再奉上我之前 MySQL 如何优化


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

Android 基础架构组面试题 | 面试

SDK相关面试的时候我觉得哦,这些sdk有任意其实你研究的比较深入就行了,应该能在面试中表现的很好了。还有就是个人建议最好还是在单一方向研究的更深入一点,别的只要大概知道干什么的就行了。配置中心以及灰度测试app必备工具之一,配置中心主要负责的就是动态化的配置...
继续阅读 »

SDK相关

面试的时候我觉得哦,这些sdk有任意其实你研究的比较深入就行了,应该能在面试中表现的很好了。还有就是个人建议最好还是在单一方向研究的更深入一点,别的只要大概知道干什么的就行了。

  1. 配置中心以及灰度测试

app必备工具之一,配置中心主要负责的就是动态化的配置,比如文本展示类似这些的。sdk提供方需要负责的是提供动态更新能力,这里有个差异化更新,只更新dif部分,还有就是流量优化等等需要开发同学考虑的。然后可以考虑下存储性能方面的提升等。

而abtest也是app必备工具之一了,动态的下发实验策略,之后开发同学可以切换实验的页面。另外主要需要考虑灰度结果计算,分桶以及版本过滤白名单等等。这里只是一个简单的介绍不展开,因为我只是一个使用方。

  1. 调试组件

个人还是更推荐滴滴的Dokit,功能点比较多而且接入相对来说比较简单。而且提供了很多给开发同学定制的能力,可以在debug情况下增加很多业务相关的测试功能,方便测试同学,核心还是浮窗太方便了。

当然很多实验性的预研功能等其实都可以直接接在这里,然后在测试环境下充分展开,之后在进行线上灰度方案。还有一些具有风险的hook操作,个人也比较建议放在debug组件上。

  1. 性能监控框架

这部分有几个不同的方面,首先是异常崩溃方面的,另外则是性能监控方面的,但是他们整体是划分在一起的,都属于线上性能监控体系的。

Crash相关的,可以从爱奇艺的xCrash学起。包含了崩溃日志,ANR以及native crash,因为版本适配的问题ANR在高版本上已经不是这么好捞了,还有就是native crash相关的。是一个非常牛逼的库了。

而线上的性能监控框架可以从腾讯的Matrix学起,以前有两篇文章介绍的内容也都是和Matrix相关的, Matrix首页上也有介绍,比如fps,卡顿,IO,电池,内存等等方面的监控。其中卡顿监控涉及到的就是方法前后插桩,同时要有函数的mapping表,插桩部分整体来说比较简单感觉。

另外关于线上内存相关的,推荐各位可以学习下快手的koom, 对于hprof的压缩比例听说能达到70%,也能完成线上的数据回捞以及监控等等,是一个非常屌的框架。下面给大家一个抄答案的方式。字节也有一个类似的原理其实也差不多。

主进程发现内存到达阈值的时候,用leakcanary的方案,通过shark fork进程内存,之后生成hrop。由于hrop文件相对较大,所以我们需要对于我们所要分析的内容进行筛选,可以通过xhook,之后对hrop的写入操作进行hook,当发现写入内容的类型符合我们的需要的情况下才进行写入。

而当我们要做线上日志回捞的情况,需要对hprof 进行压缩,具体算法可以参考koom/raphel,有提供对应的压缩算法。

最后线上回捞机制就是基于一个指令,回捞线上符合标准的用户的文件操作,这个自行设计。

其实上述几个库都还是有一个本质相关的东西,那么就是plthook,这个上面三个库应该都有对其的使用,之前是爱奇艺的xhook,现在是字节的bhook, 这个大佬也是我的偶像之一了,非常离谱了算是。

Android 性能采集之Fps,Memory,Cpu 和 Android IO监控

最近已经不咋写这部分相关了,所以也就没有深挖,但是后续可能会有一篇关于phtead hook相关的,也是之前matrix更新的一个新东西,还在测试环境灰度阶段。

  1. 基础网络组件

虽然核心可能还是三方网络库,但是因为基本所有公司都对网络方面有调整和改动,以及解析器等方面的优化,其实可以挖的东西也还是蛮多的。

应付面试的同学可以看看Android网络优化方案。当然还是要具体问题具体分析,毕竟头疼医头,脚疼医脚对吧。

之前和另外一个朋友聊了下,其实很多厂对json解析这部分有优化调整,通过apt之后更换原生成原生的解析方式,加快反序列化速度的都是可以考虑考虑的。

  1. 埋点框架

其实这个应该要放在更前面一点的,数据上报数据分析啥的其实都还是蛮重要的。

这部分因为我完全没写过哦,所以我压根不咋会,但是如果你会的话,面试的时候展开说说,可以帮助你不少。

另外还需要有线上的异常用户数据回捞系统,方便开发同学主动去把线上有异常的用户的日志给收集回来。

但是有些刁钻的页面曝光监控啦,自动化埋点啥的其实还是写过一点的,有兴趣的可以翻翻历史,还有github 上还有demo。

AndroidAutoTrack demo工程

  1. 启动相关

通过DAG(有向无环图)的方式将sdk的初始化拆解成一个个task,之后理顺依赖关系,让他们能按照固定的顺序向下执行。

核心需要处理的是依赖关系,比如说其实埋点库依赖于网络库初始化,然后APM相关的则依赖于埋点库和配置中心abtest等等,这样的依赖关系需要开发同学去理顺的。

另外就是把sdk的粒度打的细碎一点,更容易观察每个sdk任务的耗时情况,之后增加task阈值告警,超过某个加载速度就通知到相应的同学改一下。

多线程是能优化掉一部分,但是也需要避免频繁线程调度。还有就是我个人觉得这些启动相关的东西因为都无法使用sdk级别的灰度,所以改动最好慎重一点。出发点始终都是好的,但是还是结果导向吧。

启动优化的核心,我个人始终坚持的就是延迟才能优化。开发人员很难做到优化代码执行的复杂度,执行时间之类的。尽人事听天命,玄学代码。

  1. 中间件(图片 日志 存储 基础信息)

这部分没啥,最好是对第三方库有一层隔离的思维,但是这个隔离也需要对应的同学对于程序设计方面有很好的思维,说起来简单,其实也蛮复杂的。

这里就不展开了,感觉面试也很少会问的很细。

  1. 第三方sdk大杂烩(偏中台方向)

基本一个app现在都有啥分享啦,推送啦,支付啦,账号体系啦,webview,jsbridge等等服务于应用内的一些sdk,这些东西就比较偏向于业务。

有兴趣的可以看看之前写的两篇关于sdk设计相关的。

活学活用责任链 SDK开发的一点点心得 Android厂商推送Plugin化

  1. 其他方面

大公司可能都会有些动态化方案的考虑,比如插件化啊动态化之类的。这部分在下确实不行,我就不展开了啊。

编译相关

  1. 描述下android编译流程

基架很容易碰到的面试题,以前简单的描述写过。聊聊Android编译流程

虽然是几年前的知识点了,但是还是要拆开高低版本的agp做比较的。所以这部分可以回答下,基本这题就能简单的拿下了。

  1. Gradle 生命周期

简单的说下就是buildSrc先编译,之后是根目录的settings.gradle, 根build.gradle,最后才是module build

网上一堆,你自己翻一番就知道了。

  1. apt是编译中哪个阶段

APT解析的是java 抽象语法树(AST),属于javac的一部分流程。大概流程:.java -> AST -> .class

聊聊AbstractProcessor和Java编译流程

  1. Dex和class有什么区别

链接传送门

Class与dex的区别

1)虚拟机: class用jvm执行,dex用dvm执行

2)文档: class中冗余信息多,dex会去除冗余信息,包含所有类,查找方便,适合手机端

JVM与DVM

1)JVM基于栈(使用栈帧,内存),DVM基于寄存器,速度更快,适合手机端

2)JVM执行Class字节码,DVM执行DEX

3)JVM只能有一个实例,一个应用启动运行在一个DVM

DVM与ART

1)DVM:每次运行应用都需要一次编译,效率降低。JIT

2)ART:Android5.0以上默认为ART,系统会在进程安装后进行一次预编译,将代码转为机器语言存在本地,这样在每次运行时不用再进行编译,提高启动效率;。 AOP & JIT

  1. Transform是如何被执行的

Transform 在编译过程中会被封装成Task 依赖其他编译流程的Task执行。

image.png

  1. Transform和其他系统Transform执行的顺序

其实这个题目已经是个过期了,后面对这些都合并整合了,而且最新版的api也做了替换,要不然考虑下回怼下面试官?

Transform和Task之间有关?

  1. 如何监控编译速度变慢问题
./gradlew xxxxx -- scan

之后会生成一个gradle的网页,填写下你的邮箱就好了。

另外一个相对来说比较简单了。通过gradle原生提供的listener进行就行了。


// 耗时统计kt化
class TimingsListener : TaskExecutionListener, BuildListener {
private var startTime: Long = 0L
private var timings = linkedMapOf<String, Long>()


override fun beforeExecute(task: Task) {
startTime = System.nanoTime()
}

override fun afterExecute(task: Task, state: TaskState) {
val ms = TimeUnit.MILLISECONDS.convert(System.nanoTime() - startTime, TimeUnit.NANOSECONDS)
task.path
timings[task.path] = ms
project.logger.warn("${task.path} took ${ms}ms")
}

override fun buildFinished(result: BuildResult) {
project.logger.warn("Task timings:")
timings.forEach {
if (it.value >= 50) {
project.logger.warn("${it.key} cos ms ${it.value}\n")
}
}
}

override fun buildStarted(gradle: Gradle) {

}

override fun settingsEvaluated(settings: Settings) {
}

override fun projectsLoaded(gradle: Gradle) {

}

override fun projectsEvaluated(gradle: Gradle) {

}

}

gradle.addListener(TimingsListener())

  1. Gradle中如何给一个Task前后插入别的任务

最简单的可以考虑直接获取到Task实例,之后在after和before插入一些你所需要的代码。

另外一个就是通过dependOn前置和finalizedBy挂载一个任务 mustAfter

Gradle 使用指南 -- Gradle Task

  1. ksp APT Transform的区别

ksp 是kotlin专门独立的ast语法树

apt 是java 的ast语法树

transform是 agp 专门修改字节码的一个方法。

反杀时刻AsmClassVisitorFactory,可以看看我之前写的那篇文章。

  1. Transform上的编译优化能做哪些?

虽然是个即将过期的api,但是大家对他的改动还是都比较多的。

首先肯定是需要完成增量编译的,具体的可以参考我的demo工程。记住,所有的transfrom都要全量。

另外可以考虑多线程优化,将转化操作移动到子线程内,建议使用gradle内部的共享线程。

参考agp最新做法,抽象出一个新的interface,之后通过spi串联,之后将asm链式调用。我的文章也介绍过,具体的点在哪里自己盘算。

现在准备好告别Transform了吗

  1. aar 源码切换插件原理

这个前几天刚介绍过,原理和方案业内都差不多,mulite-repo应该都需要这个东西的。我的版本也比较简陋,大厂内部肯定都会有些魔改的。

相对来说功能肯定会更丰富,更全面一点。

aar和源码切换插件Plus

  1. 你们有哪些保证代码质量的手段

最简单的方式还是通过静态扫描+pipline 处理,之后在合并mr之前进行一次拦截。

静态扫描方式比较多,下面给大家简单的介绍下

阿里的sonar 但是对kt的支持很糟糕,因为阿里使用,所以有很多现成的规则可以使用,但是如果从0-1接入,你可能会直接放弃。

原生的lint,可以基于原生提供的lint api,对其进行开发,支持种类也多,基本上算是一个非常优秀的方案了,但是由于文档资料较少,对于开发的要求可能会较高。

AndroidLint

  1. 如何对第三方的依赖做静态检查?

魔高一尺道高一丈。lint还是能解决这个问题的。

Tree Api+ClassScanner = 识别三方隐私权限调用

  1. R.java code too large 解决方案

又是一个过期的问题,尽早升级agp版本,让R8帮你解决这个问题,R文件完全可以内联的。

或者用别的AGP插件的R inline也可以解决这个问题。

  1. R inline 你需要注意些什么?

预扫描,先收集调用的信息,之后在进行替换。还有javac 的时候可能就因为文件过大,直接挂掉了。

  1. 一个类替换父类 比如所有activity实现类替换baseactivity

class node 直接替换 superName ,想起了之前另外一个问题,感觉主要是要对构造函数进行修改,否则也会出异常。

  1. R8 D8 以及混淆相关的,还有R8除了混淆还能干些什么? 混淆规则有没有碰到什么奇怪的问题?

D8Dx的区别,主要涉及到编译速度以及编译产物的体积,包体积大概小11%。

R8 则是变更了整个编译流程的,其中我觉得最微妙的就是java8 lambda相关的,脱糖前后的差别还是比较大的。同时R8也少了很多之前的Transform。

R8的混淆部分,混淆除了能增加代码阅读难度意外,更多的是对于代码优化方面的。 比如无效代码优化, 同时也删除代码等等都可以做。

  1. 编译的时候有没有碰到javac的常量优化

javac会将静态常量直接优化成具体的数值。但是尤其是多模块场景下尤其容易出现异常,看起来是个实际的常量引用,但是产物上却是一个具体的常量值了。

其他部分

组件化相关

不仅仅要聊到路由,还需要聊下业务仓库的设计,如何避免两个模块之间相互相互引用导致的环问题。

另外就是路由的apt aop的部分都可以深入的聊一下。

如果只聊路由的话,你就只说了一个字符串匹配规则,非常无聊了。

路由跳转

路由跳转只是一小部分,其核心原理就是字符串匹配,之后筛选出符合逻辑的页面进行跳转。

另外就是拦截器的设计,同步异步拦截器两种完全不同的写法。

其原理基于apt+transform ,apt负责生成模块德 路由表,而transform则负责将各个模块的路由表进行收集。

服务发现

类似路由表,但是维护的是一个基于键值的类构造。ab之间当有相互依赖的情况下,可以通过基于接口编程的方式进行调整,互相只依赖抽象的接口,之后实现类在内部,通过注册的机制。之后在实际的使用地方用服务发现的机制寻找。

虚拟机部分

很多人会觉得虚拟机这部分都是硬八股,比较无聊。但是其实有时候我们碰到的一些字节码相关的问题就和这部分基础姿势相关了。

虽然用的比较少,但是也不是一个硬八股,比hashmap好玩太多了。

依赖注入

和服务发现类似,也是拿来解决不同模块间的依赖问题。可以使用hilt,依赖注入的好处就是连构造的这部分工作也有di完成了,而且构造能力更多样。可以多参数构造。

总结

其实以当前来说安卓的整个体系相对来说很复杂,第三方库以及源代码量都比较大,并不是要求每个同学都对这些有一个良好的掌握,但是大体上应该了解的还是需要了解的。

面试造火箭可不是浪得虚名啊,但是鸡架可能还是需要使用到其中一些奇奇怪怪的黑科技的。

好了胡扯结束了,今天的文章就到此为止了。

原文链接:https://juejin.cn/post/7032625978023084062?utm_source=gold_browser_extension

收起阅读 »

你用过HandlerThread么?

前言我们都用过Handler,也很熟悉怎么使用Handler,肯定也知道Handler使用过程中的注意事项,那就是内存泄漏,也知道大部分内存泄漏都是因为静态变量引用的问题。Handler是一个内部类,非static内部类或者匿名内部类都会持有外部类的引用。如果...
继续阅读 »

前言

我们都用过Handler,也很熟悉怎么使用Handler,肯定也知道Handler使用过程中的注意事项,那就是内存泄漏,也知道大部分内存泄漏都是因为静态变量引用的问题。Handler是一个内部类,非static内部类或者匿名内部类都会持有外部类的引用。如果此时Activty退出了, handler持有他的引用,则这个Activity 并不会被销毁,其实还是在内存中,所以就造成了内存泄漏 (Memory Leak) 的问题。怎么解决这个问题,网上都有很成熟的文章和技术实现,这里不再累赘,这期主要讲下Handler的另一种使用方式HandlerThread。

一、HandlerThread的本质

HandlerThread 本质上就是一个普通Thread。

Handler完成两个线程通信的代码中,需要调用Looper.prepare() 为一个线程开启一个消息循环,默认情况下Android中新诞生的线程是没有开启消息循环的。(主线程除外,主线程系统会自动为其创建Looper对象,开启消息循环。) Looper对象通过MessageQueue来存放消息和事件。一个线程只能有一个Looper,对应一个MessageQueue。 然后通过Looper.loop() 让Looper开始工作,从消息队列里取消息,处理消息。

所以要使用Handler完成线程之间的通信,首先需要调用Looper.prepare() 为该线程开启消息循环,然后创建Handle,然后调用 Looper.loop() 开始工作。这都是很常规的流程。

而HandlerThread 帮我们做好了这些事情,它内部建立了Looper。

二、HandlerThread 用法

public class OtherActivity extends AppCompatActivity {
private static final String TAG = "OtherActivity";
private Handler handler1;
private Handler handler2;
private HandlerThread handlerThread1;
private HandlerThread handlerThread2;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_other);

// 创建HandlerThread
handlerThread1 = new HandlerThread("handle-thread-1");
handlerThread2 = new HandlerThread("handle-thread-2");
// 开启HandleThread
handlerThread1.start();
handlerThread2.start();

handler1 = new Handler(handlerThread1.getLooper()) {
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
Log.i(TAG, "handleMessage: ThreadName = " + Thread.currentThread().getName()
+ " msg.what = " + msg.what);
}
};

handler2 = new Handler(handlerThread2.getLooper()) {
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
Log.i(TAG, "handleMessage: ThreadName = " + Thread.currentThread().getName()
+ " msg.what = " + msg.what);
}
};

handler2.sendEmptyMessage(2);
handler1.sendEmptyMessage(5);
}

// 释放资源
@Override
protected void onDestroy() {
super.onDestroy();
handlerThread1.quit();
handlerThread2.quitSafely();
}
}

HandlerThread创建 Looper 并执行 loop() 的线程在任务结束的时候,需要手动调用 quit。否则,线程将由于 loop() 的轮询一直处于可运行状态,CPU 资源无法释放。更有可能因为 Thread 作为 GC Root 持有超出生命周期的实例引发内存泄漏。

官方使用 quitSafely() 去终止 Looper,原因是其只会剔除执行时刻晚于 当前调用时刻 的 Message。这样可以保证 quitSafely 调用的那刻,满足执行时间条件的 Message 继续保留在队列中,在都执行完毕才退出轮询。

那么主线程需要 quit 吗?其实不需要,在内存不足的时候 App 由 AMS 直接回收进程。因为主线程极为重要,承载着 ContentProvider、Activity、Service 等组件生命周期的管理,即便某个组件结束了,它仍有继续存在去调度其他组件的必要! 换言之,ActivityThread 的作用域超过了这些组件,不该由这些组件去处理它的结束。比如,Activity destroy 了,ActivityThread 仍然要处理其他 Activity 或 Service 等组件的事务,不能结束。

HandlerThread 的在Android中的用处

Android本身就是一个巨大的消息处理机,ActivityThread类是Android APP进程的初始类,它的main函数是这个APP进程的入口。APP进程中UI事件的执行代码段都是由ActivityThread提供的。也就是说,主线程实例是存在的,只是创建它的代码我们不可见。ActivityThread的main函数就是在这个主线程里被执行的。

public final class ActivityThread {

//...
private static ActivityThread sCurrentActivityThread;
public static ActivityThread currentActivityThread() {
return sCurrentActivityThread;
}
private void attach(boolean system) {
sCurrentActivityThread = this;
//...
}
public static void main(String[] args) {
//....

// 创建Looper和MessageQueue对象,用于处理主线程的消息
Looper.prepareMainLooper();

// 创建ActivityThread对象
ActivityThread thread = new ActivityThread();

// 建立Binder通道 (创建新线程)
thread.attach(false);

Looper.loop(); //消息循环运行
throw new RuntimeException("Main thread loop unexpectedly exited");
}
}

Activity的生命周期都是依靠主线程的Looper.loop,当收到不同Message时则采用相应措施即可。

总结

管它呢,用就好了,封装好的东西干嘛不用了,可以减少我们开发过程中bug,提示开发效率,多好的一件事啊,用起来就对了,哈哈

原文链接:https://juejin.cn/post/7032649435133771807?utm_source=gold_browser_extension

收起阅读 »

android 展示PDF文件

PDF
 注:此方式展示pdf文件会增加apk大小3-4m左右 建议使用x5的webview进行加载pdf文件(可扩展) 1. 加入此依赖 implementation 'com.github.barteksc:android-pdf-viewer:3....
继续阅读 »
  •  注:此方式展示pdf文件会增加apk大小3-4m左右 建议使用x5的webview进行加载pdf文件(可扩展)


1. 加入此依赖



implementation 'com.github.barteksc:android-pdf-viewer:3.2.0-beta.1'



2. 简单介绍


此篇文章主要还是将pdf文件进行下载到本sd目录下,之后转为file文件,交给pdfview进行展示,具体的展示pdf文件可进入pdfview源码中进行查看


https://github.com/barteksc/AndroidPdfViewer

3. 开始操作



public class PDF2Activity extends AppCompatActivity {

private PDFView pdfView;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_p_d_f2);
pdfView = findViewById(R.id.pdfView);

download("xxx.pdf");

}

private void download(String url) {
DownloadUtil.download(url, getCacheDir() + "/temp.pdf", new DownloadUtil.OnDownloadListener() {
@Override
public void onDownloadSuccess(final String path) {
Log.d("MainActivity", "onDownloadSuccess: " + path);
runOnUiThread(new Runnable() {
@Override
public void run() {
preView(path);
}
});
}

@Override
public void onDownloading(int progress) {
Log.d("MainActivity", "onDownloading: " + progress);
}

@Override
public void onDownloadFailed(String msg) {
Log.d("MainActivity", "onDownloadFailed: " + msg);
}
});
}

private void preView(String path) {
File file = new File(path);
//这里只是作为一个file文件进行展示 还有其他的办法进行展示
pdfView.fromFile(file)
.enableSwipe(true) // allows to block changing pages using swipe
.swipeHorizontal(false)
.enableDoubletap(true)
.defaultPage(0)
// allows to draw something on the current page, usually visible in the middle of the screen
.enableAnnotationRendering(false) // render annotations (such as comments, colors or forms)
.password(null)
.scrollHandle(null)
.enableAntialiasing(true) // improve rendering a little bit on low-res screens
// spacing between pages in dp. To define spacing color, set view background
.spacing(0)
.load();
}


3.1 okhttp



implementation("com.squareup.okhttp3:okhttp:4.6.0")



4.DownLoadUtils


public class DownloadUtil {

public static void download(final String url, final String saveFile, final OnDownloadListener listener) {
Request request = new Request.Builder().url(url).build();
new OkHttpClient().newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
listener.onDownloadFailed(e.getMessage());
}

@Override
public void onResponse(Call call, Response response) throws IOException {
InputStream is = null;
byte[] buf = new byte[2048];
int len;
FileOutputStream fos = null;
try {
is = response.body().byteStream();
long total = response.body().contentLength();
File file = new File(saveFile);
fos = new FileOutputStream(file);
long sum = 0;
while ((len = is.read(buf)) != -1) {
fos.write(buf, 0, len);
sum += len;
int progress = (int) (sum * 1.0f / total * 100);
listener.onDownloading(progress);
}
fos.flush();
listener.onDownloadSuccess(file.getAbsolutePath());
} catch (Exception e) {
listener.onDownloadFailed(e.getMessage());
} finally {
try {
if (is != null)
is.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
if (fos != null)
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
});
}

public interface OnDownloadListener {
void onDownloadSuccess(String path);

void onDownloading(int progress);

void onDownloadFailed(String msg);
}
}

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

进来看看是不是你想要的效果,Android吸顶效果,并有着ViewPager左右切换

老规矩,先上图,看看是不是你想要的 美团: 来一个图形分析 接下来我要写一个简单示例,先分析一下布局,见下图,最外层是NestedScrollView,之后嵌套一个LinearLayout头部,中间TabLayout选择器,底部一个ViewPager Vi...
继续阅读 »

老规矩,先上图,看看是不是你想要的


美团:
美团




来一个图形分析


接下来我要写一个简单示例,先分析一下布局,见下图,最外层是NestedScrollView,之后嵌套一个LinearLayout头部,中间TabLayout选择器,底部一个ViewPager
ViewPager高度需要动态控制,看自己的需求了,如果是美团那种效果,就是
ViewPager高度 = NestedScrollView高度 - TabLayout高度
在这里插入图片描述




话不多说,代码实现


接下来我写一个例子,如果按照普通控件的嵌套方式来实现,那么肯定存在滑动冲突,会出现RecyclerView先进行滑动其次才是ScrollView滑动,那么就需要先重写NestedScrollView控件,用于控制最大的滑动距离,当达到最大滑动距离,再分发给RecyclerView滑动!




NestedScrollView重写


需要继承自NestedScrollView并重写onStartNestedScroll和onNestedPreScroll方法,如下


package com.cyn.mt

import android.content.Context
import android.util.AttributeSet
import android.view.View
import androidx.core.view.NestedScrollingParent2
import androidx.core.widget.NestedScrollView

/**
* @author cyn
*/

class CoordinatorScrollview : NestedScrollView, NestedScrollingParent2 {
private var maxScrollY = 0

constructor(context: Context?) : super(context!!)
constructor(context: Context?, attrs: AttributeSet?) : super(
context!!,
attrs
)

constructor(
context: Context?,
attrs: AttributeSet?,
defStyleAttr: Int
) : super(context!!, attrs, defStyleAttr)

override fun onStartNestedScroll(
child: View,
target: View,
axes: Int,
type: Int
)
: Boolean {
return true
}

/**
* 设置最大滑动距离
*
* @param maxScrollY 最大滑动距离
*/

fun setMaxScrollY(maxScrollY: Int) {
this.maxScrollY = maxScrollY
}

/**
* @param target 触发嵌套滑动的View
* @param dx 表示 View 本次 x 方向的滚动的总距离
* @param dy 表示 View 本次 y 方向的滚动的总距离
* @param consumed 表示父布局消费的水平和垂直距离
* @param type 触发滑动事件的类型
*/

override fun onNestedPreScroll(
target: View,
dx: Int,
dy: Int,
consumed: IntArray,
type: Int
)
{
if (dy > 0 && scrollY < maxScrollY) {
scrollBy(0, dy)
consumed[1] = dy
}
}
}

布局文件


我按照美团的布局大体写出这样的布局

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">

<!--titleBar-->
<LinearLayout
android:id="@+id/titleBar"
android:layout_width="match_parent"
android:layout_height="45dp"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingLeft="18dp"
android:paddingRight="18dp">

<EditText
android:layout_width="0dp"
android:layout_height="35dp"
android:layout_marginEnd="12dp"
android:layout_marginRight="12dp"
android:layout_weight="1"
android:background="@drawable/edit_style"
android:paddingLeft="12dp"
android:paddingRight="12dp" />

<TextView
android:layout_width="wrap_content"
android:layout_height="35dp"
android:background="@drawable/button_style"
android:gravity="center"
android:paddingLeft="15dp"
android:paddingRight="15dp"
android:text="搜索"
android:textColor="#333333"
android:textStyle="bold" />

</LinearLayout>

<!--coordinatorScrollView-->
<com.cyn.mt.CoordinatorScrollview
android:id="@+id/coordinatorScrollView"
android:layout_width="match_parent"
android:layout_height="match_parent">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">

<!--相当于分析图中头部的LinearLayout,模拟动态添加的情况-->
<LinearLayout
android:id="@+id/titleLinerLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" />

<!--相当于分析图中红色标记处TabLayout-->
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content" />

<!--相当于分析图中绿色标记处ViewPager,代码中动态设置高度-->
<androidx.viewpager.widget.ViewPager
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="wrap_content" />

</LinearLayout>

</com.cyn.mt.CoordinatorScrollview>

</LinearLayout>



Fragment


加入,在Fragment中放入RecyclerView,提供给ViewPager使用,这里代码就不贴了,可以直接下源码!源码在文章末尾!




主要代码(重点来了)


coordinatorScrollView最大滑动距离即是titleLinerLayout的高度,所以实现titleLinerLayout的post方法,来监听titleLinerLayout的高度,由于这一块布局常常是通过网络请求后加载,所以,网络请求完毕后要再次实现post设置coordinatorScrollView最大滑动距离,如第80行代码和第90行代码,在这里,我并不推荐使用多次回调监听的方法!使用post只用调用一次,如果使用多次监听View变化的方法,应该在最后一次网络请求完毕后将此监听事件remove掉!


package com.cyn.mt

import android.content.res.Resources
import android.os.Bundle
import android.os.Handler
import android.util.DisplayMetrics
import android.view.LayoutInflater.from
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.title_layout.view.*


class MainActivity : AppCompatActivity() {

//屏幕宽
var screenWidth = 0

//屏幕高
var screenHeight = 0

//tabLayout的文本和图片
private val tabTextData = arrayOf("常用药品", "夜间送药", "隐形眼镜", "成人用品", "医疗器械", "全部商家")
private val tabIconData = arrayOf(
R.mipmap.tab_icon,
R.mipmap.tab_icon,
R.mipmap.tab_icon,
R.mipmap.tab_icon,
R.mipmap.tab_icon,
R.mipmap.tab_icon
)
private var fragmentData = mutableListOf()


override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

initView()
initData()
}

private fun initView() {

//获取屏幕宽高
val resources: Resources = this.resources
val dm: DisplayMetrics = resources.displayMetrics
screenWidth = dm.widthPixels
screenHeight = dm.heightPixels

//状态栏沉浸
StatusBarUtil.immersive(this)

//titleBar填充
StatusBarUtil.setPaddingSmart(this, titleBar)

//状态栏字体颜色设置为黑色
StatusBarUtil.darkMode(this)

//动态设置ViewPager高度
coordinatorScrollView.post {
val layoutParams = viewPager.layoutParams
layoutParams.width = screenWidth
layoutParams.height = coordinatorScrollView.height - tabLayout.height
viewPager.layoutParams = layoutParams
}

}

private fun initData() {

//我模拟在头部动态添加三个布局,就用图片代替了,要设置的图片高度都是我提前算好的,根据屏幕的比例来计算的
val titleView1 = getTitleView(screenWidth * 0.42F, R.mipmap.title1)
val titleView2 = getTitleView(screenWidth * 0.262F, R.mipmap.title2)
titleLinerLayout.addView(titleView1)
titleLinerLayout.addView(titleView2)

//设置最大滑动距离
titleLinerLayout.post {
coordinatorScrollView.setMaxScrollY(titleLinerLayout.height)
}

//用于请求网络后动态添加子布局
Handler().postDelayed({
val titleView3 = getTitleView(screenWidth * 0.589F, R.mipmap.title3)
titleLinerLayout.addView(titleView3)

//再次设置最大滑动距离
titleLinerLayout.post {
coordinatorScrollView.setMaxScrollY(titleLinerLayout.height)
}

}, 200)

//添加TabLayout
for (i in tabTextData.indices) {
tabLayout.addTab(tabLayout.newTab())
tabLayout.getTabAt(i)!!.setText(tabTextData[i]).setIcon(tabIconData[i])

//添加Fragment
fragmentData.add(TestFragment.newInstance(tabTextData[i]))
}

//Fragment ViewPager
viewPager.adapter = ViewPagerAdapter(supportFragmentManager, fragmentData)

//TabLayout关联ViewPager
tabLayout.setupWithViewPager(viewPager)

//设置TabLayout数据
for (i in tabTextData.indices) {
tabLayout.getTabAt(i)!!.setText(tabTextData[i]).setIcon(tabIconData[i])
}
}

/**
* 获取一个title布局
* 我这里就用三张图片模拟的
*
* @height 要设置的图片高度
*/

private fun getTitleView(height: Float, res: Int): View {
val inflate = from(this).inflate(R.layout.title_layout, null, false)
val layoutParams = inflate.titleImage.layoutParams
layoutParams.width = screenWidth
layoutParams.height = height.toInt()
inflate.titleImage.setImageResource(res)
return inflate
}
}



最终效果


在这里插入图片描述
至此结束!




源码资源


下面3个链接均可下载源码



源码Github(推荐):github.com/ThirdGoddes…




源码CodeChina:codechina.csdn.net/qq_40881680…




源码下载:download.csdn.net/download/qq…


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

iOS面试基础知识 (五)

混编技术移动开发已经进入大前端时代。对于混编技术,笔者一般在面试中也会问,通常会问h5混编、rn、weex、flutter等相关方面的问题,以考察面试者对于混编技术的了解程度。H5混编实现相对于rn、weex等混编技术,在App里面内嵌H5实现成本较低,所以目...
继续阅读 »

混编技术


移动开发已经进入大前端时代。对于混编技术,笔者一般在面试中也会问,通常会问h5混编、rn、weex、flutter等相关方面的问题,以考察面试者对于混编技术的了解程度。


H5混编实现


相对于rn、weex等混编技术,在App里面内嵌H5实现成本较低,所以目前市面上H5混编仍是主流,笔者在面试中一般会问H5与App怎么通信。概括来说,主要有如下集中方式:


伪协议实现


伪协议指的是自己自定义的url协议,通过webview的代理拦截到url的加载,识别出伪协议,然后调用native的方法。伪协议可以这样定义:AKJS://functionName?param1=value1&param2=value2。 其中AKJS代表我们自己定义的协议,functionName代表要调用的App方法,?后面代表传入的参数。

一、UIWebView通过UIWebViewDelegate的代理方法-webView: shouldStartLoadWithRequest:navigationType:进行伪协议拦截。

二、WKWebView通过WKNavigationDelegate代理方法实现- webView:decidePolicyForNavigationAction:decisionHandler:进行伪协议拦截。

此种实现方式优点是简单。

缺点有:


  • 由于url长度大小有限制,导致传参大小有限制,比如h5如果要传一个图片的base64字符串过来,这种方式就无能为力了。
  • 需要在代理拦截方法里面写一系列if else处理,难以维护。
  • 如果App要兼容UIWebView和WKWebView,需要有两套实现,难以维护。


JSContext


为了解决伪协议实现的缺点,我们可以往webview里面注入OC对象,不过这种方案只能用于UIWebView中。此种方式的实现步骤如下:

一、在webViewDidFinishLoad方法中通过JSContext注入JS对象


self.jsContext = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
self.jsContext[@"AK_JSBridge"] = self.bridgeAdapter; //往JS中注入OC对象


二、OC对象实现JSExport协议,这样JS就可以调用OC对象的方法了


@interface AKBridgeAdapter : NSOject< JSExport >
- (void)getUID; // 获取用户ID


此种方案的优点是JS可以直接调用对象的方法,通过提供对象这种方式,代码优雅;缺点是只能用于UIWebView、不能用于WKWebView。


WKScriptMessageHandler


WKWebView可以通过提供实现了WKScriptMessageHandler协议的类来实现JS调用OC,实现步骤如下:

一、往webview注入OC对象。


[self.configuration.userContentController addScriptMessageHandler:self.adapter name:@"AK_JSBridge"]


二、实现- userContentController:didReceiveScriptMessage:获取方法调用名和参数


- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
if ([message.body isKindOfClass:[NSDictionary class]]) {
NSDictionary *dicMessage = message.body;

NSString *funcName = [dicMessage stringForKey:@"funcName"];
NSString *parameter = [dicMessage stringForKey:@"parameter"];
//进行逻辑处理
}
}


此种方案的优点是实现简单,缺点是不支持UIWebView。


第三方库WKWebViewJavascriptBridge


该库是iOS使用最广泛的JSBridge库,该库通过伪协议+JS消息队列实现了JS与OC交互,此种方案兼容UIWebView和WKWebView。


RN、Weex、Flutter混编技术


RN(React Native)是facebook开发的跨三端(iOS、Android、H5)开源框架,目前在业界使用最广泛;Weex是阿里开源的类似RN的大前端开发框架,国内有些公司在使用;Flutter是Google开发的,作为后旗之秀,目前越来越流行。

笔者一般在面试中会问一下这类框架是怎么实现页面渲染,怎么实现调用OC的,以考察面试者是否了解框架实现原理。


组件化


任何一个对技术有追求的团队,都会做组件化,组件化的目标是模块解耦、代码复用。


组件代码管理方式


目前业内一般采用pod私有库的方式来管理自己的组件。


组件通信方式


MGJRouter


MGJRouter通过注册url的方式来实现方法注册和调用


[MGJRouter registerURLPattern:@"mgj://category/travel" toHandler:^(NSDictionary *routerParameters) {
NSLog(@"routerParameters[MGJRouterParameterUserInfo]:%@", routerParameters[MGJRouterParameterUserInfo]);
// @{@"user_id": @1900}
}];

[MGJRouter openURL:@"mgj://category/travel" withUserInfo:@{@"user_id": @1900} completion:nil];


该种方案的缺点有:


  • url定义由于是字符串,有可能造成重复。
  • 参数传入不能直接传model,而是需要传字典,如果方法实现方修改一个字段的类型但没有通知调用方,调用方无法直接知道,有可能导致崩溃。
  • 通过字典传参不直观,调用方需要知道字段的名字才能获取字段值,如果字段名不定义为宏,到处拷贝字段名造成难以维护。


CTMediator


CTMediator通过CTMediator的类别来实现方法调用。

一、组件提供方实现Target、Action。


@interface Target_A : NSObject

- (UIViewController *)Action_nativeFetchDetailViewController:(NSDictionary *)params;

@end

- (UIViewController *)Action_nativeFetchDetailViewController:(NSDictionary *)params
{
// 因为action是从属于ModuleA的,所以action直接可以使用ModuleA里的所有声明
DemoModuleADetailViewController *viewController = [[DemoModuleADetailViewController alloc] init];
viewController.valueLabel.text = params[@"key"];
return viewController;
}


二、组件提供方实现CTMediator类别暴露接口给使用方。


@interface CTMediator (CTMediatorModuleAActions)

- (UIViewController *)CTMediator_viewControllerForDetail;

@end

- (UIViewController *)CTMediator_viewControllerForDetail
{
UIViewController *viewController = [self performTarget:kCTMediatorTargetA
action:kCTMediatorActionNativFetchDetailViewController
params:@{@"key":@"value"}
shouldCacheTarget:NO
];
if ([viewController isKindOfClass:[UIViewController class]]) {
// view controller 交付出去之后,可以由外界选择是push还是present
return viewController;
} else {
// 这里处理异常场景,具体如何处理取决于产品
return [[UIViewController alloc] init];
}
}


此种方案的优点是通过Targrt-Action实现了组件之间的解耦,通过暴露方法给组件使用方,避免了url直接传递字典带来的问题。

缺点是:


  • CTMediator类别实现由于需要通过performTarget方式来实现,需要写一堆方法名、方法参数名字字符串,影响阅读;
  • 没有组件管理器概念,组件直接的互相调用都是通过直接引用CTMediator类别来实现,没有实现真正的解耦。


BeeHive


BeeHive通过url来实现页面路由,通过Protocol来实现方法调用。

一、注册service


[[BeeHive shareInstance] registerService:@protocol(HomeServiceProtocol) service:[BHViewController class]];


二、调用service


id< HomeServiceProtocol > homeVc = [[BeeHive shareInstance] createService:@protocol(HomeServiceProtocol)];

// use homeVc do invocation


笔者推荐使用BeeHive这种方式来做组件化,基于Protocol(面向接口)的编程方式能让组件提供方清晰地提供接口声明给使用方;能充分利用编辑器特性,比如如果接口删除了一个参数,能通过编译器编不过来告诉调用方接口发生了变化。

收起阅读 »

iOS面试基础知识 (四)

网络相关做移动开发,除了写UI,大部分的工作就是跟后台做接口联调了,所以网络相关的知识在面试当中是相当重要且必不可少的。Get与Post区别笔者在面试中会经常问这个问题,发现有挺多面试者回答得不好。很多人不知道Get与Post网络请求参数放在哪里。Get请求参...
继续阅读 »

网络相关


做移动开发,除了写UI,大部分的工作就是跟后台做接口联调了,所以网络相关的知识在面试当中是相当重要且必不可少的。


Get与Post区别


笔者在面试中会经常问这个问题,发现有挺多面试者回答得不好。很多人不知道Get与Post网络请求参数放在哪里。

Get请求参数是以kv方式拼在url后面的,虽然http协议对url的长度没有限制,但是浏览器和服务器一般都限制长度;Post请求参数是放在body里面的,对长度没什么限制。


https原理


https与http区别


https是在http的基础上加上ssl形成的协议,http传输数据是明文的,https则是以对称加密的方式传输数据。


https证书校验过程


https采用对称加密传输数据,对称加密需要的密钥由客户端生成,通过非对称加密算法加密传输给后台。具体步骤如下:

1、客户端向服务器发起HTTPS请求,连接到服务器的443端口。

2、服务器有一个用来做非对称加密的密钥对,即公钥和私钥,服务器端保存着私钥,服务器将自己的公钥发送给客户。

3、客户端收到服务器的公钥之后,会对公钥进行检查,验证其合法性,如果发现发现公钥有问题,那么HTTPS传输就无法继续。严格的说,这里应该是验证服务器发送的数字证书的合法性,如果公钥合格,那么客户端会生成一个随机值,这个随机值就是用于进行对称加密的密钥,我们将该密钥称之为client key,然后用服务器的公钥对客户端密钥进行非对称加密,这样客户端密钥就变成密文了。

4、客户端会发起HTTPS中的第二个HTTP请求,将加密之后的客户端密钥发送给服务器。

5、服务器接收到客户端发来的密文之后,会用自己的私钥对其进行非对称解密,解密之后的明文就是客户端密钥,然后用客户端密钥对数据进行对称加密,这样数据就变成了密文。

6、后续客户端和服务器基于client key进行对称加密传输数据。


网络参数签名、加密实现方式


除了用https协议传输数据,有些对数据安全要求比较高的App比如金融类App还会对参数进行签名和加密,这样可以防止网络请求参数被篡改以及敏感业务数据泄露


网络参数签名


为了防止网络请求被篡改,一般会对请求参数进行hash,一般会有一个sign字段表示签名。


假定客户端请求参数dic如下:
{
"name":"akon",
"city":"shenzhen",
}


那么如何生成sign字段呢?

一般通用的做法是把字典按照key的字母升序排序然后拼接起来,然后再进行sha256,再md5。


  • 把字典按照key的字母排序拼接生成字符串str = "city=shenzhen&name=akon"。
  • 对str先进行sha256然后再进行md5生成sign。
    值得注意的是,为了增加破解的难度,我们可以在生成的str前面、后面加入一段我们App特有的字符串,然后对str hash可以采用base64、sha256,md5混合来做。


网络参数加密方式


为了效率,我们一般会采用对称加密加密数据,DES,3DES,AES这些方式都可以。既然要用对称加密,那就涉及到对称加密的密钥怎么生成,有如下方式:


  • 最简单的方式,代码写死密钥。密钥可以用base64或者抑或算法进行简单的加密,用的时候再解密,这种方式比裸写密钥更安全。
  • 后台下发密钥。后台可以在登录的时候下发这个密钥,客户端保存这个密钥后续用来做加密。由于客户端要保存这个密钥,所以还是存在泄露的风险。
  • 仿照https证书校验过程,客户端生成对称加密的密钥clientKey,对参数进行加密,然后用非对称加密对clientKey进行加密生成cryptKey传给后台;后台获取到cryptKey解析出clientKey,然后再用clientKey解密出请求参数。这种方式最安全,推荐使用。


AFNetworking实现原理


作为iOS使用最广泛的第三方网络库,AFNetworking基本上是面试必问的。笔者面试都会问,通过AF的一些问题,可以了解面试者是否熟练使用AF,以及是否阅读过AF的源代码。


AF的设计架构图


如果面试者能把AF的分层架构图清晰地画出来,那至少证明面试者有阅读过AF的源码。


AF关于证书校验是哪个类实现的?有哪几种证书校验方式?


AFSecurityPolicy用来做证书校验的。有三种校验方式:


  • AFSSLPinningModeNone 客户端不进行证书校验,完全信任服务端。
  • AFSSLPinningModePublicKey 客户端对证书进行公钥校验。
  • AFSSLPinningModeCertificate 客户端对整个证书进行校验。


AF请求参数编码、响应参数解码分别是哪两个类实现的?支持什么方式编码,解码?


  • AFHTTPRequestSerializer、AFHTTPResponseSerializer分别用来做编码和解码。
  • 编码方式有url query类型、 json、plist方式。
  • 解码支持NSData、json、xml、image类型。


关于AF如果再深入点可以问问具体实现细节,可以通过细节进一步考察面试者的内功。


SDWebImage实现原理


iOS下载图片基本都用SDWebImage,这个库笔者面试基本都会问。


下载流程


一、先去内存缓存找,找到了直接返回UIImage,否则走第二步;

二、去磁盘缓存里面找,找到了直接返回UIImage,否则走第三步;

三、网络下载,下载完成后存入本地磁盘和内存缓存,然后返回UIImage给调用方。


url生成key的算法是什么?


  • 内存缓存key是url
  • 磁盘缓存key是对url进行md5生成的。


清缓存时机


  • 对于内存缓存,在下载图片加载图片到内存时、内存收到警告时候进行清理。
  • 对于磁盘缓存,在App退出、进后台清理。


网络防劫持策略


H5防劫持


黑客可以通过劫持URL,注入JS代码来劫持H5,可以通过黑名单机制来解决这类问题。


DNS防劫持


DNS的过程其实是域名替换成IP的过程,这个过程如果被黑客劫持,黑客可以返回自己的IP给客户端,从而劫持App。可以通过HTTP DNS方案来解决这个问题。


网络优化


网络优化的核心点是减少网络请求次数和数据传输量。策略有很多,列举一些常用的手段:


合并接口


有些接口可以合并就合并,把几个接口合并成一个接口,可以省去每个接口建立连接的时间以及每个请求传输的http请求头和响应头。


采用pb等省流量传输协议


我们可以采用xml、json、pb等格式传输数据。

这三种方式数据量大小和性能pb>json>xml。


webp


采用webp图片可以节省客户端和服务端的带宽。


采用tcp而不是http


http是基于tcp的应用层协议,相比tcp,http多出来一个几百字节的请求头和响应头,并且每次通信都要建立连接,效率比不上tcp。


同运营商、就近接入


可以根据用户手机的运营商返回相应机房的服务器给客户端,比如联通返回联通的服务器;

可以根据用户所处区域返回相应的服务器给客户端,比如深圳返回深圳机房的服务器。

收起阅读 »

iOS面试基础知识 (三)

iOS
多线程多线程创建方式iOS创建多线程方式主要有NSThread、NSOperation、GCD,这三种方式创建多线程的优缺点如下:NSThreadNSThread 封装了一个线程,通过它可以方便的创建一个线程。NSThread 线程之间的并发控制,是需要我们自...
继续阅读 »

多线程


多线程创建方式


iOS创建多线程方式主要有NSThread、NSOperation、GCD,这三种方式创建多线程的优缺点如下:


NSThread


  • NSThread 封装了一个线程,通过它可以方便的创建一个线程。NSThread 线程之间的并发控制,是需要我们自己来控制的。它的缺点是需要我们自己维护线程的生命周期、线程之间同步等,优点是轻量,灵活。


NSOperation


  • NSOperation 是一个抽象类,它封装了线程的实现细节,不需要自己管理线程的生命周期和线程的同步等,需要和 NSOperationQueue 一起使用。使用 NSOperation ,你可以方便地控制线程,比如取消线程、暂停线程、设置线程的优先级、设置线程的依赖。NSOperation常用于下载库的实现,比如SDWebImage的实现就用到了NSOperation。


GCD


  • GCD(Grand Central Dispatch) 是 Apple 开发的一个多核编程的解决方法。GCD 是一个可以替代 NSThread 的很高效和强大的技术。在平常开发过程中,我们用的最多的就是GCD。哦,对了,NSOperation是基于GCD实现的。


多线程同步


多线程情况下访问共享资源需要进行线程同步,线程同步一般都用锁实现。从操作系统层面,锁的实现有临界区、事件、互斥量、信号量等。这里讲一下iOS中多线程同步的方式。


atomic


属性加上atomic关键字,编译器会自动给该属性生成代码用以多线程访问同步,它并不能保证使用属性的过程是线程安全的。一般我们在定义属性的时候用nonatomic,避免性能损失。


@synchronized


@synchronized指令是一个对象锁,用起来非常简单。使用obj为该锁的唯一标识,只有当标识相同时,才为满足互斥,如果线程1和线程2中的@synchronized后面的obj不相同,则不会互斥。@synchronized其实是对pthread_mutex递归锁的封装。

@synchronized优点是我们不需要在代码中显式的创建锁对象,使用简单; 缺点是@synchronized会隐式的添加一个异常处理程序,该异常处理程序会在异常抛出的时候自动的释放互斥锁,从而带来额外开销。


NSLock


最简单的锁,调用lock获取锁,unlock释放锁。如果其它线程已经调用lock获取了锁,当前线程调用lock方法会阻塞当前线程,直到其它线程调用unlock释放锁为止。NSLock使用简单,在项目中用的最多。


NSRecursiveLock


递归锁主要用来解决同一个线程频繁获取同一个锁而不造成死锁的问题。注意lock和unlock调用必须配对。


NSConditionLock


条件锁,可以设置自定义条件来获取锁。比如生产者消费者模型可以用条件锁来实现。


NSCondition


条件,操作系统中信号量的实现,方法- (void)wait和- (BOOL)waitUntilDate:(NSDate *)limit用来等待锁直至锁有信号;方法- (void)signal和- (void)broadcast使condition有信号,通知等待condition的线程,变成非阻塞状态。


dispatch_semaphore_t


信号量的实现,可以实现控制GCD队列任务的最大并发量,类似于NSOperationQueue的maxConcurrentOperationCount属性。


pthread_mutex


mutex叫做”互斥锁”,等待锁的线程会处于休眠状态。使用pthread_mutex_init创建锁,使用pthread_mutex_lock和pthread_mutex_unlock加锁和解锁。注意:mutex可以通过PTHREAD_MUTEX_RECURSIVE创建递归锁,防止重复获取锁导致死锁


 //创建锁,注意:mutex可以通过PTHREAD_MUTEX_RECURSIVE创建递归锁,防止重复获取锁导致死锁
pthread_mutexattr_t recursiveAttr;
pthread_mutexattr_init(&recursiveAttr);
pthread_mutexattr_settype(&recursiveAttr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(self.mutex, &recursiveAttr);
pthread_mutexattr_destroy(&recursiveAttr);

pthread_mutex_lock(&self.mutex)
//访问共享数据代码
pthread_mutex_unlock(&self.mutex)


OSSpinLock


OSSpinLock 是自旋锁,等待锁的线程会处于忙等状态。一直占用着 CPU。自旋锁就好比写了个 while,whil(被加锁了) ; 不断的忙等,重复这样。OSSpinLock是不安全的锁(会造成优先级反转),什么是优先级反转,举个例子:

有线程1和线程2,线程1的优先级比较高,那么cpu分配给线程1的时间就比较多,自旋锁可能发生优先级反转问题。如果优先级比较低的线程2先加锁了,紧接着线程1进来了,发现已经被加锁了,那么线程1忙等,while(未解锁); 不断的等待,由于线程1的优先级比较高,CPU就一直分配之间给线程1,就没有时间分配给线程2,就有可能导致线程2的代码就没有办法往下走,就会造成线程2没有办法解锁,所以这个锁就不安全了。

建议不要使用OSSpinLock,用os_unfair_lock来代替。


//初始化
OSSpinLock lock = OS_SPINLOCK_INIT;
//加锁
OSSpinLockLock(&lock);
//解锁
OSSpinLockUnlock(&lock);


os_unfair_lock


os_unfair_lock用于取代不安全的OSSpinLock,从iOS10开始才支持 从底层调用看,等待os_unfair_lock锁的线程会处于休眠状态,并非忙等


//初始化
os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
//加锁
os_unfair_lock_lock(&lock);
//解锁
os_unfair_lock_unlock(&lock);


性能


性能从高到低排序

1、os_unfair_lock

2、OSSpinLock

3、dispatch_semaphore

4、pthread_mutex

5、NSLock

6、NSCondition

7、pthread_mutex(recursive)

8、NSRecursiveLock

9、NSConditionLock

10、@synchronized


JSON Model互转


项目中JSON Model转换方式


平常开发过程中,经常需要进行JSON与Model互转,尤其是接口数据转换。我们可以手动解析,也可以用MJExtension、YYModel这些第三方库,用第三方库最大的好处他可以自动给你转换并且处理类型不匹配等异常情况,从而避免崩溃。


MJExtension实现原理


假定后台返回的字典dic为:
{
"name":"akon",
"address":"shenzhen",
}

我们自定义了一个类UserModel
@interface UserModel : NSObject

@property (nonatomic, strong)NSString* name;
@property (nonatomic, strong)NSString* address;

@end


  • MJExtension是如何做属性映射的?
    MJExtension在遍历dic属性时,比如遍历到name属性时,先去缓存里查找这个类是否有这个属性,有就赋值akon。没有就遍历UserModel的属性列表,把这个类的属性列表加入到缓存中,查看这个类有没有定义name属性,如果有,就把akon赋给这个属性,否则不赋值。
  • MJExtension是如何给属性赋值的?
    利用KVC机制,在查找到UserModel有name的属性,使用[self setValue:@"akon" forKey:@"name"]进行赋值。
  • 如何获取类的属性列表?
    通过class_copyPropertyList方法
  • 如何遍历成员变量列表?
    通过class_copyIvarList方法


数据存储方式


iOS常见数据存储方式及使用场景


iOS中可以采用NSUserDefaults、Archive、plist、数据库等方式等来存储数据,以上存储方式使用的业务场景如下:


  • NSUserDefaults一般用来存储一些简单的App配置。比如存储用户姓名、uid这类轻量的数据。
  • Archive可以用来存储model,如果一个model要用Archive存储,需要实现NSCoding协议。
  • plist存储方式。像NSString、NSDictionary等类都可以直接存调用writeToFile:atomically:方法存储到plist文件中。
    -数据库存储方式。大量的数据存储,比如消息列表、网络数据缓存,需要采用数据库存储。可以用FMDB、CoreData、WCDB、YYCache来进行数据库存储。建议使用WCDB来进行数据库存储,因为WCDB是一个支持orm,支持加密,多线程安全的高性能数据库。


数据库操作


笔者在面试中,一般会问下面试者数据库的操作,以此开考察一下面试者对于数据库操作的熟练程度。


  • 考察常用crud语句书写。
    创建表、给表增加字段、插入、删除、更新、查询SQL怎么写。尤其是查询操作,可以考察order by, group by ,distinct, where匹配以及联表查询等技巧。
  • SQL语句优化技巧。如索引、事务等常用优化技巧。
  • 怎么分库、分表?
  • FMDB或者WCDB(orm型)实现原理。
  • 怎么实现数据库版本迁移?
收起阅读 »

iOS面试基础知识 (二)

iOS
一、类别OC不像C++等高级语言能直接继承多个类,不过OC可以使用类别和协议来实现多继承。1、类别加载时机在App加载时,Runtime会把Category的实例方法、协议以及属性添加到类上;把Category的类方法添加到类的metaclass上。2、类别添...
继续阅读 »

一、类别


OC不像C++等高级语言能直接继承多个类,不过OC可以使用类别和协议来实现多继承。


1、类别加载时机


在App加载时,Runtime会把Category的实例方法、协议以及属性添加到类上;把Category的类方法添加到类的metaclass上。


2、类别添加属性、方法


1)在类别中不能直接以@property的方式定义属性,OC不会主动给类别属性生成setter和getter方法;需要通过objc_setAssociatedObject来实现。


@interface TestClass(ak)

@property(nonatomic,copy) NSString *name;

@end

@implementation TestClass (ak)

- (void)setName:(NSString *)name{

objc_setAssociatedObject(self, "name", name, OBJC_ASSOCIATION_COPY);
}

- (NSString*)name{
NSString *nameObject = objc_getAssociatedObject(self, "name");
return nameObject;
}


2)类别同名方法覆盖问题


  • 如果类别和主类都有名叫funA的方法,那么在类别加载完成之后,类的方法列表里会有两个funA;
  • 类别的方法被放到了新方法列表的前面,而主类的方法被放到了新方法列表的后面,这就造成了类别方法会“覆盖”掉原来类的同名方法,这是因为运行时在查找方法的时候是顺着方法列表的顺序查找的,它只要一找到对应名字的方法,就会停止查找,殊不知后面可能还有一样名字的方法;
  • 如果多个类别定义了同名方法funA,具体调用哪个类别的实现由编译顺序决定,后编译的类别的实现将被调用。
  • 在日常开发过程中,类别方法重名轻则造成调用不正确,重则造成crash,我们可以通过给类别方法名加前缀避免方法重名。


关于类别更深入的解析可以参见美团的技术文章深入理解Objective-C:Category


二、协议


定义


iOS中的协议类似于Java、C++中的接口类,协议在OC中可以用来实现多继承和代理。


方法声明


协议中的方法可以声明为@required(要求实现,如果没有实现,会发出警告,但编译不报错)或者@optional(不要求实现,不实现也不会有警告)。

笔者经常会问面试者如下两个问题:

-怎么判断一个类是否实现了某个协议?很多人不知道可以通过conformsToProtocol来判断。

-假如你要求业务方实现一个delegate,你怎么判断业务方有没有实现dalegate的某个方法?很多人不知道可以通过respondsToSelector来判断。


三、通知中心


iOS中的通知中心实际上是观察者模式的一种实现。


postNotification是同步调用还是异步调用?


同步调用。当调用addObserver方法监听通知,然后调用postNotification抛通知,postNotification会在当前线程遍历所有的观察者,然后依次调用观察者的监听方法,调用完成后才会去执行postNotification后面的代码。


如何实现异步监听通知?


通过addObserverForName:object:queue:usingBlock来实现异步通知。


四、KVC


KVC查找顺序


1)调用setValue:forKey时候,比如[obj setValue:@"akon" forKey:@"key"]时候,会按照key,iskey,key,iskey的顺序搜索成员并进行赋值操作。如果都没找到,系统会调用该对象的setValue:forUndefinedKey方法,该方法默认是抛出异常。

2)当调用valueForKey:@"key"的代码时,KVC对key的搜索方式不同于setValue"akon" forKey:@"key",其搜索方式如下:


  • 首先按get, is的顺序查找getter方法,找到的话会直接调用。如果是BOOL或者Int等值类型,会将其包装成一个NSNumber对象。
  • 如果没有找到,KVC则会查找countOf、objectInAtIndex或AtIndexes格式的方法。如果countOf方法和另外两个方法中的一个被找到,那么就会返回一个可以响应NSArray所有方法的代理集合(它是NSKeyValueArray,是NSArray的子类),调
    用这个代理集合的方法,就会以countOf,objectInAtIndex或AtIndexes这几个方法组合的形式调用。还有一个可选的get:range:方法。所以你想重新定义KVC的一些功能,你可以添加这些方法,需要注意的是你的方法名要符合KVC的标准命名方法,包括方法签名。
    -如果上面的方法没有找到,那么会同时查找countOf,enumeratorOf,memberOf格式的方法。如果这三个方法都找到,那么就返回一个可以响应NSSet所的方法的代理集合,和上面一样,给这个代理集合发NSSet的消息,就会以countOf,enumeratorOf,memberOf组合的形式调用。
  • 如果还没有找到,再检查类方法+ (BOOL)accessInstanceVariablesDirectly,如果返回YES(默认行为),那么和先前的设值一样,会按,is,,is的顺序搜索成员变量名。
  • 如果还没找到,直接调用该对象的valueForUndefinedKey:方法,该方法默认是抛出异常。


KVC防崩溃


我们经常会使用KVC来设置属性和获取属性,但是如果对象没有按照KVC的规则声明该属性,则会造成crash,怎么全局通用地防止这类崩溃呢?

可以通过写一个NSObject分类来防崩溃。


@interface NSObject(AKPreventKVCCrash)

@end

@ implementation NSObject(AKPreventKVCCrash)

- (void)setValue:(id)value forUndefinedKey:(NSString *)key{
}

- (id)valueForUndefinedKey:(NSString *)key{

return nil;
}

@end


五、KVO


定义


KVO(Key-Value Observing),键值观察。它是一种观察者模式的衍生。其基本思想是,对目标对象的某属性添加观察,当该属性发生变化时,通过触发观察者对象实现的KVO接口方法,来自动的通知观察者。


注册、移除KVO


通过如下两个方案来注册、移除KVO


- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;


通过observeValueForKeyPath来获取值的变化。


- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context


我们可以通过facebook开源库KVOController方便地进行KVO。


KVO实现


苹果官方文档对KVO实现介绍如下:


Key-Value Observing Implementation Details

Automatic key-value observing is implemented using a technique called isa-swizzling.

The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.

When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.

You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.


即当一个类型为 ObjectA 的对象,被添加了观察后,系统会生成一个派生类 NSKVONotifying_ObjectA 类,并将对象的isa指针指向新的类,也就是说这个对象的类型发生了变化。因此在向ObjectA对象发送消息时候,实际上是发送到了派生类对象的方法。由于编译器对派生类的方法进行了 override,并添加了通知代码,因此会向注册的对象发送通知。注意派生类只重写注册了观察者的属性方法。


关于kvc和kvo更深入的详解参考iOS KVC和KVO详解


六、autorelasepool


用处


在 ARC 下,我们不需要手动管理内存,可以完全不知道 autorelease 的存在,就可以正确管理好内存,因为 Runloop 在每个 Runloop Circle 中会自动创建和释放Autorelease Pool。

当我们需要创建和销毁大量的对象时,使用手动创建的 autoreleasepool 可以有效的避免内存峰值的出现。因为如果不手动创建的话,外层系统创建的 pool 会在整个 Runloop Circle 结束之后才进行 drain,手动创建的话,会在 block 结束之后就进行 drain 操作,比如下面例子:


for (int i = 0; i < 100000; i++)
{
@autoreleasepool
{
NSString* string = @"akon";
NSArray* array = [string componentsSeparatedByString:string];
}
}


比如SDWebImage中这段代码,由于encodedDataWithImage会把image解码成data,可能造成内存暴涨,所以加autoreleasepool避免内存暴涨


 @autoreleasepool {
NSData *data = imageData;
if (!data && image) {
// If we do not have any data to detect image format, check whether it contains alpha channel to use PNG or JPEG format
SDImageFormat format;
if ([SDImageCoderHelper CGImageContainsAlpha:image.CGImage]) {
format = SDImageFormatPNG;
} else {
format = SDImageFormatJPEG;
}
data = [[SDImageCodersManager sharedManager] encodedDataWithImage:image format:format options:nil];
}
[self _storeImageDataToDisk:data forKey:key];
}


Runloop中自动释放池创建和释放时机


  • 系统在 Runloop 中创建的 autoreleaspool 会在 Runloop 一个 event 结束时进行释放操作。
  • 我们手动创建的 autoreleasepool 会在 block 执行完成之后进行 drain 操作。需要注意的是:
    当 block 以异常结束时,pool 不会被 drain
    Pool 的 drain 操作会把所有标记为 autorelease 的对象的引用计数减一,但是并不意味着这个对象一定会被释放掉,我们可以在 autorelease pool 中手动 retain 对象,以延长它的生命周期(在 MRC 中)。


收起阅读 »

iOS面试基础知识 (一)

iOS
iOS面试基础知识 (一)一、Runtime原理Runtime是iOS核心运行机制之一,iOS App加载库、加载类、执行方法调用,全靠Runtime,这一块的知识个人认为是最基础的,基本面试必问。1、Runtime消息发送机制1)iOS调用一个方法时,实际上...
继续阅读 »

iOS面试基础知识 (一)


一、Runtime原理


Runtime是iOS核心运行机制之一,iOS App加载库、加载类、执行方法调用,全靠Runtime,这一块的知识个人认为是最基础的,基本面试必问。


1、Runtime消息发送机制


1)iOS调用一个方法时,实际上会调用objc_msgSend(receiver, selector, arg1, arg2, ...),该方法第一个参数是消息接收者,第二个参数是方法名,剩下的参数是方法参数;

2)iOS调用一个方法时,会先去该类的方法缓存列表里面查找是否有该方法,如果有直接调用,否则走第3)步;

3)去该类的方法列表里面找,找到直接调用,把方法加入缓存列表;否则走第4)步;

4)沿着该类的继承链继续查找,找到直接调用,把方法加入缓存列表;否则消息转发流程;

很多面试者大体知道这个流程,但是有关细节不是特别清楚。


  • 问他/她objc_msgSend第一个参数、第二个参数、剩下的参数分别代表什么,不知道;
  • 很多人只知道去方法列表里面查找,不知道还有个方法缓存列表。
    通过这些细节,可以了解一个人是否真正掌握了原理,而不是死记硬背。


2、Runtime消息转发机制


如果在消息发送阶段没有找到方法,iOS会走消息转发流程,流程图如下所示:


1)动态消息解析。检查是否重写了resolveInstanceMethod 方法,如果返回YES则可以通过class_addMethod 动态添加方法来处理消息,否则走第2)步;

2)消息target转发。forwardingTargetForSelector 用于指定哪个对象来响应消息。如果返回nil 则走第3)步;

3)消息转发。这步调用 methodSignatureForSelector 进行方法签名,这可以将函数的参数类型和返回值封装。如果返回 nil 执行第四步;否则返回 methodSignature,则进入 forwardInvocation ,在这里可以修改实现方法,修改响应对象等,如果方法调用成功,则结束。否则执行第4)步;

4)报错 unrecognized selector sent to instance。

很多人知道这四步,但是笔者一般会问:


  • 怎么在项目里全局解决"unrecognized selector sent to instance"这类crash?本人发现很多人回答不出来,说明面试者肯定是在死记硬背,你都知道因为消息转发那三步都没处理才会报错,为什么不知道在消息转发里面处理呢?
  • 如果面试者知道可以在消息转发里面处理,防止崩溃,再问下面试者,你项目中是在哪一步处理的,看看其是否有真正实践过?


二、load与initialize


1、load与initialize调用时机


+load在main函数之前被Runtime调用,+initialize 方法是在类或它的子类收到第一条消息之前被调用的,这里所指的消息包括实例方法和类方法的调用。


2、load与initialize在分类、继承链的调用顺序


  • load方法的调用顺序为:
    子类的 +load 方法会在它的所有父类的 +load 方法之后执行,而分类的 +load 方法会在它的主类的 +load 方法之后执行。
    如果子类没有实现 +load 方法,那么当它被加载时 runtime 是不会去调用父类的 +load 方法的。同理,当一个类和它的分类都实现了 +load 方法时,两个方法都会被调用。
  • initialize的调用顺序为:
    +initialize 方法的调用与普通方法的调用是一样的,走的都是消息发送的流程。如果子类没有实现 +initialize 方法,那么继承自父类的实现会被调用;如果一个类的分类实现了 +initialize 方法,那么就会对这个类中的实现造成覆盖。
  • 怎么确保在load和initialize的调用只执行一次
    由于load和initialize可能会调用多次,所以在这两个方法里面做的初始化操作需要保证只初始化一次,用dispatch_once来控制


笔者在面试过程中发现很多人对于load与initialize在分类、继承链的调用顺序不清楚。对怎么保证初始化安全也不清楚


三、RunLoop原理


RunLoop苹果原理图



图中展现了 Runloop 在线程中的作用:从 input source 和 timer source 接受事件,然后在线程中处理事件。


1、RunLoop与线程关系


  • 一个线程是有一个RunLoop还是多个RunLoop? 一个;
  • 怎么启动RunLoop?主线程的RunLoop自动就开启了,子线程的RunLoop通过Run方法启动。


2、Input Source 和 Timer Source


两个都是 Runloop 事件的来源,其中 Input Source 又可以分为三类


  • Port-Based Sources,系统底层的 Port 事件,例如 CFSocketRef ,在应用层基本用不到;
  • Custom Input Sources,用户手动创建的 Source;
  • Cocoa Perform Selector Sources, Cocoa 提供的 performSelector 系列方法,也是一种事件源;
    Timer Source指定时器事件,该事件的优先级是最低的。
    本人一般会问定时器事件的优先级是怎么样的,大部分人回答不出来。


3、解决NSTimer事件在列表滚动时不执行问题


因为定时器默认是运行在NSDefaultRunLoopMode,在列表滚动时候,主线程会切换到UITrackingRunLoopMode,导致定时器回调得不到执行。

有两种解决方案:


  • 指定NSTimer运行于 NSRunLoopCommonModes下。
  • 在子线程创建和处理Timer事件,然后在主线程更新 UI。


四、事件分发机制及响应者链


1、事件分发机制


iOS 检测到手指触摸 (Touch) 操作时会将其打包成一个 UIEvent 对象,并放入当前活动Application的事件队列,UIApplication 会从事件队列中取出触摸事件并传递给单例的 UIWindow 来处理,UIWindow 对象首先会使用 hitTest:withEvent:方法寻找此次Touch操作初始点所在的视图(View),即需要将触摸事件传递给其处理的视图,这个过程称之为 hit-test view。

hitTest:withEvent:方法的处理流程如下:


  • 首先调用当前视图的 pointInside:withEvent: 方法判断触摸点是否在当前视图内;
  • 若返回 NO, 则 hitTest:withEvent: 返回 nil,若返回 YES, 则向当前视图的所有子视图 (subviews) 发送 hitTest:withEvent: 消息,所有子视图的遍历顺序是从最顶层视图一直到到最底层视图(后加入的先遍历),直到有子视图返回非空对象或者全部子视图遍历完毕;
  • 若第一次有子视图返回非空对象,则 hitTest:withEvent: 方法返回此对象,处理结束;
  • 如所有子视图都返回空,则 hitTest:withEvent: 方法返回自身 (self)。
    流程图如下:


2、响应者链原理


iOS的事件分发机制是为了找到第一响应者,事件的处理机制叫做响应者链原理。

所有事件响应的类都是 UIResponder 的子类,响应者链是一个由不同对象组成的层次结构,其中的每个对象将依次获得响应事件消息的机会。当发生事件时,事件首先被发送给第一响应者,第一响应者往往是事件发生的视图,也就是用户触摸屏幕的地方。事件将沿着响应者链一直向下传递,直到被接受并做出处理。一般来说,第一响应者是个视图对象或者其子类对象,当其被触摸后事件被交由它处理,如果它不处理,就传递给它的父视图(superview)对象(如果存在)处理,如果没有父视图,事件就会被传递给它的视图控制器对象 ViewController(如果存在),接下来会沿着顶层视图(top view)到窗口(UIWindow 对象)再到程序(UIApplication 对象)。如果整个过程都没有响应这个事件,该事件就被丢弃。一般情况下,在响应者链中只要由对象处理事件,事件就停止传递。

一个典型的事件响应路线如下:

First Responser --> 父视图-->The Window --> The Application --> nil(丢弃)

我们可以通过 [responder nextResponder] 找到当前 responder 的下一个 responder,持续这个过程到最后会找到 UIApplication 对象。


五、内存泄露检测与循环引用


1、造成内存泄露原因


  • 在用C/C++时,创建对象后未销毁,比如调用malloc后不free、调用new后不delete;
  • 调用CoreFoundation里面的C方法后创建对对象后不释放。比如调用CGImageCreate不调用CGImageRelease;
  • 循环引用。当对象A和对象B互相持有的时候,就会产生循环引用。常见产生循环引用的场景有在VC的cellForRowAtIndexPath方法中cell block引用self。


2、常见循环引用及解决方案


1) 在VC的cellForRowAtIndexPath方法中cell的block直接引用self或者直接以_形式引用属性造成循环引用。


 cell.clickBlock = ^{
self.name = @"akon";
};

cell.clickBlock = ^{
_name = @"akon";
};


解决方案:把self改成weakSelf;


__weak typeof(self)weakSelf = self;
cell.clickBlock = ^{
weakSelf.name = @"akon";
};


2)在cell的block中直接引用VC的成员变量造成循环引用。


//假设 _age为VC的成员变量
@interface TestVC(){

int _age;

}
cell.clickBlock = ^{
_age = 18;
};


解决方案有两种:


  • 用weak-strong dance


__weak typeof(self)weakSelf = self;
cell.clickBlock = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
strongSelf->age = 18;
};


  • 把成员变量改成属性


//假设 _age为VC的成员变量
@interface TestVC()

@property(nonatomic, assign)int age;

@end

__weak typeof(self)weakSelf = self;
cell.clickBlock = ^{
weakSelf.age = 18;
};


3)delegate属性声明为strong,造成循环引用。


@interface TestView : UIView

@property(nonatomic, strong)id<TestViewDelegate> delegate;

@end

@interface TestVC()<TestViewDelegate>

@property (nonatomic, strong)TestView* testView;

@end

testView.delegate = self; //造成循环引用


解决方案:delegate声明为weak


@interface TestView : UIView

@property(nonatomic, weak)id<TestViewDelegate> delegate;

@end


4)在block里面调用super,造成循环引用。


cell.clickBlock = ^{
[super goback]; //造成循环应用
};


解决方案,封装goback调用


__weak typeof(self)weakSelf = self;
cell.clickBlock = ^{
[weakSelf _callSuperBack];
};

- (void) _callSuperBack{
[self goback];
}


5)block声明为strong

解决方案:声明为copy

6)NSTimer使用后不invalidate造成循环引用。

解决方案:


  • NSTimer用完后invalidate;
  • NSTimer分类封装


+ (NSTimer *)ak_scheduledTimerWithTimeInterval:(NSTimeInterval)interval
block:(void(^)(void))block
repeats:(BOOL)repeats{

return [self scheduledTimerWithTimeInterval:interval
target:self
selector:@selector(ak_blockInvoke:)
userInfo:[block copy]
repeats:repeats];
}

+ (void)ak_blockInvoke:(NSTimer*)timer{

void (^block)(void) = timer.userInfo;
if (block) {
block();
}
}

--



3、怎么检测循环引用


  • 静态代码分析。 通过Xcode->Product->Anaylze分析结果来处理;
  • 动态分析。用MLeaksFinder(只能检测OC泄露)或者Instrument或者OOMDetector(能检测OC与C++泄露)。


六、VC生命周期


考察viewDidLoad、viewWillAppear、ViewDidAppear等方法的执行顺序。

假设现在有一个 AViewController(简称 Avc) 和 BViewController (简称 Bvc),通过 navigationController 的push 实现 Avc 到 Bvc 的跳转,调用顺序如下:

1、A viewDidLoad 

2、A viewWillAppear 

3、A viewDidAppear 

4、B viewDidLoad 

5、A viewWillDisappear 

6、B viewWillAppear 

7、A viewDidDisappear 

8、B viewDidAppear

如果再从 Bvc 跳回 Avc,调用顺序如下:

1、B viewWillDisappear 

2、A viewWillAppear 

3、B viewDidDisappear 

4、A viewDidAppear

收起阅读 »

微信小程序如何确保每个页面都已经登陆

现状 一个微信小程序中,有首页,有个人页面,还有一些列表页面,详情页面等等,这些页面大部分是可以分享的。当分享出去的页面被一个另一个用户打开的时候,这个页面怎么确保这个用户已经登陆了呢? 网上有很多方案是在请求封装里面加一道拦截,如果没有token,就先调用登...
继续阅读 »

现状


一个微信小程序中,有首页,有个人页面,还有一些列表页面,详情页面等等,这些页面大部分是可以分享的。当分享出去的页面被一个另一个用户打开的时候,这个页面怎么确保这个用户已经登陆了呢?


网上有很多方案是在请求封装里面加一道拦截,如果没有token,就先调用登陆请求获取token后,再继续。
这种方案没毛病,只要注意一点,当一个页面有多个请求同时触发时,当所有请求拦截后,放到一个数组里面,在获取token成功后,遍历数组一个个请求就行。


但这个需求再复杂一点,比如连锁便利店小程序,大部分页面都需要有一个门店(因为需要根据门店获取当前门店商品的库存、价格等),这个门店是根据当前的定位来调用后台接口获得的,这个时候如果在请求里进行封装就太麻烦了。


解决方案


首先,我们注意到,登陆,获取定位与我们的页面请求是异步的,我们需要保证页面请求是在登陆和获取定位之后,但要是我们每个页面都写一个遍,可维护性就太差了。所以我们可以抽离出一个方法来做这件事。
所以代码就这样了:


const app = getApp()
Page({
data: {
logs: []
},
onLoad() {
app.commonLogin(()=>{
// 处理页页面请求
})
}
})

做到这里好像是解决我们的问题,但再想一想,如果还想做更多的事,比如说每个页面的onShareAppMessage统一处理,但我又不想在每个页面再写一遍,另外,我又想自己对每个页面实现一个watch,怎么做?


进一步解决方案


我们可以看到微信小程序,每个页面是一个Page(),那么我们可以给这个Page外面加一层壳子,我们可以有一个MyPage来替换这个Page,废话不多说,上代码:


tool.js 相关代码


/**
* 处理合并参数
*/
handlePageParamMerge(arg) {
let numargs = arg.length; // 获取被传递参数的数值。
let data = {}
let page = {}
for (let ix in arg) {
let item = arg[ix]
if (item.data && typeof (item.data) === 'object') {
data = Object.assign(data, item.data)
}
if (item.methods && typeof (item.methods) === 'object') {
page = Object.assign(page, item.methods)
} else {
page = Object.assign(page, item)
}
}
page.data = data
return page
}

/***
* 合并页面方法以及数据, 兼容 {data:{}, methods: {}} 或 {data:{}, a:{}, b:{}}
*/
mergePage() {
return this.handlePageParamMerge(arguments)
}

/**
* 处理组件参数合并
*/
handleCompParamMerge(arg) {
let numargs = arg.length; // 获取被传递参数的数值。
let data = {}
let options = {}
let properties = {}
let methods = {}
let comp = {}
for (let ix in arg) {
let item = arg[ix]
// 合并组件的初始数据
if (item.data && typeof (item.data) === 'object') {
data = Object.assign(data, item.data)
}
// 合并组件的属性列表
if (item.properties && typeof (item.properties) === 'object') {
properties = Object.assign(properties, item.properties)
}
// 合组件的方法列表
if (item.methods && typeof (item.methods) === 'object') {
methods = Object.assign(methods, item.methods)
}
if (item.options && typeof (item.options) === 'object') {
options = Object.assign(options, item.options)
}
comp = Object.assign(comp, item)
}
comp.data = data
comp.options = options
comp.properties = properties
comp.methods = methods
return comp
}

/**
* 组件混合 {properties: {}, options: {}, data:{}, methods: {}}
*/
mergeComponent() {
return this.handleCompParamMerge(arguments)
}

/***
* 合成带watch的页面
*/
newPage() {
let options = this.handlePageParamMerge(arguments)
let that = this
let app = getApp()

//增加全局点击登录判断
if (!options.publicCheckLogin){
options.publicCheckLogin = function (e) {
let pages = getCurrentPages()
let page = pages[pages.length - 1]
let dataset = e.currentTarget.dataset
let callback = null

//获取回调方法
if (dataset.callback && typeof (page[dataset.callback]) === "function"){
callback = page[dataset.callback]
}
// console.log('callback>>', callback, app.isRegister())
//判断是否登录
if (callback && app.isRegister()){
callback(e)
}
else{
wx.navigateTo({
url: '/pages/login/login'
})
}
}
}

const { onLoad } = options
options.onLoad = function (arg) {
options.watch && that.setWatcher(this)
onLoad && onLoad.call(this, arg)
}

const { onShow } = options
options.onShow = function (arg) {
if (options.data.noAutoLogin || app.isRegister()) {
onShow && onShow.call(this, arg)
//页面埋点
app.ga({})
}
else {
wx.navigateTo({
url: '/pages/login/login'
})
}
}

return Page(options)
}

/**
* 合成带watch等的组件
*/
newComponent() {
let options = this.handleCompParamMerge(arguments)
let that = this
const { ready } = options
options.ready = function (arg) {
options.watch && that.setWatcher(this)
ready && ready.call(this, arg)
}
return Component(options)
}

/**
* 设置监听器
*/
setWatcher(page) {
let data = page.data;
let watch = page.watch;
Object.keys(watch).forEach(v => {
let key = v.split('.'); // 将watch中的属性以'.'切分成数组
let nowData = data; // 将data赋值给nowData
for (let i = 0; i < key.length - 1; i++) { // 遍历key数组的元素,除了最后一个!
nowData = nowData[key[i]]; // 将nowData指向它的key属性对象
}

let lastKey = key[key.length - 1];
// 假设key==='my.name',此时nowData===data['my']===data.my,lastKey==='name'
let watchFun = watch[v].handler || watch[v]; // 兼容带handler和不带handler的两种写法
let deep = watch[v].deep; // 若未设置deep,则为undefine
this.observe(nowData, lastKey, watchFun, deep, page); // 监听nowData对象的lastKey
})
}

/**
* 监听属性 并执行监听函数
*/
observe(obj, key, watchFun, deep, page) {
var val = obj[key];
// 判断deep是true 且 val不能为空 且 typeof val==='object'(数组内数值变化也需要深度监听)
if (deep && val != null && typeof val === 'object') {
Object.keys(val).forEach(childKey => { // 遍历val对象下的每一个key
this.observe(val, childKey, watchFun, deep, page); // 递归调用监听函数
})
}
var that = this;
Object.defineProperty(obj, key, {
configurable: true,
enumerable: true,
set: function (value) {
if (val === value) {
return
}
// 用page对象调用,改变函数内this指向,以便this.data访问data内的属性值
watchFun.call(page, value, val); // value是新值,val是旧值
val = value;
if (deep) { // 若是深度监听,重新监听该对象,以便监听其属性。
that.observe(obj, key, watchFun, deep, page);
}
},
get: function () {
return val;
}
})
}

页面代码:


app.tool.newPage({
data: {
// noAutoLogin: false
},
onShow: function () {
// 在这里写页面请求逻辑
}
}

最后


代码是在线上跑了很久的,tool里的newPage封装,你可以根据自己的需求进行添加。总之,我这里是提供一种思路,如有更佳,欢迎分享。


作者:盗道
链接:https://juejin.cn/post/7026544177844355103

收起阅读 »

你写过的所有代码都逃不过这两方面:API 和抽象

作为前端,你可能开发过 Electron 桌面应用、小程序、浏览器上的 web 应用、基于 React Native 等跨端引擎的 app,基于 Node.js 的工具或者服务等各种应用,这些都是 JS 的不同的 runtime,开发也都是基于前端那套技术。 ...
继续阅读 »

作为前端,你可能开发过 Electron 桌面应用、小程序、浏览器上的 web 应用、基于 React Native 等跨端引擎的 app,基于 Node.js 的工具或者服务等各种应用,这些都是 JS 的不同的 runtime,开发也都是基于前端那套技术。


面对这么多的细分领域,作为前端工程师的你是否曾迷茫过:这么多技术我该学什么?他们中有没有什么本质的东西呢?


其实所有的这些技术,你写过的所有代码,都可以分为两个方面: api 和 抽象。


api


不同平台提供的 api 不同,支持的能力不同:


浏览器提供了 dom api、支持了 css 的渲染,还提供了音视频、webgl 等相关 api,这些 api 是我们开发前端应用的基础。


Node.js 提供了操作系统能力的 api,比如进程、线程、网络、文件等,这些 api 是我们开发工具链或后端应用的基础。


React Native 等跨端引擎支持了 css 的渲染,还提供了设备能力的 api,比如照相机、闪光灯、传感器、GPS 等 api,这是我们开发移动 app 的基础。


Electron 集成了 Chromium 和 Node.js,同时还提供了桌面相关的 api。


小程序支持了 css 的渲染之外,还提供了一些宿主 app 能力的 api。


此外,还有很多的 runtime,比如 vscode 插件、sketch 插件等,都有各自能够使用的 api。


不同的 JS runtime 提供了不同 api 给上层应用,这是应用开发的基础,也是应用开发的能力边界。


抽象


基于 runtime 提供的 api 我们就能完成应用的功能开发,但是复杂场景下往往会做一些抽象。


比如浏览器上的前端应用主要是把数据通过 dom api 和 css 渲染出来,并做一些交互,那么我们就抽象出了数据驱动的前端框架,抽象出了组件、状态、数据流等概念。之后就可以把不同的需求抽象为不同的组件、状态。


经过层层抽象之后,开发复杂前端应用的时候代码更容易维护、成本更低。


比如基于 Node.js 的 fs、net、http 等 api 我们就能实现 web server,但是对于复杂的企业级应用,我们通过后端框架做 MVC 的抽象,抽象出控制器、服务、模型、视图等概念。之后的后端代码就可以把需求抽象为不同的控制器和服务。


经过 MVC 的抽象之后,后端应用的分层更清晰、更容易维护和扩展。


复杂的应用需要在 api 的基础上做一些抽象。我们往往会用框架做一层抽象,然后自己再做一层抽象,经过层层抽象之后的代码是更容易维护和扩展的。这也就是所谓的架构。


如何深入 api 和抽象


api


api 是对操作系统能力或不同领域能力的封装。


比如 Node.js 的进程、线程、文件、网络的 api 是对操作系统能力的封装,想深入它们就要去学习操作系统的一些原理。


而 webgl、音视频等 api 则分别是对图形学、音视频等领域的能力的封装,想要深入它们就要去学习这些领域的一些原理。


个人觉得我们知道 api 提供了什么能力就行,没必要过度深入 api 的实现原理。


抽象


抽象是基于编程语言的编程范式,针对不同目标做的设计。


Javascript 提供了面向对象、函数式等编程范式,那么就可以基于对象来做抽象,使用面向对象的各种设计模式,或者基于函数式那一套。这是抽象的基础。


抽象是根据不同的目标来做的。


前端领域主要是要分离 dom 操作和数据,把页面按照功能做划分,所以根据这些目标就做了 mvvm 和组件化的抽象。


后端领域主要是要做分层、解耦等,于是就做了 IOC、MVC 等抽象。


可以看到,抽象是基于编程语言的范式,根据需求做的设计,好的框架一定是做了满足某种管理代码的需求的抽象。


想要提升抽象、架构设计能力的话,可以学习下面向对象的设计模式,或者函数式等编程范式。研究各种框架是如何做的抽象。


总结


不同平台提供了不同的 api,这是应用开发的基础和边界。复杂应用往往要在 api 基础上做层层抽象,一般会用框架做一层抽象,自己再做一层抽象,目标是为了代码划分更清晰,提升可维护性和可扩展性。


其实我们写过的所有代码,都可以分为 api 和抽象这两方面。


深入 API 原理的话要深入操作系统和各领域的知识。提升抽象能力的话,可以学习面向对象的设计模式或者函数式等编程范式。


不管你现在做哪个平台之上的应用开发,刚开始都是要先学习 api 的,之后就是要理解各种抽象了:框架是怎么抽象的,上层又做了什么抽象。


API 保证下限,抽象可以提高上限。而且抽象能力或者说架构能力是可以迁移的,是程序员最重要的能力之一。


作者:zxg_神说要有光
链接:https://juejin.cn/post/7031931672538906637

收起阅读 »

线性表

由于我是搞前端的为了更友好的描述数据结构,所以全部代码示例都是用TypeScript来编写。 1、线性表类型 1.顺序存储结构(数组) 2.链式存储结构(链表) 1.1、顺序存储 一般指数组,内部数据的存储单元在内存中相邻 优势: 查询很快,时间复杂度为...
继续阅读 »

由于我是搞前端的为了更友好的描述数据结构,所以全部代码示例都是用TypeScript来编写。


1、线性表类型



  • 1.顺序存储结构(数组)

  • 2.链式存储结构(链表)


1.1、顺序存储


一般指数组,内部数据的存储单元在内存中相邻



优势: 查询很快,时间复杂度为O(1)


劣势:



  1. 元素增、删操作时间复杂度为O(n)

  2. 使用时需要提前确定长度




  1. 需要占据连续内存空间


1.2、链式存储


n 个数据元素的有限序列,通常为链式,叫作线性链表或链表。链表中的元素为结点,结点是一块内存空间存储一条数据。结点通常由两个部分组成:



  • 节点存储的数据

  • 指向下一个节点的指针



来看一下链表的typescript实现


class ListNode {
val: number
next: ListNode | null
constructor(val?: any, next?: ListNode | null) {
this.val = val
this.next = (next===undefined ? null : next)
}
}

2、链表类型


链表类型大体分为下列:



  • 带头、不带头

  • 单向、双向




  • 循环、非循环


2.1 带头不带头


链表都有头指针,带头结点的链表头指针指向的是头结点,不带头结点的头指针直接指向首元结点。


首元节点:链表中用来存储元素节点的第一个节点


带头:



不带头:



操作差异: 删除和新增操作中,无论操作位置,带头结点的链表不需要修改头指针的值,而不带头结点的有时候需要。清空操作中,带头结点的保留头结点,不带头结点的要销毁。


结构差异: 带头链表不论链表是否为空,均含有一个头结点,不带头单链表均无结点。


一般使用链表都为带头链表


2.2、双向链表


单项链表中,仅有一个指针指向下一个节点的位置,双向链表中,每个节点有有个指针:



  • pre:指向上一个节点位置

  • next:指向下一个节点位置


双向链表节点图:



双向链表节点数据结构:


class TwoWayListNode {
val: number
pre: ListNode | null
next: ListNode | null
constructor(val?: any, pre?: ListNode | null, next?: ListNode | null) {
this.val = val
this.next = (next===undefined ? null : next)
}
}

双向链表图:



// 简单的来实现一下上述结构,实际使用时可自行封装统的类
const p1 = new TwoWayListNode('p1', null, null)
const p2 = new TwoWayListNode('p2', null, null)
const p3 = new TwoWayListNode('p3', null, null)

p1.next = p2
p2.pre = p1
p2.next = P3

2.3、循环链表


链表中最后一个节点指向头节点



// 简单的来实现一下上述结构,实际使用时可自行封装统的类
const p1 = new ListNode('p1', null)
const p2 = new ListNode('p2', null)
const p3 = new ListNode('p3', null)

p1.next = p2
p2.pre = p1
p2.next = P3
p3.next = p1

以此类推还有双向项循环链表,这里就不展开了


3、线性表数据处理分析


3.1、顺序存储操作


查询:


由于顺序存储中数据按照逻辑顺序依次放入连续的存储单元中,所以在顺序表结构中很容易实现查询操作,直接通过下标去拿即可。时间复杂度为O(1)


插入:


在顺序存储结构中, 插入尾部的时间复杂度为O(1),其他位置时间复杂度为O(n)。


如下图想要在3位置插入一条数据 "六",需要将"四", "五" 位置的数据依次向后移动一个单位





删除:


在顺序存储结构中, 删除尾部的时间复杂度为O(1),其他位置时间复杂度为O(n)。


如下图想删除数据 "六",先将3位置的数据设置为空,"四", "五" 位置的数据依次向前移动一个单位



3.2 链式存储


插入:



// p1 -> p2 -> p3 -> p4 
p1.next = p5
p5.next = p2





删除:



// p1 -> p2 -> p3 -> p4 
p1.next = p3
p2.next = null

查询:


链表中查找只能从链表的头指针出发,顺着连标指针逐个结点查询,直到查到想要的结果为止,时间复杂度O(n)


作者:siegaii
链接:https://juejin.cn/post/7031868181203386405

收起阅读 »

图解:什么是AVL树?

引子上一次我给大家介绍了什么是二叉搜索树,但是由于二叉搜索树查询效率的不稳定性,所以很少运用在实际的场景中,所以我们伟大的前人就对二叉搜索树进行了改良,发明了AVL树。AVL树是一种自平衡二叉搜索树,因为AVL树任意节点的左右子树高度差的绝对值不超过1,所以A...
继续阅读 »

引子

上一次我给大家介绍了什么是二叉搜索树,但是由于二叉搜索树查询效率的不稳定性,所以很少运用在实际的场景中,所以我们伟大的前人就对二叉搜索树进行了改良,发明了AVL树。

AVL树是一种自平衡二叉搜索树,因为AVL树任意节点的左右子树高度差的绝对值不超过1,所以AVL树又被称为高度平衡树。

AVL树本质上是一棵带有平衡条件的二叉搜索树,它满足二叉搜索树的基本特性,所以本次主要介绍AVL树怎么自平衡,也就是理解它的旋转过程。

二叉搜索树特性忘了的小伙伴可以看之前的文章:搞定二叉搜索树,9图足矣!同时我也将基本性质给大家再回顾一遍:

  1. 若它的左子树不为空,则左子树上所有节点的值均小于根节点的值。

  2. 若它的右子树不为空,则右子树上所有节点的值均大于根节点的值。

  3. 它的左、右子树也分别为二叉搜索树。

平衡条件:每个节点的左右子树的高度差的绝对值不超过1。

我们将每个节点的左右子树的高度差的绝对值又叫做平衡因子。

AVL树的旋转行为一般是在插入和删除过程中才发生的,因为插入过程中的旋转相比于删除过程的旋转而言更加简单和直观,所以我给大家图解一下AVL树的插入过程。

插入过程

最开始的时候为空树,没有任何节点,所以我们直接用数据构造一个节点插入就好了,比如第一个要插入的数据为18。

第一个节点插入完成,开始插入第二个节点,假如数据为20。

插入第三个节点数据为14。

第四个节点数据为16。从根节点位置开始比较并寻找16的对应插入位置。

第五个要插入的数据为12。还是一样,从树的根节点出发,根据二叉搜索树的特性向下寻找到对应的位置。

此时插入一个数据11,根据搜索树的性质,我们不难找到它的对应插入位置,但是当我们插入11这个节点之后就不满足AVL树的平衡条件了。

此时相当于18的左子树高了,右子树矮了,所以我们应该进行一次右单旋,右单旋使左子树被提起来,右子树被拉下去,相当于左子树变矮了,右子树变高了,所以一次旋转之后,又满足平衡条件了。

简单分析上图的旋转过程:**因为左子树被提上去了,所以14成为了新的根节点,而18被拉到了14右子树的位置,又因为14这个节点原来有右子节点为16,所以18与16旋转之后的位置就冲突了,但是因为16小于18,**所以这个时候根据二叉搜索树的特性,将16调整到18的左子树中去,因为旋转之后的18这个节点的左子树是没有节点的,所以16可以直接挂到18的左边,如果18的左子树有节点,那么还需要根据二叉搜索树的性质去将16与18左子树中的节点比较大小,直到确定新的位置。

经过上面的分析我们可以知道:如果新插入的节点插入到根节点较高左子树的左侧,则需要进行一次右单旋,我们一般将这种情况简单记为左左情况,第一个左说的是较高左子树的左,第二个左说的是新节点插入到较高左子树的左侧。

分析完了左左的情况,我想小伙伴们不难推出右右的情况(第一个右说的是较高右子树的右,第二个右说的是新节点插入到较高右子树的右侧),就是一次左单旋,这里就不一步一步地分析右右的情况了,因为它和左左是对称的。给大家画个图,聪明的你一眼就可以学会!

现在两种单旋的情况已经讲完了,分别是左左和右右,还剩下两种单旋的情况,不过别慌,因为双旋比你想象中的简单,而且同样,双旋也是两种对称的情况,实际上我们只剩下一种情况需要分析了,所以,加油,弄懂了的话,面试的时候就完全不用慌了!

双旋

我们假设当前的AVL树为下图。

这个时候我们新插入一个节点,数据为15,根据搜索树的性质,我们找到15对应的位置并插入,如图

我们此时再次计算每个节点的平衡因子,发现根节点18的平衡因子为2,超过了1,不满足平衡条件,所以需要对他进行旋转。

我们将刚才需要进行右单旋的左左情况和现在的这种情况放在一起对比一下,聪明的你一定发现,当前的情况相比于左左的情况只是插入的位置不同而已,左左情况插入的节点在根节点18较高左子树的左侧,而当前这种情况插入节点是在根节点18较高左子树的右侧,我们将它称为左右情况。

那么可能正看到这里的你可能不禁会想:这不跟刚才左左差不多嘛,直接右单旋不就完事了。真的是这样吗?让我们来一次右单旋看看再说。

简单分析该右单旋:**节点14上提变成新的根节点,18下拉变成根节点的右子树,又因为当前根节点14原来有右子树16,所以18与16位置冲突,**比较18与16大小之后,发现18大于16,根据搜索树的性质,将以16为根节点的子树调整到18的左子树,因为18的左子树目前为空,所以以16为根的子树直接挂在18的左侧,若18的左子树不为空,则需要根据搜索树的性质继续进行比较,直到找到合适的挂载位置。

既然一次右单旋不行,那么我们应该怎么办呢?答案就是进行一次双旋,一次双旋可以拆分成两次单旋,对于当前这种不平衡条件,我们可以先进行一次左单旋,再进行一次右单旋,之后就可以将树调整成满足平衡条件的AVL树了,话不多说,图解一下。

简单分析左右双旋先对虚线框内的子树进行左单旋,则16上提变成子树的新根,以14为根节点的子树下拉,调整到16的左子树,此时发现16的左子树为15,与14这棵子树冲突,所以根据搜索树规则进行调整,将15挂载到以14为根节点子树的右子树,从而完成一次左单旋,之后再对整棵树进行一次右单旋,节点16上提成为新的根节点,18下拉变成根节点的右子树,因为之前16没有右子树,所以以18为根节点的子树直接挂载到16的右子树,从而完成右旋。

同样,对于左右情况的对称情况右左情况我就不给大家分析了,还是将图解送给大家,相信聪明的你一看就会!

到此为止,我将AVL树的四种旋转情况都给大家介绍了一遍,仔细想想,其实不止这四种情况需要旋转,严格意义上来说有八种情况需要旋转,比如之前介绍的左左情况吧,我们说左左就是将新的节点插入到了根节点较高左子树的左侧,这个左侧其实细分一下又有两种情况,只不过这两种情况实际可以合成一种情况来看,也就是新的节点插入到左侧的时候可以成为它父亲节点的左孩子,也可以成为它父亲节点的右孩子,那么这样的话就是相当于两种情况了,简单画个图看一下吧。

就是这样上图这样,每个新插入的节点都可以是它父亲节点的左孩子或者右孩子,这取决于新插入数据的大小,比如11就是12的左孩子,13就是12的右孩子,这两种情况都属于左左情况,也就是说他们本质上是一样的,都插在了节点18较高左子树的左侧。

那么这样看来这四种旋转情况严格上看都可以多分出一种情况,变成八种情况。

后话

emmm…这样看来AVL树确实解决了二叉搜索树可能不平衡的缺陷,补足了性能上不稳定的缺陷,但是细细想来AVL树的效率其实不是很好,这里说的不是查询效率,而是插入与删除效率,上面所说的这四大种八小种情况还是很容易命中的,那么这样的话就需要花费大量的时间去进行旋转调整,我的天,这样也太难搞了!

不过聪明的前人早就为我们想好了更加利于实际用途的搜索树,在现实场景中AVL树和二叉搜索树一样,基本上用不到,我们接下来要讲的这种二叉类的搜索树才是我们经常应用的,相信见多识广的你一定猜到了它的名字,对,就是它,大名鼎鼎的红黑树!我们下次来盘他!

鄙人才疏学浅,若有任何差错,还望各位海涵,不吝指教!

喜欢本文的少侠们,欢迎关注公众号雷子的编程江湖,修炼更多武林秘籍。

一键三连是中华民族的当代美德!


作者:雷子的编程江湖
链接:https://juejin.cn/post/6886103714818424846
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

恕我直言,我怀疑你并不会用 Java 枚举

开门见山地说吧,enum(枚举)是 Java 1.5 时引入的关键字,它表示一种特殊类型的类,默认继承自 java.lang.Enum。 为了证明这一点,我们来新建一个枚举 PlayerType: public enum PlayerType { TE...
继续阅读 »

开门见山地说吧,enum(枚举)是 Java 1.5 时引入的关键字,它表示一种特殊类型的类,默认继承自 java.lang.Enum。


为了证明这一点,我们来新建一个枚举 PlayerType:


public enum PlayerType {
TENNIS,
FOOTBALL,
BASKETBALL
}

两个关键字带一个类名,还有大括号,以及三个大写的单词,但没看到继承 Enum 类啊?别着急,心急吃不了热豆腐啊。使用 JAD 查看一下反编译后的字节码,就一清二楚了。


public final class PlayerType extends Enum
{

public static PlayerType[] values()
{
return (PlayerType[])$VALUES.clone();
}

public static PlayerType valueOf(String name)
{
return (PlayerType)Enum.valueOf(com/cmower/baeldung/enum1/PlayerType, name);
}

private PlayerType(String s, int i)
{
super(s, i);
}

public static final PlayerType TENNIS;
public static final PlayerType FOOTBALL;
public static final PlayerType BASKETBALL;
private static final PlayerType $VALUES[];

static
{
TENNIS = new PlayerType("TENNIS", 0);
FOOTBALL = new PlayerType("FOOTBALL", 1);
BASKETBALL = new PlayerType("BASKETBALL", 2);
$VALUES = (new PlayerType[] {
TENNIS, FOOTBALL, BASKETBALL
});
}
}

看到没?PlayerType 类是 final 的,并且继承自 Enum 类。这些工作我们程序员没做,编译器帮我们悄悄地做了。此外,它还附带几个有用静态方法,比如说 values()valueOf(String name)


01、内部枚举


好的,小伙伴们应该已经清楚枚举长什么样子了吧?既然枚举是一种特殊的类,那它其实是可以定义在一个类的内部的,这样它的作用域就可以限定于这个外部类中使用。


public class Player {
private PlayerType type;
public enum PlayerType {
TENNIS,
FOOTBALL,
BASKETBALL
}

public boolean isBasketballPlayer() {
return getType() == PlayerType.BASKETBALL;
}

public PlayerType getType() {
return type;
}

public void setType(PlayerType type) {
this.type = type;
}
}

PlayerType 就相当于 Player 的内部类,isBasketballPlayer() 方法用来判断运动员是否是一个篮球运动员。


由于枚举是 final 的,可以确保在 Java 虚拟机中仅有一个常量对象(可以参照反编译后的静态代码块「static 关键字带大括号的那部分代码」),所以我们可以很安全地使用“==”运算符来比较两个枚举是否相等,参照 isBasketballPlayer() 方法。


那为什么不使用 equals() 方法判断呢?


if(player.getType().equals(Player.PlayerType.BASKETBALL)){};
if(player.getType() == Player.PlayerType.BASKETBALL){};

“==”运算符比较的时候,如果两个对象都为 null,并不会发生 NullPointerException,而 equals() 方法则会。


另外, “==”运算符会在编译时进行检查,如果两侧的类型不匹配,会提示错误,而 equals() 方法则不会。



02、枚举可用于 switch 语句


这个我在之前的一篇我去的文章中详细地说明过了,感兴趣的小伙伴可以点击链接跳转过去看一下。


switch (playerType) {
case TENNIS:
return "网球运动员费德勒";
case FOOTBALL:
return "足球运动员C罗";
case BASKETBALL:
return "篮球运动员詹姆斯";
case UNKNOWN:
throw new IllegalArgumentException("未知");
default:
throw new IllegalArgumentException(
"运动员类型: " + playerType);

}

03、枚举可以有构造方法


如果枚举中需要包含更多信息的话,可以为其添加一些字段,比如下面示例中的 name,此时需要为枚举添加一个带参的构造方法,这样就可以在定义枚举时添加对应的名称了。


public enum PlayerType {
TENNIS("网球"),
FOOTBALL("足球"),
BASKETBALL("篮球");

private String name;

PlayerType(String name) {
this.name = name;
}
}

04、EnumSet


EnumSet 是一个专门针对枚举类型的 Set 接口的实现类,它是处理枚举类型数据的一把利器,非常高效(内部实现是位向量,我也搞不懂)。


因为 EnumSet 是一个抽象类,所以创建 EnumSet 时不能使用 new 关键字。不过,EnumSet 提供了很多有用的静态工厂方法:



下面的示例中使用 noneOf() 创建了一个空的 PlayerType 的 EnumSet;使用 allOf() 创建了一个包含所有 PlayerType 的 EnumSet。


public class EnumSetTest {
public enum PlayerType {
TENNIS,
FOOTBALL,
BASKETBALL
}

public static void main(String[] args) {
EnumSet<PlayerType> enumSetNone = EnumSet.noneOf(PlayerType.class);
System.out.println(enumSetNone);

EnumSet<PlayerType> enumSetAll = EnumSet.allOf(PlayerType.class);
System.out.println(enumSetAll);
}
}

程序输出结果如下所示:


[]
[TENNIS, FOOTBALL, BASKETBALL]

有了 EnumSet 后,就可以使用 Set 的一些方法了:



05、EnumMap


EnumMap 是一个专门针对枚举类型的 Map 接口的实现类,它可以将枚举常量作为键来使用。EnumMap 的效率比 HashMap 还要高,可以直接通过数组下标(枚举的 ordinal 值)访问到元素。


和 EnumSet 不同,EnumMap 不是一个抽象类,所以创建 EnumMap 时可以使用 new 关键字:


EnumMap<PlayerType, String> enumMap = new EnumMap<>(PlayerType.class);

有了 EnumMap 对象后就可以使用 Map 的一些方法了:



和 HashMap 的使用方法大致相同,来看下面的例子:


EnumMap<PlayerType, String> enumMap = new EnumMap<>(PlayerType.class);
enumMap.put(PlayerType.BASKETBALL,"篮球运动员");
enumMap.put(PlayerType.FOOTBALL,"足球运动员");
enumMap.put(PlayerType.TENNIS,"网球运动员");
System.out.println(enumMap);

System.out.println(enumMap.get(PlayerType.BASKETBALL));
System.out.println(enumMap.containsKey(PlayerType.BASKETBALL));
System.out.println(enumMap.remove(PlayerType.BASKETBALL));

程序输出结果如下所示:


{TENNIS=网球运动员, FOOTBALL=足球运动员, BASKETBALL=篮球运动员}
篮球运动员
true
篮球运动员

06、单例


通常情况下,实现一个单例并非易事,不信,来看下面这段代码


public class Singleton {  
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}

但枚举的出现,让代码量减少到极致:


public enum EasySingleton{
INSTANCE;
}

完事了,真的超级短,有没有?枚举默认实现了 Serializable 接口,因此 Java 虚拟机可以保证该类为单例,这与传统的实现方式不大相同。传统方式中,我们必须确保单例在反序列化期间不能创建任何新实例。


07、枚举可与数据库交互


我们可以配合 Mybatis 将数据库字段转换为枚举类型。现在假设有一个数据库字段 check_type 的类型如下:


`check_type` int(1) DEFAULT NULL COMMENT '检查类型(1:未通过、2:通过)',

它对应的枚举类型为 CheckType,代码如下:


public enum CheckType {
NO_PASS(0, "未通过"), PASS(1, "通过");
private int key;

private String text;

private CheckType(int key, String text) {
this.key = key;
this.text = text;
}

public int getKey() {
return key;
}

public String getText() {
return text;
}

private static HashMap<Integer,CheckType> map = new HashMap<Integer,CheckType>();
static {
for(CheckType d : CheckType.values()){
map.put(d.key, d);
}
}

public static CheckType parse(Integer index) {
if(map.containsKey(index)){
return map.get(index);
}
return null;
}
}

1)CheckType 添加了构造方法,还有两个字段,key 为 int 型,text 为 String 型。


2)CheckType 中有一个public static CheckType parse(Integer index)方法,可将一个 Integer 通过 key 的匹配转化为枚举类型。


那么现在,我们可以在 Mybatis 的配置文件中使用 typeHandler 将数据库字段转化为枚举类型。


<resultMap id="CheckLog" type="com.entity.CheckLog">
<id property="id" column="id"/>
<result property="checkType" column="check_type" typeHandler="com.CheckTypeHandler"></result>
</resultMap>

其中 checkType 字段对应的类如下:


public class CheckLog implements Serializable {

private String id;
private CheckType checkType;

public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}

public CheckType getCheckType() {
return checkType;
}

public void setCheckType(CheckType checkType) {
this.checkType = checkType;
}
}

CheckTypeHandler 转换器的类源码如下:


public class CheckTypeHandler extends BaseTypeHandler<CheckType> {

@Override
public CheckType getNullableResult(ResultSet rs, String index) throws SQLException {
return CheckType.parse(rs.getInt(index));
}

@Override
public CheckType getNullableResult(ResultSet rs, int index) throws SQLException {
return CheckType.parse(rs.getInt(index));
}

@Override
public CheckType getNullableResult(CallableStatement cs, int index) throws SQLException {
return CheckType.parse(cs.getInt(index));
}

@Override
public void setNonNullParameter(PreparedStatement ps, int index, CheckType val, JdbcType arg3) throws SQLException {
ps.setInt(index, val.getKey());
}
}

CheckTypeHandler 的核心功能就是调用 CheckType 枚举类的 parse() 方法对数据库字段进行转换。



恕我直言,这篇文章看完后,我觉得小伙伴们肯定会用 Java 枚举了,如果还不会,就过来砍我!


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

程序员如何优雅的挣零花钱?

前言虽然程序员有女朋友的不多(误),但是开销往往都不小。 VPS、域名、Mac上那一堆的收费软件、还有Apple每年更新的那些设备,经常都是肾不够用的节奏。 幸好作为程序员,我们有更多挣钱的姿势。 有同学该嚷了:不就是做私单嘛。 ...
继续阅读 »

前言

虽然程序员有女朋友的不多(误),但是开销往往都不小。


VPS、域名、Mac上那一堆的收费软件、还有Apple每年更新的那些设备,经常都是肾不够用的节奏。


幸好作为程序员,我们有更多挣钱的姿势。


有同学该嚷了:不就是做私单嘛。


对,但是也不太对。做私单的确是一个简单直接方式,但在我看来,私单的投入产出比很差,并不是最优的。


但既然提到了,就先说说吧。

关于

本文作者:easychen 


GitHub地址:https://github.com/easychen




私单


远程外包


最理想的单子还是直接接海外的项目,比如freelance.com等网站。一方面是因为挣的是美刀比较划算,之前看到像给WordPress写支付+发送注册码这种大家一个周末就能做完的项目,也可以到200~300美刀;另一方面是在国外接单子比较隐蔽。


常用国外网站:



(由ahui132同学补充)



本段由tvvocold同学贡献。国内也有一个软件众包平台 CODING 码市 。 码市基于云计算技术搭建的云端软件开发平台 Coding.net 作为沟通和监管工具,快速连接开发者与需求方,旨在通过云端众包的方式提高软件交付的效率。码市作为第三方监管平台,会对所有项目进行审核以保证项目需求的明确性,并提供付款担保,让开发者只要按时完成项目开发即可获取酬劳。你可以 在这里 看到开发者对码市的评价。



当然,猪八戒这种站我就不多说了,不太适合专业程序员去自贬身价。


按需雇用


按需雇用是近几年新兴的私单方式,开发者在业余时间直接到雇主公司驻场办公,按时薪领取报酬。这种方式省去了网络沟通的低效率,也避免了和雇主的讨价还价,适合怕麻烦的程序员。


拉勾大鲲





大鲲 由拉勾网推出,考虑到拉勾上三十多万的招聘方,大鲲不缺雇主,这是其他独立平台相对弱势的地方。


实现网





实现网的价格也很不错,但是我强烈建议大家不要在介绍中透漏实名和真实的公司部门信息,因为这实在太高调了。有同学说,这是我的周末时间啊,我爱怎么用就怎么用,公司还能告我怎么的? 虽然很多公司的劳动合同里边并不禁止做兼职,但在网上如此高调的干私活,简直就是在挑衅HR:「我工作不饱和」、「公司加班不够多」… 再想象下你一边和产品经理说这个需求做不完,一边自己却有时间做私单的样子。你自己要是老板也不愿提拔这样的人吧。


(我这几天重新去看了下,人才页面已经不再显示姓名了,只用使用头像。这样只要在工作经历介绍里边注意一点,就可以避免上述问题了。)


程序员客栈





不太熟悉,但国内按需雇用的网站不多,写出来供大家参考。


Side Project


比起做私单,做一个Side Project会更划算。


Side Project的好处是你只需要对特定领域进行持续投入,就可以在很长时间获得收入。这可以让你的知识都在一棵树上分支生长,从而形成良好的知识结构,而不是变成一瓶外包万金油。


思路有两种:


一种是做小而美的,针对一个细分领域开发一个功能型应用,然后放到市场上去卖;


另一种是做大而全的基础应用(想想WordPress),方便别人在上边直接添加代码,定制成自己想要的应用。


前一种做起来比较快,但需要自己去做一些销售工作;后一种通常是开源/免费+收费模式,推广起来更简单。


有同学会说,我写的 Side Project 就是卖不掉啊。项目方向的选取的确是比较有技巧的,但简单粗暴的解决方案就是找一个现在卖得非常好、但是产品和技术却不怎样的项目,做一个只要一半价格的竞品。


比如 Mac 下有一个非常有名的写作软件,叫 Ulysses 。我试用了一下非常不错,但就是贵,283 RMB。后来看到了 Mweb ,光是免费的 Lite 版覆盖了 Ulysses 的主功能,完整版也才98RMB,几乎没有思考就买下来了。


做咨询


专家平台


如果你在技术圈子里边小有名气,或者在某一个业务上特别精通,那么通过做咨询来挣钱是一种更轻松的方式。和人在咖啡厅聊几个小时,几百上千块钱就到手了。


国内这方面的产品,我知道的有下边几个:




  • 在行: 这个是果壳旗下的,做得比较早,内容是全行业的,所以上边技术向的反而不多。




  • 缘创派: 缘创派的轻合伙栏目,主要面向创业者,适合喜欢感受创业氛围的技术专家们。




  • 极牛: 你可以认为这是一个程序员版本的「在行」,我浏览了下,虽然被约次数比在行要低不少,但专业性比较强,期望他们能尽快的推广开来吧。




  • 知加:这个项目是我参与的,面向程序员,类似「分答」的付费语音问答,刚开始内测,上边有一些硅谷科技公司的同学。感兴趣的可以看看。




做咨询虽然也是实名的,但和私活是完全不同的。咨询的时间短,不会影响到正常的休息,更不会影响上班;而且大部分公司是鼓励技术交流的,所以大家的接受度都很高。


付费社群


除了APP外,我觉得收费群也是可以做的。比如可以搞一个技术创业群,找一些创业成功的同学、做投资的同学、做法务的同学,面向想创业的同学开放,每人收个几百块的年费。然后你在创业过程中遇到的问题,都可以有人解答,不会觉得是孤零零的一个人。如果遇到了问题,群里的人可以解答;如果没遇到问题,那不是更好么。有种卖保险的感觉,哈哈哈。


比较好用的工具是 知识星球 也就是之前的小密圈。这个工具比较适合交流和讨论,长文比较痛苦。可以发布到其他地方,然后粘贴回来。





另一个靠谱的工具大概是微博的 V+ 会员。说它靠谱主要是它在微博上,所以等于整合了 「内容分发」→ 「新粉丝获取」 → 「付费用户转化」 的整个流程。


PS:交流型付费社群的一个比较难处理的事情是,很难平衡免费的粉丝和付费的社群之间的关系,所以我最后的选择是付费类的提供整块的内容,比如整理成册的小书、录制的实战视频等;而日常零碎的资料分享还是放到微博这种公开免费的平台。


写文章


投稿


很多同学喜欢写技术博客,其实把文章投给一些网站是有稿费的。比如InfoQ,他们家喜欢收3000~4000字的深度技术文章;稿费是千字150。虽然不算太多,但一篇长文的稿费也够买个入门级的Cherry键盘了。我喜欢InfoQ的地方是他们的版权要求上比较宽松。文章在他们网站发布后,你可以再发布到自己博客的;而且文章可以用于出书,只要标明原发于InfoQ即可。


更详细的说明见这里:http://www.infoq.com/cn/article-guidelines



微博的@rambone同学补充到,文章还可以发到微博、微信、简书等支持打赏的平台。考虑到简书CEO及其官博对程序员的奇葩态度,个人建议是换个咱程序员自己的平台写文章。



出版


顺便说一句,比起写文章,其实通过传统发行渠道出书并不怎么挣钱,我之前到手的版税是8%,如果通过网络等渠道销售,数字会更低。出电子书收益会好一些,我之前写过一篇文章专门介绍:《如何通过互联网出版一本小书》


以前一直写图文为主的书,用Markdown非常不错;但最近开始写技术教程类的书,发现Markdown不太够用了,最主要的问题有 ① 不支持视频标签,尤其是本地视频方案 ② 代码高亮什么的很麻烦 ③ 也没有footer note、文内说明区域的预置。


这里要向大家严重推荐Asciidoc,你可以把它看成一个增强版的Markdown,预置了非常多的常用格式,而且GitBook直接就支持这个格式(只要把.md 搞成 .adoc 就好),Atom也有实时预览插件。用了一段时间,非常喜欢。


付费文集


最近一年有不少的付费文集产品出现,可以把它看成传统出版的一个网络版。一般是写作十篇以内的系列文章,定价为传统计算机书的一半到三分之一。付费文集产品通常是独家授权,所以在选择平台方面一定要慎重,不然一个好作品可能就坑掉了。


掘金小册





小册是由掘金推出的付费文集产品。我是小册的第一批作者,一路用下来还是很不错的。文章格式直接采用 Markdown , 发布以后可以实时更新,保证内容的新鲜度,非常的方便。小册的一般定价在19~29,通用内容销量好的能过千,细分内容基本也能过百。挣零花钱的话,是个非常不错的选择。


达人课





达人课是 GitChat 旗下的付费文集产品,现在应该已经合并到 CSDN 了。GitChat 的用户群不算大,但付费意愿还可以,大概因为内容就没有免费的🤣。之前我上课的时候是提交完成以后的文档给编辑,由编辑同学手动上架。感觉比较麻烦,尤其是修改错别字什么的。


小专栏





这个平台不熟……写到这里仅供参考。


教学视频



微博的@瓜瓜射门啦同学给了自己应聘程序教学网站讲师的经验:应聘程序教学网站讲师,出视频+作业教程,平台按小时支付,这个不知道算不算挣零花钱,我算了一下去年,一年大概出 20 个小时视频,拿到手是不到 6 万的样子,平时就是周末花时间弄下。



在线教育起来以后,录制教学视频也可以赚钱了。关于录制在线课程的收益,一直不为广大程序员所知。但最近和51CTO学院 和 网易云课堂 的同学聊天,才发现一个优秀的40~60节的微专业课程,一年的收益比得上一线城市高级总监的收入。难怪最近做培训的人这么多😂


渠道和分成


大部分的平台合同有保密协议,所以不能对外讲。但网易云课堂和Udemy在公开的讲师注册协议中写明了分成,所以这里说一下。


网易云课堂


网易的课分三类:




  • 独立上架:等于网易提供平台(视频上传管理、用户管理、支付系统等),由你自己来负责营销。这个分成比例在 9:1 ,平台收取 10% 的技术服务费。我觉得非常划算。




  • 精品课:由网易帮你推广,但需要和他们签订独立的合同,会收取更多的分成。最麻烦的是,通常是独家授权。一旦签署了,就不能在其他平台上架课程了。




  • 微专业:这个是网易自己规划的课程体系,从课程的策划阶段就需要和他们深度沟通。也是网易推广力度最大、收益最大的一类课程。




方糖全栈课就放在网易平台上,觉得好的地方如下:




  • 支付渠道相对全,还支持花呗,这样对于我这种高价课就非常重要。苹果应用内购买课程会渠道费用会被苹果扣掉30%,好想关掉 🤣




  • 自带推广系统,愿意的话可以用来做课程代理系统。




Udemy


相比之下 Udemy 就很贵了,分成是 5:5 ;支付上国内用户只能通过信用卡或者银行卡绑 paypal 支付。但可以把课程推向全球。(但我英文还不能讲课🙃)


腾讯课堂没用过,欢迎熟悉的同学 PR 。


小课和大课


我个人喜欢把视频分成小课和大课两种。因为视频虽然看起来时间短,但实际上要做好的话,背后要消耗的时间、要投入精力还是非常多的。大课动不动就是几十上百个课时,绝大部分上班的程序员都没有时间来录制。所以挣零花钱更适合做小课,这种课一般就几个小时,剪辑成 10 个左右的小课时,价格在几十百来块。如果是自己专业纯熟的领域,一个长假就可以搞定。


表现形式


在课程的表现形式上,我个人更喜欢designcode.io这种图文+视频的模式,一方面是学习者可以快速的翻阅迅速跳过自己已经学会的知识;另一方面,会多出来 微博头条文章、微信公众号、知乎和简书专栏这些长文推广渠道。





当然,内容本身才是最核心的。现在那么多的免费视频,为什么要来买你的收费版?


其实现在绝大部分教学视频,往往都真的只是教学,和现实世界我们遇到的问题截然不同。里边都是一堆简化后的假项目,为了教学而刻意设计的。


这里和大家分享一个我之前想操作的想法。


就是在自己决定开始做一个开源项目后,用录屏软件把整个过程完完整整的录下来。开源的屏幕录制工具OBS,1920*1080的屏幕录成FLV格式,一个小时只需要1G,一个T的移动硬盘可以录制上千小时,对一个中型项目来说也足够了。


等项目做完,就开源放到GitHub,让大家先用起来。等迭代稳定后,再从录制的全量视频中剪辑出一系列的教程,整理出一系列的文章,放到网站上做收费课程。


这样做有几个好处:




  • 保证所有遇到的问题都是真实的,不是想象出来的,学习过这个课程的人,可以独立的将整个项目完整的实现。




  • 没有特意的录制过程,所以教程其实是软件开发的副产品,投入产出比更高。




  • 如果你的软件的确写得好,那么用过你软件的人可以成为你的客户或者推荐员。




后续


今年我录制方糖全栈课的时候就采用了上边这个思路,效果还不错,不过有几个小问题:




  • 连续性。录着视频写代码总会有一种潜在焦虑,平时经常写一会儿休息一会儿,录像时就会留下大段的空白,有点浪费空间。当然这个主要是心理上的。




  • 录音。录音的问题更大一些。因为一个长期项目很难一直处于一个安静的环境,另外基础课录制可能需要大量的讲解,几个小时写下来嗓子哑了 🤣 。最后的解决方式是剪辑的时候重新配音,不过需要注意音画同步问题。




软件


如果是没有太多界面切换的课程,那可以使用keynote自带的录音。在其他环境里边的演示的视频可以直接粘贴到keynote里面来播放。


但是当你有很多的外部界面的时候,就需要录屏了。mac上可以直接用quicktime来录制。文件,新建里边选 record screen就好。


我录全栈课的时候,因为会在三个操作系统上录一些界面,所以我选择了obs。虽然这个工具主打的是直播,但实际上它的录制功能也还是挺不错的。


剪辑的话,用mac的imovie基本就够了,主要用到的功能就是分割片段,然后把不要的删掉。音频去下底噪。部分等待时间过长的片段加速下。当然adobe家的也行,就是贵。


硬件


硬件上边,最好买一个用来支撑话筒的支架。不要用手直接握着话筒来录,这样就不会有电流声(或者很小)。外接声卡我用的是 XOX , 在 Mac 下边效果挺好,但不知道为啥在 Windows 上回声比较大(当然也可能是系统设置的原因)。


内部推荐和猎头推荐


如果你在BAT等一流互联网公司工作,如果你有一帮志同道合的程序员朋友,那么基本上每隔几个月你应该就会遇到有想换工作的同事和朋友,所以千万别错过你挣推荐费的大好时机。


一般来讲,公司内部推荐的钱会少一些,我见过的3000~6000的居多。但因为是自己公司,会比较靠谱,所以风险小一些。经常给公司推荐人才,还会提升老大多你的好感度,能优先就优先吧。


比起内部推荐,猎头推荐的推荐费则会多很多。一个30万年薪的程序员,成功入职后差不多可以拿到1万RMB的推荐费。但猎头渠道的问题在于对简历质量要求非常高,有知名公司背景的才容易成单;回款周期又特别长,一般要入职过了试用期以后才能拿到全部推荐费,得小半年。


小结


学会挣钱是一件非常重要的事情,它会让你了解商业是如何运作的,帮助你理解公司的产品逻辑、以及为你可能的技术创业打下坚实的基础。


所以我鼓励大家多去挣零花钱,最好各种姿势都都试试,会有意想不到的乐趣。如果你有更好的挣零花钱技能,欢迎发PR过来,我会挑不错的合并进去 :)


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

【小程序实战】- 将图片优化进行到底

背景 前端的性能优化,图片优化是必不可少的重要环节,大部分网站页面的构成都少不了图片的渲染。尤其在电商类项目,往往存在大量的图片,如 banner 广告图,菜单导航图,商品列表图等。图片加载数量多以及图片体积过大往往会影响页面加载速度,造成不良的用户体验。 优...
继续阅读 »

背景


前端的性能优化,图片优化是必不可少的重要环节,大部分网站页面的构成都少不了图片的渲染。尤其在电商类项目,往往存在大量的图片,如 banner 广告图,菜单导航图,商品列表图等。图片加载数量多以及图片体积过大往往会影响页面加载速度,造成不良的用户体验。


优化方案


基于上述问题的主要问题是图片数量和图片体积,所以应该怎么提高图片加载速度,提升用户体验。其实图片优化有非常多且优秀的方案,都可以从中借鉴,最后我们对图片进行不同方向的整体优化。


image-20211021191413342.png


使用合适的图片格式


目前广泛应用的 WEB 图片格式有 JPEG/JPG、PNG、GIF、WebP、Base64、SVG 等,这些格式都有各自的特点,以下大概简单总结如下:


WEB图片格式.png


使用合适的图片格式通常可以带来更小的图片字节大小,通过合理压缩率,可以减少图片大小,且不影响图片质量。


降低网络传输


小程序使用腾讯云图片服务器,提供很多图片处理功能,比如图片缩放、图片降质,格式转换,图片裁剪、图片圆角等功能。这些功能可以通过在图片URL中添加规定参数就能实现,图片服务器会根据参数设置提前将图片处理完成并保存到CDN服务器,这样大大的减少图片传输大小。


目前后台接口下发返回的图片 URL 都是未设置图片参数预处理,比如一张 800x800 尺寸高清的商品图,体积大概300k 左右,这样就很容易导致图片加载和渲染慢、用户流量消耗大,严重影响了用户体验。所以我们结合腾讯云的图片处理功能,网络图片加载前,先检测是否是腾讯云域名的图片URL,如果域名匹配,对图片URL进行预处理,预处理包括添加缩放参数添加降质参数添加WebP参数的方式减少图片网络传输大小


我们先看一张通过图片服务器是腾讯云图片处理能力,通过设置图片缩放/降质/WebP,一张尺寸800x800,体积246KB图片,最后输出生成25.6KB,图片体积足足减少了80%,效果显著。


image-20211021203109404.png


图片缩放

目前业务后台都是原图上传,原始图尺寸可能比客户端实际显示的尺寸要大,一方面导致图片加载慢,另一方面导致用户流量的浪费,其中如果是一张很大尺寸图片加载也会影响渲染性能,会让用户感觉卡顿,影响用户体验。通过添加缩放参数的方式,指定图片服务器下发更小和更匹配实际显示size的图片尺寸。


图片降质

图片服务器支持图片质量,取值范围 0-100,默认值为原图质量,通过降低图片质量可以减少图片大小,但是质量降低太多也会影响图片的显示效果,网络默认降图片质量参数设置为85,同时通过小程序提供的:wx.getNetworkTypewx.onNetworkStatusChangeoffNetworkStatusChange的接口监听网络状态变化来获取当前用户的网络类型networkType,比如用户当前使用的4G网络,则图片质量会动态设置为80,对于大部分业务情况,一方面可以大幅减少图片下载大小和保证用户使用体验,另一方面节省用户浏览 ,目前添加图片降质参数至少可以减少30-40%的图片大小。


/**
* 设置网络情况
*/
const setNetwork = (res: Record<string, any>) => {
const { isConnected = true, networkType = 'wifi' } = res;

this.globalData.isConnected = isConnected;
this.globalData.networkType = networkType.toLowerCase();
this.events.emit(EventsEnum.UPDATE_NETWORK, networkType);
};

wx.getNetworkType({ success: (res) => setNetwork(res) });
wx.offNetworkStatusChange((res) => setNetwork(res));
wx.onNetworkStatusChange((res) => setNetwork(res));

/**
* 根据网络环境设置不同质量图片
*/
const ImageQuality: Record<string, number> = {
wifi: 85,
'5g': 85,
'4g': 80,
'3g': 60,
'2g': 60,
};

/**
* 获取图片质量
*/
export const getImageQuality = () => ImageQuality[getApp().globalData.networkType ?? 'wifi'];

使用 WebP

前面简单介绍不同的图片格式都有各自的优缺点和使用场景,其中 WebP 图片格式提供有损压缩与无损压缩的图片格式。按照Google官方的数据,与PNG相比,WebP无损图像的字节数要少26%WebP有损图像比同类JPG图像字节数少25-34%。现如今各大互联网公司的产品都已经使用了,如淘宝、京东和美团等。


这里放一个 WebP 示例链接(GIF、PNG、JPG 转 Webp),直观感受 WebP 在图片大小上的优势。


image-20211020191505147.png


在移动端中 WebP的兼容性,大部分数用户都已经支持了 Can I use... Support tables for HTML5, CSS3, etc


image-20211020131150424.png


针对png/jpg图片格式,自动添加WebP参数,转成WebP图片格式。虽然WebP相比png/jpg图片解码可能需要更长时间,但相对网络传输速度提升还是很大。目前 ios 13系统版本有不少用户量的占比,小程序端获取当前系统版本,降级处理不添加WebP参数。


// 检查是否支持webp格式
const checkSupportWebp = () => {
const { system } = wx.getSystemInfoSync();
const [platform, version] = system.split(' ');

if (platform.toLocaleUpperCase() === PlatformEnum.IOS) {
return Number(version.split('.')[0]) > IOS_VERSION_13;
}

return true; // 默认支持webp格式
};


提示:由于目前图片服务器并不支持、SVG、GIFWebP,并没有做处理



优化效果


测试我们小程序首页列表接口加载图片,来对比优化前后的效果


切片.png


经过我们通过使用腾讯云图片服务器的图片处理功能,以及动态处理图片格式的方式,减少图片体积,提高图片加载速度,带来的收益比非常可观的


图片懒加载


懒加载是一种性能优化的方式,将页面内未出现在可视区域内的图片先不做加载, 等到滚动到可视区域后再去加载,对于页面加载性能上会有很大的提升,也提高了用户体验。


实现原理


使用小程序提供Intersection Observer API,监听某些节点是否可以被用户看见、有多大比例可以被用户看见。这样我们就能判断图片元素是否在可是范围中,进行图片加载。


我们基于小程序的Intersection Observer API,封装一个监听模块曝光 IntersectionObserver函数工具,提供以下用法


import IntersectionObserver from 'utils/observer/observer';

const ob = new IntersectionObserver({
selector: '.goods-item', // 指定监听的目标节点元素
observeAll: true, // 是否同时观测多个目标节点
context: this, // 小程序 this 对象实例
delay: 200, // 调用 onFinal 方法的间隔时间,默认 200ms
onEach: ({ dataset }) => {
// 每一次触发监听调用时,触发 onEach 方法,可以对数据进行一些过滤处理
const { key } = dataset || {};
return key;
},
onFinal: (data) => {
// 在触发监听调用一段时间 delay 后,会调用一次 onFinal 方法,可以进行埋点上报
if (!data) return;
console.log('module view data', data);
},
});

// 内置函数方法,如下:
ob.connect(); // 开始监听
ob.disconnect(); // 停止监听
ob.reconnect(); // 重置监听

然后在我们的FreeImage图片组件,添加可视区域加载图片的功能,以下是部分代码


import IntersectionObserver from 'utils/observer';

Component({
properties: {
src: String,
/**
* 是否开启可视区域加载图片
*/
observer: {
type: Boolean,
value: false,
},
....
},

data: {
isObserver: false,
...
},

lifetimes: {
attached() {
// 开启可视区域加载图片
if (this.data.observer) {
this.createObserver();
}
},
},
methods: {
...

/**
* 监听图片是否进入可视区域
*/
createObserver() {
const ob = new IntersectionObserver({
selector: '.free-image',
observeAll: true,
context: this,
onFinal: (data = []) => {
data.forEach((item: any) => {
this.setData({
isObserver: true,
});
ob.disconnect(); // 取消监听
});
},
});

ob.connect(); // 开始监听
}
}
})

<free-image observer src="{{ src }}" />

优化效果


测试我们小程序首页列表,使用图片懒加载的效果


27a0b7a88a6e18665fa1ff33b3726b68.gif


通过使用图片懒加载的功能,减少图片数量的加载,有效提高页面加载性能。在上述我们已经对图片体积进行优化过,所以在我们小程序中,只有在网络情况较差的情况下,才会自动开启图片懒加载功能。


优化请求数


我们项目中有很多本地图片资源,比如一些 icon 图标、标签类切图、背景图、图片按钮等。而小程序分包大小是有限制:整个小程序所有分包大小不超过 20M,而单个分包/主包大小不能超过 2M。所以为了减轻小程序体积,本地图片资源需要进行调整,比如图片压缩、上传到 CDN 服务器。这样能减少了小程序主包大小,而大部分图片都在腾讯云 CDN 服务器中,虽然可以加速资源的请求速度,当页面打开需要同时下载大量的图片的话,就会严重影响了用户的使用体验。


针对此问题,需要找到权衡点来实现来优化请求数,首先我们把图片资源进行分类,以及使用场景,最后确定我们方案如下:



  • 较大体积的图片,选择上传到 CDN 服务器

  • 单色图标使用 iconfont 字体图标,多彩图标则使用svg格式

  • 标签类的图片,则生成雪碧图之后上传到 CDN 服务器

  • 图片体积小于10KB,结合使用场景,则考虑base64 ,比如一张图片体积为3KB的背景图,由于小程序css background不支持本地图片引入,可以使用 base64 方式实现


其他策略


大图检测

实现大图检测机制,及时发现图片不符合规范的问题,当发现图片尺寸太大,不符合商品图尺寸标准时会进行上报。在小程序开发版/体验版中,当我们设置开启Debug模式,图片组件FreeImage会自动检测到大图片时,显示当前图片尺寸、以及设置图片高亮/翻转的方式提醒运营同学和设计同学进行处理



加载失败处理

使用腾讯云图片处理功能,URL预处理转换后得新 URL,可能会存在少量图片不存在的异常场景导致加载失败。遇到图片加载失败时,我们还是需要重新加载原始图片 URL, 之后会将错误图片 URL 上报到监控平台,方便之后调整 URL 预处理转换规则,同时也发现一部分错误的图片 URL 推动业务修改。


这是我们图片组件FreeImage 处理图片加载失败,以下是部分代码


onError(event: WechatMiniprogram.TouchEvent) {
const { src, useCosImage } = this.data;

this.setData({
loading: false,
error: true,
lazy: 'error',
});

// 判断是否腾讯云服务的图片
if (useCosImage) {
wx.nextTick(() => {
// 重新加载原生图片
this.setData({
formattedSrc: src, // src 是原图地址
});
});
}

// 上报图片加载失败
app.aegis.report(AegisEnum.IMAGE_LOAD_FAIL, {
src,
errMsg: event?.detail.errMsg,
});

this.triggerEvent('error', event.detail);
}

图片请求数检查

使用小程序开发者工具的体验评分功能,体验评分是一项给小程序的体验好坏打分的功能,它会在小程序运行过程中实时检查,分析出一些可能导致体验不好的地方,并且定位出哪里有问题,以及给出一些优化建议。


image-20211024170719264.png


通过体验评分的结果,可以分析我们存在短时间内发起太多的图片请求,以及存在图片太大而有效显示区域较小。所以根据分析的结果,开发需要合理控制数量,可考虑使用雪碧图技术、拆分域名或在屏幕外的图片使用懒加载等。


上传压缩

图片在上传前在保持可接受的清晰度范围内同时减少文件大小,进行合理压缩。现如今有很多不错的图片压缩插件工具,就不在详情介绍了。


推荐一个比较优秀的图片压缩网站:TinyPNG使用智能有损压缩技术将您的 WebP, PNG and JPEG 图片的文件大小降低


作者:稻草叔叔
链接:https://juejin.cn/post/7031851192481218574

收起阅读 »

代码写得好,Reduce 方法少不了,我用这10例子来加深学习!

数组中的 reduce 犹如一只魔法棒,通过它可以做一些黑科技一样的事情。语法如下: reduce(callback(accumulator, currentValue[, index, array])[,initialValue]) reduce 接受两个参...
继续阅读 »

数组中的 reduce 犹如一只魔法棒,通过它可以做一些黑科技一样的事情。语法如下:


reduce(callback(accumulator, currentValue[, index, array])[,initialValue])

reduce 接受两个参数,回调函数和初识值,初始值是可选的。回调函数接受4个参数:积累值、当前值、当前下标、当前数组。


如果 reduce的参数只有一个,那么积累值一开始是数组中第一个值,如果reduce的参数有两个,那么积累值一开始是出入的 initialValue 初始值。然后在每一次迭代时,返回的值作为下一次迭代的 accumulator 积累值。


今天的这些例子的大多数可能不是问题的理想解决方案,主要的目的是想说介绍如何使用reduce来解决问题。


求和和乘法


// 求和
[3, 5, 4, 3, 6, 2, 3, 4].reduce((a, i) => a + i);
// 30

// 有初始化值
[3, 5, 4, 3, 6, 2, 3, 4].reduce((a, i) => a + i, 5 );
// 35

// 如果看不懂第一个的代码,那么下面的代码与它等价
[3, 5, 4, 3, 6, 2, 3, 4].reduce(function(a, i){return (a + i)}, 0 );

// 乘法
[3, 5, 4, 3, 6, 2, 3, 4].reduce((a, i) => a * i);

查找数组中的最大值


如果要使用 reduce 查找数组中的最大值,可以这么做:


[3, 5, 4, 3, 6, 2, 3, 4].reduce((a, i) => Math.max(a, i), -Infinity);

上面,在每一次迭代中,我们返回累加器和当前项之间的最大值,最后我们得到整个数组的最大值。


如果你真想在数组中找到最大值,不要有上面这个,用下面这个更简洁:


Math.max(...[3, 5, 4, 3, 6, 2, 3, 4]);

连接不均匀数组


let data = [
["The","red", "horse"],
["Plane","over","the","ocean"],
["Chocolate","ice","cream","is","awesome"],
["this","is","a","long","sentence"]
]
let dataConcat = data.map(item=>item.reduce((a,i)=>`${a} ${i}`))

// 结果
['The red horse',
'Plane over the ocean',
'Chocolate ice cream is awesome',
'this is a long sentence']

在这里我们使用 map 来遍历数组中的每一项,我们对所有的数组进行还原,并将数组还原成一个字符串。


移除数组中的重复项


let dupes = [1,2,3,'a','a','f',3,4,2,'d','d']
let withOutDupes = dupes.reduce((noDupes, curVal) => {
if (noDupes.indexOf(curVal) === -1) { noDupes.push(curVal) }
return noDupes
}, [])

检查当前值是否在累加器数组上存在,如果没有则返回-1,然后添加它。


当然可以用 Set 的方式来快速删除重复值,有兴趣的可以自己去谷歌一下。


验证括号


[..."(())()(()())"].reduce((a,i)=> i==='('?a+1:a-1,0);
// 0

[..."((())()(()())"].reduce((a,i)=> i==='('?a+1:a-1,0);
// 1

[..."(())()(()()))"].reduce((a,i)=> i==='('?a+1:a-1,0);
// -1

这是一个很酷的项目,之前在力扣中有刷到。


按属性分组


let obj = [
{name: 'Alice', job: 'Data Analyst', country: 'AU'},
{name: 'Bob', job: 'Pilot', country: 'US'},
{name: 'Lewis', job: 'Pilot', country: 'US'},
{name: 'Karen', job: 'Software Eng', country: 'CA'},
{name: 'Jona', job: 'Painter', country: 'CA'},
{name: 'Jeremy', job: 'Artist', country: 'SP'},
]
let ppl = obj.reduce((group, curP) => {
let newkey = curP['country']
if(!group[newkey]){
group[newkey]=[]
}
group[newkey].push(curP)
return group
}, [])

这里,我们根据 country 对第一个对象数组进行分组,在每次迭代中,我们检查键是否存在,如果不存在,我们创建一个数组,然后将当前的对象添加到该数组中,并返回组数组。


你可以用它做一个函数,用一个指定的键来分组对象。


扁平数组


let flattened = [[3, 4, 5], [2, 5, 3], [4, 5, 6]].reduce(
(singleArr, nextArray) => singleArr.concat(nextArray), [])

// 结果:[3, 4, 5, 2, 5, 3, 4, 5, 6]

这只是一层,如果有多层,可以用递归函数来解决,但我不太喜欢在 JS 上做递归的东西😂。


一个预定的方法是使用.flat方法,它将做同样的事情


[ [3, 4, 5],
[2, 5, 3],
[4, 5, 6]
].flat();

只有幂的正数


[-3, 4, 7, 2, 4].reduce((acc, cur) => {
if (cur> 0) {
let R = cur**2;
acc.push(R);
}
return acc;
}, []);

// 结果
[16, 49, 4, 144]

反转字符串


const reverseStr = str=>[...str].reduce((a,v)=>v+a)

这个方法适用于任何对象,不仅适用于字符串。调用reverseStr("Hola"),输出的结果是aloH


二进制转十进制


const bin2dec = str=>[...String(str)].reduce((acc,cur)=>+cur+acc*2,0)

// 等价于

const bin2dec = (str) => {
return [...String(str)].reduce((acc,cur)=>{
return +cur+acc*2
},0)
}

为了说明这一点,让我们看一个例子:(10111)->1+(1+(1+(0+(1+0*2)*2)*2)*2)*2


~完,我是刷碗智,励志等退休后,要回家摆地摊的人,我们下期见!




代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug


作者:前端小智
链接:https://juejin.cn/post/7032061650479874061

收起阅读 »

协程调度器详解

协程和线程的差异目的差异线程的目的是提高CPU资源使用率, 使多个任务得以并行的运行,是为了服务于机器的.协程的目的是为了让多个任务之间更好的协作,主要体现在代码逻辑上,是为了服务开发者 (能提升资源的利用率, 但并不是原始目的)调度差异线程的调度是系统完成的...
继续阅读 »

协程和线程的差异

目的差异

  • 线程的目的是提高CPU资源使用率, 使多个任务得以并行的运行,是为了服务于机器的.
  • 协程的目的是为了让多个任务之间更好的协作,主要体现在代码逻辑上,是为了服务开发者 (能提升资源的利用率, 但并不是原始目的)

调度差异

  • 线程的调度是系统完成的,一般是抢占式的,根据优先级来分配
  • 协程的调度是开发者根据程序逻辑指定好的,在不同的时期把资源合理的分配给不同的任务.

协程与线程的关系

协程并不是取代线程,而且抽象于线程之上,线程是被分割的CPU资源,协程是组织好的代码流程,协程需要线程来承载运行,线程是协程的资源

协程的核心竞争力

简化异步并发任务。

协程上下文 CoroutineContext

  • 协程总是运行在一些以 CoroutineContext 类型为代表的上下文中 ,协程上下文是各种不同元素的集合
  • 集合内部的元素Element是根据key去对应(Map特点),但是不允许重复(Set特点)
  • Element之间可以通过+号进行组合
  • Element有如下四类,共同组成了CoroutineContext
    • Job:协程的唯一标识,用来控制协程的生命周期(new、active、completing、completed、cancelling、cancelled)
    • CoroutineDispatcher:指定协程运行的线程(IO、Default、Main、Unconfined)
    • CoroutineName: 指定协程的名称,默认为coroutine
    • CoroutineExceptionHandler: 指定协程的异常处理器,用来处理未捕获的异常

它们的关系如图所示:

CoroutineDispatcher 作用

  • 用于指定协程的运行线程
  • kotlin已经内置了CoroutineDispatcher的4个实现,分别为 Dispatchers的Default、IO、Main、Unconfined字段

public actual object Dispatchers {

@JvmStatic
public actual val Default: CoroutineDispatcher = createDefaultDispatcher()

@JvmStatic
public val IO: CoroutineDispatcher = DefaultScheduler.IO

@JvmStatic
public actual val Unconfined: CoroutineDispatcher = kotlinx.coroutines.Unconfined

@JvmStatic
public actual val Main: MainCoroutineDispatcher get() = MainDispatcherLoader.dispatcher
}

Dispatchers.Default

Default根据useCoroutinesScheduler属性(默认为true) 去获取对应的线程池

  • DefaultScheduler :Kotlin内部自己实现的线程池逻辑
  • CommonPool:Java类库中的Executor实现的线程池逻辑
internal actual fun createDefaultDispatcher(): CoroutineDispatcher =
if (useCoroutinesScheduler) DefaultScheduler else CommonPool
internal object DefaultScheduler : ExperimentalCoroutineDispatcher() {
.....
}

open class ExperimentalCoroutineDispatcher(
private val corePoolSize: Int,
private val maxPoolSize: Int,
private val idleWorkerKeepAliveNs: Long,
private val schedulerName: String = "CoroutineScheduler"
) : ExecutorCoroutineDispatcher() {
constructor(
corePoolSize: Int = CORE_POOL_SIZE,
maxPoolSize: Int = MAX_POOL_SIZE,
schedulerName: String = DEFAULT_SCHEDULER_NAME
) : this(corePoolSize, maxPoolSize, IDLE_WORKER_KEEP_ALIVE_NS, schedulerName)

......
}
//java类库中的Executor实现线程池逻辑
internal object CommonPool : ExecutorCoroutineDispatcher() {}

如果想使用java类库中的线程池该如何使用呢?也就是修改useCoroutinesScheduler属性为false

internal const val COROUTINES_SCHEDULER_PROPERTY_NAME = "kotlinx.coroutines.scheduler"

internal val useCoroutinesScheduler = systemProp(COROUTINES_SCHEDULER_PROPERTY_NAME).let { value ->
when (value) {
null, "", "on" -> true
"off" -> false
else -> error("System property '$COROUTINES_SCHEDULER_PROPERTY_NAME' has unrecognized value '$value'")
}
}

internal actual fun systemProp(
propertyName: String
): String? =
try {
//获取系统属性
System.getProperty(propertyName)
} catch (e: SecurityException) {
null
}

从源码中可以看到,使用过获取系统属性拿到的值, 那我们就可以通过修改系统属性 去改变useCoroutinesScheduler的值, 具体修改方法为

 val properties = Properties()
properties["kotlinx.coroutines.scheduler"] = "off"
System.setProperties(properties)

DefaultScheduler的主要实现都在其父类 ExperimentalCoroutineDispatcher 中

open class ExperimentalCoroutineDispatcher(
private val corePoolSize: Int,
private val maxPoolSize: Int,
private val idleWorkerKeepAliveNs: Long,
private val schedulerName: String = "CoroutineScheduler"
) : ExecutorCoroutineDispatcher() {
public constructor(
corePoolSize: Int = CORE_POOL_SIZE,
maxPoolSize: Int = MAX_POOL_SIZE,
schedulerName: String = DEFAULT_SCHEDULER_NAME
) : this(corePoolSize, maxPoolSize, IDLE_WORKER_KEEP_ALIVE_NS, schedulerName)

constructor(
corePoolSize: Int = CORE_POOL_SIZE,
maxPoolSize: Int = MAX_POOL_SIZE
) : this(corePoolSize, maxPoolSize, IDLE_WORKER_KEEP_ALIVE_NS)

override val executor: Executor
get() = coroutineScheduler

private var coroutineScheduler = createScheduler()

//创建CoroutineScheduler实例
private fun createScheduler() = CoroutineScheduler(corePoolSize, maxPoolSize, idleWorkerKeepAliveNs, schedulerName)

override val executor: Executorget() = coroutineScheduler

override fun dispatch(context: CoroutineContext, block: Runnable): Unit =
try {
//dispatch方法委托到CoroutineScheduler的dispatch方法
coroutineScheduler.dispatch(block)
} catch (e: RejectedExecutionException) {
....
}

override fun dispatchYield(context: CoroutineContext, block: Runnable): Unit =
try {
//dispatchYield方法委托到CoroutineScheduler的dispatchYield方法
coroutineScheduler.dispatch(block, tailDispatch = true)
} catch (e: RejectedExecutionException) {
...
}

internal fun dispatchWithContext(block: Runnable, context: TaskContext, tailDispatch: Boolean) {
try {
//dispatchWithContext方法委托到CoroutineScheduler的dispatchWithContext方法
coroutineScheduler.dispatch(block, context, tailDispatch)
} catch (e: RejectedExecutionException) {
....
}
}
override fun close(): Unit = coroutineScheduler.close()
//实现请求阻塞
public fun blocking(parallelism: Int = BLOCKING_DEFAULT_PARALLELISM): CoroutineDispatcher {
require(parallelism > 0) { "Expected positive parallelism level, but have $parallelism" }
return LimitingDispatcher(this, parallelism, null, TASK_PROBABLY_BLOCKING)
}
//实现并发数量限制
public fun limited(parallelism: Int): CoroutineDispatcher {
require(parallelism > 0) { "Expected positive parallelism level, but have $parallelism" }
require(parallelism <= corePoolSize) { "Expected parallelism level lesser than core pool size ($corePoolSize), but have $parallelism" }
return LimitingDispatcher(this, parallelism, null, TASK_NON_BLOCKING)
}

....
}

实现请求数量限制是调用 LimitingDispatcher 类,其类实现为

private class LimitingDispatcher(
private val dispatcher: ExperimentalCoroutineDispatcher,
private val parallelism: Int,
private val name: String?,
override val taskMode: Int
) : ExecutorCoroutineDispatcher(), TaskContext, Executor {
//同步阻塞队列
private val queue = ConcurrentLinkedQueue<Runnable>()
//cas计数
private val inFlightTasks = atomic(0)

override fun dispatch(context: CoroutineContext, block: Runnable) = dispatch(block, false)

private fun dispatch(block: Runnable, tailDispatch: Boolean) {
var taskToSchedule = block
while (true) {

if (inFlight <= parallelism) {
//LimitingDispatcher的dispatch方法委托给了DefaultScheduler的dispatchWithContext方法
dispatcher.dispatchWithContext(taskToSchedule, this, tailDispatch)
return
}
......
}
}
}

Dispatchers.IO

先看下 Dispatchers.IO 的定义

    /**
*This dispatcher shares threads with a [Default][Dispatchers.Default] dispatcher, so using
* `withContext(Dispatchers.IO) { ... }` does not lead to an actual switching to another thread &mdash;
* typically execution continues in the same thread.
*/

@JvmStatic
public val IO: CoroutineDispatcher = DefaultScheduler.IO


Internal object DefaultScheduler : ExperimentalCoroutineDispatcher() {
val IO = blocking(systemProp(IO_PARALLELISM_PROPERTY_NAME, 64.coerceAtLeast(AVAILABLE_PROCESSORS)))

......

}

IO在DefaultScheduler中的实现 是调用blacking()方法,而blacking()方法最终实现是LimitingDispatcher类, 所以 从源码可以看出 Dispatchers.Default和IO 是在同一个线程中运行的,也就是共用相同的线程池。

而Default和IO 都是共享CoroutineScheduler线程池 ,kotlin内部实现了一套线程池两种调度策略,主要是通过dispatch方法中的Mode区分的

TypeMode
DefaultNON_BLOCKING
IOPROBABLY_BLOCKING
internal enum class TaskMode {

//执行CPU密集型任务
NON_BLOCKING,

//执行IO密集型任务
PROBABLY_BLOCKING,
}
fun dispatch(block: Runnable, taskContext: TaskContext = NonBlockingContext, tailDispatch: Boolean = false) {
......
if (task.mode == TaskMode.NON_BLOCKING) {
signalCpuWork() //Dispatchers.Default
} else {
signalBlockingWork() // Dispatchers.IO
}
}

Type处理策略适合场景特点
Default1、CoroutineScheduler最多有corePoolSize个线程被创建; 2、corePoolSize它的取值为max(2, CPU核心数),即它会尽量的等于CPU核心数复杂计算、视频解码等1、CPU密集型任务特点会消耗大量的CPU资源。2、因为线程本身也有栈等空间,同时线程过多,频繁的线程切换带来的消耗也会影响线程池的性能4.对于CPU密集型任务,线程池并发线程数等于CPU核心数才能让CPU的执行效率最大化
IO1、创建线程数不能大于maxPoolSize ,公式:max(corePoolSize, min(CPU核心数 * 128, 2^21 - 2))。网络请求、IO操作等1、IO密集型 执行任务时CPU会处于闲置状态,任务不会消耗大量的CPU资源。 2.线程执行IO密集型任务时大多数处于阻塞状态,处于阻塞状态的线程是不占用CPU的执行时间。3.Dispatchers.IO构造时通过LimitingDispatcher默认限制了最大线程并发数parallelism为max(64, CPU核心数),剩余的任务被放进队列中等待。

Dispatchers.Unconfined

任务执行在默认的启动线程。之后由调用resume的线程决定恢复协程的线程

internal object Unconfined : CoroutineDispatcher() {
//为false为不需要dispatch
override fun isDispatchNeeded(context: CoroutineContext): Boolean = false

override fun dispatch(context: CoroutineContext, block: Runnable) {
// 只有当调用yield方法时,Unconfined的dispatch方法才会被调用
// yield() 表示当前协程让出自己所在的线程给其他协程运行
val yieldContext = context[YieldContext]
if (yieldContext != null) {
yieldContext.dispatcherWasUnconfined = true
return
}
throw UnsupportedOperationException("Dispatchers.Unconfined.dispatch function can only be used by the yield function. " +
"If you wrap Unconfined dispatcher in your code, make sure you properly delegate " +
"isDispatchNeeded and dispatch calls.")
}
}

每一个协程都有对应的Continuation实例,其中的resumeWith用于协程的恢复,存在于DispatchedContinuation

public abstract class CoroutineDispatcher :
AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {
......

public final override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> =
DispatchedContinuation(this, continuation)

......

}

重点看resumeWith的实现以及类委托

internal class DispatchedContinuation<in T>(
@JvmField val dispatcher: CoroutineDispatcher,
@JvmField val continuation: Continuation<T>//协程suspend挂起方法产生的Continuation
) : DispatchedTask<T>(MODE_UNINITIALIZED), CoroutineStackFrame, Continuation<T> by continuation {
.....
override fun resumeWith(result: Result<T>) {
val context = continuation.context
val state = result.toState()
if (dispatcher.isDispatchNeeded(context)) {
_state = state
resumeMode = MODE_ATOMIC
dispatcher.dispatch(context, this)
} else {
executeUnconfined(state, MODE_ATOMIC) {
withCoroutineContext(this.context, countOrElement) {
continuation.resumeWith(result)
}
}
}
}
....
}

通过isDispatchNeeded(是否需要dispatch,Unconfined=false,default,IO=true)判断做不同处理

  • true:调用协程的CoroutineDispatcher的dispatch方法
  • false:调用executeUnconfined方法
private inline fun DispatchedContinuation<*>.executeUnconfined(
contState: Any?, mode: Int, doYield: Boolean = false,
block: () -> Unit
): Boolean {
assert { mode != MODE_UNINITIALIZED }
val eventLoop = ThreadLocalEventLoop.eventLoop
if (doYield && eventLoop.isUnconfinedQueueEmpty) return false
return if (eventLoop.isUnconfinedLoopActive) {
_state = contState
resumeMode = mode
eventLoop.dispatchUnconfined(this)
true
} else {
runUnconfinedEventLoop(eventLoop, block = block)
false
}
}

从threadlocal中取出eventLoop(eventLoop和当前线程相关的),判断是否在执行Unconfined任务

  1. 如果在执行则调用EventLoop的dispatchUnconfined方法把Unconfined任务放进EventLoop中
  2. 如果没有在执行则直接执行
internal inline fun DispatchedTask<*>.runUnconfinedEventLoop(
eventLoop: EventLoop,
block: () -> Unit
) {
eventLoop.incrementUseCount(unconfined = true)
try {
block()
while (true) {
if (!eventLoop.processUnconfinedEvent()) break
}
} catch (e: Throwable) {
handleFatalException(e, null)
} finally {
eventLoop.decrementUseCount(unconfined = true)
}
}

  1. 执行block()代码块,即上文提到的resumeWith()
  2. 调用processUnconfinedEvent()方法实现执行剩余的Unconfined任务,知道全部执行完毕跳出循环

EventLoop是存放与threadlocal,所以是跟当前线程相关联的,而EventLoop也是CoroutineDispatcher的一个子类

internal abstract class EventLoop : CoroutineDispatcher() {
.....
//双端队列实现存放Unconfined任务
private var unconfinedQueue: ArrayQueue<DispatchedTask<*>>? = null
//从队列的头部移出Unconfined任务执行
public fun processUnconfinedEvent(): Boolean {
val queue = unconfinedQueue ?: return false
val task = queue.removeFirstOrNull() ?: return false
task.run()
return true
}
//把Unconfined任务放进队列的尾部
public fun dispatchUnconfined(task: DispatchedTask<*>) {
val queue = unconfinedQueue ?:
ArrayQueue<DispatchedTask<*>>().also { unconfinedQueue = it }
queue.addLast(task)
}
.....
}

内部通过双端队列实现存放Unconfined任务

  1. EventLoop的dispatchUnconfined方法用于把Unconfined任务放进队列的尾部
  2. rocessUnconfinedEvent方法用于从队列的头部移出Unconfined任务执行

Dispatchers.Main

kotlin在JVM上的实现 Android就需要引入kotlinx-coroutines-android库,它里面有Android对应的Dispatchers.Main实现,

   public actual val Main: MainCoroutineDispatcher get() = MainDispatcherLoader.dispatcher

@JvmField
val dispatcher: MainCoroutineDispatcher = loadMainDispatcher()

private fun loadMainDispatcher(): MainCoroutineDispatcher {
return try {
val factories = if (FAST_SERVICE_LOADER_ENABLED) {
FastServiceLoader.loadMainDispatcherFactory()
} else {
// We are explicitly using the
// `ServiceLoader.load(MyClass::class.java, MyClass::class.java.classLoader).iterator()`
// form of the ServiceLoader call to enable R8 optimization when compiled on Android.
ServiceLoader.load(
MainDispatcherFactory::class.java,
MainDispatcherFactory::class.java.classLoader
).iterator().asSequence().toList()
}
factories.maxBy { it.loadPriority }?.tryCreateDispatcher(factories)
?: MissingMainCoroutineDispatcher(null)
} catch (e: Throwable) {
// Service loader can throw an exception as well
MissingMainCoroutineDispatcher(e)
}
}

internal fun loadMainDispatcherFactory(): List<MainDispatcherFactory> {
val clz = MainDispatcherFactory::class.java
if (!ANDROID_DETECTED) {
return load(clz, clz.classLoader)
}

return try {
val result = ArrayList<MainDispatcherFactory>(2)
createInstanceOf(clz, "kotlinx.coroutines.android.AndroidDispatcherFactory")?.apply { result.add(this) }
createInstanceOf(clz, "kotlinx.coroutines.test.internal.TestMainDispatcherFactory")?.apply { result.add(this) }
result
} catch (e: Throwable) {
// Fallback to the regular SL in case of any unexpected exception
load(clz, clz.classLoader)
}
}

通过反射获取AndroidDispatcherFactory 然后根据加载的优先级 去创建Dispatcher

internal class AndroidDispatcherFactory : MainDispatcherFactory {

override fun createDispatcher(allFactories: List<MainDispatcherFactory>) =
HandlerContext(Looper.getMainLooper().asHandler(async = true), "Main")

override fun hintOnError(): String? = "For tests Dispatchers.setMain from kotlinx-coroutines-test module can be used"

override val loadPriority: Int
get() = Int.MAX_VALUE / 2
}
internal class HandlerContext private constructor(
private val handler: Handler,
private val name: String?,
private val invokeImmediately: Boolean
) : HandlerDispatcher(), Delay {

public constructor(
handler: Handler,
name: String? = null
) : this(handler, name, false)

......

override fun dispatch(context: CoroutineContext, block: Runnable) {
handler.post(block)
}

......
}

而createDispatcher调用HandlerContext 类 通过调用Looper.getMainLooper()获取handler ,最终通过handler来实现在主线程中运行

Dispatchers.Main 其实就是把任务通过Handler运行在Android的主线程

收起阅读 »

Android C++系列:Linux文件IO操作

1.1 read/writeread函数从打开的设备或文件中读取数据。#include ssize_t read(int fd, void *buf, size_t count); //返回值:成功返回读取的字节数,出错返回-1并设置errno,如果在调r...
继续阅读 »

1.1 read/write

read函数从打开的设备或文件中读取数据。

#include 
ssize_t read(int fd, void *buf, size_t count);
//返回值:成功返回读取的字节数,出错返回-1并设置errno,如果在调read之前已到达文件末尾,则这次read返回0

参数count是请求读取的字节数,读上来的数据保存在缓冲区buf中,同时文件的当前读 写位置向后移。

注意这个读写位置和使用C标准I/O库时的读写位置有可能不同,这个读写 位置是记在内核中的,而使用C标准I/O库时的读写位置是用户空间I/O缓冲区中的位置。比如用fgetc读一个字节,fgetc有可能从内核中预读1024个字节到I/O缓冲区中,再返回第一 个字节,这时该文件在内核中记录的读写位置是1024,而在FILE结构体中记录的读写位置是 1。

注意返回值类型是ssize_t,表示有符号的size_t,这样既可以返回正的字节数、0(表 示到达文件末尾)也可以返回负值-1(表示出错)。

read函数返回时,返回值说明了buf中 前多少个字节是刚读上来的。有些情况下,实际读到的字节数(返回值)会小于请求读的字节数count,例如:

  • 读常规文件时,在读到count个字节之前已到达文件末尾。例如,距文件末尾还有30个 字节而请求读100个字节,则read返回30,下次read将返回0。

  • 从终端设备读,通常以行为单位,读到换行符就返回了。

  • 从网络读,根据不同的传输层协议和内核缓存机制,返回值可能小于请求的字节数,后面socket编程部分会详细讲解。

write函数向打开的设备或文件中写数据。

#include 
ssize_t write(int fd, const void *buf, size_t count);
返回值:成功返回写入的字节数,出错返回-1并设置errno

写常规文件时,write的返回值通常等于请求写的字节数count,而向终端设备或网络写则不一定。

1.2 阻塞和非阻塞

读常规文件是不会阻塞的,不管读多少字节,read一定会在有限的时间内返回。从终端设备或网络读则不一定,如果从终端输入的数据没有换行符,调用read读终端设备就会阻塞,如果网络上没有接收到数据包,调用read从网络读就会阻塞,至于会阻塞多长时间也是不确定的,如果一直没有数据到达就一直阻塞在那里。同样,写常规文件是不会阻塞的,而向终端设备或网络写则不一定。

现在明确一下阻塞(Block)这个概念。当进程调用一个阻塞的系统函数时,该进程被置于睡眠(Sleep)状态,这时内核调度其它进程运行,直到该进程等待的事件发生了(比如网络上接收到数据包,或者调用sleep指定的睡眠时间到了)它才有可能继续运行。与睡眠状态相对的是运行(Running)状态,在Linux内核中,处于运行状态的进程分为两种情况:

  • 正在被调度执行。CPU处于该进程的上下文环境中,程序计数器(eip)里保存着该进程的指令地址,通用寄存器里保存着该进程运算过程的中间结果,正在执行该进程的指令,正在读写该进程的地址空间。
  • 就绪状态。该进程不需要等待什么事件发生,随时都可以执行,但CPU暂时还在执行另 一个进程,所以该进程在一个就绪队列中等待被内核调度。系统中可能同时有多个就绪的进 程,那么该调度谁执行呢?内核的调度算法是基于优先级和时间片的,而且会根据每个进程 的运行情况动态调整它的优先级和时间片,让每个进程都能比较公平地得到机会执行,同时 要兼顾用户体验,不能让和用户交互的进程响应太慢。

下面这个小程序从终端读数据再写回终端。

1.2.1 阻塞读终端

#include  #include 
int main(void) {
char buf[10];
int n;
n = read(STDIN_FILENO, buf, 10);
if (n < 0) {
perror("read STDIN_FILENO");
exit(1);
}
write(STDOUT_FILENO, buf, n);
return 0;
}

执行结果如下:

$ ./a.out hello(回车)
hello
$ ./a.out
hello world(回车) hello worl$ d
bash: d: command not found

第一次执行a.out的结果很正常,而第二次执行的过程有点特殊,现在分析一下:

Shell进程创建a.out进程,a.out进程开始执行,而shell进程睡眠等待a.out进程退出。

a.out调用read时睡眠等待,直到终端设备输入了换行符才从read返回,read只读走10 个字符,剩下的字符仍然保存在内核的终端设备输入缓冲区中。

a.out进程打印并退出,这时shell进程恢复运行,Shell继续从终端读取用户输入的命令,于是读走了终端设备输入缓冲区中剩下的字符d和换行符,把它当成一条命令解释执 行,结果发现执行不了,没有d这个命令。

如果在open一个设备时指定了O_NONBLOCK标志,read/write就不会阻塞。以read为例, 如果设备暂时没有数据可读就返回-1,同时置errno为EWOULDBLOCK(或者EAGAIN,这两个宏定义的值相同),表示本来应该阻塞在这里(would block,虚拟语气),事实上并没 有阻塞而是直接返回错误,调用者应该试着再读一次(again)。这种行为方式称为轮询 (Poll),调用者只是查询一下,而不是阻塞在这里死等,这样可以同时监视多个设备:

while(1) { 
非阻塞read(设备1);
if(设备1有数据到达)
处理数据;
非阻塞read(设备2);
if(设备2有数据到达)
处理数据; ...
}

如果read(设备1)是阻塞的,那么只要设备1没有数据到达就会一直阻塞在设备1的read 调用上,即使设备2有数据到达也不能处理,使用非阻塞I/O就可以避免设备2得不到及时处 理。

非阻塞I/O有一个缺点,如果所有设备都一直没有数据到达,调用者需要反复查询做无用功,如果阻塞在那里,操作系统可以调度别的进程执行,就不会做无用功了。在使用非阻塞I/O时,通常不会在一个while循环中一直不停地查询(这称为Tight Loop),而是每延迟 等待一会儿来查询一下,以免做太多无用功,在延迟等待的时候可以调度其它进程执行。

while(1) { 
非阻塞read(设备1);
if(设备1有数据到达)
处理数据;
非阻塞read(设备2);
if(设备2有数据到达)
处理数据;
...
sleep(n);
}

这样做的问题是,设备1有数据到达时可能不能及时处理,最长需延迟n秒才能处理,而且反复查询还是做了很多无用功。以后要学习的select(2)函数可以阻塞地同时监视多个设 备,还可以设定阻塞等待的超时时间,从而圆满地解决了这个问题。

以下是一个非阻塞I/O的例子。目前我们学过的可能引起阻塞的设备只有终端,所以我们用终端来做这个实验。程序开始执行时在0、1、2文件描述符上自动打开的文件就是终 端,但是没有O_NONBLOCK标志。所以就像例 28.2 “阻塞读终端”一样,读标准输入是阻塞 的。我们可以重新打开一遍设备文件/dev/tty(表示当前终端),在打开时指定O_NONBLOCK 标志。

1.2.2 非阻塞读终端

#include  
#include
#include
#include
#include
#define MSG_TRY "try again\n"
int main(void) {
char buf[10];
int fd, n;
fd = open("/dev/tty", O_RDONLY|O_NONBLOCK);
if(fd<0) {
perror("open /dev/tty");
exit(1);
}
tryagain:
n = read(fd, buf, 10);
if (n < 0) {
if (errno == EAGAIN) {
sleep(1);
write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));
goto tryagain;
}
perror("read /dev/tty");
exit(1);
}
write(STDOUT_FILENO, buf, n); close(fd);
return 0;
}

以下是用非阻塞I/O实现等待超时的例子。既保证了超时退出的逻辑又保证了有数据到达时处理延迟较小。

1.2.3 非阻塞读终端和等待超时

#include  
#include
#include
#include
#include
#define MSG_TRY "try again\n"
#define MSG_TIMEOUT "timeout\n"
int main(void) {
char buf[10];
int fd, n, i;
fd = open("/dev/tty", O_RDONLY|O_NONBLOCK);
if(fd<0) {
perror("open /dev/tty");
exit(1);
}
for(i=0; i<5; i++) {
n = read(fd, buf, 10);
if(n>=0)
break;
if(errno!=EAGAIN) {
perror("read /dev/tty");
exit(1);
}
sleep(1);
write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));
}
if(i==5)
write(STDOUT_FILENO, MSG_TIMEOUT, strlen(MSG_TIMEOUT));
else
write(STDOUT_FILENO, buf, n);
close(fd);
return 0;
}

1.3 lseek

每个打开的文件都记录着当前读写位置,打开文件时读写位置是0,表示文件开头,通常读写多少个字节就会将读写位置往后移多少个字节。但是有一个例外,如果以O_APPEND方 式打开,每次写操作都会在文件末尾追加数据,然后将读写位置移到新的文件末尾。lseek 和标准I/O库的fseek函数类似,可以移动当前读写位置(或者叫偏移量)。

 #include 
#include
off_t lseek(int fd, off_t offset, int whence);

参数offset和whence的含义和fseek函数完全相同。只不过第一个参数换成了文件描述符。和fseek一样,偏移量允许超过文件末尾,这种情况下对该文件的下一次写操作将延长 文件,中间空洞的部分读出来都是0。

若lseek成功执行,则返回新的偏移量,因此可用以下方法确定一个打开文件的当前偏 移量:

off_t currpos;
currpos = lseek(fd, 0, SEEK_CUR);

这种方法也可用来确定文件或设备是否可以设置偏移量,常规文件都可以设置偏移量, 而设备一般是不可以设置偏移量的。如果设备不支持lseek,则lseek返回-1,并将errno 设置为ESPIPE。注意fseek和lseek在返回值上有细微的差别,fseek成功时返回0失败时返 回-1,要返回当前偏移量需调用ftell,而lseek成功时返回当前偏移量失败时返回-1。

1.4 fcntl

先前我们以read终端设备为例介绍了非阻塞I/O,为什么我们不直接对STDIN_FILENO做 非阻塞read,而要重新open一遍/dev/tty呢?因为STDIN_FILENO在程序启动时已经被自动 打开了,而我们需要在调用open时指定O_NONBLOCK标志。这里介绍另外一种办法,可以用 fcntl函数改变一个已打开的文件的属性,可以重新设置读、写、追加、非阻塞等标志(这 些标志称为File Status Flag),而不必重新open文件。

#include  #include 
int fcntl(int fd, int cmd);
int fcntl(int fd, int cmd, long arg);
int fcntl(int fd, int cmd, struct flock *lock);

这个函数和open一样,也是用可变参数实现的,可变参数的类型和个数取决于前面的 cmd参数。下面的例子使用F_GETFL和F_SETFL这两种fcntl命令改变STDIN_FILENO的属性,加 上O_NONBLOCK选项,实现和例 28.3 “非阻塞读终端”同样的功能。

1.4.1 用fcntl改变File Status Flag

#include  
#include
#include
#include
#include
#define MSG_TRY "try again\n"
int main(void) {
char buf[10];
int n;
int flags;
flags = fcntl(STDIN_FILENO, F_GETFL); flags |= O_NONBLOCK;
if (fcntl(STDIN_FILENO, F_SETFL, flags) == -1) {
perror("fcntl");
exit(1);
}
tryagain:
n = read(STDIN_FILENO, buf, 10);
if (n < 0) {
if (errno == EAGAIN) {
sleep(1);
write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));
goto tryagain;
}
perror("read stdin");
exit(1);
}
write(STDOUT_FILENO, buf, n);
return 0;
}

1.5 ioctl

ioctl用于向设备发控制和配置命令,有些命令也需要读写一些数据,但这些数据是 不能用read/write读写的,称为Out-of-band数据。也就是说,read/write读写的数据是 in-band数据,是I/O操作的主体,而ioctl命令传送的是控制信息,其中的数据是辅助的数 据。例如,在串口线上收发数据通过read/write操作,而串口的波特率、校验位、停止位通 过ioctl设置,A/D转换的结果通过read读取,而A/D转换的精度和工作频率通过ioctl设置。

#include 
int ioctl(int d, int request, ...);

d是某个设备的文件描述符。request是ioctl的命令,可变参数取决于request,通常是 一个指向变量或结构体的指针。若出错则返回-1,若成功则返回其他值,返回值也是取决于 request。

以下程序使用TIOCGWINSZ命令获得终端设备的窗口大小。

#include  
#include
#include
#include
int main(void) {
struct winsize size;
if (isatty(STDOUT_FILENO) == 0)
exit(1);
if(ioctl(STDOUT_FILENO, TIOCGWINSZ, &size)<0) {
perror("ioctl TIOCGWINSZ error");
exit(1);
}
printf("%d rows, %d columns\n", size.ws_row, size.ws_col);
return 0;
}

在图形界面的终端里多次改变终端窗口的大小并运行该程序,观察结果。

1.6 总结

本文介绍了read/write的系统调用,以及阻塞、非阻塞相关的概念以及配置方式,等待超时方式。还介绍了lseek、fcntl、ioctl文件操作相关的系统调用。

原文链接:https://juejin.cn/post/7031846767289499685?utm_source=gold_browser_extension

收起阅读 »

iOS 简单模拟服务器如何解析客户端传来的表单数据及图片格式数据并本地保存

iOS
废话开篇:在日常开发中经常会有上传表单及图片到服务器场景,这里有两种实现方式:一、单独封装一个图片文件格式存储代码,服务器对 Response 返回值里面返回服务器图片路径,再通过其他接口绑定服务器图片路径;二、表单及图片文件直接提交。那么...
继续阅读 »


废话开篇:在日常开发中经常会有上传表单及图片到服务器场景,这里有两种实现方式:一、单独封装一个图片文件格式存储代码,服务器对 Response 返回值里面返回服务器图片路径,再通过其他接口绑定服务器图片路径;二、表单及图片文件直接提交。那么,其实说是两种方式,其实归根到底就是一种:数据传输与接收。那么,下面就在 OC 上简单模拟服务器如何解析客户端传来的表单数据及图片格式数据

以前文章地址:

# iOS 简单模拟 https 证书信任逻辑

# iOS 基于 CocoaHTTPServer 搭建手机内部服务器,实现 http 及 https 访问、传输数据

基于上述文章继续进行本次的 模拟服务器如何解析客户端传来的表单数据及图片格式数据

效果如下:

屏幕录制2021-11-18 下午4.17.38.gif

前言说明:

这里简单说一下 AFNetwork 下是如何同时进行数据参数提交及文件上传的。这里只是简单的说一下思路:

先上一段简单的 AF 请求代码

    AFHTTPSessionManager * m = [[AFHTTPSessionManager alloc] initWithBaseURL:[NSURL URLWithString:@"https://10.10.60.20"]];

NSDictionary * dic = @{@"title":@"中国万岁",@"name":@"中国人"};

    [m POST:@"https://10.10.60.20:12345/doPost" parameters:dic headers:@{} constructingBodyWithBlock:^(**id**<AFMultipartFormData>  _Nonnull formData) {

        NSDate *date = [NSDate dateWithTimeIntervalSinceNow:0]; // 获取当前时间0秒后的时间

        NSTimeInterval time = [date timeIntervalSince1970]*1000;// *1000 是精确到毫秒(13位),不乘就是精确到秒(10位)

        NSString *timeString = [NSString stringWithFormat:@"iOS%.0f", time];

        UIImage * image = [UIImage imageNamed:@"sea"];

        NSData *data = UIImageJPEGRepresentation(image, 0.5f);

        [formData appendPartWithFileData:data name:@"file" fileName:[NSString stringWithFormat:@"%@.jpg",timeString] mimeType:@"image/jpg"];

        } progress:^(NSProgress * _Nonnull uploadProgress) {          

        } success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {

        } failure:^(NSURLSessionDataTask * **_Nullable** task, NSError * _Nonnull error) {   

        }
     ];

1、网络请求参数的传入

这里代码无需过多解释,dic 就是要传输的请求参数,那么,在这个参数完成之后,其实 AFNetworking 就对参数进行了存储,并且在后面的图片上传的时候用拼接的 NSData 的方式进行数据拼接。

2、图片数据获取及 NSData 拼接

AF 调用下面的方法进行了请求数据的拼接。

[formData appendPartWithFileData:data name:@"file" fileName:[NSString stringWithFormat:@"%@.jpg",timeString] mimeType:@"image/jpg"];

3、基于第二步骤,创建多个数据读取对象,通过 Stream 进行 NSData 的依次读取,因为 AF 下的 POST 请求会跟一个 Stream 进行绑定

[self.request setHTTPBodyStream:self.bodyStream];

那么,在开启的发送请求前,AF 又重写了 Stream 下

- (NSInteger)read:(uint8_t *)buffer maxLength:(NSUInteger)length

方法。进而可以在 Stream 读取的过程中对多个文件 data 进行拼接,最终将整个数据进行一次传输。

4、注意事项:(1)AF 会在 header 里面进行数据总长度的标定,这样服务器在最先拿到 header 时便可以知晓此次传输的数据总长度。(2)AF 会随机生成一个 boundary 也放到 header 里面,这个参数的目的就是将请求中不通的 参数文件进行边界划分,这样,服务器在解析的时候就知道了哪些 data 是一个完整的数据。当然,AF 也会标定一下传输类型在 header 里,比如:Content-Type

好了,上述其实只是一个铺垫,来看一下最终如何总 data 里解析出请求参数及图片文件

步骤一、基于 CocoaHTTPServer 搭建完的本地 OC 服务器进行数据解析

对于如何搭建的请参考上面的文章链接

这里要处理的就是下面的这个方法,客户端传过来的数据都会在这个方法里执行,因为一个系统的 Stream 一次性读取最大数是有限制的,所以,对于大文件上传的过程,此方法会走多次。

- (void)processBodyData:(NSData *)postDataChunk;

思路:因为服务器收到所有的 data 里完整的参数数据都是用换行符来分割的,那么通过对 "\r\n" 换行符进行切割,那么,两个换行符之间的数据就是一个完整的参数。

- (void)parseData:(NSData *)postDataChunk
{
//这里记录图片文件 data 在数据接收总 data 里的初始位置索引
    int fileDataStartIndex = 0;
    //换行符\r\n
    UInt16 separatorBytes = 0X0A0D;
    NSData * separatorData = [NSData dataWithBytes:&separatorBytes length:2];
    int l = (int)[separatorData length];
//遍历接收的数据,找到所有以 0A0D 分割的完整 data 数据
    for (int i = 0; i < [postDataChunk length] - l; i++) {
//以换行符长度为单位依次排查、寻找
        NSRange searchRange = {i,l};
        //是换行符
        if ([[postDataChunk subdataWithRange:searchRange] isEqualToData:separatorData]) {
            
            //获取换行符之间的data的位置
            NSRange newDataRange = {self.dataStartIndex,i - self.dataStartIndex};
            self.dataStartIndex = i + l;
//这里先进性请求参数的筛选,文件data保存位置偏后,那么,一开始就需要 self.paramReceiveComplete 标识来标定是否排查到文件 data 了
            if (self.paramReceiveComplete) {
                fileDataStartIndex = i + l;
                continue;
            }

            //跳过换行符
            i += (l-1);
//获取换行符之间的完整数据格式
            NSData * newData = [postDataChunk subdataWithRange:newDataRange];
//判断是否为空
            if ([newData length]) {
//获取文本信息
                NSString *content = [[NSString alloc] initWithData:newData encoding:NSUTF8StringEncoding];
//替换所有的换行特殊字符
                content = [content stringByReplacingOccurrencesOfString:@"\r\n" withString:@""];
//这里注意的是边界信息 Boundary ,也就是 AF 给钉里面的数据不解析
                if (content.length && ![content containsString:@"--Boundary"]) {
//如果解析到文件,那么 content 里会包含 name="file" 的标识,用此标识进行数据格式的判断
                    if ([content containsString:@"name=\"file\""]){
//读到文件了
                        self.currentParserType = @"file";
                    } else {
//请求参数
                        self.currentParserType = @"text/plain";
                    }

                    //表单数据解析
                    if ([self.currentParserType containsString:@"text/plain"]){
//content 里面包含 form-data,说明是数据参数说明,里面会包含 key 值
                        if ([content containsString:@"form-data"]) {
                            NSString * key = [content componentsSeparatedByString:@"name="].lastObject;
                            key = [key stringByReplacingOccurrencesOfString:@"\"" withString:@""];
//这里临时保存了key值,在后面解析到 value 的时候进行数据绑定
                            self.currentParamKey = key;
                        } else {
//解析到了 value 用 self.currentParamKey 进行绑定
                            if (self.currentParamKey && content) {
                                [self.receiveParamDic setValue:content forKey:self.currentParamKey];
                            }
                        }
                    } else {
                        //开始文件处理,标定一下,因为由于文件大小的影响,此方法会走多次,那么,在一开始标定后,下一次再进来就直接进行文件数据的拼接
                        self.paramReceiveComplete = YES;
                    }
                }
            }
        }
    }

//文件的写入(其实这里不是很严谨,因为请求参数较小的原因,所以,即便是第一次执行此方法,里面也会有文件 data 开始读取的情况)
    NSRange fileDataRange = {fileDataStartIndex,postDataChunk.length - fileDataStartIndex};
    NSData * fileData = [postDataChunk subdataWithRange:fileDataRange];
    [self.outputStream write:[fileData bytes] maxLength:fileData.length];

}

步骤二、数据写入沙盒

声明一个 NSOutputStream 对象

@property (nonatomic,strong) NSOutputStream * outputStream;

CocoaHTTPServer -> HTTPConnection 类是不进行常规化的 init 的,所以,初始化 outputStream 这里用懒加载的形式。

- (NSOutputStream *)outputStream

{

    if (!_outputStream) {
        NSString * cachePath = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject;
        NSString * filePath = [cachePath stringByAppendingPathComponent:@"wsl.png"];
        NSLog(@"filePath = %@",filePath);
        _outputStream = [[NSOutputStream alloc] initToFileAtPath:filePath append:**YES**];
        [_outputStream open];
    }
    return _outputStream;
}

进行文件写入沙盒操作:

    NSRange fileDataRange = {fileDataStartIndex,postDataChunk.length - fileDataStartIndex};
    NSData * fileData = [postDataChunk subdataWithRange:fileDataRange];
    [self.outputStream write:[fileData bytes] maxLength:fileData.length];

在处理完数据后关闭流

- (NSObject<HTTPResponse> *)httpResponseForMethod:(NSString *)method URI:(NSString *)path
{
    [self.outputStream close];
    self.outputStream = nil;
}

步骤三、查看运行结果

先看是否获取了请求的参数:

image.png

在看图片是否保存完成,通过打印模拟器的沙盒路径,直接 前往文件夹 即可找到沙盒文件

image.png

可以看到,这里保存图片也成功了。

image.png

这里说明一下:

遵循 MultipartFormDataParserDelegate 协议也可以直接获取文件的 data ,直接去读,再去存即可。但是它没有暴露给外界数据请求的 key 而只有 value,但是如果仅作为文件的传输还是很方便的。

如下:

遵循代理协议

image.png

声明 MultipartFormDataParser 对象

image.png

MultipartFormDataParser 对象进行数据解析

image.png

进行文件数据解析代理执行

image.png

其实 CocoaHTTPServer 封装的解析工具类实现原理亦是如此。

好了,简单模拟服务器如何解析客户端传来的表单数据及图片格式数据并本地保存 功能就实现完了,代码拙劣,大神勿笑。

收起阅读 »

Metal 框架之渲染管线渲染图元

iOS
「这是我参与11月更文挑战的第14天,活动详情查看:2021最后一次更文挑战」概述在 《 Metal 框架之使用 Metal 来绘制视图内容 》中,介绍了如何设置 MTKView 对象并使用渲染通道更改视图的内容,实现了将背景色渲染为视图的内容。本示...
继续阅读 »


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

概述

在 《 Metal 框架之使用 Metal 来绘制视图内容 》中,介绍了如何设置 MTKView 对象并使用渲染通道更改视图的内容,实现了将背景色渲染为视图的内容。本示例将介绍如何配置渲染管道,作为渲染通道的一部分,在视图中绘制一个简单的 2D 彩色三角形。该示例为每个顶点提供位置和颜色,渲染管道使用该数据,在指定的顶点颜色之间插入颜色值来渲染三角形。

在本示例中,将介绍如何编写顶点和片元函数、如何创建渲染管道状态对象,以及最后对绘图命令进行编码。

triangle’s vertices.png

理解 Metal 渲染管线

渲染管线处理绘图命令并将数据写入渲染通道的目标中。一个完整地渲染管线有许多阶段组成,一些阶段需要使用着色器进行编程,而一些阶段则需要配置固定的功能件。本示例的管线主要包含三个阶段:顶点阶段、光栅化阶段和片元阶段。其中,顶点阶段和片元阶段是可编程的,这可以使用 Metal Shading Language (MSL) 来编写函数,而光栅化阶段则是不可编程的,直接使用固有功能件来配置。

render piple.png

渲染从绘图命令开始,其中包括顶点个数和要渲染的图元类型。如下是本例子的绘图命令:


// Draw the triangle.

[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle

                  vertexStart:0

                  vertexCount:3];


顶点阶段会处理每个顶点的数据。当顶点经过顶点阶段处理后,渲染管线会对图元光栅化处理,以此来确定渲染目标中的哪些像素位于图元的边界内(即图元可以转化成的像素)。片元阶段是要确定渲染目标的像素值。

自定义渲染管线

顶点函数为单个顶点生成数据,片元函数为单个片元生成数据,可以通过编写函数来指定它们的工作方式。我们可以依据希望管道完成什么功能以及如何完成来配置管道的各个阶段。

决定将哪些数据传递到渲染管道以及将哪些数据传递到管道的后期阶段,通常可以在三个地方执行此操作:

  • 管道的输入,由 App 提供并传递到顶点阶段。

  • 顶点阶段的输出,它被传递到光栅化阶段。

  • 片元阶段的输入,由 App 提供或由光栅化阶段生成。

在本示例中,管道的输入数据包括顶点的位置及其颜色。为了演示顶点函数中执行的转换类型,输入坐标在自定义坐标空间中定义,以距视图中心的像素为单位进行测量。这些坐标需要转换成 Metal 的坐标系。

声明一个 AAPLVertex 结构,使用 SIMD 向量类型来保存位置和颜色数据。


typedef struct

{

    vector_float2 position;

    vector_float4 color;

} AAPLVertex;


SIMD 类型在 Metal Shading Language 中很常见,相应的需要在 App 中使用 simd 库。 SIMD 类型包含特定数据类型的多个通道,因此将位置声明为 vector_float2 意味着它包含两个 32 位浮点值(x 和 y 坐标)。颜色使用 vector_float4 存储,因此它们有四个通道:红色、绿色、蓝色和 alpha。

在 App 中,输入数据使用常量数组指定:


static const AAPLVertex triangleVertices[] =

{

    // 2D positions,    RGBA colors

    { {  250,  -250 }, { 1, 0, 0, 1 } },

    { { -250,  -250 }, { 0, 1, 0, 1 } },

    { {    0,   250 }, { 0, 0, 1, 1 } },

};


顶点阶段为顶点生成数据,需要提供颜色和变换的位置。使用 SIMD 类型声明一个包含位置和颜色值的 RasterizerData 结构。


struct RasterizerData

{

    // The [[position]] attribute of this member indicates that this value

    // is the clip space position of the vertex when this structure is

    // returned from the vertex function.

    float4 position [[position]];



    // Since this member does not have a special attribute, the rasterizer

    // interpolates its value with the values of the other triangle vertices

    // and then passes the interpolated value to the fragment shader for each

    // fragment in the triangle.

    float4 color;

};


输出位置(在下面详细描述)必须定义为 vector_float4 类型。颜色在输入数据结构中声明。

需要告诉 Metal 光栅化数据中的哪个字段提供位置数据,因为 Metal 不会对结构中的字段强制执行任何特定的命名约定。使用 [[position]] 属性限定符来标记位置字段,使用它来保存该字段输出位置。

fragment 函数只是将光栅化阶段的数据传递给后面的阶段,因此它不需要任何额外的参数。

定义顶点函数

需要使用 vertex 关键字来定义顶点函数,包含入参和出参。


vertex RasterizerData

vertexShader(uint vertexID [[vertex_id]],

             constant AAPLVertex *vertices [[buffer(AAPLVertexInputIndexVertices)]],

             constant vector_uint2 *viewportSizePointer [[buffer(AAPLVertexInputIndexViewportSize)]])


第一个参数 vertexID 使用 [[vertex_id]] 属性限定符来修饰,它是 Metal 关键字。当执行渲染命令时,GPU 会多次调用顶点函数,为每个顶点生成一个唯一值。

第二个参数 vertices 是一个包含顶点数据的数组,使用之前定义的 AAPLVertex 结构。

要将位置转换为 Metal 的坐标,该函数需要绘制三角形的视口的大小(以像素为单位),因此需要将其存储在 viewportSizePointer 参数中。

第二个和第三个参数使用 [[buffer(n)]] 属性限定符来修饰。默认情况下,Metal 自动为每个参数分配参数表中的插槽。当使用 [[buffer(n)]] 限定符修饰缓冲区参数时,明确地告诉 Metal 要使用哪个插槽。显式声明插槽可以方便的修改着色器代码,而无需更改 App 代码。

编写顶点函数

 编写的顶点函数必须生成输出结构的两个字段,使用 vertexID 参数索引顶点数组并读取顶点的输入数据,还需要获取视口尺寸。


float2 pixelSpacePosition = vertices[vertexID].position.xy;

// Get the viewport size and cast to float.

vector_float2 viewportSize = vector_float2(*viewportSizePointer);
复制代码

顶点函数必须提供裁剪空间坐标中的位置数据,这些位置数据是 3D 的点,使用四维齐次向量 (x,y,z,w) 来表示。光栅化阶段获取输出位置,并将 x、y 和 z 坐标除以 w 以生成归一化设备坐标中的 3D 点。归一化设备坐标与视口大小无关。

NDC_ coordinates.png

归一化设备坐标使用左手坐标系来映射视口中的位置。图元被裁剪到这个坐标系中的一个裁剪框上,然后被光栅化。剪切框的左下角位于 (-1.0,-1.0) 坐标处,右上角位于 (1.0,1.0) 处。正 z 值指向远离相机(指向屏幕)。z 坐标的可见部分在 0.0(近剪裁平面)和 1.0(远剪裁平面)之间。

下图是将输入坐标系转换为归一化的设备坐标系。

ndc转换.png

因为这是一个二维应用,不需要齐次坐标,所以先给输出坐标写一个默认值,w值设置为1.0,其他坐标设置为0.0。这意味顶点函数在该坐标空间中生成的 (x,y) 已经在归一化设备坐标空间中了。将输入位置除以1/2视口大小就生成归一化的设备坐标。由于此计算是使用 SIMD 类型执行的,因此可以使用一行代码同时计算两个通道,执行除法并将结果放在输出位置的 x 和 y 通道中。


out.position = vector_float4(0.0, 0.0, 0.0, 1.0);

out.position.xy = pixelSpacePosition / (viewportSize / 2.0);


最后,将颜色值赋给 out.color 作为返回值。


out.color = vertices[vertexID].color;


编写片元函数

片元阶段对渲染目标可以做修改处理。光栅化器确定渲染目标的哪些像素被图元覆盖,仅处于三角形片元中的那些像素才会被渲染。

光栅化阶段.png

片元函数处理光栅化后的位置信息,并计算每个渲染目标的输出值。这些片元值由管道中的后续阶段处理,最终写入渲染目标。

本示例中的片元着色器接收与顶点着色器的输出中声明的相同参数。使用 fragment 关键字声明片元函数。它只有一个输入参数,与顶点阶段提供的 RasterizerData 结构相同。添加 [[stage_in]] 属性限定符以指示此参数由光栅化器生成。


fragment float4 fragmentShader(RasterizerData in [[stage_in]])


如果片元函数写入多个渲染目标,则必须为每个渲染目标声明一个变量。由于此示例只有一个渲染目标,因此可以直接指定一个浮点向量作为函数的输出,此输出是要写入渲染目标的颜色。

光栅化阶段计算每个片元参数的值并用它们调用片元函数。光栅化阶段将其颜色参数计算为三角形顶点处颜色的混合,片元离顶点越近,顶点对最终颜色的贡献就越大。

颜色插值.png

将内插颜色作为函数的输出返回。


return in.color;


创建渲染管线状态对象

完成着色器函数编写后,需要创建一个渲染管道,通过 MTLLibrary 为每个着色器函数指定一个 MTLFunction 对象。


id<MTLLibrary> defaultLibrary = [_device newDefaultLibrary];


id<MTLFunction> vertexFunction = [defaultLibrary newFunctionWithName:@"vertexShader"];

id<MTLFunction> fragmentFunction = [defaultLibrary newFunctionWithName:@"fragmentShader"];


接下来,创建一个 MTLRenderPipelineState 对象,使用 MTLRenderPipelineDescriptor 来配置管线。


MTLRenderPipelineDescriptor *pipelineStateDescriptor = [[MTLRenderPipelineDescriptor alloc] init];

pipelineStateDescriptor.label = @"Simple Pipeline";

pipelineStateDescriptor.vertexFunction = vertexFunction;

pipelineStateDescriptor.fragmentFunction = fragmentFunction;

pipelineStateDescriptor.colorAttachments[0].pixelFormat = mtkView.colorPixelFormat;



_pipelineState = [_device newRenderPipelineStateWithDescriptor:pipelineStateDescriptor

                                                        
error:&error];




除了指定顶点和片元函数之外,还可以指定渲染目标的像素格式。像素格式 (MTLPixelFormat) 定义了像素数据的内存布局。对于简单格式,此定义包括每个像素的字节数、存储在像素中的数据通道数以及这些通道的位布局。渲染管线状态必须使用与渲染通道指定的像素格式兼容的像素格式才能够正确渲染,由于此示例只有一个渲染目标并且它由视图提供,因此将视图的像素格式复制到渲染管道描述符中。

使用 Metal 创建渲染管道状态对象时,渲染管线需要转换片元函数的输出像素格式为渲染目标的像素格式。如果要针对不同的像素格式,则需要创建不同的管道状态对象,可以在不同像素格式的多个管道中使用相同的着色器。

设置视口

有了管道的渲染管道状态对象后,就可以使用渲染命令编码器来渲染三角形了。首先,需要设置视口来告诉 Metal 要绘制到渲染目标的哪个部分。


// Set the region of the drawable to draw into.

[renderEncoder setViewport:(MTLViewport){0.0, 0.0, _viewportSize.x, _viewportSize.y, 0.0, 1.0 }];


设置渲染管线状态

为渲染管线指定渲染管线状态对象。


[renderEncoder setRenderPipelineState:_pipelineState];


将参数数据发送到顶点函数

通常使用缓冲区 (MTLBuffer) 将数据传递给着色器。但是,当只需要向顶点函数传递少量数据时,可以将数据直接复制到命令缓冲区中。

该示例将两个参数的数据复制到命令缓冲区中,顶点数据是从定义的数组复制而来的,视口数据是从设置视口的同一变量中复制的,片元函数仅使用从光栅化器接收的数据,因此没有传递参数。


[renderEncoder setVertexBytes:triangleVertices

                       length:sizeof(triangleVertices)

                      atIndex:AAPLVertexInputIndexVertices];



[renderEncoder setVertexBytes:&_viewportSize

                       length:sizeof(_viewportSize)

                      atIndex:AAPLVertexInputIndexViewportSize];


编码绘图命令

指定图元的种类、起始索引和顶点数。当三角形被渲染时,vertex 函数被调用,参数 vertexID 的值分别为 0、1 和 2。


// Draw the triangle.

[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle

                  vertexStart:0

                  vertexCount:3];


与使用 Metal 绘制到屏幕一样,需要结束编码过程并提交命令缓冲区。不同之处是,可以使用相同的一组步骤对更多渲染命令进行编码。按照指定的顺序来执行命令,生成最终渲染的图像。 (为了性能,GPU 可以并行处理命令甚至部分命令,只要最终结果是按顺序渲染的就行。)

颜色插值

在此示例中,颜色值是在三角形内部插值计算出来的。有时希望由一个顶点生成一个值并在整个图元中保持不变,这需要在顶点函数的输出上指定 flat 属性限定符来执行此操作。示例项目中,通过在颜色字段中添加 [[flat]] 限定符来实现此功能。


float4 color [[flat]];


渲染管线使用三角形的第一个顶点(称为激发顶点)的颜色值,并忽略其他两个顶点的颜色。还可以混合使用 flat 着色和内插值,只需在顶点函数的输出上添加或删除 flat 限定符即可。

总结

本文介绍了如何配置渲染管道,如何编写顶点和片元函数、如何创建渲染管道状态对象,以及最后对绘图命令进行编码,最终在视图中绘制一个简单的 2D 彩色三角形。

本文示例代码下载

收起阅读 »

来聊聊 关于SwiftUI State的一些细节

本文转载自:onevcat.com/2021/01/swi…,本文转载出于传递更多信息之目的,版权归原作者或者来源机构所有。@State 基础在 SwiftUI 中,我们使用 @State 进行私有状态管理,并驱动 View&nb...
继续阅读 »


本文转载自:onevcat.com/2021/01/swi…,本文转载出于传递更多信息之目的,版权归原作者或者来源机构所有。

@State 基础

在 SwiftUI 中,我们使用 @State 进行私有状态管理,并驱动 View 的显示,这是基础中的基础。比如,下面的 ContentView 将在点击加号按钮时将显示的数字 +1:

struct ContentView: View {
@State private var value = 99
var body: some View {
VStack(alignment: .leading) {
Text("Number: (value)")
Button("+") { value += 1 }
}
}
}

当我们想要将这个状态值传递给下层子 View 的时候,直接在子 View 中声明一个变量就可以了。下面的 View 在表现上来说完全一致:

struct DetailView: View {
let number: Int
var body: some View {
Text("Number: (number)")
}
}

struct ContentView: View {
@State private var value = 99
var body: some View {
VStack(alignment: .leading) {
DetailView(number: value)
Button("+") { value += 1 }
}
}
}

在 ContentView 中的 @State value 发生改变时,ContentView.body 被重新求值,DetailView 将被重新创建,包含新数字的 Text 被重新渲染。一切都很顺利。

子 View 中自己的 @State

如果我们希望的不完全是这种被动的传递,而是希望 DetailView 也拥有这个传入的状态值,并且可以自己对这个值进行管理的话,一种方法是在让 DetailView 持有自己的 @State,然后通过初始化方法把值传递进去:

struct DetailView0: View {
@State var number: Int
var body: some View {
HStack {
Text("0: (number)")
Button("+") { number += 1 }
}
}
}

// ContentView
@State private var value = 99
var body: some View {
// ...
DetailView0(number: value)
}

这种方法能够奏效,但是违背了 @State 文档中关于这个属性标签的说明:

… declare your state properties as private, to prevent clients of your view from accessing them.

如果一个 @State 无法被标记为 private 的话,一定是哪里出了问题。一种很朴素的想法是,将 @State 声明为 private,然后使用合适的 init 方法来设置它。更多的时候,我们可能需要初始化方法来解决另一个更“现实”的问题:那就是使用合适的初始化方法,来对传递进来的 value 进行一些处理。比如,如果我们想要实现一个可以对任何传进来的数据在显示前就进行 +1 处理的 View:

struct DetailView1: View {
@State private var number: Int

init(number: Int) {
self.number = number + 1
}
//
}

但这会给出一个编译错误!

Variable ‘self.number’ used before being initialized

在最新的 Xcode 中,上面的方法已经不会报错了:对于初始化方法中类型匹配的情况,Swift 编译时会将其映射到内部底层存储的值,并完成设置。 不过,对于类型不匹配的情况,这个映射依然暂时不成立。比如下面的 var number: Int? 和输入参数的 number: Int就是一个例子。因此,我决定 还是把下面的讨论再保留一段时间。

一开始你可能对这个错误一头雾水。我们会在本文后面的部分再来看这个错误的原因。现在先把它放在一边,想办法让编译通过。最简单的方式就是把 number 声明为 Int?

struct DetailView1: View {
@State private var number: Int?

init(number: Int) {
self.number = number + 1
}

var body: some View {
HStack {
Text("1: (number ?? 0)")
Button("+") { number = (number ?? 0) + 1 }
}
}
}

// ContentView
@State private var value = 99
var body: some View {
// ...
DetailView1(number: value)
}

问答时间,你觉得 DetailView1 中的 Text 显示的会是什么呢?是 0,还是 100?

如果你回答的是 100 的话,恭喜,你答错掉“坑”里了。比较“出人意料”,虽然我们在 init中设置了 self.number = 100,但在 body 被第一次求值时,number 的值是 nil,因此 0会被显示在屏幕上。

@State 内部

问题出在 @State 上:SwiftUI 通过 property wrapper 简化并模拟了普通的变量读写,但是我们必须始终牢记,@State Int 并不等同于 Int,它根本就不是一个传统意义的存储属性。这个 property wrapper 做的事情大体上说有三件:

  1. 为底层的存储变量 State<Int> 这个 struct 提供了一组 getter 和 setter,这个 State struct 中保存了 Int 的具体数字。
  2. 在 body 首次求值前,将 State<Int> 关联到当前 View 上,为它在堆中对应当前 View 分配一个存储位置。
  3. 为 @State 修饰的变量设置观察,当值改变时,触发新一次的 body 求值,并刷新屏幕。

我们可以看到的 State 的 public 的部分只有几个初始化方法和 property wrapper 的标准的 value:

struct State<Value> : DynamicProperty {
init(wrappedValue value: Value)
init(initialValue value: Value)
var wrappedValue: Value { get nonmutating set }
var projectedValue: Binding<Value> { get }
}

不过,通过打印和 dump State 的值,很容易知道它的几个私有变量。进一步地,可以大致猜测相对更完整和“私密”的 State 结构如下:

struct State<Value> : DynamicProperty {
var _value: Value
var _location: StoredLocation<Value>?

var _graph: ViewGraph?

var wrappedValue: Value {
get { _value }
set {
updateValue(newValue)
}
}

// 发生在 init 后,body 求值前。
func _linkToGraph(graph: ViewGraph) {
if _location == nil {
_location = graph.getLocation(self)
}
if _location == nil {
_location = graph.createAndStore(self)
}
_graph = graph
}

func _renderView(_ value: Value) {
if let graph = _graph {
// 有效的 State 值
_value = value
graph.triggerRender(self)
}
}
}

SwiftUI 使用 meta data 来在 View 中寻找 State 变量,并将用来渲染的 ViewGraph 注入到 State 中。当 State 发生改变时,调用这个 Graph 来刷新界面。关于 State 渲染部分的原理,超出了本文的讨论范围。有机会在后面的博客再进一步探索。

对于 @State 的声明,会在当前 View 中带来一个自动生成的私有存储属性,来存储真实的 State struct 值。比如上面的 DetailView1,由于 @State number 的存在,实际上相当于:

struct DetailView1: View {
@State private var number: Int?
private var _number: State<Int?> // 自动生成
// ...
}

这为我们解释了为什么刚才直接声明 @State var number: Int 无法编译:

struct DetailView1: View {
@State private var number: Int

init(number: Int) {
self.number = number + 1
}
//
}

Int? 的声明在初始化时会默认赋值为 nil,让 _number 完成初始化 (它的值为 State<Optional<Int>>(_value: nil, _location: nil));而非 Optional 的 number 则需要明确的初始化值,否则在调用 self.number 的时候,底层 _number 是没有完成初始化的。

于是“为什么 init 中的设置无效”的问题也迎刃而解了。对于 @State 的设置,只有在 View 被添加到 graph 中以后 (也就是首次 body 被求值前) 才有效。

当前 SwiftUI 的版本中,自动生成的存储变量使用的是在 State 变量名前加下划线的方式。这也是一个代码风格的提示:我们在自己选择变量名时,虽然部分语言使用下划线来表示类型中的私有变量,但在 SwiftUI 中,最好是避免使用 _name 这样的名字,因为它有可能会被系统生成的代码占用 (类似的情况也发生在其他一些 property wrapper 中,比如 Binding 等)。

几种可选方案

在知道了 State struct 的工作原理后,为了达到最初的“在 init 中对传入数据进行一些操作”这个目的,会有几种选择。

首先是直接操作 _number

struct DetailView2: View {
@State private var number: Int

init(number: Int) {
_number = State(wrappedValue: number + 1)
}

var body: some View {
return HStack {
Text("2: (number)")
Button("+") { number += 1 }
}
}
}

因为现在我们直接插手介入了 _number 的初始化,所以它在被添加到 View 之前,就有了正确的初始值 100。不过,因为 _number 显然并不存在于任何文档中,这么做带来的风险是这个行为今后随时可能失效。

另一种可行方案是,将 init 中获取的 number 值先暂存,然后在 @State number 可用时 (也就是在 body ) 中,再进行赋值:

struct DetailView3: View {
@State private var number: Int?
private var tempNumber: Int

init(number: Int) {
self.tempNumber = number + 1
}

var body: some View {
DispatchQueue.main.async {
if (number == nil) {
number = tempNumber
}
}
return HStack {
Text("3: (number ?? 0)")
Button("+") { number = (number ?? 0) + 1 }
}
}
}

不过,这样的做法也并不是很合理。State 文档中明确指出:

You should only access a state property from inside the view’s body, or from methods called by it.

虽然 DetailView3 可以按照预期工作,但通过 DispatchQueue.main.async 中来访问和更改 state,是不是推荐的做法,还是存疑的。另外,由于实际上 body 有可能被多次求值,所以这部分代码会多次运行,你必须考虑它在 body 被重新求值时的正确性 (比如我们需要加入 number == nil 判断,才能避免重复设值)。在造成浪费的同时,这也增加了维护的难度。

对于这种方法,一个更好的设置初值的地方是在 onAppear 中:

struct DetailView4: View {
@State private var number: Int = 0
private var tempNumber: Int

init(number: Int) {
self.tempNumber = number + 1
}

var body: some View {
HStack {
Text("4: (number)")
Button("+") { number += 1 }
}.onAppear {
number = tempNumber
}
}
}

虽然 ContentView中每次 body 被求值时,DetailView4.init 都会将 tempNumber 设置为最新的传入值,但是 DetailView4.body 中的 onAppear 只在最初出现在屏幕上时被调用一次。在拥有一定初始化逻辑的同时,避免了多次设置。

如果一定要从外部给 @State 一个初始值,这种方式是笔者比较推荐的方式:从外部在 initializer 中直接对 @State 直接进行初始化, 是反模式的做法:一方面它事实上违背了 @State 应该是纯私有状态这一假设,另一方面由于 SwiftUI 中 View 只是一个“虚拟”的结构,而非真实的渲染 对象,即使表现为同一个视图,它在别的 view 的 body 中是可能被重复多次创建的。在初始化方法中做 @State 赋值,很可能导致已经改变的现有状态 被意外覆盖,这往往不是我们想要的结果。

State, Binding, StateObject, ObservedObject

@StateObject 的情况和 @State 很类似:View 都拥有对这个状态的所有权,它们不会随着新的 View init 而重新初始化。这个行为和 Binding 以及 ObservedObject 是正好相反的:使用 Binding 和 ObservedObject 的话,意味着 View 不会负责底层的存储,开发者需要自行决定和维护“非所有”状态的声明周期。

当然,如果 DetailView 不需要自己拥有且独立管理的状态,而是想要直接使用 ContentView中的值,且将这个值的更改反馈回去的话,使用标准的 @Bining 是毫无疑问的:

struct DetailView5: View {
@Binding var number: Int
var body: some View {
HStack {
Text("5: (number)")
Button("+") { number += 1 }
}
}
}

状态重设

对于文中的情景,想要对本地的 State (或者 StateObject) 在初始化时进行操作,最合适的方式还是通过在 .onAppear 里赋值来完成。如果想要在初次设置后,再次将父 view 的值“同步”到子 view 中去,可以选择使用 id modifier 来将子 view 上的已有状态清除掉。在一些场景下,这也会非常有用:

struct ContentView: View {
@State private var value = 99

var identifier: String {
value < 105 ? "id1" : "id2"
}

var body: some View {
VStack(alignment: .leading) {
DetailView(number: value)
Button("+") { value += 1 }
Divider()
DetailView4(number: value)
.id(identifier)
}
}

被 id modifier 修饰后,每次 body 求值时,DetailView4 将会检查是否具有相同的 identifier。如果出现不一致,在 graph 中的原来的 DetailView4 将被废弃,所有状态将被清除,并被重新创建。这样一来,最新的 value 值将被重新通过初始化方法设置到 DetailView4.tempNumber。而这个新 View 的 onAppear 也会被触发,最终把处理后的输入值再次显示出来。

总结

对于 @State 来说,严格遵循文档所预想的使用方式,避免在 body 以外的地方获取和设置它的值,会避免不少麻烦。正确理解 @State 的工作方式和各个变化发生的时机,能让我们在迷茫时找到正确的分析方向,并最终对这些行为给出合理的解释和预测。

iOS相关资料下载

收起阅读 »

详细分析iOS启动页广告

iOS
最近公司有个需求,需要添加启动页广告,查了不少资料,基本上有2种说法。一种是实时展示广告,另外一种是先保存,下次再展示本地的。对于这两种说法,仔细了研究下,有可取之处,也有一些小缺点。下面就和大家慢慢探讨下。1.先下载后展示方案先说下我采用的方案,APP首次启...
继续阅读 »


最近公司有个需求,需要添加启动页广告,查了不少资料,基本上有2种说法。一种是实时展示广告,另外一种是先保存,下次再展示本地的。对于这两种说法,仔细了研究下,有可取之处,也有一些小缺点。下面就和大家慢慢探讨下。

1.先下载后展示方案

先说下我采用的方案,APP首次启动,加载引导页,然后进入首页,这第一次不展示启动页广告。可以选择在didFinishLaunchingWithOptions里面,先网络请求广告,判断本地是有已经存储了相同的广告信息,如果是,则不用理会。不是,则存储到本地上。

等下次进来,可以判断是否有本地存储的广告信息,有则直接展示,没有就直接进入首页。

优点:启动流程流畅,无影响,不会影响用户启动体验。

缺点:广告不是实时的。例如本地广告已经下架了,这时候启动还加载本地的是不是就出问题了。对于这点,我觉得还是要看公司实际运营情况来确定,如果有后台返回的有效期,就能避免这种情况。

想了下,无伤大雅,影响也不是很大。采用这种方式感觉也不错。

2.实时展示方案

这个方案,有研究过,也是一种不错的做法。APP启动,直接网络请求广告,我们直接跳到广告页,这里也分成2种情况。

一种情况,先加载本地固定的广告,1S内有广告数据返回,倒计时开启,直接展示广告,没有广告,或者网络请求失败,直接结束倒计时,进入首页。

另外一种情况,和一开始说的先下载后展示的有点雷同,这时候就是先加载本地下载的广告,1S内有广告数据返回,倒计时开启,直接展示广告,并把广告下载到本地。如果网络请求失败,就倒计时本地下载的广告,如果没有广告,也是直接结束倒计时,进入首页。

优点:实时更新启动广告,保证每次都是最新的。

缺点:广告可以会延迟展示,用户体验可能会差点。

还是想了下,其实感觉都行,毕竟要看注重点在哪里。用户体验嘛,对于我来说,肯定是能不展示倒计时是更好的,直接进入首页。但既然有启动页这广告东西,我觉得展示也行,不要太频繁就好,不要弄得每次打开都有。这只是我的一个小小期望而已。

3.多Windows实现

对于实现这个启动广告功能,又有两种做法,其中一种是利用多windows来实现。

我们在didFinishLaunchingWithOptions里面,先添加2个window。

      // 多window实现,相当于又2个window,1个在下面,1个在上面
self.window = UIWindow(frame: UIScreen.main.bounds)
self.window?.tintColor = .darkGray;
let nav1 = UINavigationController(rootViewController: ViewController())
window?.rootViewController = nav1
window?.makeKeyAndVisible()

// self.splashWindow = UIWindow(frame: CGRect(x: 0, y: 100, width: 300, height: 500))
self.splashWindow = UIWindow(frame: UIScreen.main.bounds)
let splashVC = SplashViewViewController()
let nav = UINavigationController(rootViewController: splashVC)
splashWindow?.rootViewController = nav
splashWindow?.makeKeyAndVisible()

splashWindow是展示广告的,window是展示首页的,window在splashwindow的下面,所以我们先看到的上面是展示广告的,这种做法的好处是在倒计时广告的时候,首页其实已经在请求加载页面了,等倒计时结束,这时候首页也已经加载好了。

4.单window实现

单window的话,无非就是看rootViewController是哪个页面,我们直接由广告页,变成首页就好。这里无非要注意的就是过渡的动画。这里看自己想怎样的效果了。

这种单window用法,我们常见的有登录页,首页互相切换,还有引导页和首页切换等等,实现起来倒是不难。也打算细说了。

5.效果图

按照国际惯例,提供一下GitHubDemo:github.com/wenweijia/S…

6.总结

对于方案提出了2个,都是文字类的,听起来的确是文绉绉的,本来想弄个流程图的,有点懒,也比较忙,后面有时间再补吧

主要是抛砖引玉,还是想看下各位大佬的看法,例如有没有更好的方案,哪些方案需要完善一下,欢迎留意,谢谢!

收起阅读 »

Swift系列 -- 可选类型

「这是我参与11月更文挑战的第1天,活动详情查看:2021最后一次更文挑战」前言好记性不如烂笔头,学习过后还是要总结输出才能更有利于对知识的消化吸收。因此对于Swift的学习作了一个系列总结:Swift中的函数盘点本篇作为Swift学习总结的第二篇文章,主要探...
继续阅读 »


前言

好记性不如烂笔头,学习过后还是要总结输出才能更有利于对知识的消化吸收。因此对于Swift的学习作了一个系列总结:

本篇作为Swift学习总结的第二篇文章,主要探索的是关于Swift可选项的内容。

还记得在使用OC开发时,对于一个对象而没有初始化时,其默认值为nil,并且可以在后续的操作中将该对象重新赋值为nil。因此我们经常会遇到因为对象为nil造成的程序错误,例如向数组中插入了nil造成闪退。

但是Swift是一门类型安全语言,Swift的类型是不允许变量值为nil的,而不论是引用类型,还是值类型。但是,在实际的开发中,确实存在变量可能为nil的情况,因此Swfit中还提供了一种特殊的类型 -- 可选类型,用于处理这种情况,下面就一起探索下可选项的相关内容。

一、可选类型的本质

Swift的类型安全是指,定义一个变量时,给定了类型,那么就不能再将其它类型的值赋给该变量。当然,这也不是说Swift中定义变量时,必须显式的指定变量类型。Swift还有类型推断,即根据给定的值,自动确定变量类型。如下代码所示:

let a:Int
a = "123" => Cannot assign value of type 'String' to type 'Int'

var b = 20 // 赋值为20,类型推断为Int
b = "swift" => Cannot assign value of type 'String' to type 'Int'

var c:Int = nil => 'nil' cannot initialize specified type 'Int'
var str: String = nil => 'nil' cannot initialize specified type 'String'


在上述事例代码中,a显式指定了类型为Intb通过类型推断也被指定为Int,将String赋值给这两个变量时都报错Cannot assign value of type 'String' to type 'Int' 。

对于cstr两个变量,将其初始值赋值为nil,结果会报错nil无法为指定类型赋初始化值。需要注意的一点是,变量c虽然为Int类型,但是其初始值并不为0,即 var c:Int 和 var c:Int = 0 并不等价

不过在实际的开发过程中,我们经常会遇到无法确定一个变量是否有值的情况,比如在一个相机App中,当我们获取当前摄像头时,不能确定摄像头是否正在被其它App使用,或者摄像头硬件本身有什么问题,因此无法确定是否可以获取成功,那么此时我们就可能得到一个nil值。此时,就需要有一种类型可以接收nil,又可以接收正常的值。在Swift中,用以实现这种类型的就是可选类型

Swift的可选类型的定义方式为类型+?,具体代码如下:Xnip2021-11-17_10-57-25.png可选类型的变量可以给定一个对应类型的初始值,若不给定,则其默认值为nil

当可选类型有具体的值时,与其对应的类型也是有区别的,不能做等价处理,以Int为例:Xnip2021-11-17_14-14-51.pnga为可选类型的Int,b为普通的Int类型,虽然值都为20,但是a的类型打印出来是Optional(20)。在LLDB中po一下查看ab,结果如下:Xnip2021-11-17_14-32-30.png可以发现,b是一个纯粹的Int值20,而a是在20外面包了一层,这一点类似于一个盒子,如下图所示:Xnip2021-11-17_14-51-56.png

图中红色部分表示存储的值,如果可选类型中存储有值,则盒子中存储具体的值,本例中为Int值2,如果可选类型为nil,则盒子为空。

那么如果是多重可选项呢?即可选项能否包裹一层可选项呢?代码如下:

let result:Int?? = 20

通过LLDB调试,可以看到其实际结构如下所示:

Xnip2021-11-18_17-55-34.png

画图可表示为:

Xnip2021-11-18_17-52-31.png

通过LLDB打印出来可以看到可选类型是由一个Optional包裹的类型,那么Optional是什么呢?其实Optional是一个枚举类型,可以发现其定义如下所示:Xnip2021-11-18_23-34-47.png因此如下图所示的代码是等价的:Xnip2021-11-18_23-40-20.pngOptional通过泛型来指定其要包装的类型,并且Optional遵守了ExpressibleByNilLiteral协议,遵守该协议的枚举、结构体或类初始化时允许值为nil。

Optional枚举内部包含nonesome两个case,如果值为nil,则属于none,有值的话则包装为some,由此也可看出Swift枚举的强大。

二、强制解包

既然可选类型是将对应类型的值包在一个盒子中,那么是否可以将可选类型的值赋值给对应类型的变量呢?可以简单做个测试,结果如下:Xnip2021-11-17_17-02-06.png答案显然是否定的,编译器在编译时就会报错Value of optional type 'Int?' must be unwrapped to a value of type 'Int',Int?必须解包成一个 Int类型。

Swift中可选类型的强制解包使用一个!即可,代码如下所示:

let a:Int? = 20
var b:Int = 20
b = a!

代码第三行 b = a!中,可选类型Int a即解包为了Int,并赋值给b。当然a依然是一个可选类型,其值依然为20。

强制解包需要注意以下几点:

  • 1、强制解包后,对于原可选变量的值没有影响,其依然为可选类型
  • 2、值为nil的可选类型,强制解包会发生闪退,因此在使用强制解包时,需要确定可选类型中的值不为nil

与强制解包一起的还有一种类型,隐式解包的可选项,代码表现为类型 + !。例子如下:

let result:Int! = 20
let realInt:Int = result

可以发现result可以直接赋值给一个Int的realInt,因为隐式解包的可选项会隐式的将变量解包,而不会有明显的感知。不过需要明确的一点是,隐式解包依然是可选项,如果不是确定变量会一直有值,使用需要谨慎。

三、可选项绑定

使用可选项强制解包时,为例防止值为nil的闪退,我们可能会像下面这样写代码:

   let a:Int? = 20
   var b:Int

if a != nil {
b = a!
}

相对于直接解包,这样写安全性确实提高了一些,不过Swift提供了一种更加优雅的方式来解决这一问题,即可选项绑定,代码如下:

let a:Int? = 20
var b:Int
if let value = a {
b = value
} else {
print("a的值为nil")
}

如同代码中if后面的条件所示,可选项绑定的语法是let value = 可选项变量。可选项绑定使用在条件判断等地方,如果可选项变量为nil,则条件为false,如果可选项变量不为nil,则会自动解包并赋值给value,只是value的作用域仅限if条件后的{},不能用在else后的{}。

如果有多个可选项绑定,中间需要用,隔开,而不能使用&&,如图所示:

Xnip2021-11-17_18-49-11.png

Xnip2021-11-17_18-51-01.png

四、空合并运算符

Swift中还提供了空运算符 ??,其定义为如下代码

public func ?? <T>(optional: T?, defaultValue: @autoclosure () throws -> T?) rethrows -> T?

public func ?? <T>(optional: T?, defaultValue: @autoclosure () throws -> T) rethrows -> T

空合并运算符是一个二元运算符,假定有两个变量 a 和 b,使用空合并运算符方式为 a ?? b,使用时有以下注意事项:

  • a??b,如果a为nil,则返回b,否则返回a自身
  • a需要为可选项,否则虽然编译器不会报错,但是没有意义
  • b可以是可选项,也可以不是可选项
  • 不管a、b是否都是可选项,两者存储的类型要对应,例如Int?<=> Int 或 Int?<=> Int?
  • 如果b不是可选项,a的值不为nil,则在返回 a 时,会自动解包,事实上b决定了返回值是否解包

4.1 空合并运算符使用举例

以下为空合并运算符的几个例子,假定a、b存储皆为Int值:

  • a为nil,b为Int值2
let a:Int? = nil

let b:Int = 2

let result = a ?? b // result为b的值,且为Int类型

  • a为nil,b为可选类型Int值2
let a:Int? = nil

let b:Int? = 2

let result = a ?? b // result为b的值 Optional(2)

  • a不为nil,b为Int类型
let a:Int? = 3

let b:Int = 2

let result = a ?? b // result为a的值,并且已经解包为3

  • a不为nil,b为可选Int类型
let a:Int? = 3

let b:Int? = 2

let result = a ?? b // result为a的值,并且依然为Optional(3)

还有多个空合并运算符连接使用的情况,如下代码:

let a:Int? = 2
let b:Int? = nil
let c:Int = 4

let result = a ?? b ?? c // result值为2,是 a 解包后的值

如例所示,当多个??连接使用时,决定result值的依然是最后一个变量c,前面 a??b 得到了可选Int?值2,因为c为int类型,所以得到解包后的Int值2

4.2 空合并运算符与可选项绑定

??还可以与可选项绑定结合在一起使用,如下代码所示:

  • 类似于 a != nil || b != nil
let a:Int? = 2
let b:Int? = nil

if let result = a ?? b { // 只要a和b中有一个不为空,就可以进入该条件判断
let c = result
    print(c)
}

  • 类似于 a != nil && b != nil
let a:Int? = 2
let b:Int? = nil

if let c = a, let d = b { // 只有a和b都不为nil时,才会进入条件判断
    print(c)
    print(d)
}

通过上述两种方式,可以更加精简的进行多个可选项的nil值判断,并且在条件为真的情况下可以自动解包,直接使用解包后的值。

五、guard语句

guard语句与if语句类似,都是条件判断语句,其语法规则为:

guard 条件 else {
// 执行代码
}

不过与if语句不同的是,guard语句是条件为false时,进入{}执行代码。如下面的例子所示:

let a:Int? = 20

guard a != nil else {
print("a的值为nil")
return
}
print("a的值为\(a!)")

并且guard语句的代码块中,必须有return或者抛出异常,否则编译器会报错如下:Xnip2021-11-18_15-51-34.png

在最初接触到guard语句时,可能想已经有了if语句,为什么还需要guard呢?并且guard实现的功能,if也可以实现,会觉得其有些多余。但是经过一段时间开发,对两者做出对比后,可以发现在一定程度上,guard表达的语义更加明确,代码的可读性更高。例如,上面的代码改成if语句如下:

if a == nil {
print("a的值为nil")
return
}

print("a的值为\(a!)")

对比两段代码,语义上我们符合我们预期的值 a != nil,如果使用if语句是要判断a==nil,使用guard就判断a != nil,不符合就return即可,因此guard更加适合做容错判断。

六、总结

  • Swift可选类型的本质是Optional枚举,其包含nonesome两个case,none表示当前变量值为nil,some表示当前变量值不为nil
  • Swift可选项不能直接赋值给其包装的类型所对应的变量,应该在解包后赋值,但是需要注意的是,要在保证有值的情况下强制解包,否则会Crash
  • 对于可选类型的判空处理,可以使用可选项绑定来做,这样更加优雅

与OC不同,Swift更加注重安全性,尤其是对于nil的处理上,Swift更加的严谨,虽然在刚接触时会有些不适应,但是在开发过程中却可以省去很多对nil的容错处理,由此也可以看出Swift的强大与设计精妙。以上即为对于Swift中的可选类型的总结,欢迎大家指正。

收起阅读 »

面试官:请你实现一下JS重载?可不是TS重载哦!

一位同学:“如何实现JS重载?”我:“JS有重载吗?不是TS才有吗?”一位同学:“有的,这是网易一道面试题”我:“好吧我想想哈!”什么是重载我第一次看到重载这个词还是在以前学习Java的时候,我一直觉得JavaScript是没有重载的,直到TypeScript...
继续阅读 »
  • 一位同学:“如何实现JS重载?”
  • 我:“JS有重载吗?不是TS才有吗?”
  • 一位同学:“有的,这是网易一道面试题”
  • 我:“好吧我想想哈!”

image.png

什么是重载

我第一次看到重载这个词还是在以前学习Java的时候,我一直觉得JavaScript是没有重载的,直到TypeScript的出现,所以我一直觉得JavaScript没有重载,TypeScript才有,但是现在看来我是错的。

我理解的重载是:同样的函数,不同样的参数个数,执行不同的代码,比如:

/*
* 重载
*/
function fn(name) {
console.log(`我是${name}`)
}

function fn(name, age) {
console.log(`我是${name},今年${age}岁`)
}

function fn(name, age, sport) {
console.log(`我是${name},今年${age}岁,喜欢运动是${sport}`)
}

/*
* 理想结果
*/
fn('林三心') // 我是林三心
fn('林三心', 18) // 我是林三心,今年18岁
fn('林三心', 18, '打篮球') // 我是林三心,今年18岁,喜欢运动是打篮球

但是直接在JavaScript中这么写,肯定是不行的,咱们来看看上面代码的实际执行结果,可以看到,最后一个fn的定义,把前面两个都给覆盖了,所以没有实现重载的效果

我是林三心,今年undefined岁,喜欢运动是undefined
我是林三心,今年18岁,喜欢运动是undefined
我是林三心,今年18岁,喜欢运动是打篮球

我的做法

其实,想要实现理想的重载效果,我还是有办法的,我可以只写一个fn函数,并在这个函数中判断arguments类数组的长度,执行不同的代码,就可以完成重载的效果

function fn() {
switch (arguments.length) {
case 1:
var [name] = arguments
console.log(`我是${name}`)
break;
case 2:
var [name, age] = arguments
console.log(`我是${name},今年${age}岁`)
break;
case 3:
var [name, age, sport] = arguments
console.log(`我是${name},今年${age}岁,喜欢运动是${sport}`)
break;
}
}

/*
* 实现效果
*/
fn('林三心') // 我是林三心
fn('林三心', 18) // 我是林三心,今年18岁
fn('林三心', 18, '打篮球') // 我是林三心,今年18岁,喜欢运动是打篮球

但是那位同学说,网易的面试官好像觉得这么实现可以是可以,但是还有没有更好的实现方法,我就懵逼了。

高端做法

image.png

经过了我的一通网上查找资料,发现了一种比较高端的做法,可以利用闭包来实现重载的效果。这个方法在JQuery之父John Resig写的《secrets of the JavaScript ninja》中,这种方法充分的利用了闭包的特性!

function addMethod(object, name, fn) {
var old = object[name]; //把前一次添加的方法存在一个临时变量old里面
object[name] = function () { // 重写了object[name]的方法
// 如果调用object[name]方法时,传入的参数个数跟预期的一致,则直接调用
if (fn.length === arguments.length) {
return fn.apply(this, arguments);
// 否则,判断old是否是函数,如果是,就调用old
} else if (typeof old === "function") {
return old.apply(this, arguments);
}
}
}

addMethod(window, 'fn', (name) => console.log(`我是${name}`))
addMethod(window, 'fn', (name, age) => console.log(`我是${name},今年${age}岁`))
addMethod(window, 'fn', (name, age, sport) => console.log(`我是${name},今年${age}岁,喜欢运动是${sport}`))

/*
* 实现效果
*/

window.fn('林三心') // 我是林三心
window.fn('林三心', 18) // 我是林三心,今年18岁
window.fn('林三心', 18, '打篮球') // 我是林三心,今年18岁,喜欢运动是打篮球

结语

如果你觉得此文对你有一丁点帮助,点个赞,鼓励一下林三心哈哈。或者可以加入我的摸鱼群 想进学习群,摸鱼群,请点击这里摸鱼,我会定时直播模拟面试,简历指导,答疑解惑

image.png


作者:Sunshine_Lin
链接:https://juejin.cn/post/7031525301414805518

收起阅读 »

建议收藏!!VueRouter原理和ReactRouter原理

简述 其实Vue和React在很多地方,底层原理和语法上差别并不是很大。底层原理更多的是相同的。就比如说React有JSX,Vue有Template。其实就可以理解成一个东西,就是写法不同。文章 【今天学习了吗?hash 路由和 history 路由简介】简单...
继续阅读 »

简述


其实Vue和React在很多地方,底层原理和语法上差别并不是很大。底层原理更多的是相同的。就比如说React有JSX,Vue有Template。其实就可以理解成一个东西,就是写法不同。文章 【今天学习了吗?hash 路由和 history 路由简介】简单介绍了vue的两种路由模式,但是其背后的原理是什么呢?这里和React路由一起介绍一下!希望对读者有所帮助 ~~~


更新视图但不重新请求页面,是前端路由原理的核心之一!!


Hash模式

hash 虽然出现在 url 中,但不会被包括在 http 请求中,它是用来指导浏览器动作的,对服务器端完全无用,因此,改变 hash 不会重新加载页面。

可以为 hash 的改变添加监听事件:window.addEventListener('hashchange',callBack)

每一次改变 hash(window.localtion.hash),都会在浏览器访问历史中增加一个记录。利用 hash 的以上特点,就可以来实现前端路由"更新视图但不重新请求页面"的功能了。



我们就可以通过 hashchange 去处理一些特殊的操作,执行一些情况下才会执行的代码。而 Vue / React 应用的正是这一原理。通过不同的 路由去调用不同的 函数/JS 去生成不同的页面代码。



举个栗子:


// 这是一个hash模式的网址例子
http://www.xxx.com/#/abcd123

function callBack(e) {
// 通过event对象去获取当前的路由,来判断下一步要进行的一些操作,当然这里不止包含Dom,
// 其他的操作也是可以的
console.log(e.oldURL)
console.log(e.newURL)
}
window.addEventListener('hashchange',callBack)


目前hash模式支持最低版本是IE8,这也就是为什么都说hash模式的兼容性更好了。其实 React 和 Vue 的hash模式的路由底层就是这么简单的。



History模式


History模式,即http://www.xxxx.com/abc/dcd


这种模式会造成浏览器重新请求服务器路由,首先去获取服务器相应的path下的文件。若没有则会造成 404 Not Found! 当然这种形式需要服务端进行配合,将路由重新重定向到我们的打包出来的index.html文件上。


History模式其实就是ES6中的新增BOM对象History。Vue 和 React 设计的也很巧妙,完美的使用了ES6的新增属性。ES6新增的BOM对象History如下:


20316322-a9b1585b2694c9b6.webp


proto里面包含了replaceState 和 pushState方法。replaceState 和 pushState 其实就是vue中的 replace 和 push ,不过就是Vue的作者将其再进行了封装了。


History 存储历史记录是 队列存储 的,也可以理解为一个数组。它也是有 length 属性的。
我们平时操作 go(num) 其实调用的就是这个History队列里面的历史数据,并找到相应的索引进行一个跳转。


因为IE9才支持ES6,所以History模式并不支持IE9以下版本。所以说Hash模式的兼容更好。


以上就是 Vue 和 React 两种路由的底层原理了。


作者:不是Null
链接:https://juejin.cn/post/7031820537676611614
收起阅读 »

iOS App上架技能:不更新版本的情况下删除App Store非主语言的方法、app上架后的事项(ASO及ASA)

iOS App上架技能:不更新版本的情况下删除App Store非主语言的方法、app上架后的事项(ASO及ASA)这是我参与11月更文挑战的第17天,活动详情查看:2021最后一次更文挑战。前言iOS上架前的准备:kunnan.blog.csdn.net/a...
继续阅读 »

iOS App上架技能:不更新版本的情况下删除App Store非主语言的方法、app上架后的事项(ASO及ASA)

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

前言

  • iOS上架前的准备:kunnan.blog.csdn.net/article/det…
  • 上架技巧(不更新版本的情况下删除App Store非主语言的方法)
  • 常见上架问题及解决方案(上传ipa包被吃掉、已上架app在AppStore搜不到)
  • app上架后的事项(ASO、ASA)

I、AppStore 上架技巧

1.1 上传构建版本

archive之后通过 Xcode、macOS 版 Transporter 或 altool 上传构建版本

help.apple.com/app-store-c…

  • Xcode 上传 在这里插入图片描述
  • Transporter 在这里插入图片描述
  • 通过 altool 上传您 App 的二进制文件

您可以使用 xcrun(包含在 Xcode 中)来调用 altool,该命令行工具用于公证、验证并上传您 App 的二进制文件至 App Store。在“终端”的命令行中指定以下命令之一:

$ xcrun altool --validate-app -f file -t platform -u username [-p password] [--output-format xml]
$ xcrun altool --upload-app -f file -t platform -u username [-p password] [—output-format xml]

【注】如果您使用自动构建系统,则可以将公证过程集成到现有构建脚本中。Xcode 中的 altool 和 stapler 命令行工具可将您的软件上传至 Apple 公证服务,并将生成的凭证附加到您的可执行文件中。altool 位于:/Applications/Xcode.app/Contents/Developer/usr/bin/altool。

有关更多信息,请参见《altool 指南》

help.apple.com/asc/appsalt…

1.2 不更新版本的情况下删除App Store非主语言的方法

1、由于AppStore缓存原因导致已上架app在AppStore上搜不到的解决方案2、不更新版本的情况下删除App Store非主语言的方法(应用场景:马甲包)

blog.csdn.net/z929118967/…

1.3 对开发权限和上架权限进行分离管理

在大公司通常苹果开发账号归数据中心人管,如果没有专门测试的开发者账号,只能在公司开发者下面添加一个新用户用于测试开发;选择对应职能即可。

在这里插入图片描述 通过添加开发职能账号,方便其他开发者知道app的审核状态。 当然你也可以采用邮件转发来同步信息(当发件人是>no_reply@email.apple.com时,就转发给特定人员 ) 在这里插入图片描述

具体流程举例

苹果版本升级先发邮件给市场管理部邮箱scglb@xxx.com,由对应人员走oa申请流程,审批完成后开发同事邮件发送审批截图+具体事宜给总部研发对应同事,然后总部这边就操作后面的上架流程(打包+上架)。

II、常见上架问题及解决方案

2.1 iOS app因蓝牙功能隐蔽而导致上架被拒绝的解决方案

相关的公众号文章:https://mp.weixin.qq.com/s?__biz=MzI0MjU5MzU5Ng==&mid=2247484133&idx=1&sn=1d50f59ea026c1b4a9d540c9c1222695&chksm=e978b8b6de0f31a0bbcff38495e858d4db16a854828c1c80719df820826d8405f93b3662ef29&mpshare=1&scene=1&srcid=0114rQ5AKSFyy8QxZoG4Jrmf&sharer_sharetime=1610606706852&sharer_shareid=38c24777c9b84b8b44c56026b3aa9bd7&version=3.0.36.2330&platform=mac#rd

2.2 info.plist 的权限配置问题导致的app被吃掉了

如果上传ipa包之后,app被吃掉了,大部分是权限问题。

 <key>NSAppleMusicUsageDescription</key>
 <string>App需要您的同意,才能访问媒体资料库</string>
 <key>NSBluetoothPeripheralUsageDescription</key>
 <string>App需要您的同意,才能访问蓝牙</string>
 <key>NSCalendarsUsageDescription</key>
 <string>App需要您的同意,才能访问日历</string>
 <key>NSCameraUsageDescription</key>
 <string>App需要您的同意,才能访问相机</string>
 <key>NSLocationAlwaysUsageDescription</key>
 <string>App需要您的同意,才能始终访问位置</string>
 <key>NSLocationUsageDescription</key>
 <string>App需要您的同意,才能访问位置</string>
 <key>NSLocationWhenInUseUsageDescription</key>
 <string>App需要您的同意,才能在使用期间访问位置</string>
 <key>NSMicrophoneUsageDescription</key>
 <string>App需要您的同意,才能访问麦克风</string>
 <key>NSPhotoLibraryAddUsageDescription</key>
 <string>To save the conversion results to the phone, you need to open the album permissions.</string>
 <key>NSPhotoLibraryUsageDescription</key>
 <string>To save the conversion results to the phone, you need to open the album permissions.</string>
 <key>NSRemindersUsageDescription</key>
 <string>App需要您的同意,才能访问提醒事项</string>

  • other
 <key>NSAppleMusicUsageDescription</key>
 <string></string>
 <key>NSCalendarsUsageDescription</key>
 <string></string>
 <key>NSCameraUsageDescription</key>
 <string>是否允许此App使用你的相机?</string>
 <key>NSContactsUsageDescription</key>
 <string>是否允许此App访问你的通讯录?</string>
 <key>NSLocationWhenInUseUsageDescription</key>
 <string></string>
 <key>NSMicrophoneUsageDescription</key>
 <string>是否允许此App使用你的麦克风?</string>
 <key>NSPhotoLibraryUsageDescription</key>
 <string>是否允许此App访问你的媒体资料库?</string>
 <key>NSRemindersUsageDescription</key>
 <string></string>

III 、app上架之后的事项

3.1 ASO

blog.csdn.net/z929118967/…

3.2 管理符号表

  • 上传app上线版本的dSYMs文件到bugly,用于后续的app日志文件符号化

3.3 管理代码分支

blog.csdn.net/z929118967/…

3.4 申请iOS App上线爱思助手应用市场

iOS App如何在爱思助手应用市场上架?

blog.csdn.net/z929118967/…

3.5 Apple search ads(ASA)

searchads.apple.com/cn/

时隔五年,ASA(Apple Search Ads,即苹果搜索广告)终于上线中国大陆地区的App Store。 在这里插入图片描述

使用 Apple Search Ads Advanced,你可以在两个位置展示你的 app:

1、一个是“搜索”标签广告,在用户搜索前展示; 2、另一个是搜索结果顶部广告,在用户搜索时展示。

ITC后台和苹果广告这两者是两个不同的体系,两个账号是不同的,单独的一个苹果广告账号可以给多个App进行投放

如果公司下有多个开发者账号,可将这些账号的包授权给同一个投放账号,这样这个投放账号就可以投放不同主体的App。

Q1.目前ASA账户充值是预充值还是后付呢?

现在是要预充值的,因为苹果可能会随时根据你的消耗情况进行扣款。扣款条件主要是分两种情况,分别是满500美金或者7天扣一次,当这两个条件哪个先触达了就按哪个来。

Q2.公司注册的个人小号没有营业执照,这个号下面的App应该怎么推广?

按目前苹果在国内市场的政策来看,要使用苹果广告都需要营业执照,所以这样的小号大概率是没办法推广的。

see also

(高校学生于教育商店选购新款 iPad /Mac 可享受优惠)【修订版】

mp.weixin.qq.com/s/rkRMVUoYK…

更多内容请关注#小程序:iOS逆向,只为你呈现有价值的信息,专注于移动端技术研究领域;更多服务和咨询请关注#公众号:iOS逆向

收起阅读 »

(转载)元宇宙想要玩起来,需要哪些技术支撑?

我以前是做云计算的,现在是做云原生的,要是问我以后是做什么的,我会告诉你——元宇宙。 有人会说,你这个连游戏都不打的人有啥资格谈元宇宙?我会说,你out了,如果现在还只把元宇宙当成个跟游戏沾边的概念,那是你该提高了。 虽然现在元宇宙很火,但我来谈元宇宙绝对不是...
继续阅读 »

我以前是做云计算的,现在是做云原生的,要是问我以后是做什么的,我会告诉你——元宇宙


有人会说,你这个连游戏都不打的人有啥资格谈元宇宙?我会说,你out了,如果现在还只把元宇宙当成个跟游戏沾边的概念,那是你该提高了。


虽然现在元宇宙很火,但我来谈元宇宙绝对不是跟着小札后面炒冷饭,小札把FB改名这事之前没和我商量,算英雄所见略同吧。注意Metaverse 的Meta的英语发音不是“妈他”,而是“麦嗒”,清音“嗒”。


云计算解决了计算资源按需分别的问题,云原生解决了业务应用智能交付的问题,而元宇宙是大数据、人工智能、虚拟现实等热点技术集成后再应用回现实生活的终极形态


电影工作者超级想象力建立了大众对元宇宙的初级认识。几年前,VR眼镜刚火时被资本炒上了天,有个投资界的朋友跟我说,现在还早了点,要是VR能和生产生活场景联动起来就更有价值了。


我非常同意他的观点,VR只是解决了生态链上的一个展现环节,其他配套场景设备没有完善之前只能用来“自嗨”。在这件事上,电影就做得比较好,例如《头号玩家》中全套穿戴设备就考虑得比较周到。


当然元宇宙想要玩起来还要包含很多其他技术要素支撑,除了云计算、大数据和虚拟现实以外,第一位要解决的是万物互联,这里说的不是物联网那种设备接入,而是“******”,这里买个关子,后面是巨大的商机。第二个要解决的是数字资产,数字资产是构建元宇宙的“元”,这里最难的是制定“标准”。有了数字资产以后就可以结合数字孪生技术做初步的场景呈现,数字孪生是工业元宇宙的第一阶段,也是最容易实现元宇宙现实落地的场景,数字化工厂、数字化制造、智能智造都是这个话题的衍生话题,先不展开讨论。


近日,看到著名“十一维”主人在他的文章中对元宇宙提出了一个观点我较为赞同,元宇宙必须要解决如何生存的问题,说白就是如何挣钱养活自己,投资可以解决启动的问题,成长和不断迭代就要靠生态系统的良性运转才能实现。


转自:达叔怎么看


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

Swift组件化如何解耦

组件化如何解耦把同一模块的代码放到一起代码是两个模块的代码,不能放在同一模块的怎么办。问题1很简单,就是从代码层面做好按模块分开。 如A模块的代码全部放到A模块里面,然后要对外的时候,A模块放出对外的接口给其他模块调用。 比如日志模块,他能够独立成一个模块,他...
继续阅读 »

组件化如何解耦

  1. 把同一模块的代码放到一起

  2. 代码是两个模块的代码,不能放在同一模块的怎么办。

问题1很简单,就是从代码层面做好按模块分开。 如A模块的代码全部放到A模块里面,然后要对外的时候,A模块放出对外的接口给其他模块调用。 比如日志模块,他能够独立成一个模块,他不依赖别的模块,所以只需要把负责写日志等的代码放到一个日志模块里面,这样别人想要输出日志。就可以引入日志模块并用日志模块的接口输出日志就行。这里面没有耦合,也就不需要解耦。

问题2,比如A模块会用到B模块的方法,然后B模块又有可能用到A模块的代码,但是又不能把A和B合并为一个模块的时候怎么办。总不能A模块编译都编译不过,因为编译的时候会提示缺少B模块的方法。同样B模块也一样,缺少A模块,他编译都编译不了。

这里就需要对A模块做B模块的解耦,同样B模块也一样要做A模块的解耦。

关于解耦的方法,网上也有挺多。比较有代表性的两种如下:

  1. CTMediator的target-action模式

  2. BeeHive的用protocol实现模块间调用

这里参考BeeHive的思想,用swift实现了一个解耦的例子。

定义

  1. A模块
  2. B模块
  3. Interface模块(保存各个模块的对外接口,比如A或者B模块的对外接口,都放在这里)
  4. 主工程

先看最后的依赖图

image.png

A模块要调用B模块的接口,这里他不需要依赖B模块,他只需要依赖Interface模块,然后A模块要调用B模块的功能,他只需要调用B模块放到Interface模块的接口就行。

如何实现:

Interface模块里面有一个公共类,比如叫:ModuleInterface 然后他里面有两个方法, 一个注册函数是让别的模块把自己实例注册到这里来的 一个是获取函数,通过某个key,获取到对应的模块的实例

public class ModuleInterface {
public static let shared = ModuleInterface()
public var protocols: [String: BaseProtocol] = [:] // 维护一个字典
// 注册函数
public func registProtocol(by name: String, instance: BaseProtocol) {
self.protocols[name] = instance
}
// 获取实例
public func getProtocol(by name: String) -> BaseProtocol? {
return self.protocols[name]
}
}

这里的核心是维护一个字典,通过对应的key,找到对应的实例。

然后B模块的公开接口也放在这个Interface模块里面,如:

extension ModuleInterface: BProtocol {
// b对外的接口
public func getBModuleValue(b: String, callback: ((Int)->Void)) -> Int {
if let pro = self.getProtocol(by: "BProtocol") as? BProtocol {
return pro.getBModuleValue(b: b, callback: callback)
} else {
print("no found BProtocol instance")
callback(0)
return 0
}
}
}

这里有两个技巧

  1. 使用extension ModuleInterface: BProtocol, 这样可以把BProtocol里面定义的方法,实现到ModuleInterface类里面,这样别的模块调用的时候,统一用ModuleInterface来调用就行,入口简单
  2. 使用self.getProtocol(by: "BProtocol") as? BProtocol,通过转类型的方式得到BProtocol的实例,就可以调用B模块的方法了。而且是运行时检查,这样也解决了,没有引用B模块也能编译通过。

如上:这样每个模块只需要在初始化的时候把自己的实例添加到这个字典里面去。然后想调用其他模块的时候,只需要从这个字典拿出对应模块的实例,再去调用别的模块就行。

然后A模块要想使用B模块的getBModuleValue的方法时,他只需要引入Interface模块,然后从ModuleInterface里面去调用如下:

let a = ModuleInterface.shared.getBModuleValue(b: "a call b") { value in
print("==callBModule=result==", value)
}

整个代码实现非常简单。

具体pod的代码如下:

A模块的podspec的定义
s.dependency 'Interface'

A模块的podfile的定义
use_frameworks!

platform :ios, '9.0'

target 'AModule_Example' do
pod 'AModule', :path => '../'
pod 'Interface', :path => '../../Interface'
end

B模块的podspec的定义
s.dependency 'Interface'
B模块的podfile的定义
use_frameworks!

platform :ios, '9.0'

target 'BModule_Example' do
pod 'BModule', :path => '../'
pod 'Interface', :path => '../../Interface'
end

主工程Demo的podfile的定义
use_frameworks!

platform :ios, '9.0'

target 'Demo' do
pod 'AModule', :path => '../AModule'
pod 'BModule', :path => '../BModule'
pod 'Interface', :path => '../Interface'
end

具体代码看例子: github.com/yxh265/Modu…

收起阅读 »

拒绝编译等待 - 动态研发模式 ARK

iOS
拒绝编译等待 - 动态研发模式 ARK作者:字节跳动终端技术——徐纪光背景iOS 业界研发模式多为 CocoaPods + Xcode + Git 的多仓组件化开发模型。为追求极致的研发体验、提升研发效率,对该研发模式进行了大量优化,但目前遇到了以下瓶颈,亟需...
继续阅读 »

拒绝编译等待 - 动态研发模式 ARK

作者:字节跳动终端技术——徐纪光

背景

iOS 业界研发模式多为 CocoaPods + Xcode + Git 的多仓组件化开发模型。为追求极致的研发体验、提升研发效率,对该研发模式进行了大量优化,但目前遇到了以下瓶颈,亟需突破:

  • pod install 时间长:编译优化绝大部分任务放在了 CocoaPods 上,CocoaPods 承担了更多工作,执行时间因此变长。
  • 编译时间长:虽然现阶段绝大部分工程已经从源码编译转型成二进制编译,但编译耗时依旧在十分钟左右,且现有工程基础上已无更好优化手段。
  • 超大型工程通病:Xcode Index 慢、爆内存、甚至卡死,链接时间长。

如何处理这些问题?

究其本质,产生这些问题的原因在于工程规模庞大。据此我们停下了对传统模式各节点的优化工作,以"缩小工程规模"为切入点,探索新型研发模式——动态研发模式 ARK。

ARK[1] 是全链路覆盖的动态研发模式,旨在保证工程体验的前提下缩小工程规模:通过基线构建的方式,提供线下研发所需物料;同时通过实时的动态库转化技术,保证本地研发仅需下载和编译开发仓库。

Show Case

动态研发模式本地研发流程图如下。接下来就以抖音产品为例,阐述如何使用 ARK 做一次本地开发。

演示基于字节跳动本地研发工具 MBox[2] 。

流程图

  1. 仓库下载

ARK 研发模式下,本地研发不再拉取主仓代码,取而代之的是 ARK 仓库。ARK 仓库含有与主仓对应的所有配置,一次适配接入后期不需要持续维护。

相较传统 APP 仓库动辄几个 GB 的大小,ARK 仓库贯彻了缩减代码规模这一概念。仓库仅有应用配置信息,不包含任何组件代码。ARK 仓库大小仅 2 MB,在 1 s 内可以完成仓库下载 。

在 MBox 中的使用仅需几步点击操作。首先选择要开发的产品,然后勾选 ark 模式,选择开发分支,最后点击 Create 便可以数秒完成仓库下载。

  1. 开发组件

CocoaPods 下进行组件开发一般是将组件仓库下载到本地,修改 Podfile 对应组件 A 为本地引用 pod A, :path =>'./A' ,之后进行本地开发。而在 MBox 和 ARK 的研发流程中,仅需选择要开发的组件点击 Add 便可进行本地开发。

动态研发模式 ARK 通过解析 Podfile.lock 支持了 Checkout From Commit 功能,该功能根据宿主的组件依赖信息自动拉取相应的组件版本到本地,带来便捷性的同时也保证了编译成功率。

  1. pod install

传统研发模式下 pod install 必须要经历 解析 Podfile 依赖、下载依赖、创建 Pods.xcodeproj 工程、集成 workspace 四个步骤,其中依赖解析和下载依赖两个步骤尤为耗时。

ARK 研发模式下 Podfile 中没有组件,因此依赖解析、下载依赖这两个环节耗时几乎为零。其次由于工程中仅需开发组件步骤中添加的组件,在创建 Pods 工程、集成工程这两个环节中代码规模的降低,对提升集成速度的效果非常显著。

没有依赖信息,编译、链接阶段显然不能成功。ARK 解决方案通过自研 cocoapods-ark 及配套工具链来保证编译、链接、运行的成功,其原理后续会在系列文章中介绍。

  1. 开发组件编译&调试

和传统模式一样通过 Xcode 打开工程的 xcworkspace ,即可正常开发、调试完整的应用。

工程中仅保留开发组件,但是依然有变量、函数、头文件跳转能力;参与 Index、编译的规模变小,Xcode 几乎不存在 loading 状态,大型工程也可以秒开;编译速度大幅提升。在整个动态研发流程中,通过工具链将组件从静态库转化成动态库,链接时间明显缩短。

  1. 查看全源码

ARK 工程下默认只有开发组件的源码,查看全源码是开发中的刚需。动态研发流程提供了 pod doc 异步命令实现该能力,此命令可以在开发时执行,命令执行完成后重启工程即可通过 Document Target 查看工程中其他组件源码。

pod doc 优点:

  • 支持异步和同步,执行过程中不影响本地开发。
  • 执行指令时跳过依赖解析环节,从服务端获取依赖信息,下载源码。
  • 通过 xcodegen 异步生成 Document 工程,大幅降低 pod install 时间。
  • 仅复用 pod installer 中的资源下载、缓存模块。
  • 支持仓库统一鉴权,自动跳过无权限组件仓库。

收益

体验上: 与传统模式开发流程一致,零成本切换动态研发模式。

工具上: 站在巨人的肩膀上,CocoaPods 工具链相关优化在 ARK 同样生效。

时间上: 传统研发模式中,历经各项优化后虽然能将全链路开发时间控制在 20 分钟左右,但这样的研发体验依旧不够友好。开发非常容易在这个时间间隔内被其他事情打断,良好的研发体验应该是连贯的。结合本地开发体验我们认为,一次连贯的开发体验应该将工程集成时间控制在分钟级,当前研发模式成功做到了这一点,将全链路开发时间控制在 5 分钟以内。

成功率: 成功率一直是研发效率中容易被忽视的一个指标。据不完全数据统计,集团内应用的本地全量编译成功率不足五成。一半的同学会在首次编译后执行重试。显然,对于工程新手来说就是噩梦,这意味着很长时间将在这一环节中浪费。而 ARK 从平台基线到本地工具链贯彻 Sandbox 的理念,整体上提高编译成功率。

写在最后

ARK 当前已经在字节跳动内部多业务落地使用。从初期技术方案的探索到实际落地应用,遇到了很多技术难点,也对研发模式有了新的思考。

相关技术文章将陆续分享,敬请期待。

扩展阅读

[1] ARK: github.com/kuperxu/Kwa…

[2] MBox: mp.weixin.qq.com/s/5_IlQPWnC…

字节跳动终端技术团队(Client Infrastructure)是大前端基础技术的全球化研发团队(分别在北京、上海、杭州、深圳、广州、新加坡和美国山景城设有研发团队),负责整个字节跳动的大前端基础设施建设,提升公司全产品线的性能、稳定性和工程效率;支持的产品包括但不限于抖音、今日头条、西瓜视频、飞书、懂车帝等,在移动端、Web、Desktop等各终端都有深入研究。

火山引擎应用开发套件MARS是字节跳动终端技术团队过去九年在抖音、今日头条、西瓜视频、飞书、懂车帝等 App 的研发实践成果,面向移动研发、前端开发、QA、 运维、产品经理、项目经理以及运营角色,提供一站式整体研发解决方案,助力企业研发模式升级,降低企业研发综合成本。

收起阅读 »

iOS使用addChildViewController

iOS
「这是我参与11月更文挑战的第15天,活动详情查看:2021最后一次更文挑战」。iOS早在iOS5的时候为了解耦、更加清晰的处理页面View的逻辑,UIViewController提供了addChildViewController方法,将ViewControl...
继续阅读 »

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

iOS早在iOS5的时候为了解耦、更加清晰的处理页面View的逻辑,UIViewController提供了addChildViewController方法,将ViewController作为容器处理视图控制器的切换,将比较复杂的UI使用子ViewController来管理。

iOS5.0之前只能在ViewControllerview中不断的通过addSubView添加subViewVCview视图层级中。这样使得主ViewController中的内容越来越混乱,代码越来越多,subView的管理越来越困难。

iOS5.0之后按照MVC的原则,每个ViewController只需要管理一个view视图层次结构,因此我们可以使用childViewController来拆分开发中比较复杂的View。并且此时的childViewController拥有了与父ViewController同步的声明周期。

项目中使用:

在我们项目的APP首页的实现中使用到了,首页内容展示位推荐分类菜单,以及每个菜单下的内容展示,不同的分类下的内容view展示的UI多样化。

相关方法:

///子视图控制器数组
@property(nonatomic,readonly) NSArray *childViewControllers

///向父VC中添加子VC
- (void)addChildViewController:(UIViewController *)childController

///将子VC从父VC中移除
- (void) removeFromParentViewController

///fromViewController 当前显示在父视图控制器中的子视图控制器
///toViewController 将要显示的姿势图控制器
///duration 动画时间
/// options 动画效果(渐变,从下往上等等,具体查看API)
///animations 转换过程中得动画
///completion 转换完成
- (void)transitionFromViewController:(UIViewController *)fromViewController toViewController:(UIViewController *)toViewController duration:(NSTimeInterval)duration options:(UIViewAnimationOptions)options animations:(void (^ __nullable)(void))animations completion:(void (^ __nullable)(BOOL finished))completion

///当向父VC添加子VC之后,该方法会自动调用;
- (void)willMoveToParentViewController:(UIViewController *)parent

///从父VC移除子VC之后,该方法会自动调用
- (void)didMoveToParentViewController:(UIViewController *)parent

如何使用?:

  • 如果在view上添加的只是简单的控件的话,那么使用addSubView添加到父ViewController上;
  • 如果子视图是比较复杂的视图集合,功能丰富,就选择使用addChildViewController来添加新的子ViewController,但也需要通过addSubview将子ViewControllerview添加到父视图的视图层级中;
  • iOS5之后使用addChildViewController时的原则,我们在使用addSubview的时候,同时调用addChildViewController方法将subView对应的viewController也加到当前viewController的管理中;
  • 对于那些不需要显示的subView,只需通过addChildViewControllersubVC添加到父控制器中,需要显示时再调用transitionFromViewController方法将其显示出来;
  • 当收到系统的 Memory Warning 的时候,系统也会自动把当前没有显示的 subview 销毁掉 掉,以节省内存;
  • 优点:
  1. 使页面逻辑更加清晰明了,遵循MVC模式,每个View对应相应的ViewController;
  2. 当存在不需显示的view时,将不会被加载,减少内尺使用;
  3. 当收到内存警告时,会将没有加载出的view率先释放,优化了程序的内存释放机制;

系统方法解释:

  • addChildViewController

[A父视图控制器 addChildViewController:B子视图控制器]在视图控制器A中添加了子视图控制器B.调用这个方法时如果子视图控制器已经有父视图控制器了,那么调用该方法会先把子视图控制器从之前的父视图控制器中移除,然后再添加到当前的视图控制器上作为子视图控制器。

注意:调用addChildViewController后会自动调用willMoveToParentViewController:superVC方法;

  • removeFromParentViewController

将子视图控制器从父视图控制器中移除,移除之后将自动调用didMoveToParentViewController

注意:调用removeFromParentViewControlle后会调用didMoveToParentViewController:nil方法

  • willMoveToParentViewController

当一个视图控制器从视图控制器容器中被添加或者被删除之前,该方法被调用parent:父视图控制器,如果没有父视图控制器,将为nil;当调用removeFromParentViewController方法是必须先手动调用该方法,且parent参数为nil。

  • didMoveToParentViewController

当从一个视图控制容器中添加或者移除viewController后,该方法被调用;当调用addChildViewController方法时必须手动调用该方法,且parent参数为父控制器。

代码展示:

  • 添加子VC
//自动调用,可以省略 
//[childVC willMoveToParentViewController: superVC];
[superVC addChildViewController:childVC];
[superVC.view addSubview:childVC.view];
[childVC didMoveToParentViewController:superVC];

  • 删除子VC
[childVC willMoveToParentViewController];
[childVC removeFromParentViewController];
//自动调用,可以省略
//[childVC didMoveToParentViewController:nil];

  • 切换子VC
[self addChildViewController:newController];
[self transitionFromViewController:oldController toViewController:newController duration:1.5f options:UIViewAnimationOptionCurveEaseOut animations:^{

} completion:^(BOOL finished) {
if (finished) {
[newController didMoveToParentViewController:self];
[oldController willMoveToParentViewController:nil];
[oldController removeFromParentViewController];
self.currentVC = newController;
}
else{
self.currentVC = oldController;
}
}];

总结:

  1. addChildViewController向父视图控制器中添加子视图控制器时,添加之后自动调用willMoveToParentViewController,需要手动调用didMoveToParentViewController
  2. removeFromParentViewController将子视图控制器从父视图控制器中移除,移除之后自动调用didMoveToParentViewController: nil参数为nil,需要在移除前手动调用willMoveToParentViewController
  3. transitionFromViewController:toViewController在调用这个方法之前先调用[fromViewController willMoveToParentViewController:nil]然后在completion后调用[toViewController didMoveToParentViewController:self]方法;
  4. 在切换子视图控制器显示的时候需要保证切换的子视图控制器已经被添加到父视图控制器中;
  5. 当某个子视图控制器将从父视图控制器中删除时,parent参数为nil,即:[将被删除的VC willMoveToParentViewController:nil];
  6. 当某个子试图控制器将加入到父视图控制器时,parent参数为父视图控制器,即:[将被加入的VC didMoveToParentViewController:superVC];

参考文献

blog.csdn.net/yongyinmg/a…


作者:麻蕊老师
链接:https://juejin.cn/post/7031466347410718727

收起阅读 »

(转载)人民日报评论:理性看待当前的元宇宙热潮

近来,“元宇宙”成为热门话题,越来越频繁地出现在人们的视野里。从英伟达宣布推出为元宇宙建立提供基础的模拟和协作平台,到日本社交平台GREE开展元宇宙业务,从微软正努力打造“企业元宇宙”,到脸书改名为元宇宙(Metaverse)一词中的Meta……不仅科技公司在...
继续阅读 »

近来,“元宇宙”成为热门话题,越来越频繁地出现在人们的视野里。从英伟达宣布推出为元宇宙建立提供基础的模拟和协作平台,到日本社交平台GREE开展元宇宙业务,从微软正努力打造“企业元宇宙”,到脸书改名为元宇宙(Metaverse)一词中的Meta……不仅科技公司在元宇宙赛道上争相布局,一些机构也积极参与其中。今天,我们就来聊聊元宇宙这个话题。

图源网络图源网络  

  不得不说的是,尽管已经耳熟能详,但元宇宙概念迄今仍没有清晰准确的定义。近30年前,科幻小说《雪崩》这样描述元宇宙:戴上耳机和目镜,找到连接终端,就能够以虚拟分身的方式进入由计算机模拟、与真实世界平行的虚拟空间。有人认为元宇宙会让人更有身临其境之感,用户将置身“实体互联网”之中;有人概括出元宇宙的几大特征,称元宇宙不仅是与真实世界平行的虚拟空间,更“和现实世界相互影响”,甚至拥有与现实世界相互联通的经济系统……多元的声音不一而足,可以明确的一点是:虽然元宇宙似乎拥有广阔空间和多种可能,但目前还是一个尚未成型的新兴事物。

  元宇宙概念的走红,背后有着相应的技术支撑和社会生活因素。一方面,经过多年的发展,虚拟现实、人工智能、区块链、5G通讯、可穿戴设备等底层技术的应用日渐成熟;另一方面,因为疫情等原因,线上办公、线上课程逐渐普及,人们在虚拟空间的停留时间更长,线上生活所占的比例不断升高。在一些具体的场景中,人们捕捉到元宇宙可能给生活带来的改变。从在游戏中参加虚拟演唱会,到在虚拟空间以虚拟形象参加会议,且会上可以用语音和动作进行实时交互,这些已经成为现实的案例,一定程度上打破了虚拟与现实的界线。尽管如此,有业内人士指出,元宇宙产业还远远达不到全产业覆盖和生态开放、经济自洽、虚实互通的理想状态,在技术层面、法律层面、道德伦理层面,都还有很长一段路要走。

图源网络图源网络  

  我们离元宇宙的世界有多远?这个问题可能短期内不会有答案,但各类打着元宇宙旗号的套路与骗局已经有滋生的苗头。一些知识付费项目把元宇宙包装成一夜暴富的机会,声称“未来只有元宇宙这一条路”,以贩卖焦虑的方式借机敛财。一些人言必称元宇宙,没有任何与之相关的实体内容却热衷于抢注各种相关商标,挖空心思从元宇宙概念中分得一杯“流量羹”。这就提示我们,对待新鲜事物,保持好奇和探索的同时,也要保留一份审慎和理性。即便元宇宙有可能成为真实世界的延伸与拓展,潜在的机遇和可能带来的变革值得期待,每个人仍需理性看待当前的元宇宙热潮,警惕任何以科技和未来为名义的忽悠。

  关于元宇宙的讨论仍在继续,有人充满乐观与向往,也有不少怀疑的声音。是镜花水月还是触摸得到的未来,是资本炒作还是新的赛道,是新瓶装旧酒还是科技新突破,下结论前不妨“让子弹飞一会儿”。不过可以明确的是,一些新概念承载着人们对技术发展的信心,以及对未来美好生活的期待。推动新概念及其产业逐步走向成熟需要时间,通向令人神往的科技未来需要脚踏实地、打好发展地基。正如不论虚拟现实、增强现实还是混合现实,中心词都是“现实”,这也预示着离开了现实的支撑,终归是海市蜃楼无本之木。“基础不牢地动山摇”,这样的道理不论在真实宇宙还是元宇宙,应该都是适用的。

  这正是:

  万物皆可元宇宙?理性常在少烦忧。

原文链接:https://baijiahao.baidu.com/s?id=1716732148118786628&wfr=spider&for=pc

收起阅读 »

「设计模式」iOS 中的适配器模式 Adapter

iOS
1. 生活中的适配器 提到适配器,最先想到什么?莫过于 电源适配器 了,日常使用的电脑、手机等电子设备都会有个电源适配器,作用是将插座里输出的高压交流电转换为电子设备所需的低压直流电。另外,世界各地区除了标准电压不同以外,大部分电源插头形状也不同,所以还有一类...
继续阅读 »

适配器模式.png


1. 生活中的适配器


提到适配器,最先想到什么?莫过于 电源适配器 了,日常使用的电脑、手机等电子设备都会有个电源适配器,作用是将插座里输出的高压交流电转换为电子设备所需的低压直流电。另外,世界各地区除了标准电压不同以外,大部分电源插头形状也不同,所以还有一类适配器,用于连接插头,例如香港的标准插座是三角方头的,就需要一个适配器来连接转换。


概括起来,适配器的功能是让原本不能一起工作的多个设备在不改变自身行为的前提下能一起工作。发散一下的话,笔记本电脑上各种接口使用的扩展坞、在国外旅游可能用到的语言翻译器、各种显卡 / 声卡 / 硬盘的驱动程序等等都可以理解为适配器。


2. 适配器模式


2.1 适配器模式定义


在《Head First 设计模式》中的定义如下:



适配器模式:将一个类的接口,转换成客户期望的另一个接口。适配器让原本接口不兼容的类可以合作无间。



适配器模式中主要有三个角色:



  • Target 目标接口 / 对象

  • Adaptee 被适配的对象

  • Adapter 适配器


即:通过适配器 Adapter 将被适配对象 Adaptee 包装成支持目标接口 Target 的对象,使原来 Target 能够完成的任务现在通过适配器包装后的对象也能支持,所以适配器也称作包装器 Wrapper


例如国标三角插座一般是三角扁头的,而港版电源适配器是三角方头的,在内地就不好使,需要弄一个转换器,把港版电源适配器插在转换器上再把转换器插在国标插座上,就可以正常工作了。上面的国标插座就对应为 Target 目标接口角色,港版三角方头插头是被适配的对象,额外的专用适配器将国标三角扁头转换港版三角方头。


2.2 适配器的类型


按照实现适配器的方式可以分为两种类型:类适配器(继承)  和 对象(组合)适配器。类图如下:


适配器模式类图pure.png


类适配器通过继承,也就是子类化,然后在子类中实现目标接口。在支持多重继承的语言中(C++、Python),类适配器同时继承父类以及 Target,由于在 Objective-C 以及 Java 这类语言不支持多重继承,所以目标接口一般为 协议 Protocol / 接口 Interface


对象适配器通过组合 - 将被适配对象作为适配器的属性,在实现目标接口相关方法中根据需要访问被适配对象。


类适配器 vs 对象适配器



























类适配器对象适配器
实现方式继承组合
作用范围仅被适配者类被适配者类及其子类
其他+ 易于重载,必要时可以覆盖被适配者的行为。+ 结构上更简单,不需要额外属性指向被适配者。+ 可以选择将部分工作委托给被适配者,更具弹性。 - 需要额外属性指向被适配者。

2.3 适配器的优缺点



  • 使用者(客户)与接口绑定,而不是与实现绑定,实现解耦。

  • 让没有关联的类能一起工作,不侵入原有代码,隔离原系统的影响。

  • 过多使用适配器会导致代码结构混乱(任何模式过度使用都会有问题吧:)


2.4 适配器应用场景



  • 面对遗留代码,期望项目统一使用新特性同时兼容已有类。→ eg.《Head First 设计模式》ch 7. 关于迭代器与枚举的示例。

  • 扩展新功能,方便接入新的第三方库。→ eg. 《人人都懂设计模式》电子阅读器中通过适配第三方 PDF 解析库来扩展支持 PDF 阅读。


3. iOS 中的适配器模式


在 iOS 系统上,苹果一般通过协议(可以理解为 Target 为接口)来实现适配器。例如常用的 UITableViewDataSourceUITableViewDelegate,将一个原本不能为 UITableView 提供数据 / 响应相关事件的类包装成数据源 / 代理,显然这是类适配器。详细实践介绍参考 Raywenderlich


3.1 属性包装器 @propertyWrapper


在 Swift 中当需要为属性添加相同的逻辑代码时使用属性包装器会大大减少工作量。属性包装器可以应用于结构体、枚举或者类。


如 Swift 官方文档中的示例,期望整型属性值始终小于 12,可以定义如下 TwelveOrLess 属性包装器:


// 定义 *TwelveOrLess* 属性包装器
@propertyWrapper
struct TwelveOrLess {
// 私有存储属性 number
private var number = 0
// 包装值
var wrappedValue: Int {
get { return number }
set { number = min(newValue, 12) }
}
}

// 使用 *TwelveOrLess* 来定义一个小矩形,长宽都小于等于一定值。
struct SmallRectangle {
@TwelveOrLess var height: Int
@TwelveOrLess var width: Int
}

var rectangle = SmallRectangle()
print(rectangle.height) // 打印 "0"

rectangle.height = 10
print(rectangle.height) // 打印 "10"

rectangle.height = 24
print(rectangle.height) // 打印 "12"


*以下为个人理解,不一定正确。


可以将 @propertWrapper 也理解为一个协议,这个协议要求对象实现包装属性的 Set/Get 方法:


protocol PropertyWrapperProtocol {
var wrappedValue : Int { set get }
}


即通过 TwelveOrLess 实现 PropertyWrapperProtocol 协议,来实现一个适配器,这个适配器的作用是返回一个限定范围内的数,如果尝试设置超过预设最大值的数,也只会保存为最大值。类比于电源适配器将输入的高电压适配器低电压。当然 Swift 中的属性适配器更强大也更灵活,参考 Swift GG 翻译文档 - 属性包装器


3.2 应用代理适配器 UIApplicationDelegateAdaptor


iOS 14 中新增了 UIApplicationDelegateAdaptor 用于包装原来 UIKit 中的应用代理UIApplicationDelegateNSApplicationDelegateAdaptor for AppKit、WKExtensionDelegateAdaptor for WatchKit),以便在 SwiftUI 中访问应用代理。


@propertyWrapper struct UIApplicationDelegateAdaptor<DelegateType> where DelegateType : NSObject, DelegateType : UIApplicationDelegate


从 @propertyWrapper 可以看出实际上是属性包装器的一个具体应用场景。通过泛型 DelegateType 传入一个 NSObject 类型且遵循 UIApplicationDelegate 协议的对象,猜测内部一些操作是通过转交给这个代理对象来执行的,显然是一个 对象适配器。这样使用(参考 HackingWithSwiftstackoverflow: swiftui-app-life-cycle-ios14-where-to-put-appdelegate-code):


class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
print("do something")
return true
}
}

@main
struct testApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

var body: some Scene {
WindowGroup {
ContentView()
}
}
}


有其他 iOS 相关的适配器应用实例欢迎分享讨论。


以上就是目前学习总结的 适配器模式 相关知识了。(有些地方配合图示理解更直观,似乎还差点什么,过些日子一起补上示例代码:)


参考



  1. Raywenderlich - How To Use the Adapter Pattern. 包含一个完整的例子演示如何利用协议(数据源&委托代理)实现通用水平滚动视图。

  2. Bloodline - iOS中的设计模式 - 适配器(Adapter) 介绍挺全面的。

  3. 《Head First 设计模式》ch 7. 适配器模式与外观模式。①用插座作为示例解析适配器;②面向对象适配器小节中的 ‘现有系统 → 适配器 -) 厂商类’ 例子比较形象;③示例:Java 中通过 EnumerationIterator 枚举迭代(适配)器遵循新的 迭代器 Iterator 接口来替代早期的 枚举 Enumeration(除了判断是否还有元素及访问下一个元素,迭代器还支持移除元素) 。

  4. 《人人都懂设计模式 - 从生活中领悟设计模式》第 13 章。①中国古建筑中的榫卯结构例子,不同榫头与榫槽配合工作;②示例:一个支持 .txt 及 .epub 格式的电子阅读器项目,通过适配器适配第三方 PDF 解析库支持 PDF 阅读。


扩展



  • 一般一个适配器只包装一个被适配对象,有没有一个适配器‘包装’多个被适配对象的场景?有!那就是 外观 / 门面模式 Facade Pattern

  • 有的电源适配器除了改变插头形状/电流电压外,还会提供一些额外功能,例如状态指示灯、扩展 USB 接口等,这类特性通过 装饰者模式 Decorator Pattern 实现。(装饰者主要 添加特性,适配器主要 转换接口

链接:https://juejin.cn/post/7031011469189709838
收起阅读 »

Android App 卡顿分析

Android App 反应卡顿,从技术上将就是UI 渲染慢。 UI渲染是从您的应用程序生成一个框架并将其显示在屏幕上的行为。 为了确保用户与您的应用程序的交互顺利,您的应用程序应该在16ms内渲染帧数达到每秒60帧(为什么60fps?)。 如果您的应用程序因...
继续阅读 »

Android App 反应卡顿,从技术上将就是UI 渲染慢。


UI渲染是从您的应用程序生成一个框架并将其显示在屏幕上的行为。 为了确保用户与您的应用程序的交互顺利,您的应用程序应该在16ms内渲染帧数达到每秒60帧(为什么60fps?)。 如果您的应用程序因UI渲染速度缓慢而受到影响,那么系统将被迫跳过帧,用户将感觉到您的应用程序中出现卡顿。 我们把这个叫做jank


本篇文章主要介绍 Android 开发中的部分知识点,通过阅读本篇文章,您将收获以下内容:



1.UI 渲染简介
2.识别Jank
3.Fix Jank
4.引起Jank 通用问题举例



1.UI 渲染简介


为了帮助您提高应用程序质量,Android会自动监视您的应用程序是否有空,并在Android生命危险仪表板中显示信息。 有关如何收集数据的信息,请参阅Play Console文档。


如果您的应用程序出现问题,本页提供诊断和解决问题的指导。


Android生命危险仪表板和Android系统会跟踪使用UI Toolkit的应用程序的渲染时间统计信息(应用程序的用户可见部分是从CanvasView hierarchy绘制的)。


如果您的应用程序不使用UI Toolkit,就像使用VulkanUnityUnrealOpenGL构建的应用程序一样,则在Android Vitals仪表板中不提供时间统计信息。


您可以通过运行
adb shell dumpsys gfxinfo <package name>
来确定您的设备是否正在记录您的应用的渲染时间指标。


2.识别Jank


在您的应用程序中定位引起jank的代码可能很困难。 本部分介绍了三种识别jank的方法:



  • 1.Visual inspection


通过视觉检查,您可以在几分钟内快速浏览应用程序中的所有用例use-cases,但不能提供与Systrace相同的详细信息。



  • 2.Systrace


Systrace提供了更多的细节,但是如果你运行Systrace来处理应用程序中的所有用例,那么就会被大量的数据淹没,难以分析。



  • 3.Custom performance monitoring


Visual inspectionSystrace都会在你的本地设备上检测到。


如果不能在本地设备上重现,则可以构建自定义性能监视器Custom performance monitoring,以测量在现场运行的设备上应用的特定部分。


##1. Visual inspection


目视检查可以帮助您识别正在生产结果的使用案例。 要执行视觉检查,请打开您的应用程序并手动检查应用程序的不同部分,然后查看非常粗糙的UI。 以下是进行目视检查时的一些提示:



  • 1.运行release 版本


运行您release应用程序的版本(或至少不可调试)的版本。ART运行时为了支持调试功能而禁用了一些重要的优化,所以确保你正在寻找类似于用户将看到的东西。





    1. 开启GPU渲染




开启步骤:
Settings -->Developer options -->Profile GPU rending


开启配置文件GPU渲染,会在屏幕上显示条形图,可以快速直观地显示相对于每帧16毫秒基准测试渲染UI窗口帧所花费的时间。
每个条都有着色的组件映射到渲染管道中的一个舞台,所以你可以看到哪个部分花费的时间最长。
例如,如果框架花费大量时间处理输入,则应该查看处理用户输入的应用程序代码。


开启 GPU 渲染效果图





    1. 留意特殊组件




有一些组件,如RecyclerView,是Jank普遍的来源。 如果您的应用程序使用这些组件,那么运行应用程序的这些部分是一个好idea





    1. App 冷启动导致




有时候,只有当应用程序从冷启动启动(Clod start)时,才能复制jank



  • 5.低内存情况下jank 比较容易出现


一旦你发现产生jank的用例,你可能会有一个很好的想法是什么导致你的应用程序的结果。 但是,如果您需要更多信息,则可以使用Systrace进一步深入研究。


##2. Systrace


Systrace是一个显示整个设备在做什么的工具,并且它可以用于识别应用程序中的JankSystrace的系统开销很小,所以在仪器使用过程中你会感受到app卡顿的存在。


Systrace记录跟踪,同时在设备上执行janky用例。 有关如何使用Systrace的说明,请参阅Systrace演练。 systrace被进程和线程分解。 在Systrace中查找应用程序的过程,应该如图所示。


Systrace分析应用程序


上面3个标注点解释



  1. 当卡顿时,会有掉帧发生,如上图1所示


Systrace显示何时绘制每个框架,并对每个框架进行颜色编码以突出显示较慢的渲染时间。 这可以帮助您查找比视觉检查更准确的单个janky框架。 有关更多信息,请参阅Inspecting Frames.




  1. 掉帧提示,如上图 2所示
    Systrace检测应用程序中的问题,并在各个框架和警报面板中显示警报。 警报中的以下指示是您的最佳选择。




  2. systrace timeline 如上图3 所示




Android框架和库的一部分(如RecyclerView)包含跟踪标记。 因此,systrace时间线会显示何时在UI线程上执行这些方法,以及执行多长时间。


如果systrace没有向您显示有关长时间使用UI线程工作的详细信息,则需要使用Android CPU Profiler来记录采样或检测的方法跟踪。 一般来说,method方法痕迹不适合用于识别排队,因为由于开销太大而产生假jank,并且无法看到线程何时被阻塞。 但是,method方法跟踪可以帮助您识别应用中花费最多时间的方法。 在识别这些方法后,add Trace markers a
标记并重新运行systrace,以查看这些方法是否引起混乱。
当记录systrace时,每个跟踪标记(执行的开始 Trace.beginSection();和结束Trace.endSection();对)会增加大约10μs的开销。 为了避免假Jank结局,不要将追踪标记添加到在一帧中被称为几十次的方法中,或者短于200us左右。


如需获取更多内容,请查看Systrace详解


##3. Custom performance monitoring


如果您无法在本地设备上再现突发事件,则可以在您的应用中构建自定义性能监控,以帮助识别现场设备上的突发源。


为此,请使用FrameMetricsAggregator从应用程序的特定部分收集帧渲染时间,并使用Firebase性能监控记录和分析数据。


要了解更多信息,请参阅使用Use Firebase Performance Monitoring with Android Vitals.


#3.Fix Jank


为了解决这个问题,请检查哪些帧在16.7ms内没有完成,并寻找出错的地方。Record View#draw在一些帧中抽取异常长度,或者可能是Layout? 查看下面4这些问题的常见来源,以及其他问题。


为了避免乱码,长时间运行的任务应该在UI线程之外异步运行。 一定要注意你的代码正在运行在哪个线程上,并且在向主线程发布不重要的任务时要小心。


如果您的应用程序有一个复杂而重要的主UI(可能是中央滚动列表),请考虑编写可自动检测缓慢渲染时间的测试测试,并经常运行测试以防止出现回归。 有关更多信息,请参阅自动化性能测试代码实验室。


#4.引起Jank 通用问题举例


以下部分解释了应用程序中常见Jank问题 的来源,以及解决这些问题的最佳方案。


滑动 List


ListView和特别是RecyclerView通常用于复杂的滚动列表,这些列表最容易被忽略。 他们都包含Systrace标记,所以你可以使用Systrace来弄清楚他们是否有助于在你的应用程序jank。 一定要传递命令行参数-a <your-package-name>来获取RecyclerView中的跟踪部分(以及添加的任何跟踪标记)以显示出来。 如果可用,请遵循systrace输出中生成的警报的指导。 在Systrace里面,你可以点击RecyclerView-traced部分查看RecyclerView正在做的工作的解释。


RecyclerView: notifyDataSetChanged


如果您看到RecyclerView中的每个项目在一个框架中被反弹(并因此重新布局和重新绘制),请确保您没有调用notifyDataSetChanged()setAdapter(Adapter)swapAdapter(Adapter,boolean)为小更新。 这些方法表示整个列表内容已经改变,并且将在Systrace中显示为RV FullInvalidate。 而是在内容更改或添加时使用SortedListDiffUtil生成最小更新。


例如,考虑从服务器接收新闻内容列表的新版本的应用程序。 当您将该信息发布到适配器时,可以调用notifyDataSetChanged(),如下所示:


RecyclerView: notifyDataSetChanged

但是这带来了一个很大的缺点 - 如果它是一个微不足道的变化(也许单个项目添加到顶部),RecyclerView不知道 - 它被告知放弃所有的缓存项目状态,因此需要重新绑定一切。


最好使用DiffUtil,它将为您计算和分配最小的更新。

DiffUtil 使用


只需将您的MyCallback定义为DiffUtil.Callback实现,以通知DiffUtil如何检查您的列表。


RecyclerView: Nested RecyclerViews


嵌套RecyclerView是很常见的,特别是水平滚动列表的垂直列表(如Play Store主页上的应用程序的网格)。 这可以很好的工作,但也有很多意外四处移动。 如果在第一次向下滚动页面时看到很多内部项目膨胀,则可能需要检查是否在内部(水平)RecyclerViews之间共享RecyclerView.RecycledViewPools


默认情况下,每个RecyclerView将拥有自己的物品池。 如果在屏幕上同时显示一打itemViews,那么当itemViews不能被不同的水平列表共享的时候,如果所有的行都显示了相似类型的视图,那么这是有问题的。


RecyclerView: Nested RecyclerViews


如果要进一步优化,还可以在内部RecyclerViewLinearLayoutManager上调用setInitialPrefetchItemCount(int)
例如,如果您总是在一行中可见3.5项,请调用innerLLM.setInitialItemPrefetchCount(4);. 这将告诉RecyclerView,当一个水平行即将出现在屏幕上时,如果UI线程上有空闲时间,它应该尝试预取内部的项目


RecyclerView: Too much inflation / Create taking too long


UI线程则处于闲置状态下,RecyclerView中的预取功能应该有助于在大多数情况下通过提前完成工作来解决inflation Layout的成本问题。
如果您在一帧中看到inflation Layout(而不是标记为RV Prefetch的部分),请确保您正在测试最近的设备(Prefetch目前仅在Android 5.0 API Level 21及更高版本上支持),并使用最近版本的Support Library.


如果经常看到inflation Layout导致屏幕上出现新的Jank,验证出问题,请移除多余的ViewRecyclerView内容中的视图类型越少,当新的项目类型出现在屏幕上时,需要完成的inflation Layout就越少。


如果可能的话,将视图类型合并到合理的位置 - 如果只有图标,颜色或文本块在类型之间改变,则可以在绑定时间进行更改,并避免inflation Layout(同时减少应用程序的内存占用)。


如果您的视图类型看起来还不错,请考虑减少inflation Layout的成本。减少不必要的容器和结构视图可以帮助 - 考虑使用ConstraintLayout构建itemView,这可以很容易地减少结构视图。如果你想真正优化性能,你的项目层次结构是简单的,并且你不需要复杂的themingstyle的功能,请考虑自己调用构造函数 - 但请注意,它往往是不值得的损失的简单性和功能的权衡XML。


RecyclerView: Bind taking too long


绑定(即onBindViewHolder(VH,int))应该是非常简单的,除了最复杂的项目之外的所有项目都要花费少于一毫秒的时间。 它只需从adapter's的内部项目数据中获取POJO项目,然后在ViewHolder中的视图上调用setter。 如果RV OnBindView需要很长时间,请确认您在绑定代码中做了最少的工作。


如果您使用简单的POJO对象来保存适配器中的数据,则可以完全避免使用Data Binding l
库来将绑定代码写入onBindViewHolder


RecyclerView or ListView: layout / draw taking too long


有关绘制和布局的问题,请参阅 Layout and Rendering Performance.


ListView: Inflation


如果你不小心,ListView很容易会被意外回收。 如果每次屏幕显示项目时都看到inflation Layout,请检查Adapter.getView()的实现是否正在使用,重新绑定并返回convertView参数。 如果你的getView()实现总是inflation Layout,你的应用程序将无法从ListView中获得回收的好处。 你的getView()的结构几乎总是类似于下面的实现:


复用 convertView


Layout performance


如果Systrace显示Choreographer#doFrame的布局部分工作太多,或者工作频繁,这意味着您遇到了布局性能问题。 您的应用的布局性能取决于View层次结构的哪个部分具有更改布局参数或输入。


Layout performance: Cost


如果段长度超过几毫秒,则可能是针对RelativeLayoutsweighted-LinearLayouts.
的最差嵌套性能。


这些布局中的每一个都可以触发其子项的多个measure/layout传递,因此嵌套它们会导致嵌套深度上的O(n ^ 2)行为。 请尝试避免使用RelativeLayoutLinearLayoutweight特征,除了层次结构的最低叶节点之外的所有特征。 有几种方法可以做到这一点:



  • 优化View结构

  • 使用自定义View

  • 尝试转换到ConstraintLayout,它提供了类似的功能,并且没有性能上的缺陷。


Layout performance: Frequency


当新内容出现在屏幕上时,将会发生新的Layout,例如,当一个新项目在RecyclerView中滚动查看时。 如果在每个框架上都发生重要的布局,则可能是在布局上进行动画处理,这很可能导致丢帧。 通常,动画应该在View的绘图属性(例如setTranslationX / Y / Z()setRotation(),setAlpha()等)上运行。 这些都可以比Layout属性(如填充或边距)更好地更改。 通常通过调用触发invalidate()setter,然后在下一帧中绘制(Canvas),来更改视图的绘制属性。 这将重新记录无效的视图的绘图操作,并且通常也比布局好得多。


Rendering performance渲染性能


Android UI在两个阶段工作 - 在UI线程Record View#draw,在RenderThread上绘制DrawFrame。 第一次运行在每个无效的View上绘制(Canvas),并可能调用自定义视图或代码。 第二个在本地RenderThread上运行,但是将根据Record View#draw阶段生成的工作进行操作。


Rendering performance: UI Thread


如果Record View#draw需要很长时间,则通常是在UI线程上绘制位图的情况。 绘制位图需要使用CPU渲染,一般应该避免在主线程中绘制。 您可以使用Android CPU分析器的方法跟踪来查看这是否是问题。


绘制位图通常是在应用程序想要在显示位图之前修饰位图的时候完成的。 有时候像装饰圆角的装饰:

绘制圆角图片

如果这是您在UI线程上所做的工作,则可以在后台的解码线程上执行此操作。 在这样的一些情况下,你甚至可以在绘制时做这个工作,所以如果你的Drawable或View代码看起来像这样:
性能差代码


可以将上面代码优化为如下:


优化性能的代码


请注意,这通常也可以用于后台保护(在位图顶部绘制渐变)和图像过滤(使用ColorMatrixColorFilter),以及修改位图的其他两种常见操作。


如果由于其他原因(可能将其用作缓存)绘制到位图,则尝试绘制直接传递到ViewDrawable的硬件加速硬件,如有必要,可考虑使用LAYER_TYPE_HARDWARE调用setLayerType()来缓存复杂的渲染 输出,并仍然利用GPU渲染。


Rendering performance: RenderThread


一些canvas操作是便小的消耗,但触发RenderThread昂贵的计算。 Systrace通常会通知这些。


Canvas.saveLayer()


避免Canvas.saveLayer() - 它可以触发昂贵的,未缓存的,离屏渲染每一帧。 尽管Android 6.0的性能得到了提高(当进行优化以避免GPU上的渲染目标切换时),但是如果可能的话,避免使用这个昂贵的API仍然是好事,或者至少确保您通过CLIP_TO_LAYER_SAVE_FLAG(或者调用一个变体 不带标志)。


Animating large Paths


当硬件加速Canvas传递给Views时,Canvas.drawPath()被调用,Android首先在CPU上绘制这些路径,然后将它们上传到GPU。 如果路径较大,请避免逐帧编辑,以便高速缓存和绘制。 drawPoints(),drawLines()和drawRect / Circle / Oval / RoundRect()更有效率 - 即使最终使用更多的绘制调用,最好使用它们。


Canvas.clipPath


clipPath(Path)触发了昂贵的裁剪行为,通常应该避免。 如果可能,选择绘制形状,而不是剪裁到非矩形。 它性能更好,支持抗锯齿。 例如,下面的clipPath调用:


Canvas.clipPath


Bitmap uploads


Android将位图显示为OpenGL纹理,并且首次在一帧中显示位图时,将其上传到GPU。您可以在Systrace中将此视为上传宽度x高度纹理。这可能需要几个毫秒(见下图),但是有必要用GPU显示图像。

位图绘制


如果这些花费很长时间,请首先检查轨迹中的宽度和高度数字。确保正在显示的位图不比显示的屏幕区域大得多。如果是,则浪费上传时间和内存。通常位图加载库提供了简单的方法来请求适当大小的位图。


Android 7.0中,位图加载代码(通常由库完成)可以在需要之前调用prepareToDraw()来及早触发上传。这样上传发生的早,而RenderThread空闲。这可以在解码之后完成,也可以在将位图绑定到View时进行,只要知道位图即可。理想情况下,你的位图加载库会为你做这个,但是如果你正在管理你自己的,或者想确保你没有在新设备上点击上传,你可以在你自己的代码中调用prepareToDraw()


Thread scheduling delays


线程调度程序是Android操作系统的一部分,负责决定系统中哪些线程应该运行,何时运行以及运行多长时间。 有时候,因为你的应用程序的UI线程被阻塞或者没有运行,就会发生JankSystrace使用不同的颜色来指示线程正在Sleep(灰色)Runnable(蓝色:可以运行,但调度程序还没有选择它运行)正在运行(绿色)中断(红色或橙色)。 这对于调试线程调度延迟导致的Jank`问题非常有用。



注意:
旧版本的Android更频繁地遇到不是应用程序故障的调度问题。 在这方面进行了不断的改进,所以考虑在最近的操作系统版本上更多的调试线程调度问题,在这些版本中,被调度的线程更可能是应用程序的错误。



线程渲染过程


UI线程RenderThread预计不会运行时,有框架的一部分。 例如,UI线程RenderThreadsyncFrameState正在运行并且上传位图时被阻塞 - 这是因为RenderThread可以安全地复制UI线程所使用的数据。 另一个例子是,RenderThread在使用IPC时可以被阻塞:在帧的开始处获取缓冲区,从中查询信息,或者通过eglSwapBuffers将缓冲区传回给合成器。


在您的应用程序的执行中经常会有很长时间的暂停,这些都是由Android上的进程间通信(IPC)机制进行的。 在最近的Android版本中,这是UI线程停止运行的最常见原因之一。 一般来说,修正是为了避免调用函数来调用binder; 如果这是不可避免的,那么应该缓存该值,或将工作移动到后台线程。 随着代码库变得越来越大,如果不小心的话,通过调用一些低级别的方法,很容易意外地添加了一个binder调用,但是使用跟踪来发现和修复它们也是很容易的。


如果您有绑定事务,则可以使用以下adb命令来捕获其调用堆栈:

adb 命令捕获堆栈信息


有时像getRefreshRate()这样的无害的表面调用可能会触发绑定事务,并在频繁调用时导致严重的问题。 定期跟踪可以帮助您快速找到并解决这些问题。


UI Thread sleeping due to binder transactions in a RV fling


如果你没有看到绑定Activity,但仍然没有看到你的UI线程运行,请确保你没有等待来自另一个线程的锁定或其他操作。 通常,UI线程不应该等待来自其他线程的结果 - 其他线程应该向其发布信息post message.


Object allocation and garbage collection


对象分配和垃圾回收(GC)已经成为一个问题,因为ARTAndroid 5.0中默认运行时引入的,但是仍然有可能通过这些额外的工作来减轻你的线程负担。 对于每秒钟不会发生多次的罕见事件(如用户单击按钮)进行分配是很好的做法,但要记住,每次分配都需要付出一定的代价。 如果它处于一个频繁调用的紧密循环中,请考虑避免分配来减轻GC上的负载。


Systrace会告诉你GC是否频繁运行,Android Memory Profiler可以显示你的分配来自哪里。 如果你可以避免分配,特别是在紧密的循环中,你应该没有问题。

shows a 94ms GC on the HeapTaskDaemon thread


在最新版本的Android上,GC通常在名为HeapTaskDaemon的后台线程上运行。 请注意,大量的分配可能意味着更多的CPU资源花费在GC上.



至此,本篇已结束,如有不对的地方,欢迎您的建议与指正。同时期待您的关注,感谢您的阅读,谢谢!


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

Android组件化基础

前言 公司包含三大业务线,每条业务线都有独立的app。功能模块难免会有重合~举个栗子,直播功能本来只在业务线A使用,但是由于业务拓展,现在业务线B和C也需要使用直播功能。这时候就有必要将直播功能做成一个独立的直播组件供三条业务线使用。 构思 既然要将直播做成组...
继续阅读 »

前言


公司包含三大业务线,每条业务线都有独立的app。功能模块难免会有重合~举个栗子,直播功能本来只在业务线A使用,但是由于业务拓展,现在业务线B和C也需要使用直播功能。这时候就有必要将直播功能做成一个独立的直播组件供三条业务线使用。


构思


既然要将直播做成组件,需要考虑哪些方面呢?



  1. 既可独立运行,单独测试该组件功能;也可作为sdk,被其他项目使用

  2. 统一管理:部署到私有化仓库,其他项目可配置引用


基础实践


全局控制配置


在gradle.properties中的配置可以在项目中直接使用


# 是否作为module使用
isModule=true

build.gradle的配置



  1. 配置android构建插件


if(isModule.toBoolean()){
// lib
apply plugin: 'com.android.library'
}else{
// 独立运行的app
apply plugin: 'com.android.application'
}


  1. 禁用applicationId配置


作为library不能带有配置,否则编译会报错:Library projects cannot set applicationId. applicationId is set to 'com.example.live' in default config.


android {
...
defaultConfig {
if(!isModule.toBoolean()){
applicationId "com.example.live"
}
...
}

AndroidManifest.xml的配置


1. 独立运行


为了可独立运行,需要配置application和启动Activity


// 正常模板
<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.TestAndroidManifest">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

2. module形式使用


假如被其他项目作为组件使用,则需要修改application和启动入口配置


// 去除application不必要的属性配置
<application>
// 去除intent-filter
<activity
android:name=".MainActivity"
android:exported="true">
</activity>
</application>

这里有两个问题:



  1. application里边的属性配置可以不去掉吗?


其实在编译后,所有module的AndroidManifest会被合并到一起,假如相同属性配置不同会报错


Manifest merger failed : Attribute application@name value=(com.example.moduledemo.MainApplication) from AndroidManifest.xml:7:9-40
is also present at [:live] AndroidManifest.xml:11:9-56 value=(com.example.live.LiveApplication).
Suggestion: add 'tools:replace="android:name"' to <application> element at AndroidManifest.xml:6:5-23:19 to override.

这里我分别给和app-module和live-module指定了自定义appliation,提示合并失败了,解决方案需要通过在app-module配置tools:replace="android:name"。这里通过不同配置然后rebuild查看下输出的AndroidManifest.xml文件可以总结以下规律:



  • 假如只有一个module配置了自定义application,则直接使用该application

  • 假如每个module都配置了自定义application,则需要解决冲突。解决后会使用最后编译的那个module的application(举个例子:demo中,app-module依赖于live-module,假如都配置了自定义application,因为app后编译,所以最后会使用app-module里边定义的)



  1. activity里边的intent-filter可以不去掉吗?


合并.png


看到合并后的文件,里边包含了两个包含启动信息的activity。安装app时你会发现在桌面会有两个启动图标,并且点击他们的行为是一致的:打开第一个配置了MAIN和LAUNCHER的activity。因此是没有必要保留该配置的。


3. 动态配置AndroidManifest


根据上述的分析发现,作为module使用和独立app运行,相应的AndroidManifest.xml也需要相应的进行调整。那我们就有必要根据配置来配置使用不同的AndroidManifest文件了



  1. 在live-module增加用于sdk的AndroidManifest.xml


module.png
3. 在live-module的build.gradle配置动态引用不同的AndroidManifest.xml


android {
...
sourceSets {
main {
if(isModule){
manifest.srcFile 'src/main/module/AndroidManifest.xml'
}else{
manifest.srcFile 'src/main/AndroidManifest.xml'
}
}
}
}

总结


至此,你已经可以通过修改gradle.properties里边的liModule来控制是否以library的形式使用live组件了。这里可以思考个问题,假如我们项目中有好几个类似于live这样的组件,是否每个组件都需要做这么繁琐的配置呢?能否将这些配置抽出来,统一管理?


优化


1. 抽取独立app构建脚本


在项目根目录创建一个common_app_build.gradle


apply plugin: 'com.android.application'

android {
compileSdk 31
defaultConfig {
minSdk 21
targetSdk 31
versionCode 1
versionName "1.0"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}

sourceSets {
main {
manifest.srcFile 'src/main/AndroidManifest.xml'
}
}
}

2. 抽取构建library脚本


在项目根目录创建一个common_library_build.gradle


apply plugin: 'com.android.library'

android {
compileSdk 31
defaultConfig {
minSdk 21
targetSdk 31
versionCode 1
versionName "1.0"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}

sourceSets {
main {
manifest.srcFile 'src/main/module/AndroidManifest.xml'
}
}
}

3. 在创建一个的course module(用于验证)


4. 修改live和course两个module的build.gradle


下边以live module为例


// 直接通过配置引用不同的gradle文件,前边涉及的配置都可以去掉
if (isModule.toBoolean()) {
apply from: '../common_library_build.gradle'
} else {
apply from: '../common_app_build.gradle'
}

android {
defaultConfig {
if(!isModule.toBoolean()){
applicationId "com.example.live"
}
}
}

后续类似的组件只需要进行简单的配置,即可实现第一点的构思


module发布


这里以live module为例进行实践,# google文档:使用 Maven Publish 插件


发布live module到本地仓库


再live module的build.gradle增加以下配置


afterEvaluate {
publishing {
repositories {
maven {
url uri("../repo")
}
}
publications {
maven(MavenPublication) {
from components.release
groupId "com.example.live"
artifactId "modulelive"
version "1.0.0"
}
}
}
}

上述配置,指定将live发布到 项目/repo/ 目录下。sync完成后,会在live出现publish task


maven.png


双击publish,即会在repo生成相应的aar文件


aar.png


配置根build.gradle


为了可以使用repo里边的aar,需要增加配置


buildscript {
repositories {
...
maven {
url('repo')
}
}
...
}

app中使用:配置build.gradle


dependencies {
...
// 不直接引用project
// api project(':live')
// 改为该配置
implementation 'com.example.live:modulelive:1.0.0'
...
}

重新rebuild就可以正常使用到live组件。


发布到远程仓库


因为不同业务线项目环境不同,发布到本地项目目录下,使用比较不方便吗。所以可以考虑将组件发布到公司内部的私有仓库,供所有项目组使用:


publishing {
...
repositories {
maven {
// 仓库地址
url = "http://...."
// 仓库用户名及密码
credentials {
username ''
password ''
}
}
}
}

总结


上述主要是讲述了Android组件化的一些基础以及如何发布组件的一些流程。当然,组件化的内容不止这些内容,包括:



  • 组件间通信

  • 组件间跳转

  • 组件化混淆

  • 组件资源冲突

  • .....


这些方面都是在进行组件化设计需要思考与处理的~后续逐渐完善这块的内容


gitee:Demo地址


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

Android加载长图方案

背景介绍 在某些特定场景下,我们需要考虑加载长图的需求,比如加载一幅《清明上河图》,这个好像有点过分了,那就加载1/2的《清明上河图》吧... 那TMD还不是一样道理。 言归正传说一下我这边遇到的情况,之前有图片或大图的模块是划分为H5来实现的,现在需求变更划...
继续阅读 »

背景介绍


在某些特定场景下,我们需要考虑加载长图的需求,比如加载一幅《清明上河图》,这个好像有点过分了,那就加载1/2的《清明上河图》吧... 那TMD还不是一样道理。


言归正传说一下我这边遇到的情况,之前有图片或大图的模块是划分为H5来实现的,现在需求变更划分为原生开发,那么问题就来了。


图片尺寸为


image.png


图片大小为


image.png


这一刻我是懵逼的,哪个端图片上传的时候没限制尺寸和压缩?mdzz,
吐槽归吐槽,还是要撸起袖子解决加载长图大图的问题。
先提供几个技术方案来对比一下:


方案1:WebView加载渲染

因为图片本身也是一个URL地址,也是被WebView渲染,并且支持缩放。这是一种实现方案,遇到几M的大图WebView也是会崩溃Crash,所以这种投机的方式并不推荐。


方案2:BitmapRegionDecoder

分片加载,使用系统BitmapRegionDecoder去加载本地的图片,调用bitmapRegionDecoder.decodeRegion解析图片的矩形区域,返回bitmap,最终显示在ImageView上。这种方案需要手动处理滑动、缩放手势,网络图片还要处理缓存策略等问题。实现方式比较繁琐也不是很推荐。


方案3:SubsamplingScaleImageView

一款封装BitmapRegionDecoder的三方库,已经处理了滑动,缩放手势。我们可以考虑选择这个库来进行加载长图,但是官方上的Demo示例加载的长图均为本地图片。这可能并不符合我们的网络场景需求,所以对于网络图片,我们还要考虑不同的加载框架,


SubsamplingScaleImageView Git传送门

方案4:Glide+SubsamplingScaleImageView混合加载渲染

对于图片加载框架,Glide当然是首选,我们使用Glide进行网络图片的下载和缓存管理,FileTarget作为桥梁,SubsamplingScaleImageView进行本地资源图片的分片加载,看起来很靠谱,那么一起来实现吧。


Glide Git传送门

SubsamplingScaleImageView Git传送门

fun loadLargeImage(context: Context, res: String, imageView: SubsamplingScaleImageView) {
imageView.isQuickScaleEnabled = true
imageView.maxScale = 15F
imageView.isZoomEnabled = true
imageView.setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CUSTOM)

Glide.with(context).load(res).downloadOnly(object : SimpleTarget<File?>() {
override fun onResourceReady(resource: File, glideAnimation: Transition<in File?>?) {

val options = BitmapFactory.Options()
options.inJustDecodeBounds = true
BitmapFactory.decodeFile(resource.absolutePath, options)
val sWidth = options.outWidth
val sHeight = options.outHeight
options.inJustDecodeBounds = false
val wm = ContextCompat.getSystemService(context, WindowManager::class.java)
val width = wm?.defaultDisplay?.width ?: 0
val height = wm?.defaultDisplay?.height ?: 0
if (sHeight >= height
&& sHeight / sWidth >= 3) {
imageView.setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CENTER_CROP)
imageView.setImage(ImageSource.uri(Uri.fromFile(resource)), ImageViewState(0.5f, PointF(0f, 0f), 0))
} else {
imageView.setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CUSTOM)
imageView.setImage(ImageSource.uri(Uri.fromFile(resource)))
imageView.setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_CENTER_IMMEDIATE)
}
}
override fun onLoadFailed(errorDrawable: Drawable?) {
super.onLoadFailed(errorDrawable)
}
})

}

这是我封装起来的一个方法,就很简单就能理解了, 包括SubsamplingScaleImageView的缩放设置,默认展示状态、缩放、位置,计算当前图片高宽比为3倍进行长图渲染处理,否则按正常图片渲染处理。


最后快用下面的这张完整版《清明上河图》来试一试效果吧~ 赞


清明上河图_简书_爱吃大蒜.jpeg


如果我帮助你成功实现了加载长图的需求,千万要记得回来点赞哦,ღ( ´・ᴗ・` )比心

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

关于web中的颜色表示方法,你知道多少?

想要表示web中的各种颜色,大家首先想到的大概就是用十六进制或者RGB来表示。但在实际web中,是远不止这两种的。今天这篇文章就和大家聊一聊,在web中颜色的各种表示方法。 以如下代码为例,大家可以复制代码看看效果: HTML <div class="b...
继续阅读 »

想要表示web中的各种颜色,大家首先想到的大概就是用十六进制或者RGB来表示。但在实际web中,是远不止这两种的。今天这篇文章就和大家聊一聊,在web中颜色的各种表示方法。


以如下代码为例,大家可以复制代码看看效果:


HTML


<div class="box">
<div class="one"></div>
<div class="two"></div>
<div class="three"></div>
</div>

CSS


.box {
width: 200px;
height: 200px;
padding: 20px 20px;
display: flex;
justify-content: space-between;
}
.box > div {
width: 50px;
height: 50px;
border-radius: 4px;
}

英文单词


HTML 和 CSS 颜色规范中预定义了 140+ 个颜色名称,可以点进这里进行查看。直接用英文单词的好处是直接明了,缺点是140+个单词确实难记,也不能包含所有的颜色。


.one { background-color: red; }
.two { background-color: green; }
.three { background-color: blue; }

十六进制


十六进制表示颜色:#RRGGBB ,这里的十六进制实质就是RGB的十六进制表示法,每两位表示RR(红色)、GG(绿色)和 BB(蓝色)三色通道的色阶。所有值必须在 00 到 FF 之间。


.one { background-color: #00FFFF; }
.two { background-color: #FAEBD7; }
.three { background-color: #7FFFD4; }

对于类似于 #00FFFF 的颜色格式也可以缩写为 #0FF


.one { background-color: #0FF; }

如果需要带上透明度,还可以像下面这样增加两个额外的数字:


.one { background-color: #00FFFF80; }

RGB


rgb() 函数中,CSS语法如下:


rgb(red, green, blue)

每个参数 red, green, blue 定义颜色的强度,可以是 0 到 255 之间的整数或百分比值(从 0% 到 100%)


.one { background-color: rgb(112,128,144); }
.two { background-color: rgb(30%,10%,60%); }
.three { background-color: rgb( 0,139,139); }
复制代码

十六进制和RGB的原理都是利用了光的三原色:红色,绿色,蓝色。利用这三种颜色就能组合出上千万种颜色。简单的计算一下,256级的RGB色彩总共能组合出约1678万种色彩,即256×256×256=16777216种。至于为什么是256级,因为 0 也是数值之一。


RGBA


RGBA就是在RGB之上扩展了一个 Alpha 通道 ,指定对象的不透明度。


.one { background-color: rgba(112,128,144, 0.5); }
.two { background-color: rgb(30%,10%,60%, 0.2); }
.three { background-color: rgb( 0,139,139, 0.5); }

HSL


HSL 分别代表 色相(hue)、饱和度(saturation)和亮度(lightness),是一种将RGB色彩模型中的点在圆柱坐标系中的表示法


CSS语法如下:


hsl(hue, saturation, lightness)


  • 色相:色轮上的度数(从 0 到 360)- 0(或 360)是红色,120 是绿色,240 是蓝色。

  • 饱和度:一个百分比值; 0% 表示灰色阴影,而 100% 是全彩色。

  • 亮度:一个百分比; 0% 是黑色,100% 是白色。


例子:


.one { background-color: hsl(20, 100%, 50%); }
.two { background-color: hsl(130, 100%, 25%); }
.three { background-color: hsl(240, 80%, 80%); }

HSLA


HSLA 和 HSL 的关系与 RGBA 和 RGB 的关系类似,HSLA 颜色值在 HSL 颜色值上扩展 Alpha 通道 - 指定对象的不透明度。


CSS语法如下:


hsla(hue, saturation, lightness, alpha)

例子:


.one { background-color: hsla(20, 100%, 50%, 0.5); }
.two { background-color: hsla(130, 100%, 25%, 0.75); }
.three { background-color: hsla(240, 80%, 80%,0.4); }
复制代码

opacity


opacity 属性设置一个元素了透明度级别。


CSS语法如下:


opacity: value|inherit;

它与 RGBA 中的 A 在行为上有一定的区别:opacity 同时影响子元素的样式,而 RGBA 则不会。感兴趣的可以试一试。


关键字


除了 <color>s 的各种数字语法之外,CSS还定义了几组关于颜色的关键字,这些关键字都有各自的有点和用例。这里介绍一下两个特殊的关键字 transparentcurrentcolor


transparent


transparen 指定透明黑色,如果一个元素覆盖在另外一个元素之上,而你想显示下面的元素;或者你不希望某元素拥有背景色,同时又不希望用户对浏览器的颜色设置影响到您的设计。 transparent 就能派上用场了。


在CSS1中,transparent 是作为 background-color 的一个值来用的,在后续的 CSS2 和 CSS3 中, transparent 可以用在任何一个有 color 值的属性上了。


.one { 
background-color: transparent;
color: transparent;
border-color: transparent;
}

currentcolor


currentcolor 关键字可以引用元素的 color 属性值。


.one { 
color: red;
border: 1px solid currentcolor;
}

相当于


.one { 
color: red;
border: 1px solid red;
}

下面介绍的这些目前主流浏览器还没有很好的支持,但是已经列为CSS4标准了,所以了解一下也是挺好的。


HWB


hwb() 函数表示法根据颜色的色调、白度和黑度来表示给定的颜色。也可以添加 alpha 组件来表示颜色的透明度。


语法如下:


hwb[a](H W B[/ A])

例子:


hwb(180 0% 0%)
hwb(180 0% 0% / .5)
hwb(180, 0%, 0%, .5); /* 使用逗号分隔符 */

目前只有Safari支持。


Lab、Lch


lab() 函数表示法表示 CIE L * a * b * 颜色空间中的给定颜色,L* 代表亮度,取值范围是[0,100]; a* 代表从绿色到红色的分量,取值范围是[127,-128]; b* 代表从蓝色到黄色的分量 ,取值范围是[127,-128]。理论上可以展示出人类可以看到的全部颜色范围。


语法如下:


lab(L a b [/ A])

例子:


lab(29.2345% 39.3825 20.0664);
lab(52.2345% 40.1645 59.9971);

lch() 函数表示法表示CIE LCH 颜色空间中给定的颜色,采用了同 L * a * b * 一样的颜色空间,但它采用L表示明度值,C表示饱和度值,H表示色调角度值的柱形坐标。


语法如下:


lch(L C H [/ A])

例子:


lch(29.2345% 44.2 27);
lch(52.2345% 72.2 56.2);

关于常用颜色空间的概念,可以自行查询,或者点击这篇文章进行了解。


color()


color() 函数表示法允许在特定的颜色空间中指定颜色。


语法如下:


color( [ [<ident> | <dashed-ident>]? [ <number-percentage>+ | <string> ] [ / <alpha-value> ]? ] )

例子:


color(display-p3 -0.6112 1.0079 -0.2192);
color(profoto-rgb 0.4835 0.9167 0.2188);这里可以了解一下色域标准

CMYK


CMYK印刷四色模式



印刷四色模式,是彩色印刷时采用的一种套色模式,利用色料的三原色混色原理,加上黑色油墨,共计四种颜色混合叠加,形成所谓“全彩印刷”。四种标准颜色是:C:Cyan = 青色,又称为‘天蓝色’或是‘湛蓝’M:Magenta = 品红色,又称为‘洋红色’;Y:Yellow = 黄色;K:blacK=黑色。此处缩写使用最后一个字母K而非开头的B,是为了避免与Blue混淆。CMYK模式是减色模式,相对应的RGB模式是加色模式。



电脑显示屏使用 RGB 颜色值显示颜色,而打印机通常使用 CMYK 颜色值显示颜色。在CSS4标准中,计划利用 device-cmyk() 函数来实现。


语法如下:


device-cmyk() = device-cmyk( <cmyk-component>{4} [ / <alpha-value> ]? , <color>? )
<cmyk-component> = <number> | <percentage>

例子:


device-cmyk(0 81% 81% 30%);
device-cmyk(0 81% 81% 30% / .5);
作者:xmanlin
链接:https://juejin.cn/post/7031700587120951310

收起阅读 »

使用这11个代码,可以大大地简化我们的代码。

1.避免 if 过长 如果判断值满足多个条件,我们可能会这么写: if (value === 'a' || value === 'b' || value === 'c') { ... } 像这样如果有多个条件,if 条件就会很我,可读性降低,我们可以这样简化:...
继续阅读 »

1.避免 if 过长


如果判断值满足多个条件,我们可能会这么写:


if (value === 'a' || value === 'b' || value === 'c') { ... }

像这样如果有多个条件,if 条件就会很我,可读性降低,我们可以这样简化:


if (['a', 'b', 'c'].includes(value)) { ... }

2.双!操作符将任何变量转换为布尔值


!(NOT)运算符可以使用两次!!,这样可以将任何变量转换为布尔值(像布尔函数),当你需要在处理它之前检查某个值时非常方便。


const toto = null

!!toto // false
Boolean(toto) // false

if (!!toto) { } // toto is not null or undefined

3.可选项 (?)


在 JS 中,我们需要经常检查对象的某些属性是否存在,然后才能再处理它,不然会报错。 早期我们可能会这么干:


const toto = { a: { b: { c: 5 } } }

if (!!toto.a && !!toto.a.b && !!toto.a.b.c) { ... } // toto.a.b.c exist

如果对象嵌套很深,我们这写法就难以阅读,这时可以使用?来简化:



if (!!toto.a?.b?.c) { ... } // toto.a.b.c exist

// 如果键不存在,返回 `undefined`。
const test = toto.a?.b?.c?.d // undefined

4. 如果if中返回值时, 就不要在写 else


经常会看到这种写法:


if (...) {
return 'toto'
} else {
return 'tutu'
}

如果if有返回值了,可以这样写:


if (...) {
return 'toto'
}

return 'tutu'

5.避免forEach,多使用filtermapreduceeverysome


作为初学者,我们使用了很多forEach函数,但 JS 为我们提供了很多选择,而且这些函数是FP(函数式编程)。


filter


filter() 方法创建一个新数组, 其包含通过所提供函数实现的测试的所有元素。


const toto = [1, 2, 3, 4]

// 过滤奇数
const evenValue = toto.filter(currentValue => {
return currentValue % 2 == 0
}) // [2, 4]

map


map() 方法创建一个新数组,其结果是该数组中的每个元素是调用一次提供的函数后的返回值。


const toto = [1, 2, 3, 4]

const valueMultiplied = toto.map(currentValue => {
return currentValue * 2
}) // [2, 4, 6, 8]

reduce


reduce() 方法对数组中的每个元素执行一个由您提供的reducer函数(升序执行),将其结果汇总为单个返回值。


const toto = [1, 2, 3, 4]

const sum = toto.reduce((accumulator, currentValue) => {
return accumulator += currentValue
}, 0) // 10

Some & Every


some() 方法测试数组中是不是至少有1个元素通过了被提供的函数测试。它返回的是一个Boolean类型的值。


every() 方法测试一个数组内的所有元素是否都能通过某个指定函数的测试。它返回一个布尔值。


什么时候使用?


所有项目都符合一个条件可以用 every


const toto = [ 2, 4 ]

toto.every(val => val % 2 === 0) // true

const falsyToto = [ 2, 4, 5 ]

falsyToto.every(val => val % 2 === 0) // false

只要一个符合条件就行,用some


const toto = [ 2, 4, 5 ]

toto.some(val => val % 2 !== 0) // return true

6.不要使用 delete 来删除属性


从一个对象中 delete 一个属性是非常不好的(性能不好),此外,它还会产生很多副作用。


但是如果你需要删除一个属性,你应该怎么做?


可以使用函数方式创建一个没有此属性的新对象,如下所示:


const removeProperty = (target, propertyToRemove) => {
const { [propertyToRemove]: _, ...newTarget } = target
return newTarget
}
const toto = { a: 55, b: 66 }
const totoWithoutB = removeProperty(toto, 'b') // { a: 55 }

7.仅当对象存在时才向其添加属性


有时,如果对象已经定义了属性,我们需要向对象添加属性,我们可能会这样写:


const toto = { name: 'toto' }
const other = { other: 'other' }
// The condition is not important
const condition = true

if (condition) {
other.name = toto.name
}

❌不是很好的代码


✅ 可以用一些更优雅的东西!


const condition = true

const other = {
other: 'other',
...condition && { name: 'toto' }
}

8. 使用模板字符串


在 JS 中学习字符串时,我们需要将它们与变量连接起来


const toto = 'toto'
const message = 'hello from ' + toto + '!' // hello from toto!

如果还有其它变量,我们就得写很长的表达式,这时可以使用模板字符串来优化。


const toto = 'toto'
const message = `hello from ${toto}!` // hello from toto!

9. 条件简写


当条件为 true 时,执行某些操作,我们可能会这样写:


if(condition){
toto()
}

这种方式可以用 && 简写:


condition && toto()

10.设置变量的默认值


如果需要给一个变量设置一个默认值,可以这么做:


let toto

console.log(toto) //undefined

toto = toto ?? 'default value'

console.log(toto) //default value

toto = toto ?? 'new value'

console.log(toto) //default value

11.使用 console timer


如果需要知道一个函数的执行时间,可以这么做:


for (i = 0; i < 100000; i++) {
// some code
}
console.timeEnd() // x ms




作者:前端小智
链接:https://juejin.cn/post/7031691510533849124

收起阅读 »

(转载)领投第三代计算机图形引擎,高瓴持续开发元宇宙

本报记者 谢岚第三代计算机图形引擎开发商粒界科技11月16日宣布完成千万美元级B轮融资,由高瓴创投领投。这也是短短数月里,高瓴创投投资的第5家元宇宙公司了。成立于2015年的粒界科技,由毕业于上海交大的计算机博士吴小毛创立,从事智能化渲染及数字建模技术的产品研...
继续阅读 »

本报记者 谢岚

第三代计算机图形引擎开发商粒界科技11月16日宣布完成千万美元级B轮融资,由高瓴创投领投。

这也是短短数月里,高瓴创投投资的第5家元宇宙公司了。

成立于2015年的粒界科技,由毕业于上海交大的计算机博士吴小毛创立,从事智能化渲染及数字建模技术的产品研发,致力于“让数字世界人机交互更简单”和打造第三代图形引擎GritGene。

在粒界看来,“元宇宙”即未来世界的人机交互将出现在无限多的3D场景中。如何将这些高维、多领域的数据转换成人们方便理解的可视化数据、并与之流畅地交互,急需新一代的图形引擎作为基础技术和工具来支撑。

粒界打造的第三代图形引擎正是这样一种底层技术。区别于Unreal、Unity为代表的第一、二代图形技术,第三代图形引擎的一个重要特征,是通过高性能的LBS(基于位置的服务)接口,跳脱出前两代图形引擎仅仅为游戏开发者服务的场景,为更多场景下的专业与非专业开发者创造参与数字内容创作的可能。

高瓴则认为,数字内容对于物理世界的加速渗透,正在颠覆人与现实的交互模式。未来世界将出现巨量、无限场景的“虚拟现实”和‘混合现实’,而这需要一个强有力的数字引擎作为基础。

“粒界科技自主研发的实时图形引擎正是这样一种底层技术,其不但打通了云-边-端,更将原本只服务于游戏的渲染技术在非游戏的众多场景一一验证与落地,为虚拟现实和混合现实的发展打开了更大的可想象空间。”高瓴创投合伙人李强表示。

投云、投边缘计算:元宇宙基础设施

从公开数据可以看到,几个月来高瓴在元宇宙概念下出手连连,布局覆盖从入口、云、底层技术、2B应用到Verse等。

元宇宙作为现实世界的平行空间,其入口类似黑洞,在未来可视范围大约3-5年内,可以成为元宇宙入口的首先就是AR/VR眼镜及交互设备。在这一领域,高瓴创投投资了增强现实(AR)科技公司Nreal。

再来看云。元宇宙有先天的云属性,是云端的上层建筑,因此云基础架构的重要性不言而喻。而算力、存储、网络传输是云计算的“三驾马车”。

2021年8月,高瓴创投领投了云原生数据库「DatafuseLabs」。这一领域的代表性企业Snowflake去年上市,市值高达700亿美元,而中国云原生数据仓库的发展还倚赖一个更大优势:即庞大的数据体量和数据分析需求。

「DatafuseLabs」创始团队成员来自阿里云、Google、青云等国内外知名云计算公司,在云原生数据库领域有着丰富的工程经验,同时也是数据库开源社区活跃贡献者。

在高瓴看来,Datafuse作为一家云中立开源海量数据分析平台,以对云资源的精细调度为基础,打造了一款在数据处理量、分析速度和易用性上兼备的领先产品。“技术的发展已逐渐演化成一个超链接环境:无数用户跨多种设备地使用着海量应用,这令传统数据库面临巨大挑战,也让我们确信基于云原生的数据服务必是未来。”

除了云原生,元宇宙对算力提出了极高的要求,当前的算力架构已无法满足元宇宙对于低门槛高体验的需求,而边缘计算一定程度上能够推动算力发展,为元宇宙发展扫清障碍。高瓴创投连续投资两轮的分布式边缘云计算公司秒如科技,就是聚焦于分布式边缘云InfraSoftware底层基础设施软件研发,致力于将复杂的边缘IT基础设施简单化、自动化、智能化地提供给全球客户。

发力应用层布局:物理引擎、动作引擎到

在偏向于底层技术的toB应用层,除了图形引擎粒界科技,今年8月,高瓴创投还投资了物理引擎公司Motphys(谋先飞)和元象唯思。

元象唯思瞄准全真互联网,专注于将人工智能、云渲染、视频编解码与大系统工程等前沿技术,引入数字世界生成的过程中,致力于使互联网全面地融入并与现实结合,实现全面地“脱虚向实”。谋先飞则自主研发的物理动作引擎Motphys,拥有将角色动作展示与融合、并模拟真实物理效果等功能。该引擎省去了开发者敲击大量代码的工作,开发团队只需设置好参数,就可通过动作物理引擎将这些效果模拟出来。

既然Metaverse被看作平行宇宙,那么Verse即为最远之境。高瓴迄今为止在元宇宙概念中最重要的一笔投资小冰(原微软小冰)正接近于这一概念。小冰公司以“少女小冰”的形象为人们熟知,这与最近元宇宙中大热的虚拟人概念很接近,但其实公司的核心目标完成“全球交互量最大的人工智能框架”。

按照扎克伯格的说法,元宇宙将是互联网的升级版:一个人们可以“置身其中”而不仅仅是“观看使用”的互联网。从这个意义上说,也可以说元宇宙是下一代互联网,甚至是终极互联网。面对新一代“互联网”的崛起,不同的投资机构和科技巨头也做出了不同的选择。譬如最近正在猛投元宇宙的字节跳动,围绕的就是VR设备、沉浸式社交平台和游戏连连出手。

而高瓴在成立高瓴创投后,硬科技明显成了其主攻方向。据悉在其内部,项目的“含tech量”是决定投资的重要依据。这也不难理解其在元宇宙板块、以底层技术革新作为选择了。

(编辑 张明富)


原文链接:https://baijiahao.baidu.com/s?id=1716654323678083060&wfr=spider&for=pc

收起阅读 »

(转载)入局·元宇宙 | 清华大学沈阳:元宇宙不是游戏,其发展需获得全社会认同

近段时间,元宇宙概念炙手可热。嗅觉最为灵敏的资本市场早已闻风而动,各路产业纷纷将自己嵌套进元宇宙的蓝图,甚至有机构卖“元宇宙网课”已收入百万。那么,元宇宙和我们的生活有什么关系?又会给现实世界带来哪些影响?11月17日,清华大学新媒体研究中心执行主任沈阳在接受...
继续阅读 »

近段时间,元宇宙概念炙手可热。嗅觉最为灵敏的资本市场早已闻风而动,各路产业纷纷将自己嵌套进元宇宙的蓝图,甚至有机构卖“元宇宙网课”已收入百万。那么,元宇宙和我们的生活有什么关系?又会给现实世界带来哪些影响?

11月17日,清华大学新媒体研究中心执行主任沈阳在接受封面新闻专访时表示,元宇宙是移动互联网之后,互联网的下一种形态,将突破当下互联网产业平台形态内卷化的瓶颈。不过目前,元宇宙的产业生态系统还处于亚健康状态,要通过技术创新引领和制度创新推动其健康发展。

截至目前,市场上似乎并未给出元宇宙的准确定义,在沈阳看来,这仍是一个不断发展、演变的概念。他认为,元宇宙是整合多种新技术而产生的新型虚实相融的互联网应用和社会形态, 并且允许每个用户进行内容生产和世界编辑。“总体而言,就是对人生存维度和感官维度的拓展。”

很多影视作品都对元宇宙有过想象。比如在电影《头号玩家》、《黑客帝国》里,呈现了一个戴上眼镜,连上脑机就能脱离现实的数字游戏世界。那元宇宙是更高级的游戏吗?

对此,沈阳给出了否定的答复,他提到,元宇宙可以被理解为在现有大型多人在线游戏的基础上,加上了开放式任务、可编辑世界、XR入口、去中心化认证系统等融合形成的,是虚拟世界和现实世界的交融。

谈及元宇宙会对人类社会带来哪些颠覆性改变,沈阳认为,元宇宙将在物理层面有选择地解放人类天性。比如,超越现实世界中的时空规定,让人跳得更高、跑得更快;同时,元宇宙所具有的庞大地理空间供用户选择和探索。

就像是《头号玩家》中的“绿洲”,看似美好的背后其实也存在利益斗争、不平等和安全危机。“元宇宙也具有新兴产业不成熟、不稳定的特征。”沈阳表示,元宇宙要进一步发展,需面对算力压力、伦理舆论、隐私保护、资本操控以及知识产权等多方面潜在风险。“就像是一个人在VR头盔里面待得越久,可能越发习惯一个人的状态,元宇宙想要健康发展,用户们还需要经历一道心理建设的关卡,从认知走向认同。”


收起阅读 »

Android自定义view,实现电子签名

首先new一个类继承于Viewpublic class SignatureView extends View 自定义view,采用画笔绘制一张图片 定义一个画笔滑动的宽度 还需要对画笔进行跟踪,以便内容区域可以容纳笔划private static final...
继续阅读 »

首先new一个类继承于View

public class SignatureView extends View

自定义view,采用画笔绘制一张图片 定义一个画笔滑动的宽度 还需要对画笔进行跟踪,以便内容区域可以容纳笔划

private static final float STROKE_WIDTH = 5f;
private static final float HALF_STROKE_WIDTH = STROKE_WIDTH / 2;

通过使最小可能区域无效来优化绘制

private Paint paint = new Paint();
private Path path = new Path();

private float lastTouchX;
private float lastTouchY;
private final RectF dirtyRect = new RectF();

构造方法里面初始化画笔

public SignatureView(Context context, AttributeSet attrs) {
super(context, attrs);

paint.setAntiAlias(true);
paint.setColor(Color.BLACK);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeJoin(Paint.Join.ROUND);
paint.setStrokeWidth(STROKE_WIDTH);
}

处理手势,触发画笔路径

@Override
public boolean onTouchEvent(MotionEvent event) {
float eventX = event.getX();
float eventY = event.getY();

switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
path.moveTo(eventX, eventY);
lastTouchX = eventX;
lastTouchY = eventY;
// 现在还没有终点,所以不要浪费周期使其失效。
return true;

case MotionEvent.ACTION_MOVE:
case MotionEvent.ACTION_UP:
//开始跟画笔区域。
resetDirtyRect(eventX, eventY);

// 当硬件跟踪事件的速度快于事件的交付速度时
// 事件将包含这些跳过点的历史记录。
int historySize = event.getHistorySize();
for (int i = 0; i < historySize; i++) {
float historicalX = event.getHistoricalX(i);
float historicalY = event.getHistoricalY(i);
expandDirtyRect(historicalX, historicalY);
path.lineTo(historicalX, historicalY);
}

// 回放历史记录后,将线路连接到触点。
path.lineTo(eventX, eventY);
break;

default:

return false;
}

// 包括一半笔划宽度以避免剪裁
invalidate(
(int) (dirtyRect.left - HALF_STROKE_WIDTH),
(int) (dirtyRect.top - HALF_STROKE_WIDTH),
(int) (dirtyRect.right + HALF_STROKE_WIDTH),
(int) (dirtyRect.bottom + HALF_STROKE_WIDTH));

lastTouchX = eventX;
lastTouchY = eventY;

return true;
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawPath(path, paint);
}

抬起时调用,以确包含所有路径

private void expandDirtyRect(float historicalX, float historicalY) {
if (historicalX < dirtyRect.left) {
dirtyRect.left = historicalX;
} else if (historicalX > dirtyRect.right) {
dirtyRect.right = historicalX;
}
if (historicalY < dirtyRect.top) {
dirtyRect.top = historicalY;
} else if (historicalY > dirtyRect.bottom) {
dirtyRect.bottom = historicalY;
}
}

到这里,手写功能就已经能使用了,接下来是做一些处理

在运动事件发生时重置
lastTouchX和lastTouchY是在动作结束时设置的

private void resetDirtyRect(float eventX, float eventY) {
dirtyRect.left = Math.min(lastTouchX, eventX);
dirtyRect.right = Math.max(lastTouchX, eventX);
dirtyRect.top = Math.min(lastTouchY, eventY);
dirtyRect.bottom = Math.max(lastTouchY, eventY);
}

清除签名

那写错了怎么办,肯定要有清除签名啦

public void clear() {
path.reset();
// 重新绘制整个视图
invalidate();
}

获取图片缓存

public Bitmap getBitmapFromView(){
this.setDrawingCacheEnabled(true); //开启图片缓存
buildDrawingCache(); //构建图片缓存
Bitmap bitmap = Bitmap.createBitmap(getDrawingCache());
setDrawingCacheEnabled(false); //关闭图片缓存
return bitmap;
}

效果图

微信图片_20211115231808.jpg


收起阅读 »

(转载)漫画:什么是“元宇宙”?

什么是更高的自由度呢?或许有人觉得,我们在网络游戏当中,不是也很自由吗?想怎么玩就怎么玩。但是,无论一款网络游戏的元素有多么丰富,游戏当中的角色、任务、职业、道具、场景,都是游戏设计师预先设计好的。就拿大型网络游戏《魔兽世界》来举例,你在里面可以当一名法师,也...
继续阅读 »


什么是更高的自由度呢?或许有人觉得,我们在网络游戏当中,不是也很自由吗?想怎么玩就怎么玩。

但是,无论一款网络游戏的元素有多么丰富,游戏当中的角色、任务、职业、道具、场景,都是游戏设计师预先设计好的。

就拿大型网络游戏《魔兽世界》来举例,你在里面可以当一名法师,也可以当一名猎人,但你要是想当一名“星际战士”,那对不起,游戏里没设计这个职业。

同样的,尽管《魔兽世界》里有着各式各样的武器装备,但你想要锻造出一把“倚天剑”,那也不能,暴雪设计师没听说过这东西。

但是,在元宇宙当中,整个虚拟世界是大家共建的。每一个用户不但可以参与其中,更可以改造世界,创造出全新的元素和规则。

《我的世界》当中,玩家们的作品

如果说,在网络游戏的世界中,游戏设计师是创世的神灵。那么在元宇宙中,不再有一个权威的“神”,所有的用户都是共同参与和改造世界的“人”。

对于这样的模式,业界有一个专门的术语:UGC(User Generated Content),翻译过来就是“用户生产内容”。

网络游戏当中,也有自己的经济系统,有虚拟货币,有各种物品的交易。那么元宇宙当中的经济系统,又有什么特别之处呢?

稀缺性,是经济学的基石。在现实世界里,无论是一个馒头,还是一栋别墅,数量都是有限的。要想做出更多的馒头,建造更多的别墅,就需要更多的材料、能源、人力成本。

但是,在网络游戏当中,一切物品装备都是数字。在理论上,游戏服务商可以把一件物品无限复制,而不需要任何额外的成本。因此,在网络游戏当中,经济系统并非独立的。

但是,在元宇宙当中,每一件物品都是独一无二,不可被大量复制的。并且,这些物品在一切交易过程,都有安全的数字合约保障,使得元宇宙当中的数字资产就像现实里人们的资产一样,稀缺且稳定存在。

稀缺与稳定,是元宇宙独立经济系统的基石。

数字资产“无聊猿猴”系列

元宇宙的实现,都涉及到了哪些技术呢?

首先,是众多互联网产品和游戏当中共通的技术,比如云计算5G通信VR虚拟现实技术等等。这些技术的细节,我们就不过多阐述了。

元宇宙能够实现去中心化,底层依赖的则是区块链技术这十年来的发展进步。在区块链当中,没有一个中心服务器,每一个用户都是一个独立且平等的节点,可以自由提交自己的内容,在达成共识的前提下更新整个区块链。这也是元宇宙的用户们可以改造虚拟世界的基础。

此外,最近流行的NFT,为元宇宙的独立经济系统提供了保障。

NFT,全称Non-Fungible Token,翻译过来叫做非同质化代币。NFT同样是基于区块链实现,与之前的比特币同样属于数字代币。但不同的是,每一个比特币都是同质化的,且可以分割,比如你可以拥有0.01个比特币;而每一个NFT代币都是独一无二且不可分割的,就好比是一幅蒙娜丽莎的原画,或者你家的房子。

在元宇宙当中,每一件物品的生产和交易,都是基于NFT实现,保障了虚拟世界中一切数字物品的稀缺与稳定。

Roblox,中文音译为罗布乐思,是世界最大的多人在线创作游戏。在这个游戏平台上,玩家们既可以参与游戏本身,也可以开发创造自己的游戏。

玩家们创造出的游戏种类,包括射击、格斗、竞速、生存等丰富的类型,充分发挥了所有人的想象力。

2021年3月,Roblox在美股上市,被称为元宇宙第一股,2021年也被称为元宇宙元年。

对于元宇宙这片蓝海,各方大佬也纷纷做出了积极的反应。其中Facebook创始人扎克伯格就是元宇宙的热衷支持者,甚至把公司品牌都更名为“Meta”。

至于国内,腾讯公司投资了炙手可热的项目Roblox,而字节跳动也投资了号称“中国版Roblox”的游戏开发商代码乾坤。

如今,元宇宙成为了多家互联网巨头进行角逐的全新战场。

原文链接:https://juejin.cn/post/7029142002109267976

收起阅读 »

【白话前端】从一个故事说明白“浏览器缓存”

一则小故事 小明常去图书馆借阅英文杂志回家看,由于单词量少,他同时需要借阅一本《英汉词典》; 起初,和图书管理员不熟,每次他都要在图书馆借英文杂志和《英汉词典》,放在书包里背回家;这个过程,暂且将其称为“不缓存”; 后来,小明发现图书管理员竟是妈妈的...
继续阅读 »

一则小故事



小明常去图书馆借阅英文杂志回家看,由于单词量少,他同时需要借阅一本《英汉词典》;




起初,和图书管理员不熟,每次他都要在图书馆借英文杂志和《英汉词典》,放在书包里背回家;这个过程,暂且将其称为“不缓存”;




后来,小明发现图书管理员竟是妈妈的好朋友和好邻居王叔叔,经过相认后,王叔叔对小明说:“你每次都要借阅《英汉词典》,我直接借你一整年,在一年内你可以将它放在家里,不需要每次到图书馆来借阅。”小明听了非常高兴,因为他的书包可以轻上一大截;可以持有《英汉词典》一整年的过程,暂且称为“强缓存”;




再后来,小明发现图书管理员王叔叔经常去家里做客,两人关系也愈发亲密;小明问:“王叔叔,英文杂志的更新总是很不规律,我经常去了图书馆,英文杂志却未更新,我借到的依然是上一期的杂志,有啥办法让我少跑路吗?”




王叔叔笑着说:“这还不简单?每次你准备去借阅之前,先把你手里当前持有的杂志期号(etag)用短信发给我,如果图书馆没有更新,我就给你一个304的暗号,你就还是接着读家里那本;如果有了更新,我给你一个200的暗号,你再来图书馆拿书就行;”这个过程,暂且被称为“协商缓存”



逐渐装逼


不缓存


不缓存是最容易理解的缓存策略,也最不容易出错,只要每一次刷新页面都去服务器取数据即可;但同样的,不缓存意味着页面加载速度会更慢


要设置不缓存也很容易,只需要将资源文件的Response Header中的Cache-Control设为no-store即可;



Cache-Control: no-store



cache-control 属性之一:可缓存性



强缓存


对于已知的几乎不会发生变化的资源,可以通过调整策略,使得浏览器在限定时间内,直接从本地缓存获取,这就是所谓的强缓存;

要配置静态资源的强缓存,通常需要发送的缓存头如下:



Cache-Control:public, max-age=31536000



以下是强缓存常用的两种属性↓;


cache-control 属性之:可缓存性



cache-control 属性之: 到期



协商缓存


其实上面故事里关于协商缓存的描述,有一点是非常不准确的,那就是对于浏览器而言,小明发送给王叔叔的不是所谓的“杂志期号”,而是杂志的散列(hash);而这个hash,自然也是王叔叔(服务器端)告诉小明(客户端)的;


在真实情况下,浏览器的协商缓存要触发,只有两种情况:



1.Cache-Control 的值为 no-cache (不强缓存)



or



2.max-age 过期了 (强缓存,但总有过期的时候)



只有在这两种情况下满足其中至少一种时,才会进入协商缓存的过程;


因此,常规的协商缓存,通常分为以下几步:



step1

浏览器第一次发起请求,request上并没有相应请求头;

(小明第一次去图书馆借书)




step2

服务器第一次返回资源,response上带上了两个属性:
etag: "33a64df"

last-modified: Mon, 12 Dec 2020 12:12:12 GMT

(王叔叔借给小明一本书,并告诉小明这本杂志的编号,以及它的发刊日期)




step3

浏览器第二次发起请求,request上携带了上一次请求返回的内容:

if-none-matched: "33a64df"

if-modified-since: Mon, 12 Dec 2020 12:12:12 GMT

(小明第二次借书,先进行了询问:上一次借我的那本的编号和上一次更改后是否有变动?)




step4

服务器发现资源没有改变,于是返回了304状态码;

浏览器直接在本地读取缓存;

(王叔叔说:还没来新货,你先读着上次借的那本吧)




step5

浏览器第三次发起请求,request上携带了上一次请求返回的内容:

if-none-matched: "33a64df"

if-modified-since: Mon, 12 Dec 2020 12:12:12 GMT

(小明第三次借书,先进行了询问:上一次借我的那本的编号和上一次更改后是否有变动?)




step6

服务器检查之后发现,文件已经发生了变化,于是将新的资源、编号、最后变更时间一起返回给了客户端;并返回了200状态码;
if-none-matched: "sd423dss"

if-modified-since: Mon, 30 Dec 2020 12:12:12 GMT

(王叔叔说:来了来了,最新一期的杂志编号、发刊日期如下,这是杂志本身,也一起给你;)



上面过程展示了一次协商缓存生效的过程;


如何在项目中使用?


正常来说,一个前端单页应用(SPA)的项目结构大概如下:


├─favicon.ico
├─index.html

├─css
│ └───app.fb0c6e1c.css

├─img
│ └───logo.82b9c7a5.png

└─js
├───app.febf7357.js
└───chunk-vendors.5a5a5781.js

从命名上可以发现,文件大概分两类:



  1. index.html & favicon.ico 都属于固定命名,通常情况下名称不会再发生改变;

  2. css/js/image/ttf 等文件,则通常会以 {name}.{hash}.{suffix}的方式进行命名;


name-with-hash.png


当文件发生变化时,其命名规则,可天然保证文件hash跟着发生变化,从而保证文件的路径发生变化;


因此,针对以上场景,通常情况下可以按以下方式制定缓存策略



  1. index.html 和 favicon.ico 设置为“不缓存”或者“协商缓存”(必要不大);

  2. 名称中带hash的文件(如css/js/image/ttf),可以直接使用“强缓存”策略

作者:春哥的梦想是摸鱼
链接:https://juejin.cn/post/7030781324650610695

收起阅读 »

2021 年你需要知道的 CSS 工程化技术

目前整个 CSS 工具链、工程化领域的主要方案如下: 而我们技术选型的标准如下: 开发速度快 开发体验友好 调试体验友好 可维护性友好 扩展性友好 可协作性友好 体积小 有最佳实践指导 目前主要需要对比的三套方案: Less/Sass + PostCS...
继续阅读 »

目前整个 CSS 工具链、工程化领域的主要方案如下:


image.png


而我们技术选型的标准如下:



  • 开发速度快

  • 开发体验友好

  • 调试体验友好

  • 可维护性友好

  • 扩展性友好

  • 可协作性友好

  • 体积小

  • 有最佳实践指导


目前主要需要对比的三套方案:



  • Less/Sass + PostCSS 的纯 CSS c侧方案

  • styled-components / emotion 的纯 CSS-in-JS 侧方案

  • TailwindCSS 的以写辅助类为主的 HTML 侧方案


纯 CSS 侧方案


介绍与优点




维护状态:一般




Star 数:16.7K




支持框架:无框架限制




项目地址:github.com/less/less.j…



Less/Sass + PostCSS 这种方案在目前主流的组件库和企业级项目中使用很广,如 ant-design 等


它们的主要作用如下:



  • 为 CSS 添加了类似 JS 的特性,你也可以使用变量、mixin,写判断等

  • 引入了模块化的概念,可以在一个 less 文件中导入另外一个 less 文件进行使用

  • 兼容标准,可以快速使用 CSS 新特性,兼容浏览器 CSS 差异等


这类工具能够与主流的工程化工具一起使用,如 Webpack,提供对应的 loader 如 sass-loader,然后就可以在 React/Vue 项目中建 .scss 文件,写 sass 语法,并导入到 React 组件中生效。


比如我写一个组件在响应式各个断点下的展示情况的 sass 代码:


.component {

width: 300px;

@media (min-width: 768px) {

width: 600px;

@media (min-resolution: 192dpi) {

background-image: url(/img/retina2x.png);

}

}

@media (min-width: 1280px) {

width: 800px;

}

}

或导入一些用于标准化浏览器差异的代码:


@import "normalize.css"; 



// component 相关的其他代码

不足


这类方案的一个主要问题就是,只是对 CSS 本身进行了增强,但是在帮助开发者如何写更好的 CSS、更高效、可维护的 CSS 方面并没有提供任何建议。



  • 你依然需要自己定义 CSS 类、id,并且思考如何去用这些类、id 进行组合去描述 HTML 的样式

  • 你依然可能会写很多冗余的 Less/Sass 代码,然后造成项目的负担,在可维护性方面也有巨大问题


优化



  • 可以引入 CSS 设计规范:BEM 规范,来辅助用户在整个网页的 HTML 骨架以及对应的类上进行设计

  • 可以引入 CSS Modules,将 CSS 文件进行 “作用域” 限制,确保在之后维护时,修改一个内容不会引起全局中其他样式的效果


BEM 规范


B (Block)、E(Element)、M(Modifier),具体就是通过块、元素、行为来定义所有的可视化功能。


拿设计一个 Button 为例:


/* Block */

.btn {}



/* 依赖于 Block 的 Element */

.btn__price {}



/* 修改 Block 风格的 Modifier */

.btn--orange {}

.btn--big {}

遵循上述规范的一个真实的 Button:


<a href="#">

<span>$3</span>

<span>BIG BUTTON</span>

</a>

可以获得如下的效果:



CSS Modules


CSS Modules 主要为 CSS 添加局部作用域和模块依赖,使得 CSS 也能具有组件化。


一个例子如下:


import React from 'react';

import style from './App.css';



export default () => {

return (

<h1 className={style.title}>

Hello World

</h1>

);

};

.title {

composes: className;

color: red;

}

上述经过编译会变成如下 hash 字符串:


<h1>

Hello World

</h1>

._3zyde4l1yATCOkgn-DBWEL {

color: red;

}

CSS Modules 可以与普通 CSS、Less、Sass 等结合使用。


纯 JS 侧方案


介绍与优点




维护状态:一般




Star 数:35.2K




支持框架:React ,通过社区支持 Vue 等框架




项目地址:github.com/styled-comp…



使用 JS 的模板字符串函数,在 JS 里面写 CSS 代码,这带来了两个认知的改变:



  • 不是在根据 HTML,然后去写 CSS,而是站在组件设计的角度,为组件写 CSS,然后应用组件的组合思想搭建大应用

  • 自动提供类似 CSS Modules 的体验,不用担心样式的全局污染问题


同时带来了很多 JS 侧才有的各种功能特性,可以让开发者用开发 JS 的方式开发 CSS,如编辑器自动补全、Lint、编译压缩等。


比如我写一个按钮:


const Button = styled.button`

/* Adapt the colors based on primary prop */

background: ${props => props.primary ? "palevioletred" : "white"};

color: ${props => props.primary ? "white" : "palevioletred"};



font-size: 1em;

margin: 1em;

padding: 0.25em 1em;

border: 2px solid palevioletred;

border-radius: 3px;

`;



render(

<div>

<Button>Normal</Button>

<Button primary>Primary</Button>

</div>

);

可以获得如下效果:



还可以扩展样式:


// The Button from the last section without the interpolations

const Button = styled.button`

color: palevioletred;

font-size: 1em;

margin: 1em;

padding: 0.25em 1em;

border: 2px solid palevioletred;

border-radius: 3px;

`;



// A new component based on Button, but with some override styles

const TomatoButton = styled(Button)`

color: tomato;

border-color: tomato;

`;



render(

<div>

<Button>Normal Button</Button>

<TomatoButton>Tomato Button</TomatoButton>

</div>

);

可以获得如下效果:



不足


虽然这类方案提供了在 JS 中写 CSS,充分利用 JS 的插值、组合等特性,然后应用 React 组件等组合思想,将组件与 CSS 进行细粒度绑定,让 CSS 跟随着组件一同进行组件化开发,同时提供和组件类似的模块化特性,相比 Less/Sass 这一套,可以复用 JS 社区的最佳实践等。


但是它仍然有一些不足:



  • 仍然是是对 CSS 增强,提供非常大的灵活性,开发者仍然需要考虑如何去组织自己的 CSS

  • 没有给出一套 “有观点” 的最佳实践做法

  • 在上层也缺乏基于 styled-components 进行复用的物料库可进行参考设计和使用,导致在初始化使用时开发速度较低

  • 在 JS 中写 CSS,势必带来一些本属于 JS 的限制,如 TS 下,需要对 Styled 的组件进行类型注释

  • 官方维护的内容只兼容 React 框架,Vue 和其他框架都由社区提供支持


整体来说不太符合团队协作使用,需要人为总结最佳实践和规范等。


优化



  • 寻求一套写 CSS 的最佳实践和团队协作规范

  • 能够拥有大量的物料库或辅助类等,提高开发效率,快速完成应用开发


偏向 HTML 侧方案


介绍与优点




维护状态:积极




Star 数:48.9K




支持框架:React、Vue、Svelte 等主流框架




项目地址:github.com/tailwindlab…



典型的是 TailwindCSS,一个辅助类优先的 CSS 框架,提供如 flexpt-4text-centerrotate-90 这样实用的类名,然后基于这些底层的辅助类向上组合构建任何网站,而且只需要专注于为 HTML 设置类名即可。


一个比较形象的例子可以参考如下代码:


<button>Decline</button>

<button>Accept</button>

上述代码应用 BEM 风格的类名设计,然后设计两个按钮,而这两个类名类似主流组件库里面的 Button 的不同状态的设计,而这两个类又是由更加基础的 TailwindCSS 辅助类组成:


.btn {

@apply text-base font-medium rounded-lg p-3;

}



.btn--primary {

@apply bg-rose-500 text-white;

}



.btn--secondary {

@apply bg-gray-100 text-black;

}

上面的辅助类包含以下几类:



  • 设置文本相关: text-basefont-mediumtext-whitetext-black

  • 设置背景相关的:bg-rose-500bg-gray-100

  • 设置间距相关的:p-3

  • 设置边角相关的:rounded-lg


通过 Tailwind 提供的 @apply 方法来对这些辅助类进行组合构建更上层的样式类。


上述的最终效果展示如下:



可以看到 TailwindCSS 将我们开发网站的过程抽象成为使用 Figma 等设计软件设计界面的过程,同时提供了一套用于设计的规范,相当于内置最佳实践,如颜色、阴影、字体相关的内容,一个很形象的图片可以说明这一点:



TailwindCSS 为我们规划了一个元素可以设置的属性,并且为每个属性给定了一组可以设置的值,这些属性+属性值组合成一个有机的设计系统,非常便于团队协作与共识,让我们开发网站就像做设计一样简单、快速,但是整体风格又能保持一致。


TailwindCSS 同时也能与主流组件库如 React、Vue、Svelte 结合,融入基于组件的 CSS 设计思想,但又只需要修改 HTML 上的类名,如我们设计一个食谱组件:


// Recipes.js

import Nav from './Nav.js'

import NavItem from './NavItem.js'

import List from './List.js'

import ListItem from './ListItem.js'



export default function Recipes({ recipes }) {

return (

<div className="divide-y divide-gray-100">

<Nav>

<NavItem href="/featured" isActive>Featured</NavItem>

<NavItem href="/popular">Popular</NavItem>

<NavItem href="/recent">Recent</NavItem>

</Nav>

<List>

{recipes.map((recipe) => (

<ListItem key={recipe.id} recipe={recipe} />

))}

</List>

</div>

)

}



// Nav.js

export default function Nav({ children }) {

return (

<nav className="p-4">

<ul className="flex space-x-2">

{children}

</ul>

</nav>

)

}



// NavItem.js

export default function NavItem({ href, isActive, children }) {

return (

<li>

<a

href={href}

className={`block px-4 py-2 rounded-md ${isActive ? 'bg-amber-100 text-amber-700' : ''}`}

>

{children}

</a>

</li>

)

}



// List.js

export default function List({ children }) {

return (

<ul className="divide-y divide-gray-100">

{children}

</ul>

)

}



//ListItem.js

export default function ListItem({ recipe }) {

return (

<article className="p-4 flex space-x-4">

<img src={recipe.image} alt="" className="flex-none w-18 h-18 rounded-lg object-cover bg-gray-100" width="144" height="144" />

<div className="min-w-0 relative flex-auto sm:pr-20 lg:pr-0 xl:pr-20">

<h2 className="text-lg font-semibold text-black mb-0.5">

{recipe.title}

</h2>

<dl className="flex flex-wrap text-sm font-medium whitespace-pre">

<div>

<dt className="sr-only">Time</dt>

<dd>

<abbr title={`${recipe.time} minutes`}>{recipe.time}m</abbr>

</dd>

</div>

<div>

<dt className="sr-only">Difficulty</dt>

<dd> · {recipe.difficulty}</dd>

</div>

<div>

<dt className="sr-only">Servings</dt>

<dd> · {recipe.servings} servings</dd>

</div>

<div className="flex-none w-full mt-0.5 font-normal">

<dt className="inline">By</dt>{' '}

<dd className="inline text-black">{recipe.author}</dd>

</div>

<div>

<dt className="text-amber-500">

<span className="sr-only">Rating</span>

<svg width="16" height="20" fill="currentColor">

<path d="M7.05 3.691c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.372 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.539 1.118l-2.8-2.034a1 1 0 00-1.176 0l-2.8 2.034c-.783.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.363-1.118L.98 9.483c-.784-.57-.381-1.81.587-1.81H5.03a1 1 0 00.95-.69L7.05 3.69z" />

</svg>

</dt>

<dd>{recipe.rating}</dd>

</div>

</dl>

</div>

</article>

)

}

上述食谱的效果如下:



可以看到我们无需写一行 CSS,而是在 HTML 里面应用各种辅助类,结合 React 的组件化设计,既可以轻松完成一个非常现代化且好看的食谱组件。


除了上面的特性,TailwindCSS 在响应式、新特性支持、Dark Mode、自定义配置、自定义新的辅助类、IDE 方面也提供非常优秀的支持,除此之外还有基于 TailwindCSS 构建的物料库 Tailwind UI ,提供各种各样成熟、好看、可用于生产的物料库:



因为需要自定的 CSS 不多,而需要自定义的 CSS 可以定义为可复用的辅助类,所以在可维护性方面也是极好的。


不足



  • 因为要引入一个额外的运行时,TailwindCSS 辅助类到 CSS 的编译过程,而随着组件越来越多,需要编译的工作量也会变大,所以速度会有影响

  • 过于底层,相当于给了用于设计的最基础的指标,但是如果我们想要快速设计网站,那么可能还需要一致的、更加上层的组件库

  • 相当于引入了一套框架,具有一定的学习成本和使用成本


优化



  • Tailwind 2.0 支持 JIT,可以大大提升编译速度,可以考虑引入

  • 基于 TailwindCSS,设计一套符合自身风格的上层组件库、物料库,便于更加快速开发

  • 提前探索、学习和总结一套教程与开发最佳实践

  • 探索 styled-components 等结合 TailwindCSS 的开发方式



作者:程序员巴士
链接:https://juejin.cn/post/7030790310590447630

收起阅读 »

如何在TS里使用命名空间,来组织你的代码

前言 关于命名空间,官方有个说明,大概是这么个意思: 为了与ECMAScript 2015里的术语保持一致,从TypeScript 1.5开始,“外部模块”称为“模块”,而“内部模块”称做“命名空间”。 为了避免新的使用者被相似的名称所迷惑,建议: 任何使用...
继续阅读 »

前言


关于命名空间,官方有个说明,大概是这么个意思:


为了与ECMAScript 2015里的术语保持一致,从TypeScript 1.5开始,“外部模块”称为“模块”,而“内部模块”称做“命名空间”。


为了避免新的使用者被相似的名称所迷惑,建议:



任何使用 module关键字来声明一个内部模块的地方都应该使用namespace关键字来替换



具体的使用下面会讲到


使用命名空间


使用命名空间的方式,其实非常简单,格式如下:


namespace X {}

具体的使用可以看看下面这个例子(例子来源TS官方文档)


我们定义几个简单的字符串验证器,假设会使用它们来验证表单里的用户输入或验证外部数据


interface StringValidator {
isAcceptable(s: string): boolean;
}

let lettersRegexp = /^[A-Za-z]+$/;
let numberRegexp = /^[0-9]+$/;

class LettersOnlyValidator implements StringValidator {
isAcceptable(s: string) {
return lettersRegexp.test(s);
}
}

class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}

// Some samples to try
let strings = ["Hello", "98052", "101"];

// Validators to use
let validators: { [s: string]: StringValidator; } = {};
validators["ZIP code"] = new ZipCodeValidator();
validators["Letters only"] = new LettersOnlyValidator();

// Show whether each string passed each validator
for (let s of strings) {
for (let name in validators) {
let isMatch = validators[name].isAcceptable(s);
console.log(`'${ s }' ${ isMatch ? "matches" : "does not match" } '${ name }'.`);
}
}

现在我们是把所有的验证器都放在一个文件里


但是,随着更多验证器的加入,我们可能会担心与其它对象产生命名冲突。因此我们使用命名空间来组织我们的代码


如下使用命名空间:


namespace Validation {
export interface StringValidator {
isAcceptable(s: string): boolean;
}

const lettersRegexp = /^[A-Za-z]+$/;
const numberRegexp = /^[0-9]+$/;

export class LettersOnlyValidator implements StringValidator {
isAcceptable(s: string) {
return lettersRegexp.test(s);
}
}

export class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
}

// Some samples to try
let strings = ["Hello", "98052", "101"];

// Validators to use
let validators: { [s: string]: Validation.StringValidator; } = {};
validators["ZIP code"] = new Validation.ZipCodeValidator();
validators["Letters only"] = new Validation.LettersOnlyValidator();

// Show whether each string passed each validator
for (let s of strings) {
for (let name in validators) {
console.log(`"${ s }" - ${ validators[name].isAcceptable(s) ? "matches" : "does not match" } ${ name }`);
}
}

如上代码,把所有与验证器相关的类型都放到一个叫做Validation的命名空间里。 因为我们想让这些接口和类在命名空间之外也是可访问的,所以需要使用 export。 相反的,变量 lettersRegexpnumberRegexp是实现的细节,不需要导出,因此它们在命名空间外是不能访问的


有个问题是,如果只是一个文件,当应用变得越来越大的时候,会变得难以维护,因此我们根据需要,可选的将单文件分离到不同的文件中


下节我们会继续讲到这个问题,关于多文件的命名空间,并且我们会将上例中的单文件分割成多个文件。欢迎关注


END


以上就是本文的所有内容,如有问题,欢迎指正~


作者:LBJ
链接:https://juejin.cn/post/7031021973966684191

收起阅读 »

5 个让 Swift 更优雅的扩展——Pt.1

这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战引言作为开发者,应该编写具有高可维护性和可扩展性的代码。我们可以通过扩展原有的功能,写出更易读,更简洁的代码。下面就介绍 5 个日常开发中非常实用的扩展。1. 自定义下标来安全访问数组我想...
继续阅读 »

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


引言

作为开发者,应该编写具有高可维护性和可扩展性的代码。我们可以通过扩展原有的功能,写出更易读,更简洁的代码。

下面就介绍 5 个日常开发中非常实用的扩展。

1. 自定义下标来安全访问数组

我想每个开发人员都至少经历过一次index-out-of-bounds的报错。就是数组越界,这个大家都懂,就不过多介绍了。下面是个数组越界的例子:

let values = ["A", "B", "C"]
values[0] // A
values[1] // B
values[2] // C
values[3] // Fatal error: Index out of range

既然是下标超过了数组的大小,那我们在取值之前,先检查下标是否超过数组大小。让我们来看下面的几种方案:

  • 通过 if 来判断下标
if 2 < values.count {
values[2] // "C"
}
if 3 < values.count {
values[3] // 不会走到这里
}

虽然也可以,但显的就很重复繁琐,每次取值之前都要判断一遍下标。

  • 定义公共函数

既然每次都要检查下标,那就把检查下标的逻辑放在一个函数里

func getValue<T>(in elements: [T], at index: Int) -> T? {
guard index >= 0 && index < elements.count else {
return nil
}
return elements[index]
}

let values = ["A", "B", "C"]
getValue(in: values, at: 2) // "C"
getValue(in: values, at: 3) // nil

不仅使用泛型支持了任何类型的元素,当数组越界时,还很贴心的返回了 nil,防止崩溃。

虽然很贴心,但每次取值都要把原数组传进去,显的就很冗余。

  • extension

既然每次都要传入数组很冗余,那就把数组的参数给去掉。我们知道 Swift 一个很强大的特性就是 extension,我们给 Array定义个 extension,并把这个函数添加进去。

extension Array {
func getValue(at index: Int) -> Element? {
guard index >= 0 && index < self.count else {
return nil
}
return self[index]
}
}

let values = ["A", "B", "C"]
values.getValue(at: 2) // "C"
values.getValue(at: 3) // nil

  • subscript

虽然看起来好很多了,但可不可以像原生的取值一样, 一个[]就搞定了呢?of course!

extension Array {
subscript (safe index: Int) -> Element? {
guard index >= 0 && index < self.count else {
return nil
}
return self[index]
}
}

values[safe: 2] // "C"
values[safe: 3] // nil

自定义的[safe: 2]和原生的 [2]非常的接近了。但自定义的提供了数据越界保护机制。

  • 应用到 Collection

既然这么棒,岂能数组一人独享,我们把它应用到所有 Collection 协议。看起来是不是很优雅~😉

extension Collection {
public subscript (safe index: Self.Index) -> Iterator.Element? {
(startIndex ..< endIndex).contains(index) ? self[index] : nil
}
}


2. 平等的处理 nil 和空字符串

在处理可选值时,我们通常需要将它们与 nil 进行比较进行空检查。当为 nil 时,我们会提供一个默认值让程序继续执行。比如下面这个例子:

func unwrap(value: String?) -> String {
return value ?? "default value"
}

unwrap(value: "foo") // foo
unwrap(value: nil) // default value

但是还有种情况就是空字符串,有时,我们需要把空字符串当做 nil 的情况来处理。此时,不仅要坚持 nil,还要检查空字符串的情况

func unwrap(value: String?) -> String {
let defaultValue = "default value"
guard let value = value else {
return defaultValue
}
if value.isEmpty {
return defaultValue
}
return value
}

unwrap(value: "foo") // foo
unwrap(value: "") // default value
unwrap(value: nil) // default value

虽然也能解决问题,但依然看起来很臃肿,我们把他简化一下:

func unwrapCompressed(value val: String?) -> String {
return val != nil && !val!.isEmpty ? val! : "default value"
}

unwrapCompressed(value: "foo") // foo
unwrapCompressed(value: "") // default value
unwrapCompressed(value: nil) // default value

虽然简化了很多,但不易读,可维护性略差。

可以把空字符串先转化为 nil,再进行处理,这样就和处理 nil 的情况一致了。

public extension String {
var nilIfEmpty: String? {
self.isEmpty ? nil : self
}
}

let foo: String? = nil

if let value = foo?.nilIfEmpty {
print(value) //不会调用
}

if let value = "".nilIfEmpty {
print(value) //不会调用
}

if let value = "ABC".nilIfEmpty {
print(value) //ABC
}


总结

这里先介绍 5 个常用扩展中的其中 2 个,剩下 3 个且听下回分解啦~

  • 给集合增加扩展,防止取值越界造成崩溃
  • 给字符串增加扩展,让空字符串变为 nil

如果觉得对你有帮助,不妨在项目中试试吧~

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

收起阅读 »

iOS App - 从编译到运行

iOS
在iOS开发中,app是被直接编译成机器码后在CPU上运行的,而不是使用解释器编译成字节码再运行。从app的编译到运行的过程中,要经过编译、链接、启动几个步骤。而在iOS中,编译阶段分为前端和后端,前端使用Apple开发的Clang,后端使用LLVM。 编译...
继续阅读 »

在iOS开发中,app是被直接编译成机器码后在CPU上运行的,而不是使用解释器编译成字节码再运行。从app的编译到运行的过程中,要经过编译、链接、启动几个步骤。而在iOS中,编译阶段分为前端和后端,前端使用Apple开发的Clang,后端使用LLVM。



编译


编译过程


编译过程主要有



  • 预处理

  • 词法分析

  • 语法分析

  • 静态分析

  • 中间代码生成

  • 汇编生成

  • 链接生成可执行文件


预处理


在预处理的阶段中,编译器Clang首先预处理我们代码,做一些比如将宏替换到代码中、删除注释、处理预编译命令等工作


词法分析


在此阶段词法分析器读入预处理过的代码字节流,将其中的字符处理成有意义的词素序列,对于每个词素产生词法单元并标记位置,处理完成后进入下一步。这个过程主要是为了在下一步生成语法树做基础工作。


语法分析


这一步中使用在词法分析中生成的词法单元,抽象生成一个语法树(AST,Abstract syntax tree)。抽象语法树上的每个节点也标记了它在源代码的位置。抽象语法树的遍历比起源代码块很多,这一步主要是为了后面的静态分析。
抽象语法树AST


静态分析 | 中间代码生成


将源代码转化为抽象语法树后,编译器就可以遍历整个树来做静态分析。**常见的类型检查、语法错误、方法未定义等都是在静态分析中发现并处理的,当然静态分析能做的事情还有非常多。**在静态分析结束后,编译器会生成IR。IR是整个编译链接系统的中间产物,是一种比较接近机器码的形式,但他与平台无关,通过IR可以生成多个平台的机器码。IR是在iOS编译系统中,前端Clang和后端LLVM的分界点。Clang的任务在生成IR后结束,将IR交付给LLVM后LLVM开始工作。


汇编生成


在获得到IR后,LLVM可以根据优化策略对IR进行一些优化,如尾递归优化、循环优化、全局变量优化。在优化完成后,LLVM会调用汇编生成器将IR转化成汇编代码。此时,生成产物就是.o文件了(二进制文件)。
在生成二进制文件后,我们可以通过二进制重排的方式对我们的编译产物进行更进一步的优化,已达到缩小编译产物大小、优化启动速度等目的


链接


在将源代码编译成.o文件后,就开始链接。链接其实就是一个打包的过程,将编译出的所有.o文件和一些如dylib,.a,tbd文件链接起来,一起合并生成一个Mach-o文件。到这里,编译过程全部结束,可执行文件mach-o已生成。在链接前,符号是未跟内存地址、寄存器绑定的,尤其是一些被定义在其他模块的符号。而在链接阶段,链接器完成了上述工作,进行了除动态库符号外的符号绑定,同时将这些目标文件链接成一个可执行文件


Mach-o文件结构


-w313



  • Header

    • Header 包含该二进制文件的一般信息 字节顺序、架构类型、加载指令的数量等。 使得可以快速确认一些信息,比如当前文件用于32位还是64位,对应的处理器是什么、文件类型是什么



  • Load Commands

    • 是一张包含很多内容的表。内容包括区域的位置、符号表、动态符号表等。这一段紧跟Header,加载Mach-O文件时会使用这里的数据来确定内存的分布



  • Data

    • Data 通常是对象文件中最大的部分,包含Segement的具体数据,如静态C字符串,带参数/不带参数的OC方法,带参数/不带参数的C函数。当运行一个可执行文件时,虚拟内存 (virtual memory) 系统将 segment 映射到进程的地址空间上。

    • Segment __PAGEZERO 规定进程地址空间的前多少空间不可读写

    • Segment __TEXT 包含可执行的二进制代码

    • Segment __DATA 包含了将被更改的数据

    • Segment __LINKEDIT 包含了方法和变量的元数据,代码签名等信息。




静态链接


编译主要分为静态链接动态链接。在编译器阶段进行的是静态链接,也就是在上文中提到的过程。这一阶段是将在前面生成的各种目标文件和各种库(or module in swift)链接起来,生成一个可执行文件mach-o。




运行


装载


一个程序从可执行文件到运行,基本都要经过装载和动态库链接两个阶段。由于在可执行文件生成前已经完成了静态库链接,所以在装载时所有的源代码和静态库已经完成了装载,而动态库链接则需要下文提到的动态链接来完成。


可执行文件,或者说程序,是一个静态的概念,而进程是一个动态的概念。每个程序在运行起来后,他对应的进程都会拥有独立的地址空间,而这个地址空间是由计算机硬件(CPU的位数)决定的,当然,进程只是以为自己拥有计算机整个的地址空间,实际上他是与其他的进程共享计算机的内存(虚拟化)


装载,就是把硬盘上的可执行文件映射到虚拟内存上的过程。


装载的过程,也可以当作是进程建立的过程,一般来说有以下几个步骤。



  • 创建一个独立的虚拟地址空间

  • 读取可执行文件头,建立虚拟地址空间与可执行文件之间的映射关系。(将可执行文件中的相对地址与虚拟地址空间的地址进行绑定)

  • 将CPU的指令寄存器设为可执行文件的入口地址,交与CPU启动运行


动态链接


静态链接是链接静态库,需要链接进Mach-o文件中,如果需要更新就需要重新编译一次,所以无法动态更新和加载。而动态链接是使用dyld动态加载动态库,可以实现动态地加载和更新。并且其他的进程、框架链接的都是同一个动态库,节省了内存。


iOS中我们常用的一些如UIKitFoundation等框架都是使用动态链接的,而为了节省内存,系统将这些库放在动态库共享缓存区(Dyld shared cache)


mach-o文件中,属于动态库的符号会被标记为未定义,但他们的名字与路径会被记录下来。在运行时dyld会通过dlopendlsym导入动态库,并通过记录的路径找到对应的动态库,通过记录的名字找到对应的地址,进行符号与地址的绑定。


dlopen会将动态库映射到进程的虚拟地址空间中,由于载入的动态库中可能也会存在未定义的符号,也就是说该动态库还依赖了其他的动态库,这时会触发更多的动态库被载入,但dlopen可以决定是立刻载入这些依赖库还是延后载入。


dlopen打开动态库后返回的是引用的指针,dlsym的作用就是通过dlopen返回的动态库指针和函数符号,得到函数的地址然后使用。


动态链接解决了静态链接内存占用过多只要有库修改就要重新编译打包的缺点,但同时也引入了新的问题。



  • 结构复杂,动态链接将重定位推迟到运行时进行。

  • 引入了安全问题,这也是我们能够进行PLT HOOK的基础

  • 性能问题


而提到动态库链接,在iOS领域就必须提到我们的dyld


dyld - Dynamic Link Editor



dyld是苹果开发的动态链接器,是苹果系统的一个重要组成部分。它负责mach-o文件的动态库链接和程序的启动。相关代码已开源




  • 启动流程


main方法前的调用栈


启动工程,在_objc_init处设置一个symbolic breakpoint,Xcode会帮我们在main方法执行前设置断点。进入lldb后使用bt命令,我们就可以看到_objc_init方法前的调用栈。


可以看到,dyld是最先被启动的。_dyld_start后,首先调用的是dyldbootstrap命名空间里的start函数,dyld:bootstrap意义为dyld进行自举工作。由于动态链接器本身也是一个共享对象,那么它自己也需要重定向工作。那么为了避免循环重定向的问题,动态链接器相对于其他的共享对象需要有一些特性。第一个就是它不可以依赖于其他的共享对象,第二个是它的重定向工作可以由自己完成。这种具有一定限制条件的启动代码称为自举(bootstrap)


由于dyld比较复杂,在这里就先不详细展开,留待另一篇文章中细讲。启动的大体流程为



  • dyld 开始将程序二进制文件初始化

  • 交由 ImageLoader 读取image,其中包含了我们的类、方法等各种符号

  • 由于 runtime 向 dyld 绑定了回调,当image 加载到内存后,dyld会通知runtime进行处理

  • runtime接手后调用map_images做解析和处理,接下来load_images中调用call_load_methods方法,遍历所有加载进来的Class,按继承层级依次调用Class的 +load 方法和其 Category 的 +load 方法


所以动态链接器的工作流程为



  1. 动态链接器自举 (动态链接器的地址在可执行文件的.interp段) ->

  2. 装载共享对象(在这个步骤合并生成全局符号表)->

  3. 重定位(遍历可执行文件和每个共享对象的重定位表将GOT/PLT中需要重定位的位置进行修正)->

  4. 初始化(执行共享对象.init段中的代码,进程的.init段由程序初始化代码执行)->

  5. 将控制权交还给程序的入口


写在最后


在写这篇的过程中系统地学习了一下app从编译到运行的过程。在编译阶段,静态链接动态链接这种编译原理相关的知识很重要,有时间可以读一下编译原理那本书。运行阶段,dyld在main函数执行前做了非常多工作,其实现也很复杂,待仔细学习后再写一篇聚焦于dyld的笔记。


链接:https://juejin.cn/post/7030435738944536607
来源:稀土掘金
收起阅读 »