注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

快速使用MQTT Flutter版 SDK实现消息收发

1. 前提条件1.1 部署Flutter开发环境配置好Flutter开发环境。1.2 导入项目依赖在yaml文件里配置mqtt_client: ^9.8.1 在iOS开发中需要增加一下代码到位于ios/Runner/Info.plist中的Info.plist...
继续阅读 »

1. 前提条件

1.1 部署Flutter开发环境

配置好Flutter开发环境。

1.2 导入项目依赖

在yaml文件里配置

mqtt_client: ^9.8.1

在iOS开发中需要增加一下代码到位于ios/Runner/Info.plist中的Info.plist*文件中:


<key>NSLocalNetworkUsageDescription</key>
<string>Looking for local tcp Bonjour service</string>
<key>NSBonjourServices</key>
<array>
<string>mqtt.tcp</string>
</array>

Android 

Android AndroidManifest.xml 增加如下代码


<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

2. 实现流程

2.1 获取初始化信息

登录console后台
1.点击菜单栏【MQTT】→【服务概览】→【服务配置】,获取「连接地址」、「连接端口」、「AppID」以「及REST API地址」等信息。
注:clientID由两部分组成,组织形式为“deviceID@AppID”,deviceID由用户自定义,AppID见【服务配置】。
示例:正确的clientID格式为:“device001@aitbj0”;
2.点击菜单栏【应用概览】→【应用详情】→【开发者ID】,获取「Client ID」与「ClientSecret」信息。
3.初始化代码


static const String restapi= "https://api.cn1.mqtt.chat/app/$appID/"; //环信MQTT REST API地址 通过console后台[MQTT]->[服务概览]->[服务配置]下[REST API地址]获取

static const String endpoint= "**"; //环信MQTT服务器地址 通过console后台[MQTT]->[服务概览]->[服务配置]下[连接地址]获取
static const int port = **; // 协议服务端口 通过console后台[MQTT]->[服务概览]->[服务配置]下[连接端口]获取
static const String appID= "**"; // appID 通过console后台[MQTT]->[服务概览]->[服务配置]下[AppID]获取
static late String deviceId ;// 自定义deviceID
static late String clientID ;// deviceId + '@' + appID
static const String appClientId= "**"; //开发者ID 通过console后台[应用概览]->[应用详情]->[开发者ID]下[ Client ID]获取
static const String appClientSecret= "**"; // 开发者密钥 通过console后台[应用概览]->[应用详情]->[开发者ID]下[ ClientSecret]获取

static void init() async{
deviceId = "deviceId";
clientID = "$deviceId@$appID";
}

 

2.2 获取token

  • 首先获取App Token
Dio dio = Dio();
Response<Map<String,dynamic>> data = await dio.post("${restapi}openapi/rm/app/token",data: {"appClientId": appClientId, "appClientSecret": appClientSecret});
var token = (data.data!["body"] as Map<String, dynamic> )["access_token"];
  • 然后根据App Token获取User Token,User Token作为连接服务的密码
Response<Map<String,dynamic>> data2 = await dio.post("${restapi}openapi/rm/user/token",data: {"username": "username", "cid": clientID},options: Options(headers:  <String, dynamic>{r'Authorization': token}));
var mqtttoken = (data2.data!["body"] as Map<String, dynamic> )["access_token"];

2.3 连接服务器

创建MqttAndroidClient对象,并配置连接密码、cleansession标志、心跳间隔、超时时间等信息,调用connect()函数连接至环信MQTT消息云。

var client = MqttServerClient.withPort(endpoint, clientID, port);
/// 是否打印mqtt日志信息
client.logging(on: true);
/// 设置协议版本,默认是3.1,根据服务器需要的版本来设置
/// _client.setProtocolV31();
client.setProtocolV311();
/// 保持连接ping-pong周期。默认不设置时关闭。
client.keepAlivePeriod = 60;
/// 设置自动重连
client.doAutoReconnect();
/// 设置超时时间,单位:毫秒
client.connectTimeoutPeriod = 60000;
/// 连接成功回调
client.onConnected = _onConnected;
/// 连接断开回调
client.onDisconnected = _onDisconnected;
/// 取消订阅回调
client.onUnsubscribed = _onUnsubscribed;
/// 订阅成功回调
client.onSubscribed = _onSubscribed;
/// 订阅失败回调
client.onSubscribeFail = _onSubscribeFail;
/// ping pong响应回调
client.pongCallback = _pong;
client.connect(username,mqtt_token);



static void _onConnected() {
LogManager.log.d("连接成功....");
_initTopic();
}

static void _onDisconnected() {
LogManager.log.d("连接断开");
}

static void _onUnsubscribed(String? topic) {
LogManager.log.d("取消订阅 $topic");
}

static void _onSubscribed(String topic) {
LogManager.log.d("订阅 $topic 成功");
}

static void _onSubscribeFail(String topic) {
LogManager.log.e("订阅主题: $topic 失败");
}

static void _pong() {
LogManager.log.d("Ping的响应");
}


2.4 订阅(subscribe)

2.4.1 订阅主题
当客户端成功连接环信MQTT消息云后,需尽快向服务器发送订阅主题消息。在连接成功后调用

client.subscribe(topic, MqttQos.atLeastOnce);

2.4.2 取消订阅

_client?.unsubscribe(topic)

2.5 收发消息

2.5.1 发送消息
配置发送消息回调方法,向环信MQTT消息云中指定topic发送消息。

var builder = MqttClientPayloadBuilder();
builder.addUTF8String("This is a message");
client.publishMessage("topic", MqttQos.atLeastOnce, builder.payload!);

2.5.2 接收消息
配置接收消息回调方法,从环信MQTT消息云接收订阅消息。

_client?.updates?.listen((event) {
var recvMessage = event[0].payload as MqttPublishMessage;

LogManager.log.d("原始数据-----:${recvMessage.payload.message}");
/// 转换成字符串
LogManager.log.d(
"接收到了主题${event[0].topic}的消息: ${const Utf8Decoder().convert(recvMessage.payload.message)}");
});
收起阅读 »

咱不吃亏,也不能过度自卫

这次我谈谈不吃亏的一种人,他们不吃亏近乎强硬。这类人一点亏都不吃,以至于过度自我保护。 我们公司人事小刘负责考勤统计。发完考勤表之后,有个员工找到他,说出勤少统计了一天。 小刘一听,感觉自己有被指控的风险。 他立刻严厉起来:“每天都来公司,不一定就算全勤。没打...
继续阅读 »

这次我谈谈不吃亏的一种人,他们不吃亏近乎强硬。这类人一点亏都不吃,以至于过度自我保护。


我们公司人事小刘负责考勤统计。发完考勤表之后,有个员工找到他,说出勤少统计了一天。


小刘一听,感觉自己有被指控的风险。


他立刻严厉起来:“每天都来公司,不一定就算全勤。没打卡我是不统计的”。


最后小刘一查,发现是自己统计错了。


小刘反而更加强势了:“这种事情,你应该早点跟我反馈,而且多催着我确认。你自己的事情都不上心,扣个钱啥的只能自己兜着”


这就是明显的不愿意吃亏,即使自己错了,也不愿意让自己置于弱势。


你的反应,决定别人怎么对你。这种连言语的亏都不吃的人,并不会让别人敬畏,反而会让人厌恶,进而影响沟通


我还有一个同事老王。他是一个职场老人,性格嘻嘻哈哈,业务能力也很强。


以前同事小赵和老王合作的时候,小赵宁愿经两层人传话给老王,也不愿意和他直接沟通。


我当时感觉小赵不善于沟通。


后来,当我和老王合作的时候,才体会到小赵的痛苦。


因为,老王是一个什么亏都不吃的人,谁来找他理论,他就怼谁。


你告诉他有疏漏,他会极力掩盖问题,并且怒怼你愚昧无知。


就算你告诉他,说他家着火了。他首先说没有。你一指那不是烧着的吗?他回复,你懂个屁,你知道我几套房吗?我说的是我另一个家没着火。


有不少人,从不吃亏,无论什么情况,都不会让自己处于弱势。


这类人喜欢大呼小叫,你不小心踩他脚了,他会大喊:践踏我的尊严,和你拼了!


心理学讲,愤怒源于恐惧,因为他想逃避当前不利的局面


人总会遇到各种不公的待遇,或误会,或委屈。


遇到争议时,最好需要确认一下,排除自己的问题。


如果自己没错,那么比较好的做法就是:“我认为你说得不合理,首先……其次……最后……”。


不盲目服软,也不得理不饶人,全程平心静气,有理有据。这种人绝对人格魅力爆棚,让人敬佩。


最后,有时候过度强硬也是一种策略,可以很好地过滤和震慑一些不重要的事物。


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

flutter有哪些架构的框架?该怎么选择

flutter有哪些架构的框架? Flutter是一种新兴的跨平台移动应用开发框架,它提供了丰富的UI组件和工具,使得应用开发更加容易。在Flutter中,有很多架构框架可供选择,以下是几个比较常用的架构框架: BLoC (Business Logic Co...
继续阅读 »

flutter有哪些架构的框架?


Flutter是一种新兴的跨平台移动应用开发框架,它提供了丰富的UI组件和工具,使得应用开发更加容易。在Flutter中,有很多架构框架可供选择,以下是几个比较常用的架构框架:



  1. BLoC (Business Logic Component):BLoC是一种状态管理模式,它将应用程序中的业务逻辑和UI分离,使得应用程序更易于维护和测试。在Flutter中,可以使用flutter_bloc库来实现BLoC架构。
    Provider:Provider是Flutter中的一个轻量级状态管理库,它使用InheritedWidget实现状态共享,可以有效地解决Flutter应用中的状态管理问题。

  2. MobX:MobX是一种基于响应式编程的状态管理库,它使用可观察对象来管理应用程序的状态,并自动更新与之相关的UI组件。在Flutter中,可以使用mobx库来实现MobX架构。

  3. Redux:Redux是一种流行的状态管理模式,在Flutter中也有相应的实现库redux_flutter。Redux通过单一数据源管理应用程序的状态,并使用纯函数来处理状态的更新,可以有效地解决Flutter应用中的状态管理问题。
    以上是常用的Flutter架构框架,每个框架都有其优点和适用场景,开发者可以根据自己的需求选择合适的架构框架。


除了上面提到的框架之外,还有以下几个Flutter架构框架:



  1. GetX:GetX是一种轻量级的Flutter架构框架,它提供了路由管理、状态管理和依赖注入等功能,可以大大简化Flutter应用的开发。

  2. MVC:MVC是一种经典的软件架构模式,它将应用程序分为模型、视图和控制器三个部分,可以有效地分离关注点,使得应用程序更易于维护和扩展。

  3. MVP:MVP是一种衍生自MVC的架构模式,它将应用程序分为模型、视图和Presenter三个部分,Presenter负责处理业务逻辑,将模型数据展示到视图上。

  4. MVVM:MVVM是一种流行的架构模式,它将应用程序分为模型、视图和视图模型三个部分,视图模型负责处理业务逻辑,将模型数据展示到视图上。


总之,Flutter中有很多架构框架可供选择,每个框架都有其优点和适用场景,开发者可以根据自己的需求选择合适的架构框架。


Flutter BLoC


Flutter BLoC是一种状态管理模式,它将应用程序中的业务逻辑和UI分离,使得应用程序更易于维护和测试。BLoC这个缩写代表 Business Logic Component,即业务逻辑组件。
BLoC的核心思想是将UI层和业务逻辑层分离,通过Stream或者Sink等异步编程方式,将UI层和业务逻辑层连接起来。具体来说,BLoC模式包含以下三个部分:
Events:事件,即UI层的用户操作或其他触发条件,例如按钮点击,网络请求完成等等。
Bloc:业务逻辑层,用于处理Events,处理业务逻辑,并向UI层提供状态更新。
State:状态,即UI层的显示状态,例如页面的loading状态,数据请求成功或失败状态等等。


BLoC的核心是Bloc和State之间的联系,Bloc接收Events,并根据业务逻辑处理后,通过Stream向UI层提供状态更新。UI层监听Bloc的Stream,获取最新的State,并根据State更新UI状态。
在Flutter中,可以使用StreamController来创建BLoC。StreamController是一个异步数据流控制器,可以创建一个Stream用于事件流,创建一个Sink用于事件的注入。
Flutter框架提供了一个非常好的BLoC框架flutter_bloc,它封装了BLoC的核心逻辑,使得开发者可以更加方便地使用BLoC模式进行状态管理。使用flutter_bloc框架,只需要定义好Events、Bloc和State,然后将它们组合起来,就可以实现状态管理,从而将UI层和业务逻辑层分离。


总之,Flutter BLoC是一种状态管理模式,它通过将UI层和业务逻辑层分离,使用Stream或Sink等异步编程方式将它们连接起来,实现了应用程序的业务逻辑和UI分离。


如何选择使用


选择使用哪种Flutter架构框架取决于具体的应用程序需求和开发团队的技术水平。以下是一些常见的应用场景和推荐的架构框架:
对于小型应用程序,可以考虑使用轻量级的状态管理库,如Provider或GetX。
对于需要处理复杂业务逻辑的应用程序,可以使用BLoC、MobX或Redux等状态管理框架。
对于需要实现高度可测试性的应用程序,可以考虑使用MVC、MVP或MVVM等经典的软件架构模式。
总之,选择哪种Flutter架构框架取决于具体的应用程序需求和开发团队的技术水平。开发者应该根据自己的需求和技能水平选择最适合的架构框架,以提高开发效率和代码质量。


GetX和BLoC的优缺点


GetX和BLoC都是常见的Flutter架构框架,它们各有优缺点,下面是它们的主要特点和优缺点的比较:


GetX


优点:


简单易用:GetX是一种轻量级的Flutter框架,提供了简单易用的依赖注入、路由管理和状态管理等功能,可以大大简化Flutter应用的开发。
性能优秀:GetX使用原生的Dart语言构建,不需要任何代码生成,因此运行速度非常快,同时也具有很好的内存管理和性能优化能力。
功能完备:GetX提供了路由管理、依赖注入、状态管理、国际化、主题管理等功能,可以满足大多数应用程序的需求。


缺点:


社区相对较小:相比其他流行的Flutter框架,GetX的社区相对较小,相关文档和教程相对较少,需要一定的自学能力。
不适合大型应用:由于GetX是一种轻量级框架,不适合处理大型应用程序的复杂业务逻辑和状态管理,需要使用其他更加强大的框架。


BLoC


优点:


灵活可扩展:BLoC提供了灵活的状态管理和业务逻辑处理能力,可以适应各种应用程序的需求,同时也具有良好的扩展性。
可测试性强:BLoC将UI和业务逻辑分离,提高了代码的可测试性,可以更容易地编写和运行测试代码。
社区活跃:BLoC是一种流行的Flutter框架,拥有较大的社区和用户群体,相关文档和教程比较丰富,容易入手。


缺点:


学习曲线较陡峭:BLoC是一种相对复杂的框架,需要一定的学习曲线和编程经验,初学者可能需要花费较多的时间和精力。
代码量较大:由于BLoC需要处理UI和业务逻辑的分离,因此需要编写更多的代码来实现相同的功能,可能会增加开发成本和维护难度。
总之,GetX和BLoC都是常见的Flutter架构框架,它们各有优缺点。选择哪种框架取决于具体的应用程序需求和开发团队的技术水平。


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

一位程序员,做了一个浏览器插件,赚了 4 万美元

序 今天分享的是一个能为开发者带来 4 万刀收入的浏览器插件 ReaderMode;这又是一篇旧文, 4 万刀已经是前年(2021 年)的事了。 这个插件花了 8 个月时间累计营收 1 万刀,但仅仅一年多后,累计收入就翻了 4 倍。听起来好像不太真实,但看完你...
继续阅读 »


今天分享的是一个能为开发者带来 4 万刀收入的浏览器插件 ReaderMode;这又是一篇旧文, 4 万刀已经是前年(2021 年)的事了。


这个插件花了 8 个月时间累计营收 1 万刀,但仅仅一年多后,累计收入就翻了 4 倍。听起来好像不太真实,但看完你可能会对浏览器插件市场有不一样的看法。


想法 → 第一版上线


最早,一位马来西亚的程序员 Ryzal 在筹划着做个小产品来赚点生活费;基于他日常的观察认为浏览器插件是一个可以比较低成本的快速验证想法的方式,所以就决定在插件领域下工夫;在看完一圈插件,然后结合自己平时浏览网页时对各种广告以及乱七八糟排版的不爽,于是就想一个可以让用户无干扰、沉浸式阅读文章的插件应该会很受欢迎。


本来就是想着可以快速验证想法,他马上就开始了;只花了一个周末,就出了 MVP 版 - 最小可行产品。


把 MVP 发给了几个朋友和群组里,打算着是收集一些用户的吐槽和建议。结果竟然有几个人直接付费购买了 pro 版。



浏览器插件也有人愿意付费?!



国内可能比较少见,但在国外需要付费使用的插件还是挺常见的。很大程度也是海外用户付费意愿比较强的缘故,更多的内容在上一篇《为什么开发者应该多关注海外市场》有展开详谈。


商业模式


ReaderMode 采用的商业模式是 freemium,大白话说就是基础功能免费使用,高级功能付费使用。Freemium 是国外 to C 的产品比较主流的商业模式了,比如 Notion, Slack, Dropbox 等。


这种模式的好处是,用户没有心理门槛,有点好奇心的小伙伴都可能会试试(反正不花钱)。这样基础用户量就有了,但凡你的产品是有亮点的(即使是免费的功能),就会有一批忠实用户;这些忠实的用户会帮你转发,推荐给更多的用户,这就是口碑传播 word of mouth (WoM)。WoM 传播未必是最快的,但是一定是转化效率最高的传播之一。


用户基数大了,自然就会“漏斗”出付费用户了。


可以说 freemium 模式下的免费用户是营销推广的利器;其实从财务角度来看,你可以把免费版的投入(时间、精力、钱等)看作是市场推广费用。


从这个角度来思考免费档的功能以及其设计能创造出更高的 ROI。


Soft launch


回到时间线上,Ryzal 根据 beta 用户的反馈做了一些调整,花了一个月时间才把产品打磨到可以对外发布的程度。


在正式发布之前,他还做了一次叫 soft launch,就是在自己个人的渠道小范围地做一次宣发。这也是国外独立开发者比较常用的发布策略。其目的有多层:



  • 二次验证需求

  • 获得更多的关注者

  • 压力测试

  • “大家来找 bug“

  • 为正式发布暖场


这是 Ryzal 当时 soft launch 发的推;留意他还提供了个 8 折优惠码吸引早期试用用户。


image.png


这次 soft launch 算不错,给 ReaderMode 带了 100 多位用户。顺势他在这条推下面做了个小调研:


image.png


很明显这是为了在 ProductHunt 上面做正式发布做准备了;前面提到的为正式发布暖场的意图也在这里显现出来了。


为什么在 PH 的正式发布那么重要?还需要暖场?


ProductHunt launch


首先,PH 是什么?



PH 是一个新产品发现平台。提交新产品的方式可以是创造者自己提交,或者是用户自己发现了好用好玩的产品提交到平台。



由于平台每天都会收到的数十条新品提交,为了鼓励优质的产品,他有一个排名机制;这个排名的依据是当天新产品的 upvote(可以简单理解为点赞)数量。


PH 的流量非常大,如果能在当天的发布排名中占到前 5,带来的曝光是非常可观的;更不用说如果进入前 3,还会有专门的徽章。


image.png


之前分享的独立设计师独立开发者案例,都是在 PH 获取早期用户。对于初创产品,把发布节奏和细节把控好,产品冷启动的问题可能一下子就解决了,随之还带来一大拨免费 PR 流量。


其实,不仅是独立产品、创业公司,就连大公司也会把新产品提交到平台,可见 PH 在行业内的影响力之大。


从 0 到 2000 美元


Ryzal 把发布的内容都准备好后,在 PH 上正式发布并顺利地拿下来了当天的最高赞。


image.png


ReaderMode 的 PH 链接


更加幸运的是,不仅 PH 的创始人 Ryan(推特大 V)为其推广,连 LifeHacker 这种大媒体都留意到了这个产品并对其作出了报导。


短短发布后的 24 小时内,这个产品就收了 2000 美元;而这离写下第一行代码应该只有不到 2 个月的时间。


再次证明了,产品力本身就是最好的营销技巧。


增长到 40,000 美元


整体下来从发布到 5000 用户,两周时间,没花一分钱。有了一个成功的 PH 发布,带来近 10 家媒体(可能实际更多)的曝光,这个产品已经有不少 credibility 了;3 个月后,1 万用户达成。


image.png


截止至 2021 年 5 月左右的收入,总计 4 万多刀,分两部分组成。



  1. 一部分产品的直接收入:


image.png



  1. 另一部分是通过一些 deal sites(比如 AppSumo)的收入:


image.png


接下来就躺赚了?


不存在的,这是许多开发者对做独立产品的一个误解。


事实上,Ryzal 在 PH 发布后的第二天就开始了新一轮的迭代。今天,ReaderMode 已不再是一个简单的插件了,而是一个



All-in-one reading, bookmarking, highlighting and research app.



所以,开发者们码起来、迭代起来!


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

当我再次用Kotlin完成五年前已经通过Kotlin完成的项目后

关于贝塞尔曲线, 这里就不多赘述了. 简单来说, 针对每一个线段, 某个点到两端的比例都是一样的, 而贝塞尔曲线就是这个过程的中线段两端都在同一位置的线段(点)过程的集合. 如图, AD和AB的比例, BE和BC的比例还有DF和DE的比例都是一样的.这个比例从...
继续阅读 »

关于贝塞尔曲线, 这里就不多赘述了. 简单来说, 针对每一个线段, 某个点到两端的比例都是一样的, 而贝塞尔曲线就是这个过程的中线段两端都在同一位置的线段(点)过程的集合.


如图, AD和AB的比例, BE和BC的比例还有DF和DE的比例都是一样的.这个比例从0到1, F点的位置连成线, 就是ABC这三个点的贝塞尔曲线.


Bezier


两次完成的感受


虽然时隔五年, 但是对这个项目的印象还是比较深刻的(毕竟当时找啥资料都不好找).


当时的项目还用的是Kotlin Synthetic来进行数据绑定(虽然现在已经被弃用了), 对于当时还一直用findViewById和@BindView的我来说, 这是对我最大的惊喜. 是的, 当时用Kotlin最大惊喜就是这个. 其它的感觉就是这个"语法糖"看起来还挺好用的. 而现在, 我可以通过Compose来完成页面的布局. 最直观的结果是代码量的减少, 初版功能代码(带xml)大概有800行, 而这次完成整个功能大概只需要450行.


在使用过程中对"Compose is function"理念的理解更深了一步, 数据就是数据. 将数据作为一个参数放到Compose这个function中, 在数据变化的时候重新调用function, 达到更新UI的效果. 显而易见的事情是我们不需要的额外的持有UI的对象了, 我们不必考虑UI中某个元素和另一个元素直接的关联, 不必考虑某个元素响应什么样的操作. 我们只需要考虑某个Compose(function) 在什么样的情况下(入参)需要表现成什么样子.


比如Change Point按钮点下时, 会更改mInChange的内容, 从而影响许多其它元素的效果, 如果通过View来实现, 我需要监听Change Point的点击事件, 然后依次修改影响到的元素(这个过程中需要持有大量其它View的对象). 不过当使用Compose后, 虽然我们仍要监听Change Point的点击事件, 但是对对应Change Point的监听动作来说, 它只需要修改mInChange的内容就行了, 修改这个值会发生什么变化它不需要处理也不要知道. 真正需要变化的Compose来处理就可以了(可以理解为参数变化了, 重新调用了这个function)


特性的部分使用的并不多, 比较项目还是比较小, 很多特性并没有体现出来.


最令我感到开心的是, 再一次完成同样的功能所花费的时间仅仅只有半天多, 而5年前完成类似的功能大概用了一个多星期的时间. 也不知道我和Kotlin这5年来哪一方变化的更大😆.


贝塞尔曲线工具


先来看一下具有的功能, 主要的功能就是绘制贝塞尔曲线(可绘制任意阶数), 显示计算过程(辅助线的绘制), 关键点的调整, 以及新增的绘制进度手动调整. 为了更本质的显示绘制的结果, 此次并没有对最终结果点进行显示优化, 所以在短时间变化位置大的情况下, 可能出现不连续的现象.


3_point_bezier


more_point_bezier


bizier_change


bezier_progress


代码的比较


既然是同样的功能, 不同的代码, 即使是由不同时期所完成的, 将其相互比较一下还是有一定意义的. 当然比较的内容都尽量提供相同实现的部分.


屏幕触摸事件监测层


主要在于对屏幕的触碰事件的监测


初版代码:

override fun onTouchEvent(event: MotionEvent): Boolean {


touchX = event.x
touchY = event.y
when (event.action) {
MotionEvent.ACTION_DOWN -> {
toFindChageCounts = true
findPointChangeIndex = -1
//增加点前点击的点到屏幕中
if (controlIndex < maxPoint || isMore == true) {
addPoints(BezierCurveView.Point(touchX, touchY))
}
invalidate()
}
MotionEvent.ACTION_MOVE ->{
checkLevel++
//判断当前是否需要检测更换点坐标
if (inChangePoint){
//判断当前是否长按 用于开始查找附件的点
if (touchX == lastPoint.x && touchY == lastPoint.y){
changePoint = true
lastPoint.x = -1F
lastPoint.y = -1F
}else{
lastPoint.x = touchX
lastPoint.y = touchY
}
//开始查找附近的点
if (changePoint){
if (toFindChageCounts){
findPointChangeIndex = findNearlyPoint(touchX , touchY)
}
}

//判断是否存在附近的点
if (findPointChangeIndex == -1){
if (checkLevel > 1){
changePoint = false
}

}else{
//更新附近的点的坐标 并重新绘制页面内容
points[findPointChangeIndex].x = touchX
points[findPointChangeIndex].y = touchY
toFindChageCounts = false
invalidate()
}
}

}
MotionEvent.ACTION_UP ->{
checkLevel = -1
changePoint = false
toFindChageCounts = false
}

}
return true
}

二次代码:

 Canvas(
...
.pointerInput(Unit) {
detectDragGestures(
onDragStart = {
model.pointDragStart(it)
},
onDragEnd = {
model.pointDragEnd()
}
) { _, dragAmount ->
model.pointDragProgress(dragAmount)
}
}
.pointerInput(Unit) {
detectTapGestures {
model.addPoint(it.x, it.y)
}
}
)
...

/**
* change point position start, check if have point in range
*/
fun pointDragStart(position: Offset) {
if (!mInChange.value) {
return
}
if (mBezierPoints.isEmpty()) {
return
}
mBezierPoints.firstOrNull() {
position.x > it.x.value - 50 && position.x < it.x.value + 50 &&
position.y > it.y.value - 50 && position.y < it.y.value + 50
}.let {
bezierPoint = it
}
}

/**
* change point position end
*/
fun pointDragEnd() {
bezierPoint = null
}

/**
* change point position progress
*/
fun pointDragProgress(drag: Offset) {
if (!mInChange.value || bezierPoint == null) {
return
} else {
bezierPoint!!.x.value += drag.x
bezierPoint!!.y.value += drag.y
calculate()
}
}

可以看到由于Compose提供了Tap和Drag的详细事件, 从而导致新的代码少许多的标记位变量.


而我之前一度认为是语法糖的特性来给我带来了不小的惊喜.


譬如这里查找点击位置最近的有效的点的方法,


初版代码:

//判断当前触碰的点附近是否有绘制过的点
private fun findNearlyPoint(touchX: Float, touchY: Float): Int {
Log.d("bsr" , "touchX: ${touchX} , touchY: ${touchY}")
var index = -1
var tempLength = 100000F
for (i in 0..points.size - 1){
val lengthX = Math.abs(touchX - points[i].x)
val lengthY = Math.abs(touchY - points[i].y)
val length = Math.sqrt((lengthX * lengthX + lengthY * lengthY).toDouble()).toFloat()
if (length < tempLength){
tempLength = length

if (tempLength < minLength){
toFindChageCounts = false
index = i
}
}
}

return index
}


而二次代码:

        mBezierPoints.firstOrNull() {
position.x > it.x.value - 50 && position.x < it.x.value + 50 &&
position.y > it.y.value - 50 && position.y < it.y.value + 50
}.let {
bezierPoint = it
}

和Java的Stream类似, 链式结构看起来更加的易于理解.


贝塞尔曲线绘制层


主要的贝塞尔曲线是通过递归实现的


初版代码:

//通过递归方法绘制贝塞尔曲线
private fun drawBezier(canvas: Canvas, per: Float, points: MutableList<Point>) {

val inBase: Boolean

//判断当前层级是否需要绘制线段
if (level == 0 || drawControl){
inBase = true
}else{
inBase = false
}


//根据当前层级和是否为无限制模式选择线段及文字的颜色
if (isMore){
linePaint.color = 0x3F000000
textPaint.color = 0x3F000000
}else {
linePaint.color = colorSequence[level].toInt()
textPaint.color = colorSequence[level].toInt()
}

//移动到开始的位置
path.moveTo(points[0].x , points[0].y)

//如果当前只有一个点
//根据贝塞尔曲线定义可以得知此点在贝塞尔曲线上
//将此点添加到贝塞尔曲线点集中(页面重新绘制后之前绘制的数据会丢失 需要重新回去前段的曲线路径)
//将当前点绘制到页面中
if (points.size == 1){
bezierPoints.add(Point(points[0].x , points[0].y))
drawBezierPoint(bezierPoints , canvas)
val paint = Paint()
paint.strokeWidth = 10F
paint.style = Paint.Style.FILL
canvas.drawPoint(points[0].x , points[0].y , paint)
return
}


val nextPoints: MutableList<Point> = ArrayList()

//更新路径信息
//计算下一级控制点的坐标
for (index in 1..points.size - 1){
path.lineTo(points[index].x , points[index].y)

val nextPointX = points[index - 1].x -(points[index - 1].x - points[index].x) * per
val nextPointY = points[index - 1].y -(points[index - 1].y - points[index].y) * per

nextPoints.add(Point(nextPointX , nextPointY))
}

//绘制控制点的文本信息
if (!(level !=0 && (per==0F || per == 1F) )) {
if (inBase) {
if (isMore && level != 0){
canvas.drawText("0:0", points[0].x, points[0].y, textPaint)
}else {
canvas.drawText("${charSequence[level]}0", points[0].x, points[0].y, textPaint)
}
for (index in 1..points.size - 1){
if (isMore && level != 0){
canvas.drawText( "${index}:${index}" ,points[index].x , points[index].y , textPaint)
}else {
canvas.drawText( "${charSequence[level]}${index}" ,points[index].x , points[index].y , textPaint)
}
}
}
}

//绘制当前层级
if (!(level !=0 && (per==0F || per == 1F) )) {
if (inBase) {
canvas.drawPath(path, linePaint)
}
}
path.reset()

//更新层级信息
level++

//绘制下一层
drawBezier(canvas, per, nextPoints)

}



二次代码:

{
lateinit var preBezierPoint: BezierPoint
val paint = Paint()
paint.textSize = mTextSize.toPx()

for (pointList in model.mBezierDrawPoints) {
if (pointList == model.mBezierDrawPoints.first() ||
(model.mInAuxiliary.value && !model.mInChange.value)
) {
for (point in pointList) {
if (point != pointList.first()) {
drawLine(
color = Color(point.color),
start = Offset(point.x.value, point.y.value),
end = Offset(preBezierPoint.x.value, preBezierPoint.y.value),
strokeWidth = mLineWidth.value
)
}
preBezierPoint = point

drawCircle(
color = Color(point.color),
radius = mPointRadius.value,
center = Offset(point.x.value, point.y.value)
)
paint.color = Color(point.color).toArgb()
drawIntoCanvas {
it.nativeCanvas.drawText(
point.name,
point.x.value - mPointRadius.value,
point.y.value - mPointRadius.value * 1.5f,
paint
)
}
}
}
}

...
}


/**
* calculate Bezier line points
*/
private fun calculateBezierPoint(deep: Int, parentList: List<BezierPoint>) {
if (parentList.size > 1) {
val childList = mutableListOf<BezierPoint>()
for (i in 0 until parentList.size - 1) {
val point1 = parentList[i]
val point2 = parentList[i + 1]
val x = point1.x.value + (point2.x.value - point1.x.value) * mProgress.value
val y = point1.y.value + (point2.y.value - point1.y.value) * mProgress.value
if (parentList.size == 2) {
mBezierLinePoints[mProgress.value] = Pair(x, y)
return
} else {
val point = BezierPoint(
mutableStateOf(x),
mutableStateOf(y),
deep + 1,
"${mCharSequence.getOrElse(deep + 1){"Z"}}$i",
mColorSequence.getOrElse(deep + 1) { 0xff000000 }
)
childList.add(point)
}
}
mBezierDrawPoints.add(childList)
calculateBezierPoint(deep + 1, childList)
} else {
return
}
}

初版开发的时候受个人能力限制, 递归方法中既包含了绘制的功能也包含了计算下一层的功能. 而二次编码的时候受Compose的设计影响, 尝试将所有的点状态变为Canvas的入参信息. 代码的编写过程就变得更加的流程.


当然, 现在的我和五年前的我, 开发的能力一定是不一样的. 即便如此, 随着Kotlin的不断发展, 即使是同样用Kotlin完成的项目, 随着新的概念的提出, 更多更适合新的开发技术的出现, 我们仍然从Kotlin和Compose收获更多.


我和Kotlin的小故事


初次认识Kotlin是在2017的5月, 当时Kotlin还不是Google所推荐的Android开发语言. 对我来说, Kotlin更多的是个新的技术, 在实际的工作中也无法进行使用.


即使如此, 我也尝试开始用Kotlin去完成更多的内容, 所幸如此, 不然这篇文章就无法完成了, 我也错过了一个更深层次了解Kotlin的机会.


但是即便2018年Google将Kotlin作为Android的推荐语言, 但Kotlin在当时仍不是一个主流的选择. 对我来说以下的一些问题导致了我在当时对Kotlin的使用性质不高. 一是新语言, 社区构建不完善, 有许多的内容需要大家填充, 带来就是在实际的使用情况中会遇到各种的问题, 这些问题在网站中没有找到可行的解决方案. 二是可以和Java十分便捷互相使用的特性, 这个特性是把双刃剑,
虽然可以让我更加无负担的使用Kotlin(不行再用Java写呗.). 但也使得我认为Kotlin是个Java++或者Java--. 三是无特殊性, Kotlin并没有带来什么新的内容, Kotlin能完成的事情Java都能做完成, (空值和data class之类的在我看来更多的是一个语法糖.) 那么我为什么要用一种新的不熟悉的技术来完成我都需求?


所幸的是, 还是有更多的人在不断的推进和建设Kotlin. 也吸引了越来越多的人加入. 近年来越来越多的项目中都开始有着Kotlin的踪迹, 我将Kotlin添加到现有的项目中也变得越来越能被大家所接受. 也期待可以帮助到更多的人.


相关代码地址:


初次代码


二次代码


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

Android自定义一个省份简称键盘

我正在参加「掘金·启航计划」hello啊各位老铁,这篇文章我们重新回到Android当中的自定义View,其实最近一直在搞Flutter,初步想法是,把Flutter当中的基础组件先封装一遍,然后接着各个工具类,列表,网络,统统由浅入深的搞一遍,弄完Flutt...
继续阅读 »

我正在参加「掘金·启航计划」

hello啊各位老铁,这篇文章我们重新回到Android当中的自定义View,其实最近一直在搞Flutter,初步想法是,把Flutter当中的基础组件先封装一遍,然后接着各个工具类,列表,网络,统统由浅入深的搞一遍,弄完Flutter之后,再逐步的更新Android当中的技术点,回头一想,还是穿插着来吧,再系统的规划,难免也有变化,想到啥就写啥吧,能够坚持输出就行。

今天的这个知识点,是一个自定义View,一个省份的简称键盘,主要用到的地方,比如车牌输入等地方,相对来说还是比较的简单,我们先看下最终的实现效果:

实现方式呢有很多种,我相信大家也有自己的一套实现机制,这里,我采用的是组合View,用的是LinearLayout的方式。

今天的内容大致如下:

1、分析UI,如何布局

2、设置属性和方法,制定可扩展效果

3、部分源码剖析

4、开源地址及实用总结

一、分析UI,如何布局

拿到UI效果图后,其实也没什么好分析的,无非就是两块,顶部的完成按钮和底部的省份简称格子,一开始,打算用RecyclerView网格布局来实现,但是最后的删除按钮如何摆放就成了问题,直接悬浮在网格上边,动态计算位置,显然不太合适,也没有这样去搞的,索性直接抛弃这个方案,多布局的想法也实验过,但最终还是选择了最简单的LinearLayout组合View形式。

所谓简单,就是在省份简称数组的遍历中,不断的给LinearLayout进行追加子View,需要注意的是,本身的View,也就是我们自定义View,继承LinearLayout后,默认的是垂直方向的,往本身View追加的是横向属性的LinearLayout,这也是换行的效果,也就是,一行一个横向的LinearLayout,记住,横向属性的LinearLayout,才是最终添加View的直接父类。

换行的条件就是基于UI效果,当模于设置length等于0时,我们就重新创建一个水平的LinearLayout,这就可以了,是不是非常的简单。

至于最后的删除按钮,使其靠右,占据两个格子的权重设置即可。

二、设置属性和方法,制定可扩展效果

当我们绘制完这个身份简称键盘后,肯定是要给他人用的,基于灵活多变的需求,那么相对应的我们也需要动态的进行配置,比如背景颜色,文字的颜色,大小,还有边距,以及点击效果等等,这些都是需要外露,让使用者选择性使用的,目前所有的属性如下,大家在使用的时候,也可以对照设置。

设置属性

属性类型概述
lp_backgroundcolor整体的背景颜色
lp_rect_spacingdimension格子的边距
lp_rect_heightdimension格子的高度
lp_rect_margin_topdimension格子的距离上边
lp_margin_left_rightdimension左右距离
lp_margin_topdimension上边距离
lp_margin_bottomdimension下边距离
lp_rect_backgroundreference格子的背景
lp_rect_select_backgroundreference格子选择后的背景
lp_rect_text_sizedimension格子的文字大小
lp_rect_text_colorcolor格子的文字颜色
lp_rect_select_text_colorcolor格子的文字选中颜色
lp_is_show_completeboolean是否显示完成按钮
lp_complete_text_sizedimension完成按钮文字大小
lp_complete_text_colorcolor完成按钮文字颜色
lp_complete_textstring完成按钮文字内容
lp_complete_margin_topdimension完成按钮距离上边
lp_complete_margin_bottomdimension完成按钮距离下边
lp_complete_margin_rightdimension完成按钮距离右边
lp_text_click_effectboolean是否触发点击效果,true点击后背景消失,false不消失

定义方法

方法参数概述
keyboardContent回调函数获取点击的省份简称简称信息
keyboardDelete函数删除省份简称简称信息
keyboardComplete回调函数键盘点击完成
openProhibit函数打开禁止(使领学港澳),使其可以点击

三、关键源码剖析

这里只贴出部分的关键性代码,整体的代码,大家滑到底部查看源码地址即可。

定义身份简称数组

    //省份简称数据
private val mLicensePlateList = arrayListOf(
"京", "津", "渝", "沪", "冀", "晋", "辽", "吉", "黑", "苏",
"浙", "皖", "闽", "赣", "鲁", "豫", "鄂", "湘", "粤", "琼",
"川", "贵", "云", "陕", "甘", "青", "蒙", "桂", "宁", "新",
"藏", "使", "领", "学", "港", "澳",
)

遍历省份简称

mLength为一行展示多少个,当取模为0时,就需要换行,也就是再次创建一个水平的LinearLayout,添加至外层的垂直LinearLayout中,每个水平的LinearLayout中,则是一个一个的TextView。

  //每行对应的省份简称
var layout: LinearLayout? = null
//遍历车牌号
mLicensePlateList.forEachIndexed { index, s ->
if (index % mLength == 0) {
//重新创建,并添加View
layout = createLinearLayout()
layout?.weightSum = 1f
addView(layout)
val params = layout?.layoutParams as LayoutParams
params.apply {
topMargin = mRectMarginTop.toInt()
height = mRectHeight.toInt()
leftMargin = mMarginLeftRight.toInt()
rightMargin = mMarginLeftRight.toInt() - mSpacing.toInt()
layout?.layoutParams = this
}
}

//创建文字视图
val textView = TextView(context).apply {
text = s
//设置文字的属性
textSize = px2sp(mRectTextSize)
//最后五个是否禁止
if (mNumProhibit && index > (mLicensePlateList.size - 6)) {
setTextColor(mNumProhibitColor)
mTempTextViewList.add(this)
} else {
setTextColor(mRectTextColor)
}

setBackgroundResource(mRectBackGround)
gravity = Gravity.CENTER
setOnClickListener {
if (mNumProhibit && index > (mLicensePlateList.size - 6)) {
return@setOnClickListener
}
//每个格子的点击事件
changeTextViewState(this)
}
}

addRectView(textView, layout, 0.1f)
}

追加最后一个View

由于最后一个视图是一个图片,占据了两个格子的大小,所以需要特殊处理,需要做的就是,单独设置权重weight和单独设置宽度width,如下所示:

  /**
* AUTHOR:AbnerMing
* INTRODUCE:追加最后一个View
*/
private fun addEndView(layout: LinearLayout?) {
val endViewLayout = LinearLayout(context)
endViewLayout.gravity = Gravity.RIGHT
//删除按钮
val endView = RelativeLayout(context)
//添加删除按钮
val deleteImage = ImageView(context)
deleteImage.setImageResource(R.drawable.view_ic_key_delete)
endView.addView(deleteImage)

val imageParams = deleteImage.layoutParams as RelativeLayout.LayoutParams
imageParams.addRule(RelativeLayout.CENTER_IN_PARENT)
deleteImage.layoutParams = imageParams
endView.setOnClickListener {
//删除
mKeyboardDelete?.invoke()
invalidate()
}
endView.setBackgroundResource(mRectBackGround)
endViewLayout.addView(endView)
val params = endView.layoutParams as LayoutParams
params.width = (getScreenWidth() / mLength) * 2 - mMarginLeftRight.toInt()
params.height = LayoutParams.MATCH_PARENT

endView.layoutParams = params

layout?.addView(endViewLayout)
val endParams = endViewLayout.layoutParams as LayoutParams
endParams.apply {
width = (mSpacing * 3).toInt()
height = LayoutParams.MATCH_PARENT
weight = 0.4f
rightMargin = mSpacing.toInt()
endViewLayout.layoutParams = this
}


}

四、开源地址及使用总结

开源地址:github.com/AbnerMing88…

关于使用,其实就是一个类,大家可以下载源码,直接复制即可使用,还可以进行修改里面的代码,非常的方便,如果懒得下载源码,没关系,我也上传到了远程Maven,大家可以按照下面的方式进行使用。

Maven具体调用

1、在你的根项目下的build.gradle文件下,引入maven。

 allprojects {
repositories {
maven { url "https://gitee.com/AbnerAndroid/almighty/raw/master" }
}
}

2、在你需要使用的Module中build.gradle文件下,引入依赖。

 dependencies {
implementation 'com.vip:plate:1.0.0'
}

代码使用

   <com.vip.plate.LicensePlateView
android:id="@+id/lp_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:lp_complete_text_size="14sp"
app:lp_margin_left_right="10dp"
app:lp_rect_spacing="6dp"
app:lp_rect_text_size="19sp"
app:lp_text_click_effect="false" />

总结

大家在使用的时候,一定对照属性表进行选择性使用;关于这个省份简称自定义View,实现方式有很多种,我目前的这种也不是最优的实现方式,只是自己的一个实现方案,给大家一个作为参考的依据,好了,铁子们,本篇文章就先到这里,希望可以帮助到大家。


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

Android 官方架构中的 UseCase 该怎么写?

1. UseCase 的用途 Android 最新的架构规范中,引入了 Domain Layer(译为领域层or网域层),建议大家使用 UseCase 来封装一些复杂的业务逻辑。 Android 最新架构:developer.android.com/topi...
继续阅读 »

1. UseCase 的用途


Android 最新的架构规范中,引入了 Domain Layer(译为领域层or网域层),建议大家使用 UseCase 来封装一些复杂的业务逻辑。



Android 最新架构:developer.android.com/topic/archi…



传统的 MVVM 架构中,我们习惯用 ViewModel 来承载业务逻辑,随着业务规模的扩大,ViewModel 变得越来越肥大,职责不清。



Clean Architecture 提出的关注点分离和单一职责(SRP)的设计原则被广泛认可,因此 Android 在最新架构中引入了 Clean Architecture 中 UseCase 的概念。ViewModel 归属 UI Layer,更加聚焦 UiState 的管理,UI 无关的业务逻辑下沉 UseCase,UseCase 与 ViewModel 解耦后,也可以跨 ViewModel 提供公共逻辑。



Android 架构早期的示例代码 todo-app 中曾经引入过 UseCase 的概念,最新架构中只不过是将 UseCase 的思想更明确了,最新的 UseCase 示例可以从官方的 NIA 中学习。




2. UseCase 的特点


官方文档认为 UseCase 应该具有以下几个特点:


2.1 不持有状态


可以定义自己的数据结构类型,但是不能持有状态实例,像一个纯函数一样工作。甚至直接推荐大家将逻辑重写到 invoke 方法中,像调用函数一样调用实例。


下面是 NIA 中的一个示例:GetRecentSearchQueriesUseCase



2.2 单一职责


严格遵守单一职责,一个 UseCase 只做一件事情,甚至其命名就是一个具体行为。扫一眼 UseCase 的文件目录大概就知道 App 的大概功能了。


下面 NIA 中所有 UseCases:



2.3 可有可无


官方文档中将 UseCase 定义为可选的角色,按需定义。简单的业务场景中允许 UI 直接访问 Repository。如果我们将 UseCase 作为 UI 与 Data 隔离的角色,那么工程中会出现很多没有太大价值的 UseCase ,可能就只有一行调用 Repoitory 的代码。


3. 如何定义 UseCase


如上所述,官方文档虽然对 UseCase 给出了一些基本定义,但是毕竟是一个新新生概念,很多人在真正去写代码的时候仍然会感觉不清晰,缺少有效指引。在究竟如何定义 UseCase 这个问题上,还有待大家更广泛的讨论,形成可参考的共识。本文也是带着这个目的而生,算是抛砖引玉吧。


3.1 Optional or Mandatory?


首先,官方文档认为 UseCase 是可选的,虽然其初衷是好的,大家都不希望出现太多 One-Liner 的 UseCase,但是作为一个架构规范切忌模棱两可,这种“可有可无”的规则其结局往往就是“无”。


业务刚起步时由于比较简单往往定义在 Repository 中,随着业务规模的扩大,应该适当得增加 UseCase 封装一些复杂的业务逻辑,但是实际项目中此时的重构成本会让开发者变得“懒惰”,UseCase 最终难产。


那放弃 UseCase 呢?这可能会造成 Repository 的职责不清和无限膨胀,而且 Repository 往往不止有一个方法, ViewModel 直接依赖 Repository 也违反了 SOLID 中的另一个重要原则 ISP ,ViewModel 会因为不相关的 Repository 改动导致重新编译。



ISP(Interface Segregation Principle,接口隔离原则) 要求将接口分离成更小的和更具体的接口,以便调用方只需知道其需要使用的方法。这可以提高代码的灵活性和可重用性,并减少代码的依赖性和耦合性。



为了降低前期判断成本和后续重构成本,如果我们有业务持续壮大的预期,那不妨考虑将 UseCase 作为强制选项。当然,最好这需要研究如何降低 UseCase 带来的模板代码。


3.2 Class or Object?


官方建议使用 Class 定义 UseCase,每次使用都实例化一个新对象,这会做成一些重复开销,那么可否用 object 定义 UseCase 呢?


UseCase 理论上可以作为单例存在,但 Class 相对于 Object 有以下两个优势:



  • UseCase 希望像纯函数一样工作,普通 Class 可以确保每次使用时都会创建一个新的实例,从而避免状态共享和副作用等问题。

  • 普通类可以通过构造参数注入不同的 Repository,UseCase 更利于复用和单元测试


如果我们强烈希望 UseCase 有更长的生命周期,那借助 DI 框架,普通类也可以简单的支持。例如 Dagger 中只要添加 @Singleton 注解即可

@Singleton
class GetRecentSearchQueriesUseCase @Inject constructor(
private val recentSearchRepository: RecentSearchRepository,
) {
operator fun invoke(limit: Int = 10): Flow<List<RecentSearchQuery>> =
recentSearchRepository.getRecentSearchQueries(limit)
}

3.3 Class or Function?


既然我们想像函数一样使用 UseCase ,那为什么不直接定义成 Function 呢?比如像下面这样

fun GetRecentSearchQueriesUseCase : Flow<List<RecentSearchQuery>> 

这确实遵循了 FP 的原则,但又丧失了 OOP 封装性的优势:



  • UseCase 往往需要依赖 Repository 对象,一个 UseCase Class 可以将 Repository 封装为成员存储。而一个 UseCase Function 则需要调用方通过参数传入,使用成本高不说,如果 UseCase 依赖的 Repository 的类型或者数量发生变化了,调用方需要跟着修改

  • 函数起不到隔离 UI 和 Data 的作用,ViewModel 仍然需要直接依赖 Repository,为 UseCase 传参

  • UseCase Class 可以定义一些 private 的方法,相对于 Function 更能胜任一些复杂逻辑的实现


可见,在 UseCase 的定义上 Function 没法取代 Class。当然 Class 也带来一些弊端:



  • 暴露多个方法,破坏 SRP 原则。所以官方推荐用 verb in present tense + noun/what (optional) + UseCase 动词命名,也是想让职责更清晰。

  • 携带可变状态,这是大家写 OOP 的惯性思维

  • 样板代码多


3.4 Function interface ?


通过前面的分析我们知道:UseCase 的定义需要兼具 FP 和 OOP 的优势。这让我想到了 Function(SAM) Interface 。Function Interface 是一个单方法的接口,可以低成本创建一个匿名类对象,确保对象只能有一个方法,同时具有一定封装性,可以通过“闭包”依赖 Repository。此外,Kotlin 对 SAM 提供了简化写法,一定程度也减少了样板代码。



Functional (SAM) interfaces:
kotlinlang.org/docs/fun-in…



改用 Function interface 定义 GetRecentSearchQueriesUseCase 的代码如下:

fun interface GetRecentSearchQueriesUseCase : () -> Flow<List<RecentSearchQuery>>

用它创建 UseCase 实例的同时,实现函数中的逻辑

val recentSearchQueriesUseCase = GetRecentSearchQueriesUseCase {
//...
}

我在函数实现中如何 Repository 呢?这要靠 DI 容器获取。官方示例代码中都使用 Hilt 来解耦 ViewModel 与 UseCase 的,ViewModel 不关心 UseCase 的创建细节。下面是 NIA 的代码, GetRecentSearchQueriesUseCase 被自动注入到 SearchViewModel 中。

@HiltViewModel
class SearchViewModel @Inject constructor(
recentSearchQueriesUseCase: GetRecentSearchQueriesUseCase // UseCase 注入 VM
//...
) : ViewModel() {
//...
}

Function interface 的 GetRecentSearchQueriesUseCase 没有构造函数,需要通过 Dagger 的 @Module 安装到 DI 容器中,provideGetRecentSearchQueriesUseCase 参数中的 RecentSearchRepository 可以从容器中自动获取使用。

@Module
@InstallIn(ActivityComponent::class)
object UseCaseModule {
@Provides
fun provideGetRecentSearchQueriesUseCase(recentSearchRepository: RecentSearchRepository) =
GetRecentSearchQueriesUseCase { limit ->
recentSearchRepository.getRecentSearchQueries(limit)
}
}

当时用 Koin 作为 DI 容器时也没问题,代码如下:

single<GetRecentSearchQueriesUseCase> {
GetRecentSearchQueriesUseCase { limit ->
recentSearchRepository.getRecentSearchQueries(limit)
}
}

4. 总结


UseCase 作为官方架构中的新概念,尚没有完全深入人心,需要不断探索合理的使用方式,本文给出一些基本思考:




  • 考虑到架构的扩展性,推荐在 ViewModel 与 Repository 之间强制引入 UseCase,即使眼下的业务逻辑并不复杂




  • UseCase 不持有可变状态但依赖 Repository,需要兼具 FP 与 OOP 的特性,更适合用 Class 定义而非 Function




  • 在引入 UseCase 之前应该先引入 DI 框架,确保 ViewModel 与 UseCase 的耦合。




  • Function Interface 是 Class 之外的另一种定义 UseCase 的方式,有利于代码更加函数式


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

Android 在一个APP里打开另一个APP

前言 不知道你们有没有注意过,每次打开一些软件的时候都会有广告引导页,有时候手滑点到了,会有进入手机上的另一个APP,这有没有引起你的注意呢? 运行效果图 # 正文 为了测试这个功能,首先要创建两个项目,然后运行起来都安装在你的手机上,这里为了方便了解,取名...
继续阅读 »

前言


不知道你们有没有注意过,每次打开一些软件的时候都会有广告引导页,有时候手滑点到了,会有进入手机上的另一个APP,这有没有引起你的注意呢?


运行效果图


在这里插入图片描述


# 正文
为了测试这个功能,首先要创建两个项目,然后运行起来都安装在你的手机上,这里为了方便了解,取名就是应用A和应用B,流程就是A应用里面打开B应用。

首先当然是创建项目了


DemoA


在这里插入图片描述


DemoB


在这里插入图片描述


创建好之后,别的先不管,都在手机上安装一下再说


在这里插入图片描述


① 打开另一个APP


接下来在DemoA的MainActivity里面写一个按钮,用于点击之后打开DemoB应用

	<Button
android:id="@+id/btn_open_b"
android:text="打开DemoB"
android:textAllCaps="false"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>

在这里插入图片描述


也在DemoB的布局文件改一下显示内容

<TextView
android:textSize="18sp"
android:textColor="#000"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="DemoB" />

运行一下


在这里插入图片描述


这样就打开了。那假如我要传递数据到DemoB呢?


② 数据传递


传数据其实就跟平时单个APP内部不同页面传数据类似,也是用Intent


在这里插入图片描述


然后在另一个APP里面接收并显示出来。现在先修改一下DemoB的布局,增加一个TextView用来显示接收的内容。

<TextView
android:id="@+id/tv_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:textColor="#000"
android:textSize="16sp" />

DemoB的MainActivity里


在这里插入图片描述


一旦两个应用程序里面改动了代码你就要在手机上运行一下,否则你改动的代码就不会生效


然后运行一下:


在这里插入图片描述


传值的问题就解决了。


③ 打开指定页面


通过包名跳转APP是进入默认的启动页面,你可以打开你的AndroidManifest.xml文件查看


在这里插入图片描述


那个Activity下面有这个默认启动就是那个

            <intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

至于要打开指定的页面有两个方法


1.通过包名、类名


首先在DemoB的下面再创建一个TestActivity,简单加一个TextView


在这里插入图片描述


在这里插入图片描述


因为是要DemoB的TestActivity页面,所以这个activity在AndroidManifest.xml中需要配置


android:exported 属性,布尔类型,是否支持其他应用访问目标 Activity,默认值为 true;android:exported="true"


否则你跳转会报错的,现在运行DemoB,使改动的代码生效
然后修改DemoA里面MainActivity的代码


在这里插入图片描述


运行效果


在这里插入图片描述


这样就可以了。


2.通过Action


修改DemoB的AndroidManifest.xml


在这里插入图片描述


然后运行在手机上,再修改DemoA的MainActivity


在这里插入图片描述


运行效果


在这里插入图片描述


其实还有一种方式是通过URL打开另一个APP,但是我不推荐这样做,为什么?没有原因...


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

内向性格的开发同学,没有合适的工作方法是不行的

一、背景 做软件开发同学的从性格上来说有两类人:外向的、内向的。 外向的人在工作中擅长交流,内向的人在工作中善于总结,两种的人都是开发团队需要的。 外向的人在工作中善于活跃团队内的气氛,逐渐走向技术管理路线,带领团队走的更远,控制开发目标与路线;内向的人更擅长...
继续阅读 »

一、背景


做软件开发同学的从性格上来说有两类人:外向的、内向的。


外向的人在工作中擅长交流,内向的人在工作中善于总结,两种的人都是开发团队需要的。


外向的人在工作中善于活跃团队内的气氛,逐渐走向技术管理路线,带领团队走的更远,控制开发目标与路线;内向的人更擅长观察,容易成为团队的定心骨,逐渐走向技术专家路线,肯研究肯花时间提高自己。



那么,在这个过程中,内向人前期的成长尤为重要,合适的工作方法和习惯也会提高在团队中的地位,而不是单纯的低头干活,本文分享下自己的经验,不一定对希望对大家有参考。


不同的性格的人,具有不同的工作方式和方法,和生活习惯,对于软件开发这个职场环境来说,内向性格不是劣势,很多人外表看着外向,其实潜意识也有很多内向性格的特征。


内向也是人的宝贵的一面,有时也是能力优势的一部分(如善于深度思考等),如果让自己掌握外向同学的行动方式,逐渐的做出改变,会更好。



二、现状


 刚毕业不久进入到职场中工作的毕业生,如果性格是外向的,那么他其实问题并不大,很多的时候,可以快速调整自己,并被其他人看到自己的工作成果,而内向性格的毕业生,如果在职场中没有主动去做某些工作和承担哪些职责,或对自己目前的工作状况没有及时调整和改变,就会造成成长缓慢,有的人会出现明明自己每天努力学习,却还是工作中那个让同时感觉能力最差的,导致经常没有分配到核心的开发工作,长此以往,消极的各种状态就出现了。


比如内向性格的毕业生在初入职场中经常会出现如下症状:


1、明知项目组的工作环境和方式存在一些不健康的因素,自己不太愿意去参与或评论


2、对开发整体流程和环节不清楚,及需求的判断有问题,需求频繁改动,代码写了被删除,自己却不敢说,或说了一次被骂以后沉默了


3、项目组缺失技术经理等全流程人员,需求自己理解和功能设计,自己却没有及时吧自己的想法与他人沟通 ,外包团队更明显


4、身边缺乏可以聊天的mentor、同事,自己感觉开发能力无法提升,却一直憋在心里,产生怀疑


5、不知道工作中如何问同事问题,才愿意帮忙解答,持续很长时间未获得同事的信任


6、有时过于逞强,不想让别人觉得自己不行,不会拒绝,实际工作量与评估有差别,导致自己延误工期。



以上的这些问题,可能不止内向性格的人会有,很多外向的人可能也会有,只是在内向性格的人身上更明显而已,如果内向性格的毕业生,明知道自己有这种情况,却不思考解决办法和改变,长时间后自我开始产生怀疑。 职场中,沟通、反馈、改变是很重要的,但是沟通不一定就是说话,反馈不一定是面对面,而改变是一直要持续去做的。 


之前看过一点得到的沟通训练营的视频教程,感觉里面有些技巧是值得大家去学习的,不仅仅是开发类型的同学。


三、经验分享


 下面我分享下,我的一些经验,可能不太对,但是希望可以帮助到看到这篇文章,深有同感的你。 


问题1:内向性格的毕业生,说的话,或者请求别人的东西,别人听不懂怎么办?


 这里先记住一件事情,在职场中,开发者要学会给不懂技术的人员,讲明白事情,要逐渐学会用生活中的事情去类比。


这个真的很重要,当你给不懂技术人讲的多以后,很多人可能都会来请教你关于某件事的理解,这个通常我们和系统的售前、需求人员、产品人员用的比较多,得学会用生活中的例子或故事去告诉他,XX能做,XX不能做的原因是什么。要坚持去练习。 


 对于请教一些人技术问题时,不管是同事也好还是网友也好,要明确自己给他的这个消息,别人是否会听懂,马上给出解决办法,还是别人看到这个问题以后,还要和我交流1小时才能知道是啥意思,这个也是很多有经验的人,不愿因帮助低级程序员的原因,这里分享下请教问题的描述模板: 


我遇到了一个问题或场景:【问题描述】,我想要实现【X功能】,但是出现了【Y现象】,我经过以下尝试:【思路细节】,但是不能解决,报错如下:【报错信息或截图】,或者我使用【关键词】百度,但是找不到答案,请问我该怎么解决或分析。


 而很多时候有经验的人,也会发现你百度的搜索词不对,这个时候,他根据你的阐述可能会告诉你怎么输入比较靠谱的搜索词来解决办法。 


问题2:评估工作计划有时过于逞强,不想让别人觉得自己不行,不会拒绝


这个真的想说,工作前期真的别逞强,没做过就是没做过,不行就是不行,别找啥接口,但是别直接和负责人说这个东西我不会(这个是很不好的,不能说不会,这是明显不相干的意思),比较合适的说法是:这个东西或概念我暂时不太清楚,没接触过过,需要一会儿或下来后我需要去研究下,然后咱们在沟通或者确定一下。 


 而很多内向性格的毕业生,缺少了这种意识,同时安排某项工作任务时,缺少对任务的分解能力和排期能力和工作后排期后的To do List梳理能力,以至于自己5天完成的任务,口头说2天就搞定了。 


 其实这种,前期mentor该给你做个示范分解的操作,或者自己主动问下,如何分解项目的需求和任务。


 而真正开发的时候,每天可能都感觉这里需要加上XXX功能,那里需要加上YYY功能,但是不知道是否需要做,这里我的建议是,把他加入到自己To do List中,然后找个时间和同事去沟通下这个想法,长此以往,同事的心里,你就是一个有想法的人,虽然不善言辞。


 主要就是这里,我们要体现自己的一个工作的对待方式,而不是一直被动接受,不拒绝,不反馈。 


问题3:明显知道产品经理、项目经理等等人员对需求的认识不足,自己闷着不反馈和说话


 很多时候,任务的返工和需求的变更,有一部分是这个原因的,在经验尚少的情况下,自己未能说出自己对这个需求的认识和怀疑,就去搞了,最后大家都不是特别的好,尤其是在产品需求设计初期,包括需求提出者也是理解不够的,这里可能有很多内容其实是你可以提供的服务,也有一些是产品在犹豫使用哪种方式实现的功能,在与你讨论后,觉得你说的又道理,而决定复用你已经有的系统。 


 很多出入职场的同学,觉得没成长也有这方面的一点原因,自己开发的功能,缺少自己设计思想和认知的影子,如果能在当前系统中体现出了自己的想法,时间久了多少成就感会有点提升的。 


要学会做自己负责的模块/功能的主人,把他们当做自己的孩子一样,主键养成主人翁的意识


问题4:项目组,当前啥都没有,文档、测试,自己也和别人一样不做改变


 这个也是目前很多公司的现状,但是不代表别人不干,你就不干,这个时候,谁主动,谁就能表现一把,同时,这也是被动让同事主动问你或咨询你的机会。


 比如没有协同的东西,那你能不能自己先装个Confluence Wiki或飞书云文档工具,自己先用起来,然后某个时机在同事眼前展示下,自己基于这个软件形成的技术思考、技术经验、技术记录等等等。


比如没有自动发布或代码质量的东西,那你能不能自己先搞个jenkins、sonarqube、checkstyle、findbug,让自己每次写完的代码,自己先搞下,然后某个时机告诉同事这个东西必须这么写怎怎么样。


 是不是有人又说了,工作没时间搞这些东西,你是不是又在扯皮呢,我只能说起码比你空闲时间自己偷偷学习公司短期内用不上的技术或长时间用不上的东西好吧,至少我能非常快速的获得1个同事的信任、2个同事的信任,从而获得团队的信任与核心工作的委派。


大部分人的想用的技术都是和公司的技术栈不搭边的,至少先把脚下的路走出来。


四、总结


 其实最近几年,发现好像很多人被卷字冲昏了头脑,每天都在想着高大尚的技术点和八股文,导致短期的这个工作没干好,还说没成长,以至于某些情况下还被认为是工作和团队中那个能力最差的,即使做了很多的努力。我想说的是,某段时间点或时期内,至少要把当前工作做好在谈论吧,这个在一些内向性格的人身上会表现的明显一些。


IT行业,很多优秀的人也是内向性格的,掌握了合适方法,会让他们成为内向性格顶端的那批优秀的人群。 


说道性格吧,即使是内向型的,可能针对十二星座还是衍生出不同的人生和结果,每个星座的也是有区别的。而在这里面最突出的我觉得是天蝎座的人群。


身为天蝎座的我,经常会想到那些和我一个星座的大佬们:


搜狐创始人张朝阳、腾讯创始人马化腾、百度创始人李彦宏、雅虎创始人杨致远、微软创始人比尔.盖茨、联想集团CEO杨元庆、推特CEO杰克.多尔西、新浪董事长曹国伟。


他们的成长也一直在激励着我。


作者:爱海贼的无处不在
来源:juejin.cn/post/7232625387296096312
收起阅读 »

30 岁了!通过 AI 问答完成了这篇思考文章

大家好,我是 shixin。 岁数越大,越不愿承认自己的真实年龄。前段时间别人问我年纪的时候,我嘴硬的说“二十九周岁”,现在,就只能无奈的说“三十”了。 说来也奇怪,为什么会觉得无奈呢?我想,我是想保留「二十多岁」的青春朝气、心无旁骛,抗拒「三十多岁」的中年危...
继续阅读 »

大家好,我是 shixin。


岁数越大,越不愿承认自己的真实年龄。前段时间别人问我年纪的时候,我嘴硬的说“二十九周岁”,现在,就只能无奈的说“三十”了。


说来也奇怪,为什么会觉得无奈呢?我想,我是想保留「二十多岁」的青春朝气、心无旁骛,抗拒「三十多岁」的中年危机、生活压力。


无论怎样我终究还是和三十岁相遇了,既然逃不掉,那今天就和它聊一聊。


三十岁意味着什么


我拿着这个问题问了 ChatGPT,它根据我的上下文给的回答如下:




可以看到,它给的回答还是蛮好的,基本上道出了现在困扰我的一些点。


三十岁,工作和生活对我的要求更高了。


工作方面,现在需要考虑的比以前更多了一些。除了个人贡献还需要做团队贡献,为自己的小组和整个团队带来更大价值,把自己知道的技术和经验传播给更多小伙伴。


家庭方面,快到要小孩的时候了。理论上三十岁已经年纪不小应该响应国家号召,但无奈生娃养娃的成本太大,还得多奋斗几年才有底气。今年准备先把婚礼办了(疫情影响婚礼日期改了好几次,上帝保佑这次顺利),过两年再考虑要孩子吧。


至于工作生活的平衡,老实讲目前还没有足够的资本,还得在工作上投入大部分时间。如何解决这种情况呢?是个值得思考的问题。


三十岁前我的人生


三十岁前,我的人生里有很多意想不到


十岁的我,没有想到未来我会去包头,更没有想到会在高中遇到现在的老婆。那时的我在呼和浩特,有四五个很要好的朋友,搬家的时候心里有一万个不舍。


十五岁的我,没有想到我会去西安读书,学的是计算机。那时的我还在想方设法溜到网吧通宵打游戏。


二十岁的我,没有想到我会从事安卓开发,也没有想到会去上海工作。那时的我在盲目瞎学,手机上写 OJ,看小甲鱼和黑马程序员,图书馆借了几本很老的 MFC 和 HTML CSS 书,跟着例子敲出来一个 H5 打飞机游戏。


二十五岁的我,没有想到我会在上海定居。那时我想的是干几年去西安定居,在那里离老家近一点,买房压力也小一点。后来机缘巧合,在买房时和几个前辈朋友聊了聊,听了他们的劝导,改成在上海定居。




ChatGPT 的这段回答让我泪目。有时候打的字越多,越渴望得到认可的回复,这种感觉,它给到了。



三十岁的我,虽然没有 100% 达到五年前预想的目标,但好在完成了一些当时觉得很难的事,比如买房、写书、直播分享,这些事是我成长的见证,也让我沉淀下一些经验和教训。


希望自己可以继续保持的


我希望自己继续保持的第一个点:在损失可以接受的情况下,多尝试多探索。


之前打德扑的时候,我属于比较激进和浪的那种,这种性格的缺点是会浪费很多筹码,但优点是过程很有趣,也常常会博到一些额外的收益。


生活里也是类似,在大学做小生意的时候,我愿意多跑几家店看看有没有价格更合适的货,也愿意多推开一扇门去问一下是否有需求,虽然收到不少白眼、也没赚大钱,但这段经历让我意识到:反正被拒绝也没什么损失,多试一次就多一个机会。


第二个需要继续保持的点:多种善因。


过往人生的关键节点,让我深刻的感受到:当下的果,往往来自过去不经意间种下的因。


就拿今年的几件事来说:



  1. 二月有机会在社区里做分享,缘自去年国庆主动报名 GDE 项目,虽然没通过筛选,但好在建立了联系,有这种机会时人家才会想到我



  1. 上周组里做的 ReactNative 技术培训,缘自字节时做的 Diggo 项目,在其中提升了前端开发技术,以至于后面做 RN 很顺畅,从而走在团队前头


今年很多事都是之前种下的善因结出的果实,除了满足,还需要多想想:



  1. 怎样为以后种下更多善因



  1. 现在要做的事,从长期来看,重复多次后的收益是怎样的



第三个需要继续保持的点:每日、每周、每年必做计划。


每日预则立,不立则废。我是一个善忘的人,如果哪天没有定好计划,基本上就稀里糊涂的过去了。首次发现这个问题,是我写2016 年度总结的时候,回顾发现好多细节都不记得了,有的月份里可能只记得一两件事,剩下的日子都进了黑洞无影无踪。


从那以后我就经常做记录、做计划,既然内存不够用,那就用磁盘缓存。做好每天的计划后,即使被突发事情分了心,我也可以及时调整状态回归高优。在日积月累下,才渐渐地完成了一件件看似很难的事,比如一篇有价值的文章、一个高质量的开源库(github.com/shixinzhang…)。



希望自己可以避免的


除了需要继续保持的,我也有很多后悔的事,比如做错事、说错话、浪费时间。


总结原因后,大概有这几点需要避免:



  1. 避免思想上的懒惰,少说这样的话:没办法、算了、就这样吧;多说:我试试、或许这样做就可以



  1. 避免和他人比较,比别人优秀或者差都不重要,重要的是有没有持续前进



  1. 避免没有进展的时候硬逼自己,多思考方向、方法是不是有问题



  1. 避免花钱的时候只看价钱,不对比购买后的体验和长期区别



  1. 避免做计划的时候过于悲观,目标定高点才可能做的更好



  1. 避免追求完美而不愿意开始,做完比做好优先级更高



  1. 避免在累的时候不休息,贪图享乐而继续浑浑噩噩




  1. 避免骄傲自满、自我膨胀,骄傲一来羞耻就来了




大胆想象一下,三十五岁的我


借用亚马逊的逆向工作法,先想象一下我 35 岁的情况:



  1. 第一种可能:独立开发了某个产品,为细分领域的人提供了独特的价值,从而获得不错的收益,业务比较忙的时候雇佣了几个助手



  1. 第二种可能:继续打工,但因为技术较好、沟通表达能力不错、有商业思维,担任某个业务的技术负责人



  1. 第三种可能:因为工作经验和年纪薪资不匹配被裁,投简历基本没有回复,最后忍痛降薪 50% 接了个 offer


要达到第一种情况,需要具备技术广度,可以独立完成产品的需求调研、设计、全栈开发和运营,更重要的是,尽早捕捉到信息,挖掘出其中的信息不平衡点或者需求点。这种情况对人的要求更高、风险也更高。


要达到第二种情况,需要付出的努力比上面略微少一点,需要具备一定的技术深度和广度、提升对公司业务和行业趋势的了解,主导完成一些有价值的事,同时在公司内部有一定的影响力。这种情况比第一种更稳一点。


要避免第三种情况,需要经常了解市场相关岗位的要求,不断提升自己的技术和业务价值以匹配要求,最好有代表性的作品和影响力。


总结


这篇文章是我三十岁当天开始动笔写的,因为种种原因拖到今天才完成,实在不应该(捂脸哭。


总是听人讲“三十而立”,为了看自己到底立没立,我看了好些名人的视频,想从中寻找答案。



到现在我悟了,所谓的“立“就是建立、确定、稳固。人活着最重要的就是吃饱和开心,三十岁,能够有一技之长和自我融洽的三观,就算是立住了吧!



作者:张拭心
来源:juejin.cn/post/7210386831451357221
收起阅读 »

Go 开发短网址服务笔记

这篇文章是基于课程 Go 开发短地址服务 做的笔记 项目地址:github.com/astak16/sho… 错误处理 在处理业务逻辑时,如果出错误了,需要统一处理错误响应的格式,这样可以方便前端处理错误信息 所以需要定义一个 Error 接口,它包含了 er...
继续阅读 »

这篇文章是基于课程 Go 开发短地址服务 做的笔记


项目地址:github.com/astak16/sho…


错误处理


在处理业务逻辑时,如果出错误了,需要统一处理错误响应的格式,这样可以方便前端处理错误信息


所以需要定义一个 Error 接口,它包含了 error 接口,以及一个 Status() 方法,用来返回错误的状态码


type Error interface {
error
Status() int
}

这个接口用来判断错误类型,在 go 中可以通过 e.(type) 判断错误的类型


func respondWithError(w http.RespondWrite, err error) {
switch e.(type) {
case Error:
respondWithJSON(w, e.Status(), e.Error())
default:
respondWithJSON(w, http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
}

go 中 实现 Error 接口,只需要实现 Error()Status() 方法即可


func () Error() string {
return ""
}
func () Status() int {
return 0
}

这样定义的方法,只能返回固定的文本和状态码,如果想要返回动态内容,可以定义一个结构体


然后 ErrorStatus 方法接受 StatusError 类型


这样只要满足 StatusError 类型的结构体,就可以返回动态内容


所以上面的代码可以修改为:


type StatusError struct {
Code int
Err error
}
func (se StatusError) Error() string {
return se.Err.Error()
}
func (se StatusError) Status() int {
return se.Code
}

middlerware


RecoverHandler


中间件 RecoverHandler 作用是通过 defer 来捕获 panic,然后返回 500 状态码


func RecoverHandler(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
defer func() {
if r := recover(); r != nil {
log.Println("Recover from panic %+v", r)
http.Error(w, http.StatusText(500), 500)
}
}()
next.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}

LoggingHandler


LoggingHandler 作用是记录请求耗时


func (m Middleware) LoggingHandler(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
end := time.Now()
log.Printf("[%s] %q %v", r.Method, r.URL.Path, end.Sub(start))
}
return http.HandlerFunc(fn)
}

中间件使用


alicego 中的一个中间件库,可以通过 alice.New() 来添加中间件,具体使用如下:


m := alice.New(middleware.LoggingHandler, middleware.RecoverHandler)
mux.Router.HandleFunc("/api/v1/user", m.ThenFunc(controller)).Methods("POST")

生成短链接


redis 连接


func NewRedisCli(addr string, passwd string, db int) *RedisCli {
c := redis.NewClient(&redis.Options{
Addr: addr,
Password: passwd,
DB: db,
})

if _, err := c.Ping().Result(); err != nil {
panic(err)
}
return &RedisCli{Cli: c}
}

生成唯一 ID


redis 可以基于一个键名生成一个唯一的自增 ID,这个键名可以是任意的,这个方法是 Incr


代码如下:


err = r.Cli.Incr(URLIDKEY).Err()
if err != nil {
return "", err
}

id, err := r.Cli.Get(URLIDKEY).Int64()
if err != nil {
return "", err
}

fmt.Println(id) // 每次调用都会自增

存储和解析短链接


一个 ID 对应一个 url,也就是说当外面传入 id 时需要返回对应的 url


func Shorten() {
err := r.Cli.Set(fmt.Sprintf(ShortlinkKey, eid), url, time.Minute*time.Duration(exp)).Err()
if err != nil {
return "", err
}
}
func UnShorten() {
url, err := r.Cli.Get(fmt.Sprintf(ShortlinkKey, eid)).Result()
}

redis 注意事项


redis 返回的 error 有两种情况:



  1. redis.Nil 表示没有找到对应的值

  2. 其他错误,表示 redis 服务出错了


所以在使用 redis 时,需要判断返回的错误类型


if err == redis.Nil {
// 没有找到对应的值
} else if err != nil {
// redis 服务出错了
} else {
// 正确响应
}

测试


在测试用例中,如何发起一个请求,然后获取响应的数据呢?



  1. 构造请求


var jsonStr = []byte(`{"url":"https://www.baidu.com","expiration_in_minutes":60}`)
req, err := http.NewRequest("POST", "/api/shorten", bytes.NewBuffer(jsonStr))
if err != nil {
t.Fatal(err)
}
req.Header.Set("Content-Type", "application/json")


  1. 捕获 http 响应


rw := httptest.NewRecorder()


  1. 模拟请求被处理


app.Router.ServeHTTP(rw, req)


  1. 解析响应


if rw.Code != http.ok {
t.Fatalf("Excepted status created, got %d", rw.Code)
}

resp := struct {
Shortlink string `json:"shortlink"`
}{}
if err := json.NewDecoder(rw.Body).Decode(&resp); err != nil {
t.Fatalf("should decode the response", err)
}

最终完整代码:


var jsonStr = []byte(`{"url":"https://www.baidu.com","expiration_in_minutes":60}`)
req, err := http.NewRequest("POST", "/api/shorten", bytes.NewBuffer(jsonStr))
if err != nil {
t.Fatal(err)
}
req.Header.Set("Content-Type", "application/json")

rw := httptest.NewRecorder()
app.Router.ServeHTTP(rw, req)

if rw.Code != http.ok {
t.Fatalf("Excepted status created, got %d", rw.Code)
}
resp := struct {
Shortlink string `json:"shortlink"`
}{}

if err := json.NewDecoder(rw.Body).Decode(&resp); err != nil {
t.Fatalf("should decode the response")
}

代码


log.SetFlags(log.LstdFlags | log.Lshortfile)


作用是设置日志输出的标志


它们都是标志常量,用竖线 | 连接,这是位操作符,将他们合并为一个整数值,作为 log.SetFlags() 的参数



  • log.LstdFlags 是标准时间格式:2022-01-23 01:23:23

  • log.Lshortfile 是文件名和行号:main.go:23


当我们使用 log.Println 输出日志时,会自动带上时间、文件名、行号信息


recover 函数使用


recover 函数类似于其他语言的 try...catch,用来捕获 panic,做一些处理


使用方法:


func MyFunc() {
defer func() {
if r := recover(); r != nil {
// 处理 panic 情况
}
}
}

需要注意的是:



  1. recover 函数只能在 defer 中使用,如果在 defer 之外使用,会直接返回 nil

  2. recover 函数只有在 panic 之后调用才会生效,如果在 panic 之前调用,也会直接返回 nil

  3. recover 函数只能捕获当前 goroutinepanic,不能捕获其他 goroutinepanic


next.ServerHttp(w, r)


next.ServeHTTP(w, r),用于将 http 请求传递给下一个 handler


HandleFunc 和 Handle 区别


HandleFunc 接受一个普通类型的函数:


func myHandle(w http.ResponseWriter, r *http.Request) {}
http.HandleFunc("xxxx", myHandle)

Handle 接收一个实现 Handler 接口的函数:


func myHandler(w http.ResponseWriter, r *http.Request) {}
http.Handle("xxxx", http.HandlerFunc(myHandler))

他们的区别是:使用 Handle 需要自己进行包装,使用 HandleFunc 不需要


defer res.Body.Close()


为什么没有 res.Header.Close() 方法?


因为 header 不是资源,而 body 是资源,在 go 中,一般操作资源后,要及时关闭资源,所以 gobody 提供了 Close() 方法


res.Bodyio.ReadCloser 类型的接口,表示可以读取响应数据并关闭响应体的对象


w.Write()


代码在执行了 w.Writer(res) 后,还会继续往下执行,除非有显示的 returepanic 终止函数执行


func controller(w http.ResponseWriter, r *http.Request) {
if res, err := xxx; err != nil {
respondWithJSON(w, http.StatusOK, err)
}
// 这里如果有代码,会继续执行
}
func respondWithJSON(w http.ResponseWriter, code int, payload interface{}) {
res, _ json.Marshal(payload)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
w.Write(res)
}

需要注意的是,尽管执行了 w.Writer() 后,还会继续往下执行,但不会再对响应进行修改或写入任何内容了,因为 w.Write() 已经将响应写入到 http.ResponseWriter 中了


获取请求参数


路由 /api/info?shortlink=2


a.Router.Handle("/api/info", m.ThenFunc(a.getShortlinkInfo)).Methods("GET")

func getShortlinkInfo(w http.ResponseWriter, r *http.Request) {
vals := r.URL.Query()
s := vals.Get("shortlink")

fmt.Println(s) // 2
}

路由 /2


a.Router.Handle("/{shortlink:[a-zA-Z0-9]{1,11}}", m.ThenFunc(a.redirect)).Methods("GET")

func redirect(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
shortlink := vars["shortlink"]

fmt.Println(shortlink) // 2
}

获取请求体


json.NewDecoder(r.Body) 作用是将 http 请求的 body 内容解析为 json 格式


r.body 是一个 io.Reader 类型,它代表请求的原始数据


如果关联成功可以用 Decode() 方法来解析 json 数据


type User struct {
Name string `json:"name"`
Age int `json:"age"`
}

func controller(w http.ResponseWriter, r *http.Request){
var user User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
fmt.Println(err)
}
fmt.Println(user)
}

new


用于创建一个新的零值对象,并返回该对象的指针


它接受一个类型作为参数,并返回一个指向该类型的指针


适用于任何可分配的类型,如基本类型、结构体、数组、切片、映射和接口等


// 创建一个新的 int 类型的零值对象,并返回指向它的指针
ptr := new(int) // 0

需要注意的是:new 只分配了内存,并初始化为零值,并不会对对象进行任何进一步的初始化。如果需要对对象进行自定义的初始化操作,可以使用结构体字面量或构造函数等方式


往期文章



  1. Go 项目ORM、测试、api文档搭建

  2. Go 实现统一加载资源的入口<
    作者:uccs
    来源:juejin.cn/post/7237702874880393274
    /a>

收起阅读 »

2023了,该用一下pnpm了

web
前言 大家好,我是 simple ,我的理想是利用科技手段来解决生活中遇到的各种问题。 performant npm ,意味高性能的 npm。pnpm由 npm/yarn 衍生而来,解决了 npm/yarn 内部潜在的bug,极大的优化了性能,扩展了使用场景。...
继续阅读 »

前言


大家好,我是 simple ,我的理想是利用科技手段来解决生活中遇到的各种问题


performant npm ,意味高性能的 npm。pnpm由 npm/yarn 衍生而来,解决了 npm/yarn 内部潜在的bug,极大的优化了性能,扩展了使用场景。被誉为"最先进的包管理工具"。


npm,yarn,pnpm的安装区别


首先我创建了三个文件夹分别是npm,yarn和pnpm用于比较三者之间的区别。首先初始化项目,然后安装了express来观察三个文件夹的区别。


npm和yarn的node_modules都是点开之后一眼看不到尽头。


image.png image.png


pnpm的node_modules略有区别。


image.png


npm/yarn 包结构分析


出现这种情况,是因为yarn和npm安装依赖包,会在node_modules下都平铺出来。现在安装了express,但express中也会有很多不同的依赖。在这些依赖里面有可能又引用了新的依赖,导致node_modules一点开就一望无际了。


初心是好的,因为平铺就可以复用很多依赖。如果说我package Apackage B都用了lodash@3.0.0这个包,那么使用平铺,我只需要下载一次lodash即可,节约了装包时间和存储空间。


但是真实开发情况往往是现在下载了五个依赖,其中A,B依赖引用了lodash@3.0.0,而C,D,E引用了lodash@4.0.0。咋整?


npmyarn目前给出的方案是将其中一个版本,(假设是)lodash@3.0.0的版本放在根目录的node_modules下面,而将需要lodash@4.0.0版本安装到C,D,E的node_modules下。如下所示,A, B可以直接使用lodash@3.0.0,而4,5,6想要使用就只能独自安装lodash@4.0.0


├─── lodash@3.0.0
├─── package-A
├─── package-B
├─── package-C
│ └── lodash@4.0.0
├─── package-D
│ └── lodash@4.0.0
├─── package-E
│ └── lodash@4.0.0

pnpm包结构分析


按照上文的例子,如果pnpm也安装五个包,A,B依赖引用了lodash@3.0.0,而C,D,E引用了lodash@4.0.0


├─ .pnpm
│ └── lodash@3.0.0
│ └── lodash@4.0.0
│ └── package-A@1.0.0
│ └── package-B@1.0.0
│ └── package-C@1.0.0
│ └── package-D@1.0.0
│ └── package-E@1.0.0
├──── package-A 符号链接
├──── package-B 符号链接
├──── package-C 符号链接
├──── package-D 符号链接
├──── package-E 符号链接

pnpm文件夹的node_modules下除了.pnpm文件夹外,就剩下一个package A,B,C,D,E,并且这五个包都是符号链接,它们真正的地址都是在.pnpm下。


也就是说,pnpm通过.pnpm/<name>@<version>/node_modules/<name>找到不同的包,这不仅解决了包重复下载的问题,还顺手解决了幽灵依赖的问题。



幽灵依赖:即开发者并未在package.json中下载相关包,但是在开发过程中却可以直接引用的问题。就是因为npm将依赖直接在node_modules下直接展开,导致开发者可以直接引用。问题就是当开发者升级一些包的时候,那些幽灵依赖可能并不存在于新的版本中,导致项目崩溃。



.pnpm store


pnpm牛皮的地方不只是借用了符号链接解决了包引用的问题,更是借助了硬链接解决了整个直接所有的项目依赖都给整合了,一个包全局只保存一份,并且是通过链接,速度比复制还要快的多。


借一张pnpm官网的图。


image.png


从图可以看出,.pnpm store就是依赖的实际存储位置,Mac/linux在{home dir}>/.pnpm-store/v3,windows在当前盘/.pnpm-store/v3。这样就会有个好处,你在多个项目使用的是同一个依赖时,一个包全局只保存一份,这也太省空间了吧。(只要你下载过一次,如果你没有清理.pnpm store,第二次就算你不联网照样能帮你install。)


pnpm store清理


但是,随着使用时间越长,pnpm store也会越来越大。并且随着项目版本的迭代,可能很多包都不再需要了pnpm store依旧会保留着它。此时我们需要定时清理一下。


未引用的包是系统上的任何项目中都未使用的包。 在大多数安装操作之后,包有可能会变为未引用状态,例如当依赖项变得多余时。


最好的做法是 pnpm store prune 来清理存储,但不要太频繁。 有时,未引用的包会再次被需要。 这可能在切换分支和安装旧的依赖项时发生,在这种情况下,pnpm 需要重新下载所有删除的包,会暂时减慢安装过程。


请注意,当 存储服务器正在运行时,这个命令是禁止使用的。


pnpm store prune

作者:simple_lau
来源:juejin.cn/post/7237856777588670521
收起阅读 »

原型模式与享元模式

原型模式与享元模式 原型模式和享元模式,前者是在创建多个实例时,对创建过程的性能进行调优;后者是用减少创建实例的方式,来调优系统性能。这么看,你会不会觉得两个模式有点相互矛盾呢? 其实不然,它们的使用是分场景的。在有些场景下,我们需要重复创建多个实例,例如在循...
继续阅读 »

原型模式与享元模式


原型模式和享元模式,前者是在创建多个实例时,对创建过程的性能进行调优;后者是用减少创建实例的方式,来调优系统性能。这么看,你会不会觉得两个模式有点相互矛盾呢?


其实不然,它们的使用是分场景的。在有些场景下,我们需要重复创建多个实例,例如在循环体中赋值一个对象,此时我们就可以采用原型模式来优化对象的创建过程;而在有些场景下,我们则可以避免重复创建多个实例,在内存中共享对象就好了。


今天我们就来看看这两种模式的适用场景,看看如何使用它们来提升系统性能。


原型模式


原型模式是通过给出一个原型对象来指明所创建的对象的类型,然后使用自身实现的克隆接口来复制这个原型对象,该模式就是用这种方式来创建出更多同类型的对象。


使用这种方式创建新的对象的话,就无需再通过new实例化来创建对象了。这是因为Object类的clone方法是一个本地方法,它可以直接操作内存中的二进制流,所以性能相对new实例化来说,更佳。


实现原型模式


我们现在通过一个简单的例子来实现一个原型模式:


   //实现Cloneable 接口的原型抽象类Prototype
class Prototype implements Cloneable {
//重写clone方法
public Prototype clone(){
Prototype prototype = null;
try{
prototype = (Prototype)super.clone();
}catch(CloneNotSupportedException e){
e.printStackTrace();
}
return prototype;
}
}
//实现原型类
class ConcretePrototype extends Prototype{
public void show(){
System.out.println("原型模式实现类");
}
}

public class Client {
public static void main(String[] args){
ConcretePrototype cp = new ConcretePrototype();
for(int i=0; i< 10; i++){
ConcretePrototype clonecp = (ConcretePrototype)cp.clone();
clonecp.show();
}
}
}


要实现一个原型类,需要具备三个条件:



  • 实现Cloneable接口:Cloneable接口与序列化接口的作用类似,它只是告诉虚拟机可以安全地在实现了这个接口的类上使用clone方法。在JVM中,只有实现了Cloneable接口的类才可以被拷贝,否则会抛出CloneNotSupportedException异常。

  • 重写Object类中的clone方法:在Java中,所有类的父类都是Object类,而Object类中有一个clone方法,作用是返回对象的一个拷贝。

  • 在重写的clone方法中调用super.clone():默认情况下,类不具备复制对象的能力,需要调用super.clone()来实现。


从上面我们可以看出,原型模式的主要特征就是使用clone方法复制一个对象。通常,有些人会误以为 Object a=new Object();Object b=a; 这种形式就是一种对象复制的过程,然而这种复制只是对象引用的复制,也就是a和b对象指向了同一个内存地址,如果b修改了,a的值也就跟着被修改了。


我们可以通过一个简单的例子来看看普通的对象复制问题:


class Student {
private String name;

public String getName() {
return name;
}

public void setName(String name) {
this.name= name;
}

}
public class Test {

public static void main(String args[]) {
Student stu1 = new Student();
stu1.setName("test1");

Student stu2 = stu1;
stu2.setName("test2");

System.out.println("学生1:" + stu1.getName());
System.out.println("学生2:" + stu2.getName());
}
}


如果是复制对象,此时打印的日志应该为:


学生1:test1
学生2:test2


然而,实际上是:


学生1:test2
学生2:test2


通过clone方法复制的对象才是真正的对象复制,clone方法赋值的对象完全是一个独立的对象。刚刚讲过了,Object类的clone方法是一个本地方法,它直接操作内存中的二进制流,特别是复制大对象时,性能的差别非常明显。我们可以用 clone 方法再实现一遍以上例子。


//学生类实现Cloneable接口
class Student implements Cloneable{
private String name; //姓名

public String getName() {
return name;
}

public void setName(String name) {
this.name= name;
}
//重写clone方法
public Student clone() {
Student student = null;
try {
student = (Student) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return student;
}

}
public class Test {

public static void main(String args[]) {
Student stu1 = new Student(); //创建学生1
stu1.setName("test1");

Student stu2 = stu1.clone(); //通过克隆创建学生2
stu2.setName("test2");

System.out.println("学生1:" + stu1.getName());
System.out.println("学生2:" + stu2.getName());
}
}


运行结果:


学生1:test1
学生2:test2


深拷贝和浅拷贝


在调用super.clone()方法之后,首先会检查当前对象所属的类是否支持clone,也就是看该类是否实现了Cloneable接口。


如果支持,则创建当前对象所属类的一个新对象,并对该对象进行初始化,使得新对象的成员变量的值与当前对象的成员变量的值一模一样,但对于其它对象的引用以及List等类型的成员属性,则只能复制这些对象的引用了。所以简单调用super.clone()这种克隆对象方式,就是一种浅拷贝。


所以,当我们在使用clone()方法实现对象的克隆时,就需要注意浅拷贝带来的问题。我们再通过一个例子来看看浅拷贝。


//定义学生类
class Student implements Cloneable{
private String name; //学生姓名
private Teacher teacher; //定义老师类

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Teacher getTeacher() {
return teacher;
}

public void setTeacher(Teacher teacher) {
this.teacher = teacher;
}
//重写克隆方法
public Student clone() {
Student student = null;
try {
student = (Student) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return student;
}

}

//定义老师类
class Teacher implements Cloneable{
private String name; //老师姓名

public String getName() {
return name;
}

public void setName(String name) {
this.name= name;
}

//重写克隆方法,对老师类进行克隆
public Teacher clone() {
Teacher teacher= null;
try {
teacher= (Teacher) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return student;
}

}
public class Test {

public static void main(String args[]) {
Teacher teacher = new Teacher (); //定义老师1
teacher.setName("刘老师");
Student stu1 = new Student(); //定义学生1
stu1.setName("test1");
stu1.setTeacher(teacher);

Student stu2 = stu1.clone(); //定义学生2
stu2.setName("test2");
stu2.getTeacher().setName("王老师");//修改老师
System.out.println("学生" + stu1.getName + "的老师是:" + stu1.getTeacher().getName);
System.out.println("学生" + stu1.getName + "的老师是:" + stu2.getTeacher().getName);
}
}


运行结果:


学生test1的老师是:王老师
学生test2的老师是:王老师


观察以上运行结果,我们可以发现:在我们给学生2修改老师的时候,学生1的老师也跟着被修改了。这就是浅拷贝带来的问题。


我们可以通过深拷贝来解决这种问题,其实深拷贝就是基于浅拷贝来递归实现具体的每个对象,代码如下:


   public Student clone() {
Student student = null;
try {
student = (Student) super.clone();
Teacher teacher = this.teacher.clone();//克隆teacher对象
student.setTeacher(teacher);
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return student;
}


适用场景


前面我详述了原型模式的实现原理,那到底什么时候我们要用它呢?


在一些重复创建对象的场景下,我们就可以使用原型模式来提高对象的创建性能。例如,我在开头提到的,循环体内创建对象时,我们就可以考虑用clone的方式来实现。


例如:


for(int i=0; i<list.size(); i++){
Student stu = new Student();
...
}


我们可以优化为:


Student stu = new Student();
for(int i=0; i<list.size(); i++){
Student stu1 = (Student)stu.clone();
...
}


除此之外,原型模式在开源框架中的应用也非常广泛。例如Spring中,@Service默认都是单例的。用了私有全局变量,若不想影响下次注入或每次上下文获取bean,就需要用到原型模式,我们可以通过以下注解来实现,@Scope(“prototype”)。


享元模式


享元模式是运用共享技术有效地最大限度地复用细粒度对象的一种模式。该模式中,以对象的信息状态划分,可以分为内部数据和外部数据。内部数据是对象可以共享出来的信息,这些信息不会随着系统的运行而改变;外部数据则是在不同运行时被标记了不同的值。


享元模式一般可以分为三个角色,分别为 Flyweight(抽象享元类)、ConcreteFlyweight(具体享元类)和 FlyweightFactory(享元工厂类)。抽象享元类通常是一个接口或抽象类,向外界提供享元对象的内部数据或外部数据;具体享元类是指具体实现内部数据共享的类;享元工厂类则是主要用于创建和管理享元对象的工厂类。


实现享元模式


我们还是通过一个简单的例子来实现一个享元模式:


//抽象享元类
interface Flyweight {
//对外状态对象
void operation(String name);
//对内对象
String getType();
}


//具体享元类
class ConcreteFlyweight implements Flyweight {
private String type;

public ConcreteFlyweight(String type) {
this.type = type;
}

@Override
public void operation(String name) {
System.out.printf("[类型(内在状态)] - [%s] - [名字(外在状态)] - [%s]\n", type, name);
}

@Override
public String getType() {
return type;
}
}


//享元工厂类
class FlyweightFactory {
private static final Map<String, Flyweight> FLYWEIGHT_MAP = new HashMap<>();//享元池,用来存储享元对象

public static Flyweight getFlyweight(String type) {
if (FLYWEIGHT_MAP.containsKey(type)) {//如果在享元池中存在对象,则直接获取
return FLYWEIGHT_MAP.get(type);
} else {//在响应池不存在,则新创建对象,并放入到享元池
ConcreteFlyweight flyweight = new ConcreteFlyweight(type);
FLYWEIGHT_MAP.put(type, flyweight);
return flyweight;
}
}
}


public class Client {

public static void main(String[] args) {
Flyweight fw0 = FlyweightFactory.getFlyweight("a");
Flyweight fw1 = FlyweightFactory.getFlyweight("b");
Flyweight fw2 = FlyweightFactory.getFlyweight("a");
Flyweight fw3 = FlyweightFactory.getFlyweight("b");
fw1.operation("abc");
System.out.printf("[结果(对象对比)] - [%s]\n", fw0 == fw2);
System.out.printf("[结果(内在状态)] - [%s]\n", fw1.getType());
}
}


输出结果:


[类型(内在状态)] - [b] - [名字(外在状态)] - [abc]
[结果(对象对比)] - [true]
[结果(内在状态)] - [b]


观察以上代码运行结果,我们可以发现:如果对象已经存在于享元池中,则不会再创建该对象了,而是共用享元池中内部数据一致的对象。这样就减少了对象的创建,同时也节省了同样内部数据的对象所占用的内存空间。


适用场景


享元模式在实际开发中的应用也非常广泛。例如Java的String字符串,在一些字符串常量中,会共享常量池中字符串对象,从而减少重复创建相同值对象,占用内存空间。代码如下:


 String s1 = "hello";
String s2 = "hello";
System.out.println(s1==s2);//true


还有,在日常开发中的应用。例如,池化技术中的线程池就是享元模式的一种实现;将商品存储在应用服务的缓存中,那么每当用户获取商品信息时,则不需要每次都从redis缓存或者数据库中获取商品信息,并在内存中重复创建商品信息了。


总结


原型模式和享元模式,在开源框架,和实际开发中,应用都十分广泛。


在不得已需要重复创建大量同一对象时,我们可以使用原型模式,通过clone方法复制对象,这种方式比用new和序列化创建对象的效率要高;在创建对象时,如果我们可以共用对象的内部数据,那么通过享元模式共享相同的内部数据的对象,就可以减少对象的创建,实现系统调优。


本文由mdnic

e多平台发布

收起阅读 »

Java字符串常量池和intern方法解析

Java字符串常量池和intern方法解析 这篇文章,来讨论一下Java中的字符串常量池以及Intern方法.这里我们主要讨论的是jdk1.7,jdk1.8版本的实现. 字符串常量池 在日常开发中,我们使用字符串非常的频繁,我们经常会写下类似如下的代码: S...
继续阅读 »

Java字符串常量池和intern方法解析


这篇文章,来讨论一下Java中的字符串常量池以及Intern方法.这里我们主要讨论的是jdk1.7,jdk1.8版本的实现.


字符串常量池


在日常开发中,我们使用字符串非常的频繁,我们经常会写下类似如下的代码:



  • String s = "abc";

  • String str = s + "def";


通常,我们一般不会这么写:String s = new String("jkl"),但其实这么写和上面的写法还是有很多区别的.


先思考一个问题,为什么要有字符串常量池这种概念?原因是字符串常量既然是不变的,那么完全就可以复用它,而不用再重新去浪费空间存储一个完全相同的字符串.字符串常量池是用于存放字符串常量的地方,在Java7和Java8中字符串常量池是堆的一部分.


假如我们有如下代码:


String s = "abc";
String s1 = s + "def";    
String s2 = new String("abc");
String s3 = new String("def");

那么从内存分配的角度上看,最终会有哪些字符串生成呢,首先我先给出一张图来代表最终的结论,然后再分析一下具体的原因:


image.png


现在来依次分析上面的代码的执行流程:




  1. 执行String s = "abc",此时遇到"abc"这个字符串常量,会在字符串常量池中完成分配,并且会将引用赋值给s,因此这条语句会在字符串常量池中分配一个"abc".(这里其实没有空格,是因为生成文章时出现了空格,下文中如果出现同样情况,请忽略空格)




  2. 执行String s1 = s + "def",其实这个语句看似简单,实则另有玄机,它其实最终编译而成的代码是这样的:String s1 = new StringBuilder("abc").append("def").toString().首先在这个语句中有两个字符串常量:"abc"和"def",所以在字符串常量池中应该放置"abc"和"def",但是上个步骤已经有"abc"了,所以只会放置"def".另外,new StringBuilder("abc")这个语句相当于在堆上分配了一个对象,如果是new出来的,是在堆上分配字符串,是无法共享字符串常量池里面的字符串的,也就是说分配到堆上的字符串都会有新的内存空间. 最后toString()也是在堆中分配对象(可以从源码中看到这个动作),最终相当于执行了new String("abcdef");所以总结起来,这条语句分析起来还是挺麻烦的,它分配了以下对象:



    • 在字符串常量池分配"abc",但本来就有一个"abc"了,所以不需要分配

    • 在字符串常量池中分配“def"

    • 在堆中分配了"abc"

    • 在堆中分配了"abcdef"




  3. 执行String s2 = new String("abc").首先有个字符串常量"abc",需要分配到字符串常量池,但是字符串常量池中已经有"abc"了,所以无需分配.因此new String("abc")最终在堆上分配了一个"abc".所以总结起来就是,在堆中分配了一个"abc"




  4. 执行String s3 = new String("def");.首先有个字符串常量"def",需要分配到字符串常量池,但是字符串常量池中已经有"def"了,所以无需分配.因此new String("def")最终在堆上分配了一个"def".所以总结起来就是,在堆中分配了一个"def"。




总结起来,全部语句执行后分配的对象如下:



  • 在堆中分配了两个"abc",一个"abcdef",一个"def"

  • 在字符串常量池中分配了一个"abc",一个"def"


也就是图中所表示的这些对象,如果明白了对象是如何分配的,我们就可以分析以下代码的结果:


String s = "abc";
String s1 = s + "def";    
String s2 = new String("abc");
String s3 = new String("def");
String s4 = "abcdef";
String s5 = "abc";
System.out.println(s == s2); //false 前者引用的对象在字符串常量池 后者在堆上
System.out.println(s == s5);; //true 都引用了字符串常量池中的"abc"
System.out.println(s1 == s4); //false 前者引用的对象在字符串常量池,后者在堆上

intern方法


在字符串对象中,有一个intern方法.在jdk1.7,jdk1.8中,它的定义是如果调用这个方法时,在字符串常量池中有对应的字符串,那么返回字符串常量池中的引用,否则返回调用时相应对象的引用,也就是说intern方法在jdk1.7,jdk1.8中只会复用某个字符串的引用,这个引用可以是对堆内存中字符串中的引用,也可能是对字符串常量池中字符串的引用.这里通过一个例子来说明,假如我们有下面这段代码:


String str = new String("abc");
String str2 = str.intern();
String str3 = new StringBuilder("abc").append("def").toString();
String str4 = str3.intern();
System.out.println(str == str2);
System.out.println(str3 == str4);  

那么str2和str以及str3和str4是否相等呢?如果理解了上面对字符串常量池的分析,那么我们可以明白在这段代码中,字符串在内存中是这么分配的:



  • 在堆中分配两个"abc",一个“abcdef"

  • 在字符串常量池中分配一个"def",一个"abc"



  1. 当执行String str2 = str.intern();时,会先从字符串常量池中寻找是否有对应的字符串,此时在字符串常量池中有一个"abc",那么str2就指向字符串常量池中的"abc",而str是new出来的,指向的是堆中的"abc",所以str不等于str2;



  1. 当执行String str4 = str3.intern();会先从字符串常量池中寻找"abcdef",此时字符串常量池中并没有"abcdef",因此str4会指向堆中的"abcdef",因此str3等于str4,我们会发现一个有意思的地方:如果将第三句改成String str3 = new StringBuilder("abcdef").toString();,也就是把append后面的字符串和前面的字符串做一个拼接,那么结果就会变成str3不等于str4.所以这两种写法的区别还是挺大的.


要注意的是,在jdk1.6中intern的定义是如果字符串常量池中没有对应的字符串,那么就在字符串常量池中创建一个字符串,然后返回字符串常量池中的引用,也就是说在jdk1.6中,intern方法返回的对象始终都是指向字符串常量池的.如果上面的代码在jdk1.6中运行,那么就会得到两个false,原因如下:



  1. 当执行String str2 = str.intern();时,会先从字符串常量池中寻找是否有对应的字符串,此时在字符串常量池中有一个"abc",那么str2就指向字符串常量池中的"abc",而str是new出来的,指向的是堆中的"abc",所以str不等于str2;



  1. 当执行String str4 = str3.intern();会先从字符串常量池中寻找"abcdef",此时字符串常量池中并没有"abcdef",因此执行intern方法会在字符串常量池中分配"abcdef",然后str4最终等于这个字符串的引用,因此str3不等于str4,因为上面的str3指向堆,而str4指向字符串常量池,所以两者一定不会相等.


深入理解JVM虚拟机一书中,就有类似的代码:


String str1 = new StringBuilder("计算机").append("软件").toString();
System.out.println(str1 == str1.intern());
String str2 = new StringBuilder("ja").append("va").toString();
System.out.println(str2 == str2.intern());

在jdk1.6中,两个判断都为false.因为str1和str2都指向堆,而intern方法得出来的引用都指向字符串常量池,所以不会相等,和上面叙述的结论是一样的.在jdk1.7中,第一个是true,第二个是false.道理其实也和上述所讲的是一样的,对于第一个语句,最终会在堆上创建一个"计算机软件"的字符串,执行str1.intern()方法时,先在字符串常量池中寻找字符串,但没有找到,所以会直接引用堆上的这个"计算机软件",因此第一个语句会返回true,因为最终都是指向堆.而对于第三个语句,因为和第一个语句差不多,按理说最终比较也应该返回true.但实际上,str2.intern方法执行的时候,在字符串常量池中是可以找到"java"这个字符串的,这是因为在Java初始化环境去加载类的时候(执行main方法之前),已经有一个叫做"java"的字符串进入了字符串常量池,因此str2.intern方法返回的引用是指向字符串常量池的,所以最终判断的结果是false,因为一个指向堆,一个指向字符串常量池.


总结


从上面的分析看来,字符串常量池并不像是那种很简单的概念,要深刻理解字符串常量池,至少需要理解以下几点:



  • 理解字符串会在哪个内存区域存放

  • 理解遇到字符串常量会发生什么

  • 理解new String或者是new StringBuilder产生的对象会在哪里存放

  • 理解字符串拼接操作+最终编译出来的语句是什么样子的

  • 理解toString方法会发生什么


这几点都在本文章中覆盖了,相信理解了这几点之后一定对字符串常量池有一个更深刻的理解.其实这篇文章的编写原因是因为阅读深入理解JVM虚拟机这本书的例子时突然发现作者所说的和我所想的是不一样的,但是书上可能对这方面没有展开叙述,所以我去查了点资料,然后写了一些代码来验证,最终决定写一篇文章来记录一下自己的理解,在编写代码过程中,还发现了一个分析对象内存地址的类库,我放在参考资料中了.


参考资料


http://www.baeldung.com/java-object… 查看java对象内存地址


作者:JL8
来源:juejin.cn/post/7237827233351073849
收起阅读 »

你真的了解Systrace吗?

欢迎关 Android茶话会 在技术学习、个人成长的道路上,让我们一起前进! 一、什么是SysTrace? 在日常开发中有时候遇到棘手的性能问题就需要使用这个工具,Systrace 是 「Android 4.1」 中新增的性能数据采样和分析工具。它可帮助开发...
继续阅读 »

欢迎关 Android茶话会 在技术学习、个人成长的道路上,让我们一起前进!



一、什么是SysTrace?


在日常开发中有时候遇到棘手的性能问题就需要使用这个工具,Systrace 是 「Android 4.1」 中新增的性能数据采样和分析工具。它可帮助开发者收集 Android 关键子系统
(如 SurfaceFlinger/SystemServer/Kernel/Input/Display 等 Framework 部分关键模块、服务,View」系统等 的运行信息,从而帮助开发者更直观的「分析系统瓶颈,改进性能」image.png


二、如何使用SysTrace?


2.1 采集trace


首先我们需要了解Trace的采集主要涉及几部分:采集方式、自定义trace阶段、Release包抓取Trace和系统Trace类同异步调用的差异;


命令行采集:



  • 设备要求**「Android 4.3 (API level 18)及以上」**

  • 命令如下:python systrace.py [options][「categories」],示例如下:


python /Users/yangzhiyong/Library/Android/sdk/platform-tools/systrace/systrace.py -t 10 -o trace.html gfx input view sched freq wm am hwui workq res dalvik sync disk load perf hal rs idle mmc -a com.ss.android.lark.debug

# -o : 指示输出文件的路径和名字
# -t : 抓取时间(最新版本可以不用指定, 按 Enter 即可结束),单位为秒
# -b : 指定 buffer 大小 (一般情况下,默认的 Buffer 是够用的,如果你要抓很长的 Trae , 那么建议调大 Buffer )
# -a : 指定 app 包名 (如果要 Debug 自定义的 Trace 点, 记得要加这个)



  • 查看支持的categories



    • adb shell atrace --list_categories

    • python systrace.py -l




// 粗体部分是常用的categories;
         gfx - Graphics
       input - Input
        view - View System
     webview - WebView
          wm - Window Manager
          am - Activity Manager
          sm - Sync Manager
       audio - Audio
       video - Video
      camera - Camera
         hal - Hardware Modules
         res - Resource Loading
      dalvik - Dalvik VM
          rs - RenderScript
      bionic - Bionic C Library
       power - Power Management
          pm - Package Manager
          ss - System Server
    database - Database
     network - Network
         adb - ADB
    vibrator - Vibrator
        aidl - AIDL calls
         pdx - PDX services
       sched - CPU Scheduling
        freq - CPU Frequency
        idle - CPU Idle
        disk - Disk I/O
        sync - Synchronization
  memreclaim - Kernel Memory Reclaim
  binder_driver - Binder Kernel driver
  binder_lock - Binder global lock trace

系统自带工具:




  • 设备要求**「Android 9(API level 28)及其以上」**




  • 开发者模式->系统跟踪




  • trace导出:



    • 通知栏分享;

    • adb pull /data/local/traces/ .

    • systrace --from-file .ctrace .perfetto-trace 转换成html




网页版采集分析工具:💻



  • 设备要求**「Android 10(API level 29)及其以上;」**

  • Perfetto UI




除此之外还有一些常用的技巧





  1. 自定义TAG


image.png 具体使用可参考:developer.android.com/topic/perfo…




  • 「定义」



    • 在事件开始调用Trace.beginSection(event);

    • 在事件结束调用Trace.endSection(),需成对调用;




  • 「使用」 添加了自定义TAG后,需要-a指定包名参数,才可以采集到自定义的trace信息;





  1. Release包抓取trace


反射调用setAppTracingAllowed即可:


try {
    Class threadClazz = Class.forName("android.os.Trace");
    Method setAppTracingAllowed = threadClazz.getDeclaredMethod("setAppTracingAllowed"boolean.class);
    setAppTracingAllowed.invoke(nulltrue);
catch (Exception e) {
    e.printStackTrace();
}

3. 同异步trace的差异


「同步」「异步」「区别」
beginSection(@NonNull String sectionName)beginAsyncSection(@NonNull String methodName, int cookie)异步调用需要传methodName及cookie,开始结束匹配更准确,且不用B-A-A-B类似嵌套,不过该接口为隐藏接口,需反射调用,可使用SystemTracer类;
endSection()endAsyncSection(@NonNull String methodName, int cookie)

2.2 分析


拿到这些trace 我们如何分析也是一个重要的部分


2.2.1 Trace文件打开


首先是打开这个文件



  1. chrome://tracing:推荐:不过近期版本该工具对trace中线程、进程名解析不出来,不利于查看;

  2. ui.perfetto.dev/#!/: 推荐,不过新版分析工具不支持VSync的高亮;

  3. perfetto.bytedance.net/#!/viewer: 不推荐,仅支持单进程查看,对于涉及系统调用的分析不方便;


较早版本sysTrace生成的html文件浏览器即可打开,但最近版本已无法正常打开,需要通过chrome工具手动加载;


2.2.2 面板区域说明


image.png


image.png



  1. 用户屏幕交互

  2. CPU 使用率

  3. CPU各核心的运行情况

  4. 进程信息

  5. 进程变量/进程

  6. 选中区段的详细信息

  7. 鼠标操作选项,可通过1-4快速切换

  8. 进程过滤

  9. VSync高亮配置


2.2.3 常用快捷键


比较常用的快捷键是W S A D M V;



  • W : 放大 Systrace,放大可以更好地看清局部细节

  • S : 缩小 Systrace,缩小以查看整体

  • A : 左移

  • D : 右移

  • M : 高亮选中当前鼠标点击的段(这个比较常用,可以快速标识出这个方法的左右边界和执行时间,方便上下查看)

  • V : 高亮VSync的时机,方便分析掉帧的原因;


image.png


image.png


2.2.4 线程状态


a) 线程状态

「线程状态」「Systrace中的显示」「说明」
绿色 : 运行中(Running)image.png我们经常会查看 Running 状态的线程,查看其运行的时间,与竞品做对比,分析快或者慢的原因
蓝色 : 可运行(Runnable)image.pngRunnable 状态的线程状态持续时间越长,则表示 cpu 的调度越忙,没有及时处理到这个任务
白色 : 休眠中(Sleep)image.png一般是在等事件驱动
橘色 : 不可中断的睡眠态 IO Block(Uninterruptible Sleep WakeKill)image.png一般是标示 IO 操作慢,如果有大量的橘色不可中断的睡眠态出现,那么一般是由于进入了低内存状态
紫色 : 不可中断的睡眠态(Uninterruptible Sleep)image.png一般是陷入了内核态,有些情况下是正常的,有些情况下是不正常的,需要按照具体的情况去分析

b) 线程唤醒

一个任务在进入 Running 状态之前,会先进入 Runnable 状态进行等待,而 Systrace 会把这个状态也标示在 Systrace 上; image.png image.png stateWhenDescheduled定义:chromium.googlesource.com/external/tr…


c) 线程参数说明


  • Wall Duration:函数执行的总耗时

  • CPU Duration:在 CPU 上执行的耗时

  • Self Time:自身执行的总耗时,不包括子方法

  • CPU Self Time:自身在 CPU 上执行的耗时,不包括子方法


d) 函数调用虚实区别

如下图,我们看到标识setUpHWComposer调用的紫色条并不是实心的,实心的部分代表CPU Duration相对于Wall Duration的占比: image.png


2.2.5 CPU信息


image.png


image.png



  1. C-State


为了在CPU空闲的时候降低功耗,CPU可以被命令进入low-power模式。每个CPU都有几种power模式,这些模式被统称为C-states或者C-modes; C-States从C0开始,C0是CPU的正常工作模式,CPU处于100%运行状态。C后的数越高,CPU睡眠得越深,CPU的功耗被降低得越多,同时需要更多的时间回到C0模式。


「C-State」「描述」
C-0RUN MODE,运行模式。
C-1STANDBY,就位模式,随时准备投入运行
C-2DORMANT,休眠状态,被唤醒投入运行时有一定的延迟
C-3SHUTDOWN,关闭状态,需要有较长的延迟才能进入运行状态,减少耗电


  1. Clock Frequency:CPU当前运行频率;

  2. Clock Frequency Limits:CPU最大最小频率,通过该参数可以判断CPU核心差异,如大中小核


2.2.6 常见系统进程、线程




  • 进程




    • system_server



      • AMS

      • WMS

      • SurfaceFlinger






  • 线程



    • UI Thread //主线程

    • Render Thread //渲染线程

    • Binder Thread //跨进程调用线程




2.2.7 常见问题


a) 「锁等待」

image.png 「monitor contention」 with owner 「caton_dump_stack (11056)」 waiters=0 blocking from 「boolean android.os.MessageQueue.enqueueMessage(android.os.Message, long)(MessageQueue.java:544)」 image.png 结合代码看,这段信息的意思是,「caton_dump_stack」线程(线程ID是11056)作为「owner」持有了主线程消息队列对象锁,「waiters」表示等待在该对象锁上的其他线程数(不包括当前线程),所以总的有1个线程等待对象锁释放,当前线程等待的位置是「enqueueMessage调用处」


b) 「SurfaceFlinger绘制」

SurfaceFlinger主要是收集各个UI渲染层的数据合成发送给Hardware Composer;一般应用的渲染层包括状态栏、应用页面、导航栏,每个部分都是单独渲染生成Buffer的,基本步骤如下:



  1. 应用收到VSYNC-app信号后,在**「UI Thread」完成数据计算;准备好后,将数据发送到「RenderThread」**;

  2. 应用在**「RenderThread」完成数据渲染后,将数据填充到「SurfaceFlinger」**的对应页面BufferQueue中;

  3. 在**「SurfaceFlinger」收到VSYNC-sf信号后,「SurfaceFlinger」** 会遍历它的层列表的BufferQueue,以寻找新的缓冲区。如果找到新的缓冲区,它会获取该缓冲区;否则,它会继续使用以前获取的缓冲区。「SurfaceFlinger」 必须始终显示内容,因此它会保留一个缓冲区。如果在某个层上没有提交缓冲区,则该层会被忽略;

  4. **「SurfaceFlinger」**向 「HWC」 提供一个完整的层列表,并询问“您希望如何处理这些层?”

  5. 「HWC」 的响应方式是将每个层标记为叠加层或 GLES 合成;

  6. 「SurfaceFlinger」 会处理所有 GLES 合成,将输出缓冲区传送到 「HWC」,并让 「HWC」 处理其余部分;


image.png


image.png


c) 「掉帧」

上一部分描述了SurfaceFlinger合成每帧数据的过程,在上述过程中,如果**「UI Thread」计算不及时,或者「RenderThread」渲染不及时,或者「BufferQueue」**中可用Buffer不足导致在下一次VSYNC信号来临之前,没有准备好需要显示的帧的数据,就会出现丢帧,Systrace 报告列出了渲染界面帧的每个进程,并指明了沿时间轴渲染的每个帧。在 16.6 毫秒内渲染的必须保持每秒 60 帧稳定帧速率的帧会以绿色圆圈表示。渲染时间超过 16.6 毫秒的帧会以黄色或红色帧圆圈表示:



  1. 绿色:未丢帧;

  2. 棕色:轻微丢帧,丢1帧 ;

  3. 红色:严重丢帧,丢大于1帧;


image.png 通过Systrace右上角的View Options > Highlight VSync,我们可以高亮VSYNC信号到来的时刻,需要注意的是,高亮的VSYNC主要是VSYNC-app信号(可在SurfaceFlinger进程中查看),且灰白相间的位置是VSYNC信号到来的时刻。 如下图,显示了丢1帧的情况,第二个VSYNC到来时,主线程仍未完成显示帧数据的计算,所以出现丢帧的问题。 image.png


三、常见trace工具对比


其实我们分析trace不光只有系统这一种方法,下图做个简单的总结


「工具名」「类型」「原理」「优缺点」「使用场景」「使用说明」
Traceviewinstrument利用 Android Runtime 函数调用的 event 事件,将函数运行的耗时和调用关系写入 trace 文件中。「优点:」 全函数调用分析;「缺点:」 工具本身带来的性能开销过大,有时无法反映真实的情况。比如一个函数本身的耗时是 1 秒,开启 Traceview 后可能会变成 5 秒,而且这些函数的耗时变化并不是成比例放大;- 线下- 整个程序执行流程的耗时已废弃,之前DDMS有相关工具入口
Nanoscopeinstrument直接修改 Android 虚拟机源码,在ArtMethod执行入口和执行结束位置增加埋点代码,将所有的信息先写到内存,等到 trace 结束后才统一生成结果文件。「优点:」- 全函数调用分析;- 性能开销小;- 可以支持分析任意一个应用,可用于做竞品分析;「缺点:」- 需要自己刷 ROM,并且当前只支持 Nexus 6P,或者采用其提供的 x86 架构的模拟器;- 默认只支持主线程采集,其他线程需要代码手动设置;- 线下- 整个程序执行流程的耗时github.com/uber/nanosc…
systracesample实际是其他工具的封装,Systrace使用atrace开启追踪,然后读取ftrace的缓存,并且把它重新转换成HTML格式。「优点:」- 可以看到整个流程系统和应用程序的调用流程。包括系统关键线程的函数调用,例如渲染耗时、线程锁,GC 耗时等;- 性能损耗可以接受;「缺点:」- 不支持应用程序代码的耗时分析;需手动添加或者编译期插装;- 线下- 分析系统调用developer.android.com/topic/perfo…
Simpleperfsample利用 CPU 的性能监控单元(PMU)提供的硬件 perf 事件。「优点:」- 支持Native分析;- 性能开销非常低;「缺点:」- Java分析对Android版本要求比较高;- 线下- 分析 Native 代码的耗时android.googlesource.com/platform/sy… 在 Android M 和以前,Simpleperf 不支持 Java 代码分析。- 在 Android O 和以前,需要手动指定编译 OAT 文件。- 在 Android P 和以后,无需做任何事情,Simpleperf 就可以支持 Java 代码分析;
Profiler(CPU Profiler)混合- Sample Java Methods 的功能类似于 Traceview 的 sample 类型;- Trace Java Methods 的功能类似于 Traceview 的 instrument 类型;- Trace System Calls 的功能类似于 systrace;- Sample Native (API Level 26+) 的功能类似于 Simpleperf;「优点:」- 集成在IDE中,操作简单;「缺点:」- 性能开销大,应用明显卡顿;- 无法用于自动化测试等场景;- 线下developer.android.com/studio/prof…

「instrument」:获取一段时间内所有函数的调用过程,可以通过分析这段时间内的函数调用流程,再进一步分析待优化的点。


「sample」:有选择性或者采用抽样的方式观察某些函数调用过程,可以通过这些有限的信息推测出流程中的可疑点,然后再继续细化分析。


作者:Android茶话会
来源:juejin.cn/post/7238172236185354297
收起阅读 »

身为Ikun,我想用console.log输出giegie打球的视频~

web
前言 大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心。 事情是这样的,这天我醒来,觉得身为一个 “ikun”,我得向我的giegie看齐,早点把篮球水平练上去,早点追上我的giegie,于是我便开始了我的打球(打铁)...
继续阅读 »

前言


大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心。


事情是这样的,这天我醒来,觉得身为一个 “ikun”,我得向我的giegie看齐,早点把篮球水平练上去,早点追上我的giegie,于是我便开始了我的打球(打铁)之旅~


May-28-2023 23-36-14.gif


突发奇想


晚上,当我在分析着我的打球视频时,我又感觉我做这些是远远不够的!我要将这一切融入到前端里,让别人知道,咱们 ”ikun“ 是一个爱giegie,也爱学习的团体!我想要达到以下的效果


May-27-2023 23-47-31.gif


于是我想,我能不能把这个打球(打铁)视频,在控制台里 console.log 出来呢?我在心里演练了一遍,我觉得是可行的,我的思路有两个


直接用 console.log 输出视频?


好吧,目前 console.log 不支持输出视频吧~此路不通啊!


细分成每一帧去输出?


这个方式的思路具体分为以下几步:



  • 捕获视频的每一帧

  • 将每一帧转换成图片

  • 使用console.log输出生图片


如何捕获视频的每一帧?


使用 video 的 requestVideoFrameCallback 方法即可,requestVideoFrameCallback() 是一个新的WEB API,2021 年 1 月 25 日提交的草案。requestVideoFrameCallback() 方法允许WEB开发者注册一个回调方法,回调方法在新视频帧发送到合成器时在渲染步骤中运行。这是为了让开发人员对视频执行高效的每帧视频操作,例如视频处理和绘制到画布上(截屏)、视频分析或与外部音频源同步。


如何将每一帧转换成图片?


这得使用 canvas来完成,主要依赖了两个方法



  • ctx.drawImage:将一帧画面画到 canvas 上

  • canvas.toDataURL('image/png'):将 canvas 画布上的图像转成base64的URL


console.log 能输出图片?


console.log 是可以输出图片了,这一特性很久前就有了~不信你们复制以下代码,去尝试一下~


image.png


console.log(
"%c image",
`background-image: url(https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ca412c9f87fb4df1b5402a5ad64474f1~tplv-k3u1fbpfcp-watermark.image?);
background-size: contain;
background-repeat: no-repeat;
padding: 200px;
`

)

开始实现吧~


那我们开始实现吧,我这边的技术栈是 Vue3哦~


先看看效果


先看看效果是怎么样的


May-28-2023 23-36-26.gif


初始化代码


我们需要 video 和 canvas,这两个标签,前者是视频标签,后者是画布标签


<template>
<video ref="videoRef" width="640" height="360" controls playsinline muted>
<source src="./kunkun.mp4" />
您的浏览器不支持 video 标签。
video>
<canvas ref="canvasRef" width="640" height="360">canvas>
template>

封装 useIKun


封装一个 Vue3 的 hooks,名为 useIKun,用来处理 ikun 的打球视频转换图片输出~




接下来我们开始封装 useIKun


import { onMounted } from 'vue'
import type { Ref } from 'vue'

const CANVAS_WIDTH_BASE = 220
const CANVAS_HEIGHT_BASE = 220

export const useIKun = ({
videoInstance,
canvasInstance,
}: {
videoInstance: Ref
canvasInstance: Ref
}
) => {
onMounted(() => {
// 播放视频
handleVideoPlay()
})

// 获取dom实例
const getInstances = () => {
const video = videoInstance.value
const canvas = canvasInstance.value
return {
video,
canvas,
}
}

// 获取canvas尺寸信息
const getCanvasSize = () => {
const { video } = getInstances()
// videoWidht、videoHeight视频原始宽度、高度(单位:px)
const width = video.videoWidth
const height = video.videoHeight

const rate = height / width

return {
width: CANVAS_WIDTH_BASE,
height: CANVAS_HEIGHT_BASE * rate,
}
}

const handleVideoPlay = () => {
const video = videoInstance.value
const canvas = canvasInstance.value
video.oncanplay = () => {
const { width, height } = getCanvasSize()
canvas.width = width
canvas.height = height
video.play() // 播放视频

// 判断HTMLVideoElement是否支持requestVideoFrameCallback()方法
if ('requestVideoFrameCallback' in video) {
// 下此视频帧呈现时触发回调
video.requestVideoFrameCallback(updateVideo)
}
}
}

/*
* 根据当前视频帧绘制图片
*/

const updateVideo = () => {
const video = videoInstance.value
const canvas = canvasInstance.value
const ctx = canvas.getContext('2d')
if (ctx) {
const { width, height } = getCanvasSize()
ctx.drawImage(video, 0, 0, width, height) // 使用视频帧(当前帧)绘制canvas
const dataURL = canvas.toDataURL('image/png')
console.log(
'%c image',
`background-image: url(${dataURL});
background-size: contain;
background-repeat: no-repeat;
padding: 200px;
`
,
)
video.requestVideoFrameCallback(updateVideo)
}
}
}

console.clear() ?


我们可以看到效果出来了,控制台里确实“播放了视频”,但其实不是视频,其实是以非常快的速度,去打印出一帧一帧的图片出来,由于速度很快,所以给你一种在放视频的假象,不信你看其实控制台里不止一个画面哦~


May-28-2023 23-36-26.gif


所以我们咋办,每次输出图片的时候用 console.clear() 清除一下吗?我们可以试试~


  const updateVideo = () => {
const video = videoInstance.value
const canvas = canvasInstance.value
const ctx = canvas.getContext('2d')
if (ctx) {
const { width, height } = getCanvasSize()
ctx.drawImage(video, 0, 0, width, height) // 使用视频帧(当前帧)绘制canvas
const dataURL = canvas.toDataURL('image/png')
// 清除
+ console.clear()
console.log(
'%c image',
`background-image: url(${dataURL});
background-size: contain;
background-repeat: no-repeat;
padding: 200px;
`
,
)
video.requestVideoFrameCallback(updateVideo)
}
}

再来看看效果~ 显然这样是不行的,console.clear 会导致闪烁问题~


May-28-2023 23-36-37.gif


Gif图?


所以我又有新思路,将每一帧的图像收集起来,然后组成一个 Gif 图,然后输出在控制台,不就行了!!!


gifshot


想要完成这个事情,就要借助这个库——gifshot,他的作用是可以把你传给他的图像数组,组成一个 gif图


重新封装 useIKun


 import { onMounted, ref } from 'vue'
import type { Ref } from 'vue'
import gifshot from 'gifshot'

const CANVAS_WIDTH_BASE = 220
const CANVAS_HEIGHT_BASE = 220
const IMG_SPAN = 5

export const useIKun = ({
videoInstance,
canvasInstance,
}: {
videoInstance: Ref
canvasInstance: Ref
}
) => {
const imgs = ref<string[]>([])

onMounted(() => {
// 播放视频
handleVideoPlay()
// 监听播放结束
onVideoEnded()
})

const getInstances = () => {
const video = videoInstance.value
const canvas = canvasInstance.value
return {
video,
canvas,
}
}

const getCanvasSize = () => {
const { video } = getInstances()
// videoWidht、videoHeight视频原始宽度、高度(单位:px)
const width = video.videoWidth
const height = video.videoHeight

const rate = height / width

return {
width: CANVAS_WIDTH_BASE,
height: CANVAS_HEIGHT_BASE * rate,
}
}

/*
* 控制视频播放
*/

const handleVideoPlay = () => {
const { video, canvas } = getInstances()
video.oncanplay = () => {
const { width, height } = getCanvasSize()
canvas.width = width
canvas.height = height
video.play() // 播放视频

// 判断HTMLVideoElement是否支持requestVideoFrameCallback()方法
if ('requestVideoFrameCallback' in video) {
// 下此视频帧呈现时触发回调
video.requestVideoFrameCallback(updateVideo)
}
}
}

/*
* 根据当前视频帧绘制图片
*/

const updateVideo = () => {
const { video, canvas } = getInstances()
const ctx = canvas.getContext('2d')
if (ctx) {
const { width, height } = getCanvasSize()
ctx.drawImage(video, 0, 0, width, height) // 使用视频帧(当前帧)绘制canvas
const dataURL = canvas.toDataURL('image/png')
imgs.value.push(dataURL)
video.requestVideoFrameCallback(updateVideo)
}
}

/*
* 监听视频停止播放
*/

const onVideoEnded = () => {
const { video } = getInstances()
video.onended = () => {
console.log('播放完了')
console.log(imgs.value.length)
const currentImgs = imgs.value
const resultImgs: string[] = []
currentImgs.forEach((img, index) => {
// 稀释图片数组,怕太大太久~你们也可以选择不走这一步
if (index % IMG_SPAN === 0) {
resultImgs.push(img)
}
})
// gifshot转换gif
gifshot.createGIF(
{
fps: 10,
width: 220,
height: 500,
images: resultImgs,
},
(obj) => {
console.log(obj)
if (!obj.error) {
const url = obj.image
// 输出最终的gif地址
console.log(
'%c image',
`background-image: url(${url});
background-size: contain;
background-repeat: no-repeat;
padding: 200px;
`
,
)
}
},
)
}
}
}


结果


哎,,,这画质,虽然比较糙,但是也算是本 ikun 为 giegie做出的一点点贡献了~


May-27-2023 23-31-48.gif


结语 & 加学习群 & 摸鱼群


我是林三心



  • 一个待过小型toG型外包公司、大型外包公司、小公司、潜力型创业公司、大公司的作死型前端选手;

  • 一个偏前端的全干工程师;

  • 一个不正经的掘金作者;

  • 一个逗比的B站up主;

  • 一个不帅的小红书博主;

  • 一个喜欢打铁的篮球菜鸟;

  • 一个喜欢历史的乏味少年;

  • 一个喜欢rap的五音不全弱鸡

作者:Sunshine_Lin
来源:juejin.cn/post/7238195267286138939
收起阅读 »

Flutter 3.10 之 Flutter Web 路线已定,可用性进一步提升,快来尝鲜 WasmGC

随着 Flutter 3.10 发布,Flutter Web 也引来了它最具有「里程碑」意义的更新,这里的「里程碑」不是说这次 Flutter Web 有多么重大的更新,而是 Flutter 官方对于 Web 终于有了明确的定位和方向。 提升 首先我们简单聊提...
继续阅读 »

随着 Flutter 3.10 发布,Flutter Web 也引来了它最具有「里程碑」意义的更新,这里的「里程碑」不是说这次 Flutter Web 有多么重大的更新,而是 Flutter 官方对于 Web 终于有了明确的定位和方向


提升


首先我们简单聊提升,这不是本篇的重点,只是顺带。


本次提升主要在于两个大点:Element 嵌入支持和 fragment shaders 支持


首先是 Element 嵌入,Flutter 3.10 开始,现在可以将 Flutter Web嵌入到网页的任何 HTML 元素中,并带有 flutter.js 引擎和 hostElement 初始化参数


简单来说就是不需要 iframe 了,如下代码所示,只需要通过 initializeEnginehostElement 参数就可以指定嵌入的元素,灵活度支持得到了提高

<html>
 <head>
   <!-- ... -->
   <script src="flutter.js" defer></script>
 </head>
 <body>

   <!-- Ensure your flutter target is present on the page... -->
   <div id="flutter_host">Loading...</div>

   <script>
     window.addEventListener("load", function (ev) {
       _flutter.loader.loadEntrypoint({
         onEntrypointLoaded: async function(engineInitializer) {
           let appRunner = await engineInitializer.initializeEngine({
             // Pass a reference to "div#flutter_host" into the Flutter engine.
             hostElement: document.querySelector("#flutter_host")
          });
           await appRunner.runApp();
        }
      });
    });
   </script>
 </body>
</html>


PS :如果你的项目是在 Flutter 2.10 或更早版本中创建的,要先从目录中删除 /web 文件 ,然后通过 flutter create . --platforms=web 重新创建模版。



fragment shaders 部分一般情况下大家可能并不会用到,shaders 就是以 .frag 扩展名出现的 GLSL 文件,在 Flutter 里是在 pubspec.yaml 文件下的 shaders 中声明,现在它支持 Web 了:

flutter:
shaders:
  - shaders/myshader.frag


一般运行时会把 frag 文件加载到 FragmentProgram 对象中,通过 program 可以获取到对应的 shader,然后通过 Paint.shader 进行使用绘制, 当然 Flutter 里 shaders 文件是存在限制的,比如不支持 UBO 和 SSBO 等。



当然,这里不是讲解 shaders ,而是宣告一下,Flutter Web 支持 shaders 了


未来


其实未来才是本篇的重点,我们知道 Flutter 在 Web 领域的支持上一直在「妥协」,Flutter Web 在整个 Flutter 体系下一直处于比较特殊的位置,因为它一直存在两种渲染方式:html 和 canvaskit


简单说 html 就是转化为 JS + Html Element 渲染,而 canvaskit 是采用 Skia + WebAssembly 的方式,而 html 的模式让 Web 在 Flutter 中显得「格格不入」,路径依赖和维护成本也一直是 Flutter Web 的头痛问题



面对这个困境,官方在年初的 Flutter Forword 大会上提出重新规划 Flutter Web 的未来,而随着 Flutter 3.10 的发布,官方终于对于 Web 的未来有了明确的定位:



“Flutter 是第一个围绕 CanvasKit 和 WebAssembly 等新兴 Web 技术进行架构设计的框架。”



Flutter 团队表示,Flutter Web 的定位不是设计为通用 Web 的框架,类似的 Web 框架现在有很多,比如 Angular 和 React 等在这个领域表现就很出色,而 Flutter 应该是围绕 CanvasKit 和 WebAssembly 等新技术进行架构设计的框架。


所以 Flutter Web 未来的路线会是更多 CanvasKit ,也就是 WebAssembly + Skia ,同时在这个领域 Dart 也在持续深耕:从 Dart 3 开始,对于 Web 的支持将逐步演进为 WebAssembly 的 Dart native 的定位



什么是 WebAssembly 的 dart native ?一直以来 Flutter 对于 WebAssembly 的支持都是:使用 Wasm 来处理CanvasKit 的 runtime,而 Dart 代码会被编译为 JS,而这对于 Dart 团队来时,其实是一个「妥协」的过渡期。


而随着官方与 WebAssembly 生态系统中的多个团队的深入合作,Dart 已经开始支持直接编译为原生的 wasm 代码,一个叫 WasmGC 的垃圾收集实现被引入标准,该扩展实现目前在基于 Chromium 的浏览器和 Firefox 浏览器中在趋向稳定。



目前在基准测试中,执行速度提高了 3 倍



要将 Dart 和 Flutter 编译成 Wasm,你需要一个支持 WasmGC 的浏览器,目前 Chromium V8 和 Firefox 团队的浏览器都在进行支持,比如 Chromium 下:



通过结构和数组类型为 WebAssembly 增加了对高级语言的有效支持,以 Wasm 为 target 的语言编译器能够与主机 VM 中的垃圾收集器集成。在 Chrome 中启用该功能意味着启用类型化函数引用,它会将函数引用存储在上述结构和数组中。




现在在 Flutter master 分支下就可以提前尝试 wasm 的支持,运行 flutter build web --help 如果出现下图所示场, 说明支持 wasm 编译。



之后执行 flutter build web --wasm 就可以编译一个带有 native dart wasm 的 web 包,命令执行后,会将产物输出到 build/web_wasm 目录下。


之后你可以使用 pub 上的 dhttpd 包在 build/web_wasm目录下执行本地服务,然后在浏览器预览效果。

> cd build/web_wasm
> dhttpd
Server started on port 8080

目前需要版本 112 或更高版本的 Chromium 才能支持,同时需要启动对应的 Chrome 标识位:



  • enable-experimental-webassembly-stack-switching

  • enable-webassembly-garbage-collection



当然,目前阶段还存在一些限制,例如:



Dart Wasm 编译器利用了 JavaScript-Promise Integration (JSPI) 特性,Firefox 不支持 JSPI 提议,所以一旦 Dart 从 JSPI 迁移出来,Firefox 应启用适当的标志位才能运行。



另外还需要 JS-interop 支持,因为为了支持 Wasm,Dart 改变了它针对浏览器和 JavaScript 的 API 支持方式, 这种转变是为了防止把 dart:htmlpackage:js 编译为 Wasm 的 Dart 代码,大多数特定于平台的包如 url_launcher 会使用这些库。



最后,目前 DevTools 还不支持 flutter run 去运行和调试 Wasm


最后


很高兴能看到 Flutter 团队最终去定了 Web 的未来路线,这让 Web 的未来更加明朗,当然,正如前面所说的,Flutter 是第一个围绕 CanvasKit 和 WebAssembly 等新兴 Web 技术进行架构设计的框架


所以 Flutter Web不是为了设计为通用 Web 的框架去 Angular 和 React 等竞争,它是让你在使用 Flutter 的时候,可以将能力很好地释放到 Web 领域,而 CanvasKit 带来的一致性更符合 Flutter Web 的定位,当然,解决加载时长问题会是任重道远的需求。


最后不得不提 WebGPU, WebGPU 作为新一代的 WebGL,可以提供在浏览器绘制 3D 的全新实现,它属于 GPU硬件(显卡)向 Web(浏览器)开放的低级 API,包括图形和计算两方面相关接口。


WebGPU 来自 W3C 制定的标准,与 WebGL 不同,WebGPU 不是基于 OpenGL ,它是一个新的全新标准,发起者是苹果,目前由 W3C GPU 与来自苹果、Mozilla、微软和谷歌一起制定开发,不同于 WebGL (OpenGL ES Web 版本),WebGPU 是基于 Vulkan、Metal 和 Direct3D 12 等,能提供更好的性能和多线程支持。



WebGPU 已经被正式集成到 Chrome 113 中,首个版本可在会支持 Vulkan 的 ChromeOS 设备、 Direct3D 12 的 Windows 设备和 macOS 的 Chrome 113 浏览器,除此之外 Linux、Android 也将在 2023 年内开始陆续发布,同步目前也初步登陆了 Firefox 和 Safari 。



提及 WebGPU 的原因在于:WebGPU + WebAssembly 是否在未来可以让 Web 也支持 Impeller 的可能?


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

Studio Bot - 让 AI 帮我写 Android 代码

Google I/O 2023 不出所料,今年的 I/O 大会只突出了一个关键词 “AI”。Google 旗下的各类产品都成了其展示 AI 实力的舞台。连面向开发者的产品 Android Studio 也新增了 Studio bot 功能,希望通过 AI 帮...
继续阅读 »

Google I/O 2023




不出所料,今年的 I/O 大会只突出了一个关键词 “AI”。Google 旗下的各类产品都成了其展示 AI 实力的舞台。连面向开发者的产品 Android Studio 也新增了 Studio bot 功能,希望通过 AI 帮助开发者更轻松地写代码:


ezgif.com-video-to-gif.gif


Studio Bot 使用谷歌编码基础模型 Codey(后者基于最新 PaLM2 大语言模型)帮助开发者生成程序代码,提升生产力。我们还可以向 Studio Bot 询问有关 Android 开发的知识,或者帮助修复当前代码中的错误。Studio Bot 正处于早期阶段,目前只对 US 地区开放,但是支持中文交流!感兴趣的小伙伴可以翻墙体验。


凡涉及到代码自然会让人担心到安全问题,Google 非常重视隐私安全,我们与 Studio Bot 的聊天内容不会被用作其他用途,可以放心使用。


Studio Bot 启动方式




1. 更新 Android Studio


更新到当前最新版的 Android Studio Hedgehog.



2. 打开功能视图


View > Tool Windows > Studio Bot



3. 登录账号


使用 Google 账号登录,点击 Next 就可以开始对话了



Studio Bot 可以做什么?




1. 生成代码


这是非常实用的功能,我们可以让 Studio Bot 帮我们生成所需的代码。而且相对于依靠搜索得到的各种参差不齐的信息,Studio Bot 通过强大的生成式 AI 能力,给出的答案可读性更好,质量更可靠。例如,我需要一段创建 Room 数据库的代码,得到的回答如下:



而且,Studio Bot能够记住对话的上下文,你可以追加相关问题,它可以自己理解你的意图,比如我希望将刚才生成的代码改为 Kotlin 的,如下:


代码变成了 Kotlin 版本,还配了详细的说明


2. 回答问题


回答各种技术问题,比如关于 Android Studio 使用技巧,甚至任何通用的 Android 开发知识。



3. 解读代码


这个功能相当炸裂,你可以选中 IDE 中的任意代码片段,去 Ask Studio Bot 获取代码的解读。


image.png


以下是解读的结果,将每一行代码翻译成更能听懂的“人话”。 Studio Bot 是支持中文的,中文回答的效果看起来也不错,对技术词语的翻译很到位,一点不晦涩。


image.png


对于很多一眼看不懂的花哨代码,将会非常有用,是大家学习开源项目的利器!


一些常见问题




1. Studio Bot 会将我的代码发送到 Google 服务器吗?


发送给 Studio Bot 的代码需要上传服务器才能获得回答,但是这些代码不会被滥用,如果你担心代码安全可以不提问关于你的代码的问题,IDE 的私有代码绝不会被私自上传服务器


2. 代码会用来训练 Studio Bot 模型吗


Ask Studio Bot 这样的功能不会将你的代码送去训练模型,只是用来获取问题答案


3. Studio Bot 的回答是准确无误的吗?


Studio Bot 目前还是实验性产品,无法保证答案的绝对正确。Bot 在回答后会跟有 “赞” 和 “踩”,通过这些反馈将帮助模型更好地成长,准确度会越来越高。


4. Studio Bot 可以提供关于代码的帮助吗?


当然,如前面介绍的,它可以生成代码,也可以基于你的代码提供一些解读,它主要的场景就是服务写代码这件事情


5. Studio Bot 在回答中如何引用来源?


Studio Bot 应该更多地生成原创内容,而不是复制已有内容。万一 Studio Bot 引用了大篇幅源码,那么它会标记引用来源,引用源可能涉及开源许可证,所以参考回答时也需要遵守许可证的要求。


6. 如何对 Studio Bot 进行反馈?


前面提到了,可以得到回答后,即时给出“赞”或者“踩”的反馈,帮助其成长,服务他人也更好地服务自己。


7. 可以问 Studio Bot 任何问题吗?


Studio Bot 是为了回答各类 Android 开发问题而生的,其他领域的问题它可能无法很好的回答。


8. 与其它大语言模型机器人(如 ChatGPT, GoogleBard 等)有什么不同?


Studio Bot 为 Android Studio 设计,可以与 IDE 很好的集成,提供很多开箱机用的面向编码的功能,这是一般的对话机器人所没有的。


9. 给一些使用建议?


问题尽量简洁清晰,如果 Bot 没有理解你的问题那可以重新组织一下语句,另外对于回答需要有所判断,毕竟这种生成式的答案无法保证绝对正确。


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

Android如何设计一个全局可调用的ViewModel对象?

很多时候我们需要维护一个全局可用的ViewModel,因为这样可以维护全局同一份数据源,且方便使用协程绑定App的生命周期。那如何设计全局可用的ViewModel对象? 一、思路 viewModel对象是存储在ViewModelStore中的,那么如果我们创建...
继续阅读 »

很多时候我们需要维护一个全局可用的ViewModel,因为这样可以维护全局同一份数据源,且方便使用协程绑定App的生命周期。那如何设计全局可用的ViewModel对象?


一、思路


viewModel对象是存储在ViewModelStore中的,那么如果我们创建一个全局使用的ViewModelStore并且在获取viewModel对象的时候从它里面获取就可以了。


viewModel是通过ViewModelProviderget方法获取的,一般是ViewModelProvider(owner: ViewModelStoreOwner, factory: Factory).get(ViewModel::class.java)


如何将ViewModelProviderViewModelStore关联起来? 纽带就是ViewModelStoreOwner, ViewModelStoreOwner是一个接口,需要实现getViewModelStore()方法,而该方法返回的就是ViewModelStore:

public interface ViewModelStoreOwner {
/**
* Returns owned {@link ViewModelStore}
*
* @return a {@code ViewModelStore}
*/
@NonNull
ViewModelStore getViewModelStore(); //返回一个ViewModelStore
}

让某个类实现这个接口,重写方法返回我们定义的ViewModelStore就可以了。


至于上面ViewModelProvider构造方法的第二个参数Factory是什么呢?


源码中提供了二种Factory,一种是NewInstanceFactory,一种是AndroidViewModelFactory,它们的主要区别是:




  • NewInstanceFactory创建ViewModel时,会为每个Activity或Fragment创建一个新的ViewModel实例,这会导致ViewModel无法在应用程序的不同部分共享数据。(ComponentActivity源码getDefaultViewModelProviderFactory方法)




  • AndroidViewModelFactory可以访问应用程序的全局状态,并且ViewModel实例可以在整个应用程序中是共享的。




根据我们的需求,需要用的是AndroidViewModelFactory。


二、具体实现


1、方式一:可以全局添加和获取任意ViewModel


定义Application,Ktx.kt文件

import android.app.Application

lateinit var appContext: Application

fun setApplicationContext(context: Application) {
appContext = context
}

定义全局可用的ViewModelOwner实现类

object ApplicationScopeViewModelProvider : ViewModelStoreOwner {

private val eventViewModelStore: ViewModelStore = ViewModelStore()

override fun getViewModelStore(): ViewModelStore {
return eventViewModelStore
}

private val mApplicationProvider: ViewModelProvider by lazy {
ViewModelProvider(
ApplicationScopeViewModelProvider,
ViewModelProvider.AndroidViewModelFactory.getInstance(appContext)
)
}

fun <T : ViewModel> getApplicationScopeViewModel(modelClass: Class<T>): T {
return mApplicationProvider.get(modelClass)
}
}

定义一个ViewModel通过StateFlow定义发送和订阅事件的方法

class EventViewModel : ViewModel() {

private val mutableStateFlow = MutableStateFlow(0)

fun postEvent(state: Int) {
mutableStateFlow.value = state
}

fun observeEvent(scope: CoroutineScope? = null, method: (Int) -> Unit = { _ -> }) {
val eventScope = scope ?: viewModelScope
eventScope.launch {
mutableStateFlow.collect {
method.invoke(it)
}
}
}
}

定义一个调用的类

object FlowEvent {

//发送事件
fun postEvent(state: Int) {
ApplicationScopeViewModelProvider.getApplicationScopeViewModel(EventViewModel::class.java)
.postEvent(state)
}

//订阅事件
fun observeEvent(scope: CoroutineScope? = null, method: (Int) -> Unit = { _ -> }) {
ApplicationScopeViewModelProvider.getApplicationScopeViewModel(EventViewModel::class.java)
.observeEvent(scope, method)
}
}

测试代码如下:

class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//打印协程名称
System.setProperty("kotlinx.coroutines.debug", "on")

FlowEvent.observeEvent {
printMsg("MainActivity observeEvent before :$it")
}
//修改值
FlowEvent.postEvent(1)


FlowEvent.observeEvent {
printMsg("MainActivity observeEvent after :$it")
}

}

}

//日志
内容:MainActivity observeEvent before :0 线程:main @coroutine#1
内容:MainActivity observeEvent before :1 线程:main @coroutine#1
内容:MainActivity observeEvent after :1 线程:main @coroutine#2

2、方式二:更方便在Activity和Fragment中调用


定义Application,让BaseApplication实现ViewModelStoreOwner

//BaseApplication实现ViewModelStoreOwner接口
class BaseApplication : Application(), ViewModelStoreOwner {

private lateinit var mAppViewModelStore: ViewModelStore
private var mFactory: ViewModelProvider.Factory? = null

override fun onCreate() {
super.onCreate()
//设置全局的上下文
setApplicationContext(this)
//创建ViewModelStore
mAppViewModelStore = ViewModelStore()

}

override fun getViewModelStore(): ViewModelStore = mAppViewModelStore

/**
* 获取一个全局的ViewModel
*/
fun getAppViewModelProvider(): ViewModelProvider {
return ViewModelProvider(this, this.getAppFactory())
}

private fun getAppFactory(): ViewModelProvider.Factory {
if (mFactory == null) {
mFactory = ViewModelProvider.AndroidViewModelFactory.getInstance(this)
}
return mFactory as ViewModelProvider.Factory
}
}

Ktx.kt文件也有变化,如下

lateinit var appContext: Application

fun setApplicationContext(context: Application) {
appContext = context
}

//定义扩展方法
inline fun <reified VM : ViewModel> Fragment.getAppViewModel(): VM {
(this.requireActivity().application as? BaseApplication).let {
if (it == null) {
throw NullPointerException("Application does not inherit from BaseApplication")
} else {
return it.getAppViewModelProvider().get(VM::class.java)
}
}
}

//定义扩展方法
inline fun <reified VM : ViewModel> AppCompatActivity.getAppViewModel(): VM {
(this.application as? BaseApplication).let {
if (it == null) {
throw NullPointerException("Application does not inherit from BaseApplication")
} else {
return it.getAppViewModelProvider().get(VM::class.java)
}
}
}

BaseActivityBaseFragment中调用上述扩展方法

abstract class BaseActivity: AppCompatActivity() {

//创建ViewModel对象
val eventViewModel: EventViewModel by lazy { getAppViewModel() }

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

}
}
abstract class BaseFragment: Fragment() {

//创建ViewModel对象
val eventViewModel: EventViewModel by lazy { getAppViewModel() }

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

}
}

测试代码

class MainActivity : BaseActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//打印协程名称
System.setProperty("kotlinx.coroutines.debug", "on")

eventViewModel.observeEvent {
printMsg("MainActivity observeEvent :$it")
}

findViewById<AppCompatButton>(R.id.bt).setOnClickListener {
//点击按钮修改值
eventViewModel.postEvent(1)
//跳转到其他Activity
Intent(this, TwoActivity::class.java).also { startActivity(it) }
}
}

}
class TwoActivity : BaseActivity() {

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

eventViewModel.observeEvent {
printMsg("TwoActivity observeEvent :$it")
}
}
}

日志

内容:MainActivity observeEvent :0 线程:main @coroutine#1
内容:MainActivity observeEvent :1 线程:main @coroutine#1
内容:TwoActivity observeEvent :1 线程:main @coroutine#2

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

2023年的现代安卓开发

2023年的现代安卓开发 大家好👋🏻, 我想和大家分享一下如何用2023年的最新趋势构建Android应用. 免责声明 这是一篇来自我的观点和专业经验的文章, 考虑到了安卓开发者社区的不同意见, 也不断回顾了谷歌为安卓提供的指南. 我必须明确指出, 有一些非常...
继续阅读 »

2023年的现代安卓开发


大家好👋🏻, 我想和大家分享一下如何用2023年的最新趋势构建Android应用.


免责声明


这是一篇来自我的观点和专业经验的文章, 考虑到了安卓开发者社区的不同意见, 也不断回顾了谷歌为安卓提供的指南.


我必须明确指出, 有一些非常有趣的工具, 模式和架构, 我可能没有提到, 但这并不意味着它们不能成为开发Android应用程序的其他有趣的选择.


什么是Android?


Android是一个基于Linux内核的开源操作系统, 由谷歌开发.它被广泛用于各种设备, 包括智能手机, 平板电脑, 电视和智能手表.


目前, 安卓是世界上移动设备使用最多的操作系统;根据statcounter的报告, 以过去12个月为样本, 安卓的市场份额为71.96%.


接下来, 我将提到一个工具, 库, 架构, 指南和其他实用工具的清单, 我认为这些工具对在Android上构建现代应用程序非常重要.


Kotlin ❤️


0_piQN_I004o_ugTCN.webp


Kotlin是由JetBrains开发的一种编程语言.由谷歌推荐, 谷歌在2017年5月正式宣布了它(见这里的出版物).它是一种现代编程语言, 具有与Java的兼容性, 可以在JVM上运行, 这使得它在Android应用开发中的采用速度非常快.


无论你是否是安卓新手, 你都应该考虑将Kotlin作为你的首选, 不要逆水行舟🏊🏻 😎, 谷歌在2019年谷歌I/O大会上宣布了这一做法.使用Kotlin, 你将能够使用现代语言的所有功能, 包括Coroutines的强大实力和使用为Android生态系统开发的现代库.


官方kotlin文档在这里


Jetpack Compose 😍


0_kG-9BQIyUm8MblpZ.webp



Jetpack Compose是Android推荐的用于构建本地UI的现代工具包.它简化并加速了Android上的UI开发.




Jetpack Compose是Android Jetpack库的一部分, 使用Kotlin编程语言来轻松创建本地用户界面.同时, 它还与其他Android Jetpack库(如LiveData和ViewModel)集成, 使其更容易建立反应性和可维护的Android应用程序.


Jetpack Compose的一些主要特点包括:



  1. 声明式UI.

  2. 可定制的小工具.

  3. 易于与现有代码集成.

  4. 实时预览.

  5. 改进性能.


资源:



Jetpack Compose文档


Android Jetpack


0_3LHozcwxQYiKVhPG.webp



Jetpack是一套库, 帮助开发人员遵循最佳实践, 减少模板代码, 并编写在不同的Android版本和设备上一致运行的代码, 以便开发人员可以专注于他们关心的代码.




它的一些最常用的工具是:



Material Design


1_D3MK4AocfnktSnVGe4rg0g.webp



Material Design是一个由指导方针, 组件和工具组成的适应性系统, 支持用户界面设计的最佳实践.在开源代码的支持下, Material Design简化了设计师和开发人员之间的合作, 并帮助团队快速建立漂亮的产品.



Material Design网站


Material Design得到了来自谷歌的设计师和开发人员的支持, 它将使我们有一个指南来为我们的Android, Flutter和Web的UI/UX工作.


目前, Material Design的最后一个版本是3, 你可以看到更多这里.


Clean Architecture


0_KgFh38gn_lDEuoB9.webp


Clean Architecture的概念是由Robert C. Martin提出的.它的基础是通过将软件划分为不同的层来分离责任.


特点:



  1. 独立于框架.

  2. 可测试.

  3. 独立于用户界面.

  4. 独立于数据库.

  5. 独立于任何外部代理.


依赖性规则


博文Clean Architecture对依赖性规则做了很好的描述.



使得这个架构发挥作用的首要规则是依赖性规则.这个规则说, 源代码的依赖关系只能指向内部.内圈的任何东西都不能知道外圈的任何东西.特别是, 外圈中声明的东西的名字不能被内圈中的代码所提及.这包括, 函数, 类, 变量或任何其他命名的软件实体.



博文Clean Architecture


安卓系统中的Clean Architecture



  • 表示层: Activities, Fragments, ViewModels, 其他视图组件.

  • 领域层: 用例, 实体, 仓库, 其他的域组件.

  • 数据层: 存储库的实现, 映射器, DTO等.


Presentation层的架构模式


架构模式是一种更高层次的策略, 旨在帮助设计一个软件架构, 其特点是在一个可重复使用的框架内为常见的架构问题提供解决方案.架构模式类似于设计模式, 但它们的规模更大, 解决的是更多的全局性问题, 如系统的整体结构, 组件之间的关系以及数据的管理方式.


在Presentation层中, 我们有一些架构模式, 其中我想强调以下几点:



  • MVVM

  • MVI


我不想逐一解释, 因为在互联网上你可以找到太多的相关信息.


此外, 你还可以看看应用架构指南


0_QJ56TjhdXPcQweAk.webp


依赖注入


依赖注入是一种软件设计模式, 它允许客户端从外部来源获得其依赖, 而不是自己创建.它是一种在对象和其依赖关系之间实现反转控制(IoC)的技术.



模块化


模块化是一种软件设计技术, 它允许你将一个应用程序划分为独立的模块, 每个模块都有自己的功能和责任.


0_NNUw83lZ228t5yLD.webp


模块化的好处


可重复使用: 通过拥有独立的模块, 它们可以在应用程序的不同部分甚至在其他应用程序中重复使用.


严格的可见性控制: 模块使你能够轻松地控制你向你的代码库的其他部分暴露的内容.


可定制的交付: Google Play的特性交付使用应用程序捆绑的高级功能, 允许你有条件地或按需交付你的应用程序的某些功能.


可扩展性: 通过独立的模块, 功能可以被添加或删除而不影响应用程序的其他部分.


易于维护: 通过将应用程序分为独立的模块, 每个模块都有自己的功能和责任, 更容易理解和维护代码.


易于测试: 通过拥有独立的模块, 它们可以被隔离测试, 这使得检测和修复错误变得容易.


架构的改进: 模块化有助于改善应用程序的架构, 使代码有更好的组织和结构.


改进协作: 通过独立的模块, 开发人员可以同时工作在应用程序的不同部分, 不受干扰.


构建时间: 一些Gradle功能, 如增量构建, 构建缓存或并行构建, 可以利用模块化来提高构建性能.


更多内容请见官方文档.


网络



序列化


在本节中, 我想提及我认为的两个重要工具: MoshiRetrofit一起广泛使用, 以及Kotlin Serialization, 这是Jetbrain的Kotlin团队的赌注.



MoshiKotlin Serialization是Kotlin和Java的两个序列化/反序列化库, 允许你将对象转换成JSON或其他序列化格式, 反之亦然.两者都提供了一个用户友好的界面, 为在移动和桌面应用程序中使用而优化.Moshi主要专注于JSON序列化, 而Kotlin Serialization则支持各种序列化格式, 包括JSON.


图像加载



要从互联网上加载图片, 有几个第三方库可以帮助你处理这个过程.图片加载库为你做了很多繁重的工作;它们既能处理缓存(这样你就不会多次下载图片), 也能处理网络逻辑以下载图片并在屏幕上显示.




Reactivity / Thread Management反应性/线程管理


1_jm3wnFbTBvURFtLlcQAYRg.webp


当我们谈论反应式编程和异步进程时, 我们的第一选择是Kotlin Coroutines;由于suspend函数Flow, 我们可以满足所有这些需求.然而, 我认为在这一节中值得强调的是RxJava的重要性, 即使在Android应用程序的开发中.对于我们这些已经在Android上工作了几年的人来说, 我们知道RxJava是一个非常强大的工具, 它有非常多的功能来处理数据流.今天我仍然认为RxJava是一个值得考虑的替代方案.



本地存储


在构建移动应用程序时, 很重要的一点是要有在本地持久化数据的能力, 比如一些会话数据或缓存数据等等.根据你的应用程序的需要, 选择合适的存储方式是很重要的.我们可以存储非结构化的数据, 如键值或结构化的数据, 如数据库.请记住, 这一点并没有提到我们可用的所有本地存储类型(如文件存储), 只是提到了允许我们保存数据的工具.


1_rILOhf6I_dtR-ircBkKvtQ.webp


建议:



测试



R8优化


R8是默认的编译器, 它将你项目的Java字节码转换为在Android平台上运行的DEX格式.它是一个帮助我们混淆和减少应用程序代码的工具, 通过缩短类和其属性的名称, 消除项目内未使用的代码和资源.想了解更多, 请查看Android文档中关于缩减, 混淆和优化你的应用程序.


1_KzoahZDnZ25lv5ydi39JSw.webp



  • 代码缩减

  • 资源缩减

  • 混淆

  • 优化


Play特性交付



Google Play的应用服务模式, 称为动态交付, 使用Android App Bundles为每个用户的设备配置生成和提供优化的APK, 因此用户只下载运行你的应用所需的代码和资源.



Android文档


0_FitxQQeB7XC7MVUq.webp


自适应布局


0_MHJwbEuvl8cXDjeq.webp


随着具有不同外形尺寸的移动设备使用的增长, 我们需要有一些工具, 使我们的Android应用程序能够适应不同类型的屏幕.这就是为什么Android为我们提供了Window Size类, 简单地说, 它是三个大的屏幕格式组, 为我们开发设计标记了关键点.这样我们就避免了考虑许多屏幕设计的复杂性, 将我们的可能性减少到三组, 即: Compat, MediumExpanded..


Windows Size类


1_5Tm17OKlC5n0oy6L641A5g.webp


1_Qv1nt0JJzQPzFfr2G78ulg.webp


支持不同的屏幕尺寸


我们拥有的另一个重要资源是经典布局, 这是预定义的屏幕设计, 可以用于我们的安卓应用中的大多数场景, 还向我们展示了如何将其适应大屏幕的指南.


1_XASUz4kVTK4I0dH8F5slYQ.gif


其他相关资源



Form-Factor培训


Google I/O 2022上的Form Factors


性能


0_QcvMmljmmcvCuqfN.webp


当我们为Android开发应用程序时, 我们必须确保用户体验更好, 不仅是在应用程序的开始, 而且在整个执行过程中.出于这个原因, 重要的是要有一些工具, 使我们能够对可能影响应用程序性能的情况进行预防性分析和持续监测, 因此, 这里有一个工具清单, 可以帮助你达到这个目的:



应用内更新



当你的用户在他们的设备上保持你的应用程序的更新时, 他们可以尝试新的功能, 以及从性能改进和错误修复中获益.虽然有些用户在他们的设备连接到无计量的连接时启用后台更新, 但其他用户可能需要被提醒安装更新.应用内更新是Google Play核心库的一项功能, 提示活跃用户更新你的应用.



应用内更新功能在运行Android 5.0(API级别21)或更高的设备上得到支持.此外, 应用内更新仅支持Android移动设备, Android平板电脑和Chrome OS设备.



0_m8wEQzEW1M1fwwKC.webp


应用内评论


Google Play应用内评论API让你可以提示用户提交Play Store的评分和评论, 而不需要离开你的应用或游戏, 这很方便.


一般来说, 应用内评论流程可以在你的应用的整个用户旅程中的任何时候被触发.在流程中, 用户可以使用1至5星系统对你的应用程序进行评分, 并添加一个可选的评论.一旦提交, 评论将被发送到Play Store并最终显示出来.


为了保护用户隐私和避免API被滥用, 您的应用程序应遵循关于何时请求应用内评论评论提示的设计的严格准则.


应用内评论文档


0_--T1rkTL7DEGJT9B.webp


辅助功能


0_fO3BnqLh8b-H_zLo.webp


辅助功能是软件设计和建造的一个重要特征, 除了改善他们的用户体验外, 还为有可访问性需求的人提供了使用应用程序的能力.这个概念旨在改善的一些残疾是:有视力问题的人, 色盲, 听力问题, 灵巧问题和认知障碍等等.


考虑的因素:



  • 增加文本的可见性(颜色对比, 可调整文本).

  • 使用大型, 简单的控件

  • 描述每个用户界面元素


查看辅助功能--Android文档


安全性


0_Fk42FqLrujNE0O1Z.png


安全性是我们在开发保护设备的完整性, 数据的安全性和用户的信任的应用程序时必须考虑的一个方面, 甚至是最重要的方面, 这就是为什么我在下面列出了一系列的提示, 将帮助你实现这一目的.


<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.myapp">
<permission android:name="my_custom_permission_name"
android:protectionLevel="signature" />


  • 不要将应用程序配置所需的密钥, 令牌或敏感数据直接放在项目库内的文件或类中.使用local.properties代替.


版本目录


Gradle提供了一种集中管理项目依赖关系的标准方式, 称为版本目录;它在7.0版本中试验性地引入, 并在7.4版本中正式发布.


优点是:



  • 对于每个目录, Gradle都会生成类型安全的访问器, 这样你就可以在IDE中用自动完成的方式轻松添加依赖关系.

  • 每个目录对一个构建的所有项目都是可见的.它是一个集中的地方, 可以声明一个依赖的版本, 并确保对该版本的改变适用于每个子项目.

  • 目录可以声明依赖包, 这是通常一起使用的"依赖包组".

  • 目录可以将依赖的组和名称与它的实际版本分开, 并使用版本参考来代替, 这样就可以在多个依赖之间共享一个版本声明.


更多请查看


Logger


Logger是一种软件工具, 用于登记有关程序执行的信息;重要事件, 错误调试信息和其他可能对诊断问题或了解程序如何工作有用的信息.记录器可以被配置为将信息写入不同的位置, 如日志文件, 控制台, 数据库, 或通过将信息发送到日志服务器.



Linter


0_T3lk9cUYryUAo6G1.webp


Linter是一种编程工具, 用于分析程序源代码, 以发现代码中的潜在问题或漏洞.这些问题可能是语法问题, 不恰当的代码风格, 缺乏文档, 安全问题等等, 它们会对代码的质量和可维护性产生影响.



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

工作三年后, 我作为Java后端开发的一些心得

一: 关于开发 我把关于编程的写在最前面, 我觉得对于开发人员来讲, 编程能力才是混饭的手艺, 它也一定程度上也决定了你的钱包和获得工作的筹码. 1. 敢于和善于使用package 对于Java后端开发来讲, 在长时间的web开发中. 大家已经熟悉了...
继续阅读 »

一: 关于开发


我把关于编程的写在最前面, 我觉得对于开发人员来讲, 编程能力才是混饭的手艺, 它也一定程度上也决定了你的钱包和获得工作的筹码.


1. 敢于和善于使用package


对于Java后端开发来讲, 在长时间的web开发中. 大家已经熟悉了MVC架构, 也被这套结构所束缚. 导致创建出来的包也一直都是controller, manager, service, dao. 也将各种各样的类文件都放入其中. 这并不是一种好的做法.


其实我们可以大胆的创建相关的package, 只要让结构更合理, 可读性更高.


比如可以把对接前端的类写到request, response包; 把一些处理器提取出来放到handler中, 把一些定时任务放到schedule包; 把参数构造相关的放到generator; 把校验相关的放到validator下等等. 


2. 合理的提取业务逻辑, 让方法只做和它相关的事情.


不知道大家是否看到过下面这种代码.  例如在一个单据的创建方法中, 做一系列的事情, 比如一坨校验参数逻辑, 一堆取价逻辑, 一堆扣减库存逻辑, 再加上创建单据本身的逻辑. 写下来一个方法上百行不止.

public class OrderManager {

@Autowired
private OrderService orderService;

public String createOrder(Request request){
// 1. 检查单据创建参数是否合法
if(Objects.isNull(request)) {
throw ....;
}
if(CollectionUtils.isEmpty(request.getGoodsList())){
throw ...;
}
...

// 获取商品的价格
List<Goods> goodsList = request.getGoods();
goodsList.forEach(goods -> {
// 查询商品价格

// 校验价格

// 对价格进行填充

});

// 构造单据逻辑

// 创建单据

return orderCode;
}
}

这个方法里做的事, 没有多余的事情, 但是没有合理的进行业务逻辑的提取. 导致代码看起来非常的杂乱.我们可以对一些业务逻辑做提取封装. 来的到更好的可读性和解耦. 这只是一个简单的例子, 在复杂的业务逻辑里更需要合理提取, 否则屎山就出现了.

public class OrderManager {

@Autowired
private OrderService orderService;

@Autowired
private OrderValidator orderValidator;

@Autowired
private OrderGenerator orderGenerator;

@Autowired
private PriceService priceService;

@Autowired
private InventoryService inventoryService;

public String createOrder(Request request){
// 1. 检查单据创建参数是否合法
this.orderValidator(request);

// 获取商品的价格
this.priceService(request.getGoods());

// 构造单据逻辑
Order order = this.orderGenerator.generator(request);

// 创建单据
this.orderService.create(order);
return order.getOrderCode();
}
}

3. 合理的方法命名和方法定义


方法名的定义很令人苦恼, 常常思前想后想不到好名字. 我曾经因为方法命名不好, 被疯狂的comments


方法命名的好坏受个人主观影响, 所以只说几个共同点:



  1. 言简意赅 准确表达方法内容

  2. 方法名与方法内容匹配

  3. 尽量别生僻单词...


对于方法的参数, 参数过多的时候, 对方法进行拆解或者抽象出对象去传参:

// 错误的案例
public int setParam(int xxx, String xxx, String xxx, String xxx, String xxx, int xxx, String xxx, String xxx){
...
}

// 合理的写法
public int setParam(XxxRequest request) {

}

4. 控制方法的圈复杂度, 让代码更有层次感


我一直觉得, 好的代码读起来应该像故事一样, 有前因, 有后果, 中间娓娓道来.  简单举个例子, 我们可能会遇到在方法中去for循环处理数据的情况. 比如在一个方法中, 套了三层循环.

List<String> orderCodeList = request.getOrderCodes();

// 第一层循环
for(String orderCode : orderCodeList){

// 查询单号对应的单据明细
List<OrderItem> items = this.orderItemService.getByCode(orderCode);
// 第二层循环
for(OrderItem item : items) {
// 执行操作1
// 执行操作2
// 执行操作3

// 第三层循环
for()
}
}

可以把每一层for循环都提取出来, 成为单独的一个方法, 来降低圈复杂度, 提高可读性

List<String> orderCodeList = request.getOrderCodes();

// 第一层循环
for(String orderCode : orderCodeList){
this.processSingleOrder(orderCode);
}

public void processSingleOrder(String orderCode){
// 查询单号对应的单据明细
List<OrderItem> items = this.orderItemService.getByCode(orderCode);
this.processItemsData(items);
}

public void processItemsData(){
for(OrderItem item : items) {
// 执行操作1
// 执行操作2
// 执行操作3
}
}

5. 不知道的知识可以去问问Google, 不要自己编


不知道标题是否贴切, 但是大家看了例子就会明白我的意思.


其中最为典型的例子我认为 是对Obejct和集合的判空 和 创建集合

// 对于判断, 很多人喜欢这样写
if( list == null || list.size == 0)
或者
if(order == null)

// 对于创建集合, 去创建空集合, 再添加
List<String> list = new ArrList();
list.add("xxx");
list.add("bbb");

其实只要我们Google以下, Java下如何对集合判空, 就能看到apache.commons 或者google.common等很多类库已经包含这些内容, 并且实现的更严谨, 更优美. 要请善于使用搜索引擎去填补自己不了解的知识.

CollectionUtils.isEmpty(list);

List<String> list = Lists.newArrayList("xxx", "bbb");

6. 不要for循环请求数据库和外部系统接口


慢请求的分析, 可能不需要要先去看有没有复杂的关联查询, 或者是不是数据库查询有没有命中索引, 而是先去看是不是有大哥在for循环去请求数据库和dubbo接口. 循环了1w次. 我曾经遇到过多次请求超时都是因为有人在代码里for循环去select , update. 


对于这种问题的解决, 将循环调用改成单次的批量接口就可以解决问题. 对于mysql的化in操作就可以解决, 对于外部系统对接的, 双方提供批量接口就可以了.


7. 没有意义的注释不要写


我很反感在类上要先写上 @Author @Date @Description 一大串内容表明这是你的杰作, 这不是JDK 也不是什么开源项目!!! 除了你和你的同事没人去看.


再或者像下面这样在方法上直白翻译了一堆废话, 注释不是这么用的...

/**
* 查询用户名称
*
* @Param name:用户名
* @Return 用户
*/
public User getByName(String name);

8. 不要忽视UnitTest


写过单元测试的会发现, 编写完善的单元测试会占用大量的时间, 一般都会超过需求的开发时间, 但是我还是认为单元测试是必须且重要的. 因为作为研发人员, 才是最了解代码中哪里容易出问题的. 更容易写出发现问题的测试用例. 并且代码迭代或者修改后, 也能更快速的发现问题, 将问题停留在研发阶段去解决, 提高整体的进度.


9.善于使用AI编程工具


在我使用了Github copilot和ChatGPT半年后, 我发现我的摸鱼时间变多了... 因为AI编程工具帮我完成了一定量的工作. 例如最常用的代码补全, 代码自动生成, 自动生成单元测试等等


在当今, 熟练掌握AI编程工具, 是提高自己工作效率的极佳的方法. 在未来, AI也一定会代替掉一部分程序员的工作.
1b48670f07e84e188917094af9f05120~tplv-k3u1fbpfcp-watermark.png


10. 利用IDE的工具来完成代码和优化代码



  1. IDEA自带功能扫描代码无用引用, 重复代码等坏味道 : Code → Inspect Code

  2. SonarLint

  3. MyBatis Plus

  4. Lombok

  5. Alibaba Java Coding Guidelines

  6. CheckStyle-IDEA

  7. ...


11. 拥抱新技术


  可能随着工作的时间变长, 大家对新鲜技术的兴趣并不像之前感兴趣. 或者认为目前的技术足够, 远不会过时, 即使过时了, 也会有公司使用.


  技术是不断迭代更新的, 使用技术的人也要随之更新. 当大家都去开始了解和使用云服务, 容器化, 使用JDK 17的新特性, 开始用云原生框架去替换现有技术时, 咱总不能一直玩转jdk 1.8吧.


  了解一些新技术并不是什么值得炫耀的, 不知道也不一定影响你工作和赚钱, 但是当互联网红利已经逐渐褪去, 内卷在越来越重的今天, 机会也变得弥足珍贵. 更好的知识储备, 也能让你能获得下一份工作, 在人才市场获得更多青睐.


  我也一直认为, 开发对很多人来说不光是工作, 也有着一份热爱.


二. 关于处理工作和人际关系


1. 开发并不只是开发


  这个标题就是字面意思, 指的并不是光顾忌自己的开发任务. 同时也要关注公司的运营和公司业务或者说自己负责的项目的业务.


  我见过一些程序员只是单纯的根据产品的文档写需求, 你需求怎么写, 我功能就怎么写. 但是研发在看待需求时, 应该持有自己的见解, 观点和建议. 这也就是需求评审的目的.


  不要觉得需求是产品提的, 和研发没有任何关系. 但是你需要考虑到, 当需求存在问题,  后续的需求优化, bug修复, 甚至数据处理, 可都是要由研发来做的. 简单来说, 错在产品, 但引发问题由你处理.


  而公司的运营, 关系到了你在公司的生存和发展, 所以关注着公司的运营情况, 也大概你知道你明年的涨薪是否有希望, 年终奖是否能按时发放, 以及你是否应该考虑换一个公司去继续搬砖.


2. 合理的分配和安排自己的工作


  拿到需求不要急于开发, 不要急于开发, 不要急于开发.


  我见过一些开发, 在拿到需求后会马不停蹄的开始Coding,  然后就出现边写边改, 再写又发现哪里存在问题, 最后发现写不通,  推翻了之前的结构再写. 


  这个可能并不适合所有人, 但是我认为在开始Coding之前, 是需要构思一下再着手的. 花一些时间分析一下这个需求, 考虑下设计到的各个部分, 构思下自己的开发思路, 设想下其中可能遇到的问题, 当思路清晰后, 再去着手开发, 这样会让你能够流畅的完成开发工作, 并且让你的代码质量更高.


3. 对自己的工作要有Owner意识, 答应的事情要尽力去做到


  什么是工作的Owner意识,  简单来说, 就是这个工作分配给你, 你就是第一负责人.


  对于分配到自己手里的工作, 首先要有一个正确的评估. 可以简单的分为: 这个工作你能不能做, 能不能按时做完, 要怎么做, 最后能做到什么效果.    


  如果因为种种原因做不到, 需要提前预报风险, 不要等到最后一刻告诉大家, 你没做到. 任务分配给你, 是因为这是你的工作, 也有一部分信任在, 是相信你可以做好, 别去辜负别人的信任,  信任可能因为一件事就确立起来, 也可能因为一件事情就毁掉.


4. 我不管别人摸鱼, 但不要影响到我的工作


  工作难免偷懒, 大家都有想休息放松的时候. 我对这个事情的看法就是, 摸鱼可以, 但是不要影响别人的工作. 


  在整个项目或者需求的流程里, 产品, 后端开发, 前端开发, 测试人员都只是其中的一环. 对于各个环节的人员来说, 都是这样, 可以适当摸鱼, 但是不要压缩了别人安排好的时间.


5. 自己的问题勇于承认, 但不是我的锅我不背


  承认自己的问题并不是一个可耻的事情, 但是不承认被别人扒出来可是非常尴尬的. 


  如果你不能按时完成开发任务, 可以说明你的原因, 尽快的提出来, 别等到最后到了Deadline你说你做不完. 


  或者因为你的bug导致了线上事故, 也没必要遮遮掩掩. 快速的定位问题, 解决问题, 在会议上复盘问题, 最好下次发生同样的状况就好, 也没必要因此给自己很大的心理压力和负担. 常在河边走, 哪有不湿鞋. 


但是, 对于甩锅这种问题, 没有人不反感. 我不去讨论什么叫甩锅, 我只去讨论怎么避免甩锅这种事情的发生. 




  • 在对于需求, 会议, 形成良好的书面文档, 各方进行确认




  • 有问题避免天知地知你知我知, 有问题大家一起沟通, 沟通后形成相关的书面文档




  • 当出现这种问题的时候, 拿出自己的证据来证明自己, 不是老子的锅老子不背




6. 摆正自己和领导的位置


  对于领导, 你是他的下属, 不管你们是酒友还是烟友或者是pao友, 你对他最重要的是工作的能力和处理问题的能力. 认真对待分配的任务, 做好自己的分内工作, 让他看到你对他在工作上的价值, 才是建立你们工作关系的基础.


7. 合理的看待别人的反对和批评


  可能每一个参加工作的人都被批评过或者吐槽, 被领导也好, 被同事也好. 在面对批评时, 不要急于反驳. 大家作为成年人, 很少会有人毫无原因和根据的前提下去吐槽你的问题.     


  他提出的问题, 可能就是你切实存在的问题, 他不说, 下一个人也会说, 尽早了解自己的问题并及时改掉不是坏事. 不能不在意, 也不要太在意.


8. 如果你领导或者同事是sb


   能忍忍, 不能忍就滚. 你不能强迫别人走, 你忍不了, 你自己走.


三: 总结


以上只是我个人的一些经验和体会了, 工作三年相比很多大佬来比, 也只是个小毛孩. 但是希望能帮助到大家, 有问题也欢迎大家积极讨论.
  


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

何不食肉糜?

21年的时候,微博上有过一番口诛笔伐,就是就是管清友建议刚开始工作的年轻人就近租房不要把时间浪费在上班的路上,要把时间利用起来投资自己,远比省下的房租划算。 视频见这里:http://www.bilibili.com/video/BV1Bb… 当时我印象非常...
继续阅读 »

21年的时候,微博上有过一番口诛笔伐,就是就是管清友建议刚开始工作的年轻人就近租房不要把时间浪费在上班的路上,要把时间利用起来投资自己,远比省下的房租划算。


视频见这里:http://www.bilibili.com/video/BV1Bb…



当时我印象非常深刻,微博评论是清一色的 “何不食肉糜”,或者说“房租你付?”


可能是因为这件事情的刺激,管清友后来才就有了“我特别知道年轻人建议专家不要建议”的言论。


对还是错?


在我看来,管清友的这个建议可以说是掏心掏肺,非常真诚,他在视频里也说了,他是基于很多实际案例才说的这些话,不是说教。


为什么我这么肯定呢?


很简单,我就是代表案例。


我第一家公司在浦东陆家嘴,四号线浦东大道地铁站旁边,我当时来上海的时候身无分文,借的家里的钱过来的,我是贫困家庭。


但,为了节约时间,我就在公司附近居住,步行五分钟,洗头洗澡都是中午回住的地方完成,晚上几乎都是11:00之后回去,倒头就睡,因为时间可以充分利用。


节约的时间,我就来研究前端技术,写代码,写文章,做项目,做业务,之前的小册(免费章节,可直接访问)我也提过,有兴趣的可以去看看。


现在回过头来看那段岁月,那是充满了感激和庆幸,自己绝对做了一个非常正确的决定,让自己的职业发展后劲十足。


所以,当看到管清友建议就近租房的建议,我是非常有共鸣的,可惜世界是参差的,管清友忽略了一个事实,那就是优秀的人毕竟是少数,知道如何主动投资自己的人也是凤毛麟角,他们根本就无法理解。


又或者,有些人知道应该要投资自己,但是就是做不到,毕竟辛苦劳累,何苦呢,做人,不就是应该开心快乐吗?


说句不好听的,有些人的时间注定就是不值钱的。


工作积极,时间长是种优势?


一周前,我写了篇文章,谈对“前端已死”的看法,其中提到了“团队下班最晚,工作最积极”可以作为亮点写在简历里。


结果有人笑出了声。



好巧的是,管清友的租房建议也有人笑了,出没出声并不知道。



也有人回复“何不食肉糜”。


这有些出乎我的意料,我只是陈述一个简单的事实,却触动了很多人的敏感神经。


我突然意识到,有些人可能有一个巨大的认知误区,就是认为工作时长和工作效率是负相关的,也就是那些按时下班的是效率高,下班晚的反而是能力不足,因为代码不熟,bug太多。



雷军说你说的很有道理,我称为“劳模”是因为我工作能力不行。


你的leader也认为你说的对,之前就是因为我每天准时下班,证明了自己的能力,所以自己才晋升的。


另外一个认知误区在于,把事实陈述当作目标指引。


如果你工作积极,是那种为自己而工作的人,你就在简历中体现,多么正常的建议,就好比,如果你是北大毕业的,那你就在简历中体现,没任何问题吧。


我可没有说让你去拼工作时长,装作工作积极,就好比我没有让你考北大一样。


你就不是这种类型的人,对吧,你连感同身受都做不到,激动个什么呢,还一大波人跟着喊666。


当然,我也理解大家的情绪,我还没毕业的时候,也在黑心企业待过,钱少事多尽煞笔,区别在于,我相对自驱力和自学能力强一些,通过自己的努力跳出了这个循环。


但大多数人还是被工作和生活推着走,所以对加班和内卷深恶痛绝,让本就辛苦的人生愈发艰难,而这种加班和内卷并没有带来收入的提升。


那问题来了,有人通过努力奋斗蒸蒸日上,有人的辛苦努力原地踏步,同样的,有的人看到建议觉得非常有用,有的人看到建议觉得何不食肉糜,区别在哪里呢?


究竟是资本作恶呢?还是自己能力不足呢?


那还要建议吗?


管清友不再打算给年轻人建议了,我觉得没必要。


虽然,大多数时候,那些听得进去建议的人大多不需要建议,而真正需要建议的又听不进,但是,那么多年轻人,总有一部分潜力股,有一些真正需要帮助的人。


他们可能因为环境等原因,有短暂的迷茫与不安,但是,来自前人发自真心的建议或许可以让他们坚定自己前进方向,从而走出不一样的人生。


就像当年我被乔布斯的那些话语激励过那般。


所以,嘲笑之人任其笑之,只要能帮助到部分人,那就有了价值。


因此,我不会停止给出那些我认为对于成长非常有帮助的建议。


(完)


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

短短1个小时,让公司损失近3万

这是一个悲伤的故事,也是教训最深刻的一次。发生在2022年1月份,春节前几周。在聊这个事之前,我想借用美团的一个案例作为切入点。 (我们公司不是美团的这种业务,但也利用了会员发券这种机制,都是在待支付勾选会员产生待使用的券,最后选择使用,这里我就拿美团来讲) ...
继续阅读 »

这是一个悲伤的故事,也是教训最深刻的一次。发生在2022年1月份,春节前几周。在聊这个事之前,我想借用美团的一个案例作为切入点。


(我们公司不是美团的这种业务,但也利用了会员发券这种机制,都是在待支付勾选会员产生待使用的券,最后选择使用,这里我就拿美团来讲)


先来看下面这幅图,大家点外卖再熟悉不过的一个页面!


image.png


当勾选开通会员时,系统会自动给你发6张优惠券(取消勾选,则6张券消失)


image.png


那么问题来了,这6张券是怎样的一种方式存在?



因为这里要考虑到,用户勾选只是勾选,还没有真正的发到用户钱包里,只有用户支付了,才能真正给用户发送,这里面就牵扯到这个临时数据怎么处理更好



我想了想,无非三种




  • 前端自己生成数据,给后端规约传参




  • 后台落noSql,用户在选择券的时候,后台查询优惠券接口会把noSql里的东西也带上




  • 后台存关系型数据库,这里就会牵扯到太多的垃圾数据,因为很多用户可能只是勾选,并不会购买




大的方面应该就这三种,至于细节,那各凭本事,看谁处理的好。


最难的需求


时间拉回到今年1月份,这是春节前最悠哉的时光,年终奖都定好了!


忽然开会说要在待支付界面引入会员机制,周期为一周,快速上线,要先看数据。根据数据节后再做调整。没给开发留一点点评估的时间,还没容得上我们说话,就。。。。


image.png


这里简单说下需求吧:


平台会员原来就有,只是没有介入到待支付,原来购买平台会员发两张券,这次到待支付要根据用户不同的属性发送不同的券,张数也不尽相同


作为产品部的技术负责人,在这个周期范围内,首要做的就是看如何快速上线,我和产品商量砍了很多需求,原型设计上的很多细节都包括在内,否则干死都不一定能上线(天下产品都一样,研发不硬,产品必欺。但这次是运营是拿着尚方宝剑给产品下的命令,时间既然是不能变的,那就只能把需求点减到最少)


就这样,技术方案用了最简单的,也是最不安全的,没错,全部交给前端去生成券的数据。金额都是写死的,说白了,就是前端按照ui图出的,后台没有出接口,因为在整体支付流程还有大量工作需要因为平台会员的介入而有大量工作(别说不专业,没办法)。


所以,减免多少钱,是由前端传的(这里可能很多人会笑话我,因为没有一家是前端传金额的,是的,我们做了)


image.png


看到这里肯定有人说,虽然不合理,但是应该也不会有大问题啊。


可是问题就是爆发出来了。我们有一种券,叫”全免券“,就可以免掉本次费用。前端因为很多数据写死了,结果这个全免券没有考虑进去。测试当时测试的时候也忽略了,导致线上在某种情况下会走全免券的机制


黑色星期五


我们任何上线的时间都会定到周四晚上,因为周四升级,周五如果有问题,可以处理回退。


清晨睡的正香,电话响了,一看群里,炸锅了。我们的用户端主要是微信小程序,了解的都知道有个审核期,后台服务晚上升级好之后,小程序是早上运维给审核通过的。


结果运营早上看到很多数据,好多用户支付都是0元,对比一看全都购买过平台会员。顿时我就没有了睡意,赶紧通知运维把小程序回退到上一个版本(幸亏后台接口兼容处理得当)


问题就是A类用户在B种情况下,传到后台就是走全免券的逻辑。


顿时“精神抖擞”的我收拾收拾背包去公司了


image.png


最后好像运营给出一个数据,3万左右。我私下里也大概算了下。。。。。。


年终奖整个team都削了点,包括我们部分老大,包括测试。主要责任在我,方案是我定的,确实不是最佳选择。


总结教训


这确实是我入行以来最大的bug,作为负责人没有处理好可能出现的问题,从方案到落地,需要慎之又慎。


协调各部门,统筹方案。


也给产品和运营个教训吧。就说到这里吧,希望给大家点经验,祝大家写不出八阿哥


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

谈公平:你十年寒窗,比人家三代家业?

有一个想法,憋了好久了,迟迟没敢说,怕三观正不正,被喷。 网上经常看到这样的一幕:一个老太太在路灯下啃着馒头卖菜,旁边广场上是穿着华丽跳舞的老太太。 网友说,这个世界太不公平了,都是同样的年龄,为什么差别这么大。取消那些跳舞老太太的退休金,让她们也去卖菜。 ...
继续阅读 »

有一个想法,憋了好久了,迟迟没敢说,怕三观正不正,被喷。


网上经常看到这样的一幕:一个老太太在路灯下啃着馒头卖菜,旁边广场上是穿着华丽跳舞的老太太。


WechatIMG5.jpeg


网友说,这个世界太不公平了,都是同样的年龄,为什么差别这么大。取消那些跳舞老太太的退休金,让她们也去卖菜。


我看完评论就在想:都去卖菜,就是公平吗?或者,都去跳广场舞才是公平?


到底什么是公平?


其实大众很难说清楚什么是公平,但是经常会提“这不公平!”。


一个出身贫困的农村孩子,经过千辛万苦,终于考上了大学。他毕业后,起早贪黑努力工作。终于,他在城里买下了两套房子,自住一套,出租一套。


看到上面的例子,大家都会说,哎呀,这叫天道酬勤!老天不会辜负努力的孩子。正是他通过自己十几年的努力,才有了今天的成绩。相比于他早就辍学的同村伙伴,相比于那些逃课打游戏的大学同学,这一切对于他来说,是公平的!


哎,再来一个例子。有一个富二代,他从小就逃课、打架,高考也没考好,花钱读了个海外大学,大学也没学啥,就学会了一周换一个女朋友。他刚毕业,他父亲就给他一栋大楼,让他练习如何赚钱。


这个例子,公平吗?


想好了再回答,考验感性和理性的时候到了。


不着急回答。对于上面的例子,我想把时间线再拉长一些。


这位富二代的爷爷,也是出身农村。他老人家从小就父母双亡。一开始他跟着伯父生活,但是伯母经常虐待他。因此,他离家出走,到城里乞讨。后来,他被好心的包子铺老板收留。他在包子铺打工,因为能吃苦,干活勤快,又懂得感恩,所以深受老板和顾客的喜爱。凭借着一膀子力气,他在城里做到了成家立业娶媳妇,后来还有了下一代。这下一代出生便在城里,教育条件也好,从小又跟着父母做生意,见得多,识得也广。后来,下一代凭借着父母积攒的老客户,在事业上越做越大,最终形成了自己的商业帝国。再后来,就有了上面的那个富二代。


大家说,将时间线拉长之后,这三代人也是一个“天道酬勤”的故事吧?开头富二代爷爷的经历,可以对标大学生的一生。


我发现人类的社会和动物的族群,有一个很明显的区别,那就是物质和精神的继承。


Snip20230517_1.png


在一个狮群里,狮王是靠本领拼出来的。下一任狮王,是不是由它的儿子当,能否继续拥有这一方领土和交配权,得看小狮子能不能打败其他狮子,成为最强者。不止是狮子,猴子也是一样。猴群里猴王是最强壮、最聪明的猴子。要是老猴王不行了,新猴王会通过战斗取代它。之所以需要最强的猴子当猴王,那是因为它能够保护整个猴群。虽然,这个猴王不是无所不能。但在这群猴子里面,找不出来比它更合适的了。


动物界的这些名利、地位、经验,是没法传给下一代的。它们的群落会不定期重新洗牌


但是我们人类社会却不一样。一个富豪,就算他儿子不聪明,甚至身体有残疾,一样有很多人像对待猴王一样仰视他,他一样能获得最优质的繁殖资源。


你说,人类的这种方式高级不高级。


从短期看,不高级。上面说的那种人,要是在动物界,早就被淘汰了。一头斑马,即便是脚部受了点伤,也基本就宣告死亡了。大自然就是要优胜劣汰。谁让这头斑马不注意,凭什么狮子单单就咬伤了你,而不是别人。就算运气不好,这也是一种劣势。动物界,就是以此来保证强大的遗传特征,流通在群体的基因库中。


但是从长期看,人类的这种方式却很高级。因为正是有了资源继承这种特权,才让人类一想到能为子孙后代留点东西、做点事情,就不怕苦不怕累,成为永动机。就算是这一代是个病秧子,没有关系,只要能挺过去,后面还有机会强盛起来。


我们人类为什么这么想?我们是韭菜成精了吗?


你可能不知道,你是被遗传基因控制的。


Snip20230517_2.png


你的身高、体重,哪里长手,哪里长耳朵,都是写在基因里的。你只是照着图纸在搭建而已。


你不要觉得你有自我。哥们,咱们不配!你知道吗?基因才有自我。


基因的想法不是让你活下去,而是让它自己活下去。而且,还要一代更比一代强。它活下去的方式,就是依靠你来进行繁殖和生育。你之所以怕死,其实是基因怕自己遗传不下去。有些动物,比如鲑鱼、蝴蝶等,它们繁殖完就死掉了。


人类的基因很高级,有了后代后不但不死,而且还让你把孩子养大,甚至还给孩子看孩子。幸好DNA里没法存货物,不然有人可能抱着金条出生。这不是因为有爱,这是因为基因的控制!


为什么动物就做不到这些?因为在自然界,资源是有限的,有时候自己和后代只能保留一个。基因选择了留它自己。


人类通过物质和财富的“遗传”,解决了这个问题。那你说,这种方式是不是非常高级。


也正是这种物质和精神可以继承,才让人类从动物界中脱颖而出,成为了地球的主人。


说这么多,还扯到了生物和伦理,好像有点跑题了。但这也从某一个方面佐证了一些道理。有些事,从局部来看很不公平。但是,当把时间线拉长再看,这又是合理的。


大自然怎么会过一天算一天呢?她是想永生的。


这时,当我们再面对一些局部不公平时,你不必太过于消沉。把时间拉长,你可以把自己作为一个起点。想想那些让你感到气愤的“持有特权者”,他们从几十年前,就已经开始像你现在一样努力了


难道你想要用你的几年奋斗,去超越人家几十年甚至上百年的沉淀吗?从长远来看,恐怕这也不公平吧?


WechatIMG4.jpeg


一个指导大家考研的老师,他说她女儿可以不用考研。短期看,这好像这是个笑话。但是,他说,考研是为了更好地谋生。她的女儿可以不用为生计发愁,把精力投入到她喜欢做的事情。


“人人平等”最早源于宗教。他们说,不管你如何威风,最后到上帝那里都一样。而法律上的平等,是为了避免社会失去秩序。


世上没有绝对的公平。因为单就“公平”这个词的定义,就很难说清楚。


好了,就说这么多吧。


我是一个理工男,思考问题的方式可能有些偏激。文中提到了“奋斗”和“努力”(不知何时这两个词变味了),我不是想给大家灌鸡汤。理工科最讨厌鸡汤,一点逻辑都没有。我只是从理性的角度,给大家分享一种思路。


总之,到与不到的,还请大家多多包涵。


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

透过Redis看待我们是否应该继续使用c语言

杰克-逊の黑豹,恰饭了啦 []( ̄▽ ̄) keywords: Redis c cpp 厌弃c语言的现象 长久以来,程序员们对待c语言的态度非常矛盾,主要应该有这么几种: 喜爱、工作中使用c语言; 不反感、工作中使用c语言; 不反感、工作中不使用c语言; 反感...
继续阅读 »

杰克-逊の黑豹,恰饭了啦 []( ̄▽ ̄)


keywords: Redis c cpp


厌弃c语言的现象


长久以来,程序员们对待c语言的态度非常矛盾,主要应该有这么几种:



  • 喜爱、工作中使用c语言;

  • 不反感、工作中使用c语言;

  • 不反感、工作中不使用c语言;

  • 反感、工作中使用c语言;

  • 反感、碰都不碰c语言;


赞许c语言的程序员,可能会有这些观点:



  • 使用c语言的程序员是最nb的;

  • 真正的编程大佬都使用c语言;


讨厌c语言的程序员,大抵是因为这些:



  • 没有舒服的包管理机制;

  • 没有丰富的标准库;

  • 没有面向对象的语料支持;

  • 内存不够安全(内存泄漏、悬垂指针、非法指针、无效指针等等);

  • 编译器输出信息不够好;

  • 错误处理不够舒服;

  • 项目管理复杂;


再加上,像Rust/Go/Swift等这种现代编程语言的出现,更让很多程序员厌弃c语言。



现代编程语言和c语言比起来,有什么最大的特点?以我个人而言,最大的特点就是编程语言的表达能力
非常出色,不是C语言可以比拟的。表达能力更具体地讲就是更灵活、更模块化、更适合人阅读。



难道说c语言真的是魔鬼吗?


反驳者一定会举出嵌入式开发、操作系统开发、驱动开发领域的成功案例。


但我觉得,这样就又大又空洞了。


近日,我在看Redis1.3.6源码,觉得倒是个很好的例子说说这事儿,不过读者大可放心,本文不是xx源码阅读, xx源码细品的文章,更不是什么八股文,单纯作为一个例子,解释下c语言是不是魔鬼这回事儿。


先说下我个人的结论吧。


c语言是不是魔鬼,取决于你能hold住多少c语言代码量。hold住,c语言的缺点不是事儿;hold不住,c语言就会带来悲剧。一定程度上,这就像我们尝试三个小时保持精神高度集中一样,诚然你说自己的c语言功夫非常扎实,但是随着项目规模增大,我可不相信你照样可以注意到每个角落的c语言隐患。


如果你是95后,c语言不适合作为你的主力开发语言,但是你可以学习由c语言开发且非常稳定的项目,也可以基于c语言ABI将这些c语言项目植入到别的编程语言开发的项目中,比如Rust/Go/Javascript/Swift项目。


为什么选择Redis1.3.6


事物总是由简入繁,功能总是由少渐多,项目总是由骨到肉。


越早的版本,越容易看出作者的宏观思路。


越新的版本,越会充斥各种新功能、小功能,越发遮掩作者最初的构思。


我从github下载好Redis后,发现最早的一个版本号就是1.3.6, 所以就用这个做参考了。



实际上,我硬着头皮看了两天最新的版本,发现确实啃不动。



看看Redis怎么解决包管理问题的


在1.3.6版本中,redis没有依赖的包,但是在最新版本中,redis将这些依赖放置于deps文件夹下。这里边没有什么特别的地方,就是将依赖的仓库源码放置在一个单独的文件夹下,定期去看看仓库源码有没有更新,如果更新的话,按需要将本地做更新,就和你的本地仓库、远程仓库一样。
截屏2023-05-23 23.29.23.png


同时,你也看到了,依赖并不多确实可以这样做,如果依赖多的话,这么搞就难搞了,就需要包管理程序(第三方的或者自己开发的)。不过嘛,一般用c语言开发的项目,都是追求极度性能,功能非常专一的项目,不会掺入太多的依赖。但是应用层项目就不同了,依赖贼多。


看看Redis怎么解决标准库不太丰富的


c语言开发时用到的API基本上是操作系统原生提供的API,这些API都分布在操作系统提供的固定的.h文件中,比如:



  • unistd.h

  • sys/sysctl.h

  • pthread.h

  • sys/stat.h

  • fcntl.h

  • execinfo.h

  • ucontext.h


当然也需要c语言提供的标准库.h文件,比如:



  • stdlib.h

  • stdio.h

  • stdarg.h

  • erron.h

  • ctype.h


例如redis.c文件中就用到了很多耳熟能详的.h文件:


截屏2023-05-24 00.19.15.png


两类之外的功能,要么从第三方拷贝,要么就要自己写。当然,在redis1.3.6中依赖并不多,只需要一些数据结构的实现,比如哈希表(hash table)、动态字符串(dynamic string)、双向链表(double linked list)、压缩型字符串映射表(zip map, 应该是redis作者独创的)。这些都是redis作者实现的。


可不要被实现吓坏了。


自己创造式地想到一个数据结构,然后用代码写出来,叫做实现;


自己参考数据结构书籍、论文中的理论,给出自己的实现,这也叫做实现。


都成年人了,要知道:考试不是只叫做闭卷考试


像pqsort(部分快速排序),redis作者就是参考NetBSD平台下的libc源码实现的,作者在代码注释中给出了声明:


截屏2023-05-24 00.16.58.png


你可能会问了,我实现的版本性能无法保证怎么办?redis作者其实也告诉我们答案了,那就是先搞出一版实现,至于性能优劣是另一个问题,可以先不用管它。于是,在ae.c的文件中,我们看到redis作者写到这样的注释:


截屏2023-05-24 00.24.19.png


c项目一般保持功能专一、性能卓越,常常要定制化一些数据结构的代码,所以即便标准库加入这些数据结构,也未必能满足c项目的需要,可能也派不上多大用场。


如此看来,c项目不太需要那种普适、统一的标准库,更需团队开发、企业开发、组织开发范围内的标准库。


但是对于技术经验尚浅(没查阅论文、手册、其余资料,独立实现一些功能库)的开发者来说,他们需要的是涵盖功能的标准库,而不是刚提到的那种高度定制化的标准库。


所以标准库不太丰富的这种问题,对于项目不大、开发者具备经验的情况而言,不成问题;对于经验不足的开发而言,是个头大的问题;对于项目贼大的情况而言,无论经验多少,都是头大的问题。


看看Redis是怎么解决面向对象编程的


面向对象编程,说的就是一种编程思想,但一部分开发者常常会被编程语言的形式所蒙蔽,认为编程语言给出了面向对象代码形式(提供class extends public等关键字),才算面向对象了,对于没有给出这种形式的编程语言,就无法面向对象编程。


看看Redis是怎么做的吧。


截屏2023-05-24 00.41.58.png


struct等效于classaeBeforeSleepProc就是类中定义的成员方法啊,其定义如下:


截屏2023-05-24 00.43.17.png


至于继承,可以使用组合的方式替代;


至于多态,可以继续使用函数指针或者改用函数映射表替代;


宽泛地讲,只要有了封装, 即便没有给出严格的继承多态, 也可以认为是面向对象。毕竟,面向对象是一种编程思想,不是僵死的格式。


不过呢,语言本身如果提供了面向对象概念的关键字,代码直接理解起来的难度就大大降低,加之有IDE功能的帮助,读代码更加顺畅,这是c代码所不及的。这也就是说,当项目的概念变得非常多,c代码即便可以面向对象编程,但也是个问题,会让读代码的人一通找啊找。


Redis怎么解决内存安全问题的


这个问题确实是要害,在这个版本中,redis并没有给出什么非常好的内存安全手段,有的话,也只是在定义数据结构的时候,给出该数据结构的内存释放方法:


截屏2023-05-24 00.54.20.png


和cpp的析构函数一个道理,只不过cpp是编译器插入内存释放代码,c语言是开发者手动加入。


在代码量hold的住的情况下,开发者确实有足够的精力和耐心确保内存问题,但是代码量一旦上涨,开发者恐怕就顾不过来了。你可以看到像linux这样大型的项目有不少issue,但你很少听说hello-world级别的c代码有什么issue。


所以,到底是人去处理内存问题还是编译器处理内存问题,归根结底就是在大量工作的条件下,你更相信人的表现还是机器的表现。相比之下,我觉得机器更靠谱一些。


c项目很复杂,无法阅读?


一般而言,在同样成熟的情况下,c项目的代码读起来会麻烦一些。但这是成熟之后,也就是在加入杂七杂八功能、BUG布丁等等之后的c项目。以Redis 1.3.6为例,其结构相当简洁:


截屏2023-05-24 01.02.53.png


主体框架就这么直白,后续版本的复杂化,都是功能扩展、结构调整,这条主线是不可能断掉的。所以,别把C项目想象的那么复杂,那么难。C项目的复杂还有一种可能是C语言表达能力导致的。因为C语言语法足够简洁,直接用的操作系统API,封装比较少,别的语言三言两语交代的事情,C语言可能要多说很多话才可以表述出来,但是编译到了二进制形式的产物,C语言就比别的语言“简洁”了。


后话


C语言并没有那么可怕,它不是开发者的定时炸弹,也不是开发者的无双宝剑。C语言到底怎么样,很大程度上要看代码量和开发者的承受能力。并不是说很多项目由C语言编写,就证明C语言是一门你可以信赖并使用的语言,很大程度上讲,很多项目用C语言开发是历史问题,在那个年代和阶段,编程语言的选择余地太少,哼不能从1980年等到2010年以后,再使用现代化编程语言编写代码吧?


从另一个角度上看,这个时代你完全可以不使用C语言,没必要跟着上古大佬的品味走。须知,工具是用来解放你的生产力的,不是给你的生产力添堵的。你用不用C语言,和你是不是一个合格的软件工程师,没什么必要的联系。


不过,不用归不用,学习还是要学习的,毕竟ABI层面还是以C语言为标准的,想理解函数调用、指令跳转等内容,C语言是避不开的。


对于要不要继续使用C语言,你有什么看法呢,欢迎留言。


作者:杰克逊的黑豹
来源:juejin.cn/post/7236354905493176377
收起阅读 »

iOS加固保护新思路

之前有写过【如何给iOS APP加固】,但是经过一段时间的思考,我找到了更具有实践性的代码,具体可以看下面。 技术简介 iOS加固保护是基于虚机源码保护技术,针对iOS平台推出的下一代加固产品。可以对iOS APP中的可执行文件进行深度混淆、加固,并使用独创的...
继续阅读 »

之前有写过【如何给iOS APP加固】,但是经过一段时间的思考,我找到了更具有实践性的代码,具体可以看下面。


技术简介


iOS加固保护是基于虚机源码保护技术,针对iOS平台推出的下一代加固产品。可以对iOS APP中的可执行文件进行深度混淆、加固,并使用独创的虚拟机技术对代码进行加密保护,使用任何工具都无法直接进行逆向、破解。对APP进行完整性保护,防止应用程序中的代码及资源文件被恶意篡改。


技术功能


目前iOS加固主要包含逻辑混淆、字符串加密、代码虚拟化、防调试、防篡改以及完整性保护这三大类功能。通过对下面的代码片段进行保护来展示各个功能的效果:


- (void) test {
if (_flag) {
test_string(@"Hello, World!",@"你好,世界!","Hello, World!");
} else {
dispatch_async(dispatch_get_mian_queue(), ^{
do_something( );
});
}
int i=0;
while (i++ < 100) {
sleep(1);
do_something( );
}
}

将代码编译后拖入IDA Pro中进行分析,可以得到这样的控制流图,只有6个代码块,且跳转逻辑简单,可以很容易地判断出if-else以及while的特征:


1.png


将其反编译为伪代码,代码逻辑及源代码中使用的字符串均清晰可见,与源代码结构基本一致,效果如下


void __cdecl -[ViewController test](ViewController *self, SEL a2)
{
signed int v2; // w19
__int64 v3; //x0

if ( self->_flag )
sub_100006534 ( CFSTR("Hello, World!"),CFSTR("你好,世界!"), "Hello, World!" );
else
dispatch_async ( &_dispatch_main_q, &off_10000C308 );
v2 = 100;
do
{
v3 = sleep( 1u );
sub_100006584( v3 );
--v2;
}
while ( v2 );
}

1 代码逻辑混淆


通过将原始代码的控制流进行切分、打乱、隐藏,或在函数中插入花指令来实现对代码的混淆,使代码逻辑复杂化但不影响原始代码逻辑。


对代码进行逻辑混淆保护后,该函数的控制流图会变得十分复杂,且函数中穿插了大量不会被执行到的无用代码块,以及相互间的逻辑跳转,逆向分析的难度大大增强:


2.png
若开启防反编译功能,则控制流图会被完全隐藏,只剩下一个代码块,且无法反编译出有效代码(如下图所示),这对于对抗逆向分析工具来说非常有效,包括但不限于(IDA Pro, Hopper Disassembler, Binary Ninja, GHIDRA等)


3.png


void __cdecl -[ViewController test](ViewController *self , SEL a2)
{
JUMPOUT (__CS__, sub_100005A94(6LL, a2));
}

2 字符串加密


把所有静态常量字符串(支持C/C++/OC/Swift字符串)进行加密,运行时解密,防止攻击者通过字符串进行静态分析,猜测代码逻辑。


对代码中的字符串进行加密之后,所有的字符串都被替换为加密的引用,任何反编译手段均无法看到明文的字符串。你好,世界!,Hello, World!等字符串原本可以被轻易的反编译出来,但保护之后已经看不到了:


void __cdecl -[ViewController test](ViewController *self, SEL a2)
{
__int64 v2; //x0
__int64 v3; //x0
signed int v4; // w19
__int64 v5; //x0

if ( self->_flag )
{
v2 = sub_100008288();
v3 = sub_10000082E8(v2);
sub_100008228(v3);
sub_100007FB0( &stru_100010368, &stru_10001038, &unk_100011344);
}
else
dispatch_async ( &_dispatch_main_q, &off_100010308 );
}
v4 = 100;
do
{
v5 = sleep( 1u );
sub_100008004( v5 );
--v4;
}
while ( v4 );
}

3 代码虚拟化


将原始代码编译为动态的DX-VM虚拟机指令,运行在DX虚拟机之上,无法被反编译回可读的源代码,任何工具均无法直接反编译虚拟机指令。


采用代码虚拟化保护后,对函数进行反编译将无法看到任何与原代码相似的内容,函数体中只有对虚拟机子系统的调用:


void __cdecl -[ViewController test](ViewController *self, SEL a2)
{
SEL v2; // x19
__int64 v3; // x21

v2 = a2;
v3 = sub_10000C1EC;
(( void (* )(void))sub_10000C1D8)();
sub_10000C1D8( v3, v2);
sub_10000C180( v3, 17LL);
}

4 防调试


防止通过调试手段分析应用逻辑,开启防调试功能后,App进程可以有效地阻止各类调试器的调试行为:


4.png


5 防篡改,完整性保护


防止应用程序中的代码及资源文件被恶意篡改,杜绝盗版或植入广告等二次打包行为。


结语


以上内容是不限制ios版本的。不过,对于App类型,仅支持.xcarchive格式,不支持.ipa格式。并且,有以下注意事项:



  • Build Setting 中 Enable Bitcode 设置为 YES

  • 使用 Archive 模式编译以确保Bitcode成功启用,否则编译出的文件将只包含bitcode-marker

  • 若无法开启Bitcode,可使用辅助工具进行处理,详见五、iOS加固辅助工具 -> 5.2 启用 bitcode

  • 压缩后体积在 2048M 以内


以上是根据顶象的加固产品操作指南出具的流程,如需要更详细的说明,可以自行前往用户中心~


作者:昀和
来源:juejin.cn/post/7236634496765509692
收起阅读 »

入坑两个月自研创业公司

一、拿offer 其实入职前,我就感觉到有点不对劲,居然要自带电脑。而且人事是周六打电话发的offer!自己多年的工作经验,讲道理不应该入这种坑,还是因为手里没粮心中慌,工作时间长的社会人,还是不要脱产考研、考公,疫情期间更是如此,本来预定2月公务员面试,结果...
继续阅读 »

一、拿offer


其实入职前,我就感觉到有点不对劲,居然要自带电脑。而且人事是周六打电话发的offer!自己多年的工作经验,讲道理不应该入这种坑,还是因为手里没粮心中慌,工作时间长的社会人,还是不要脱产考研、考公,疫情期间更是如此,本来预定2月公务员面试,结果一直拖到7月。


二、入职工作


刚入职工作时,一是有些抗拒,二呢是有些欣喜。抗拒是因为长时间呆家的惯性,以及人的惰性,我这只是呆家五个月,那些呆家一年两年的,再进入社会,真的很难,首先心理上他们就要克服自己的惰性和惯性,平时生活习惯也要发生改变


三、人言可畏


刚入职工作时,有工作几个月的老员工和我说,前公司的种种恶心人的操作,后面呢我也确实见识到了:无故扣绩效,让员工重新签署劳动协议,但是,也有很多不符实的,比如公司在搞幺蛾子的时候,居然传出来我被劝退了……


四、为什么离开


最主要的原因肯定还是因为发不出工资,打工是为了赚钱,你想白嫖我?现在公司规模也不算小了,想要缓过来,很难。即便缓过来,以后就不会出现这样的状况了?公司之前也出现过类似的状况,挺过来的老员工们我也没看到有什么优待,所以这家公司不值得我去熬。技术方面我也基本掌握了微信和支付宝小程序开发,后面不过是需求迭代。个人成长方面,虽然我现在是前端部门经理,但前端组跑的最快,可以预料后面我将面临无人可用的局面,我离职的第二天,又一名前端离职了,约等于光杆司令,没意义。


五、收获


1.不要脱产,不要脱产
2.使用uniapp进行微信和支付宝小程序开发
3.工作离家近真的很爽
4.作为技术人员,只要你的上司技术还行,你的工期他是能正常估算,有什么难点说出来,只要不是借口,他也能理解,同时,是借口他也能一下识别出来,比如,一个前端和我说:“后端需求不停调整,所以没做好。”问他具体哪些调整要两个星期?他又说不出来。这个借口就不要用了,但是我也要走了,我也没必要去得罪他。
5.进公司前,搞清楚公司目前是盈利还是靠融资活,靠融资活的创业公司有风险…


六、未来规划


关于下一份工作:
南京真是外包之城,找了两周只有外包能满足我目前18k的薪资,还有一家还降价了500…
目前offer有
vivo外包,20k
美的外包,17.5k
自研中小企业,18.5k


虽然美的外包薪资最低,但我可能还是偏向于美的外包。原因有以下几点:
1.全球手机出货量下降,南京的华为外包被裁了不少,很难说以后vivo会不会也裁。
2.美的目前是中国家电行业的龙头老大,遥遥领先第二名,目前在大力发展b2c业务,我进去做的也是和商场相关。
3.美的的办公地点离我家更近些
4.自研中小企业有上网限制,有过类似经验的开发人,懂得都懂,很难受。


关于考公:
每年10月到12月准备下,能进就进,不能再在考

作者:哇哦谢谢你
来源:juejin.cn/post/7160138475688165389
公上花费太多时间了。

收起阅读 »

夯实基础:彻底搞懂零拷贝

零拷贝 零拷贝我相信大家都听说过,Netty 也用到了零拷贝来大幅提升网络吞吐量,但是大多数人对零拷贝中的原理和过程却很难讲清楚,接下来我会给大家详细讲解这方面的内容。 首先,我们看看,没有零拷贝的时候,应用程序是如何从服务器的磁盘读数据并通过网卡发送到网络的...
继续阅读 »

零拷贝


零拷贝我相信大家都听说过,Netty 也用到了零拷贝来大幅提升网络吞吐量,但是大多数人对零拷贝中的原理和过程却很难讲清楚,接下来我会给大家详细讲解这方面的内容。
首先,我们看看,没有零拷贝的时候,应用程序是如何从服务器的磁盘读数据并通过网卡发送到网络的。


无零拷贝时,数据的发送流程


非DMA非零拷贝 (1).png


大家可以通过上图看到,应用程序把磁盘数据发送到网络的过程中会发生4次用户态和内核态之间的切换,同时会有4次数据拷贝。过程如下:



  1. 应用进程向系统申请读磁盘的数据,这时候程序从用户态切换成内核态。

  2. 系统也就是 linux 系统得知要读数据会通知 DMA 模块要读数据,这时 DMA 从磁盘拉取数据写到系统内存中。

  3. 系统收到 DMA 拷贝的数据后把数据拷贝到应用内存中,同时把程序从内核态变为用户态。

  4. 应用内存拿到从应用内存拿到数据后,会把数据拷贝到系统的 Socket 缓存,然后程序从用户态切换为内核态。

  5. 系统再次调用 DMA 模块,DMA 模块把 Socket 缓存的数据拷贝到网卡,从而完成数据的发送,最后程序从内核态切换为用户态。


如何提升文件传输的效率?


我们程序的目的是把磁盘数据发送到网络中,所以数据在用户内存和系统内存直接的拷贝根本没有意义,同时与数据拷贝同时进行的用户态和内核态之间的切换也没有意义。而上述常规方法出现了4次用户态和内核态之间的切换,以及4次数据拷贝。我们优化的方向无非就是减少用户态和内核态之间的切换次数,以及减少数据拷贝的次数



为什么要在用户态和内核态之间做切换?


因为用户态的进程没有访问磁盘上数据的权限,也没有把数据从网卡发送到网络的权限。只有内核态也就是操作系统才有操作硬件的权限,所以需要系统向用户进程提供相应的接口函数来实现数据的读写。
这里涉及了两个系统接口调用分别是:


read(file, tmp_buf, len);


write(socket, tmp_buf, len);



于是,零拷贝技术应运而生,系统为我们上层应用提供的零拷贝方法方法有下列两种:



  • mmap + write

  • sendfile


MMAP + write


这个方法主要是用 MMAP 替换了 read。
对应的系统方法为:
buf = mmap(file,length)
write(socket,buf,length)
所谓的 MMAP, 其实就是系统内存某段空间和用户内存某段空间保持一致,也就是说应用程序能通过访问用户内存访问系统内存。所以,读取数据的时候,不用通过把系统内存的数据拷贝到用户内存中再读取,而是直接从用户内存读出,这样就减少了一次拷贝。
我们还是先看图:


零拷贝之mmap+write.png
给大家简述一下步骤:



  1. 应用进程通过接口调用系统接口 MMAP,并且进程从用户态切换为内核态。

  2. 系统收到 MMAP 的调用后用 DMA 把数据从磁盘拷贝到系统内存,这时是第1次数据拷贝。由于这段数据在系统内存和应用内存是共享的,数据自然就到了应用内存中,这时程序从内核态切换为用户态。

  3. 程序从应用内存得到数据后,会调用 write 系统接口,这时第2次拷贝开始,具体是把数据拷贝到 Socket 缓存,而且用户态切换为内核态。

  4. 系统通过 DMA 把数据从 Socket 缓存拷贝到网卡。

  5. 最后,进程从内核态切换为用户态。


这样做到收益是减少了一次拷贝但是用户态和内核态仍然是4次切换


sendfile


这个系统方法可以实现系统内部不同设备之间的拷贝。具体逻辑我们还是先上图:


零拷贝之sendfile.png
大家可以看到,使用 sendfile 主要的收益是避免了数据在应用内存和系统内存或socket缓存直接的拷贝,同时这样会避免用户态和内核态之间的切换。
基本原理分为下面几步:



  1. 应用进程调用系统接口 sendfile,进程从用户态切换完内核态。

  2. 系统接收到 sendfile 指令后,通过 DMA 从磁盘把数据拷贝到系统内存。

  3. 数据到了系统内存后,CPU 会把数据从系统内存拷贝到 socket 缓存中。

  4. 通过 DMA 拷贝到网卡中。

  5. 最后,进程从内核态切换为用户态。


但是,这还不是零拷贝,所谓的零拷贝不会在内存层面去拷贝数据,也就是系统内存拷贝到 socket 缓存,下面给大家介绍一下真正的零拷贝。


真正的零拷贝


真正的零拷贝是基于 sendfile,当网卡支持 SG-DMA 时,系统内存的数据可以直接拷贝到网卡。如果这样实现的话,执行流程就会更简单,如下图所示:


真正的零拷贝.png


基本原理分为下面几步:



  1. 应用进程调用系统接口 sendfile,进程从用户态切换完内核态。

  2. 系统接收到 sendfile 指令后,通过 DMA 从磁盘把数据拷贝到系统内存。

  3. 数据到了系统内存后,CPU 会把文件描述符和数据长度返回到 socket 缓存中(注意这里没有拷贝数据)。

  4. 通过 SG-DMA 把数据从系统内存拷贝到网卡中。

  5. 最后,进程从内核态切换为用户态。


零拷贝在用户态和内核态之间的切换是2次,拷贝是2次,大大减少了切换次数和拷贝次数,而且全程没有 CPU 参与数据的拷贝。


零拷贝的重要帮手 PageCache


大家知道,从缓存中读取数据的速度一定要比从磁盘中读取数据的速度要快的多。那么,有没有一种技术能让我们从内存中读取磁盘的数据呢?
PageCache 的目的就是让磁盘的数据缓存化到系统内存。那么,PageCache会选取哪些磁盘数据作为缓存呢?具体步骤是这样的:



  • 首先,当用户进程要求方法哪些磁盘数据时,DMA会把这部分磁盘数据缓存到系统内存里,那么问题又来了,磁盘空间明显比内存空间大的多,不可能不限制的把磁盘的数据拷贝到系统内存中,所以必须要有一个淘汰机制来把访问概率低的内存数据删除掉。

  • 那么,用什么方法淘汰内存的数据呢?答案是 LRU (Least Recently Used),最近最少使用。原理的认为最近很少使用的数据,以后使用到的概率也会很低。


那么,这样就够了吗?我们设想一个场景,我们在操作数据的时候有没有顺序读写的场景?比如说消息队列的顺序读取,我们消费了 id 为1的 message 后,也会消费 id 为2的 message。而消息队列的文件一般是顺序存储的,如果我们事先把 id 为2的 message 读出到系统内存中,那么就会大大加快用户进程读取数据的速度。


这样的功能在现代操作系统中叫预读,也就是说如果你读某个文件的了32 KB 的字节,虽然你这次读取的仅仅是 0 ~ 32 KB 的字节,但系统内核会自动把其后面的 32~64 KB 也读取到 PageCache,如果在 32~64 KB 淘汰出 PageCache 前,用户进程读取到它了,那么就会大大加快数据的读取速度,因为我们是直接在缓存上读出的数据,而不是从磁盘中读取。


作者:肖恩Sean
来源:juejin.cn/post/7236988255073370167
收起阅读 »

阅阿里大裁员消息有感

这几天网上流传阿里计划裁员 5000+人,也有说 2.5w 的,数量多少已经不重要了,只看到国内外各大公司纷纷裁员,明显已经跟前几年的风气大不相同,深感软件行业越发进入下行趋势。 这样大规模的“输送”人才,势必会在后期影响到行业里的每个人,至少有以下几个影响:...
继续阅读 »

这几天网上流传阿里计划裁员 5000+人,也有说 2.5w 的,数量多少已经不重要了,只看到国内外各大公司纷纷裁员,明显已经跟前几年的风气大不相同,深感软件行业越发进入下行趋势。


这样大规模的“输送”人才,势必会在后期影响到行业里的每个人,至少有以下几个影响:




  1. 长期在中小企业的打工人更难找到工作


    这点是显而易见的,也是最容易想到的点,相比大厂出身、学历光环的人,普通人在工作竞争中本就难以取得优势,以前还可以靠着业务快速扩张、人才缺口等因素,逆袭大厂或者找到高薪工作,这就是在高速增长中带来的红利。但在行业下行期,阶层固化将会更加严重,跨越阶层的行为会越来越难。这本质上和社会发展是一致的。


    进而可以推论,由于出身好的人更容易找到工作,他本身也更倾向于和自己类似的人,于是即使他在招聘时发现一个能力强的人,如果这个人正好出身平凡,他也更愿意去选择与他经历类似的人。这样的恶性循环会进一步的加剧找到工作的难度。


    而且在这个恶性循环中,能力已经不是主要因素了。能力确实重要,但是想像以前一样“不拘一格降人才”,可能就不太可能了。




  2. 圈子文化未来会在软件行业更加流行


    其实很多传统行业都是有进入壁垒的,你要是不认识其中的人,没有几个小圈子,根本混不下去。只有软件开发,或者说互联网行业,由于相较而言创建时间短,而且国外的风气也比较自由,影响之下我们总体还是比较崇尚单打独斗的,不太讲究向上管理、建立关系网那一套。


    但是现在,你说一个阿里员工找到了新工作,他能不推几个认识的同事?阿里高管跳了一个中小公司,他能不招几个原来的左膀右臂么?


    其实原先也一直有这种情况,但可能是小范围的。但随着大厂员工逐渐向中小企业流动,这种现象预计也会成为普遍现象。


    所以不要抱着我只要不跳槽苟着就影响不到我这种想法,山雨欲来风满楼,仔细观察下身边的现象,就能找到自己的判断。




  3. 不要再乐观的估计工资涨幅


    这个不用说了,已经有很多降薪入职中小公司的新闻了。




这里我看到还有一种观点,说裁员这事也是有好处的,可以让大厂员工带着他的能力和经验到广大中小公司,对整个行业是有提升作用的,长期看是利好。


这种观点比较容易唬人,乍一看逻辑没毛病,实际是放P


首先,软件行业最大的特点,就是开源。这也是吸引很多人(包括我)的一点。你能力强不强只取决于你想不想学,不存在想学但是学不到的情况。


所以普通打工人与大厂员工的能力差距只取决于是否愿意勤快地学习,没有环境不同导致的差距。


再说经验,确实大厂在资源投入上更舍得花钱,让经验的积攒更快了。但本质上,大厂员工所谓带来的经验最多把他原来的那一套东西再开发一套。这里并没有什么创新,何来行业利好?


顶多是让老板们赚钱的速度加快了。更方便赚钱了,利好老板。


甚至可能老板觉得开发的真不错,不需要这么多人了,于是带动一批中小公司裁员潮。


在这个只有普通打工人受伤的世界里,我们要如何应对呢? 我也想了以下几点。



  1. 不要焦虑


最重要的还是放平心态,个人没法影响时代潮流,只能顺应时代潮流。


如果因为焦虑去报班了,那就正中了培训机构下怀,被噶了韭菜。


还是要学习,提高自己总是没错的,可以自己找资料,自己学习。



  1. 多关注生活技能


业余之外可以多关注别的技能,也许还能发现自己的新天赋呢?


比如学学厚黑学,心理学,以免在圈子斗争中被人拿捏。


学学炒股,但不要投太多钱; 学学做饭,至少让自己的胃得到满足。



  1. 尝试做一名独立开发者


其实各大厂裁员,业务收缩未必不是好事,只要我们能找准一些生活中的痛点,找到一些目标人群,做一些开发工作,也是一条好路子,搞得好,也许也能创业。


就这些么多吧,一些小想法,手机码字不易,见谅。


寒冬之下,如果有人愿意组织一些圈子,报团取

作者:FengY_HYY
来源:juejin.cn/post/7237037029569822779
暖,请别忘了拉上我。

收起阅读 »

根据高德地图,画个心给你对象吧

web
文章来源 因为逛掘金的时候,看到了这篇文章,但是文章写的没头没尾,于是我就按照他给的思路自己实现了一下,感觉效果来不错,就在这里分享给大家一下。 高德地图接入 用这个申请的应用的key可以进行sdk的接入。可以通过高德地图的文档API来查询具体的API如何使...
继续阅读 »

文章来源


因为逛掘金的时候,看到了这篇文章,但是文章写的没头没尾,于是我就按照他给的思路自己实现了一下,感觉效果来不错,就在这里分享给大家一下。


image.png


高德地图接入


用这个申请的应用的key可以进行sdk的接入。可以通过高德地图的文档API来查询具体的API如何使用。


本文中主要使用高德地图的4个API



  • 初始化地图

  • 绘制路径(PathSimplifier)

  • 点标记(Marker)

  • 信息窗体(InfoWindow)


具体步骤



  1. 需要初始化地图,首先你要有一个容器去绘制地图,所以你要在html下有一个div容器,id随便起,然后使用new AMap.Map(#id)的方式去初始化地图,初始化的时候还可以带入参数(中心点、zoom)等。


   this.map = new AMap.Map('container', {
center:[x,y],
zoom: 10
})

这里你可以去查询当前你所在位置的坐标作为中心点传入,zoom我的案例里没有使用,因为使用了zoom,初始化的时候会卡一下,不知道具体原因。



  1. 绑定事件,获取心型坐标。给map绑定点击事件,每点击一次打一个标记点,然后你画一个心型,记录路径点。此处需要画左右两条路径哦


 this.map.on('click', (e) => {
const position = [+e.lnglat.getLng(), +e.lnglat.getLat()]
const marker = new AMap.Marker({
position: [+e.lnglat.getLng(), +e.lnglat.getLat()],
})
if (!window.list) {
window.list = [position]
} else {
window.list.push(position)
}
marker.setMap(this.map)
})

image.png
可以通过window.list来查看所有标记点的坐标,记住这些坐标,后面画线的时候要用哦。(tips:可以先画一条线,然后再控制台把window.list清空,再画另一条线。两条线的坐标点数量尽量一致,防止出现先后抵达的问题。)



  1. 下面你已经找好了两条线的坐标,下面你需要画两条线了


const initPath = () => {
AMapUI.load(['ui/misc/PathSimplifier'], (PathSimplifier) => {
if (!PathSimplifier.supportCanvas) {
alert('当前环境不支持 Canvas!')
return
}
//启动页面
this.pathSimplifierIns = new PathSimplifier({
zIndex: 100,
map: this.map, //所属的地图实例
getPath: function (pathData, pathIndex) {
//返回轨迹数据中的节点坐标信息,[AMap.LngLat, AMap.LngLat...] 或者 [[lng|number,lat|number],...]
return pathData.path
},
})
this.pathSimplifierIns.setData([
{
name: '轨迹1',
path: this.path1,
},
{
name: '轨迹2',
path: this.path2,
},
])
var navg0 = this.pathSimplifierIns.createPathNavigator(
0, //关联第1条轨迹
{
loop: false, //循环播放
speed: 800,
pathNavigatorStyle: {
width: 40,
height: 40,
autoRotate: false, // 禁止调整方向
// 经过路径的样式
pathLinePassedStyle: {
lineWidth: 6,
strokeStyle: 'black',
dirArrowStyle: {
stepSpace: 15,
strokeStyle: 'red',
},
},
//设置头像 不需要可以删除
content: PathSimplifier.Render.Canvas.getImageContent(
aJpg,
onload,
onerror,
),
},
},
)
var navg1 = this.pathSimplifierIns.createPathNavigator(
1, //关联第1条轨迹
{
loop: false, //循环播放
speed: 800,
pathNavigatorStyle: {
width: 40,
height: 40,
autoRotate: false, // 禁止调整方向
// 经过路径的样式
pathLinePassedStyle: {
lineWidth: 6,
strokeStyle: 'blue',
dirArrowStyle: {
stepSpace: 15,
strokeStyle: 'red',
},
},
//设置头像 不需要可以删除
content: PathSimplifier.Render.Canvas.getImageContent(
bJpg,
onload,
onerror,
),
},
},
)
// 设置定时器,方式map加载卡顿时,动画先开始
setTimeout(() => {
navg0.start()
navg1.start()
// 设置途径路上的 说的话
navg1.on('move', (e) => {
const idx = navg1.getCursor().idx // 走到了第几个点
const list = [
'不开门一直敲',
'是一种打扰',
'不回复本身',
'就是一种回复',
'双向奔赴才有意义',
]
let text = ''
if(idx < 3) {
text = list[0]
} else if(idx < 8) {
text = list[1]
} else if(idx < 13) {
text = list[2]
} else if(idx < 17) {
text = list[3]
} else {
text = list[4]
}
const cont = `<div class="toptit">
<p>${text}</p>
</div>`


// 设置气泡
this.infoWindow.setContent(cont)
this.infoWindow.open(this.map, e.target.getPosition())
})
}, 3000)
this.pathSimplifierIns.renderLater()
})
}


  1. 上面代码把信息窗体漏了,信息窗体也需要初始化,在初始化地图的后就行


  mounted() {
this.map = new AMap.Map('container',)
this.infoWindow = new AMap.InfoWindow({
offset: new AMap.Pixel(0, 0),
})
}

成品展示



这个demo里面,没有设置头像和要说的话,因为头像icon无法放上去,后续需要的画 可以自己添加哦。


结语


520已经过去了,这个就等着七夕给你们对象制造点浪漫吧。。


作者:哈库拉马塔塔
来源:juejin.cn/post/7236593783843913787
收起阅读 »

聊聊「短信」渠道的设计与实现

有多久,没有发过短信了? 一、背景简介 在常规的分布式架构下,「消息中心」的服务里通常会集成「短信」的渠道,作为信息触达的重要手段,其他常用的手段还包括:「某微」、「某钉」、「邮件」等方式; 对于《消息中心》的设计和实现来说,在前面已经详细的总结过,本文重点...
继续阅读 »

有多久,没有发过短信了?



一、背景简介


在常规的分布式架构下,「消息中心」的服务里通常会集成「短信」的渠道,作为信息触达的重要手段,其他常用的手段还包括:「某微」、「某钉」、「邮件」等方式;


对于《消息中心》的设计和实现来说,在前面已经详细的总结过,本文重点来聊聊消息中心的短信渠道的方式;



短信在实现的逻辑上,也遵循消息中心的基础设计,即消息生产之后,通过消息中心进行投递和消费,属于典型的生产消费模型;


二、渠道方对接


在大部分的系统中,短信功能的实现都依赖第三方的短信推送,之前总结过《三方对接》的经验,这里不再赘述;


但是与常规第三方对接不同的是,短信的渠道通常会对接多个,从而应对各种消息投递的场景,比如常见的「验证码」场景,「通知提醒」场景,「营销推广」场景;



这里需要考虑的核心因素有好几个,比如成本问题,短信平台的稳定性,时效性,触达率,并发能力,需要进行不同场景的综合考量;


验证码:该场景通常是用户和产品的关键交互环节,十分依赖短信的时效性和稳定性,如果出问题直接影响用户体验;


通知提醒:该场景同样与业务联系密切,但是相对来说对短信触达的时效性依赖并不高,只要在一定的时间范围内最终触达用户即可;


营销推广:该场景的数据量比较大,并且从实际效果来看,具有很大的不确定性,会对短信渠道的成本和并发能力重点考量;


三、短信渠道


1、流程设计


从整体上来看短信的实现流程,可以分为三段:「1」短信需求的业务场景,「2」消息中心的短信集成能力,「3」对接的第三方短信渠道;



需求场景:在产品体系中,需要用到短信的场景很多,不过最主要的还是对用户方的信息触达,比如身份验证,通知,营销等,其次则是对内的重要消息通知;


消息中心:提供消息发送的统一接口方法,不同业务场景下的消息提交到消息中心,进行统一维护管理,并根据消息的来源和去向,适配相应的推送逻辑,短信只是作为其中的一种方式;


渠道对接:根据具体的需求场景来定,如果只有验证码的对接需求,可以只集成一个渠道,或者从成本方面统筹考虑,对接多个第三方短信渠道,建议设计时考虑一定的可扩展;


2、核心逻辑


单从短信这种方式的管理来看,逻辑复杂度并不算很高,但是很依赖细节的处理,很多不注意的细微点都可能导致推送失败的情况;



实际在整个逻辑中,除了「验证码」功能有时效性依赖之外,其他场景的短信触达都可以选择「MQ队列」进行解耦,在消息中心的设计上,也具备很高的流程复用性,图中只是重点描述短信场景;


3、使用场景


3.1 验证码


对于「短信」功能中的「验证码」场景来说,个人感觉在常规的应用中是最复杂的,这可能会涉及到「账户」和相关「业务」的集成问题;


验证码获取


这个流程相对来说路径还比较简短,只要完成手机号的校验后,按照短信推送逻辑正常执行即可;



这里需要说明的是,为了确保系统的安全性,通常会设定验证码的时效性,并且只能使用一次,但是偶尔可能因为延时问题,引起用户多次申请验证码,基于缓存可以很好的管理这种场景的数据结构;


验证码消费


验证码的使用是非常简单的,现在很多产品在设计上,都弱化了登录和注册的概念,只要通过验证码机制,会默认的新建帐户和执行相关业务流程;



无论是何种业务场景下的「验证码」依赖,在处理流程时都要先校验其「验证码」的正确与否,才能判断流程是否向下执行,在部分敏感的场景中,还会限制验证码的错误次数,防止出现账户安全问题;


3.2 短信触达


无论是「通知提醒」还是「营销推广」,其本质上是追求信息的最终触达即可,大部分短信运营商都可以提供这种能力,只是系统内部的处理方式有很大差异;



在部分业务流程中,需要向用户投递短信消息,在营销推广的需求中,更多的是批量发送短信,部分需求其内部逻辑上,还可能存在一个转化率统计的问题,需要监控相关短信的交互状态;


四、模型设计


由于短信是集成在消息中心的服务中,其相关的数据结构模型都是复用消息管理的,具体细节描述,参考《消息中心》的内容即可,此处不赘述;



从技术角度来看的话,涉及经典的生产消费模型,第三方平台对接,任务和状态机管理等,消息中心作为分布式架构的基础服务,在设计上还要考虑一定的复用性。


五、参考源码


编程文档:
https://gitee.com/cicadasmile/butte-java-note

应用仓库:
https:/
/gitee.com/cicadasmile/butte-flyer-parent

作者:知了一笑
来源:juejin.cn/post/7237082256480649271
收起阅读 »

为什么有些蛮厉害的人,后来都不咋样了

前言 写这篇文章目的是之前在一篇文章中谈到,我实习那会有个老哥很牛皮,业务能力嘎嘎厉害,但是后面发展一般般,这引起我的思考,最近有个同事发了篇腾讯pcg的同学关于review 相关的文章,里面也谈到架构师的层次,也再次引起我关于架构师的相关思考,接下来我们展...
继续阅读 »

前言




写这篇文章目的是之前在一篇文章中谈到,我实习那会有个老哥很牛皮,业务能力嘎嘎厉害,但是后面发展一般般,这引起我的思考,最近有个同事发了篇腾讯pcg的同学关于review 相关的文章,里面也谈到架构师的层次,也再次引起我关于架构师的相关思考,接下来我们展开聊聊吧~


摆正初心




我写这篇文章,初心是为了找到导致这样结果的原因,而不是站在一个高高在上的位置,对别人指手画脚,彰显自己多牛皮。(PS:我也鄙视通过打压别人来展示自己,你几斤几两,大家都是聪明人看得出来,如果你确实优秀,别人还打压,说明他急了,哈哈哈)


查理芒格说过一句话:如果我知道在哪里会踩坑,避开这些,我已经比很多人走得更远了。


思考结果




我觉得是没有一个层级的概念导致的,这个原因筛掉了大部分人,突破层级的难度筛掉了另外一批人,运气和机会又筛掉另一波人。


没有层级概念


为什么这么讲呢?


我们打游戏的时候,比如说王者,会有废铁、青铜、钻石、铂金、荣耀、荣耀王者,对吧。它的层级大家都清楚,但是在现实生活中,你会闷逼了,我当前处在那个阶段,上一层是什么水平,需要什么技能,什么样的要求。


其次也会对自己能力过高的评价,如果你一直在组里面,你可能一直是一把手,到了集团,可能变成10名内,到了公司排名,可能几百名后。我们需要站在一个更高、更全面的角度去了解自己的位置。


出现这种情况也很正常


举个栗子,以前我实习那会,有个老哥业务能力特别强,啥活都干得快,嘎嘎牛皮,这是个背景


如果团队里头你最厉害了,那你的突破点,你的成长点在哪里?


对吧,大家都比你菜了,自然你能从别人身上学习到的就少了,还有一种情况是你觉得你是最厉害的,这种想法也是最要命的,会让你踏步不前。这时的解法,我认为是自驱力,如果你学哲学,就知道向内求,自我检讨,自己迭代更新,别人就是你,你就是别人,别人只是一面镜子。


层级的概念


那时看到他搞业务特别厉害,但现在看是做需求厉害,但是缺乏深度。我对比以前的开发经历,跟现在在架构组的工作经历,感受很明显。一个是为了完成任务,一个需要深度,什么深度呢?这个埋下伏笔,为后面架构师层级再展开聊聊。


从初级到中级,到高级,再到主程、再到TL,技术经理,再到架构师,再到负责人。当完成任务的时候,是最基本的事情,深入的时候,从coding入手,在代码上有所追求,比如说可读性,用用设计模式,再深入想到代码可扩展性。。。



当你了解下一个层级的要求的时候,有了目标才能有效的突破它。



突破层级的难度


这是在上一个原因基础上一个加强版,你了解了各个层级的要求,但是突破这些要求,可能由于阅历,或者能力,或者天赋不足,导致突破困难。


image.png


这里我想聊聊架构师的思考,之前在转正答辩上,一个领导问我你怎么理解架构的,我当时没有概念,但是接触相关工作以及观看相关文章,有了更深理解。



这里讲的是coding部分,属于架构师负责的一部分,规范


我不禁想想平时什么工作内容涉及到这个?


比如说契约,规定了依赖jar版本;定义了协议,什么类型输出的格式,转换的类型;开发的规范,设计文档的样式;像文中review的过程,确实比较少,目的是为了减少代码的坏味道。就像文中讲到,如果你定义的一个规范,可以在300+人里面hold,让系统一直在正常迭代,那么算合格的架构师。


一次广义上review


我一般下班会遇到基础服务的小伙伴聊聊天,我说话很少,就喜欢听听别人聊点什么,他跟我聊了几天,我发现问题是现有商品代码已经不足以支持业务的快速迭代,因为冗余其他东西太多了。比如说一个毛胚商品,然后它也快速的加上其他属性,变成一个加工品。但是现在场景变成了它就是一个加工品,你想拆成其他加工品,很困难,就是字段冗余到商品表里头了。


这个时候到架构已经不适合业务快速迭代了,需要重构,大破大立,还需要大佬牵头。review狭义上是代码层发现问题,如果你从一线同学那里听到的东西,能发现问题,也是一种review。



架构师不止规范,需要深度



需要什么深度呢?


从一个做需求的点看,从需求理解,这个是业务深度,从设计文档上,严谨程度,扩展性、风险点、可行性,设计深度。从开发阶段,coding,技术规范,技术功底,这个是技术深度


跳出需求的点,从大的面来看,需求为了解决什么问题,不做行不行,业务价值在哪里?做了这一期还有后续吗,这是业务的前景。然后规划是怎样的,先从哪里入手,然后有木有计划去推进?这是思考的深度


抽象的能力



里面反复提到抽象的能力,比如说逻辑、物理架构图,这个有助于你理解整个系统的调用关系,形成闭环,可以从全局的角度去理解,我当前做的需求在什么位置,为了解决什么问题。


再到通过问题看到本质,从技术方案看到实质。有一次一位同学跟我探讨DDD,跟我说防腐层不应该是这样写的,你只是用了策略模式去写,应该有个一个门面,然后后面是实现逻辑。我听到这里就明白他被绕进去了,DDD是一个思想,他幻化出来一些对应的框架,它的精髓是高内聚、低耦合。你说策略模式,能否将外部rpc调用分隔开呢?当然可以,它算不算防腐层呢?也算~


最近一次做代码优化的时候,我用了责任链的设计模式,将190行的代码,拆分成4个模块,每个类大概30行,当然190行包括换行。但是实际效果除了行数变少外,每个模块分工特别清晰,这个模块在处理特定的逻辑,当某部分有问题的时候,直接找到那个模块修改即可。(这就是高内聚的魅力)


抽象另一种体现:模块化


最近在牵头做账单,其实我也没做过,我就找了几篇大厂的文章看看,拿来吧你,哈哈


image.png


分为几个步骤,下载账单,解析账单,对账,差异处理(平账)。是不是瞬间有了几个模块,文件模块,包括上传、下载,解析文件对吧。然后是账单模块,可能会分成订单,还有一些退款的,然后是对账处理结果,属于对账模块,文件解析出来的东西跟账单对比,哪些是对的上的,哪些又是异常的,这个模块还有后续的处理结果,自动平账,或者人工处理。



模块化也是高内聚的体现,这就是DDD的思想,只不过人家现在有名头而已~



运气


这个就不展开了,有点玄学,也看投胎,也看老天赏不赏饭吃。我觉得嘛,不管有没有有运气,都要不卑不亢,努力提升自己,很多结果我们决定不了的,但是过程我们可以说了算,人生不也是这样嘛,那就好好享受过程吧~


image.png


最后




《矛盾论》,还是里面的观点,我们需要全面的认识自己的定位,找到自己的优势,不断突破自我。有些厉害,只是暂时性的,而长远来看,只是冰山一角。


作者:大鸡腿同学
来源:juejin.cn/post/7133246541623459847
收起阅读 »

你在公司混的差,可能和组织架构有关!

如果你接触过公司的面试工作,一定见过很多来自大公司的渣渣。这些人的薪资和职位,比你高出很多,但能力却非常一般。 如果能力属实,我们大可直接把这些大公司的员工打包接收,也免了乱七八糟的面试工作。但可惜的是,水货的概率通常都比较大,新的公司也并不相信他们的能力。尤...
继续阅读 »

如果你接触过公司的面试工作,一定见过很多来自大公司的渣渣。这些人的薪资和职位,比你高出很多,但能力却非常一般。


如果能力属实,我们大可直接把这些大公司的员工打包接收,也免了乱七八糟的面试工作。但可惜的是,水货的概率通常都比较大,新的公司也并不相信他们的能力。尤其是这两年互联网炸了锅,猪飞的日子不再,这种情况就更加多了起来。


反过来说也一样成立,就像是xjjdog在青岛混了这么多年,一旦再杀回北上广,也一样是落的下乘的评价。


除了自身的努力之外,你在上家公司混的差,还与你在组织架构中所处于的位置和组织架构本身有关。


一般公司会有两种组织架构方式:垂直化划分层级化划分


1. 垂直划分


垂直划分,多以业务线为模型进行划分。各条业务线共用公司行政资源,相互之间关联不大。


各业务线之间,内部拥有自治权。


image.png


如上图所示,公司共有四个业务线。




  • 业务线A,有前端和后端开发。因为成员能力比较强,所以没有测试运维等职位;




  • 业务线B倡导全栈技能,开发后台前端一体化;




  • 业务线C的管理能力比较强,仅靠少量自有研发,加上大量的外包,能够完成一次性工作。




  • 业务线D是传统的互联网方式,专人专岗,缺什么招什么,不提倡内部转岗




运行模式




  1. 业务线A缺人,缺项目,与业务线BCD无任何关系,不允许借调




  2. 业务线发展良好,会扩大规模;其他业务线同学想要加入需要经过复杂的流程,相当于重新找工作




  3. 业务线发展萎靡,会缩减人员,甚至会整体砍掉。优秀者会被打散吸收进其他业务线




好处




  1. 业务线之间存在竞争关系,团队成员有明确的奋斗目标和危机意识




  2. 一条业务线管理和产品上的失败,不会影响公司整体运营




  3. 可以比较容易的形成单向汇报的结构,避免成本巨大且有偏差的多重管理




  4. 便于复制成功的业务线,或者找准公司的发展重点




坏处




  1. 对业务线主要分管领导的要求非常高




  2. 多项技术和产品重复建设,容易造成人员膨胀,成本浪费




  3. 部门之间隔阂加大,共建、合作困难,与产品化相逆




  4. 业务线容易过度自治,脱离掌控




  5. 太激进,大量过渡事宜需要处理




修订


为了解决上面存在的问题,通常会有一个协调和监管部门,每个业务线,还需要有响应的协调人进行对接。以以往的观察来看,效果并不会太好。因为这样的协调,多陷于人情沟通,不好设计流程规范约束这些参与人的行为。


image.png


在公司未摸清发展方向之前,并不推荐此方式的改革。它的本意是通过竞争增加部门的进取心,通过充分授权和自治发挥骨干领导者的作用。但在未有成功案例之前,它的结果变成了:寄希望于拆分成多个小业务线,来解决原大业务线存在的问题。所以依然是处于不太确定的尝试行为。


2. 水平划分


水平划分方式,适合公司有确定的产品,并能够形成持续迭代的团队。


它的主要思想,是要打破“不会做饭的项目经理不是好程序员”的思维,形成专人专业专岗的制度。


这种方式经历了非常多的互联网公司实践,可以说是最节约研发成本,能动性最高的组织方式。主要是因为:




  • 研发各司其职,做好自己的本职工作可以避免任务切换、沟通成本,达到整体最优




  • 个人单向汇报,组织层级化,小组扁平化。“替领导负责,就是替公司负责”




  • 任何职位有明确的JD,可替换性高,包括小组领导




这种方式最大的问题就是,对团队成员的要求都很高。主动性与专业技能都有要求,需要经过严格的面试筛选。


坏处




  • 是否适合项目类公司,存疑




  • 存在较多技术保障部门,公共需求 下沉容易造成任务积压




  • 需要对其他部门进行整合,才能发挥更大的价值




分析


image.png


如上图,大体会分为三层。




  • 技术保障,保障公司的底层技术支撑,问题处理和疑难问题解决。小组多但人少,职责分明




  • 基础业务,公司的旗舰业务团队,需求变更小但任何改动都非常困难。团队人数适中




  • 项目演化,纯项目,可以是一锤子买卖,也可以是服务升级,属于朝令夕改类需求的聚居地。人数最多




可以看到项目演化层,多是脏活,有些甚至是尝试性的项目-----这是合理的。




  1. 技术保障和基础业务的技术能力要求高,业务稳定,适合长期在公司发展,发展属性偏技术的人群,流动性小,招聘困难




  2. 项目演化层,业务多变,项目奖金或者其他回报波动大,人员流动性高,招聘容易




成功的孵化项目,会蜕变成产品,或者基础业务,并入基础业务分组。


从这种划分可以看出,一个人在公司的命运和发展,在招聘入职的时候就已经确定了。应聘人员可以根据公司的需求进行判断,提前预知自己的倾向。


互联网公司大多数将项目演化层的人员当作炮灰,因为他们招聘容易,团队组件迅速,但也有很多可能获得高额回报,这也是很多人看中的。


3.组合


组合一下垂直划分和层级划分,可以是下面这种效果。


image.png


采用层级+垂直方式进行架构。即:首选层级模式,然后在项目演化层采用垂直模式,也叫做业务线,拥有有限的自治权。


为每一个业务线配备一个与下层产品化或者技术保障对接的人员。


绩效方面,上层的需求为下层的实现打分。基础业务和技术保障,为绿色的协调人员打分。他们的利益是一致的。


End


大公司出来的并不一定是精英,小公司出来的也并不一定是渣渣。这取决于他在公司的位置和所从事的内容。核心部门会得到更多的利益,而边缘的尝试性部门只能吃一些残羹剩饭。退去公司的光环,加上平庸的项目经历,竞争力自然就打上一个折扣。


以上,仅限IT行业哦。赵家人不在此列。


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

程序员能有什么好玩意?

业10年了,看看一枚老成员都有什么好玩意(有个人的、同事的、公司的……)。【多图预警!!!摸鱼预警!!!】 桌面预警 桌面上放了个二层小架子,之前还有个盆栽的,可惜死掉了,悼念&缅怀+1。 喷雾预警 好几年前的习惯,之前是理肤泉的喷雾。当年的我还是很...
继续阅读 »

业10年了,看看一枚老成员都有什么好玩意(有个人的、同事的、公司的……)。【多图预警!!!摸鱼预警!!!


桌面预警


桌面上放了个二层小架子,之前还有个盆栽的,可惜死掉了,悼念&缅怀+1。


image.png


喷雾预警


好几年前的习惯,之前是理肤泉的喷雾。当年的我还是很暴躁的,需要一点水分帮我降降温,不过,当编程没有啥思路的时候,喷一喷感觉还不错。


image.png


养生预警


西洋参


有个同事是吉林的,某一天送给我一个山货大礼包,其中就有这瓶西洋参参片。偶尔会取几片泡水,当然喝茶的时候更多一些。【咖啡基本是戒了】


image.png


手串


年前,我领导说想弄个串儿盘着,防止老年痴呆。


我就买了些散珠自己串了些串,团队内,每人分了一串儿。


自己也留了些手串,每天选一串佩戴,主要是绕指柔的玩法。


image.png


image.png


image.png


茶事


喝茶也又些年头了,喝过好喝的,也扔过不好喝的。最近主要喝云南大白,家里的夫人也比较喜欢,


香道


疫情的风刮过来,听说艾草的盘香可以消毒杀菌,就买了盘香,还有个小香炉。周末在家会点一点,其实没那么好闻,但是仪式感满满的。


手霜


大概是东北恶劣的天气原因,办公室的手霜还是不少的,擦一擦,编码也有了仪式感。


盆栽


公司之前定了好多盆栽,我也选了一盆(其实是产品同学的,我的那盆已经养死了)。


image.png


打印机


家里买了台打印机,主要是打印一些孩子的东西,比如涂鸦的模版、还有孩子的照片。


image.png


工作预警


笔记本


大多用的是Mac,大概也不会换回Windows了。


image.png


耳机


还是用的有线耳机,没赶上潮流。哈哈


image.png


键盘


依然没赶上机械键盘的潮流,用的妙控……


面对疾风吧!


之前客户送的,小摆件。


image.png


证书


证书不少,主要是毕业时候发的,哈哈哈。



  1. 前年,公司组织学习了PMP,完美拿到了毕业后的第一个证书。

  2. 公司组织的活动的证书OR奖杯(干瞪眼大赛、乒乓球大赛、羽毛球大赛等),最贵的奖品应该是之前IDEA PK大赛获得的iwatch。

  3. 年会时发的证书。作为优秀的摸鱼份子,每年收到的表彰并不少,大多是个人的表彰,还有就是团队的证书,当然我更关心证书下面的奖金。

  4. 社区的证书。大致是技术社区的证书,嗯嗯,掘金的就一个,某年的2月优秀创作者,应该是这个。


家里的办公桌


夫人是个文艺女青年,喜欢装点我们的家,家里的办公桌的氛围还是很OK的。当然工作之余,也喜欢和夫人喝点小酒,我喜欢冰白,同好可以探讨哈。


image.png


悲伤的事情


疫情


疫情对我们的生活影响还是比较大的,特别是对我一个大龄程序员而言。


未来


今年打算给家庭计划一些副业,有余力的情况下,能够增加一些收入。人生已经过去了半数,感悟到生命的可贵,感情的来之不易,愿我们身边的人都越来越幸福。


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

这一次,我还是想选择自由

辞职回老家有一周多了。 这几天我啥也没写,一直在考虑是继续找工作还是真正开始自由职业。 找下份工作肯定是有高和稳定的收入,但很有可能还是做不喜欢做的事情,可能是和之前一样一成不变的状态,每天准时上下班通勤,下班在一线大城市的出租屋里打游戏看小说,偶尔写点技术...
继续阅读 »

辞职回老家有一周多了。


这几天我啥也没写,一直在考虑是继续找工作还是真正开始自由职业。



找下份工作肯定是有高和稳定的收入,但很有可能还是做不喜欢做的事情,可能是和之前一样一成不变的状态,每天准时上下班通勤,下班在一线大城市的出租屋里打游戏看小说,偶尔写点技术文章。


自由职业的话收入不稳定,赚多赚少都要靠自己。但可以住在小县城的家里,有妈妈做的好吃的菜,有可爱粘人的猫猫,有我新买的投影仪可以和妈妈一起看电视,可以和美好的一切在一起。我不喜欢旅游之类的,宅在家里就已经是我最幸福的状态了。




而且具体做啥可以自己来决定,我有挺多想研究的东西的。


这两天也在面试了,还是那些八股文,卷来卷去的,没啥意思。可能如果真的去了字节,我会更不适应。要不还是不继续面了。


我去年也自由职业过,现在和那时候的区别是我粉丝更多了,技术积累也更多了,而且给我妈新买了个房子,可以在这里继续我的神光实验室。



上图是神光实验室 1.0,之前在老家附近租的一个出租屋。


神光实验室 2.0 是这样的,在新家里:




上次结束自由职业是因为我爸的要求,他说还是希望我有个正当工作。


现在我爸没了,没有人会阻止我了。


我没有负债,还有一定的积蓄,而且我现在啥也不干也有能养活自己的收入。




要不就再任性一次,在家里继续自己的技术梦想,继续搞神光实验室?🤔


就这么愉快的决定了!


这一次,我还是想遵循自己的内心,选择自由,选择和喜欢的一切在一起。


以后公众号会保持日更,其余时间写小册和准备出版的书。


努力一点的话,各方面应该还是可以的。



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

在国企做程序员怎么样?

有读者咨询我,在国企做开发怎么样? 当然是有利有弊,国企相对稳定,加班总体没有互联网多,不过相对而言,工资收入没有互联网高,而且国企追求稳定,往往技术栈比较保守,很难接触新的技术,导致技术水平进步缓慢。 下面分享一位国企程序员的经历,希望能给大家一些参考价值。...
继续阅读 »

有读者咨询我,在国企做开发怎么样?


当然是有利有弊,国企相对稳定,加班总体没有互联网多,不过相对而言,工资收入没有互联网高,而且国企追求稳定,往往技术栈比较保守,很难接触新的技术,导致技术水平进步缓慢。


下面分享一位国企程序员的经历,希望能给大家一些参考价值。



下文中的“我”代表故事主人公



我校招加入了某垄断央企,在里面从事研发工程师的工作。下面我将分享一些入职后的一些心得体会。


在国企中,开发是最底层最苦B的存在,在互联网可能程序员还能够和产品经理argue,但是在国企中,基本都是领导拍脑袋的决定,即便这个需求不合理,或者会造成很多问题等等,你所需要的就是去执行,然后完成领导的任务。下面我会分享一些国企开发日常。


1、大量内部项目


在入职前几个月,我们都要基于一种国产编辑器培训,说白了集团的领导看市场上有eclipse,idea这样编译器,然后就说咱们内部也要搞一个国产的编译器,所有的项目都要强制基于这样一个编译器。


在国企里搞开发,通常会在项目中塞入一大堆其他项目插件,本来一个可能基于eclipse轻松搞定的事情,在国企需要经过2、3个项目跳转。但国企的项目本来就是领导导向,只需给领导演示即可,并不具备实用性。所以在一个项目集成多个项目后,可以被称为X山。你集成的其他项目会突然出一些非常奇怪的错误,从而导致自己项目报错。但是这也没有办法,在国企中搞开发,有些项目或者插件是被要求必须使用的。


2、外包


说到开发,在国企必然是离不开外包的。在我这个公司,可以分为直聘+劳务派遣两种用工形式,劳务派遣就是我们通常所说的外包,直聘就是通过校招进来的校招生。


直聘的优势在于会有公司的统一编制,可以在系统内部调动。当然这个调动是只存在于规定中,99.9%的普通员工是不会调动。劳务派遣通常是社招进来的或者外包。在我们公司中,项目干活的主力都是外包。我可能因为自身本来就比较喜欢技术,并且觉得总要干几年技术才能对项目会有比较深入的理解,所以主动要求干活,也就是和外包一起干活。一开始我认为外包可能学历都比较低或者都不行,但是在实际干活中,某些外包的技术执行力是很强的,大多数项目的实际控制权在外包上,我们负责管理给钱,也许对项目的了解的深度和颗粒度上不如外包。


上次我空闲时间与一个快40岁的外包聊天,才发现他之前在腾讯、京东等互联网公司都有工作过,架构设计方面都特别有经验。然后我问他为什么离开互联网公司,他就说身体受不了。所以身体如果不是特别好的话,国企也是一个不错的选择。


3、技术栈


在日常开发中,国企的技术一般不会特别新。我目前接触的技术,前端是JSP,后端是Springboot那一套。开发的过程一般不会涉及到多线程,高并发等技术。基本上都是些表的设计和增删改查。如果个人对技术没啥追求,可能一天的活2,3小时就干完了。如果你对技术有追求,可以在剩余时间去折腾新技术,自由度比较高。


所以在国企,作为普通基层员工,一般会有许多属于自己的时间,你可以用这些时间去刷手机,当然也可以去用这些时间去复盘,去学习新技术。在社会中,总有一种声音说在国企呆久了就待废了,很多时候并不是在国企待废了,而是自己让自己待废了。


4、升职空间


每个研发类央企都有自己的职级序列,一般分为技术和管理两种序列。


首先,管理序列你就不用想了,那是留给有关系+有能力的人的。其实,个人觉得在国企有关系也是一种有能力的表现,你的关系能够给公司解决问题那也行。


其次,技术序列大多数情况也是根据你的工龄长短和PPT能力。毕竟,国企研发大多数干的活不是研发与这个系统的接口,就是给某个成熟互联网产品套个壳。技术深度基本上就是一个大专生去培训机构培训3个月的结果。你想要往上走,那就要学会去PPT,学会锻炼自己的表达能力,学会如何讲到领导想听到的那个点。既然来了国企,就不要再想钻研技术了,除非你想跳槽互联网。


最后,在国企底层随着工龄增长工资增长(不当领导)还是比较容易的。但是,如果你想当领导,那还是天时地利人和缺一不可。


5、钱


在前面说到,我们公司属于成本单位,到工资这一块就体现为钱是总部发的。工资构成分由工资+年终奖+福利组成。


1.工资构成中没有绩效,没有绩效,没有绩效,重要的事情说三遍。工资是按照你的级别+职称来决定的,公司会有严格的等级晋升制度。但是基本可以概括为混年限。年限到了,你的级别就上去了,年限没到,你天天加班,与工资没有一毛钱关系。


2.年终奖,是总部给公司一个大的总包,然后大领导根据实际情况对不同部门分配,部门领导再根据每个人的工作情况将奖金分配到个人。所以,你干不干活,活干得好不好只和你的年终奖相关。据我了解一个部门内部员工的年终奖并不会相差太多。


3.最后就是福利了,以我们公司为例,大致可以分为通信补助+房补+饭补+一些七七八八的东西,大多数国企都是这样模式。


总结


1、老生常谈了。在国企,工资待遇可以保证你在一线城市吃吃喝喝和基本的生活需要没问题,当然房子是不用想的了。


2、国企搞开发,技术不会特别新,很多时候是项目管理的角色。工作内容基本体现为领导的决定。


3、国企研究技术没有意义,想当领导,就多学习做PPT和领导搞好关系。或者当一个平庸的人,混吃等死,把时间留给家人,也不乏是一种好选择。


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

面试题:Android 中 Intent 采用了什么设计模式?

答案是采用了原型模式。 原型模式的好处在于方便地拷贝某个实例的属性进行使用、又不会对原实例造成影响,其逻辑在于对 Cloneable 接口的实现。 话不多说看下 Intent 的关键源码: // frameworks/base/core/java/a...
继续阅读 »

答案是采用了原型模式


原型模式的好处在于方便地拷贝某个实例的属性进行使用、又不会对原实例造成影响,其逻辑在于对 Cloneable 接口的实现。


话不多说看下 Intent 的关键源码:

 // frameworks/base/core/java/android/content/Intent.java
 public class Intent implements Parcelable, Cloneable {
    ...
     private static final int COPY_MODE_ALL = 0;
     private static final int COPY_MODE_FILTER = 1;
     private static final int COPY_MODE_HISTORY = 2;
 
     @Override
     public Object clone() {
         return new Intent(this);
    }
 
     public Intent(Intent o) {
         this(o, COPY_MODE_ALL);
    }
 
     private Intent(Intent o, @CopyMode int copyMode) {
         this.mAction = o.mAction;
         this.mData = o.mData;
         this.mType = o.mType;
         this.mIdentifier = o.mIdentifier;
         this.mPackage = o.mPackage;
         this.mComponent = o.mComponent;
         this.mOriginalIntent = o.mOriginalIntent;
        ...
 
         if (copyMode != COPY_MODE_FILTER) {
            ...
             if (copyMode != COPY_MODE_HISTORY) {
                ...
            }
        }
    }
    ...
 }

可以看到 Intent 实现的 clone() 逻辑是直接调用了 new 并传入了自身实例,而非调用 super.clone() 进行拷贝。


默认的拷贝策略是 COPY_MODE_ALL,顾名思义,将完整拷贝源实例的所有属性进行构造。其他的拷贝策略是 COPY_MODE_FILTER 指的是只拷贝跟 Intent-filter 相关的属性,即用来判断启动目标组件的 actiondatatypecomponentcategory 等必备信息。无视启动 flagbundle 等数据。

 // frameworks/base/core/java/android/content/Intent.java
 public class Intent implements Parcelable, Cloneable {
    ...
     public @NonNull Intent cloneFilter() {
         return new Intent(this, COPY_MODE_FILTER);
    }
 
     private Intent(Intent o, @CopyMode int copyMode) {
         this.mAction = o.mAction;
        ...
 
         if (copyMode != COPY_MODE_FILTER) {
             this.mFlags = o.mFlags;
             this.mContentUserHint = o.mContentUserHint;
             this.mLaunchToken = o.mLaunchToken;
            ...
        }
    }
 }

还有中拷贝策略是 COPY_MODE_HISTORY,不需要 bundle 等历史数据,保留 action 等基本信息和启动 flag 等数据。

 // frameworks/base/core/java/android/content/Intent.java
 public class Intent implements Parcelable, Cloneable {
    ...
     public Intent maybeStripForHistory() {
         if (!canStripForHistory()) {
             return this;
        }
         return new Intent(this, COPY_MODE_HISTORY);
    }
 
     private Intent(Intent o, @CopyMode int copyMode) {
         this.mAction = o.mAction;
        ...
 
         if (copyMode != COPY_MODE_FILTER) {
            ...
             if (copyMode != COPY_MODE_HISTORY) {
                 if (o.mExtras != null) {
                     this.mExtras = new Bundle(o.mExtras);
                }
                 if (o.mClipData != null) {
                     this.mClipData = new ClipData(o.mClipData);
                }
            } else {
                 if (o.mExtras != null && !o.mExtras.isDefinitelyEmpty()) {
                     this.mExtras = Bundle.STRIPPED;
                }
            }
        }
    }
 }

总结起来:































Copy Modeaction 等数据flags 等数据bundle 等历史
COPY_MODE_ALLYESYESYES
COPY_MODE_FILTERYESNONO
COPY_MODE_HISTORYYESYESNO

除了 Intent,Android 源码中还有很多地方采用了原型模式。




  • Bundle 也实现了 clone(),提供了 new Bundle(this) 的处理:

     public final class Bundle extends BaseBundle implements Cloneable, Parcelable {
        ...
         @Override
         public Object clone() {
             return new Bundle(this);
        }
     }



  • 组件信息类 ComponentName 也在 clone() 中提供了类似的实现:

     public final class ComponentName implements Parcelable, Cloneable, Comparable<ComponentName> {
        ...
         public ComponentName clone() {
             return new ComponentName(mPackage, mClass);
        }
     }



  • 工具类 IntArray 亦是如此:

     public class IntArray implements Cloneable {
        ...
         @Override
         public IntArray clone() {
             return new IntArray(mValues.clone(), mSize);
        }
     }



原型模式也不一定非得实现 Cloneable,提供了类似的实现即可。比如:




  • Bitmap 没有实现该接口但提供了 copy(),内部将传递原始 Bitmap 在 native 中的对象指针并伴随目标配置进行新实例的创建:

     public final class ComponentName implements Parcelable, Cloneable, Comparable<ComponentName> {
        ...
         public Bitmap copy(Config config, boolean isMutable) {
            ...
             noteHardwareBitmapSlowCall();
             Bitmap b = nativeCopy(mNativePtr, config.nativeInt, isMutable);
             if (b != null) {
                 b.setPremultiplied(mRequestPremultiplied);
                 b.mDensity = mDensity;
            }
             return b;
        }
     }



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

落地包体积监控,用Kotlin写一个APK差异分析CLI

引言 当谈到包体积优化时,网上不乏优秀的方案与文章,如 混淆、资源、ReDex、R8、SO 优化等等。 但聊到 包体积监控 时,总是感觉会缺乏落地性,或者总是会下意识认为这可能比较麻烦,需要其他部门连同配合。通常对于有APM基础的团队而言,这倒不算什么,但往往...
继续阅读 »

引言


当谈到包体积优化时,网上不乏优秀的方案与文章,如 混淆资源ReDexR8SO 优化等等。


但聊到 包体积监控 时,总是感觉会缺乏落地性,或者总是会下意识认为这可能比较麻烦,需要其他部门连同配合。通常对于有APM基础的团队而言,这倒不算什么,但往往对于小公司而言,到了这一步,可以说就戛然而止😶。


但回到问题本身,这并非难事。或者说,其实很简单 :)


计算差异通知汇总数据 ,三步即可。


按照最朴素的想法,无论多大的团队,也能至少完成前两步,事实上,也的确如此。


故此,本篇将结合实际需求以及背景,使用 Kotlin 去写一个 APK差异化 对比的基础 CLI 工具,并搭配 CI 完成流水线监控。



本篇并不涉及深度源码等,更多是实操,所以文章风格比较轻松,可放心食用 😁


最终组件地址: Github



写在开始


关于 CLI(command-line interface) ,每个开发同学应该都非常熟悉,可以说基本就是日常操作,比如我们经常在 命令行 里会去敲几个命令,触发几个操作等,常见的 gitgradle java 等。


在图形化(GUI)的现在,CLI 往往代表着一种 老派风格 ,有人抵触,觉得繁琐🤨,当然也有同学觉得简单直接。


但总体上的趋势是,越来越多工具趋于图形化。不过两者依然处于一种 互补 ,而非竞争,不同场景也有各自的优势以及差异化。比如在某些场景下,当我们需要去 简化开发流程 时,此时 CLI 就会作为首选项就会映入眼前。


聊聊背景


最近在做 下厨房-懒饭App 的体积优化,优化做完了(后续出文章),那如何做防劣化呢?


因为我们的项目是在 Github 上托管,所以自然而然也有相应的 Action 作为check,所以此时首先最基础想的就是:




  • 直接拉上一个版本的 apk 作为基准包,然后和本次的包一个 diff ,并保存结果;

  • 如果结果中,某个类别(如 resdex 等)超出指定阈值,则在PR里加一个🤖评论,以及飞书通知一下。

  • 至于分版本统计结果等等,这些都是后话了…



先找轮子


思路有了,那关键的工具,diff工具 怎么搞?


作为一个正经的开发仔,所以此时首选肯定是去 Github Action 市场上找现成的(没事就别乱造轮子,你造的又没人家好😅)。


结果发现,还真有,真不戳!


image-20230429223207105



来自微软的开源,那肯定有保障啊!



集成看看效果:


image-20230501232508815


嗯,看着还不错,不过这个输出怎么改呢,官方只有MD格式,而且看着过糙,作为一个稍微有点审美的同学。


那就考虑先 fork 改一下呢,fork 前看了一下仓库:


image-20230429224231548


我是辣鸡🥲,这下触摸到知识盲区了,压根不知道怎么改,无疑大大增加了后续迭代成本,以及看看上一次的版本时间(此处无声胜有声😶)。


那既然没有合适的 Action ,那就自己找一个 jar 工具也行啊,于是又去找了一下现有的jar工具,发现只有腾讯的 matrix-apk-canary 可用,但是这也太顶了吧。虽然功能强大,可是不符合我们现在的需要啊,我还得去手动算两次,然后再拿着json结果去对比,想想就复杂。


回到我们现在,我们完全不需要这么复杂,我们只是需要一个 diff工具 而已。



既然没有合适,那就自己造一个,反正diff逻辑也并不复杂。🤔



万事开头难


Jar怎么写?😅



是的,我也没写过这玩意,但本能觉得很简单。



先去 IDE 直接创建个项目,感觉应该选 JVM ,依赖配置上 Gradle 也更接近 Android 开发者的使用习惯,具体如下:


image-20230430113323152


凭着以前用 IDEKotlin 时的记忆,Jvm 参数应该是在这里进行传递🤔:


image-20230430113659826


输出也没啥问题,正常打印了出来:

Hello World!
Program arguments: Petterp,123

但这不是我要的样子啊,我的 理想状态 下是这种操作:

java -jar xxx.jar -x xxx 

不过就算现在能直接这样使用,也不能进行快速开发,首先调试就是个麻烦事。


再回到原点,我甚至不知道怎么在命令行传参呢🥲


说说CLIKT


此时就不得不提一个开款库,用 KotlinCLI 的最强库: CLIKT ,也是无意之间发现的一个框架,可以说是神器不足为过。


简介


Clikt(发音为“clicked”)是一个多平台的 Kotlin 库,可以使编写命令行界面变得简单和直观,它是“Kotlin 的命令行界面”。


该库旨在使编写命令行工具的过程变得轻松,同时支持各种用例,并在需要时允许高级自定义。


Clikt 具有以下特点:



  • 命令的任意嵌套;

  • 可组合、类型安全的参数值;

  • 生成帮助输出和 shell 自动完成脚本;

  • 针对 JVM、NodeJS 和本地 Linux、Windows 和 MacOS 的多平台包;


简而言之,Clikt 是一个功能丰富的库,可以帮助开发者快速构建命令行工具,同时具有灵活的自定义和多平台支持。



以上来自官网文档



依赖方式


因为我们是使用 Gradle 来进行依赖管理,所以直接添加相应的依赖即可:

implementation("com.github.ajalt.clikt:clikt:3.5.2")

同时因为使用的是 Gradle ,所以默认会带有一个 application 插件,因此提供一个 Gradle 任务,来将我们的 jar和脚本 控绑在一起启动(run Main时),从而免除了每次调试都要在命令行 java -jar xxx,非常方便。


示例效果









image-20230430121553523Kapture 2023-04-30 at 12.00.12

代码也非常简单,我们定义了两个参数,countname,其中 count 存在默认参数,而 name 没有,故需要我们必须传递,直接运行run方法,然后根据提示键入value即可,就这么简单。👏



在往常的jar命令里,通常都只存在一次性输入的场景。比如必须直接输入全部kay-value,如果输入错误,或者异常,日志或者输出全凭jar包开发者的自觉程度。可以说大多数jar包并不易用,当然这主要的原因是,传统的cli开发的确比较麻烦,并不是所有开发者都能完善好边界。


使用 CLIKT 之后,上面的问题可以说非常低成本解决,我们可以提前配置提示语句,报错语句等等。它可以做到提示使用者接下来该输入什么,也可以做到对输入进行check,甚至如果输入错误或者不符合要求,直接会进行提示,也可以选择继续让用户输入。



上述的示例只是非常简单的一个常见,CLIKT 官网有更多的用法以及高级示例,如果感兴趣,也可以看看。


常见问题


如何打jar包


上面我们实现了 jar包 的编写和本地调试,那该怎么打成 jar包 在命令行运行呢?


因为我们使用了 Gradle 进行依赖配置,那么相应的,也可以使用附带的命令即可,默认有这几个命令可供选择:




  • jar


    直接打成jar包,后续直接在命令行java -jar 的方式驱动。




  • distTar || distZip


    简单来说就是,同时会附带可执行程序 exec 的方式,从而免除 java -jar 的硬编码,直接点击执行或者在命令行输入 文件名+附带的参数 即可。不过从打包方式上而言,其最终也需要依附于jar任务。





这里感谢 虾哥(掘金: 究极逮虾户) 解惑,原本以为 exec 这种方式会导致传参时的部分默认值无法设置问题。



jar包没有主清单属性


上面打完jar包,在命令行运行时,报错如下:

xxx.jar中没有主清单属性

这是什么鬼,不是已经配置过了吗?直接 run main 方法没有什么问题啊?

application {
mainClassName = 'HelloKt'
}

经过一顿查阅,发现配置需要这样改一下,build.gradle 增加以下配置:

jar {
exclude("**/module-info.class")
from {
configurations.runtimeClasspath.collect {
it.isDirectory() ? it : zipTree(it)
}
}
manifest {
attributes 'Main-Class': "HelloKt"
}
}

原理也很简单,你打出来的 jar 包得配置路径啊。我们调试时走的 application 插件里的 run 。而打 jar 包, jar 命令没配置,导致其并不知道你的配置,所以不难理解为啥找不到主清单属性。


再聊实现思路


要对比 Apk 的差异,最简单的思路莫过于直接解压Apk。因为 Apk 其实是一种 Zip 格式,所以我们只需要遍历解压文件,根据文件后缀以及不同的文件夹分类统计大小即可,比较简单粗暴。


当然如果要做的更精细一点,比如要统计 资源文件差异代码增长aar变化 等,就要借助其他方式,比如 Android 团队就为我们提供了 apkanalyzer,或者可以通过 META-INF/MANIFEST.MF 文件作为基准进行对比。



业内开源的比较好的有腾讯的 matrix-apk-canary,其设计灵巧,功能也更加强大,具体在实现上,我们也可以借鉴其设计思想。



因为本次我们的需求无需上述那么复杂,只需要在意 apk资源dexlib 等差异,所以直接采用手动解压Apk的方式,手动统计,反而更加直接。


核心代码


image-20230430232551506


思路如下:



  • 解压 apk ,开始进行遍历;

  • 按照自定义的规则进行分类,从而得到apk的实际文件类型映射 Map;

  • 遍历过程中,同时 分类统计 各类型大小以及子集;


匹配与模型设计















image-20230430232857348image-20230430233015109
自定义规则文件Model

一些小Tips


关于分层的想法


一个合格 CLI 设计,基本应该包含下面的流程:



配置 -> 分析 -> 输出





  • 配置


    顾名思义,就是指的是开发者友好,即对用户而言,报错详细,配置灵巧,藏复杂于内部。


    比如在阈值的设定上,除了最基本的分类,也要提供统一默认配置,同时要对用户键入的 key-value 做基本的 check ,这些借助 CLIKT 框架能很低成本的实现。




  • 分析


    拿到上一步的配置结果后,接下来就要开始进行分析,此时我们要考虑设计上的分层,比如匹配规则如何定义,采用怎样的数据结构比较好,规则是否严谨,甚至如果要替换基础实现思路,改动会不会依然低成本;




  • 输出


    输出理论上应该包含多个途径,比如 jsonmd命令行 等等,不同的用户场景也必然不同。比如应用于 CI 、或者自定义结果统计等;在具体的设计上,开发者也应该考虑进行分层,比如输出这里只接受数据源,直接按照规则处理即可,而非再次对数据源进行修改。




灵活运用语言技巧


image-20230430233321713


Kotlin 内联类 是一个很棒的特性,无论是性能还是可读性方面,如果我们有某个字段,是使用基本类型作为定义,那么此时就可以考虑将其定义为内联类。


比如我们本篇中的 file大小(size字段),通常我们会使用 Long 类型进行代表,但是 Long 类型用于展示而言,可读性并不好,所以此时使用内联类对其进行包装,并搭配 操作符重载 ,使得开发中的体验度会提高不少。


关于CI方面


关于 CI 方面,首选就是 Github Action,具体 Github 也有专门的教程,上手难度也很低,几分钟足以,对于经常写开源库的作者而言,这个应该也算是基本技巧。相应的,既然我们也是产出了一个 CLI 组件,那么每次 release 时都手动上传jar包,或者版本的定义上,如果每次都手动修改,怎么都显得 不优雅


故此,我们可以考虑每次 发布新的release版本 之后,就触发一次 Action,然后打一个 jar 包,并将其上传到我们最新的 release 里。相应的,自动化的版本也可以在这里进行匹配,都比较简单。


这里,以自动化发布jar为例:

name: Cli Release

on:
release:
types: [ published ]

permissions: write-all

jobs:

build_assemble:
runs-on: ubuntu-latest
env:
OUTPUT_DIR: build/libs
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: set up JDK 11
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'temurin'
cache: gradle

- uses: burrunan/gradle-cache-action@v1
name: Cache gradle
- name: Grant execute permission for gradlew
run: chmod +x gradlew

- name: Build jar
run: ./gradlew jar

- uses: AButler/upload-release-assets@v2.0
with:
files: build/libs/apk-size-diff-cli.jar
repo-token: ${{ github.token }}
release-tag: ${{ github.event.release.tag_name}}

总体步骤依然非常简单,我们定义这个工作流的触发时机为每次 release 时,然后 拉代码配置gradle打jar包、上传到最新release-assets里。


效果如下:


image-20230501194707110


最终效果


image-20230502001351177


最终搭配 Github CI 实现的效果如上,开源地址 apk-size-diff-cli


使用方式也非常简单,本地使用的话,执行 jar 命令(或者使用 exec 的方式,免除 java -jar) 即可,如下示例所示:

java -jar apk_size_diff_cli.jar -b base.apk -c current.apk -d outpath/result -tss 102400


默认会在指定的输出路径,如 outpath/result 输出一个名为 apk_size_diff.md 的文档。


其中 -tss 指的是默认各类别的阈值大小,比如 apk、dex 等如果某一项本次对比上次超过102400,则输出结果里会有相应提示。



如果大家对这个组件比较感兴趣,也不妨点个Star,整体实现较为干净利落,fork更改也非常简单。


结语


本篇到这里就算结束了,总体也并不算什么高深技巧或者深度文章,更多的是站在一个 技术需求 的背景下,由0到1,完成一个 CLI 组件的全流程开发,希望整个过程以及思考会对大家有所帮助。


参考



关于我


我是 Petterp ,一个 Android工程师 ,如果本文对你有所帮助,欢迎 点赞、评论、收藏,你的支持是我持续创作的最大鼓励!


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

Android那两个你碰不到但是很重要的类之ViewRootImpl

前言 这两个类就是ActivityThread和ViewRootImpl,之所以说碰不到是因为我们无法通过正常的方式引用这两个类或者其类的对象,调用方法或者直接拿他的属性。但他们其实又无处不在,应用开发中很多时候都和他们息息相关,阅读他们掌握其内部实现对我们理...
继续阅读 »

前言


这两个类就是ActivityThread和ViewRootImpl,之所以说碰不到是因为我们无法通过正常的方式引用这两个类或者其类的对象,调用方法或者直接拿他的属性。但他们其实又无处不在,应用开发中很多时候都和他们息息相关,阅读他们掌握其内部实现对我们理解Android运行机理有醍醐灌顶之疗效,码读百变其义自见,常读常新。本文就尝试从几个我们经常接触的方面先谈谈ViewRootImpl。


1.1 ViewRootImpl哪来的?


首先是ViewRootImpl,位于android.view包下,从它所处的位置大概能猜到,跟View相关。其作用一句话总结,就是连接Window和View的纽带。


这个要从我们最熟悉的Activity开始,我们知道Activity的设置布局View是通过setContentView() 方法这个方法里面也大有文章,我们简单的梳理下。



  • Activity setcontentView()内部调用了getWindow().setContentView(layoutResID);也就是调用了Window的setContentView方法,Android里Window的唯一实现类就是PhoneWindow,PhoneWindow setContentView,初始化DecorView和把我们设置的View作为其子类。

  • 目光转移到ActivityThread 没错是我们提及的另外一个主角,先关注他的handleResumeActivity()方法,里面关键的代码如下。

public void handleResumeActivity(){
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
ViewManager wm = a.getWindowManager();
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
wm.addView(decor, l);
}


  • WindowManager的实现类WindowManageImpl的addView方法里调用了mGlobal.updateViewLayout(view, params);

  • 最后我们在WindowManagerGlobal的addView方法里找到了
public void addView(){
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
}

小结



  • 通过梳理这个过程我们知道,setContenview()其实只是在Window的下面挂了一个View链,View链的根就是ViewRootImpl。

  • 通过Window把View和Activity联系在一起。

  • View链的真正添加操作最终交给了WindowManagerGlobal执行。

  • 补充一点:PopupWindow本质就是在当前Window下挂了一个View链,PopupWindow本身没有Window,就如雷锋塔没有雷锋一样;Dialog是有自己的window关于这点可自行查阅源码考证。


2 ViewRootImpl 一个View链渲染的中转站


View的渲染是自顶而下、层层向下发起的,大致经历测量布局和绘制,View链的管理者也就是ViewRootImpl。通过scheduleTraversals()方法发起渲染动作。交给Choreographer安排真正执行的事件。关于Choreographer不熟悉的可以参考我的其他文章。最终执行performTraversals() 方法。

private void performTraversals(){
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
performLayout(lp, mWidth, mHeight);
performDraw();
}

3 不能在子线程操作View?


ViewRoot的RequestLayout中有这样一段代码:

@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}

void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}


  • 我们对View的操作,比如给TextView设置text,最终都会触发ViewRootImpl的requestLayout() 方法,该方法有如上的一个check逻辑。这就是我们常说的不能在子线程中更新View。

  • 其实子线程中可以执行View的操作,但是有个前提是:View还未挂载时。 View未挂载时不会触发requestLayout,只是一个普普通通的java对象。那挂载逻辑在哪?


4 View 挂载



  • 在ViewRootImpl的performTraversals() 里有这个代码
private void performTraversals(){
host.dispatchAttachedToWindow(mAttachInfo, 0);//此处的host为ViewGroup
}


  • ViewGroup的dispatchAttachedToWindo()方法会把AttachInfo对象分配每一个View,最终实现我们所谓的挂载。
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
for (int i = 0; i < count; i++) {
final View child = children[i];
child.dispatchAttachedToWindow(info,
combineVisibility(visibility, child.getVisibility()));
}



  • 实现挂载的View有任何风吹草动就会把事件传递到大bossViewRootImpl这里了。


通过addView添加进的View也是会收到父View的mAttachInfo这里不展开了。


5 View.post()的Runnable最终在哪执行了?

public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.post(action);
}
getRunQueue().post(action);
return true;
}


  • 以上是View post()的代码,可见如果已经实现挂载的View,会直接把post进来的消息交给Hanlder处理,不然就post了HandlerActionQueue里等待后续被处理。
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
..
if (mRunQueue != null) {
mRunQueue.executeActions(info.mHandler);//内部也是调用handler.post()
mRunQueue = null;
}
..
}


  • 最终这些Runnable会在View挂载的时候执行,也就是dispatchAttachedToWindow()方法里执行。


6 为什么View.post 可以获取宽高




  • 这是一个延伸问题,在Activity的OnCreate()方法中直接获取宽高是获取不到的,我们通常会使用view.post一个Runnable来获取。原因就是Activity onCreate时通过setContentView只是创建了View而未实现挂载,挂载是在onResume时,未挂载的View没有经历测量过程。




  • 而通过post的方式,通过上一小节知道,未挂载的View上post之后,任务会在挂载之后,通过handler重新post,此时iewRootImpl已经执行了performTraversals()完成了测量自然就可以得到宽高。




7 还有一点值得注意


ViewRootImpl 不单单是渲染的中转站,还是触摸事件的中转站。


硬件传感器接收到触摸事件经过层层传递分发到应用窗口的第一站就是ViewRootImpl。为什么这么说?因为我有证据~。这是ViewRoot里的代码

public void setView(){
..
mInputEventReceiver = new WindowInputEventReceiver(inputChannel,
Looper.myLooper());
}


  • WindowInputEventReceiver是ViewRootImpl的一个内部类,其接收到input事件后,就会进行事件分发。

  • 这里给我们的启发是,并不是所有的主线程任务执行都是通过Handler机制, onTouch()事件是底层直接回调过来的,这就和我们之前卡顿监控说的方案里有一项就是对onTouchEvent的监控。




  • ViewRoot的代码有一万多行,本文分析的只是冰山一角,里面有大量细节直接研究。

  • 通过ViewRootImpl相关几个点,简单的做了介绍分析希望对你有帮助。

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

这玩意真的有用吗?对,是的!Kotlin 的 Nothing 详解

视频先行这是一篇视频形式的分享,如果你方便看,可以直接去看视频:哔哩哔哩:这里抖音:这里YouTube:这里视频先行哔哩哔哩YouTube下面是视频内容的脚本文案原稿分享。文案原稿Kotlin 的 Nothing 类,无法创建出任何实例:p...
继续阅读 »

视频先行

这是一篇视频形式的分享,如果你方便看,可以直接去看视频:

视频先行

哔哩哔哩

YouTube

下面是视频内容的脚本文案原稿分享。

文案原稿

Kotlin 的 Nothing 类,无法创建出任何实例:

public class Nothing private constructor()

所以所有 Nothing 类型的变量或者函数,都找不到可用的值:

val nothing: Nothing = ???
fun nothing(): Nothing {
...
return ???
}

就这么简单。但——它有啥用啊?

Nothing 的本质

大家好,我是扔物线朱凯。上期讲了 Kotlin 的 Unit,这期讲 Nothing。 Nothing 的源码很简单:

public class Nothing private constructor()

可以看到它本身虽然是 public 的,但它的构造函数是 private 的,这就导致我们没法创建它的实例;而且它不像 Unit 那样是个 object

public object Unit {
override fun toString() = "kotlin.Unit"
}

而是个普通的 class;并且在源码里 Kotlin 也没有帮我们创建它的实例。

这些条件加起来,结果就是:Nothing 这个类既没有、也不会有任何的实例对象。

基于这样的前提,当我们写出这个函数声明的时候:

fun nothing(): Nothing {

}

我们不可能找到一个合适的值来返回。你必须返回一个值,但却永远找不到合适的返回值。悖论了。

作用一:作为函数「永不返回」的提示

怎么办?

不怎么办。这个悖论,就是 Nothing 存在的意义:它找不到任何可用的值,所以,以它为返回值类型的一定是个不会返回的函数,比如——它可以总是抛异常。 什么意思?就是说,我这么写是可以的:

fun nothing() : Nothing {
throw RuntimeException("Nothing!")
}

这个写法并没有返回任何结果,而是抛异常了,所以是合法的。

可能有的人会觉得有问题:抛异常就可以为所欲为吗?抛异常就可以忽略返回值了吗?——啊对,抛异常就是可以忽略返回值,而且这不是 Nothing 的特性,而是本来就是这样,而且你本来就知道,只是到这里的时候,你可能会忘了。 例如这个写法:

fun getName() : String {
if (nameValue != null) {
return nameValue
} else {
throw NullPointerException("nameValue 不能为空!")
}
}

——其实这个函数可以有更加简洁的写法:

fun getName() = nameValue ?: throw NullPointerException("nameValue 不能为空!")

不过我们为了方便讲解,就不简化了:

fun getName() : String {
if (nameValue != null) {
return nameValue
} else {
throw NullPointerException("nameValue 不能为空!")
}
}

在这个函数里,一个 if 判断,true 就返回,false 就抛异常,这个写法很常见吧?它在 else 的这个分支,是不是就只抛异常而不返回值了?实际上 Java 和 Kotlin 的任何方法或者说函数,在抛异常的时候都是不返回值的——你都抛异常的还返回啥呀返回?是吧?

所以我如果改成这样:

fun getName() : String {
throw NullPointerException("不能为空!")
}

其实也是可以的。只是看起来比较奇怪罢了,会让人觉得「怎么会这么写呢」?但它的写法本身是完全合法的。而且如果我把函数的名字改一下,再加个注释:

/**
当遇到姓名为空的时候,请调用这个函数来抛异常
*/
fun throwOnNameNull() : String {
throw NullPointerException("姓名不能为空!")
}

这就很合理了吧?不干别的,就只是抛异常。这是一种很常用的工具函数的写法,包括 Kotlin 和 Compose 的官方源码里也有这种东西。

那么我们继续来看它的返回值类型:我都不返回了,就没必要还写 String 了吧?那写什么?可以把它改成 Unit

/**
当任何变量为空的时候,请统一调用这个函数来抛异常
*/
fun throwOnNameNull() : Unit {
throw NullPointerException("姓名不能为空!")
}

有问题吗?没问题。

不过,Kotlin 又进了一步,提供了一个额外的选项:你还可以把它改成 Nothing

/**
当任何变量为空的时候,请统一调用这个函数来抛异常
*/
fun throwOnNameNull() : Nothing {
throw NullPointerException("姓名不能为空!")
}

虽然我找不到 Nothing 的实例,但是这个函数本来就是永远抛异常的,找不到实例也没关系。哎,这不就能用了吗?对吧?

不过,能用归能用,这么写有啥意义啊?是吧?价值在哪?——价值就在于,Nothing 这个返回值类型能够给使用它的开发者一个明确的提示:这是个永远不会返回的函数。这种提示本身,就会给开发提供一些方便,它能很好地避免函数的调用者对函数的误解而导致的一些问题。我们从 Java 过来的人可能第一时间不太能感受到这种东西的用处,其实你要真说它作用有多大吧,我觉得不算大,主要是很方便。它是属于「你没有的话也不觉得有什么不好的,但是有了之后就再也不想没有它」的那种小方便。就跟 120Hz 的屏幕刷新率有点像,多少带点毒。

Kotlin 的源码、Compose 的源码里都有不少这样的实践,比如 Compose 的 noLocalProviderFor() 函数:

private fun noLocalProvidedFor(name: String): Nothing {
error("CompositionLocal $name not present")
}

好,这就是 Nothing 的作用之一:作为函数的返回值类型,来明确表达「这是个永不返回的函数」。

其实 Nothing 的「永不返回」除了抛异常之外,还有一种场景,就是无限循环:

fun foreverRepeat(): Nothing {
while (true) {
...
}
}

不过一般很少有人这么去用,大部分都是用在我刚才说的抛异常的场景,这是非常常见的一种用法,你写业务可能用不到,但是基础架构团队给全公司写框架或者对外写 SDK 的话,用到它的概率就非常大了。

作用二:作为泛型对象的临时空白填充

另外 Nothing 除了「没有可用的实例」之外,还有个特性:它是所有类型共同的子类型。这其实是违反了 Kotlin 的「类不允许多重继承」的规定的,但是 Kotlin 强行扩充了规则:Nothing 除外,它不受这个规则的约束。虽然这违反了「类不允许多重继承」,但因为 Nothing 不存在实例对象,所以它的多重继承是不会带来实际的风险的。——我以前还跟人说「Nothing 是所有类型的子类型」这种说法是错误的,惭愧惭愧,是我说错了。

不过,这个特性又有什么作用呢?它就能让你对于任何变量的赋值,都可以在等号右边写一个 Nothing

val nothing: Nothing = TODO()
var apple: Apple = nothing

这儿其实有个问题:我刚说了 Nothing 不会有任何的实例,对吧?那么这个右边就算能填 Nothing 类型的对象,可是这个对象我用谁啊?

val nothing: Nothing = ???
var apple: Apple = nothing

谁也没法用。

但是我如果不直接用 Nothing,而是把它作为泛型类型的实例化参数:

val emptyList: List<Nothing> = ???
var apples: List<Apple> = emptyList

这就可以写了。一个元素类型为Nothing 的 List,将会导致我无法找到任何的元素实例来填充进去,但是这个 List 本身是可以被创建的:

val emptyList: List<Nothing> = listOf()
var apples: List<Apple> = emptyList

只不过这种写法看起来好像有点废,因为它永远都只能是一个空的 List。但是,如果结合上我们刚说的「Nothing 是所有类型的子类型」这个特性,我们是不是可以把这个空的 List 赋值给任何的 List 变量?

val emptyList: List<Nothing> = listOf()
var apples: List<Apple> = emptyList
var users: List<User> = emptyList
var phones: List<Phone> = emptyList
var images: List<Image> = emptyList

这样,是不是就提供了一个通用的空 List 出来,让这一个对象可以用于所有 List 的初始化?有什么好处?既省事,又省内存,这就是好处。

这种用法不只可以用在 ListSet 和 Map 也都没问题:

val emptySet: Set<Nothing> = setOf()
var apples: Set<Apple> = emptySet
var users: Set<User> = emptySet
var phones: Set<Phone> = emptySet
var images: Set<Image> = emptySet
val emptyMap: Map<String, Nothing> = emptyMap()
var apples: Map<String, Apple> = emptyMap
var users: Map<String, User> = emptyMap
var phones: Map<String, Phone> = emptyMap
var images: Map<String, Image> = emptyMap

而且也不限于集合类型,只要是泛型都可以,你自定义的也行:

val emptyProducer: Producer<Nothing> = Producer()
var appleProducer: Producer<Apple> = emptyProducer
var userProducer: Producer<User> = emptyProducer
var phoneProducer: Producer<Phone> = emptyProducer
var imageProducer: Producer<Image> = emptyProducer

它的核心在于,你利用 Nothing 可以创建出一个通用的「空白」对象,它什么实质内容也没有,什么实质工作也做不了,但可以用来作为泛型变量的一个通用的空白占位值。这就是 Nothing 的第二个主要用处:作为泛型变量的通用的、空白的临时填充。多说一句:这种空白的填充一定是临时的才有意义,你如果去观察一下就会发现,这种用法通常都是赋值给 var 属性,而不会赋值给 val

val emptyProducer: Producer<Nothing> = Producer()
// 没人这么写:
val appleProducer: Producer<Apple> = emptyProducer
val userProducer: Producer<User> = emptyProducer
val phoneProducer: Producer<Phone> = emptyProducer
val imageProducer: Producer<Image> = emptyProducer

因为赋值给 val 那就是永久的「空白」了,永久的空白那不叫空白,叫废柴,这个变量就没意义了。

作用三:语法的完整化

另外,Nothing 的「是所有类型的子类型」这个特点,还帮助了 Kotlin 语法的完整化。在 Kotlin 的下层逻辑里,throw 这个关键字是有返回值的,它的返回值类型就是 Nothing。虽然说由于抛异常这件事已经跳出了程序的正常逻辑,所以 throw 返回不返回值、返回值类型是不是 Nothing 对于它本身都不重要,但它让这种写法成为了合法的:

val nothing: Nothing = throw RuntimeException("抛异常!")

并且因为 Nothing 是所有类型的子类型,所以我们这么写也行:

val nothing: String = throw RuntimeException("抛异常!")

看起来没用是吧?如果我再把它改改,就有用了:

var _name: String? = null
val name: String = _name ?: throw NullPointerException("_name 在运行时不能为空!")

throw 的返回值是 Nothing,我们就可以把它写在等号的右边,在语法层面假装成一个值来使用,但其实目的是在例外情况时抛异常。

Kotlin 里面有个 TODO() 函数对吧:

val someValue: String = TODO()

这种写法不会报错,并不是 IDE 或者编译器做了特殊处理,而是因为 TODO() 的内部是一个 throw:  TODO() 返回的是 Nothing,是 String 的子类,怎么不能写了?完全合法!虽然 throw 不会真正地返回,但这让语法层面变得完全说得通了,这也是 Nothing 的价值所在。

除了 throw 之外,return 也是被规定为返回 Nothing 的一个关键字,所以我也可以这么写:

fun sayMyName(first: String, second: String) {
val name = if (first == "Walter" && second == "White") {
"Heisenberg"
} else {
return // 语法层面的返回值类型为 Nothing,赋值给 name
}
println(name)
}

这段代码也是可以简化的:

fun sayMyName(first: String, second: String) {
if (first == "Walter" && second == "White") println("Heisenberg")
}

不过同样,咱不是为了讲东西么,就不简化了:

fun sayMyName(first: String, second: String) {
val name = if (first == "Walter" && second == "White") {
"Heisenberg"
} else {
return // 语法层面的返回值类型为 Nothing,赋值给 name
}
println(name)
}

虽然直接强行解释为「return 想怎么写就怎么写」也是可以的,但 Kotlin 还是扩充了规则,规定 return 的返回值是 Nothing,让代码从语法层面就能得到解释。

这就是 Nothing 的最后一个作用:语法层面的完整化。

总结

好,Nothing 的定义、定位和用法就是这些。如果没记全,很正常,再看一遍。你看视频花的时间一定没有我研究它花的时间多,所以多看两遍应该不算浪费时间。 下期我会讲一个很多人不关注但很有用的话题:Kotlin 的数值系统,比如 Float 和 Double 怎么选、为什么 0.7 / 5.0 ≠ 0.14 这类的问题。关注我,了解更多 Android 开发相关的知识和技能。我是扔物线,我不和你比高低,我只助你成长。我们下期见!


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

iOS H5页面秒加载预研

背景 原生架构+H5页面的组合是很常见的项目开发模式了,H5的优势是跨平台、开发快、迭代快、热更新,很多大厂的App大部分业务代码都是H5来实现的,众所周知H5页面的体验是比原生差的,特别是网络环境差的时候,如果首屏页面是H5的话,那酸爽遇见过的都懂 白屏警告...
继续阅读 »

背景


原生架构+H5页面的组合是很常见的项目开发模式了,H5的优势是跨平台、开发快、迭代快、热更新,很多大厂的App大部分业务代码都是H5来实现的,众所周知H5页面的体验是比原生差的,特别是网络环境差的时候,如果首屏页面是H5的话,那酸爽遇见过的都懂


白屏警告




白屏主要就是下载页面资源失败或者慢导致的,下面可以看下H5加载过程


H5加载过程


这部分内容其实网上已经很多了,如下


初始化 webview -> 请求页面 -> 下载数据 -> 解析HTML -> 请求 js/css 资源 -> dom 渲染 -> 解析 JS 执行 -> JS 请求数据 -> 解析渲染 -> 下载渲染图片


主要要优化的就是下载到渲染这段时间,最简单的方案就是把整个web包放在App本地,通过本地路径去加载,做到这一步,再配合上H5页面做加载中占位图,基本上就不会有白屏的情况了,而且速度也有了很明显的提升


可参考以下测试数据(网络地址加载和本地路径加载)


WiFi网络打开H5页面100次耗时:(代表网络优秀时)


本地加载平均执行耗时:0.28秒


网络加载平均执行耗时:0.58秒


4G/5G手机网络打开H5页面100次耗时:(代表网络良好时)


本地加载平均执行耗时:0.43秒


网络加载平均执行耗时:2.09秒


3G手机网络打开H5页面100次耗时:(代表网络一般或者较差时)


本地加载平均执行耗时:1.48秒


网络加载平均执行耗时:19.09秒


ok,恭喜你,H5离线秒加载功能优化完毕,这么简单?


IMG_4941.JPG


进入正题


通过上述实现本地加载后速度虽然是快了不少,但H5页面的数据显示还是需要走接口请求的,如果网络环境不好的话,也是会很慢的,这个问题怎么解决呢?


下面开始上绝活,这里引入一个概念 WKURLSchemeHandler
在iOS11及以上系统中,可以通过WKURLSchemeHandler自定义拦截请求,有什么用?简单说就是可以利用原生的数据缓存去加载H5页面,可以无视网络环境的影响,在拦截到H5页面的网络请求后先判断本地是否有缓存,有缓存的话可以直接拼接一个成功的返回,没有的话直接放开继续走网络请求,成功后再缓存数据,对H5来说也是无侵入无感知的。


首先自定义一个SchemeHandler类,遵守WKURLSchemeHandler协议,实现协议方法


@protocol WKURLSchemeHandler <NSObject>

/*! @abstract Notifies your app to start loading the data for a particular resource
represented by the URL scheme handler task.
@param webView The web view invoking the method.
@param urlSchemeTask The task that your app should start loading data for.
*/

- (void)webView:(WKWebView *)webView startURLSchemeTask:(id <WKURLSchemeTask>)urlSchemeTask;

/*! @abstract Notifies your app to stop handling a URL scheme handler task.
@param webView The web view invoking the method.
@param urlSchemeTask The task that your app should stop handling.
@discussion After your app is told to stop loading data for a URL scheme handler task
it must not perform any callbacks for that task.
An exception will be thrown if any callbacks are made on the URL scheme handler task
after your app has been told to stop loading for it.
*/

- (void)webView:(WKWebView *)webView stopURLSchemeTask:(id <WKURLSchemeTask>)urlSchemeTask;

@end

直接上代码


#import <Foundation/Foundation.h>
#import <WebKit/WebKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface YXWKURLSchemeHandler : NSObject<WKURLSchemeHandler>

@end

NS_ASSUME_NONNULL_END

#import "CMWKURLSchemeHandler.h"
#import "YXNetworkManager.h"
#import <SDWebImage/SDWebImageManager.h>
#import <SDWebImage/SDImageCache.h>

@implementation YXWKURLSchemeHandler

- (void) webView:(WKWebView *)webView startURLSchemeTask:(id<WKURLSchemeTask>)urlSchemeTask {
NSURLRequest * request = urlSchemeTask.request;
NSString * urlStr = request.URL.absoluteString;
NSString * method = urlSchemeTask.request.HTTPMethod;
NSData * bodyData = urlSchemeTask.request.HTTPBody;
NSDictionary * bodyDict = nil;

if (bodyData) {
bodyDict = [NSJSONSerialization JSONObjectWithData:bodyData options:kNilOptions error:nil];
}

NSLog(@"拦截urlStr=%@", urlStr);
NSLog(@"拦截method=%@", method);
NSLog(@"拦截bodyData=%@", bodyData);

// 图片加载
if ([urlStr hasSuffix:@".jpg"] || [urlStr hasSuffix:@".png"] || [urlStr hasSuffix:@".gif"]) {
SDImageCache * imageCache = [SDImageCache sharedImageCache];
NSString * cacheKey = [[SDWebImageManager sharedManager] cacheKeyForURL:request.URL];
BOOL isExist = [imageCache diskImageDataExistsWithKey:cacheKey];
if (isExist) {
NSData * imgData = [[SDImageCache sharedImageCache] diskImageDataForKey:cacheKey];
[urlSchemeTask didReceiveResponse:[[NSURLResponse alloc] initWithURL:request.URL MIMEType:[self createMIMETypeForExtension:[urlStr pathExtension]] expectedContentLength:-1 textEncodingName:nil]];
[urlSchemeTask didReceiveData:imgData];
[urlSchemeTask didFinish];
return;
}
[[SDWebImageManager sharedManager] loadImageWithURL:request.URL options:SDWebImageRetryFailed progress:nil completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL * _Nullable imageURL) {}];
}

// 网络请求
NSData * cachedData = (NSData *)[YXNetworkCache httpCacheForURL:urlStr parameters:bodyDict];
if (cachedData) {
NSHTTPURLResponse * response = [self createHTTPURLResponseForRequest:urlSchemeTask.request];
[urlSchemeTask didReceiveResponse:response];
[urlSchemeTask didReceiveData:cachedData];
[urlSchemeTask didFinish];
} else {
NSURLSessionDataTask * dataTask = [[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (error) {
[urlSchemeTask didFailWithError:error];
} else {
[YXNetworkCache setHttpCache:data URL:urlStr parameters:bodyDict];
[urlSchemeTask didReceiveResponse:response];
[urlSchemeTask didReceiveData:data];
[urlSchemeTask didFinish];
}
}];
[dataTask resume];
}
} /* webView */

- (NSHTTPURLResponse *) createHTTPURLResponseForRequest:(NSURLRequest *)request {
// Determine the content type based on the request
NSString * contentType;

if ([request.URL.pathExtension isEqualToString:@"css"]) {
contentType = @"text/css";
} else if ([[request valueForHTTPHeaderField:@"Accept"] isEqualToString:@"application/javascript"]) {
contentType = @"application/javascript;charset=UTF-8";
} else {
contentType = @"text/html;charset=UTF-8"; // default content type
}

// Create the HTTP URL response with the dynamic content type
NSHTTPURLResponse * response = [[NSHTTPURLResponse alloc] initWithURL:request.URL statusCode:200 HTTPVersion:@"HTTP/1.1" headerFields:@{ @"Content-Type": contentType }];

return response;
}

- (NSString *) createMIMETypeForExtension:(NSString *)extension {
if (!extension || extension.length == 0) {
return @"";
}

NSDictionary * MIMEDict = @{
@"txt" : @"text/plain",
@"html" : @"text/html",
@"htm" : @"text/html",
@"css" : @"text/css",
@"js" : @"application/javascript",
@"json" : @"application/json",
@"xml" : @"application/xml",
@"swf" : @"application/x-shockwave-flash",
@"flv" : @"video/x-flv",
@"png" : @"image/png",
@"jpg" : @"image/jpeg",
@"jpeg" : @"image/jpeg",
@"gif" : @"image/gif",
@"bmp" : @"image/bmp",
@"ico" : @"image/vnd.microsoft.icon",
@"woff" : @"application/x-font-woff",
@"woff2": @"application/x-font-woff",
@"ttf" : @"application/x-font-ttf",
@"otf" : @"application/x-font-opentype"
};

NSString * MIMEType = MIMEDict[extension.lowercaseString];
if (!MIMEType) {
return @"";
}

return MIMEType;
} /* MIMETypeForExtension */

- (void) webView:(WKWebView *)webView stopURLSchemeTask:(id<WKURLSchemeTask>)urlSchemeTask {
NSLog(@"stop = %@", urlSchemeTask);
}

@end

重点来了,需要hook WKWebview 的 handlesURLScheme 方法来支持 http 和 https 请求的代理


直接上代码


#import "WKWebView+SchemeHandle.h"
#import <objc/runtime.h>

@implementation WKWebView (SchemeHandle)

+ (void) load {
static dispatch_once_t onceToken;

dispatch_once(&onceToken, ^{
Method originalMethod1 = class_getClassMethod(self, @selector(handlesURLScheme:));
Method swizzledMethod1 = class_getClassMethod(self, @selector(cmHandlesURLScheme:));
method_exchangeImplementations(originalMethod1, swizzledMethod1);
});
}

+ (BOOL) cmHandlesURLScheme:(NSString *)urlScheme {
if ([urlScheme isEqualToString:@"http"] || [urlScheme isEqualToString:@"https"]) {
return NO;
} else {
return [self handlesURLScheme:urlScheme];
}
}

@end

这里会有一个疑问了?为什么要这么用呢,这里主要是为了满足对H5端无侵入、无感知的要求
如果不hook http和https的话,就需要在H5端修改代码了,把scheme修改成自定义的customScheme,全部都要改,而且对安卓还不适用,所以别这么搞,信我!!!


老老实实hook,一步到位


如何使用


首先是WKWebView的初始化,直接上代码


- (WKWebView *) wkWebView {
if (!_wkWebView) {
WKWebViewConfiguration * config = [[WKWebViewConfiguration alloc] init];
// 允许跨域访问
[config setValue:@(true) forKey:@"allowUniversalAccessFromFileURLs"];

// 自定义HTTPS请求拦截
YXWKURLSchemeHandler * handler = [YXWKURLSchemeHandler new];
[config setURLSchemeHandler:handler forURLScheme:@"https"];

_wkWebView = [[WKWebView alloc] initWithFrame:CGRectMake(0, 0, kScreenWidth, kScreenHeight) configuration:config];
_wkWebView.navigationDelegate = self;
}
return _wkWebView;
} /* wkWebView */

NSString * htmlPath = [[NSBundle mainBundle] pathForResource:@"index" ofType:@"html" inDirectory:@"H5"];
NSURL * fileURL = [NSURL fileURLWithPath:htmlPath];
NSURLRequest * request = [NSURLRequest requestWithURL:url];
[self.wkWebView loadRequest:request];

ok,到了这一步基本上就能看到效果了,H5页面的接口请求和图片加载都会拦截到,直接使用原生的缓存数据,忽略网络环境的影响实现秒加载了


可参考以下测试数据(自定义WKURLSchemeHandler拦截和不拦截)


3G手机网络打开H5页面100次耗时:(代表网络一般或者较差时)

Figure_1.png


拦截后加载平均执行耗时:0.24秒


不拦截加载平均执行耗时:1.09秒


可以发现通过自定义WKURLSchemeHandler拦截后,加载速度非常平稳根本不受网络的影响,不拦截的话虽然整体加载速度并不算太慢,但是随网络波动比较明显。


不足


这套方案也还是有一些缺点的,比如



  1. App打包的时候需要预嵌入web模块的数据,会导致App包的大小增加(需要尽量缩小web模块的包大小,特别是资源文件的优化)

  2. App需要设计一套web模块包的更新机制,同时需要设计一个web包上传发布平台,后续版本管理更新比之前直接上传服务器替换相对麻烦一些

  3. 需要更新时下载全量web模块包会略大,也可以考虑用BSDiff差分算法来做增量更新解决,但是会增加程序复杂度


总结


综上就是本次H5页面秒加载全部的预研过程了,总的来说,基本上可以满足秒加载的需求。没有完美的解决方案,只有合适的方案,有舍有得,根据公司项目情况来。


作者:风轻云淡的搬砖
来源:juejin.cn/post/7236281103221096505
收起阅读 »

话说工作的“边界感”

一句话的合作例子 今天有一个业务的运营同学匆匆忙忙找到我,说要跟我这边有一个问题,业务要合作,然后已经提前和我老板打过招呼了。事情是这样的,我这边负责的是工作台门户,然后作为一个平台业务,有大量的客户需要找到对应的服务商来给自己定制门户。然后这位同学负责的是定...
继续阅读 »


一句话的合作例子


今天有一个业务的运营同学匆匆忙忙找到我,说要跟我这边有一个问题,业务要合作,然后已经提前和我老板打过招呼了。事情是这样的,我这边负责的是工作台门户,然后作为一个平台业务,有大量的客户需要找到对应的服务商来给自己定制门户。然后这位同学负责的是定制业务,所以要统一把所有的定制业务全部收口,但是这位定制同学的业务没有对应的技术研发同学,所以他就找到我的老板同步了这个情况。


分工协作的本质


其实问题的合作方式是比较简单的,但是当她跟我说最终客户定制界面也由我来开发的时候,比如定制的费用是多少、定制的时间要求等等,我就觉得问题有些奇怪了。因为按照常理来说,我负责的是工作台,但是由于有定制业务相关的逻辑,所以我要处理一定的业务逻辑,但是让我去承担这个定制页面的开发,我觉得是有问题的。


举一个简单的例子,假如我现在是一个博物馆,原来用户是直接可以免费没有任何阻挡地进入博物馆的,但是突然有一天市政府说所有公共设施要收费了,那么对于博物馆的工作人员来说肯定是支持的,但是突然你又告诉我,我这个博物馆还要去维护全市统一的收费系统,这个就是不合理的。哪怕他找我的主管沟通结果也是一样,因为我和我的主管是属于博物馆体系的工作人员,他也没有义务和责任去维护整个所有的公共设施的收费系统。但是作为公共设施系统的一部分,如果有统一的收费规则,那么对于博物馆来说也是要遵守的。


所以这面就引出了我对于业务边界上面的一个思考。我经常看到同学给我转发一段话,说跟你老板打沟通了业务的合作情况,你的老板觉得非常不错,于是这位同学就匆匆忙忙的找到我来开始谈业务,谈实施细节并且需要我快速落地。而实际上这种所谓的业务协同的情况大部分也只会停留在沟通的层面,在最终落地的时候,往往和业务同学的预期不相符。在业务同学眼里看来,就是你们阴奉阳违,恨不得马上就开始投诉。


这里面非常核心的一个误区就是业务同学往往没有划清业务界限和系统界限的边界。对于业务同学来说,边界可能不会那么明显,但对于一个系统开发的同学来说,业务和边界是非常明显的,因为系统是物理存在的,有着天然的“隔离”。所以对于业务同学,如果想要顺畅的推动业务,必须要事先清晰的划分参与方的角色和业务边界,并且可以进一步了解到系统边界在哪里。


这个由谁来做就涉及到了一个很大权责问题。简单来说就是我做了有什么好处,换句话来说做这件事和我的职务目标有什么关系?如果没有关系,我为什么要做?就算同一个公司,也有很多需要完成的事,比如公司保洁不到位,我作为公司的员工,是否也立即从事保洁?


如果是我的职务目标,我的责任有多少?我承担了既定的责任,那我是否能够承担起对应的权利?在我上次借用的博物馆的例子可以看到,如果我承担了全市的公共系统的收费设施的维护,那么我的权利在哪里?如果我的权利只是在博物馆这一个地方的收费上面,那么这就变成了权责不对等。


但是如果我做成了全市公共收费系统,并且能掌管全市所有公共设施的收费业务,那么对于这个收费系统的开发权则是相等的,但是对于我本身职务的权责又是不等的,因为公司请我来管理博物馆的,而非管理整个全市的收费系统。


所以在思考业务推进的时候,首先就要思考系统的边界和权责对等关系,如果这一层面没有理清楚的话,合作大概率是不能完成的。而很多的业务同学就以“我和你老板谈好的东西,为什么你不去做”这么简单的方式来拷问协同关系,我觉得是非常的幼稚的。


所以我希望其实我们在去和别人沟通业务的时候,往往要带着权责,带着边界的思考,去和对方去讨论,去协商,去沟通。简单来说,我在跟你聊之前,我要知道你的系统,你的业务边界在哪里?我跟你聊的时候,我要清晰地告诉你,这个事情做了对你有什么好处,对我有什么好处,哪部分应该你做,哪部分应该我来做。只有在这样的一种沟通方式下面才是真正合理的,真正是可以落地的沟通和协作方式。


而在这些问题没有达成一致之前,由谁来做都没有定下来的时候,应该先去往上升,在顶层设计里面去规划去重新思考如何从组织设计的方式去让业务协作自然的发生。


总结


这里再总结一下,这里是一个小的心得。这个案例也告诉我们,我们去沟通协同的时候要有边界感,包括业务的边界和系统的边界。只有把边界理顺了,合作才有可能。


作者:ali老蒋
来源:juejin.cn/post/7233808084085309477
收起阅读 »

韩国程序员面试考什么?

大家好,我是老三,在G站闲逛的时候,从每日热门上,看到一个韩国的技术面试项目,感觉有点好奇,忍不住点进去看看。 韩国的面试都考什么?有没有国内的卷呢? 可以看到,有8.k star,2.2k forks,在韩国应该算是顶流的开源项目了。 再看看贡献者,嗯,...
继续阅读 »

大家好,我是老三,在G站闲逛的时候,从每日热门上,看到一个韩国的技术面试项目,感觉有点好奇,忍不住点进去看看。


韩国的面试都考什么?有没有国内的卷呢?
瘦巴巴的老爷们


可以看到,有8.k star,2.2k forks,在韩国应该算是顶流的开源项目了。


star


再看看贡献者,嗯,明显看出来是韩国人。
贡献者


整体看一下内容。


第一大部分是计算机科学,有这些小类:



  • 计算机组成


计算机组成原理



  • 数据结构


数据结构




  • 数据库
    数据库




  • 网络




网络



  • 操作系统


操作系统


软件工程


先不说内容,韩文看起来也够呛,但是基础这一块,内容结构还是比较完整的。


第二大部分是算法:
算法


十大排序、二分查找、DFS\BFS…… 大概也是那些东西。


第三大部分是设计模式,内容不多。
设计模式


第四大部分是面试题:
面试题


终于到了比较感兴趣的部分了,点进语言部分,进去看看韩国人面试都问什么,随便抽几道看看:
面试题



  • Vector和ArrayList的区别?

  • 值传递 vs 引用传递?

  • 进程和线程的区别?

  • 死锁的四个条件是什么?

  • 页面置换算法?

  • 数据库是无状态的吗?

  • oracle和mysql的区别?

  • 说说数据库的索引?

  • OSI7层体系结构?

  • http和https的区别是?

  • DI(Dependency Injection)?

  • AOP(Aspect Oriented Programming)?

  • ……


定睛一看,有种熟悉的感觉,天下八股都一样么?


第五大部分是编程语言:
编程语言


包含了C、C++、Java、JavaScript、Python。


稍微看看Java部分,也很熟悉的感觉:



  • Java编译过程

  • 值传递 vs 引用传递

  • String & StringBuffer & StringBuilder

  • Thread使用


还有其它的Web、Linux、新技术部分就懒得再一一列出了,大家可以自己去看。


这个仓库,让我来评价评价,好,但不是特别好,为什么呢?大家可以看看国内类似的知识仓库,比如JavaGuide,那家伙,内容丰富的!和国内的相比,这个仓库还是单薄了一些——当然也可能是韩国的IT环境没那么卷,这些就够用了。


再扯点有点没的,我对韩国的IT稍微有一点点了解,通过Kakao。之前对接过Kakao的支付——Kakao是什么呢?大家可以理解为韩国的微信就行了,怎么说呢,有点离谱,他们的支付每天大概九点多到十点多要停服维护,你能想象微信支付每天有一个小时不可用吗?


也有同事对接过Kakao的登录,很简单的一个Oauth2,预估两三天搞定,结果也是各种状况,搞了差不多两周。


可能韩国的IT环境真的没有那么卷吧!


有没有对韩国IT行业、IT面试有更多了解的读者朋友呢?欢迎和老三交流。



对了,仓库地址是:github.com/gyoogle/tec…



作者:三分恶
来源:juejin.cn/post/7162709958574735397
收起阅读 »

初入Android TV/机顶盒应用开发小记1

1.前期 去年公司开展了一个新的项目,是一个运行在机顶盒上的App。项目经理把整个部门的几个重要成员叫到会议室开了场研讨,讨论了关于整个项目的详细情况,但是公司现有做安卓哥们都没有开发过Tv上的项目,所以当时都没有人主动想要负责这个项目的开发。可能在会上我问的...
继续阅读 »

1.前期


去年公司开展了一个新的项目,是一个运行在机顶盒上的App。项目经理把整个部门的几个重要成员叫到会议室开了场研讨,讨论了关于整个项目的详细情况,但是公司现有做安卓哥们都没有开发过Tv上的项目,所以当时都没有人主动想要负责这个项目的开发。可能在会上我问的问题比较多引起了注意。然后就毫无意外的把这个项目客户端开发硬塞了给我负责。我也是醉了。。。


2.准备


在美工(设计师)正画高保真图的这段时间我也开始了研究关于在机顶盒上的一些相关技术储备,也试的写了一些Demo出来。感觉还是阔以拿捏的。但是当前我等到高保真出来的时侯大家一起探讨机顶盒上的一些交互时,我发现我的的相关技术储备还是有点欠缺,没办法,只能先跟着图开始做着。


3.开干


没过这方面开发的哥们可能不知道。开发一个几个按钮外加一个列表的页面如果是手机端的我不用半小时就写完了,但是我在开发TV上的类似的页面时我足足做一了一个多星期。而且产经理看了还是不满意,说这不行,说那焦点有问题。还经常用几个主流的Tv应用在跟我展示说别人也是这么做,也是那么做。


4.遇到问题


就拿一个控件获取焦点时的问题来说吧,别人主流的TV应用里的控件获取焦点显示焦框时,控件里的内容是不是被挤压的。而且有的焦框带有阴影,阴影还会复盖在别的控件之上,也就是说焦点框不占据控件的大小,如果有传统的方式给控件设置src或是background属性来显示焦点框的话是会占据控件原本的大小的。


如图下面三个正方形的控件的宽高都是100dp的,第1个和第2个是可以获取焦点的,第3个是用来作为对比大小的。给第1,2个控件添加了一个selector类型的drawable,作为当控件获取焦点时的的焦点框,很明显可以看到,当获取焦点时第2个控件显示了一个红色的焦点框,但是焦点框却挤压了控件的内容,也就是说焦点框显示在控件100dp之内。像这种方式是有问题的。我们要的是焦点框要显示的控件之外的区域这样就会不占用控件的大小。


7.焦点知识入门-焦点框问题[00_08_02][20230518-190621-4].JPG


5寻找解决方案


顺着这个问题在各大社区寻找解决方案,找到了一种可以把焦点框显示在控件之外的方式。核心就是默认情况下Android的组件可以绘制超出它原本大小之外的区域,但是默认只会显示控件大小之内的区域,如果我把给这个控件的父控件的两个属性设置为false那么就可以进显示控件之外的内容了。而这两个属性就是:


android:clipChildren="false"
android:clipToPadding="false"

接着就要自定义一个控件,这里以ImageView为例,首页要拿到获取焦点框图片的边框大小:


int resourceId = ta.getResourceId(R.styleable.EBoxImageView_ebox_iv_focused_border,
-1);
Rect mFocusedDrawable.getPadding(mFocusDrawableRect);


然后计算出焦点框加点组件之后的整个显示的区域的大小,


private void mergeRect(Rect layoutRect, Rect drawablePaddingRect) {
layoutRect.left -= drawablePaddingRect.left;
layoutRect.right += drawablePaddingRect.right;
layoutRect.top -= drawablePaddingRect.top;
layoutRect.bottom += drawablePaddingRect.bottom;
}

最后再根据这个大小区域绘制焦点框,而绘制内容这一块直接调用super.onDraw(canvas);就可以了。


下面是一个完整的代码:


public class MyImageView extends AppCompatImageView {
private static final String TAG = "EBoxImageView";

private static final Rect mLayoutRect = new Rect();
private static final Rect mFocusDrawableRect = new Rect();

private final static Drawable DEFAULT_FOCUSED_DRAWABLE = ResourceUtils.getDrawable(R.drawable.drawable_focused_border);
private Drawable mFocusedDrawable = DEFAULT_FOCUSED_DRAWABLE;

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

public MyImageView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}

public MyImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(attrs);
}

private void init(AttributeSet attributeSet) {
setScaleType(ScaleType.FIT_XY);

TypedArray ta = getContext().obtainStyledAttributes(attributeSet, R.styleable.MyImageView);
boolean selectorMode = ta.getBoolean(R.styleable.MyImageView_ebox_iv_selected_mode,false);
int resourceId = ta.getResourceId(R.styleable.MyImageView_ebox_iv_focused_border,
-1);
ta.recycle();

if(selectorMode){
setFocusable(false);
}else {
setFocusable(true);
}

if (resourceId != -1) {
mFocusedDrawable = ResourceUtils.getDrawable(resourceId);
}
mFocusedDrawable.getPadding(mFocusDrawableRect);
}


@Override
public void invalidateDrawable(@NonNull Drawable dr) {
super.invalidateDrawable(dr);
invalidate();
}

@CallSuper
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (isFocused()||isSelected()) {
getDrawingRect(mLayoutRect);

mergeRect(mLayoutRect, mFocusDrawableRect);
mFocusedDrawable.setBounds(mLayoutRect);
canvas.save();
mFocusedDrawable.draw(canvas);
canvas.restore();
}

}

/**
* 合并drawable的padding到borderRect里去
*
*
@param layoutRect 当前布局的Rect
*
@param drawablePaddingRect borderDrawable的Rect
*/

private void mergeRect(Rect layoutRect, Rect drawablePaddingRect) {
layoutRect.left -= drawablePaddingRect.left;
layoutRect.right += drawablePaddingRect.right;
layoutRect.top -= drawablePaddingRect.top;
layoutRect.bottom += drawablePaddingRect.bottom;
}

/**
* 指定一个焦点框图片资源,如果不调用此方法默认用,R.drawable.drawable_recommend_focused
*
*
@param focusDrawableRes
*/

public void setFocusDrawable(@DrawableRes int focusDrawableRes) {
mFocusedDrawable = ResourceUtils.getDrawable(focusDrawableRes);
mFocusedDrawable.getPadding(mFocusDrawableRect);
}


总结



以上就是在开发AndroidTV、机顶盒中遇到的焦点框问题的解决方案,后来在CS某N社区中找到一套关于AndroidTV项目开发实战的视频教程看了一下还不错,在其它地方也找不更好的资源。再加上项目实现太赶没有那么多的试错时间成本,然后就报名买了那教程。后来边看边开发,用着这套视频的作者提供的一个UI库,来开发我的项目确定方便快速了很多。现在整套视频教程还没看完,一边学习一边写公司的项目和写博客。


作者:本拉茶
来源:juejin.cn/post/7234447275861475365
收起阅读 »

为什么有些蛮厉害的人,后来都不咋样了

前言 写这篇文章目的是之前在一篇文章中谈到,我实习那会有个老哥很牛皮,业务能力嘎嘎厉害,但是后面发展一般般,这引起我的思考,最近有个同事发了篇腾讯pcg的同学关于review 相关的文章,里面也谈到架构师的层次,也再次引起我关于架构师的相关思考,接下来我们展...
继续阅读 »

前言




写这篇文章目的是之前在一篇文章中谈到,我实习那会有个老哥很牛皮,业务能力嘎嘎厉害,但是后面发展一般般,这引起我的思考,最近有个同事发了篇腾讯pcg的同学关于review 相关的文章,里面也谈到架构师的层次,也再次引起我关于架构师的相关思考,接下来我们展开聊聊吧~


摆正初心




我写这篇文章,初心是为了找到导致这样结果的原因,而不是站在一个高高在上的位置,对别人指手画脚,彰显自己多牛皮。(PS:我也鄙视通过打压别人来展示自己,你几斤几两,大家都是聪明人看得出来,如果你确实优秀,别人还打压,说明他急了,哈哈哈)


查理芒格说过一句话:如果我知道在哪里会踩坑,避开这些,我已经比很多人走得更远了。


思考结果




我觉得是没有一个层级的概念导致的,这个原因筛掉了大部分人,突破层级的难度筛掉了另外一批人,运气和机会又筛掉另一波人。


没有层级概念


为什么这么讲呢?


我们打游戏的时候,比如说王者,会有废铁、青铜、钻石、铂金、荣耀、荣耀王者,对吧。它的层级大家都清楚,但是在现实生活中,你会闷逼了,我当前处在那个阶段,上一层是什么水平,需要什么技能,什么样的要求。


其次也会对自己能力过高的评价,如果你一直在组里面,你可能一直是一把手,到了集团,可能变成10名内,到了公司排名,可能几百名后。我们需要站在一个更高、更全面的角度去了解自己的位置。


出现这种情况也很正常


举个栗子,以前我实习那会,有个老哥业务能力特别强,啥活都干得快,嘎嘎牛皮,这是个背景


如果团队里头你最厉害了,那你的突破点,你的成长点在哪里?


对吧,大家都比你菜了,自然你能从别人身上学习到的就少了,还有一种情况是你觉得你是最厉害的,这种想法也是最要命的,会让你踏步不前。这时的解法,我认为是自驱力,如果你学哲学,就知道向内求,自我检讨,自己迭代更新,别人就是你,你就是别人,别人只是一面镜子。


层级的概念


那时看到他搞业务特别厉害,但现在看是做需求厉害,但是缺乏深度。我对比以前的开发经历,跟现在在架构组的工作经历,感受很明显。一个是为了完成任务,一个需要深度,什么深度呢?这个埋下伏笔,为后面架构师层级再展开聊聊。


从初级到中级,到高级,再到主程、再到TL,技术经理,再到架构师,再到负责人。当完成任务的时候,是最基本的事情,深入的时候,从coding入手,在代码上有所追求,比如说可读性,用用设计模式,再深入想到代码可扩展性。。。



当你了解下一个层级的要求的时候,有了目标才能有效的突破它。



突破层级的难度


这是在上一个原因基础上一个加强版,你了解了各个层级的要求,但是突破这些要求,可能由于阅历,或者能力,或者天赋不足,导致突破困难。


image.png


这里我想聊聊架构师的思考,之前在转正答辩上,一个领导问我你怎么理解架构的,我当时没有概念,但是接触相关工作以及观看相关文章,有了更深理解。



这里讲的是coding部分,属于架构师负责的一部分,规范


我不禁想想平时什么工作内容涉及到这个?


比如说契约,规定了依赖jar版本;定义了协议,什么类型输出的格式,转换的类型;开发的规范,设计文档的样式;像文中review的过程,确实比较少,目的是为了减少代码的坏味道。就像文中讲到,如果你定义的一个规范,可以在300+人里面hold,让系统一直在正常迭代,那么算合格的架构师。


一次广义上review


我一般下班会遇到基础服务的小伙伴聊聊天,我说话很少,就喜欢听听别人聊点什么,他跟我聊了几天,我发现问题是现有商品代码已经不足以支持业务的快速迭代,因为冗余其他东西太多了。比如说一个毛胚商品,然后它也快速的加上其他属性,变成一个加工品。但是现在场景变成了它就是一个加工品,你想拆成其他加工品,很困难,就是字段冗余到商品表里头了。


这个时候到架构已经不适合业务快速迭代了,需要重构,大破大立,还需要大佬牵头。review狭义上是代码层发现问题,如果你从一线同学那里听到的东西,能发现问题,也是一种review。



架构师不止规范,需要深度



需要什么深度呢?


从一个做需求的点看,从需求理解,这个是业务深度,从设计文档上,严谨程度,扩展性、风险点、可行性,设计深度。从开发阶段,coding,技术规范,技术功底,这个是技术深度


跳出需求的点,从大的面来看,需求为了解决什么问题,不做行不行,业务价值在哪里?做了这一期还有后续吗,这是业务的前景。然后规划是怎样的,先从哪里入手,然后有木有计划去推进?这是思考的深度


抽象的能力



里面反复提到抽象的能力,比如说逻辑、物理架构图,这个有助于你理解整个系统的调用关系,形成闭环,可以从全局的角度去理解,我当前做的需求在什么位置,为了解决什么问题。


再到通过问题看到本质,从技术方案看到实质。有一次一位同学跟我探讨DDD,跟我说防腐层不应该是这样写的,你只是用了策略模式去写,应该有个一个门面,然后后面是实现逻辑。我听到这里就明白他被绕进去了,DDD是一个思想,他幻化出来一些对应的框架,它的精髓是高内聚、低耦合。你说策略模式,能否将外部rpc调用分隔开呢?当然可以,它算不算防腐层呢?也算~


最近一次做代码优化的时候,我用了责任链的设计模式,将190行的代码,拆分成4个模块,每个类大概30行,当然190行包括换行。但是实际效果除了行数变少外,每个模块分工特别清晰,这个模块在处理特定的逻辑,当某部分有问题的时候,直接找到那个模块修改即可。(这就是高内聚的魅力)


抽象另一种体现:模块化


最近在牵头做账单,其实我也没做过,我就找了几篇大厂的文章看看,拿来吧你,哈哈


image.png


分为几个步骤,下载账单,解析账单,对账,差异处理(平账)。是不是瞬间有了几个模块,文件模块,包括上传、下载,解析文件对吧。然后是账单模块,可能会分成订单,还有一些退款的,然后是对账处理结果,属于对账模块,文件解析出来的东西跟账单对比,哪些是对的上的,哪些又是异常的,这个模块还有后续的处理结果,自动平账,或者人工处理。



模块化也是高内聚的体现,这就是DDD的思想,只不过人家现在有名头而已~



运气


这个就不展开了,有点玄学,也看投胎,也看老天赏不赏饭吃。我觉得嘛,不管有没有有运气,都要不卑不亢,努力提升自己,很多结果我们决定不了的,但是过程我们可以说了算,人生不也是这样嘛,那就好好享受过程吧~


image.png


最后




《矛盾论》,还是里面的观点,我们需要全面的认识自己的定位,找到自己的优势,不断突破自我。有些厉害,只是暂时性的,而长远来看,只是冰山一角。


作者:大鸡腿同学
来源:juejin.cn/post/7133246541623459847
收起阅读 »

一个28岁程序员入行自述和感受

我是一个容易焦虑的人,工作时候想着跳槽,辞职休息时候想着工作,休息久了又觉得自己每天在虚度光阴毫无意义,似乎陷入了一个自我怀疑自我焦虑的死循环了。我想我该做的点什么去跳出这个循环。。。 自我叙述 我相信,每个人都有一个自命不凡的梦,总觉得自己应该和别人不一样,...
继续阅读 »

我是一个容易焦虑的人,工作时候想着跳槽,辞职休息时候想着工作,休息久了又觉得自己每天在虚度光阴毫无意义,似乎陷入了一个自我怀疑自我焦虑的死循环了。我想我该做的点什么去跳出这个循环。。。


自我叙述


我相信,每个人都有一个自命不凡的梦,总觉得自己应该和别人不一样,我不可能如此普通,自己的一生不应该泯然众生,平凡平庸的度过。尤其是干我们it这一行业的,都有一个自己的程序员梦,梦想着,真的能够用 “代码改变世界”


入行回顾



你们还记得自己是什么时候,入行it行业的吗



我今年已经28岁了,想起来入行,还挺久远的,应该是2016入行的,我也算是半路出家的,中间有过武术梦 歌唱梦 但是电脑什么上学那会就喜欢玩,当然是指游戏,




武术梦




来讲讲我得第一个·梦,武术梦,可能是从小受到武打演员动作电视剧的影响,尤其那个时候,成龙大哥的电影,一直再放,我觉得学武术是很酷的一件事情,尤其那会上小学,还是初中我的体育还是非常好的,


然后我们家那个时候电视还是黑白的,电视机。哈哈哈😀电视台就那么几个,放来放去,有一个台一直重复放成龙电影,还有广告, 都是 学武术就到 xxxx学校, 我被洗脑了吧


于是真的让我爸,打电话质询了一下,可是好像他们这种武术学校都是托管式的,封闭式学习,听说很苦,,,,当然这不是重点,重点每年学费非常的贵,en~,于是乎我的这个梦想终止了,。。




歌唱梦




为啥会有唱歌想法,你猜对了,是被那个时候的好声音给影响了,那个时候好声音是真的很火,看的时候我一度以为我也可以上好声音,去当歌手然后出道,当明星,什么的。


不过不经历打击,怎么会知道自己的下线在哪里呢


我小学换了两到三个学校,到初中,再到高中,你们还记得自己读高中那会吗,高中是有专业选择的,入学军训完以后。


我们代班主任,和我们说有三个专业方向可以选择,艺术类,分美术,和唱歌,然后是文化类,然后艺术类就业考大学分数会低很多,然后一系列原因,哈哈哈,我就选择了歌唱班。


我最好伙伴他选择了,美术类就是素描。这里我挺后悔没有选择 美术类。


到了歌唱班,第一课就是到专业课有钢琴的教室,老是要测试每个同学的,音色和音高,音域
然后各自上台表演自己的拿手的一首歌,。我当时测试时候就是跟着老师的弹的钢琴键瞎唱,


表演的歌曲是张雨生《大海》 也就唱了高潮那么几句。。 😀现在想起来还很羞耻,那是我第一次在那么多人面前唱歌,


后面开始上课老师说我当时分班时候音色什么还不错,但学到后面,我是音准不太行,我发现。再加上我自己的从小感觉好像有点自卑敏感人格,到现在把,我唱歌,就越来越差,


当然我们也有乐理。和钢琴课,我就想主助攻乐理和钢琴,


但是我很天真


乐理很难学习,都是文科知识需要背诵,但是他也要有视唱,也就是唱谱子,duo,re,mi,fa,suo,la,xi,duo。。等,我发现我也学不进去


后面我又开始去学钢琴,但是钢琴好像需要一定童子功,不然可能很难学出来,于是我每天早上6点钟起来,晚上吃完饭就去钢琴教师抢占位置, 还得把门堵着怕人笑话,打扰我,


结果你们也猜到了,音乐方面天赋很重要,然后就是性格上面表演上面,要放得开,可是我第一年勉强撑过去了,后面第二年,专业课越来越多了,我感觉我越来越自卑~,然后成绩就越来越差,老师也就没太重视,嗯~好不容撑到了第二年下半年,放暑假,


但是老师布置任务暑假要自己去外面练钢琴,来了之后要考试,我还花钱去外面上了声乐课钢琴课,哎,我感觉就是浪费钱,,,,,因为没什么效果,性格缺陷加上天赋不行,基本没效果,那段时间我也很痛苦的,因为越来越感觉根本容入不进去班级体,尤其是后面高二,了专业课很多大部分是前面老师带着发生开嗓,后面自由练习,我也不好意思,不想练习,所以
到后面,高二下学习我就转学了,,,,


当然我们班转学的,不止我一个,还有一个转学的 和我一个寝室的,他是因为音高上不去,转到了文科班, 还有一个是挺有天赋,我挺羡慕的,但是人家挺喜欢学习,不喜欢唱歌什么,就申请转到了,文科班。 不过她转到文科班,没多久也不太好,后面好像退学了,,我一直想打听他的消息,都在也没打听到了




玩电脑




我对电脑的组装非常感兴趣,喜欢研究电脑系统怎么装,笔记本拆装,台式机拆装,我会拿我自己的的笔记本来做实验,自己给自己配台式机,自己给自己笔记本增加配置,哈哈哈哈。对这些都爱不释手。



这还是我很早时候,自己一点一点比价,然后去那种太平洋电脑城,电脑一条街,那种地去找人配置的。想想那时候配置这个电脑还挺激动,这是人生的第一台自己全部从零开始组装配的电脑,


本来打算,后面去电脑城上班,开一个笔记本维修,电脑装配的门面的,(因为自己研究了很多笔记本系统,电脑组装),可是好像听电脑城的人说,电脑组装什么的已经不赚钱了,没什么价格利润,都是透明的而且更新迭代非常的快,电脑城这种店铺也越来越少了,都不干了,没有新人再去干这个了,于是乎我的第一份工作失业 半道崩殂了,哈哈哈哈还没有开始就结束了。




学it




后面我又报名自学了,it编程,《xxx鸟》 但是学it我学起来,好像挺快的,挺有感觉的,入学前一个星期,要等班人数到齐才能开班,我们先来的就自己学习打字了,我每天都和寝室人,一起去打字,我感觉那段时间我过得挺开心和充实的,


后面我们觉得自带寝室不好,环境差,于是就几个人一起,搬出去住了,一起学习时候有一个年级26了,我和他关系还蛮好的,不过现在也没什么联系了,,,


学习时候,每次做项目时候我都是组长,那个时候原来是有成就感的,嗯,学习it好像改变了,我学唱歌那个时候,一些自卑性格,可能是遇到了一个好的老师吧


当然后面就顺利毕业,然后找到了工作了,,,


直到现在我还在it行业里


作者:程序员三时
来源:juejin.cn/post/7230351646798643255
收起阅读 »

如何告诉后端出身的领导:这个前端需求很难实现

本文源于一条评论。 有个读者评论说:“发现很多前端的大领导多是后端。他们虽然懂一点前端,但总觉得前端简单。有时候,很难向其证明前端不好实现。” 这位朋友让我写一写,那我就写一写。 反正,不管啥事儿,我高低都能整两句,不说白不说,说了也白说。本文暂且算是一条评...
继续阅读 »

本文源于一条评论。


test.png


有个读者评论说:“发现很多前端的大领导多是后端。他们虽然懂一点前端,但总觉得前端简单。有时候,很难向其证明前端不好实现。


这位朋友让我写一写,那我就写一写。


反正,不管啥事儿,我高低都能整两句,不说白不说,说了也白说。本文暂且算是一条评论回复吧。愿意看扯淡的同学可以留一下。


现象分析


首先,前半句让我很有同感。因为,我就是前端出身,并且在研发管理岗上又待过。这个身份,让我既了解前端的工作流程,又经常和后端平级一起交流经验。这有点卧底的意思。


有一次,我们几个朋友聚餐闲聊。他们也都是各个公司的研发负责人。大家就各自吐槽自己的项目。


有一个人就说:“我们公司,有一个干前端的带项目了,让他搞得一团糟”


另一个人听到后,就叹了一口气:“唉!这不是外行领导内行吗?!


我当时听了感觉有点别扭。我扫视了一圈儿,好像就我一个前端。因此,我也点了点头(默念:我是卧底)。


是啊,一般的项目、研发管理岗,多是后端开发。因为后端是业务的设计者,他掌握着基本的数据结构以及业务流转。而前端在他们看来,就是调调API,然后把结果展示到页面上。


另外,一般项目遇到大事小情,也都是找后端处理。比如手动修改数据、批量生成数据等等。从这里可以看出,项目的“根基”在后端手里。


互联网编程界流传着一条鄙视链。它是这样说的,搞汇编的鄙视写C语言的,写C的鄙视做Java的,写Java的鄙视敲Python的,搞Python的鄙视写js的,搞js的鄙视作图的。然后,作图的设计师周末带着妹子看电影,前面的那些大哥继续加班写代码。


当然,我们提倡一个友好的环境,不提倡三六九等。这里只是调侃一下,采用夸张的手法来阐述一种现象。


这里所谓的“鄙视”,其本质是源于谁更接近原理。


比如写汇编的人,他认为自己的代码能直接调用“CPU”、“内存”,可以直接操纵硬件。有些前端会认为美工设计的东西,得依靠自己来实现,并且他们不懂技术就乱设计,还有脸要求100%还原。


所以啊,有些只做过后端的人,可能会认为前端开发没啥东西。


好了,上面是现象分析。关于谁更有优越感,这不能细究啊,因为这是没有结论的。如果搞一个辩论赛,就比方说”Python一句话,汇编写三年“之类论据,各有千秋。各种语言既然能出现,必定有它的妙用。


我就是胡扯啊。不管您是哪个工种,请不要对号入座。如果您觉得被冒犯了,可以评论“啥都不懂”,然后离开。千万不要砸自己的电脑。


下面再聊聊第二部分,面对这种情况,有什么好的应对措施?


应对方法


我感觉,后端出身的负责人,就算是出于人际关系,也是会尊重前端开发的


“小张啊,对于前端我不是很懂。所以请帮我评估下前端的工期。最好列得细致一些,这样有利于我了解人员的工作量。弄完了,发给我,我们再讨论一下!”


一般都是这么做。


这种情况,我们就老老实实给他上报就行了,顶多加上10%~30%处理未知风险的时间。


但是,他如果这样说:“什么?这个功能你要做5天?给你1天都多!


这时,他是你的领导,对你又有考核,你怎么办?


你心里一酸:“我离职吧!小爷我受不了这委屈!”


这……当然也可以。


如果你有更好的去处,可以这样。就算是没有这回事,有好去处也赶紧去。


但是,如果这个公司整体还行呢?只是这个直接领导有问题。那么你可以考虑,这个领导是流水的,你俩处不了多长时间。哪个公司每年不搞个组织架构调整啊?你找个看上的领导,吃一顿饭,求个投靠呗。


或者,这个领导只是因为不懂才这样。谁又能样样精通呢?给他说明白,他可能就想通了。人是需要说服的。


如果你奔着和平友好的心态去,那么可以试试以下几点:


第一,列出复杂原因


既然你认为难以实现,那肯定是难以实现。具体哪里不好实现,你得说说


记得刚工作时,我去找后端PK,我问他:“你这个接口怎么就难改了?你不改这个字段,我们得多调好几个接口,而且还对应不起来!”


后端回复我:“首先,ES……;其次,mango……;最后,redis……”


我就像是被反复地往水缸中按,听得”呜噜呜噜“的,一片茫然。


虽然,我当时不懂后端,但我觉得他从气势上,就显得有道理


到后来,还是同样的场景。我变成了项目经理,有一个年轻的后端也是这么回复我的。


我说:“首先,你不用……;其次,数据就没在……;最后,只需要操作……”。他听完,挠了挠头说,好像确实可以这么做。


所以,你要列出难以实现的地方。比如,没有成熟的UI组件,需要自己重新写一个。又或者,某种特效,业内都没有,很难做到。再或者,某个接口定得太复杂,循环调组数据,会产生什么样的问题。


如果他说“我看到某某软件就是这样”。


你可以说,自己只是一个初级前端,完成那些,得好多高级前端一起研究才行。


如果你懂后端,可以做一下类比,比如哪些功能等同于多库多表查询、多线程并发问题等等,这可以辅助他理解。不过,如果能到这一步,他的位子好像得换你来坐。


第二,给出替代方案


这个方案,适用于”我虽然做不了,但我能解决你的问题“。


就比如那个经典的打洞问题。需求是用电钻在墙上钻一个孔,我搞不定电钻,但是我用锤子和钉子,一样能搞出一个孔来。


如果,你遇到难以实现的需求。比如,让你画一个很有特色的扇形玫瑰图。你觉得不好实现,不用去抱怨UI,这只会激化问题。咱可以给他提供几个业界成熟的组件。然后,告诉他,哪里能改,都能改什么(颜色、间距、字体……)。


我们有些年轻的程序员很直,遇到不顺心的事情就怼。像我们大龄程序员就不这样。因为有房贷,上有老下有小。年龄大的程序员就想,到底是哪里不合适,我得怎样通过我的经验来促成这件事——这并不光彩,年轻人就得有年轻人的样子。


第二招是给出替代方案。那样难以实现,你看这样行不行


第三,车轮战,搞铺垫


你可能遇到了一个硬茬。他就是不变通,他不想听,也不想懂,坚持“怎么实现我不管,明天之前要做完”。


那他可能有自己的压力,比如老板就是这么要求他的。他转手就来要求你。


你俩的前提可能是不一样。记住我一句话,没有共同前提,是不能做对比的。你是员工,他是领导,他不能要求你和他有一样的压力,这就像你不能要求,你和他有一样的待遇。多数PUA,就是拿不同的前提,去要求同样的结果。


那你就得开始为以后扯皮找铺垫了。


如果你们组有多个前端,可以发动大家去进谏。


”张工啊,这个需求,确实不简单,不光是小刘,我看了,也觉得不好实现“


你一个人说了他不信,人多了可能就信了。


如果还是不信。那没关系,已经将风险提前抛出了


“这个需求,确实不好实现。非要这么实现,可能有风险。工期、测试、上线后的稳定性、用户体验等,都可能会出现一些问题”


你要表现出为了项目担忧,而不是不想做的样子。如果,以后真发生了问题,你可以拿出“之前早就说过,多次反馈,无人理睬”这类的说辞。


”你居然不想着如何解决问题,反倒先想如何逃避责任?!“


因此说,这是下下策。不建议程序员玩带有心机的东西。


以上都是浅层次的解读。因为具体还需要结合每个公司,每个领导,每种局面。


总之,想要解决问题,就得想办法


作者:TF男孩
来源:juejin.cn/post/7235966417564532794
收起阅读 »

程序员你为什么迷茫

今天在知乎收到一个问题:「程序员你为什么迷茫?」,看到这问题时突然间「福灵心至」想写点什么,大概是好久没写这种「焦虑文」,想想确实挺久没更新《程序员杂谈》这个专栏了,那就写点「找骂」的东西吧。 其实也不算「贩卖焦虑」,主要是我一直不卖课也不做广告,就是纯粹...
继续阅读 »

今天在知乎收到一个问题:「程序员你为什么迷茫?」,看到这问题时突然间「福灵心至」想写点什么,大概是好久没写这种「焦虑文」,想想确实挺久没更新《程序员杂谈》这个专栏了,那就写点「找骂」的东西吧。




其实也不算「贩卖焦虑」,主要是我一直不卖课也不做广告,就是纯粹想着写点东西,不过如果你看了之后觉得「焦虑」了,那也没有「售后服务」。



当我看到「程序员你为什么迷茫?」这个问题的时候,我第一想法居然是:大概是因为预期和结果不一致


现在想想确实好像是这样,在步入职场之前,你一直以为程序员是一个技术岗,是用一双手和一台电脑,就可以改变世界的科技岗位,是靠脑吃饭,至少在社会地位上应该是个白领。


但是入职后你又发现,明明是个技术岗位,干的却是体力活,别人在工地三班倒,而你是在电脑前 996,唯一庆幸的是你可以吹着空调,目前的收入还挺可观。



但是现在看来,程序员的职业生涯好像很短,农民工少说可以干到 40 多,为什么程序员 35 就需要毕业再就业?明明一心一意搬砖,说好奋斗改变人生,最后老板却说公司只养有价值的人,而有价值的人就是廉价而又精力充沛的年轻人


那时候你也开始明白,好像努力工作 ≠ 改变人生,因为你努力改变的是老板的人生,工作带来的自我提升是短暂的,就像工地搬砖,在你掌握搬砖技巧之后,剩下的都只是机械性的重复劳动,机械劳动的勤奋只会带来精神上的奋斗感,并不致富,而对比工地,通过电脑搬砖需要的起点更高,但是这个门槛随着技术成熟越来越低,因为搜索引擎上的资源越来越多,框架和社区越来约成熟,所以🧱越来越好拿,工价也就上不去了,甚至已经开始叫嚣用 AI 自动化来代替人工。



其实对于「老人」来说,这个行业一开始不是这样,刚入行的时候到处在抢人,他们说这是红利期,那时候简历一放出来隔天就可以上岗,那时候的老板每天都强调狼性,而狼需要服从头领,只有听话的狼才有肉吃,所以年轻时总是充满希望,期待未来可以成为头狼,也能吃上肉住炕头。


虽然期间你也和老板说过升职加薪,但是老板总是语重心长告诉大家:



年轻人不好太浮躁,你还需要沉淀,公司这是在培养你,所以你也要劳其筋骨,饿其体肤,才能所有成就,路要一步一走,饭要一步一吃,总会有的。



当然你可以看到了一些人吃到了肉,所以你也相信自己可以吃到肉,因为大家都是狼,而吃到肉的狼也在不停向你传递吃肉经验:



你只需要不停在电梯里做俯卧撑,就可以让电梯快一点到顶楼,从而占据更好的吃肉位置,现在电梯人太多了,你没空间做俯卧撑,那就多坐下蹲起立,这样也是一种努力。




直到有一天,公司和你突然和你说:



你已经跟不上公司的节奏了,一个月就请了三次病假,而且工作也经常出错,所以现在需要你签了这份自愿离职协议书,看在你这么多年的劳苦功高,公司就不对你做出开除证明,到时候给其他人也是说明你是有更好机会才离职的,这样也可以保全你的脸面。



而直到脱离狼群之后你才明白,原来沉淀出来的是杂质,最终是会被过滤掉,电梯空间就那么点,超重了就动不了,所以总有人需要下来


所以你回顾生涯产生了疑惑和迷茫:程序员不是技术岗位吗?作为技术开发不应该是越老越值钱吗?为什么经验在 3-5 年之后好像就开始可有可无,而 35 岁的你更是被称为依附在企业的蛀虫,需要给年轻人让路。


回想刚入行那会,你天天在想着学习技术,无时无刻都在想着如何精通和深入,你有着自己的骄傲,想着在自己的领域内不断探索,在一亩三分地里也算如鱼得水,但是如今好像深入了,为什么就开始不被需要了


回过头来,你发现以前深入研究的框架已经被替代,而当年一直让他不要朝三暮四嚼多不烂的前辈,在已经转岗到云深不知处,抱着一技之长总不致于饿死是你一直以来的想法,但是如今一技之长好像补不上年龄的短板。



如今你有了家庭,背负贷款,而立之年的时候,你原以为生涯还只是开始,没想到早已过了巅峰,曾经的你以为自己吃的技术饭,做的是脑力活,壮志踌躇对其他事务不屑一顾,回过头来,如今却是无从下手,除了在电脑对着你熟悉的代码,你还能做什么?放下曾经的骄傲,放下以往的收入去吃另一份体力活?



但是不做又能怎样?提前透支的未来时刻推着你在前行,哪怕前面是万丈深渊。



所以以前你认为技术很重要,做好技术就行了,但是现在看,也许「技术」也只是奇技淫巧之一,以前你觉得生育必须怀胎十月,但是现在互联网可以让十个孕妇怀胎一月就早产,这时候你才发现,你也没自己想象中的重要。


所以你为什么迷茫?因为到了年龄之后,好像做什么都是错的,你发现其实你并没有那么热爱你的职业,你只想养家糊口,而破局什么的早就无所谓了,只要还能挣扎就行。



所以我写这些有什么用?没什么用,只是有感而发,大部分时候我们觉得自己是一个技术人才,但是现在看来技术的门槛好像又不是那么的高,当技术的门槛没那么高之后,你我就不过是搬砖的人而已,既然是搬砖,那肯定是年轻的更有滋味


所以,也许,大概,是不是我们应该关心下技术之外的东西?是不是可以考虑不做程序员还能做什么?35 岁的人只会越来越多,而入行的年轻人也会越来越多,但是近两年互联网的发展方向看起来是在走「降本增效」,所以这时候你难道不该迷茫一下?


程序员是迷茫或者正是如此,我们都以为自己是技术人,吃的是脑力活,走的是人才路,但是经过努力才知道,也许你的技术,并没有那么技术。


作者:恋猫de小郭
来源:juejin.cn/post/7236668944340779063
收起阅读 »

28岁小公司程序员,无车无房不敢结婚,要不要转行?

大家好,这里是程序员晚枫,又来分享程序员的职场故事了~ 今天分享的这位朋友叫小青,我认识他2年多了。以前从事的是土木行业,2年前找我咨询转行程序员的学习路线和职业规划后,通过自学加入了一家创业公司,成为了一名Java开发。 **最近他遇到了新的职业上的困惑,又...
继续阅读 »

大家好,这里是程序员晚枫,又来分享程序员的职场故事了~


今天分享的这位朋友叫小青,我认识他2年多了。以前从事的是土木行业,2年前找我咨询转行程序员的学习路线和职业规划后,通过自学加入了一家创业公司,成为了一名Java开发。


**最近他遇到了新的职业上的困惑,又找我聊了一下,我也没想到好的解决方法,**大家可以一起看一下~下面是沟通的核心内容。


1、他的问题


小青是中原省份省会城市的大专毕业,毕业季就去了帝都实习和工作。后来发现同学中有转行程序员的,薪资很诱惑,所以就找到我咨询如何学习和转行,现在一家帝都创业公司负责后端开发。工资1w出头。


今年已经28岁了,有一个女朋友,最近女方家里催他结婚,他自己也有结婚的意愿。但是考虑到自己人在大城市,无车无房,创业公司的工作也不稳定,以后吃住花销,结婚后养孩子的花销,再看看自己1w多的工资,女朋友做财务,一个月到手不到1w。


双方家里也都是普通家庭,给不了什么实质的资助,靠自己目前的收入根本不敢想象成家后压力有多大。


所以目前非常迷茫, 不知道自己在28岁这个年龄应该怎么办,应不应该成家?应该怎样提高收入?


虽然自己很喜欢程序员这份工作,但是感觉自己学历不好,天花板有限,程序员还能继续干下去吗?


2、几个建议


平时收到后台读者的技术问题或者转行的困惑,我都会尽力给一些详尽的回复。


但是这次听到小青的问题,说实话,我也不知道该说什么。


在28岁这种黄金年龄,想去大城市奋斗一番也是人之常情,但因为现实的生活压力,却不得不面临着选择离开大城市或者转行到自己不喜欢但是更务实的职业里去。


如果想继续留在帝都,我能想到的有以下几个办法:



  • 首先,如果想继续从事程序员工作,努力提高收入。最快的办法就是跳槽,已经工作2年多了,背一背八股文,总结一下项目经验,应该是可以跳槽到一家更好的公司了。

  • 其次,探索另一个副业收入,例如自媒体。因为我自己就是通过在各个平台开通了自媒体账号:程序员晚枫,分享自己的程序员学习经验获得粉丝,进而得到自媒体收入的。小青也可以实事求是的分享一下自己大专毕业从建筑工作转行到程序员的经验,应该也能帮助到别人。

  • 最后,努力提高学历,想继续在程序员这行卷出高收入,趁着年轻,获得一个本科或者本科以上的学历还是很有帮助的。


受限于自己的经验,我只能给出以上几个建议了。


大家还有什么更有效的建议,欢迎在评论区交流~


3、写在最后


说句题外话,很多人都觉得程序员工资高想来转行,但其实程序员和其它行业一样,高收入的只是一小部分,而且那部分人既聪明又努力。


最重要的是,高收入的那部分人里,大部分都不是转行的,而是在一个专业深耕了多年,最终获得了应有的报酬。


无意冒犯,但听完小青的经历,我依然要给大专以下,想转行程序员拿高薪的朋友提个醒:如果不是十分热爱,请务必三思~


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

环信React-UIKIT 库使用指南,瞬间实现即时通讯功能!

一、前言为了加快即时通讯需求产品开发速度,将更多的时间放在关系核心业务逻辑的处理上,环信正式推出可扩展,易使用的 React 版本的 UIKIT 库!此 UIKit 库 是基于 环信 Web SDK 开发的的 UI 组件库,它提供了常用的 UI 组件、包含聊天...
继续阅读 »

一、前言

为了加快即时通讯需求产品开发速度,将更多的时间放在关系核心业务逻辑的处理上,环信正式推出可扩展,易使用的 React 版本的 UIKIT 库!

此 UIKit 库 是基于 环信 Web SDK 开发的的 UI 组件库,它提供了常用的 UI 组件、包含聊天业务逻辑的模块组件以及容器组件,允许用户使用 renderX 方法进行自定义。UIKIT 库 还提供了一个 provider 来管理数据,该 provider 自动监听 chat SDK 事件以更新数据并驱动 UI 更新。开发人员可以使用此库根据实际业务需求快速构建定制的即时通讯应用程序。


二、前置技能

  • 了解 React 框架基本使用
  • 了解环信 SDK 基本功能
  • 了解 Mobox 状态管理库基本使用

三、快速开始

1、创建空白项目
在自己的终端执行命令使用 vite 模版创建一个 react+typescript 项目
有同学可能会问为什么不用 react-cli 创建一个基于 webpack 的模板项目?只是单纯的觉得 vite 快相对也是比较好用,但是 webpack 搭建的项目也会遇到一个问题具体问题后面描述。

Vite 模版地址:
https://vitejs.cn/guide/#scaffolding-your-first-vite-project

终端命令
yarn create vite my-react-app --template react-ts


2、试运行空白项目
终端执行yarn install初始化项目依赖
终端执行yarn run dev启动项目,检查是否正常运行。
上述两部没有问题则清除模版默认代码,并重新运行检查是否正常。

3、安装 UIKIT 库
终端命令
yarn add chatuim2

4、注册全局 Provider 组件

1、App.tsx中引入Provider组件
import { Provider } from 'chatuim2';


2、引入 UIkit 库中的样式引入到App.tsx
import 'chatuim2/style.css';


3、App.tsx给一个根 dom 元素作为容器并给与默认样式。
function App() {
return
;
}
/* App.css */
.app_container {
width: 100%;
}


4、注册Proveider组件,并传入 appKey
appKey 是在环信注册并创建应用项目生成的,具体可以参考该文档

import { Provider } from 'chatuim2';
import { APPKEY } from './config';
import './App.css';
import 'chatuim2/style.css';
function App() {
return (
<div className='app_container'>
<Provider initConfig={{ appKey: APPKEY }}></Provider>
</div>
);
}

export default App;


5、手动建立与环信的连接。
在项目目录src下创建views文件夹,并在views下新建一个main文件夹
main中从 UIKIT 库中引入 useClient(此为 UIKIT 中内置使用 IM 实例的 hook),并处理 IM 登录(这一步非常重要,因为之后登录成功之后,后续所有操作方可有效。)

import { useEffect } from 'react';
/* EaseIM */
import { useClient } from 'chatuim2';
const Main = () => {
const client = useClient();
useEffect(() => {
client &&
client
.open({
user: '环信ID号',
pwd: '环信ID密码',
})
.then((res: any) => {
console.log('get token success', res);
});
}, [client]);
return
;
};

export default Main;

6、引入 Conversation 会话 UI 组件
views下新建一个conversation组件,作为会话列表组件的容器。

import { ConversationList } from 'chatuim2';
import './index.css';
const Conversation = () => {
return (
<div className='conversation_container'>
<ConversationList />
</div>
);
};
export default Conversation;



main中引入并注册conversation组件。

import { useEffect } from 'react';
/* EaseIM */
import { useClient } from 'chatuim2';
/* components */
import Conversation from '../conversation';
const Main = () => {
const client = useClient();
useEffect(() => {
client
&&
client
.open({
user
: 'hfp',
pwd
: '1',
})
.then((res: any) => {
console
.log('get token success', res);
});
});
}, [client]);
return (
<div className='main_container'>
<Conversation />
</div>
);
};

export default Main;



不要忘了main组件同样需要在 App.tsx 根组件下进行注册
import { Provider } from 'chatuim2';
import { APPKEY } from './config';
import './App.css';
import 'chatuim2/style.css';
import Main from './views/main';
function App() {
return (
<div className='app_container'>
<Provider initConfig={{ appKey: APPKEY }}>
<Main />
</Provider>
</div>
);
}

export default App;


7、引入 Chat 聊天 UI 组件
流程与上面会话组件的引入类型创建一个名为chatContainer组件作为Chat组件容器。

import { Chat } from 'chatuim2';
import './index.css';
const ChatContainer = () => {
return (
<div className='emChat_container'>
<Chat />
</div>
);
};

export default ChatContainer;


同样需要在main组件中引入注册
import { useEffect } from 'react';
/* EaseIM */
import { useClient } from 'chatuim2';
/* components */
import Conversation from '../conversation';
import ChatContainer from '../chatContainer';
const Main = () => {
const client = useClient();
useEffect(() => {
client &&
client
.open({
user: 'hfp',
pwd: '1',
})
.then((res: any) => {
console.log('get token success', res);
});
}, [client]);
return (
<div className='main_container'>
<Conversation />
<ChatContainer />
</div>
);
};

export default Main;


8、启动运行看看效果



四、遇到的问题
使用 react-cli 创建的项目,在注册Provider组件时出现截图报错。


解决方式

目前尝试的解决方式是,安装babel-loadersource-map-loader执行yarn ejest展现 webpack 相关配置,并在webpack.config.js中,babel-loader下增加sourceType: 'unambiguous',这段代码。

相关配置代码如下面示例:
{
test: /\.(js|mjs|jsx|ts|tsx)$/,
include: paths.appSrc,
loader: require.resolve('babel-loader'),
options: {
customize: require.resolve(
'babel-preset-react-app/webpack-overrides'
),
sourceType: 'unambiguous',
presets: [
[
require.resolve('babel-preset-react-app'),
{
runtime: hasJsxRuntime ? 'automatic' : 'classic',
},
],
],

plugins: [
isEnvDevelopment &&
shouldUseReactRefresh &&
require.resolve('react-refresh/babel'),
].filter(Boolean),
// This is a feature of `babel-loader` for webpack (not Babel itself).
// It enables caching results in ./node_modules/.cache/babel-loader/
// directory for faster rebuilds.
cacheDirectory: true,
// See #6846 for context on why cacheCompression is disabled
cacheCompression: false,
compact: isEnvProduction,
},
},





七、相关可参考文档地址
UIKIT 源码地址:https://github.com/easemob/Easemob-UIKit-web
环信 Web 端开发文档:http://docs-im-beta.easemob.com/document/web/quickstart.html
该示例源码地址:https://github.com/HuangFeiPeng/react-uikit-demo-test

小结
该 UIKIT 组件新鲜出炉,如果有兴趣可以进行下载体验,另有一些不足之处,也虚心接受大家批评指正,如果能共享你的代码那真是再好不过,诚信邀请你提交你的 PR。

另有其他使用问题请在评论区友好交流。

收起阅读 »