注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

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。

另有其他使用问题请在评论区友好交流。

收起阅读 »

时隔2年终于开源了基于RecyclerView的阅读器动画方案

Tips:这是一次针对广告业务场景下阅读器动画实现方案的探索。实现效果 项目地址:github:BookViewapp module:完整的阅读器demogpu_test module:独立的仿真动画demo手绘一张图,呈现实现原理如果用文字来阐述原...
继续阅读 »

Tips:这是一次针对广告业务场景下阅读器动画实现方案的探索。

实现效果

image.png 项目地址:github:BookView

  • app module:完整的阅读器demo
  • gpu_test module:独立的仿真动画demo

手绘一张图,呈现实现原理

如果用文字来阐述原理难免要长篇大论,何况这里涉及到Z轴View堆叠,借此机会展示下我的绘画能力吧🐶(瞎搞)

核心类就4个,它们的职责跟它们的名字很相近。

  • BookView 摆放 BookRecyclerView 与PuppetView
  • BookRecyclerView 作为底层容器,接受滑动事件,完成页面更换与事件分配
  • PaperLayout 作为页面卡片根布局,也就是设置给RecyclerView.Adapter加载的布局
  • PuppetView 但本身不处理任何事件,只是展示动画 

这东西前后花了得有2周的时间,从0开发了仿真动画,涉及到一些三角函数,相似三角形还专门复习了初中的相关课程。RecycleView,LayoutManager也是那时候深入学习的。

写在最后

当时会下这么多功夫去开发这个阅读器,其真正的原因是我当时还负责阅读器+广告两个业务的产品设计。没错就是跟很多创业团队一样一人肩多职,也正是这份对产品的追求才推着我不断追求更好的产品体验,进而才有了更多的技术积累。直到现在,偶尔在团队人手紧张时我还会负责一些产品设计工作。

很多时候会分不清面前的挑战是挫折还是机遇,但只要沉下心去解决问题,结果就不会都是坏的。

我很喜欢的一句话「铁打的个人能力,流水的公司」用在这里相当合适。

在2年前做阅读器产品时发过一篇文章:LayoutManager实现翻页动画 - 掘金,目的是抛出一个解决方案的同时寻求不同的思路。同时在V2上发过一篇帖子:http://www.v2ex.com/t/694298#re…, 收集到了不同的灵感。

2年前就有人跟我要完整demo,当时我也很想放出来,但出于职业素养维护公司利益没有这样做。可就在前两天有个掘金的同学很真诚的再次跟我询问这个方案,妥妥的点燃了我回馈社区的心呀。现在那个项目垮了,公司也....。那我可没啥负担了,完整的开源出来,希望能帮助一些与他一样碰到阻碍的人,相互成长。我一直也受到了很多社区帮助,包括Google GDE,这算是我的一点回馈吧。

清理好后优先发给了他,晚上看到他给我反馈的视频,还是蛮开心的。

之前我有几次想要好好整理下做个开源,但最终都搁置了。现在想想还是因为那时候纯粹是出于功利心吧。当时想着这东西还是有点门槛的,微信读书做到50fps,我做到40fps+还支持广告,放出来赚点star不是很轻松,可见虚荣对人的激励是很有限的。现在的我也更愿意做一些获取深度快乐的事情。


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

入坑两个月自研创业公司

一、拿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月准备下,能进就进,不能再在考公上花费太多时间了。


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

金三银四好像消失了,IT行业何时复苏!

疫情时候不敢离职,以为熬过来疫情了,行情会好一些,可是疫情结束了,反而行情更差了, 这是要哪样 我心中不由一万个 草泥🐴 路过 我心中不惊有了很多疑惑和感叹 接着上一篇 一个28岁程序员入行自述和感受 自我10连问 我的心情 自去年下半年以来,互联网行业一片...
继续阅读 »

疫情时候不敢离职,以为熬过来疫情了,行情会好一些,可是疫情结束了,反而行情更差了,
这是要哪样 我心中不由一万个 草泥🐴 路过



我心中不惊有了很多疑惑和感叹 接着上一篇


一个28岁程序员入行自述和感受


自我10连问


我的心情


自去年下半年以来,互联网行业一片寒冬传言,众多企业倒闭,裁员。本以为随着疫情、春季和金融楼市的回暖,一切都会变好。然而,站在这个应该是光明的时刻,举世瞩目的景象却显得毫无生气。令人失望的是,我们盼望已久的春天似乎仍未到来。


我的工作生涯


我已经从业近十年,然而最近两年一直在小公司中工作,



我的技术和经历并不出色。随着年龄的增长,是否我的技能也在快速提高呢?我们该如何前进呢 ,转产品,产品到达极限,转管理,可是不会人情事故,



我们该如何继续前进呢?目前还没有人给出答案。


第一家公司


我记得那是很早的时候了,那个时候简历投递出去,就马上会收到很多回复,不像现在 ,
失联招聘前程堪忧boss直坑


你辛苦的写完简历,满怀期待投递了各大招聘平台,可等来的 却是已读未回,等的心也凉透了。


好怀念之前的高光时刻 神仙打架日子


前面我面试 几乎一周都安排满了,都面试不过来,我记得那会最多时候一天可以跑三家面试哈哈哈,也是很拼命的,有面试机会谁不想多试试呢


我第一家进入的是一个外包公司,叫xxx东软集团, 那个时候也不不懂,什么是外包给公司,只看工资给的所有offer中最高的,然后就去了哈哈哈哈。


入职第一天,我背着我自己的电脑满怀着激动就去了,然后被眼前一幕吸引了,办公的人真多啊,办公室都是拿透明玻璃隔开那种,人挺多,我一想公司还挺大的,
随后我就被带到也是一个玻璃格子办公室,里面就三个人,加我一个4个。


我害怕极了,这个时候一个稍微有一些秃顶的 大叔过来了 哈哈哈(内心台词,早就知道这一行干就了,会秃头这不会就是下一个我把


他把我安排在了靠近玻璃门的也就是大门位置,这是知道我准备随时跑路节奏吗。然后就去忙他自己的了。整个上午我就像是一个被遗忘在角落里的人一样。根本没人管我,就这样第一天结束了,我尴尬了做了一整天。


这工作和我想象的有点不太一样啊!


后面第三天还是如此,办公室里依旧是那么几个人,直到第四天,大叔来了,问我直到多线程吗,让我用多线程写一个抽奖的活动项目。(内心我想终于有事情干了,可是也高兴不起来谁知道怎么写)


不过好在,他也没有说什么时候交,只是说写完了给他看一下,经过我几天的,复制粘贴工程师 一顿谷歌,百度,终于是勉强写出来了。。。。。


后面,就又陆陆续续来了几个小伙伴,才开始新项目和开会,第一份工作大致就是这样开始了我的职业生涯。怎么说呢和我想象的有所不一样,但又有一些失望。


后面干了1年多,我就离职了原因是太累了没时间休息,一个项目接着一个项目的


第二家公司


在离开第一家公司时候,我休息了好长一段时间,调整了我自己的状态


也了解了什么是外包公司,什么是工作外派,也是我这一次就在投递简历,和面试时候刻意去避免进那种外包,和外派公司。


面试什么也还算顺利,不到半个月就拿到了offer。 但是工资总体来说比上一家是要少一点,但是我也接受了,是一家做本地生鲜电商公司,,本来生活科技有公司, 我觉得公司氛围,和公司都挺不错的,就入职了。


入职了我们那个项目经理还算很热情的,让同事帮我开git账号,开了邮箱,我自己拉取了公司项目,然后同事帮我运行调试环境第一天,项目什么都跑了起来,


你知道的,每次去一家新公司,开始新项目难的是项目复杂配置文件,和各种mave包依赖,接口,环境冲突,所以跑起来项目自己一个人摸索还是需要一些时间的。


在这家公司前期也还不错,公司维护自己项目,工作时间也比较自由和灵活,


大体流程是,每次有新的pm时候 产品经理就会组织各个部门开会


h5端-移动端-接口端开会讨论需求细节和实现,如果有问题头就会pass掉


然后产品经理就会把需求指派到每一个头,头把需求指派给组员,然后组员按照
redmine 上截止时间开发需求,


开发过程中自己去找对应接口负责方,其他业务负责方 去对接数据,没有问题了就可以提交给指定测试组测试了。


然后测试组头会把,测试分配给他们组员,进行测试。


有问题了就会在指派回来给对应负责各个开发同学去解决bug,直到测试完成。测试会让你提交堡垒环境 然后等待通知发布上线,


我们一般是晚上8点时候发布,发布时候,一般开发人员都要留守,直到发布上线没有问题了,才可以回家。如果弄得很晚的话,第二天可以晚点上班。


这一点是我觉得比较好的地方,工作时间弹性比较自由。


记得有一次生产事故。


我影响很深刻,东西上线了,然后产品经理说这个和他设计的预期的不符合要求,需要重新写,那一晚我们整组弄得很晚,就是为了等我弄好去吃饭。


你知道人在心急如焚情况下,是写不好代码的最后还是同事帮我一起完成了产品经理变态需求修改。。。。。。(也就在那时候我才知道产品经理和开发为什么不和了


因为五行相克


因为经常这样发版,然后一起吃饭公司报销。我们组员和领导关系还算不错氛围还挺好。在这一家公司我待了挺久的。


离职原因
后期因为说公司项目战略升级。空降了一位携程cto,还带来了他的手下人,我们组头,职权被削弱了,我不在由原来头管理了。再加上后面一些其他原因。老同事一个一个走了。


最后这个组只剩下我,和一个进来不久新同事。 不久我也离职了。


第三家公司


这次离职后,我调整休息了差不多有一年,中间离开上海去了江苏,因为家里,女朋友等各种事情。后面我才又从新去了上海,开始找工作。


找工作期间投奔的同事,合同事住一起。


这次面试我明显感觉,有一些慌张了,可能是太久没上班原因,有一些底气不足。好在也是找到了工作虽然不太理想。


这个过程太曲折了,后面公司终究没有扛过疫情,可以说在疫情边缘倒闭了,钱赔偿也没拿到,。。。这里就不赘述了。


IT行业如何破局 大家有什么想法和故事吗。可以关注 程序员三时公众号 进行技术交流讨论


嗯~还想往下面写一点什么,,,下一篇分享一下我现在工作和未来思考


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

Java、Kotlin不香吗?为什么Flutter要选用Dart作为开发语言?

以上片段改编自成龙大哥经典的洗发水广告,虽然梗本身有点过时了,但却很形象地反映了我对Dart语言态度的转变:从最初的排斥到最后的喜欢。 对于任何想要了解一门新兴技术的开发者来说,语言常常是横亘在学习之路上的第一道障碍,如C/C++之于音视频,Python之于人...
继续阅读 »




以上片段改编自成龙大哥经典的洗发水广告,虽然梗本身有点过时了,但却很形象地反映了我对Dart语言态度的转变:从最初的排斥到最后的喜欢。


对于任何想要了解一门新兴技术的开发者来说,语言常常是横亘在学习之路上的第一道障碍,如C/C++之于音视频,Python之于人工智能等,当然也包括Dart之于Flutter。


尤其当你原先从事的是Android开发时,你肯定也曾产生过这样的疑惑:



既然同可以归到移动开发的范畴,也同属于Google旗下的团队,为什么Flutter不能沿用既有的Java或Kotlin语言来进行开发呢?



通过阅读本文,你的疑惑将得到充分的解答,你不仅能够了解到Flutter团队在选用Dart作为开发语言时的考量,还能充分感受到使用Dart语言进行开发的魅力所在。


照例,先奉上思维导图一张,方便复习:





热重载 (Hot Reload)一直以来都是Flutter对外推广的一大卖点,这是因为,相对于现有的基于原生平台的移动开发流程来讲,热重载在开发效率上确实是一个质的飞跃。



简单讲,热重载允许你在无需重启App的情况下,快速地构建页面、添加功能或修复错误。这个功能很大程度上依赖于Dart语言的一个很突出的特性:


同时支持AOT编译与JIT编译


AOT编译与JIT编译


AOT Compilation(Ahead-of-Time Compilation, 提前编译)是指在程序执行之前,将源代码或中间代码(如Java字节码)转换为可执行的机器码的过程。这么做可以提高程序的执行效率,但也需要更长的编译时间。


JIT Compilation(Just-in-Time Compilation, 即时编译)是指在程序执行期间,将源代码或中间代码转换为可执行的机器码的过程。这么做可以提高程序的灵活性和开发效率,但也会带来一些额外的开销,例如会对程序的初始执行造成一定的延迟。


用比较贴近生活的例子来解释二者之间的区别,就是:



AOT编译就像你在上台演讲之前,把原本全是英文的演讲稿提前翻译成中文,并写在纸上,这样当你上台之后,就可以直接照着译文念出来,而不需要再在现场翻译,演讲过程能更为流畅,但就是要在前期花费更多的时间和精力来准备。




JIT编译就像你在上台演讲之前,不需要做过多的准备,等到上台之后,再在现场将演讲稿上的英文逐句翻译成中文,也可以根据实际情况灵活地调整演讲内容,但就是会增加演讲的难度,遇到语法复杂的句子可能也会有更多的停顿。



可以看到,两种编译方式的应用场景不同,各有优劣,而Dart是为数不多的同时支持这两种编译方式的主流编程语言之一。根据当前所处项目阶段的不同,Dart提供了两种不同的构建模式:开发模式与生产模式。


开发模式与发布模式


在开发模式下,会利用 Dart VM 的 JIT 编译器,在运行时将内核文件转换为机器码,以实现热重载等功能,缩短开发周期。


热重载的流程,可以简单概括为以下几步:




  1. 扫描改动:当我们保存编辑内容或点击热重载按钮时,主机会扫描自上次编译以来的任何有代码改动的文件。




  2. 增量编译:将有代码改动的文件增量编译为内核文件。




  3. 推送更新:将内核文件注入到正在运行的 Dart VM。




  4. 代码合并:使用新的字段和函数更新类。




  5. Widget重建:应用的状态会被保留,并重建 widget 树,以便快速查看更改效果。




而在发布模式下,则会利用 Dart VM 的 AOT 编译器,在运行前将源代码直接转换为机器码,以实现程序的快速启动和更流畅地运行。


这里的“更流畅地运行”指的是在运行时能够更快地响应用户的操作,提供更流畅的用户体验,而不是单指让程序运行得更“快”。


这是因为Dart代码在被转换为机器码后,是可以直接在硬件上运行的,而不需要在运行时进行解释或编译,因此可以减少运行时的开销,提高程序的执行效率。


此外,经 AOT 编译后的代码,会强制执行健全的 Dart 类型系统,并使用快速对象分配和分代垃圾收集器来更好地管理内存。


因此,根据当前所处项目阶段的不同,采用不同的构建模式,Dart语言可以实现两全其美的效果


单线程模型


现如今,几乎所有的智能终端设备都支持多核CPU,为使应用在设备上能有更好的表现,我们常常会启动多个共享内存的线程,来并发执行多个任务。


大多数支持并发运行线程的计算机语言,如我们熟知的Java、Objective-C等,都采用了“抢占”的方式在线程之间进行切换,每个线程都被分配了一个时间片以执行任务,一旦超过了分配的时间,操作系统就会中断当前正在执行的线程,将CPU分配给正在等待队列的下一个线程。


但是,如果是在更新线程共享资源(如内存)期间发生的抢占行为,则可能会引致竞态条件的产生。竞态条件会导致严重的错误,轻则数据丢失,重则应用崩溃,且难以被定位和修复。


修复竞争条件的典型做法就是加锁,但锁本身会导致卡顿,甚至引发死锁等更严重的问题。


那Dart语言又是怎么解决这个问题的呢?


Dart语言采用了名为Isolate的单线程模型,Isolate模型是以操作系统提供的进程和线程等更为底层的原语进行设计的,所以你会发现它既有进程的特征(如:不共享内存),又有线程的特征(如:可处理异步任务)。


正如Isolate这个单词的原意“隔离”一样,在一个Dart应用中,所有的Dart代码都在Isolate内运行,每个Isolate都会有自己的堆内存,从而确保Isolate之间相互隔离,无法互相访问状态。在需要进行通信的场景里,Isolate会使用消息机制。


因为不共享内存,意味着它根本不允许抢占,因此也就无须担心线程的管理以及后台线程的创建等问题。


在一般场景下,我们甚至完全无需关心Isolate,通常一个Dart应用会在主Isolate下执行完所有代码。


虽然是单线程模型,但这并不意味着我们需要以阻塞UI的方式来运行代码,相反,Dart语言提供了包括 async/await 在内的一系列异步工具,可以帮助我们处理大部分的异步任务。关于 async/await 我们后面会有一篇单独的文章讲到,这里先不展开,只需要知道它跟Kotlin的协程有点像就可以了。



如图所示,Dart代码会在readAsString()方法执行非Dart代码时暂停,并在 readAsString()方法返回值后继续执行。


Isolate内部会运行一个消息循环,按照先进先出的模式处理重绘、点击等事件,可以与Android主线程的Looper相对照。



如图所示,在main()方法执行完毕后,事件队列会依次处理每一个事件。


而如果某个同步执行的操作花费了过长的处理时间,可能会导致应用看起来像是失去了响应。



如图所示,由于某个点击事件的同步处理耗时过长,导致其超过了处理两次重绘事件的期望时间间隔,直观的呈现就是界面卡顿。


因此,当我们需要执行消耗CPU的计算密集型工作时,可以将其转移到另外一个Isolate上以避免阻塞事件循环,这样的Isolate我们称之为后台运行对象



如图所示,生成的这个Isolate会执行耗时的计算任务,在结束后退出,并把结果返回。


由于这个Isolate持有自己的内存空间,与主Isolate互相隔离,因此即使阻塞也不会对其他Isolate造成影响。


快速对象分配与分代垃圾回收


在Android中,视图 (View)是构成用户界面的基础块,表示用户可以看到并与之交互的内容。在Flutter中,与之大致对应的概念则是Widget。Widget也是通过多个对象的嵌套组合,来形成一个层次结构关系,共同构建成一棵完整的Widget树。


但两者也不能完全等同。首先,Widget并非视图本身,最终的UI树是由一个个称之为Element的节点构成的;其次,Widget也不会直接绘制任何内容,最终的绘制工作是交由RenderObject完成的。Widget只是一个不可变的临时对象,用于描述在当前状态下视图应该呈现的样子


而所谓的Widget树只是我们描述组件嵌套关系的一种说法,是一种虚拟的结构。但 Element和RenderObject是在运行时实际存在的,如图:



这就好比手机与其规格参数的关系。Widget就像是一台手机的规格参数,是对当前组装成这个手机的真正的硬件配置的描述,当手机的硬件有更新或升级时,重新生成的规格参数也会有所变化。


由于Widget是不可变的,因此,我们无法直接对其更新,而是要通过操作状态来实现。但实际上,当Widget所依赖的状态发生改变时,Flutter框架就会重新创建一棵基于当前最新状态绘制的新的Widget树,对于原先的Widget来说它的生命周期其实已经结束了。


有人可能会对这种抛弃了整棵Widget树并完全重建一棵的做法存有疑问,担心这种行为会导致Flutter频繁创建和销毁大量短暂的Widget对象,给垃圾回收带来了巨大压力,特别对于一些可能由数千个Widget组合而成的复杂页面而言。


实际上这种担心完全没有必要,Dart的快速对象分配与分代垃圾回收足以让它应对这种情况。


快速对象分配


Dart以指针碰撞(Bump Pointer)的形式来完成对象的内存分配。


指针碰撞是指在堆内存中,Dart VM使用一个指针来跟踪下一个可用的内存位置。当需要分配新的内存时,Dart VM会将指针向前移动所需内存大小的距离,从而分配出新的内存空间



这种方式可以快速地分配内存,而不需要查找可用的内存段,并且使内存增长始终保持线性


另外,前面我们提到,由于每个Isolate都有自己的堆内存,彼此隔离,无法互相访问状态,因此可以实现无锁的快速分配。


分代垃圾回收


Dart的垃圾回收器是分代的,主要分为新生代(New Generation)与老年代(Old Generation)。


新生代用于分配生命周期较短的临时对象。其所在的内存空间会被分为两半,一个处于活跃状态,另一个处于非活跃状态,并且任何时候都只使用其中的一半。



新的对象会被分配到活跃的那一半,一旦被填满,垃圾回收器就会从根对象开始,查找所有对象的引用状态。




被引用到的对象会被标记为存活状态,并从活跃的一半复制到非活跃的一半。而没有被引用到的对象会被标记为死亡状态,并在随后的垃圾回收事件中被清除。



最后,这两半内存空间会交换活跃状态,非活跃的一半会再次变成活跃的一半,并且继续重复以上过程。



当对象达到一定的生命周期后,它们会被提升为老年代。此时的垃圾回收策略会分为两个阶段:标记与清除。


首先,在标记阶段,会遍历整个对象图,标记仍在使用的对象。


随后,在清除阶段,会扫描整个内存,回收任何没有被标记的对象,然后清除所有标记。


这种形式的垃圾回收发生频率不高,但有时需要暂停Dart Runtime以支持其运行。


为了最小化地降低垃圾回收事件对于应用程序的影响,垃圾回收器为Flutter引擎提供了钩子,当引擎检测到应用程序处于空闲状态并且没有用户交互时会发出通知,使得垃圾回收器可以在不影响性能的情况下执行回收工作。


另外,同样由于每个Isolate都在自己都独立线程内运行,因此每个Isolate的垃圾回收事件不会影响到其他Isolate的性能。


综上可知,Flutter框架所采用的工作流程,很大程度上依赖于其下层的内存分配器和垃圾回收器对于小型的、短生命周期的对象高效的内存分配和回收,缺少这个机制的语言是无法有效运作的


学习成本低


对于想要转岗Flutter的Android或iOS开发者,Dart语言是很友好的,其语法与Kotlin、Swift等语言都存在一些相似之处。


例如,它们都是面向对象的语言,都支持类、接口、继承、抽象类等概念。绝大多数开发者都拥有面向对象开发的经验,因此可以以极低的学习成本学习Dart语言。


此外,Dart语言也拥有着许多与其他语言相似的优秀的语法特性,可以提高开发人员的生产力,例如:




  • 字符串插值:可以直接在字符串中嵌入变量或表达式,而不需要使用+号相连:
    var name = 'Bob'; print('Hello, $name!');




  • 初始化形式参数:可以在构造函数中直接初始化类的属性,而不需要在函数体中赋值:
    class Point { num x, y; Point(this.x, this.y); }




  • 函数式编程风格:可以利用高阶函数、匿名函数、箭头函数等特性简化代码的结构和逻辑:
    var numbers = [1, 2, 3]; var doubled = numbers.map((n) => n * 2);




Dart团队配合度高


拥有一定工作年限的Android开发者,对于早些年Oracle与Google两家科技公司的Java API版权之争可能还有些许印象。


简单讲就是,Oracle认为Google在Android系统中对Java API的复制使用侵犯了其版权和专利权,这场持续了11年的专利纠纷最终以Google的胜利结束。


相比之下,Dart语言与Flutter之间则没有那么多狗血撕逼的剧情,相反,Flutter与Dart社区展开了密切合作,Dart社区积极投入资源改进Dart语言,以便在Flutter中更易使用。


例如,Flutter在最开始采用Dart语言时,还没有用于生成原生二进制文件的AOT工具链,但在Dart团队为Flutter构建了这些工具后,这个缺失已经不复存在了。


结语


以上,就是我汇总Flutter官网资料及Flutter社区推荐博文的说法之后,总结出的Flutter选用Dart作为开发语言的几大主要原因,希望对于刚入门或想要初步了解Flutter开发的小伙伴们有所帮助。


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

这么好的Android开发辅助工具App不白嫖可惜了

过年期间闲来没事,手撸了一个辅助Android开发调试的工具App,适合Android开发者和测试同学使用。 Github地址下载, Gitee地址下载(需要登录gitee) 或者去Google Play安装 功能概览 对我这样的懒人开发者来说,反复的做同样一...
继续阅读 »

过年期间闲来没事,手撸了一个辅助Android开发调试的工具App,适合Android开发者和测试同学使用。


Github地址下载,
Gitee地址下载(需要登录gitee)


或者去Google Play安装


功能概览


对我这样的懒人开发者来说,反复的做同样一件事简直太煎熬了,因此我把我平时开发中需要反复操作的命令和一些繁琐的操作整理成了一个工具。


废话不多说, 先上图了解下工具的大概功能有哪些(内容比截图丰富,欢迎下载体验)












CodeCrafts的核心是一个可拖动的侧边栏的悬浮窗,悬浮窗可以折叠或展开,悬浮窗中包含5大块功能分别对应一个TAB, 这5大块功能分别是应用控制、开发者选项、常用功能,常用系统设置和全局功能


请看视频预览:


floating-bar.gif


高清原图 introduction-floating-bar.gif


功能明细


1. 应用控制


应用控制能力将一些日常开发过程中对应用的一些繁琐的操作或者命令行指令转变为可视化的操作,而且还有自动收集和整理Crash, ANR日志,并且可以自动关联Logcat日志


文字太繁琐, 请直接看视频


application-control.gif


高清原图 introduction-application-controls.gif


2. 开发者选项


这里的开发者选项功能是将系统的开发者选项中一些最常用的开关放在悬浮窗中, 随时启用或关闭。
优势是不需要频繁去系统的开发者选项中去找对应开关,一键开闭。


我调研了其他有类似能力的工具App,都是引导用户去开发者选项中去开启或关闭功能。CodeCrafts一键开闭,无需跳转到系统开发者选项页面。


请看视频预览:


developer-options.gif


p2.gif


3. 最常用功能


没什么好介绍的,略。


4. 常用系统设置页面


这里承载了一些开发过程中经常需要打开的系统设置页面的快捷按钮,没什么好介绍的,略


5. 全局功能


这里的全局是相对于应用控制的,应用控制可以选择你正在开发的任意一款App, 然后应用控制中的所有能力都是对你的这个App的操作。 而全局控制中的功能不针对选中的App,所有App都适用


5.1 实时数据(Realtime data)


实时数据会随着当前页面变化或者系统事件实时变化



(以上图为例介绍, 实时数据的内容不仅仅只有这些)




















































内容含义用途
org.chromium.chrome.browser.firstrun.FirstRunActivity当前Activity的类名代码定位
launch time: 208ms当前Activity的冷启动耗时启动优化
com.android.chrome当前Activity所在应用的包名常用信息
Chrome(uid: 10163)当前Activity所在应用的名称和UID常用信息
pid: 23017当前Activity的进程ID常用信息
192.168.2.56,...当前系统的IP地址,可能有多个adb connect等
system当前应用是系统应用
allowBackUp当前应用有allowBackUp属性告警

实时数据未来还会有更多的扩展内容


5.2 不锁定屏幕


不会进入锁屏状态,也不会灭屏,避免开发过程中老是自动锁屏。


和系统开发者选项中的功能类似,区别是无论是否插入USB线都有效,开发者选项中的拔掉USB线后就无效了。
都可以用,具体选择看你的使用场景。


5.3 Latest Crashes


显示缓存中最近发生的Crash的调用堆栈,可能为空也可能不止一个Crash堆栈, 需要自行查看是否是你关注的Crash。


使用说明


CodeCrafts的很多功能依赖Shell权限, 如果发现存在功能不可用的情况,一般都是shell权限获取失败了, 只需要通过在电脑终端输入adb命令"adb tcpip 5555"指令, CodeCrafts就可以自动获取shell权限了。


image.png


adb tcpip 5555



  1. 第一次使用,连接电脑终端发送"adb tcpip 5555" 或

  2. 手机断电重启,连接电脑终端发送"adb tcpip 5555" 或

  3. 莫名其妙功能不能用了,连接电脑终端发送"adb tcpip 5555"


新增功能


有不少人反馈对CodeCrafts的实现原理感兴趣,后面新增的功能尽量配上实现原理



  1. CodeCrafts之断点调试 (1.0.15新增)


建设中



  1. 文件沙盒, 快速浏览App的文件目录

  2. 自动化,自动化点击,输入(比如自动跳广告,自动输入账号密码?)

  3. 组件检查, 快速查看View的类型, id, 颜色等

  4. ...


后期规划



  1. 悬浮窗的tab和内容可动态配置

  2. 应用控制增加应用性能数据

  3. 提供外部SDK接口,外部应用可接入CodeCrafts进行定制化改造


CodeCrafts持续更新中...


Github地址下载,
Gitee地址下载(需要登录gitee)


或者去Google Play安装


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

2023年 android 裸辞跳槽经历

前言 上家公司我呆了整整三年,趁着合同到期,不想续签了,于是想出来试试新的机会,从毕业到现在每次换工作都是裸辞的,哪怕今年行情不怎么好也是裸辞的,自我认为,一来经济上也没有太大压力,二来裸辞之后能全心全意准备面试,成功率大大提升,但也不排除运气太差,一直找不到...
继续阅读 »

前言


上家公司我呆了整整三年,趁着合同到期,不想续签了,于是想出来试试新的机会,从毕业到现在每次换工作都是裸辞的,哪怕今年行情不怎么好也是裸辞的,自我认为,一来经济上也没有太大压力,二来裸辞之后能全心全意准备面试,成功率大大提升,但也不排除运气太差,一直找不到工作的情况,所以每个人需要根据自己的实际情况做选择。


我是 2 月 1 号先找我的上级聊离职的事情,然后又找公司人事聊合同不续签的事情,然后约定在 3 月 15 号离职。提完离职之后跟想象的不一样,以为提完离职就可以放松下来了,没想到一直到 3 月 14 号还在写需求🤣


复习准备


提完离职之后就开始准备简历和投简历了,因为离职这件事从去年就开始考虑了,所以在去年 11 月份就开始有意识的准备了,刷算法,刷八股文。大概用了两个月时间,leetcode 刷了 100 道题,当然一开始刷算法肯定很受挫,第一题就不会,这很正常,我一般是先看题目,不会就看题解,然后背,每隔一周就把上周刷的算法再复习一遍。


640.jpeg


关于八股文的准备就是复习 Java,kotlin,Android 基础,计算机网络,数据结构,还有设计模式等等部分,这些东西比较耗时同时知识点比较多又杂,所以只能慢慢复习,就跟大学考研一样,第一遍就是全面复习,然后把不会的知识点记录下来,然后每天上下班地铁上快速的过一遍,就是无意识的记录,看到一个知识点就在脑子里回想跟它有关联的其他知识点,这样一直持续到 2 月份,然后还有简历,简历很重要,因为面试官基本都是根据你简历上面写的提问,当然也不排除不按套路来的面试官,我就碰到过一次,一上来就一直问我计算机网络底层相关的东西,如 socket,websocket,xmpp 以及如何处理高并发的东西,然后其他东西一点都没问,这就让我很郁闷了,总之简历一定要写好,同时简历里面的提到的知识点一定要滚瓜烂熟。


找工作途径


关于在哪里投递简历的话,我是用了拉钩,BOSS,51job,猎聘网,然后拉钩充值了一个月会员 169 元(屁用都没有,感觉拉钩已经死掉了,职位不更新,投简历没反应),BOSS 先买了一个月会员,然后又续费了一个月,每个月 69 元,最后也是通过 BOSS 面试找到工作的。大伙有需要可以了解一下,这里不是给 BOSS 打广告哈。然后 51job 这个 App,用了一圈感觉都是外企或者国企事业单位发布的职位比较多,不过整体感觉用处不大,没起什么作用。猎聘网 App,广告打得到处都是,然而没卵用。这就是我找工作的途径。


offer情况


从 2 月份开始投递简历,大概投了几十份简历,总共收到的面试机会大概有 15 家,参加的面试有 10 家左右,还有其他的就是不合适啥的就拒绝掉了,最后统计了一下,只有成功了 3 家,收到了 3 家录用通知,成功率只有 30%,因为我的目标很明确,就是想去外企,国企事业单位,初创企业,互联网的公司不大想去,所以一些大厂我都没有投简历。可以想象今年的行情很差,我三年前也就是在 20 年的时候换工作,那时候都是工作追着你跑的,今年行情的确不行,你去找工作,别人都不愿意搭理你。


关于具体的面试经验总结可以看我公众号里面具体的文章,以上便是我今年换工作的整体情况。


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

用 Compose 写 App 可以多快?

休整半年多的我,在今年年后就在思考与尝试我的事业应该怎么走了。其实在去年年终总结中,我已经提及了我的几个方向。 我最开始的方向就是迈入养生行业,虽然我有技术,也有医术,但是没客户,所以我大概需要很长的时间去累积客户,加上现在客户都迷恋让肌肉放松的推拿按摩,以及...
继续阅读 »

休整半年多的我,在今年年后就在思考与尝试我的事业应该怎么走了。其实在去年年终总结中,我已经提及了我的几个方向。


我最开始的方向就是迈入养生行业,虽然我有技术,也有医术,但是没客户,所以我大概需要很长的时间去累积客户,加上现在客户都迷恋让肌肉放松的推拿按摩,以及房租设备之类的开销。还不如找个小公司安心上班来得舒服。可是我又耐不住想折腾的心。


所以我开启了 PlanB,去走中医知识学习领域,虽然现在市面上有一些这类的 app,不过他们都不是真正有中医知识的人主导的,只能说是一堆资源的简单聚合,或者为了卖课而存在。而根本不知道学习中医的痛点是什么,怎样才能真正的提升医术,这就是我的市场了。所以我从零做了一个 app,目前完成了首个版本了。这是真的做一个 app 满足自己的需求。虽然目前功能、数据还很少,但我认为它是有价值的,虽然可能没有钱途。


aa1.png


App 已经在官网villa.qhplus.cn、华为、小米、应用宝、Oppo 的应用市场上架了, 但对于开发而言,并不会懂这个 App 的内容以及结构,毕竟不是为你们而设计的,但是你们可以体验下 Compose 已然是多么的丝滑了。因为这个 app 是全部用 Compose 开发。从立项开始到现在,仅用一个半月左右的时间完成开发,是时候让你们感受下 Compose 开发的速度了。先欣赏下设计稿的数量(辛苦我美丽动人的老婆大人了)。


aa2.png


并且我做的是全栈式的开发,其包括:



  1. 思考产品形态

  2. rust 写后端服务

  3. 数据爬取、清洗与整理入库

  4. vue3 写官网、隐私协议等 H5 界面

  5. 为了上架、登录、push 等要开一个壳公司,跑各种流程(最繁琐、最耗时的工作)


即使是 app 端,也要有各种数据逻辑、上报、存储等逻辑,可想而知,能分配给写 UI 的时间能有多少?


当然,这也归功于在去年修整期间我写的 emo 组件库,极大的加速了业务层的开发。


问:为什么不考虑小程序开发,Flutter 开发,RN 开发?


答:小程序挺好的,但是它却很封闭,我想要实现桌面小组件之类的功能,小程序就完全做到,但对于中医条文,用小组件来让我们每天回忆一条条文,是个我个人很喜欢的功能。


RN 的性能太差,而且用它,就要牺牲诸如动画、复杂布局等各种场景。并且往往这些需要与原生交互的场景,就要用力十倍才能解决。


不用 Flutter,首先当然是因为我不会,其次是它和 RN 都是 UI 层面,如果和数据层一起考虑,那就没那么简单了。 而我用 Compose,与整个 Android 生态都是打通的,所以性能又高,开发速度又快。何乐而不为?跨平台?各自写就行了,不再去入整体跨平台的坑了。跨平台的坑不仅是技术抽象应对各自生态不是那么稳定的坑,还有人力资源协调的坑。总会让人心累。


下面我们可以来看看 Composeemo 协同开发带来的一些爽点:


界面管理


Composescheme 路由的方式来处理界面跳转、曝光,就非常简单了, 每一个新界面就是一个 Composable,加上 @ComposeScheme 就完了

@ComposeScheme(
action = SchemeConst.ACTION_THINK_DETAIL,
alternativeHosts = [HolderActivity::class]
)
@SchemeLongArg(name = SchemeConst.ARG_ID)
@SchemeLongArg(name = SchemeConst.ARG_COMMENT_ID)
@Composable
fun ThinkDetailPage(navBackStackEntry: NavBackStackEntry) {
LogicPage(navBackStackEntry = navBackStackEntry) {
// content
}
}

@Composable
fun LogicPage(
navBackStackEntry: NavBackStackEntry,
saveToLatest: Boolean = false,
content: @Composable () -> Unit
) {
content()
LaunchedEffect(navBackStackEntry) {
val scheme = navBackStackEntry.arguments?.getString(SchemeKeys.KEY_ORIGIN)?.let { Uri.decode(it) }
if (scheme != null) {
// 上报 scheme,作为曝光
// 保存 scheme,如果用户退出了,直接重入这个界面。
// 这个在调试中很好用。例如某个界面,需要点5层才能进去,每次编译重启就要点5次才能看到这个界面,那就蛋疼了,所以如果每次把它记录起来,启动就进去,那开发就顺很多了
}
}
}

界面状态


很多界面基本上就是列表,然后就有空界面、错误提示情况,列表,列表可能还有加载更多。在原来 View 体系,就要做各种 View 的显示隐藏操作,写起来贼麻烦。 用 Compose 封装起来就简单了。 看我的封装结果

val logic by vm.thinkFlow.collectAsStateWithLifecycle()
LogicBox(
modifier = Modifier
.fillMaxWidth()
.weight(1f),
logic = { logic },
reload = {
vm.reload()
},
emptyText = "空"
) { list ->
// 列表数据
}

把它往 LogicPage 里面套就完事了,当然这也是数据逻辑层我抽象了强大的 logic 逻辑。借助这个逻辑,可以分分钟完成数据的从网络数据拉取,再到读存 DB,再到界面的渲染,可以快速补充完成空界面、加载出错、加载更多、下拉刷新等功能。


多级评论


aa3.jpg


看我这个思辨详情页面,假设以旧的 RecyclerView 体系来做这个,想想都痛苦。而我是数据逻辑层加UI一起两三个小时搞定, 毫无 bug


另外这里还有一个“从通知点击进来滚动到当前评论”的场景,如果是原生或者 RN 来做,最痛苦的事情就是滚动时机了,一般最终会使用 post 万能大法大法,然而总有没滚动的情况发生,然后产品就找过来了。


Compose 也就是一小段代码的事了:

if (vm.targetCommentId > 0) {
val targetCommentIndex = remember(vo) {
indexOfTargetCommentId(vo, vm.targetCommentId)
}
if (targetCommentIndex > 0) {
LaunchedEffect(Unit) {
vm.listState.scrollToItem(targetCommentIndex, 0)
}
}
}

嵌套滚动


看看这个一般的嵌套滚动界面


aa4.gif


即使有 NestedScroll 或者 CoordinatorLayout,但新手用不懂,高手也容易遗忘某些配置而踩坑。


那么 Compose 需要多少代码呢?

val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
if (available.y < 0 && vm.scrollState.canScrollForward) {
scope.launch {
vm.scrollState.scrollBy(-available.y)
}
return available
}
return super.onPreScroll(available, source)
}
}
}
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(vm.scrollState)
.nestedScroll(nestedScrollConnection)
) {
BookInfoBasic(info)
BookInfoPageTabSegment(vm = vm)
HorizontalPager(...)
}

这样就完成整个界面了,其实也是对 nestedScroll 的封装,道理和 View 体系一样,只是用起来更方便了。


ChatGPT


ChatGPT 对于 Compose 而言,很不好,毕竟其训练依赖的是旧版本,所以会有很多错误,所以不能用的,但是它在逻辑层面就很好用了,例如文件上传、下载等,我都是让它写,写完自己校验下,就完工了。为了赶时髦,我当然也在 app 里接入了 ChatGPT,当然,我做了配置,目前对外不开放。


aa5.jpg


漫长的审核


正如文章开始所说,开发我用了一个多月,但是后面的审核上线则是用了两个月左右,其实说到底还是对规则的不熟悉。在电子版权、安全评估报告等环节都是在处理一份之后才知道必须要另一个,所以化并行为串行了。并且做安全评估,给我的感觉就是我的 app 分分钟有上百万的日活,实际上整个圈子可能不过数万人,但我也得完成相应的功能,例如接入飞书机器人,在飞书群里完成内容审核功能。


所以这两个月,最多的就是认识到了整个市场,在公司注册、记账、上架等各个环节衍生的无数商业行为,很多都是收智商税和信息差赚差价的。所以中国有商业头脑的人还是很多,在各个小环节拉拢豪绅、巧立名目,只要有信息差,我就可以无限拉高价格。因为也铸就了现在创业那高不可攀的围墙。


当然,我已经进到墙内了,如果能够成功,那这个墙就是对我的保护了,毕竟干的又不是 ChatGpt 那种无法轻易复制的产品,所以这堵高墙就可以为我争取更多的成长时间了。


在这两个月,我打造了另一款产品:emo-ai。最主要的功能就是 ChatGpt 的代理,目前维护了一个小用户群体,收到了第一桶小金。


此外,我也了解了下 StableDiffusion,本地搭建了 StableDiffusionWebUi 的环境,了解它的 prompt 玩法、 controlnetlora 之类的知识。绘图入魔怔~


aa6.jpg


最后


ChatGPT 的爆火,让人们见识到了 AI 的力量,开发、设计、文案等领域,都快被取代了。中医这个领域,虽然目前完全没有被波及,但我以前曾提过:


**治疗 = 根据【当前的症状/指标】推荐出【相应的药物】


所以医学本质是一个推荐系统,西医强调靶向治疗,中医则是用阴阳五行之类的建立了一个巨大的模型,从这个角度上来讲,中医显然更胜一筹。


但是深度神经网络时代,显然我们可以训练参数规模更大的模型,来完成辨证论治的过程。


但是模型的训练,少不了数据的支撑以及模型的建立。


数据来说,中医有几千年的数据积累,是世界上最大最全的资源,只是需要整合与结构化。


而模型最为重要的是模型的结构是怎样的?损失函数、优化器如何定义?经过长久的学习,我已然有了一些思路,也是我跨领域融合所独有的见解。


但目前数据还不是结构化的,GPU 也不是我能买得起的,所以这条路还很长,也是岐黄小筑想要承载的梦想。


拥有梦想,也要脚踏实地,因为我目前做的事情就比较苦逼了。我要把一本本书籍的拆分出来,存成结构化的数据,并对内容做链接,用技术只能得到模糊的结果,最后还要自己去校对。录入系统的管理后台也还在建设中。


所以, 最后再吹下 Compose, 为我节约了大量的时间。在 View 时代,鉴于喜欢写 UI 和能够写 UI 的人真的偏少,我大概能够一次性取代 6 个业务开发,那在 Compose 的加持下,也许取代 60 个业务开发也不是什么大问题了。


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

前端 markdown 到 pdf 生成方案

web
前端 markdown 到 pdf 生成方案 (检查修订中...) 接到需求,需要把数据同学生成的 markdown 格式的 ChatGPT 日报在平台上进行展示,并提供可下载的 PDF 文件。这里简单记录下使用到的技术和遇到的问题。 1.  方案对比 这个是...
继续阅读 »

前端 markdown 到 pdf 生成方案


(检查修订中...)


接到需求,需要把数据同学生成的 markdown 格式的 ChatGPT 日报在平台上进行展示,并提供可下载的 PDF 文件。这里简单记录下使用到的技术和遇到的问题。


1.  方案对比


这个是现在项目中使用的方案,整体步骤如下,先有个全局的认识:


1.  下载云端的 markdown文件


2.  通过 markejs 把 markdown 字符串 解析成 html 字符串


3.  React 解析 html 字符串,通过 dangerouslySetInnerHTML 渲染成 DOM 结构


4.  通过 html2pdf 首先把 DOM 结构转为 Canvas,然后转为 Image,最终输出到 Pdf 文件提供下载


markdown 字符串到 html 字符串,直接选型了 github.com/markedjs/ma… ,它是一个比较高效的 markdown 解析库,使用简单,也有一些 hooks 方便我们获取解析过程和解析结果中的一些信息,比如我们需要生成一二三级标题的导航,可配置的东西也很多,感兴趣的可以看看


不过从 html 生成 pdf,倒腾了几个方案。最后确定使用了 html -> canvas -> pdf 的方案,主要优势是还原度高,简单。但是也存在图片失真,分页导致文字分割,文字不可复制等问题,不过这些缺点是在可接受范围的。


与之相反的方案是使用 文字版本的 PDF文件下载。思路是把 dom 结构生成一个个的 JSON 信息,通过 PdfMake GitHub - bpampuch/pdfmake: Client/server side PDF printing in pure JavaScript 框架,把文字输出到 PDF 文件上。这里有个最大的难点是 文字的处理,后面再展开谈谈


简单总结下:


方案框架选择优点缺点其他问题
Canvas图片[html2pdf.jsClient-side HTML-to-PDF rendering using pure JS.](ekoopmans.github.io/html2pdf.js…)处理简单,高还原文字无法复制,分页上元素被切割,容易失真(已有解决方案)内容过多时生成空白PDF文件(已有解决方案)
文字GitHub - bpampuch/pdfmake: Client/server side PDF printing in pure JavaScript文字可复制,内容清晰,不存在分页上元素被切割处理繁杂,中文和 emoji 暂时没有太好的处理方案(需要导入字体库,导致生成时间过长,试了下腾讯文档导出 pdf 里面的emoji表情都被过滤了)-


2.  Markdown -> HTML


这里借助 marked 可以很容易实现,直接上代码:


import { marked } from 'marked';
import DOMPurify from 'dompurify';

const [renderText, setRenderText] = useState('');

...
// 核心代码
setRenderText(DOMPurify.sanitize(marked.parse(text), { ADD_ATTR: ['target'] }));
const tokens = marked.lexer(text);
const headings = tokens
.filter((token) => {
return token.type === 'heading' && token.depth < 4;
})
.map((heading) => {
return {
level: heading.depth,
text: heading.text,
slug: heading.text
.toLowerCase()
.trim()
// remove html tags
.replace(/<[!/a-z].*?>/gi, '')
// remove unwanted chars
.replace(/[\u2000-\u206F\u2E00-\u2E7F\'!"#$%&()*+,./:;<=>?@[]^`{|}~]/g, '')
.replace(/\s/g, '-'),
};
});
...

...
// 下面是导航
{headings && Array.isArray(headings) && headings.length > 0 && (
<ul className="markdown-preview-nav">
{headings.map((item) => (
<li
className={`markdown-preview-nav-depth-${item.level}`}
key={item.slug}
onClick={debounce(debounceTime, () =>
{
handleNavigatorScroll(item.slug, contentRef, 10);
})}
>
<a className={`${navigator === item.slug ? 'active' : ''}`}>{item.text}</a>
</li>
))}
</ul>

)}

export function handleNavigatorScroll(curNavigator, contentRef, offset = 100) {
const anchorElement = document.getElementById(curNavigator);
if (!(anchorElement instanceof HTMLElement)) {
return;
}
if (!(contentRef.current instanceof HTMLElement)) {
return;
}
contentRef.current.scrollTo({
top: anchorElement?.offsetTop - offset,
behavior: 'smooth', // 平滑滚动
});
}
...

2.1 html 标签属性保留


GitHub - cure53/DOMPurify: DOMPurify - a DOM-only, super-fast, uber-tolerant XSS sanitizer for HTML, MathML and SVG. DOMPurify works with a secure default, but offers a lot of configurability and hooks. Demo: DOMPurify 主要是防止 XSS 攻击,这个没有太多需要解释说明的


主要需要提醒的是,DOMPurify会把大部分标签的上的属性给过滤掉,比如 target 属性。所以我们在第二个参数上 加了 ADD_ATTR 配置,保留这个属性,因为需要传递 _blank ,允许用户通过新窗口打开链接


2.2 目录生成


另外一个需求是需要获取到 一二三级 标题,生成目录导航。我们可以通过 marked.lexer 获取到解析后的元素数组,把类型为 heading 并且层级小于 4 的元素挑选出来,组成我们的标题导航。


这里还有页面跳转的功能,我们需要跳转到和当前点击导航相匹配 id 的元素,主要通过 slug 来判断。一开始在网上找了下面一段代码:


heading.text.toLowerCase().replace(/[^(\w|\u4e00-\u9fa5)]+/g, '-')

发现不能 100% 匹配上所有的情况,导致失效。后面扒了下源码(如上面代码所示的几个 replace 函数)替换上去,功能正常。


不过更好的方式是使用 marked 自带的办法,如下所示(印象中有人提过这个 issues,所以刚去查了下,简单验证了下没有问题):


// add slug with occurrences by UziTech · Pull Request #20 · Flet/github-slugger · GitHub

const slugger = new marked.Slugger();

console.log(
slugger.slug(heading.text),

heading.text
.toLowerCase()
.trim()
// remove html tags
.replace(/<[!/a-z].*?>/gi, '')
// remove unwanted chars
.replace(/[\u2000-\u206F\u2E00-\u2E7F\'!"#$%&()*+,./:;<=>?@[]^`{|}~]/g, '')
.replace(/\s/g, '-'),
);

2.3 样式


产品对于样式这块并没有太多的要求,让参考 dumi - 为组件研发而生的静态站点框架 的样式。所以一开始扒拉了下它的样式文件过来用。但是发现需熬过并不是太尽如人意 。最终找了 Issues · sindresorhus/github-markdown-css · GitHub 来使用,GitHub 上使用的 markdown 样式库


import 'github-markdown-css/github-markdown-light.css';

不过这里也要提个小问题,一开始我是直接引用 github-markdown-css,测试反馈样式上有点问题,怎么是黑色的主题,看了下源码,发现使用了一个有意思的媒体查询:prefers-color-scheme - CSS:层叠样式表 | MDN ,学习了~。遂改成只用 light 主题的样式


2.4 <details> & <summary>


产品侧反馈存在大量用户的评论,想要能折叠起来,点击的时候才进行展示。一开始想着 React 来控制这种行为,但是后面想起 HTML5 本身也有类似原生的标签可以使用:details 标签(<details>: The Details disclosure element - HTML: HyperText Markup Language | MDN),于是就拿来用了。


它在 markdown 文件中如何使用,可以参考下下面的讨论:


gist.github.com/scmx/eca72d…


这里需要注意的一个问题是,

标签后面必须要加一个空行,否则会导致后续生成的 PDF 文件展示出现问题(具体是在PDF文件中,折叠的内容也会展示出来,但是不占据空间,导致内容重叠),原因不详(待研究),如下所示:


<details>
<summary>**重要评论** </summary>
// 这个空行是必须的!!!这个空行是必须的!!!这个空行是必须的!!!
>> 内容 1
</details>

还有另外一个问题是如下图所示:



我们使用 details 和 summary 标签的时候,在页面上是可以看到箭头的,但是生成 PDF 的话,箭头消失了,原因不详(待研究)。这里简单的处理方式是打了个补丁:


.markdown-body details ::marker,
.markdown-body details ::-webkit-details-marker {
font-size: 0;
}

.markdown-body details summary:before {
font-size: 14px;
content: '▶';
display: inline-block;
margin-right: 5px;
color: #24292f;
}

.markdown-body details[open] summary:before {
content: '▼';
}

2.5 <table>


生成出来的PDF文件中,会发现过宽的 table 元素会展示不全,可以添加下面的样式来解决:


.markdown-body table {
word-break: break-all;
}

3.  当前方案:HTML -> Canvas -> PDF


这块主要使用的是 GitHub - eKoopmans/html2pdf.js: Client-side HTML-to-PDF rendering using pure JS.,而它主要依赖的是 GitHub - niklasvh/html2canvas: Screenshots with JavaScriptGitHub - parallax/jsPDF: Client-side JavaScript PDF generation for everyone. 感兴趣的都可以去看看。整体流程引用它里面的内容:


.from() -> .toContainer() -> .toCanvas() -> .toImg() -> .toPdf() -> .save()

这里主要讲讲使用过程,以及遇到的问题。先上整体主要的代码:


import canvasSize from 'canvas-size';
import html2pdf from 'html2pdf.js';
import axios from 'axios';
import * as FileSaver from 'file-saver';

const markdownRef = useRef<HTMLElement>(null);
let isValidCanvas = true;

const worker = html2pdf()
.set({
pagebreak: { mode: ['avoid-all', 'css'] },
html2canvas: {
scale: 2,
useCORS: true,
onrendered: function (canvas) {
isValidCanvas = canvasSize.test({
width: canvas.width,
height: canvas.height,
});

if (isValidCanvas) {
worker
.toImg()
.toPdf()
.save(fileName + '.pdf')
.then(() => {
setRendering(false);
});
} else {
axios
.get(mdFileUrl, { responseType: 'blob' })
.then((res) => {
if (res?.data) {
FileSaver.saveAs(res.data, fileName + '.md');
}
setRendering(false);
})
.finally(() => setRendering(false));
}
},
},
margin: [0, 10, 0, 10],
image: {
type: 'jpeg',
quality: 1,
},
})
.from(markdownRef.current)
.toCanvas();

3.1 Canvas 过大,PDF输出空白


这个是使用 Canvas 方案的时候遇到的最大问题,差点弃坑。中间也修改了几个版本,最终代码如上所示。接下来会简单说说过程。


PDF 输出空白文件这个其实在官方 issues 上也有不少的提问的,比如这一条:github.com/eKoopmans/h…,整整50多条评论,遇到这个问题的人还是不少


主要原因还是浏览器的支持问题,浏览器会限制生成的 Canvas 元素尺寸,超过的话生成一个空白的 Canvas 元素:


The HTML canvas element is widely supported by modern and legacy browsers, but each browser and platform combination imposes unique size limitations that will render a canvas unusable when exceeded

引用:GitHub - jhildenbiddle/canvas-size: Determine the maximum size of an HTML canvas element and test support for custom canvas dimensions

3.1.1 文档拆分


针对这个问题,给出的解决方案大部分是把整个文档分成几个部分,生成小块的 Canvas ,然后一点点的渲染到 PDF文件 上去。


但是这里会有两个问题:


1.  文档拆分的标准是什么?这个我很难定下来,因为给到的 markdown 文件内容没有固定可拆分的标准


2.  使用 addPage 会生成新的一页,导致出现大量的空白位置(没找到可以在当前页面后继续添加内容的方法)


于是放弃了这个方案


3.1.2 兜底方案 - 判断文档是否过大


最终和产品协商的方案是,如果文档太大,我们提供原始的 markdown 文件供用户下载。那接下来的问题变成了,文档什么时候会过大?


官方针对这个问题贴了个链接:stackoverflow.com/questions/6…,不过已经是2014年的答案了,这份数据并不可靠。


不同浏览器的尺寸限制并不一样,项目中使用的是:GitHub - jhildenbiddle/canvas-size: Determine the maximum size of an HTML canvas element and test support for custom canvas dimensions,基本原理是生成一个 Canvas 元素然后收集相关的信息来判断尺寸的限制,引用它的一段话:


Unfortunately, browsers do not provide a way to determine what their limitations are, nor do they provide any kind of feedback after an unusable canvas has been created. This makes working with large canvas elements a challenge, especially for applications that support a variety of browsers and platforms.

This micro-library provides the maximum area, height, and width of an HTML canvas element supported by the browser as well as the ability to test custom canvas dimensions. By collecting this information before a new canvas element is created, applications are able to reliably set canvas dimensions within the size limitations of each browser/platform.

引用:GitHub - jhildenbiddle/canvas-size: Determine the maximum size of an HTML canvas element and test support for custom canvas dimensions

3.1.2.1 最初方案


于是乎,一开始采用了下面的判断方案:


const isValidCanvas = canvasSize.test({
width: markdownRef.current?.clientWidth,
height: markdownRef.current?.clientHeight,
});

直接拿了 DOM 结构元素的宽高来进行判断。一开始误认为生成的 PDF 文件会和页面上的元素宽高是一致的,所以采用了这种判断方式。但是实际上不是的,不传递 width / height 参数的时候,看到生成的 canvas 宽度基本都是 719 像素。问了下 Warp: The terminal for the 21st century,它的答复是这样的:


html2pdf是一个将HTML转换为PDF的工具,它使用了wkhtmltopdf引擎来进行转换。在转换过程中,wkhtmltopdf会将HTML渲染成一个canvas,然后将canvas转换为PDF。canvas的宽度默认为719像素,这是因为wkhtmltopdf使用的默认DPI为96,而71996dpi下A4纸的像素宽度。

暂时没有细细考究。所以我上面的判断方式是肯定存在问题的,后续也发现了很多通过这种方式生成的文档,存在空白的问题。需要调整优化


清晰度问题

另外针对图片失真的问题,解决方案是通过设置 html2canvas 参数来进行优化,比如设置 scale = 2,扩大 Canvas 元素的宽高来输出更加清晰的图片到 PDF 文件中,那我们使用 HTML 元素的宽高误差就更大了


3.1.2.2 最终版本


最终版本就是一开始贴出来的完整代码。不过也是几经修改才确定下来的,这其中遇到了以下一些问题:


获取宽高的方法

html2pdf 提供的方法基本都是基于 Promise 的工作流,产物一个个往下传递。但是翻阅了很久也没有找到一个方法可以去判断生成的 Canvas 的宽高,来决定是 resolve 继续执行 PDF 的生成,还是 reject 掉去执行兜底方案。


于是开始看 html2canvas 的文档,发现文档非常简单!也没有找到有用的信息。但是既然是开源项目,于是重施旧计 - 看源码。主要的搜索方向是找相关的方法,通过 on 关键字找到了 onrendered 函数,可以获取到生成的 Canvas 的信息。


不过要注意的是,这个方法已经被标记为废弃,后续版本也许不能使用了。其实最好的方式可以是单独引用 html2canvas 和 pdfjs,生成的 canvas 元素传递给 pdfjs,而不是使用 html2pdf 这个集成库,感兴趣的可以研究下。


于是乎我们的代码就从原来的 toPdf() 一步到位,变成了 toCanvas,然后在 onrendered 函数里面判断是否继续执行后续的 toImg 和 toPdf 方法了


3.2 下载的文件后缀丢失


有产品反馈下载完的文件没有 pdf 的后缀,我尝试了几个都是有的,于是要了相关的文件名信息,发现如果文件名中存在一些特殊的字符的时候就会产生这种情况,最终主动补全了后缀(开发过程中,一开始是手动写了,发现不写也没问题,然后去掉了,尴尬):


.save(fileName + '.pdf')

另外一种方法是可以把文件名中的特殊字符给过滤掉,但是为了保留原来的文件名,还是放弃了这种方案


3.3 跨域问题


需要加载图片的话,需要添加 Options | html2canvas useCORS 配置,并且源图片网站开放允许跨域的访问域名


3.4 元素切割问题


暂时没有发现太好的解决方案,只能从两方面去缓和,但还是存在,具体的配置说明可以看官网:html2pdf.js | Client-side HTML-to-PDF rendering using pure JS.


pagebreak: { mode: ['avoid-all', 'css'] },

margin: [0, 10, 0, 10], // top, left, bottom, right

因为切割的主要是分页的部分,我们把 top 和 bottom 的间隙都设置为0,尽可能缓和这种切割感。


而配合 pagebreak 属性中的 css mode,我们可以添加下面这段样式:


  * {
break-inside: avoid;
break-after: always;
break-before: always;
}

但是最终还是存在分页上被切割的元素,原因不详


4.  备选方案:HTML -> Text -> PDF


我认为备选方案是更加好的方案,但是却存在一些还没解决的问题,所以不建议在现网中使用。下面主要来讨论下这个方案开发过程中遇到的一些问题。


这个方案的核心是使用:GitHub - bpampuch/pdfmake: Client/server side PDF printing in pure JavaScript


pdfmake 是一个用于生成PDF文档的JavaScript库,服务端和客服端都可以使用。它的目标是简化PDF文档生成的复杂性,提供简单易用的API和清晰易读的文档定义。它支持多语言、自定义字体、图表、表格、图像、列表、页眉页脚等常见的PDF文档功能


我们可以在官方的 pdfmake.org/playground.… 上体验。简单贴一段官网中的案例:


// playground requires you to assign document definition to a variable called dd
var dd = {
content: [
{
text: 'This paragraph uses header style and extends the alignment property',
style: 'header',
alignment: 'center'
},
{
text: [
'This paragraph uses header style and overrides bold value setting it back to false.\n',
'Header style in this example sets alignment to justify, so this paragraph should be rendered \n',
'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Malit profecta versatur nomine ocurreret multavit, officiis viveremus aeternum superstitio suspicor alia nostram, quando nostros congressus susceperant concederetur leguntur iam, vigiliae democritea tantopere causae, atilii plerumque ipsas potitur pertineant multis rem quaeri pro, legendum didicisse credere ex maluisset per videtis. Cur discordans praetereat aliae ruinae dirigentur orestem eodem, praetermittenda divinum. Collegisti, deteriora malint loquuntur officii cotidie finitas referri doleamus ambigua acute. Adhaesiones ratione beate arbitraretur detractis perdiscere, constituant hostis polyaeno. Diu concederetur.'
],
style: 'header',
bold: false
}
],
styles: {
header: {
fontSize: 18,
bold: true,
alignment: 'justify'
}
}
}

最主要的话是做好 文档定义 这块的工作,传给 pdfmake ,生成文字版和清晰的PDF文件。


那我们如何通过 maked 生成的 html,翻译成 pdfmake 所需要的 JSON 格式的文档定义对象呢?观察上面的文档定义,content 主要是内容的定义,里面包含了内容主体,样式定义。其中的 style 指定了一个字符串,我们可以在 styles 中定义这个字符串对应的样式。


所以这里需要做的就是我们要把 html 中的每个标签,都用 pdfmake 的文档定义重新定义一份。可想而知,工作量会很大,幸亏这块已经有成熟的框架支持了:GitHub - Aymkdn/html-to-pdfmake: This module permits to convert HTML to the PDFMake format,参考 HTML to PDFmake online convertor 官网的案例,可以更快理解。


难点1 - 样式定义


借助 html-to-pdfmake ,标签的定义可以快速完成,但是我们还需要完成样式的定义,这又是一个不小的工作量,等于要我们上面提到的 GitHub - sindresorhus/github-markdown-css: The minimal amount of CSS to replicate the GitHub Markdown style 翻译一遍,否则和页面上展示的内容样式肯定不太一致,导致用户的疑惑。这块还没有精力去处理


难点2 - 字体支持


使用 pdfmake 默认提供的几个字体,并不支持中文和 emoji 表情,会看到一堆乱码,我们需要寻找一个能同时支持中英文和 emoji 表情的字体库。这里先说下结论,尚未找到这样的字体库,因为本身对字体库这块理解也不深。


这里还要考虑的一个问题是,即使找到了这样的字体库,它还需要支持不同操作系统的字体展示,我们知道平时打开PDF文件的时候,一些PDF阅读器如果是不支持的字体的话会做回退字体展示,但是有些PDF阅读器会直接全文档显示空白,这也是个头疼的事情。


后面找到了一个在 windows 、mac、安卓和苹果手机上都能正常展示的字体:GitHub - adobe-fonts/source-han-sans: Source Han Sans | 思源黑体 | 思源黑體 | 思源黑體 香港 | 源ノ角ゴシック | 본고딕,当然也少不了 chatgpt 的意见了:



而对于 emoji 文字的支持,推荐的是:fonts.google.com/noto,但是尝试了下没有成功使用,后续再研究了。这里考虑的方案是把 emoji 表情文字给过滤掉 😄。(其实发现腾讯文档导出PDF文件的时候,也是没有 emoji 表情的,估计确实不好处理)


先按照使用 source-han-sans 字体的方案来,参考:pdfmake 官方文档,我们就可以导入自己的字体库来使用了。官方推荐的是用在线的字体链接来导入,我们把文件上传到 cdn,然后下载下来使用就好了。这边建议预加载文字库,加快生成 PDF 的速度


欢迎收看~


作者:codytang
来源:juejin.cn/post/7234315967564103737
收起阅读 »

2023年35大龄程序员最后的挣扎

一、自身情况其实30岁的时候已经开始焦虑了,并且努力想找出路。提升技术,努力争增加自己的能力。努力争取进入管理层,可是卷无处不在,没有人离开这个坑位,你努力的成效很低。大环境我们普通人根本改变不了。自己大龄性价比不高,中年危机就是客观情况。无非就是在本赛道继续...
继续阅读 »


一、自身情况

我非科班出身,年龄到了35岁。然后剧本都差不多,2022年12月各种裁员,失业像龙卷风一样席卷社会各个角落。

  1. 其实30岁的时候已经开始焦虑了,并且努力想找出路。

  2. 提升技术,努力争增加自己的能力。

  3. 努力争取进入管理层,可是卷无处不在,没有人离开这个坑位,你努力的成效很低。

  4. 大环境我们普通人根本改变不了。

  5. 自己大龄性价比不高,中年危机就是客观情况。

  6. 无非就是在本赛道继续卷,还是换赛道卷的选择了。

啊Q精神:我还不是最惨的那一批,最惨的是19年借钱买了恒大的烂尾楼,并且在2021年就失业的那拨人。简直不敢想象,那真是绝望啊。心里不够坚强的,想不开轻生的念头都会有。我至少拿了点赔偿,手里还有些余粮,暂时饿不死。


二、大环境情况

  1. 大环境不好已经不是秘密了,整个经济走弱。大家不敢消费,对未来信心不足已经是板上钉钉的事了。

  2. 这剧本就是30年前日本的剧本,不敢说一摸一样。可以说大差不差了,互联网行业的薪资会慢慢的回归平均水平,或者技术要求在提升一个等级。

  3. 大部分普通人,还是做应用层拧螺丝,少部分框架师能造轮子也就是2:8理论。

  4. 能卷进这20%里,就能在上一层楼。也不是说这行就不行了,只不过变成了存量市场,而且坑位变少,人并没有变少还增加了。

  5. 不要怀疑自己的能力,这也不是你的问题了,是外部环境导致的市场萎缩。我们能做的就是,脱下孔乙己的长衫,先保证生活。努力干活,不违法乱纪做什么都是光荣了,不要带有色眼镜看待任何人。

三、未来出路

未来的出路在哪里?

这个我也很迷惑,因为大佬走的路,并不是我们这些普通的不能在普通的人能够走的通的。当然也有例外的情况,这些就是幸存者偏差了。

我先把chartGPT给的答应贴出来:


可以看到chartGPT还是给出,相对可行有效的方案。当然这些并不是每个人都适用。

我提几个普通人能做的建议(普通人还是围绕生存在做决策):

  1. 有存款的,并且了解一些行业的可以开店,比如餐饮店,花店,水果店等。

  2. 摆摊,国家也都改变政策了。

  3. 超市,配送员,外卖员。

  4. 开滴滴网约车。

以上都是个人不成熟的观点,jym多多包涵。

作者:可乐泡枸杞

来源:juejin.cn/post/7230656455808335930

收起阅读 »

程序员能有什么好出路?

我自己耶 从业10年了,经常在娱文中看到这种的文章,我怀疑是精准推送!!30岁以上的程序员该何去何从? - 知乎30岁: 程序员心中永远的痛?过了30岁,程序员该怎么办? - 阿里云开发者社区30岁转行程序员晚了吗?分享30岁转行的经历 - SegmentFa...
继续阅读 »

我自己耶


从业10年了,经常在娱文中看到这种的文章,我怀疑是精准推送!!

  1. 30岁以上的程序员该何去何从? - 知乎
  2. 30岁: 程序员心中永远的痛?
  3. 过了30岁,程序员该怎么办? - 阿里云开发者社区
  4. 30岁转行程序员晚了吗?分享30岁转行的经历 - SegmentFault
  5. 30岁后“大龄程序员"应该何去何从? - 脉脉
  6. 程序员:伤不起的三十岁 - 菜鸟教程
  7. 程序员迷茫:30岁以上的“大龄程码农”出路在哪?java码 ... - 掘金
  8. 30岁老程序员迷茫| Laravel - LearnKu


关于职场的焦虑无处不在,而这些文章也加重了我们的焦虑。就我个人而言,我也仔细想过这个问题,其实从本质上来说,只是个“竞争力”的问题。


如果你觉得自己没有竞争力了,那么你就会焦虑,而你又将焦虑的原因归结于一个你没办法改变的问题,那就是“年龄”。于是一个逻辑自洽的描述出来了:


30岁了,没啥竞争力,未来何去何从?

出路耶


我从事这个行业,其实是个人挺喜欢编程的,觉得编程是一件挺舒心的事情,所以没有考虑过换行。周围其实有一些同事,离开了这个行当,有一些赚了更多的钱,也有一些日子过的更不舒心,这里不予置评。


我简单的叙述一些可能的出路,这些出路没什么对错的区别,只是在我们人生抉择中,希望你能看到更多的选项。


技术深造


如果你在技术上有优势,这是一条可以走通的路子,未来的方向大致是“架构师”、“技术顾问”等等。这需要你有一些大型项目的经验,所以一些在大型公司就业的程序员,天然的拥有更多的机会。


通常技术深造主要是两部分:



  1. 技术视野,你需要一定的知识广度,对常用技术有深刻的理解,对部分不常用技术也要熟悉。

  2. 技术能力,有的时候,亲自动手能力、解决问题能力会很重要。


项目管理


很多程序员转行做了项目管理,其实在我们的日常工作中,项目管理一直伴随着我们,时长日久,我们对项目管理会变的更熟悉一些。这也造成了一些错觉,让我们觉得项目管理没那么难,“我去我也行”。


但是,项目管理从来不是一项普通的工作,相对于程序员,项目管理人员面临的环境会更加复杂。

  1. 面对客户。有时候,会遇见一些喜欢刁难我们的客户的。
  2. 面对团队。团队也可能不和谐。
  3. 计划乱了、工期排期、风险控制、质量管理、干系人管理等等专业知识。


自由职业


依赖于自己过硬的技术,可以承接一些外包的项目,成为一名自由的外包人员。



  1. 你的人际关系会很重要。周围有一些能打单的朋友,会让你工作的很舒服。

  2. 把事情做好,赢得信赖。

  3. 来自第三方平台的外包项目还是比较坑的,尽量做熟人生意。


跑单


当然,你在行业内可能会认识不少的朋友,他们的手里可能有些业务需要外包人员进行开发,那么拿下这些合同,找到自己朋友里面有时间做私活的人,然后我完成它。



  1. 你的人际关系更为重要。通常,这会给你带来财富。

  2. 做好自己的品牌,赢得认可,那么就有赢得钞票的机会。


插件独立开发者


一个人开发一个应用,然后上架,成功率是很低的。所以依托于平台,做一些平台内的插件,然后依托于平台推广,那么成功的几率会大一些。



  1. 你的技术能力很重要,毕竟没有专门的测试人员进行测试。

  2. 你选择的平台很重要,比如跨境电商、钉钉、微信、谷歌浏览器等等。

  3. 更加重要的是,你要对这个方向感兴趣。


独立开发者


如果你财富自由了,又喜欢编程,可以成为一名伟大的独立开发者,你脑海中的任何想法,都可以通过双手变为现实。



  1. 因为热爱,所以你会有更多的可能。

  2. 能力足够,可以参与开源的基金会,参与一些开源项目。

  3. 如果财富没自由,那也不影响我们在闲暇时间里追逐我们的梦想。


团购


IT行业是一个挺特殊的团体,他们的某些消费习惯趋于雷同,针对这些消费习惯和爱好,做一些团购,相信会赚到不少钱。



  1. 还是人际关系。

  2. 你喜欢做这些事情,从免费到收费循序渐进。

  3. 记住,双赢才能长久,IT行的聪明人是比较多的。


大公司养老团


找个大的,稳定的公司养老,但是也要留好退路,居安思危。


其他


比如炒股、搞理财的、做导游的、创业的……


每个人都会有自己的选择,有的人做好了准备,有的人还懵懵懂懂,2023年的行情如何还未可知,希望能长风破浪吧

作者:襄垣
来源:juejin.cn/post/7194295837265461305

收起阅读 »

一行代码就能完成的事情,为什么要写两行

web
今天休息休息,复习一下使用的简洁运算方式以及常用的单行代码 三元运算符 用三元运算符代替简单的if else if (age < 18) {   me = '小姐姐'; } else {   me = '老阿姨'; } 改用三元运算符,一行就能搞定 m...
继续阅读 »

今天休息休息,复习一下使用的简洁运算方式以及常用的单行代码


三元运算符


用三元运算符代替简单的if else


if (age < 18) {
  me = '小姐姐';
} else {
  me = '老阿姨';
}

改用三元运算符,一行就能搞定


me = age < 18 ? '小姐姐' : '老阿姨';

复杂的判断三元运算符就有点不简单易懂了


const you = "董员外"
const your = "菜鸡本鸡"
const me = you ?"点再看":your?"点赞":"分享"

判断


当需要判断的情况不止一个时,第一个想法就是使用 || 或运算符


if(
    type == 1 ||
    type == 2 ||
    type == 3 ||
    type == 4 ||
){
   //...
}

ES6中的includes一行就能搞定


if( [1,2,3,4,5].includes(type) ){
   //...
}

取值


在写代码的时候,经常会用到取值的操作


const obj = {
    a:1,
    b:2,
    c:3,
}
//老的取值方式
const a = obj.a;
const b = obj.b;
const c = obj.c;

老的取值方式,直接用对象名加属性名去取值。如果使用ES6的解构赋值一行就能搞定


const {a,b,c} = obj;

获取对象属性值


在编程的过程中经常会遇到获取一个值并赋给另一个变量的情况,在获取这个值时需要先判断一下这个对象是否存在,才能进行赋值


if(obj && obj.name){
  const name = obj.name
}

ES6提供了可选连操作符?.,可以简化操作


const name = obj?.name;

反转字符串


将一个字符串进行翻转操作,返回翻转后的字符串


const reverse = str => str.split('').reverse().join('');

reverse('hello world');   // 'dlrow olleh'

生成随机字符串


生成一个随机的字符串,包含字母和数字


const randomString = () => Math.random().toString(36).slice(2);
//函数调用
randomString();

数组去重


用于移除数组中的重复项


const unique = (arr) => [...new Set(arr)];

console.log(unique([1, 2, 2, 2, 3, 4, 4, 5, 6, 6]));

数组对象去重


去除重复的对象,对象的key值和value值都分别相等,才叫相同对象


const uniqueObj = (arr, fn) =>arr.reduce((acc, v) => {if (!acc.some(x => fn(v, x))) acc.push(v);return acc;}, []);
 
uniqueObj([{id1, name'大师兄'}, {id2, name'小师妹'}, {id1, name'大师兄'}], (a, b) => a.id == b.id)
// [{id: 1, name: '大师兄'}, {id: 2, name: '小师妹'}]

合并数据


当我们需要合并数据,并且去除重复值时,你是不是要用for循环? ES6的扩展运算符一行就能搞定!!!


const a = [1,2,3];
const b = [1,5,6];
const c = [...new Set([...a,...b])];//[1,2,3,5,6]

判断数组是否为空


判断一个数组是否为空数组,它将返回一个布尔值


const notEmpty = arr => Array.isArray(arr) && arr.length > 0;

notEmpty([1, 2, 3]);  // true

交换两个变量


//旧写法
let a=1;
let b=2;
let temp;
temp=a
a=b
b=temp

//新写法
[a, b] = [b, a];

判断奇还是偶


const isEven = num => num % 2 === 0;

isEven(996)

获取两个数之间的随机整数


const random = (minmax) => Math.floor(Math.random() * (max - min + 1) + min);

random(150);

检查日期是否为工作日


传入日期,判断是否是工作日


const isWeekday = (date) => date.getDay() % 6 !== 0;
console.log(isWeekday(new Date(20211111)));
// false 
console.log(isWeekday(new Date(20211113)));
// true

高级


滚动到页面顶部


不用引入element-ui等框架,一行代码就能实现滚动到顶部


const goToTop = () => window.scrollTo(00);
goToTop();

浏览器是否支持触摸事件


通过判断浏览器是否有ontouchstart事件来判断是否支持触摸


const touchSupported = () => {
  ('ontouchstart' in window || window.DocumentTouch && document instanceof window.DocumentTouch);
}
console.log(touchSupported());

当前设备是否为苹果设备


前端经常要兼容andriod和ios


const isAppleDevice = /Mac|iPod|iPhone|iPad/.test(navigator.platform);
console.log(isAppleDevice);
// Result: will return true if user is on an Apple device

复制内容到剪切板


使用 navigator.clipboard.writeText 来实现将文本复制到剪贴板


const copyToClipboard = (text) => navigator.clipboard.writeText(text);

copyToClipboard("双十一来了~");

检测是否是黑暗模式


用于检测当前的环境是否是黑暗模式,返回一个布尔值


const isDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches

console.log(isDarkMode)

网站变成黑白


有时候网站在某种特定的情况下,需要使整个网站变成黑白的颜色


filter:grayscale(100%)

只需要将这一行代码filter:grayscale(100%)放到body上,一下就能致黑



一行代码就能完成的事情,凭什么写两行!!!


作者:董员外
来源:juejin.cn/post/7150275723784585246
收起阅读 »

写出干净的 JavaScript 5 个小技巧

web
降低阅读负担,启发创作心智,轻松学习 JavaScript 技巧,日拱一卒,jym,冲~ 1. 将数字定义为常量 我们常常会用到数字,比如以下代码: const isOldEnough = (person) => { return person.g...
继续阅读 »



降低阅读负担,启发创作心智,轻松学习 JavaScript 技巧,日拱一卒,jym,冲~


take-it-easy-relax.gif


1. 将数字定义为常量


我们常常会用到数字,比如以下代码:


const isOldEnough = (person) => {
return person.getAge() >= 100;
}

谁知道这个 100 具体指的是什么?我们通常需要结合函数上下文再推测、判断这个 100 它可能是具体代表一个什么值。


如果这样的数字有多个的话,一定会很容易造成更大的困惑。


写出干净的 JavaScript:将数字定义为常量


即可清晰的解决这个问题:


const AGE_REQUIREMENT = 100;
const isOldEnough = (person) => {
return person.getAge() >= AGE_REQUIREMENT;
}

现在,我们通过声明常量的名字,即可立马读懂 100 是“年龄要求”的意思。修改时也能迅速定位、一处修改、多处生效。


2. 避免将布尔值作为函数参数


将布尔值作为参数传入函数中是一种常见的容易造成代码混乱的写法。


const validateCreature = (creature, isHuman) => {
if (isHuman) {
// ...
} else {
// ...
}
}

布尔值作为参数传入函数不能表示出明确的意义,只能告诉读者,这个函数将会有判断发生,产生两种或多种情况。


然而,我们提倡函数的单一职责原则,所以:


写出干净的 JavaScript:避免将布尔值作为函数参数


const validatePerson = (person) => {
// ...
}
const validateCreature = (creature) => {
// ...
}

3. 将多个条件封装


我们经常会写出这样的代码:


if (
person.getAge() > 30 &&
person.getName() === "simon" &&
person.getOrigin() === "sweden"
) {
// ...
}

不是不行,只是隔久了会一下子看不懂这些判断到底是要干嘛的,所以建议把这些条件用变量或函数进行封装。


写出干净的 JavaScript:将多个条件封装


const isSimon =
person.getAge() > 30 &&
person.getName() === "simon" &&
person.getOrigin() === "sweden";
if (isSimon) {
// ...
}

或者


const isSimon = (person) => {
return (
person.getAge() > 30 &&
person.getName() === "simon" &&
person.getOrigin() === "sweden"
);
};
if (isSimon(person)) {
// ...
}

噢,原来这些条件是为了判断这个人是不是 Simon ~


这样的代码是声明式风格的代码,更易读。


4. 避免否定的判断条件


条件判断中,使用否定判断,会额外造成一种思考负担。


比如下面的代码,条件 !isCreatureNotHuman(creature) 双重否定,读起来就会觉得有点费劲。


const isCreatureNotHuman = (creature) => {
// ...
}

if (!isCreatureNotHuman(creature)) {
// ...
}

写出干净的 JavaScript:避免否定的判断条件


改写成以下写法则读起来更轻松,虽然这只是一个很小的技巧,但是在大量的代码逻辑中,多处去遵循这个原则,肯定会很有帮助。


很多时候读代码就是读着读着,看到一个“很烂”的写法,就忍不了了,细节会叠加,千里之堤溃于蚁穴。


const isCreatureHuman = (creature) => {
// ...
}
if (isCreatureHuman(creature)) {
// ...
}

5. 避免大量 if...else...


这一点,本瓜一直就有强调:


🌰比如以下代码:


if(x===a){
res=A
}else if(x===b){
res=B
}else if(x===c){
res=C
}else if(x===d){
//...
}

改写成 map 的写法:


let mapRes={
a:A,
b:B,
c:C,
//...
}
res=mapRes[x]

🌰再比如以下代码:


const isMammal = (creature) => {
if (creature === "human") {
return true;
} else if (creature === "dog") {
return true;
} else if (creature === "cat") {
return true;
}
// ...
return false;
}

改写成数组:


const isMammal = (creature) => {
const mammals = ["human", "dog", "cat", /* ... */];
return mammals.includes(creature);
}

写出干净的 JavaScript:避免大量 if...else...


所以,当代码中出现大量 if...else... 时,多想一步,是否能稍加改造让代码看起来更加“干净”。




小结:上述技巧可能在示例中看起来不值一提,但是在实际的项目中,当业务逻辑复杂起来、当代码量变得很大的时候,这些小技巧一定能给出正面的作用、帮助,甚至超乎想象。



OK,以上便是本篇分享。点赞关注评论,为好文助力👍


我是掘金安东尼 🤠 100 万人气前端技术博主 💥 INFP 写作人格坚持 1000 日更文 ✍ 关注我,安东尼陪你一起度过漫长编程岁月 🌏



作者:掘金安东尼
来源:juejin.cn/post/7131994944067076127
收起阅读 »

前端如何实现同时发送多个相同请求时只发送一个?

web
原生实现 为了控制并发请求,可以使用以下两种常见的方式: 防抖 防抖是指在一段时间内多次触发同一事件,只执行最后一次触发的操作。在前端中,可以利用定时器来实现防抖的效果。具体实现方法如下: function debounce(func, delay) { ...
继续阅读 »

原生实现


为了控制并发请求,可以使用以下两种常见的方式:


防抖


防抖是指在一段时间内多次触发同一事件,只执行最后一次触发的操作。在前端中,可以利用定时器来实现防抖的效果。具体实现方法如下:


function debounce(func, delay) {
let timer;
return function() {
clearTimeout(timer);
timer = setTimeout(() => {
func.apply(this, arguments);
}, delay);
}
}

在发送请求的地方使用防抖函数,如下所示:


const sendRequest = debounce(() => {
// 发送请求的代码
}, 500);

上述代码中,sendRequest 是一个防抖函数,它将在 500ms 后执行。如果在 500ms 内再次触发 sendRequest 函数,计时器会被重新启动,并且等待 500ms,以确保只有最后一次触发才会发送请求。


节流


节流是指在一段时间内只执行一次操作。与防抖不同的是,节流是指在一定的时间间隔内只执行一次操作。在前端中,可以使用定时器来实现节流的效果。具体实现方法如下:


function throttle(func, delay) {
let timer;
return function() {
if (!timer) {
timer = setTimeout(() => {
func.apply(this, arguments);
timer = null;
}, delay);
}
}
}

在发送请求的地方使用节流函数,如下所示:


const sendRequest = throttle(() => {
// 发送请求的代码
}, 500);

上述代码中,sendRequest 是一个节流函数,它将在 500ms 后执行。如果在 500ms 内再次触发 sendRequest 函数,由于计时器还没有结束,函数不会执行任何操作。只有当计时器结束后才会再次触发请求。


Vue + Axios


在 Vue 中使用 Axios 发送请求,可以使用 Axios 的 CancelToken 来取消重复的请求,从而实现并发多个相同的请求只发送一个的效果。


具体实现方法如下:



  1. 创建 CancelToken


首先,需要创建一个 CancelToken,用于取消请求。在 Vue 中,可以在组件的 data 中定义一个 cancelToken 对象:


data() {
return {
cancelToken: axios.CancelToken.source().token
}
}


  1. 发送请求时使用 CancelToken


在发送请求时,需要将定义的 cancelToken 对象作为配置的 cancelToken 属性传递给 Axios:


axios.get(url, {
cancelToken: this.cancelToken
}).then(response => {
// 处理响应结果
}).catch(error => {
// 处理请求错误
});


  1. 取消重复的请求


在发送新的请求之前,需要取消之前正在进行的相同请求。可以通过判断上一次请求的 CancelToken 和当前请求的 CancelToken 是否相同来实现:


if (this.lastRequestCancelToken) {
this.lastRequestCancelToken.cancel('取消重复的请求');
}
this.lastRequestCancelToken = this.cancelToken;

上述代码中,lastRequestCancelToken 是用于保存上一次请求的 CancelToken 对象的变量。在发送新的请求之前,需要先取消之前正在进行的相同请求,并将当前的 CancelToken 对象赋值给 lastRequestCancelToken 变量。


完整的实现代码如下:


data() {
return {
cancelToken: axios.CancelToken.source().token,
lastRequestCancelToken: null
}
},
methods: {
fetchData() {
if (this.lastRequestCancelToken) {
this.lastRequestCancelToken.cancel('取消重复的请求');
}
this.lastRequestCancelToken = this.cancelToken;
axios.get(url, {
cancelToken: this.cancelToken
}).then(response => {
// 处理响应结果
}).catch(error => {
// 处理请求错误
});
}
}

上述代码中,fetchData 是发送请求的方法,在方法中使用了 CancelToken 来控制并发多个相同的请求只发送一个。


使用 React 实现


如果你正在使用 React,你可以使用 axios 库来实现控制只发送一个请求的功能。具体实现如下:


import React, { useState } from 'react';
import axios from 'axios';

function App() {
const [requestCount, setRequestCount] = useState(0);

function fetchData() {
if (requestCount === 0) {
setRequestCount(1);
axios.get('http://example.com/api/data')
.then(response => {
console.log(response.data);
setRequestCount(0);
})
.catch(error => {
console.error(error);
setRequestCount(0);
});
}
}

return (
<div>
<button onClick={fetchData}>Fetch Data</button>
</div>

);
}

export default App;

这段代码使用了 React 的 useState 钩子函数,定义了一个状态变量 requestCount,用于记录当前正在发送的请求数量。在发送请求之前,先检查是否已经有一个请求正在处理。如果没有,则将 requestCount 设为 1,表示有一个请求正在处理。在请求成功或失败后,将 requestCount 设为 0,表示请求已经处理完毕。


更多题目


juejin.cn/column

/7201…

收起阅读 »

偏爱console.log的你,肯定会觉得这个插件泰裤辣!

web
前言 毋庸置疑,要说前端调试代码用的最多的,肯定是console.log,虽然我现在 debugger 用的比较多,但对于生产环境、小程序真机调试,还是需要用到 log 来查看变量值,比如我下午遇到个场景:选择完客户后返回页面,根据条件判断是否弹窗: if (...
继续阅读 »

前言


毋庸置疑,要说前端调试代码用的最多的,肯定是console.log,虽然我现在 debugger 用的比较多,但对于生产环境、小程序真机调试,还是需要用到 log 来查看变量值,比如我下午遇到个场景:选择完客户后返回页面,根据条件判断是否弹窗:


if (global.isXXX || !this.customerId || !this.skuList.length) return

// 到了这里才会执行弹窗的逻辑

这个时候只能真机调试,看控制台打印的值是怎样的,但对于上面的条件,如果你这样 log 的话,那控制台只会显示:


console.log(global.isXXX, !this.customerId, !this.skuList.length)
false false false

且如果参数比较多,你可能就没法立即将 log 出的值对应到相应的变量,还得回去代码里面仔细比对。


还有一个,我之前遇到过一个项目里一堆 log,同事为了方便看到 log 是在哪一行,就在 log 的地方加上代码所在行数,但因为 log 那一刻已经硬编码了,而代码经常会添加或者删除,这个时候行数就不对了:




比如你上面添加了一行,这里的所有行数就都不对了



所以,我希望 console.log 的时候:



  1. 控制台主动打印源码所在行数

  2. 变量名要显示出来,比如上面例子的 log 应该是 global.isXXX = false !this.customerId = false !this.skuList.length = false

  3. 可以的话,每个参数都有分隔符,不然多个参数看起来就有点不好分辨


即源码不做任何修改:



而控制台显示所在行,且有变量名的时候添加变量名前缀,然后你可以指定分隔符,如换行符\n



因为之前有过 babel 插件的经验,所以想着这次继续通过写一个 babel plugin 实现以上功能,所以也就有了babel-plugin-enhance-log,那究竟怎么用?很简单,下面 👇🏻 我给大家说说。


babel-plugin-enhance-log


老规矩,先安装插件:


pnpm add babel-plugin-enhance-log -D
# or
yarn add babel-plugin-enhance-log -D
# or
npm i babel-plugin-enhance-log -D

然后在你的 babel.config.js 里面添加插件:


module.exports = (api) => {
return {
plugins: [
'enhance-log',
...
],
}
}

看到了没,就是这么简单,之后再重新启动,去你的控制台看看,小火箭咻咻咻为你刷起~



options


上面了解了基本用法后,这里再给大家说下几个参数,可以看下注释,应该说是比较清楚的:


interface Options {
/**
* 打印的前缀提示,这样方便快速找到log 🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀
* @example
* console.log('line of 1 🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀', ...)
*/

preTip?: string
/** 每个参数分隔符,默认空字符串,你也可以使用换行符\n,分号;逗号,甚至猪猪🐖都行~ */
splitBy?: boolean
/**
* 是否需要endLine
* @example
* console.log('line of 1 🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀', ..., 'line of 10 🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀')
* */

endLine?: boolean
}

然后在插件第二个参数配置即可(这里偷偷跟大家说下,通过/** @type {import('babel-plugin-enhance-log').Options} */可以给配置添加类型提示哦):



return {
plugins: [
['enhance-log', enhanceLogOption],
],
...
}

比如说,你不喜欢小 🚀,你喜欢猪猪 🐖,那可以配置 preTip 为 🐖🐖🐖🐖🐖🐖🐖🐖🐖🐖:



比如说,在参数较多的情况下,你希望 log 每个参数都换行,那可以配置 splitBy 为 \n



或者分隔符是;:



当然,你也可以随意指定,比如用个狗头🐶来分隔:



又比如说,有个 log 跨了多行,你希望 log 开始和结束的行数,中间是 log 实体,那可以将 endLine 设置为 true:





我们可以看到开始的行数是13,结束的行数是44,跟源码一致



实现思路


上面通过多个例子跟大家介绍了各种玩法,不过,我相信还是有些小伙伴想知道怎么实现的,那我这里就大致说下实现思路:


老规格,还是通过babel-ast-explorer来查看


1.判断到 console.log 的 ast,即 path 是 CallExpression 的,且 callee 是 console.log,那么进入下一步



2.拿到 console.log 的 arguments,也就是 log 的参数



3.遍历 path.node.arguments 每个参数



  • 字面量的,则无须添加变量名

  • 变量的,添加变量名前缀,如 a =

  • 如果需要分隔符,则根据传入的分隔符插入到原始参数的后面


4.拿到 console.log 的开始行数,创建一个包含行数的 StringLiteral,同时加上 preTip,比如上面的 🚀🚀🚀🚀🚀🚀🚀,或者 🐖🐖🐖🐖🐖🐖🐖🐖🐖🐖,然后 unshift,放在第一个参数的位置


5.拿到 console.log 的结束行数,过程跟第 4 点类似,通过 push 放到最后一个参数的位置



6.在这过程中需要判断到处理过的,下次进来就要跳过,防止重复添加


以下是源码的实现过程,有兴趣的可以看看:


import { declare } from '@babel/helper-plugin-utils'
import generater from '@babel/generator'
import type { StringLiteral } from '@babel/types'
import { stringLiteral } from '@babel/types'


const DEFAULT_PRE_TIP = '🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀'
const SKIP_KEY = '@@babel-plugin-enhance-logSkip'

function generateStrNode(str: string): StringLiteral & { skip: boolean } {
const node = stringLiteral(str)
// @ts-ignore
node.skip = true
// @ts-ignore
return node
}

export default declare<Options>((babel, { preTip = DEFAULT_PRE_TIP, splitBy = '', endLine = false }) => {
const { types: t } = babel
const splitNode = generateStrNode(splitBy)
return {
name: 'enhance-log',
visitor: {
CallExpression(path) {
const calleeCode = generater(path.node.callee).code
if (calleeCode === 'console.log') {
// add comment to skip if enter next time
const { trailingComments } = path.node
const shouldSkip = (trailingComments || []).some((item) => {
return item.type === 'CommentBlock' && item.value === SKIP_KEY
})
if (shouldSkip)
return

t.addComment(path.node, 'trailing', SKIP_KEY)

const nodeArguments = path.node.arguments
for (let i = 0; i < nodeArguments.length; i++) {
const argument = nodeArguments[i]
// @ts-ignore
if (argument.skip)
continue
if (!t.isLiteral(argument)) {
if (t.isIdentifier(argument) && argument.name === 'undefined') {
nodeArguments.splice(i + 1, 0, splitNode)
continue
}
// @ts-ignore
argument.skip = true
const node = generateStrNode(`${generater(argument).code} =`)

nodeArguments.splice(i, 0, node)
nodeArguments.splice(i + 2, 0, splitNode)
}
else {
nodeArguments.splice(i + 1, 0, splitNode)
}
}
// the last needn't split
if (nodeArguments[nodeArguments.length - 1] === splitNode)
nodeArguments.pop()
const { loc } = path.node
if (loc) {
const startLine = loc.start.line
const startLineTipNode = t.stringLiteral(`line of ${startLine} ${preTip}:\n`)
nodeArguments.unshift(startLineTipNode)
if (endLine) {
const endLine = loc.end.line
const endLineTipNode = t.stringLiteral(`\nline of ${endLine} ${preTip}:\n`)
nodeArguments.push(endLineTipNode)
}
}
}
},
},
}
})

对了,这里有个问题是,我通过标记 path.node.skip = true 来跳过,但是还是会多次进入:


if (path.node.skip) return
path.node.skip = true

所以最终只能通过尾部添加注释的方式来避免多次进入:



有知道怎么解决的大佬还请提示一下,万分感谢~


总结


国际惯例,我们来总结一下,对于生产环境或真机调试,或者对于一些偏爱 console.log 的小伙伴,我们为了更快在控制台找到 log 的变量,通常会添加 log 函数,参数变量名,但前者一旦代码位置更改,打印的位置就跟源码不一致,后者又得重复写每个参数变量名的字符串,显得相当的麻烦。


为了更方便地使用 log,我们实现了个 babel 插件,功能包括:



  1. 自动打印行数

  2. 可以根据个人喜好加上 preTip,比如刷火箭 🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀,或者可爱的小猪猪 🐖🐖🐖🐖🐖🐖🐖🐖🐖🐖

  3. 同时,对于有变量名的情况,可以加上变量名前缀,比如 const a = 1, console.log(a) => console.log('a = ', a)

  4. 还有,我们可以通过配置 splitBy、endLine 来自主选择任意分隔符、是否打印结束行等功能


最后


不知道大家有没有在追不良人,我是从高三追到现在。今天是周四,不良人第六季也接近尾声了,那就谨以此文来纪念不良人第六季的完结吧~



好了,再说一句,如果你是个偏爱 console.log 的前端 er,那请你喊出:泰裤辣(逃~)


作者:暴走老七
来源:juejin.cn/post/7231577806189133884
收起阅读 »

python简单实现校园网自动认证,赶紧部署到你的路由器上吧

2023-05-20:使用python库来实现定时任务,不再依赖系统的定时任务,部署起来容易100倍 原文链接 python实现校园网自动认证 - 歌梦罗 - 努力生活,努力热爱 (gmero.com) 说在前面 大部分校园网都需要认证才能够上网,而且就...
继续阅读 »

2023-05-20:使用python库来实现定时任务,不再依赖系统的定时任务,部署起来容易100倍




原文链接 python实现校园网自动认证 - 歌梦罗 - 努力生活,努力热爱 (gmero.com)



说在前面


大部分校园网都需要认证才能够上网,而且就算你保持在线隔一段时间也会将你强行踢下线,导致游戏中断,自己挂在校园网的web程序宕机等头疼的问题。


本文主要是使用python与主机的计划任务来实现校园网的自动认证,需要注意的是,我们学校的校园网认证系统与认证方式可能与你们学校的不一样,所以主要是分享我的实现思路,以供大家参考参考。


准备工作:



  • 一台电脑

  • 一台用于挂载自动认证脚本的服务器(也可以是你的电脑,或者不那么硬的路由器之类的)


了解校园网认证的过程



这是最繁琐的一步,在网上见过的这么多认证的教程,感觉我们学校是最复杂的



其实总结来说这一步就是反复的在浏览器f12里观察你在认证的时候都经过了哪些程序,我们学校的认证流程大概是这样的



我们需要传入以下参数进行认证,其中后三项传空值,因为是本地认证后面脚本就懒得加密密码了,把passwordEncrypt传false,password直接明文就可以了,可能不安全,但校园网无所谓。



看起来感觉实现很容易对不对?python随便几行request就能实现了的。但有以下几个问题:



  • 重复的认证是无效的,不会重新计算在线时间(到点还是踢你)

  • 高频的认证可能导致莫名奇妙的bug,比如明明已经认证成功了而认证服务器那边确觉得你没有认证,导致认证界面直接卡死(因为123.123.123.123在你已经接入互联网的情况下是无法访问的)

  • 多次认证还可能导致出现验证码,目前脚本还无法处理验证码


于是我们在程序中需要判断是否已经认证了,在已经认证的情况下运行程序需要先登出来重置在线时间从而推迟强制下线的时间(于是,我们又需要找到两个请求:一个是判断认证情况的,一个是退出登录的)


经过疯狂的F12和api调试之后,找到了172.208.2.102/eportal/InterFace.do?method=logout172.208.2.102/eportal/InterFace.do?method=getUserInfo两个api来进行登出和判断操作


python实现


认证流程用python的requests库很容易就能实现,定时任务用schedule来创建也很容易。


import re
import time
import urllib.parse
from urllib.parse import urlparse

import requests
import schedule

apiUrl = "http://172.208.2.102/eportal/InterFace.do"
authUrl = apiUrl + "?method=login"
logoutUrl = apiUrl + "?method=logout"
getQueryUrl = "http://123.123.123.123/"
getUserInfoUrl = apiUrl + "?method=getOnlineUserInfo"
webService = "中国电信"


# 判断是否已经认证了
def is_authed():
   try:
       resp = requests.get(getUserInfoUrl, timeout=3)
       if resp.status_code != 200:
           # 网络问题直接退出
           print("判断认证状态失败")
           return False
       else:
           json_data = resp.json()
           result = json_data["result"]
           return result != "fail"
   except requests.RequestException:
       print("判断认证状态失败: 请求失败")
       return False


# 获取query认证信息
def get_query_str():
   try:
       resp = requests.get(getQueryUrl, timeout=8)
       if resp.status_code != 200:
           print("获取query信息失败")
           return False
       pattern = "href='(.*)'"
       match = re.search(pattern, resp.text)

       if match:
           url = urlparse(match.group(1))
           return url.query
       else:
           return
   except requests.RequestException:
       print("获取query信息失败: 请求失败")
       return False


# 认证
def do_auth():
   query_str = get_query_str()
   if not query_str:
       return False
   # 表单数据
   data = {
       "userId": "871390441",
       "password": "yourpassword",
       "service": urllib.parse.quote(webService),
       "queryString": query_str,
       "passwordEncrypt": "false",
       "operatorPwd": ,
       "operatorUserId": ,
       "validcode":
  }

   try:
       resp = requests.post(authUrl, data)
       if resp.status_code == 200 and resp.json()["result"] == "success":
           print("认证成功")
           return True
       else:
           print("认证失败")
           return False
   except requests.RequestException:
       print("认证失败: 请求失败")
       return False


# 退出登录
def do_logout():
   resp = requests.get(logoutUrl)

   if resp.status_code == 200:
       if resp.json()["result"] == "success":
           print("退出登录成功")
           return True
       else:
           print("退出登录失败: " + resp.json()["message"])
           return False
   else:
       print("退出登录失败: 网络错误")
       return False


# 一次认证流程
def auth_job():
   print("\n====校园网自动认证开始====")
   if is_authed():
       if do_logout():
           do_auth()
   else:
       do_auth()
   print("====校园网自动认证结束====\n")


if __name__ == '__main__':
   auth_job()
   # 定时任务
   schedule.every().day.at("12:00").do(auth_job)
   schedule.every().day.at("00:00").do(auth_job)

   while True:
       schedule.run_pending()
       time.sleep(1)


代码部分有了思路之后其实就很简单了,接下来就是最重要的部署环节了。


部署到服务器


我这里以部署到我的破烂x86linux服务器上为例(windows就更简单了,直接运行python程序即可),采取docker部署的方式


首先写Dockerfile, 这里我就不解释了,不懂的话找一找资料吧


FROM python:3.11-slim-bullseye

WORKDIR /app

ADD . /app

RUN pip3 config set global.index-url http://mirrors.aliyun.com/pypi/simple && \
  pip3 config set install.trusted-host mirrors.aliyun.com && \
  pip install --upgrade pip &&\
  pip3 install requests &&\
  pip3 install schedule


CMD python3 -u main.py

然后在当前文件夹输入命令来建立镜像和运行容器,很简单对不对


docker build -t autoauth:v1 . 
docker run --restart always --name myautoauth autoauth:v1

写在最后


以上操作在我这是完美运行的,需要一些折腾,但你能看完我这篇博客说明你肯定也是个喜欢折腾的人吧。上面的一些命名方法路径之类的不一定是绝对的。


作者:歌梦罗
来源:juejin.cn/post/7234864940799213625
收起阅读 »

环信 uni-app-demo 升级改造计划——整体代码重构优化(二)

概述本次关于 uni-app 代码整体重构工作,基于上一期针对 uni-app 官网 demo 从 vue2 迁移 vue3 框架衍生而来,在迁移过程中有明显感知,目前的项目存在的问题为,项目部分代码风格较为不统一,命名不够规范,注释不够清晰、可读性...
继续阅读 »

概述

本次关于 uni-app 代码整体重构工作,基于上一期针对 uni-app 官网 demo 从 vue2 迁移 vue3 框架衍生而来,在迁移过程中有明显感知,目前的项目存在的问题为,项目部分代码风格较为不统一,命名不够规范,注释不够清晰、可读性差、以造成如果复用困难重重,本地重构期望能够充分展示 api 在实际项目中的调用方式,尽可能达到示例代码可移植,或能辅助进行即时通讯功能二次开发的能力。

目的

  • 使代码更加可读。

  • 简化或去除冗余代码。

  • 部分组件以及逻辑重命名、重拆分、重合并。

  • 增加全局状态管理。

  • 修改 SDK 引入方式为通过 npm 形式引入

  • 收束 SDK 大部分 API 到统一文件、方便管理调用。

  • 升级 SDK api 至最新的调用方式(监听、发送消息)

  • 增加会话列表接口、消息漫游接口。

  • SDK 指环信 IM uni-app SDK

重构计划

一、修改原 WebIM 的导出导入使用方式。

目的

  1. 现有 uniSDK 已支持 npm 形式导入。
  2. 原有实例化代码与 config 配置较为混乱不够清晰。
  3. 分离初始化以及配置形成独立文件方便管理。

实现

  1. 项目目录中创建 EaseIM 文件夹并创建 index.js,在 index.js 中完成导入 SDK 并实现实例化并导出。
  2. EaseIM -> config 文件夹并将 SDK 中相关配置在此文件中书写并导出供实例化使用。

影响(无影响)

二、引入 pinia 进行状态管理

pinia 还能通过$reset()方法即可完成对某个 store 的初始化,利用该方法可以非常方便的在切换账号时针对缓存在 stores 中的数据初始化,防止切换后的账号与上一个账号的数据造成冲突。

目的

  1. 存放 SDK 部分数据以及状态(登录状态、会话列表数据、消息数据)
  2. 方便各组件状态或数据取用避免数据层层传递。
  3. 用以平替原有本地持久化数据存储。
  4. 可以替代原有 disp 发布订阅管理工具,因为 store 中状态改变,各组件可以进行重新计算或监听,无需通过发布订阅通知改变状态。

实现

  1. 在 mian.js 中引入 pinia,并挂载
//Pinia
import * as Pinia from 'pinia';
export function createApp() {
const app = createSSRApp(App);
app.use(Pinia.createPinia());
return {
app,
Pinia,
};
}
  1. 项目目录中新建 stores 并创建各个所需 store,类似目录如下:

影响(无影响)

三、重新梳理 App.vue 根组件中代码

目的

  1. 简化项目中 App.vue 根组件中的冗长代码。
  2. 迁移根组件中的监听代码。
  3. globalData,method 中代码转为 stores 中或剔除。
  4. disp 代码剔除。

实现

  1. App.vue 中的监听嵌入至 EaseIM 文件夹下的 listener 集中管理,并在 App.vue 中重新进行挂载
  2. import ‘@/EaseIM’;从而实现实例化 SDK。
  3. 将需要 IM 连接成功后调用的数据,合为一个方法中,并在 onConnected 触发后调用。
  4. 部分关于 SDK 调用的代码迁入至 EaseIM 文件夹下的 imApis 文件夹中
  5. 部分有关 SDK 的工具方法代码迁入至 EaseIM 文件夹下的 utils 文件夹中

影响

App.vue 改动相对较大,主要为监听的迁移,一部分方法迁移至 stores 中,并且需要重新进行监听的挂载。具体代码可在后续迁移前后比对中看到,或者文尾的看到 github 中看到代码地址。

四、优化 login 页面代码

目的

原有 login 组件登录部分代码比较冗长并且可读性较差因此进行优化。

实现

  1. 删除原有操作 input 的代码,改为通过 v-model 双向绑定。
  2. 拆分登录代码为通过 username+password,以及手机号+验证码两个登录方法。
  3. 增加登录存储 token 方法,方便后续重连时通过用户名+token 形式进行重新登录。
  4. 登录成功之后将登录的 id 以及手机号信息 set 进入到 stores 中。

影响(无影响)

五、增加 home 页面

目的

  1. 作为 Conversation、Contacts、Me 三个核心页面容器组件,方便页面切换管理。
  2. 作为 Tabbar 的容器组件

实现

  1. 去除原有会话、联系人、我的(原 setting 页面)pages.json 的路由配置,增加 home 页面路由相关配置。
  2. pages 中增加 home 组件,并以组件化的形式引入三个核心页面组件。
  3. 项目根目录中新建 layout 文件夹并增加 tabbar 组件,将三个页面中的 tabbar 功能抽离至 tabbar 组件中,并增加相应切换逻辑。

影响

此改动主要影响为要涉及到将原 setting 组件改为 Me 组件,并将三个原有页面 pages.json 删除并在 home 中引入,并无其他副作用。

六、重构 Conversation、Contacts、Me 等基础组件

目的

  1. 将原有数据(会话列表数据,联系人数据,昵称头像数据)来源切换为从 SDK 接口+stores 中获取。
  2. 去除组件内的 disp 发布订阅相关代码,以及 WebIM 的使用。
  3. 调整原组件代码中的不合理的命名,去除不再使用的方法简化该组件代码。

实现

以 Conversation 组件举例

  1. 以 SDK 接口 getConversationlist 获取会话列表数据,缓存至 stores 中并做排序处理,在组件中使用计算属性获取作为新的会话列表数据来源。
  2. 由于会话列表的更新是动态的,因此不再需要 disp 订阅一系列的事件进行处理,因此相关代码可以开始进行删除。
  3. 原有的通过会话列表跳转至联系人页面或者其他群组页面命名改为单词从 into 改为 entry 并改为驼峰命名,经过改造该组件用不到的方法则完全删除。

影响

主要影响则是这些组件内的逻辑代码会有从结构以及数据源会有较大变化,需要边改造边验证,并且会与 stores、EaseIM 等组件有较大的关系,需要耐心进行调整。

七、增加 emChatContainer 组件

目的

  1. 新增此组件命名更为语义化,能够通过组件名看出其实际功能为 emChat 聊天页组件容器。
  2. 合并原有 singleChatEntry 组件以及 groupChatEntry 组件,两个相似功能组件至统一的一个 emChatContainer 内。

实现

  1. 在 pages 下新建一个名为 emChatContainer 的组件,并先将 components 下的 chat 组件参考 singleChatEntry 组件引入,并在 pages 中配置对应路由路径映射。
  2. 观察发现该组件作为 chat 组件容器,主要向下传递两个核心参数,1)目标 ID(也就是聊天的目标环信 ID)。2)chatType(也就是目标聊天的类型,常规为单聊、群聊。),且这两个核心参数经常被 chat 组件中的各个子组件用到,一层层向下传递较为繁琐,因此使用到 Vue 组件传参方法之一的,provide、inject 方式将参数注册并向下传递下去。
  3. 完成合并之后将 singleChatEntry、groupChatEntry 删去,并且将原有用到向该组件跳转的方法路径全部指向 emChatContainer,且在 pages.json 中删除对应的页面路径。

影响

从会话进入到聊天页、从联系人、群组页面进入到聊天页的路由跳转路径全部改为 emChatContainer,并且将会改变 chat 组件使用 targetId(聊天目标 ID)以及 chatType 的方式,因为需要改为通过 inject 接收。

八、emChat 组件重构

目的

  1. 改写该组件下不合理的文件命名。
  2. 删除非必要的 js 文件或组件。
  3. 该组件内各个功能子组件进行局部代码重构。

实现

  1. 配合 emChatContainer 将 chat 组件改名为 emChat。
  2. 删除其组件内的 msgpackager.js、msgstorage.js、msgtype.js、pushStorage.js,这几个 js 文件。
  3. messagelist inputbar 改为驼峰命名。
  4. messageList 组件内的消息列表来源改为从 stores 中获取,增加下拉通过 getHistroyMessage 获取历史消息。
  5. 子组件内接收目标 id 以及消息类型改为通过 inject 接收。
  6. msgType 从 EaseIM/constant 中获取。
  7. 发送消息 API 全部改为 SDK4.xPromise 写法,在 EaseIM/imApis/emMessages 统一并导出,在需要的发送的组件中导入调用,剔除原有发送消息的方式。

影响

该组件调整难度最大,因为牵扯的组件,以及需要新增的调整的代码较多,需要逐个组件修改并验证,具体代码将在下方局部展示。详情请参看源码地址。

九、新增重连中提示监听回调

目的

能够在 IM websocket 断开的时候有相应的回调出来,并给到用户相应的提示。

实现

在 addEventHandler 监听中增加 onReconnecting 监听回调,并且在实际触发的时候增加 Toast 提示监听 IM 正在重连中。

PS:onReconnecting 属于实验性回调。

影响(无影响)


由于篇幅限制,本文的下半部分请参考:https://blog.csdn.net/huan132456765/article/details/130763984

友情链接

最后多说一句,如果觉得有帮助请点赞支持一下!本 demo 还有三期计划(增加音视频功能),敬请期待!

友情链接

最后多说一句,如果觉得有帮助请点赞支持一下!本 demo 还有三期计划(增加音视频功能),敬请期待!

收起阅读 »

【思考】时间不等人,学会聚焦重要事情,享受现在的快乐!

时间对每个人都是有限的 时间是一种奢侈品,在许多情况下,我们不能在拥有更多的财富和资源,但我们可以通过聪明的时间管理来确保我们能够在有限的时间内做更多的事情。所以我们必须学会如何利用时间,找出哪些对我们最重要的事情,并把它们放在更加优先的位置。 找出那些对你最...
继续阅读 »

时间对每个人都是有限的


时间是一种奢侈品,在许多情况下,我们不能在拥有更多的财富和资源,但我们可以通过聪明的时间管理来确保我们能够在有限的时间内做更多的事情。所以我们必须学会如何利用时间,找出哪些对我们最重要的事情,并把它们放在更加优先的位置。


找出那些对你最重要的事情


我们不是时间的奴隶,而是它的朋友。活到老学到老,要知道区分好事情的先后优先级。你需要挖掘出对你而言真正重要的事情,并且把它们放在更优先的位置。只有专注于最重要的事情,你才能够彻底放下手中的琐事,让时间为你而舞。


时间管理不应该是目标,它应该是一种手段来实现目标。你需要找出那些对你最重要的事情,并将它们放在更优先的位置。只有这样,你才能在有限的时间里取得最好的成果。


不要把快乐推迟到未来


当你把所有的重要事情放到更优先的位置上后,你需要学会享受现在。不要把快乐推迟到未来,学会珍惜每个瞬间。因为时间是快乐的载体,珍惜当下不仅可以让你快乐,还能让你更加享受未来的生活。


面对优先事情时,学会说“不”


你会遇到很多无关紧要的请求,但是你需要学会拒绝那些与你最重要的事情无关的请求。说“不”并不是一种自私的行为,它可以让你从无关紧要的事情中解放出来,让你有更多的时间去关注那些真正重要的事情。


对“无关紧要”的事情说不!有时候,那些琐事会在你不经意间夺去你的时间,让你无暇顾及真正重要的事情。对于那些无关紧要的请求,大声说不!真正重要的事情,才是我们的核心,更值得我们花费精力。


图片遵循CC0 1.0协议


时间的价值取决于你如何利用它


时间的价值在于你如何使用它。尽管每个人的时间价值不同,但对于每个人来说,时间都是宝贵的。在一天结束时,如果你有实现一些重要的事情并感到充实和满足,那么这一天就是美好的。因此,你需要学会关注你的目标,并且学会更好地安排时间。


时间管理要始于自我管理


自我管理是实现聪明时间管理的核心。自我管理意味着在思想上建立目标和优先事项,并且始终监控、计划和执行任务。如果你能够始于自我管理,你将能够更加有效地管理时间,并且做出更加明智的决策。


学习重点管理和时间分配


重点管理和时间分配是实现聪明时间管理的关键。你需要始终关注那些重要的事情,并且制定适当的计划,并在时间上严格控制。因为时间是快速流逝的,你需要掌握时间并且利用它,不让时间流失于无关紧要的事情中。


把握时间,抓住机会。时间的价值在于你如何使用它。每天结束时,回首自己所做的一切是什么感觉?充实满足?还是焦虑疲惫?抓住机会,把握时间,完成更多的事情,才是让我们拥有更充实快乐的生活。


图片遵循CC0 1.0协议


享受时间的本质


最后,记住时间是有限的资源,珍惜现在的每一分每一秒,学会享受时间的本质。当我们的思维放松,我们的身心健康时,我们的工作表现会更加出色。在享受生活的同时,我们也应该学习新的技能和经验。所以让我们将时间花在有意义的事情上,并且享受成功的成果。


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

Android jetpack Compose之约束布局

概述 我们都知道ConstraintLayout在构建嵌套层级复杂的视图界面时可以有效降低视图树的高度,使视图树扁平化,约束布局在测量布局耗时上比传统的相对布局具有更好的性能,并且约束布局可以根据百分比自适应各种尺寸的终端设备。因为约束布局确实很好用,所以,官...
继续阅读 »

概述


我们都知道ConstraintLayout在构建嵌套层级复杂的视图界面时可以有效降低视图树的高度,使视图树扁平化,约束布局在测量布局耗时上比传统的相对布局具有更好的性能,并且约束布局可以根据百分比自适应各种尺寸的终端设备。因为约束布局确实很好用,所以,官方也为我们将约束布局迁移到了Compose平台。本文就是介绍约束布局在Compose中的使用。


实例解析


在使用约束布局之前,我们需要先在项目中的app.gradle脚本中添加compose版本的ConstraintLayout依赖

implementation('androidx.constraintlayout:constraintlayout-compose:1.0.1')

引入依赖以后,我们就可以看下如何在Compose中使用约束布局了


1.创建引用


在传统View系统中,我们在布局XML文件中可以给View设置资源的ID,并将资源ID作为索引来声明对应组件的摆放位置。而在Compose的约束布局中,可以主动创建引用并绑定到某个具体组件上,从而实现与资源ID相似的功能,每个组件都可以利用其他组件的引用获取到其他组件的摆放位置信息,从而确定自己摆放位置。



Compose 创建约束布局的方式有两种,分别时createRef()和createRefs(),根据字面意思我们就可以很清楚的知道,createRef(),每次只会创建一个引用,而createRefs()每次可以创建多个引用(最多可以创建16个),创建引用的方式如下:

// 创建单个引用
val text = createRef()
// 创建多个引用
val (button1,button2,text) = createRefs()

2.绑定引用


当我们创建完引用后就可以使用Modifier.constrainAs()修饰符将我们创建的引用绑定到某个具体组件上,可以在contrainAs尾部Lambda内指定组件的约束信息。我们需要注意的是,我们只能在ConstraintLayout尾部的Lambda中使用createRefer,createRefs函数创建引用,并使用Modifier.constrainAs函数来绑定引用,因为ConstrainScope尾部的Lambda的Reciever是一个ConstraintLayoutScope作用域对象。我们可以先看下面一段代码了解下约束布局的引用绑定:

@Composable
fun ConstrainLayoutDemo()
{
ConstraintLayout(modifier = Modifier
.width(300.dp)
.height(100.dp)
.padding(5.dp)
.border(5.dp, color = Color.Red)) {
val portraitImageRef = remember {
createRef()
}

Image(painter = painterResource(id = R.drawable.portrait)
, contentDescription = null,
modifier = Modifier.constrainAs(portraitImageRef){
top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
start.linkTo(parent.start)
})

}
}

运行结果:
在这里插入图片描述



上面的代码是实现一个用户卡片的部分代码,从代码中看到我们使用约束的时候需要用到Modifier.constrainsAs(){……}的方式。Modifier.constrainsAs的尾部Lambda是一个ConstrainScope作用域对象,可以在其中获取当前组件的parent,top,bottom,start,end等信息。并使用linkTo指定组件约束。在上面的界面中,我们希望用户的头像可以居左对齐,所以将top拉伸至父组件的顶部,bottom拉伸至父组件的底部,start拉伸至父组件的左边。我们再为卡片添加上用户的昵称和描述,全部代码如下所示:

@Composable
fun ConstrainLayoutDemo()
{
ConstraintLayout(modifier = Modifier
.width(300.dp)
.height(100.dp)
.padding(5.dp)
.border(5.dp, color = Color.Red)) {
val (portraitImageRef,usernameTextRef,descriptionTextRef) = remember {
createRefs()
}

Image(painter = painterResource(id = R.drawable.portrait)
, contentDescription = null,
modifier = Modifier.constrainAs(portraitImageRef){
top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
start.linkTo(parent.start)
})

Text(text = "旅游小美女", fontSize = 16.sp, maxLines = 1,
textAlign = TextAlign.Left,
modifier = Modifier.constrainAs(usernameTextRef){
top.linkTo(portraitImageRef.top)
start.linkTo(portraitImageRef.end,10.dp)
})

Text(text = "个人描述。。。。。。。。", fontSize = 14.sp,
color = Color.Gray,
fontWeight = FontWeight.Light,
modifier = Modifier.constrainAs(descriptionTextRef){
top.linkTo(usernameTextRef.bottom,5.dp)
start.linkTo(portraitImageRef.end,10.dp)
}
)
}
}

运行结果:
在这里插入图片描述
在上面的代码中我们也可以在ConstrainScope中指定组件的宽高信息,在ConstrainScope中直接设置width与height的可选值如下所示:



在ConstrainScope中指定组件的宽高信息时,通过在Modifier.constrainAs(xxxRef){width = Dimension.可选值}来设置,可选值如下:
Dimension.wrapContent: 实际尺寸为根据内容自适应
Dimension.matchParent: 实际尺寸为铺满父组件的尺寸
Dimension,wrapContent: 实际尺寸为根据约束信息拉伸后的尺寸
Dimension.preferredWrapContent: 如果剩余空间大于更具内容自适应的尺寸时,实际尺寸为自适应的尺寸,如果剩余空间小于内容自适应尺寸时,实际尺寸为剩余空间尺寸
Dimension.ratio(String): 根据字符串计算实际尺寸所占比率:如1 :2
Dimension.percent(Float): 根据浮点数计算实际尺寸所占比率
Dimension.value(Dp): 将尺寸设置为固定值
Dimension.perferredValue(Dp): 如果剩余空间大于固定值时,实际尺寸为固定值,如果剩余空间小于固定值时,实际尺寸则为剩余空间尺寸



我们想象下,假如用户的昵称特别长,那么按照我们上面的代码展示则会出现展示不全的问题,所以我们可以通过设置end来指定组件所允许的最大宽度,并将width设置为preferredWrapContent,意思是当用户名很长时,实际的宽度会做自适应调整。我们将上面展示用户名的地方改一下,代码如下:

// 上面的代码只用改这个部分
Text(
text = "旅游小美女美美美美美名字很长长长长长长长长长",
fontSize = 16.sp,
textAlign = TextAlign.Left,
modifier = Modifier.constrainAs(usernameTextRef){
top.linkTo(portraitImageRef.top)
start.linkTo(portraitImageRef.end,10.dp)
end.linkTo(parent.end,10.dp)
width = Dimension.preferredWrapContent
})

运行结果:
在这里插入图片描述


辅助布局工具


在传统View的约束布局中有Barrier,GuideLine等辅助布局的工具,在Compose中也继承了这些特性,方便我们完成各种复杂场景的布局需求。


1.Barrier分界线


Barrier顾名思义就是一个屏障,使用它可以隔离UI布局上面的一些相互挤压的影响,举一个例子,比如我们希望两个输入框左对齐摆放,并且距离文本组件中的最长者仍保持着10dp的间隔,当用户名和密码等发生变化时,输入框的位置能够自适应调整。这里使用Barrier特性可以简单的实现这一需求:

@Composable
fun InputFieldLayout(){
ConstraintLayout(
modifier = Modifier
.width(400.dp)
.padding(10.dp)
) {
val (usernameTextRef, passwordTextRef, usernameInputRef, passWordInputRef) = remember { createRefs() }
val barrier = createEndBarrier(usernameTextRef, passwordTextRef)
Text(
text = "用户名",
fontSize = 14.sp,
textAlign = TextAlign.Left,
modifier = Modifier
.constrainAs(usernameTextRef) {
top.linkTo(parent.top)
start.linkTo(parent.start)
}
)

Text(
text = "密码",
fontSize = 14.sp,
modifier = Modifier
.constrainAs(passwordTextRef) {
top.linkTo(usernameTextRef.bottom, 20.dp)
start.linkTo(parent.start)
}
)
OutlinedTextField(
value = "",
onValueChange = {},
modifier = Modifier.constrainAs(usernameInputRef) {
start.linkTo(barrier, 10.dp)
top.linkTo(usernameTextRef.top)
bottom.linkTo(usernameTextRef.bottom)
height = Dimension.fillToConstraints
}
)
OutlinedTextField(
value = "",
onValueChange = {},
modifier = Modifier.constrainAs(passWordInputRef) {
start.linkTo(barrier, 10.dp)
top.linkTo(passwordTextRef.top)
bottom.linkTo(passwordTextRef.bottom)
height = Dimension.fillToConstraints
}
)
}
}

运行结果:
在这里插入图片描述


2.Guideline引导线


Barrier分界线需要依赖其他引用,从而确定自身的位置,而使用Guideline不依赖任何引用,例如,我们希望将用户头像摆放在距离屏幕顶部2:8的高度位置,头像以上是用户背景,以下是用户信息,这样的需求就可以使用Guideline实现,代码如下:

@Composable
fun GuidelineDemo(){
ConstraintLayout(modifier = Modifier
.height(300.dp)
.background(color = Color.Gray)) {
val guideline = createGuidelineFromTop(0.2f)
val (userPortraitBackgroundRef,userPortraitImgRef,welcomeRef) = remember {
createRefs()
}

Box(modifier = Modifier
.constrainAs(userPortraitBackgroundRef) {
top.linkTo(parent.top)
bottom.linkTo(guideline)
height = Dimension.fillToConstraints
width = Dimension.matchParent
}
.background(Color(0xFF673AB7)))

Image(painter = painterResource(id = R.drawable.portrait),
contentDescription = null,
modifier = Modifier
.constrainAs(userPortraitImgRef) {
top.linkTo(guideline)
bottom.linkTo(guideline)
start.linkTo(parent.start)
end.linkTo(parent.end)
}
.size(100.dp)
.clip(CircleShape)
.border(width = 2.dp, color = Color(0xFF96659E), shape = CircleShape))

Text(text = "不喝奶茶的小白兔",
color = Color.White,
fontSize = 26.sp,
modifier = Modifier.constrainAs(welcomeRef){
top.linkTo(userPortraitImgRef.bottom,10.dp)
start.linkTo(parent.start)
end.linkTo(parent.end)
})

}
}

运行结果:
在这里插入图片描述



在上面的代码中,我们使用createGuidelineFromTop()方法创建了一条从顶部出发的引导线,然后用户背景就可以依赖这条引导线确定宽高了,然后对于头像,我们只需要将top和bottom连接至引导线即可



3.Chain链接约束


ContraintLayout的另一个好用的特性就是Chain链接约束,通过链接约束可以允许多个组件平均分配布局空间,类似于weight修饰符。例如我们要展示一首古诗,用Chain链接约束实现如下:

@Composable
fun showQuotesDemo() {
ConstraintLayout(
modifier = Modifier
.size(400.dp)
.background(Color.Black)
) {
val (quotesFirstLineRef, quotesSecondLineRef, quotesThirdLineRef, quotesForthLineRef) = remember {
createRefs()
}

createVerticalChain(
quotesFirstLineRef, quotesSecondLineRef, quotesThirdLineRef, quotesForthLineRef,
chainStyle = ChainStyle.Spread
)

Text(text = "窗前明月光,",
color = Color.White,
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.constrainAs(quotesFirstLineRef) {
start.linkTo(parent.start)
end.linkTo(parent.end)
})

Text(text = "疑是地上霜。",
color = Color.White,
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.constrainAs(quotesSecondLineRef) {
start.linkTo(parent.start)
end.linkTo(parent.end)
})

Text(text = "举头望明月,",
color = Color.White,
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.constrainAs(quotesThirdLineRef) {
start.linkTo(parent.start)
end.linkTo(parent.end)
})

Text(text = "低头思故乡。",
color = Color.White,
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.constrainAs(quotesForthLineRef) {
start.linkTo(parent.start)
end.linkTo(parent.end)
})
}
}

运行结果:
在这里插入图片描述
如上面代码所示,我们要展示四句诗就需要创建四个引用对应四句诗,然后我们就可以创建一条垂直的链接约束将四句诗词连接起来,创建链接约束时末尾参数可以传一个ChainStyle,用来表示我们期望的布局样式,它的取值有三个,效果和意义如下所示:



(1)Spread:链条中的每个元素平分整个父空间

 createVerticalChain(
quotesFirstLineRef, quotesSecondLineRef, quotesThirdLineRef, quotesForthLineRef,
chainStyle = ChainStyle.Spread)

运行效果:


在这里插入图片描述



(2)SpreadInside:链条中的首尾元素紧贴边界,剩下的每个元素平分整个父空间

 createVerticalChain(
quotesFirstLineRef, quotesSecondLineRef, quotesThirdLineRef, quotesForthLineRef,
chainStyle = ChainStyle.SpreadInside)

运行效果:


在这里插入图片描述



(3)Packed:链条中的所有元素都聚集到中间,效果如下

 createVerticalChain(
quotesFirstLineRef, quotesSecondLineRef, quotesThirdLineRef, quotesForthLineRef,
chainStyle = ChainStyle.Packed)

运行效果:


在这里插入图片描述


总结


关于Compose约束布局的内容就是这些了,本文主要是简单的介绍了Compose中约束布局的基本使用,要熟练掌握Compose约束布局,还需要读者多去联系,多使用约束布局写界面,这样就会熟能生巧,在此我只做一个抛砖引玉的活。有任何疑问,欢迎在评论区交流。


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

安卓组件学习——NavigationView导航视图

前言 日新计划可真头疼,每天更文养成习惯是好,但有时候没思路就很烦,回到正题,本篇回到很久之前的组件学习(安卓UI设计开发——Material Design(BottomSheetDialogFragment篇) - 掘金 (juejin.cn)),这次我们来...
继续阅读 »

前言


日新计划可真头疼,每天更文养成习惯是好,但有时候没思路就很烦,回到正题,本篇回到很久之前的组件学习(安卓UI设计开发——Material Design(BottomSheetDialogFragment篇) - 掘金 (juejin.cn)),这次我们来看看许多APP首页常用的NavigationView——滑动菜单如何使用。


2/22 更正:NavigationView为导航视图,DrawerLayout为抽屉布局,一起组合成滑动菜单(实现侧滑交互体验)


733c1e864882815b26a38f1520a843a.jpg


正篇


首先,使用NavigationView前我们先创建一个新项目,我这里为了学习这些组件,命名为MaterialDemo,作为我们学习Materia组件的项目,然后因为要使用Material库的NavigationView,所以我们项目的app目录下的build.gradle文件中的dependencies闭包中添加下面依赖:

implementation 'com.google.android.material:material:1.8.0'
implementation 'de.hdodenhof:circleimageview:3.1.0'

其中第二个依赖是我们导入的开源项目CircleImageView,这可以让我们更容易实现图片圆形化,也就是这个滑动菜单栏上圆形头像的形成。


当然,新建的是Kotlin安卓空Activity项目,我们采用了ViewBinding,所以同时也要在该文件下启用ViewBinding,位置在android闭包中:

buildFeatures {
viewBinding = true
}

sync Gradle(同步 Gradle)完成后,我们再把res/values/theme.xml文件AppThemeparent主题换为Theme.MaterialComponents.Light.NoActionBar,用来适配我们的Material库组件:


image.png


我们还得事先准备几张图片备用(按钮,头像等),这里我放在了drawable-xxhdpi目录下,当然如果有需要可以到文末我的Github项目中找:


image.png


接着,我们在res目录下创建新的名为menu文件夹(和之前文章安卓开发基础——Menu菜单的使用 - 掘金 (juejin.cn)一样):


image.png


image.png


再在这个文件夹上右击->New->Menu resource file ,创建一个nav_menu.xml文件,添加下面的代码:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<group android:checkableBehavior="single">
<item
android:id="@+id/navCall"
android:icon="@drawable/nav_call"
android:title="Call" />
<item
android:id="@+id/navFriends"
android:icon="@drawable/nav_friends"
android:title="Friends" />
<item
android:id="@+id/navLocation"
android:icon="@drawable/nav_location"
android:title="Location" />
<item
android:id="@+id/navMail"
android:icon="@drawable/nav_mail"
android:title="Mail" />
<item
android:id="@+id/navTask"
android:icon="@drawable/nav_task"
android:title="Tasks" />
</group>
</menu>

上面代码中我们加了一个group标签,并把其中的checkableBehavior属性设置为single,这样就能让菜单项变为只可以单选。
然后我们就能预览到这个菜单样式,这就是我们即将用的具体的菜单项:


image.png


但NavigationView样式不是这个预览到的,因为有菜单项这样还是不够的,我们还需要准备一个herderLayout用于显示NavigationView的头部布局,这个布局可以按照需求来定制,这里我们就构建了头像、用户名和邮箱地址这三项,这个布局我们直接在layout目录正常创建布局文件就行,我们这里创建一个名为nav_header的XML布局文件:


image.png
该文件代码如下:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="180dp"
android:padding="10dp"
android:background="?attr/colorPrimary">

<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/iconImage"
android:layout_width="70dp"
android:layout_height="70dp"
android:src="@drawable/nav_icon"
android:layout_centerInParent="true" />

<TextView
android:id="@+id/mailText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:text="tonygreendev@gmail.com"
android:textColor="#FFF"
android:textSize="14sp" />

<TextView
android:id="@+id/userText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@id/mailText"
android:text="Tony Green"
android:textColor="#FFF"
android:textSize="14sp" />

</RelativeLayout>

我们使用了相对布局(RelativeLayout)作为最外层布局,其中CircleImageView就是之前加依赖提到的开源控件,将我们的图片圆形化,和ImageView用法一样,这里我们用于构建圆形的头像图片,且设为居中,还有两个TextView分别就是我们的用户名和邮箱地址,用相对布局属性定位即可。


这些做完后,我们的准备阶段就完成了,接下来我们开始使用NavigationView控件:


我们先把布局添加NavigationView:

<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/drawerLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">

<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">

<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
</FrameLayout>


<com.google.android.material.navigation.NavigationView
android:id="@+id/navView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="start"
app:menu="@menu/nav_menu"
app:headerLayout="@layout/nav_header"/>

</androidx.drawerlayout.widget.DrawerLayout>

Activity中:

package com.example.materialdemo

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.example.materialdemo.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {

lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)

setSupportActionBar(binding.toolbar)

supportActionBar?.let {
it.setDisplayHomeAsUpEnabled(true)
it.setHomeAsUpIndicator(R.drawable.ic_menu)
}
binding.navView.setCheckedItem(R.id.navCall)
binding.navView.setNavigationItemSelectedListener {
binding.drawerLayout.closeDrawers()
true
}
}
}

最终效果:


36ef295afdeec85f9a5cb6a449700d38.gif


这里一开始忘记加我的项目地址了,现在补上:GitHub - ObliviateOnline/MaterialDemo: Material库组件学习


总结


今天学了NavigationView,结果忘记写前面的滑动菜单DrawerLayout和标题栏Toolbar控件了,下一篇就把前面的构建过程给理一遍。


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