注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

环信FAQ

环信FAQ

集成常见问题及答案
RTE开发者社区

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

关于WorkManager需要知道的一切

背景WorkManager 是google为Android推出Jetpack开发套件的成员之一,是一个持续化工作的推荐解决方案。 我们在开发过程中通常会遇到一些需要持续化的操作需求,例如,上传文件到服务端,Token过期刷新,定期更新服务端下发的配置等。主要的...
继续阅读 »

背景

WorkManager 是google为Android推出Jetpack开发套件的成员之一,是一个持续化工作的推荐解决方案。 我们在开发过程中通常会遇到一些需要持续化的操作需求,例如,上传文件到服务端,Token过期刷新,定期更新服务端下发的配置等。

主要的用途是:

立即开始执行一个任务

有的任务必须立即开始执行,但这里可能会有个疑问,为什么立即开始执行的任务不直接写代码开始执行,要利用WorkManager呢?这主要归功于WorkManager的一些特性,例如根据约束安排任务,链式化任务等,让你代码写得更简洁,更好的扩展性。

需要长时间定期运行的任务

任务需要长时间定期运行的的情况,例如我们我需要每一个小时上传一次用户日志.

延迟任务

有的任务需要延后一段时间执行.


再继续讲解它如何使用之前先来说说它的几个特性.

特性

约束

提供一些约束条件去执行任务,当约束条件满足后才开始执行,例如,当连接上Wifi, 当设备有足够的电量等条件

可靠性

安排的任务保证能够顺利执行,因为WorkManager内部以SqLite存储任务的执行的情况,为执行的和执行失败的都会重新尝试.

加急任务

可能你会给WorkManager安排很多Task, 某些Task也许优先级比较高,需要立即执行,WorkManager提供加急的特性,可以尽早执行这类Task.

链式任务

某些Task可能需要顺序执行,也可能需要并行执行,WorkManager同样提供这类API满足需求

val continuation = WorkManager.getInstance(context)
    .beginUniqueWork(
        Constants.IMAGE_MANIPULATION_WORK_NAME,
        ExistingWorkPolicy.REPLACE,
        OneTimeWorkRequest.from(CleanupWorker::class.java)
    ).then(OneTimeWorkRequest.from(WaterColorFilterWorker::class.java))
    .then(OneTimeWorkRequest.from(GrayScaleFilterWorker::class.java))
    .then(OneTimeWorkRequest.from(BlurEffectFilterWorker::class.java))
    .then(
        if (save) {
            workRequest<SaveImageToGalleryWorker>(tag = Constants.TAG_OUTPUT)
        } else /* upload */ {
            workRequest<UploadWorker>(tag = Constants.TAG_OUTPUT)
        }
    )

线程的互操作性

无缝继承了Coroutines和RxJava的异步特性,在WorkManager也能使用这类异步的API


实战

说了这么多,下面我们来看看如何使用WorkManager

第一步,添加依赖

dependencies {
    def work_version = "2.8.1"

    // (Java only)
    implementation "androidx.work:work-runtime:$work_version"

    // Kotlin + coroutines
    implementation "androidx.work:work-runtime-ktx:$work_version"

    // optional - RxJava2 support
    implementation "androidx.work:work-rxjava2:$work_version"

    // optional - GCMNetworkManager support
    implementation "androidx.work:work-gcm:$work_version"

    // optional - Test helpers
    androidTestImplementation "androidx.work:work-testing:$work_version"

    // optional - Multiprocess support
    implementation "androidx.work:work-multiprocess:$work_version"
}

第二步,自定义Worker

这里我们需要定义,这个Task需要做什么,创建一个自定义Worker类,这里我们创建一个UploadWorker用于做一些后台上传的操作

class UploadWorker(appContext: Context, workerParams: WorkerParameters):
       Worker(appContext, workerParams) {
   override fun doWork(): Result {
       // 做一些上传操作
       uploadImages()
       // 代表任务执行成功
       return Result.success()
   }
}

在doWork中我们可以写上任务需要执行的代码,当任务结束后需要返回一个Result,这个Result有三个值

Result.success() 任务执行成功

Result.failure() 任务执行失败

Result.retry() 任务需要重试

第三步, 创建一个WorkRequest

当定义完需要做什么后我们需要创建一个WorkRequest去启动这个任务的执行。WorkManager提供了很多灵活的API用于定义任务的启动逻辑,例如是否执行一次还是周期性执行,它的约束条件是什么等。这里演示我们使用OneTimeWorkRequest.

val uploadWorkRequest: WorkRequest =
   OneTimeWorkRequestBuilder<UploadWorker>()
       .build()

第四步, 提交WorkRequest

当创建完成WorkRequest,我们需要把它交给WorkManager去执行

WorkManager
    .getInstance(myContext)
    .enqueue(uploadWorkRequest)

进阶

一次性任务

创建一个简单的一次性任务

val myWorkRequest = OneTimeWorkRequest.from(MyWork::class.java)

如果需要增加一些配置如约束等可以使用builder

val uploadWorkRequest: WorkRequest =
   OneTimeWorkRequestBuilder<MyWork>()
       // 添加额外配置
       .build()
加急工作

WorkManager 执行重要的工作,同时让系统更好地控制对资源的访问。

加急工作具有以下特点:

重要性:加急工作适合对用户重要或由用户启动的任务。

速度:加急工作最适合立即开始并在几分钟内完成的短任务。

配额:限制前台执行时间的系统级配额决定加急作业是否可以启动。

电源管理:电源管理限制(例如省电模式和打瞌睡模式)不太可能影响加急工作。

启动加急工作的方式也非常简单,可以直接调用setExpedited()设置该WorkRequest为一个加急任务

val request = OneTimeWorkRequestBuilder()
    .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
    .build()

WorkManager.getInstance(context)
    .enqueue(request)

这里setExpedited会有一个参数OutOfQuotaPolicy,代表系统配额不足时候,把该任务作为一个普通任务对待.

周期性任务

我们有一些需求例如,备份应用数据,上传日志,下载一些应用配置,需要周期性进行,我们可以定义PeriodicWorkRequest去创建周期性的任务.


val saveRequest =
       PeriodicWorkRequestBuilder<SaveImageToFileWorker>(1, TimeUnit.HOURS)
           .build()

上面这段代码每一小时执行一次任务.

但是这个时间约束也不是固定的,这里定义的时间实际上是最小间隔时间,系统会根据当前系统的情况进行适当调整。

我们还可以定义flexInterval让间隔提前一点


val myUploadWork = PeriodicWorkRequestBuilder<SaveImageToFileWorker>(
       1, TimeUnit.HOURS, // repeatInterval
       15, TimeUnit.MINUTES) // flexInterval
    .build()

这样我们执行任务的时间是repeatInterval - flexInterval,上面代码的任务会在1小时-15分钟的时候执行.

image.png

周期性任务遇上约束条件

当周期性任务遇到一些约束条件不满足的时候将会延迟执行,直到约束条件满足.

关于约束

WorkManager提供了下列的一些约束条件.

NetworkType 网络条件约束,例如只能在连接WIFI的情况下执行.

BatteryNotLow 非电量低约束,在有充足的电量的时候执行.

RequiresCharging 需要充电的时候执行约束

DeviceIdle 在设备无状态时候运行,这样不会对设备的效率产生影响.

StorageNotLow 当设备有有足够的存储空间时候运行

创建一个约束使用Contraints.Builder()并赋值给WorkRequest.Builder().

下面代码展示创建一个约束该任务只会在wifi并且在充电的时候执行.

val constraints = Constraints.Builder()
   .setRequiredNetworkType(NetworkType.UNMETERED)
   .setRequiresCharging(true)
   .build()

val myWorkRequest: WorkRequest =
   OneTimeWorkRequestBuilder<MyWork>()
       .setConstraints(constraints)
       .build()

延迟任务

如果你指定的任务没有约束或者约束已经满足,那么它会立即开始执行,如果想让它有个最少的延迟,可以指定一个最小的延迟执行时间.

下面这个例子展示设置最小10分钟后开始加入队列.

val myWorkRequest = OneTimeWorkRequestBuilder<MyWork>()
   .setInitialDelay(10, TimeUnit.MINUTES)
   .build()

上面展示的针对OneTimeWorkRequestBuilder同样也适用于PeriodicWorkRequest.

退避策略

如果一个任务失败返回Result.retry(), 你的任务可以在稍等进行重试,这种退避策略可以自定义,这里连个自定义的属性

退避延迟:退避延迟执行下次尝试任务的最少时间,通常我们自定义最少不能低于[MIN_BACKOFF_MILLIS]

退避策略:退避策略可以指定两种一个是LINEAR(线性)和EXPONENTIAL(幂等)

实际上每一个任务都有一个默认的退避策略,缺省的退避策略是EXPONENTIAL和30s的延迟,但是你可以自定义,下面是一个自定义的例子。

val myWorkRequest = OneTimeWorkRequestBuilder<MyWork>()
   .setBackoffCriteria(
       BackoffPolicy.LINEAR,
       OneTimeWorkRequest.MIN_BACKOFF_MILLIS,
       TimeUnit.MILLISECONDS)
   .build()

Tag任务

每一个任务都可以附加上一个标签,稍后可以使用这个标签找到该任务,取消它或者查看的执行进度. 如果你有一组任务,可以添加同一个标签,可以统一的操作它们。例如使用WorkManager.cancelAllWorkByTag(String)取消所有的任务,使用WorkManager.getWorkInfosByTag(String)返回一个任务信息列表查看当前任务的状态.

下面是一个展示给任务赋值一个标签

val myWorkRequest = OneTimeWorkRequestBuilder<MyWork>()
   .addTag("cleanup")
   .build()

链式任务

WorkManager还提供了一种定义顺序执行任务或者并发执行任务的方式。

使用这种方式创建任务通过

WorkManager.beginWith(OneTimeWorkRequest)
WorkManager.beginWith(List<OneTimeWorkRequest>)

以上两个方式都会返回一个WorkContinuation.

WorkContinuation随后可以继续调用

then(OneTimeWorkRequest)
then(List<OneTimeWorkRequest>)

执行链式任务

最后可以调用enqueue()去执行你的工作链. 举个例子

WorkManager.getInstance(myContext)
   .beginWith(listOf(plantName1, plantName2, plantName3))
   .then(cache)
   .then(upload)
   .enqueue()

Input Mergers

一个父任务的执行结果可以传递给子任务,例如上面plantName1, plantName2, plantName3执行的结果可以传递

给cache任务,WorkManager使用InputMerger去管理这些多个父任务的输出结果到子任务.

这里有两种不同类型的的InputMerger

  • OverwritingInputMerger 从输入到输出增加所有的key,遇到冲突的情况,覆盖之前的key的值

  • ArrayCreatingInputMerger 从输入到输出增加所有的key,遇到冲突的情况进行创建数组

工作链的状态

当前面的任务阻塞住的时候后面的任务同样也是阻塞状态.

image.png

当前面的任务执行成功后,后面的任务才能继续开始执行

image.png

当一个任务失败进行重试的时候并不会影响并发的任务

image.png

一个任务失败,后面的任务也会是失败状态

image.png

对于取消也是这样

image.png

结语

总的来说WorkManager是一个非常好用的组件,它解决了一些曾经实现起来比较繁琐的功能,例如它的约束执行,我们可以等待有网络时候执行任务。我们利用周期性执行任务功能能够很方便的执行一些诸如刷新token, 定期日志上传等功能.


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

末日终极坐标安卓辅助工具

前言 本工具完全免费,无需联网 本文档只介绍工具的使用方法,有时间再写一篇介绍一下实现细节。 总的来讲本工具可以帮助你时刻知道自己的坐标并知道和宝箱的位置关系,减少资源浪费。 5分钟即可完成100汽油的使用,大大节省时间。 阅读本文档前提是大家是《末日血战》等...
继续阅读 »

前言


本工具完全免费,无需联网


本文档只介绍工具的使用方法,有时间再写一篇介绍一下实现细节。

总的来讲本工具可以帮助你时刻知道自己的坐标并知道和宝箱的位置关系,减少资源浪费。

5分钟即可完成100汽油的使用,大大节省时间。


阅读本文档前提是大家是《末日血战》等同款游戏的玩家。


工具下载安装


链接: pan.baidu.com/s/14GE-713c… 提取码: 29c5


安装工具


工具安装后,桌面会有这个图标。

在打开工具前需进入应用设置页打开这个应用的显示悬浮窗权限图片说明


填入初始坐标


打开工具大致显示是这个样子,坐标初始都是0,那么填入相应的坐标保存就会是这个样子。
14ae0a88098b04a18b0a0c36b400e3c.jpg
可以看到左上角有个小图,这是一个直角坐标系的缩略图,左上角位置是(0,0)右下角位置(301,301),拖拽可以移动位置。
小图中会显示3个红点,一个绿点。绿点表示当前坐标,红点表示终极坐标。绿点的坐标数值是固定显示在左上角的,不随绿点移动


此时可以按返回键退出app,但是不要杀掉应用。


建立坐标系


初次使用,建议可以打开小程序截一张生存之路的全屏图,然后我们打开这张图并横屏显示图片开始操作。当已经熟悉工具如何使用后可以在游戏中进行操作了


建立坐标系

打开图片或者游戏,进入到这个界面,点击左上角悬浮窗上的开始按钮,会看到这样一个界面(没有中间两条直线)
47b446b703476c931a27667de16d706.jpg
我们的目标就是为了建立中间两条直线。


严格按图片指示的顺序操作。尽可能点击在轴线的中心位置
d6bdd46daf0a94960c54bf8918af5f5.png
以上操作只需要执行一次,后续就不需要操作了。
完成以上操作,就得到有两条线的图了。这个时候就完成了建立坐标系。

如果坐标系建立的不太好(比较斜,良好的坐标系有助于减少误差。),可以重新再来直到满意为止。


开始寻找终极坐标


注意观察小地图,找一个我们没有到达的离的近的终极坐标为目标。可以看到x和y的差距。

举个例子,我们当前坐标49,52,刚刚已经在48,52这里取得了一个宝箱,那么下一个目的地选237,29。因为是x坐标相差较大,我们x太小,而y坐标我们的大一点。所以主要的方向是加x,少量的减y。
05c5394993b5a8877db3d81c8ce6425.png
所以我们应该按x轴正方向和y轴负方向这里走,因为x相差较大,所以如果可以的话(有障碍物就走不了)就直接沿着x轴正方向走就好了。


我想游戏玩家应该还是知道要往哪走的,但是容易算错坐标或者根本懒得记,凭运气。那么我们指定往哪走之后,接下来怎么使用这个工具。



  • 1、点击一个位置(我们要让小车开到的位置),这个时候小车不会走,因为我们工具盖住了游戏

  • 2、app回退一下(不是杀掉应用),这时可以发现小车在抖动了,其实就是小车可以走了,再点一下刚才那个位置,小车就会走到那个位置。这样我们就完成了一次移动和坐标记录。小地图当前坐标就会变化。绿点也会移动。

  • 3、小车走完之后,我们再点开始,然后重复1,2 步骤。


补充



  • 1、本工具存在误差,一般每次执行在小车x,y <=|2| 基本100%准确。x,y <= |3| 100个汽油可能会有|2|以内坐标的误差(仅本人测试数据)

  • 2、点击位置尽可能点在地图块的中间,这样可以减少误差。遇到坐标点在路径上,可以进入其中对当前坐标进行校准,当然一般是不需要的。

  • 3、如果遇到了事件,我们就处理完事件后再点开始按钮

  • 4、回退怎么用:右下角回退用途是当我们不想走这一步,可以点回退按钮撤销这一步,然后重新再点一个点。如果还是后悔不想走这一步(这个回退是指回退我们的记录,游戏中的步骤我们肯定是做不到回退的)可以再次回退,但要注意只能回退一步。

  • 5、在工具操作界面(即点击开始后显示坐标线和按钮的那种情况),GETXY按钮下的白色坐标数字是表示当前这一步的行进地图坐标,例如3,2表示向x轴正方向移动3格,y轴正方向移动2格。可以通过这个坐标判断工具计算的坐标是否准确。

  • 6、本次汽油用完后,就可以杀掉辅助工具app了。下次有汽油可以继续直接使用,记住使用过程中的退出都是回退而不是杀掉app

  • 7、如果已经在工具上操作坐标了,但是发现汽油不够了,这个时候最好是买几个汽油仍然走到刚才记录的位置。当然也可以使用回退功能,再重新操作工具点击到你汽油够的位置。


最后


希望大家先熟悉工具流程,可以截一张图去操作,参考上文补充5的说明,通过这个坐标数值可以知道工具记录是否准确。然后再在游戏中操作避免浪费资源。如果通过截图去熟悉工具使用,在正式使用前要核对一下当前坐标是否准确。坐标可以随时矫正。

希望大家游戏愉快,也希望本工具对大家有所帮助。


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

为什么谷歌搜索不支持无限分页

这是一个很有意思却很少有人注意的问题。 当我用Google搜索MySQL这个关键词的时候,Google只提供了13页的搜索结果,我通过修改url的分页参数试图搜索第14页数据,结果出现了以下的错误提示: 百度搜索同样不提供无限分页,对于MySQL关键词,百度...
继续阅读 »

这是一个很有意思却很少有人注意的问题。


当我用Google搜索MySQL这个关键词的时候,Google只提供了13页的搜索结果,我通过修改url的分页参数试图搜索第14页数据,结果出现了以下的错误提示:


Google不能无限分页


百度搜索同样不提供无限分页,对于MySQL关键词,百度搜索提供了76页的搜索结果。


百度不能无限分页


为什么不支持无限分页


强如Google搜索,为什么不支持无限分页?无非有两种可能:



  • 做不到

  • 没必要


「做不到」是不可能的,唯一的理由就是「没必要」。


首先,当第1页的搜索结果没有我们需要的内容的时候,我们通常会立即更换关键词,而不是翻第2页,更不用说翻到10页往后了。这是没必要的第一个理由——用户需求不强烈。


其次,无限分页的功能对于搜索引擎而言是非常消耗性能的。你可能感觉很奇怪,翻到第2页和翻到第1000页不都是搜索嘛,能有什么区别?


实际上,搜索引擎高可用和高伸缩性的设计带来的一个副作用就是无法高效实现无限分页功能,无法高效意味着能实现,但是代价比较大,这是所有搜索引擎都会面临的一个问题,专业上叫做「深度分页」。这也是没必要的第二个理由——实现成本高。


我自然不知道Google的搜索具体是怎么做的,因此接下来我用ES(Elasticsearch)为例来解释一下为什么深度分页对搜索引擎来说是一个头疼的问题。


为什么拿ES举例子


Elasticsearch(下文简称ES)实现的功能和Google以及百度搜索提供的功能是相同的,而且在实现高可用和高伸缩性的方法上也大同小异,深度分页的问题都是由这些大同小异的优化方法导致的。


什么是ES


ES是一个全文搜索引擎。


全文搜索引擎又是个什么鬼?


试想一个场景,你偶然听到了一首旋律特别优美的歌曲,回家之后依然感觉余音绕梁,可是无奈你只记得一句歌词中的几个字:「伞的边缘」。这时候搜索引擎就发挥作用了。


使用搜索引擎你可以获取到带有「伞的边缘」关键词的所有结果,这些结果有一个术语,叫做文档。并且搜索结果是按照文档与关键词的相关性进行排序之后返回的。我们得到了全文搜索引擎的定义:



全文搜索引擎是根据文档内容查找相关文档,并按照相关性顺序返回搜索结果的一种工具



2022-06-08-085125.png


网上冲浪太久,我们会渐渐地把计算机的能力误以为是自己本身具备的能力,比如我们可能误以为我们大脑本身就很擅长这种搜索。恰恰相反,全文检索的功能是我们非常不擅长的。


举个例子,如果我对你说:静夜思。你可能脱口而出:床前明月光,疑是地上霜。举头望明月,低头思故乡。但是如果我让你说出带有「月」的古诗,想必你会费上一番功夫。


包括我们平时看的书也是一样,目录本身就是一种符合我们人脑检索特点的一种搜索结构,让我们可以通过文档ID或者文档标题这种总领性的标识来找到某一篇文档,这种结构叫做正排索引


目录就是正排索引


而全文搜索引擎恰好相反,是通过文档中的内容来找寻文档,诗词大会中的飞花令就是人脑版的全文搜索引擎。


飞花令就是全文搜索


全文搜索引擎依赖的数据结构就是大名鼎鼎的倒排索引(「倒排」这个词就说明这种数据结构和我们正常的思维方式恰好相反),它是单词和文档之间包含关系的一种具体实现形式。


单词文档矩阵


打住!不能继续展开了话题了,赶紧一句话介绍完ES吧!



ES是一款使用倒排索引数据结构、能够根据文档内容查找相关文档,并按照相关性顺序返回搜索结果的全文搜索引擎



高可用的秘密——副本(Replication)


高可用是企业级服务必须考虑的一个指标,高可用必然涉及到集群和分布式,好在ES天然支持集群模式,可以非常简单地搭建一个分布式系统。


ES服务高可用要求其中一个节点如果挂掉了,不能影响正常的搜索服务。这就意味着挂掉的节点上存储的数据,必须在其他节点上留有完整的备份。这就是副本的概念。


副本


如上图所示,Node1作为主节点,Node2Node3作为副本节点保存了和主节点完全相同的数据,这样任何一个节点挂掉都不会影响业务的搜索。满足服务的高可用要求。


但是有一个致命的问题,无法实现系统扩容!即使添加另外的节点,对整个系统的容量扩充也起不到任何帮助。因为每一个节点都完整保存了所有的文档数据。


因此,ES引入了分片(Shard)的概念。


PB级数量的基石——分片(Shard)


ES将每个索引(ES中一系列文档的集合,相当于MySQL中的表)分成若干个分片,分片将尽可能平均地分配到不同的节点上。比如现在一个集群中有3台节点,索引被分成了5个分片,分配方式大致(因为具体如何平均分配取决于ES)如下图所示。


分片


这样一来,集群的横向扩容就非常简单了,现在我们向集群中再添加2个节点,则ES会自动将分片均衡到各个节点之上:


横向扩展


高可用 + 弹性扩容


副本和分片功能通力协作造就了ES如今高可用支持PB级数据量的两大优势。


现在我们以3个节点为例,展示一下分片数量为5,副本数量为1的情况下,ES在不同节点上的分片排布情况:


主分片和副分片的分布


有一点需要注意,上图示例中主分片和对应的副本分片不会出现在同一个节点上,至于为什么,大家可以自己思考一下。


文档的分布式存储


ES是怎么确定某个文档应该存储到哪一个分片上呢?



通过上面的映射算法,ES将文档数据均匀地分散在各个分片中,其中routing默认是文档id。


此外,副本分片的内容依赖主分片进行同步,副本分片存在意义就是负载均衡、顶上随时可能挂掉的主分片位置,成为新的主分片。


现在基础知识讲完了,终于可以进行搜索了。


ES的搜索机制


一图胜千言:


es搜索



  1. 客户端进行关键词搜索时,ES会使用负载均衡策略选择一个节点作为协调节点(Coordinating Node)接受请求,这里假设选择的是Node3节点;

  2. Node3节点会在10个主副分片中随机选择5个分片(所有分片必须能包含所有内容,且不能重复),发送search request;

  3. 被选中的5个分片分别执行查询并进行排序之后返回结果给Node3节点;

  4. Node3节点整合5个分片返回的结果,再次排序之后取到对应分页的结果集返回给客户端。



注:实际上ES的搜索分为Query阶段Fetch阶段两个步骤,在Query阶段各个分片返回文档Id和排序值,Fetch阶段根据文档Id去对应分片获取文档详情,上面的图片和文字说明对此进行了简化,请悉知。



现在考虑客户端获取990~1000的文档时,ES在分片存储的情况下如何给出正确的搜索结果。


获取990~1000的文档时,ES在每个分片下都需要获取1000个文档,然后由Coordinating Node聚合所有分片的结果,然后进行相关性排序,最后选出相关性顺序在990~100010条文档。


深度分页


页数越深,每个节点处理的文档也就越多,占用的内存也就越多,耗时也就越长,这也就是为什么搜索引擎厂商通常不提供深度分页的原因了,他们没必要在客户需求不强烈的功能上浪费性能。


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

【android activity重难点突破】这些知识还不会,面试八成被劝退

Activity作为android四大组件之一,地位就不用多说了吧,该组件看起来是比较简单的,但是也涉及到很多知识点,要想全部理解并在合适的业务场景下使用,也是需要一定的技术沉淀,本文主要是对activity一些重要知识点进行总结整理,可能平时不一定用到,但是...
继续阅读 »

Activity作为android四大组件之一,地位就不用多说了吧,该组件看起来是比较简单的,但是也涉及到很多知识点,要想全部理解并在合适的业务场景下使用,也是需要一定的技术沉淀,本文主要是对activity一些重要知识点进行总结整理,可能平时不一定用到,但是一定要有所了解。


当然这些知识点并没有设计过多源码部分,比如activity的启动流程什么的,主要是零散的知识点,对于activity的启动流程网上文章太多了,后面自己也准备重新梳理下,好记性不如烂笔头,在不断学习整理的过程中,一定会因为某个知识点而豁然开朗。


image.png


1.生命周期


①.两个页面跳转


MainActivity跳转到SecordActivity的生命周期,重点关注MainonPauseonStopSecord几个关键生命周期的顺序,以及从Secord返回时与Main的生命周期的交叉:


image.png


可以发现Main页面的onPause生命周期之后直接执行SecordonCreate,onStart,onResume,所以onPause生命周期内不要执行耗时操作,以免影响新页面的展示,造成卡顿感。


②.弹出Dialog



  • 单纯的弹出Dialog是不会影响Activity的生命周期的;

  • 启动dialog themeActivity的时候,启动的activity只会执行onPause方法,onStop不会执行,被启动的activity会正常走生命周期,back的时候,启动的Activity会对应执行onResume方法;


image.png


③.横竖屏切换



  • AndroidManifest不配置configChanges时,横竖屏切换,会销毁重建Activity,生命周期会重新走一遍;

  • 当ActivityconfigChanges="orientation|screenSize"时,横竖屏切换不会重新走Activity生命周期方法,只会执行onConfigurationChanged方法,如需要可以在此方法中进行相应业务处理;



如横竖屏切换时需要对布局进行适配,可在res下新建layout-portlayout-land目录,并提供相同的xml布局文件,横竖屏切换时即可自动加载相应布局。(前提是未配置configChanges忽略横竖屏影响,否则不会重新加载布局)



④.启动模式对生命周期的影响


1.A(singleTask)启动(startActivity)B(standard),再从B启动A,生命周期如下:


A启动B:A_onPause、B_onCreate、B_onStart、B_onResume、A_onStop


第二步:B_onPause、A_onNewIntent、A_onRestart、A_onStart、A_onResume、B_onStop、B_onDestory


2.A(singleTask)启动A,或者A(singleTop)启动A


A_onPause、A_onNewIntent、A_Resume


3.singleInstance模式的activity


多次启动A(singleInstance),只有第一次会创建一个单独的任务栈(全局唯一),再次启动会调用A_onPause、A_onNewIntent、A_Resume


2.启动模式


Activity的启动模式一直是standardsingleTopsingleTasksingleInstance四种,Android 12新增了singleInstancePerTask启动模式,在这里不一一介绍,仅介绍重要知识点。


①.singleTask


1.Activity是一个可以跨进程、跨应用的组件,当你在 A App里打开 B AppActivity的时候,这个Activity会直接被放进A的Task里,而对于B的Task,是没有任何影响的。


从A应用启动B应用,默认情况下启动的B应用的Activity会进入A应用当前页面所在的任务栈中,此时按home建,再次启动B应用,会发现B应用并不会出现A启动的页面(前提是A应用启动的不是B应用主activity,如果是必然一样),而是如第一次启动一般.


如果想要启动B应用的时候出现被A应用启动的页面,需要设置B应用被启动页的launchmodesingleTask,此时从A应用的ActivityA页面启动B应用的页面ActivityBlaunchmodesingleTask),发现动画切换方式是应用间切换,此时ActivityBActivityA分别处于各自的任务栈中,并没有在一个task中,此时按Home键后,再次点击启动B应用,发现B应用停留在ActivityB页面。


如果想要实现上述效果,除了设置launchmode之外,还可以通过设置allowTaskReparenting属性达到同样的效果,Activity 默认情况下只会归属于一个 Task,不会在多个Task之间跳来跳去,但你可以通过设置来改变这个逻辑,如果你不设置singleTask,而是设置allowTaskReparentingtrue,此时从A应用的ActivityA页面启动B应用的页面ActivityB(设置了allowTaskReparentingtrue),ActivityB会进入ActivityA的任务栈,此时按Home键,点击启动B应用,会进入ActivityB页面,也就是说ActivityBActivityA的任务栈移动到了自己的任务栈中,此时点击返回,会依次退出ActivityB所在任务栈的各个页面,直到B应用退出。


注意:allowTaskReparenting在不同Android版本上表现有所不同,Android9以下是生效的,Android9,10又是失效的,但Android11又修复好了,在使用时一定要好好测试,避免一些因版本差异产生的问题。


②.singleInstance


singleInstance具备singleTask模式的所有特性外,与它的区别就是,这种模式下的Activity会单独占用一个Task栈,具有全局唯一性,即整个系统中就这么一个实例,由于栈内复用的特性,后续的请求均不会创建新的Activity实例,除非这个特殊的任务栈被销毁了。以singleInstance模式启动的Activity在整个系统中是单例的,如果在启动这样的Activity时,已经存在了一个实例,那么会把它所在的任务调度到前台,重用这个实例。


③.singleInstancePerTask


释义:singleInstancePerTask的作用和singleTask几乎一模一样,不过singleInstancePerTask不需要为启动的Activity设置一个特殊的taskAffinity就可以创建新的task,换句话讲就是设置singleInstancePerTask模式的activity可以存在于多个task任务栈中,并且在每个任务栈中是单例的。


多次启动设置singleInstancePerTask模式的Activity并不会多次创建新的任务栈,而是如singleInstance模式一样,把当前Activity所在的任务栈置于前台展示,如果想每次以新的任务栈启动需要设置FLAG_ACTIVITY_MULTIPLE_TASKFLAG_ACTIVITY_NEW_DOCUMENT,使用方式如下:

intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK | Intent.FLAG_ACTIVITY_NEW_DOCUMENT);

此时,每次启动Activity就会单独创建新的任务栈。


注意:测试需要在Android12的真机或者模拟器上,否则默认为Standard模式


3.taskAffinity


taskAffinity可以指定任务栈的名字,默认任务栈是应用的包名,前提是要和singleTask,singleInstance模式配合使用,standardsingleTop模式无效,当app存在多个任务栈时,如果taskAffinity相同,则在最近任务列表中只会出现处于前台任务栈的页面,后台任务栈会“隐藏”在某处,如果taskAffinity不同,最近任务列表会出现多个任务页面,点击某个就会把该任务栈至于前台。


4.清空任务栈


activity跳转后设置FLAG_ACTIVITY_CLEAR_TASK即可清空任务栈,并不是新建一个任务栈,而是清空并把当前要启动的activity置于栈底,使用场景比如:退出登录跳转到登录页面,可以以此情况activity任务栈。

intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK|Intent.FLAG_ACTIVITY_NEW_TASK);


注意:FLAG_ACTIVITY_CLEAR_TASK必须与FLAG_ACTIVITY_NEW_TASK一起使用.



5.Activity.FLAG


FLAG_ACTIVITY_NEW_TASK


FLAG_ACTIVITY_NEW_TASK并不像起名字一样,每次都会创建新的task任务栈,而是有一套复杂的规则来判断:



  • 通过activity类型的context启动,如果要启动的ActivitytaskAffinity与当前Activity不一致,则会创建新的任务栈,并将要启动的Activity置于栈底,taskAffinity一致的话,就会存放于当前activity所在的任务栈(注意启动模式章节第三点taskAffinity的知识点);

  • taskAffinity一致的情况下,如果要启动的activity已经存在,并且是栈根activity,那么将没有任何反应(启动不了要启动的activity)或者把要启动的activity所在的任务栈置于前台;否则如果要启动的activity不存在,将会在当前任务栈创建要启动的activity实例,并入栈;

  • taskAffinity一致的情况下,如果要启动的activity已经存在,但不是栈根activity,依然会重新创建activity示例,并入栈(前提是:要启动的activitylaunchModestandard,意思就是是否会创建新实例会受到launchMode的影响);

  • activitycontext启动activity时(比如在service或者broadcast中启动activity),在android7.0之前和9.0之后必须添加FLAG_ACTIVITY_NEW_TASK,否则会报错(基于android-32的源码,不同版本可能不同):
//以下代码基于android 12
public void startActivity(Intent intent, Bundle options) {
warnIfCallingFromSystemProcess();
final int targetSdkVersion = getApplicationInfo().targetSdkVersion;

//检测FLAG_ACTIVITY_NEW_TASK
if ((intent.getFlags() & Intent.FLAG_ACTIVITY_NEW_TASK) == 0
&& (targetSdkVersion < Build.VERSION_CODES.N
|| targetSdkVersion >= Build.VERSION_CODES.P)
&& (options == null
|| ActivityOptions.fromBundle(options).getLaunchTaskId() == -1)) {
//未设置FLAG_ACTIVITY_NEW_TASK,直接抛出异常
throw new AndroidRuntimeException(
"Calling startActivity() from outside of an Activity "
+ " context requires the FLAG_ACTIVITY_NEW_TASK flag."
+ " Is this really what you want?");
}
//正常启动activity
mMainThread.getInstrumentation().execStartActivity(
getOuterContext(), mMainThread.getApplicationThread(), null,
(Activity) null, intent, -1, options);
}


注意:FLAG_ACTIVITY_NEW_TASK的设置效果受到taskAffinity以及其他一些配置的影响,实际使用过程中一定要进行充分测试,并且不同的android版本也会表现不同,极端场景下要仔细分析测试,选择最优方案;




提示:通过adb shell dumpsys activity activities命令可以查看activity任务栈;



6.多进程


正常情况下,app运行在以包名为进程名的进程中,其实android四大组件支持多进程,通过manifest配置process属性,可以指定与包名不同的进程名,即可运行在指定的进程中,从而开启多进程,那么,开启多进程有什么优缺点呢?


多进程下,可以分散内存占用,可以隔离进程,对于比较重的并且与其他模块关联不多的模块可以放在单独的进程中,从而分担主进程的压力,另外主进程和子进程不会相互影响,各自做各自的事,但开启了多进程后,也会带来一些麻烦事,比如会引起Application的多次创建,静态成员失效,文件共享等问题。


所以是否选择使用多进程要看实际需要,我们都知道app进程分配的内存是有限的,超过系统上限就会导致内存溢出,如果想要分配到更多的内存,多进程不失为一种解决方案,但是要注意规避或处理一些多进程引起的问题;


设置多进程的方式:

android:process=":childProcess" //实际上完整的进程名为:包名:childProcess,这种方式声明的属于私有进程。

android:process="com.child.process" //完整的进程名即为声明的名字:com.child.process,这种方式声明的属于全局进程。

7.excludeFromRecents


excludeFromRecents如果设置为true,那么设置的Activity将不会出现在最近任务列表中,如果这个Activity是整个Task的根Activity,整个Task将不会出现在最近任务列表中.


8.startActivityForResult被弃用


使用Activity Result Api代替,使用方式如下:

private val launcherActivity = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()) {
Log.e("code","resultCode = "+it.resultCode)
}

findViewById<Button>(R.id.btn_jump).setOnClickListener {
launcherActivity.launch(Intent(this@MainActivity,SecordActivity::class.java))
}

//要跳转的Activity设置回调数据:
val resultIntent = Intent()
resultIntent.putExtra("dataKey","data value")
setResult(1001,resultIntent)
finish()

关于registerForActivityResult更多请点击这里查看。


9.Deep link


简单理解,所谓Deep Link就是可以通过外部链接来启动app或者到达app指定页面的一想技术,比如可以通过点击短信或者网页中的链接来拉起app到指定页面,以达到提供日活或者其他目的,一般流程是可以通过在manifestactivity标签中配置固定的scheme来实现这种效果,形如:

<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE"/>
<data
android:scheme="jumptest"
android:host="work"
android:port="8801"
android:path="/main"
/>
</intent-filter>

然后在网页中就可以通过如下方式来启动当前activity:

<a href="jumptest://work:8801/main?data=123456">你好</a>


格式 <scheme>://<host>:<port>/<path>?<query>



被启动的app可以通过如下方式拿到传递的参数以及schmea配置项:

val host = schemeIntent.data?.host
val path = schemeIntent.data?.path
val scheme = schemeIntent.data?.scheme
val query = schemeIntent.data?.query
Log.e("scheme","host = $host, path = $path, scheme = $scheme, query = $query")

结果:


image.png



注意:


1.intent-filter与Main主Activity搭配使用时,要单独开启一个intent-filter,否则匹配不到。

2.从android12开始,设置了intent-filter标签后,activity的exported必须设置成true,这个要注意(android12之前,其实添加了intent-filter,系统也会默认设置exported为true)。



①.app link


App link是一种特殊的Deep link,它的作用就是可以使通过网站地址打开app的时候,不需要用户选择使用哪个应用来打开,换种说法就是,我可以设置默认打开次地址的应用,这样一来,就可以直接引导到自己的app。


更多关于App link的可以参考这篇文章,或者看官网介绍


10.setResult和finish的顺序关系


通过startActivityForResult启动activity,通常会在被启动的activity的合适时机调用setResult来回调数据给上一个页面,然后当前页面返回的时候就会回调onActivityResult,这里要注意setResult的调用时机,请一定要在activity的finish()方法之前调用,否则可能不会生效(不会回调onActivityResult)


原因如下:

private void finish(int finishTask) {
if (mParent == null) {
int resultCode;
Intent resultData;
//会在finish的时候把回调数据赋值
synchronized (this) {
resultCode = mResultCode;
resultData = mResultData;
}
···
if (ActivityClient.getInstance().finishActivity(mToken, resultCode, resultData,
finishTask)) {
mFinished = true;
}
} else {
mParent.finishFromChild(this);
}
···
}

//setResult对mResultCode,mResultData赋值
public final void setResult(int resultCode) {
synchronized (this) {
mResultCode = resultCode;
mResultData = null;
}
}


由上述代码可以看出,setResult必须在finish之前赋值,才能够在finish的时候拿到需要callback的数据,以便在合适的时机回调onActivityResult


11.onSaveInstanceState()和onRestoreInstanceState()


activity在非正常情况被销毁的时候(非正常情况:横竖屏切换,系统配置发生变化,内存不足后台activity被回收等),当重新回到该activity,系统会重新实例化该对象,如果没有对页面输入的内容进行保存,就会存在内容丢失的情况,此时可以通过onSaveInstanceState来保存页面数据,在onCreate或者onRestoreInstanceState中对数据进行恢复,形如:

override fun onSaveInstanceState(outState: Bundle) {
outState.putString("SAVE_KEY","SAVE_DATA")
outState.putString("SAVE_KEY","SAVE_DATA2")
super.onSaveInstanceState(outState)
}
//需要判空,savedInstanceState不一定有值
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if(null != savedInstanceState){
saveData = savedInstanceState.getString("SAVE_KEY") ?: ""
saveData2 = savedInstanceState.getString("SAVE_KEY2") ?: ""
}
setContentView(R.layout.activity_main)
}

//或者在onRestoreInstanceState恢复数据,无需判空,回调此方法一定有值
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
saveData = savedInstanceState.getString("SAVE_KEY") ?: ""
saveData2 = savedInstanceState.getString("SAVE_KEY2") ?: ""
super.onRestoreInstanceState(savedInstanceState)
}



注意:请使用onSaveInstanceState(outState: Bundle)一个参数的方法,两个参数的方法和ActivitypersistableMode有关。



本文主要对Activity重难点知识进行整理和解释,希望对大家有所帮助,当然难免存在错误,如有发现,希望指正,如果感觉不错,麻烦点个赞,这将给我持续更文以更大的动力,后续如有其他知识点,也会持续更新。


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

浅谈中间人攻击

之前聊了一些前端安全性的问题,好多小伙伴反馈说聊的东西比较浅,很轻松就搞懂了,但实际上,真正生产环境下的网络安全问题多数是由一种或者多种简单的攻击方式混合导致的,所以掌握好基本的网络安全原理还是很有必要的,今天来聊聊中间人攻击。 中间人攻击 所谓的中间人攻击一...
继续阅读 »

之前聊了一些前端安全性的问题,好多小伙伴反馈说聊的东西比较浅,很轻松就搞懂了,但实际上,真正生产环境下的网络安全问题多数是由一种或者多种简单的攻击方式混合导致的,所以掌握好基本的网络安全原理还是很有必要的,今天来聊聊中间人攻击。


中间人攻击


所谓的中间人攻击一般发生在双方通信的过程当中,使用技术手段对通信进行拦截,然后基于通信的中间进行信息的转发,让通信双方错以为双方是在一条私密的信道上进行通信,实际上,整个会话已经被中间人完全掌控,其中的信息自然也是一览无余。


image.png


发生中间人攻击,信息泄露是肯定的了,还可能发生信息的篡改,所以危害比较大。


中间人攻击分析


和其他的安全问题一样,中间人攻击吃准了通信的双方缺乏授信的手段,然后占据双方通信的信道。基于这样的思路,可以想到的中间人攻击策略:


1、wifi欺骗:这个实际上是实现起来难度最小的一种攻击方法,大概的方法是攻击者创建一个周围受信的wifi,比如周围商场或者饭店为名称的wifi,引诱受害者链接wifi访问网络,这个时候数据通过wifi进行访问,那么在wife端很容易可以监控到使用者的信息。


2、HTTPS欺骗:利用大家对https协议的信任,通过一些是是而非的网站,比如:apple.com和Apple.com,或者浏览器识别但是肉眼不识别的特殊字符,比如o、ο,一个是英文的o,一个是希腊字母的omicron,肉眼不可见,但是浏览器确实会区分。


3、SSL劫持:通过给通信一方下发假的(中间人的)证书来阶段通信双方通信,一般以伪造SSL证书来攻击。


4、DNS欺骗:好多小伙伴在进入特殊的内网环境,比如公司的办公网,可能会配置自己的dns问题,比如windows当中的hosts文件,DNS欺骗就是通过修改DNS服务器的解析信息,将要访问的域名解析到中间人的服务器上。


5、电子邮件劫持:这个是近年来听说最多的一种攻击方式(我们公司的邮箱也发生过),这种攻击更需要社会学的知识,比如,以公司财务的类似的邮箱地址发送退税等邮件,诱惑受害者点击攻击链接。


当然攻击方式还有很多,但是上面的5种是我们常见的攻击方式。


思考:


中间人攻击其实已经相当有危害性了,因为这个攻击的发起人在了解技术的同时,对受害人的一些信息也是很了解的,比如:社会关系,家庭住址,对中间人攻击的防御更多的是需要考虑到使用网络的谨慎:


1、不随便链接模式的wife。


2、不要忽略浏览器的安全警告,好多小伙伴完全不在意。


3、不访问一些不好的网站(嘿嘿嘿)


4、定期查看自己的网络访问情况。


5、不要把核心的个人隐私放到计算机的浏览器缓存当中,比如银行卡的支付密码。


今天聊的是纯粹的理论,还是欢迎各位大佬多多指点。


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

我是如何保障亿级用户系统五年0故障

我负责了我们公司几个非常大的平台系统,日均访问量超过了千万级别,用户过亿。其中某个推送系统每天的消息推送量是过亿级别的。尽管流量和用户都巨大,高峰期的请求也非常高,但是这五年来我们没有出现过任何恶性的线上影响用户的定级故障,那我们是怎么样做到的呢?里面有没有一...
继续阅读 »

我负责了我们公司几个非常大的平台系统,日均访问量超过了千万级别,用户过亿。其中某个推送系统每天的消息推送量是过亿级别的。尽管流量和用户都巨大,高峰期的请求也非常高,但是这五年来我们没有出现过任何恶性的线上影响用户的定级故障,那我们是怎么样做到的呢?里面有没有一些值得借鉴的方法可以供大家参考呢?


首先并不是说我们这个系统天生就是稳定的,任何人来维护都不会引发故障,而实际上我们在这五年中也迭代了大量的需求,期间也发生过或大或小的一些变更事故、代码事故,但是都被我们良好的机制应急保障的非常成功,所以没有继续上升成影响多数用户的恶性故障,那么我今天就来跟大家分享一下我是怎么做到的。



对于故障的认识


首先是对于故障稳定性的认知上面。我经历过很多故障,特别是刚开始毕业的时候,由于自己的经验不够成熟,对于系统的故障的认知不够全面,所以导致了一系列影响线上客户的问题,而有一些还被升级成了定级故障。


所以对于大流量高并发的系统来说,最首要就是要建立对系统故障的认知。一个页面一个人访问,100个人访问和1万个人访问,它的影响面是不同的。研发同学对于自己所构建的系统能够影响多少用户应该有一个清晰的评估。比如我现在维护的系统每天的访问量有几千万,所以它的稳定性是至关重要的,稍有不慎可能会引起大面积的用户不可用。


作为研发同学,一定要认识到故障对于用户的体感来说是非常恶劣的,我们这个职责本身就要求我们要敬畏线上进敬畏客户,特别是对于我们这种实时系统,一旦发生了问题,用户可用性就会被打断,有时候造成的影响甚至是无法挽回的。


因此,对故障的认知、对职业的认知,就要求我们不能心存侥幸、马马虎虎、粗糙编码和上线。我们实际上通过各种案例发现,很多一线的研发同学丝毫不尊重用户进而造成引起恶性的线上事故。比如未经测试直接上线、发布后不管不问系统监控情况、业务出现问题后无法联系到相关的开发同学等等。


稳定性治理机制


在完成了自己对于故障的影响面认知程度之外,现在就是到了我们重点环节,就是要建立一整套完整的制度来保障稳定性。



  • 大盘和监控


在整个稳定性的保障里面,我觉得监控和告警是最重要的,因为如果没有了监控和告警,就无异于盲人摸象,整个系统到底有什么问题,问题什么时候会发生。发生了以后是什么样的影响面都不知道的情况下的话,就等于一个瞎子。


所以在系统或者业务上线的时候,就要同时伴随着监控和大盘的上线,我们不允许一个新的模块上线却不存在对应的监控的情况。


一般来说整个监控体系本身应该是比较完善的,有硬件、软件和业务系统的监控指标。也有跟周期相关的大盘的监控指标,比如说和上周的同比,和昨天的同比等等。在很多时候还可以对中间件进行一系列完整的监控,比如说对于数据库的监控,对于缓存的监控,对于PC框架调用的监控等。


还有一些可以针对自己业务单个接口的监控,在一些比较特殊的情况下的话,还有针对关键字的监控,比如可以单独配置监控日志里的NullPoint,用来快速定位到某些具体的问题,目前开源的一些监控系统都具备了这种即时数据收集和展现的能力。


除了监控之外,还要配套的就是报警机制。如果系统出了问题,研发同学第一时间感知不到。监控就等于是白费的,同时根据故障的等级、接口的调用量,我们会配置不同等级的监控,比如说非常紧急的问题,会用电话的方式进行报警。稍微弱一点的可能用群或者用短信的方式进行报警。

【集团报警】[2022/12/28 02:26] mm-orchard-push[hsf消费者成功率]
[C] 共有1台机器[hsf消费者成功率]触发[CRITICAL]报警, 摘要:
* 3x.6x.5x.1xx 当前时间的值: 87.50% < 90%


租户: 应用监控,应用: mm-orchard-push
报警统计:首次触发

报警的通知对象一般是业务的负责人或者固定的值班告警群等。这种报警的目的是能够第一时间让应用的负责人能感知到故障,并且让业务或者应用负责人作为接口人,能快速地找到上下游进行应急处理。当然告警机制本身也是需要演练的,以防止通知机制由于各种原因失灵导致无法及时把问题同步给负责人。比如以前就发生过系统短信欠费导致开发负责人收不到短信的问题发生。



  • 日常值班


还有一个事前预防性的措施就是日常的值班,日常的值班也分了两种,一种是我们的早值班,早值班主要是在8点~10点,这一段时间可能大部分的开发同学都没有来到公司上班的时候,我们会要求至少有一位员工是在线上观察问题。这个观察问题可以是查看系统日志或者获取线上用户的投诉CASE。


这个机制的保障可以监控到一些时间错位的问题。比如我们昨天晚上的发布,客户流量比较少,没有触发用户投诉,到了第二天早上客户大量的访问系统而造成的不可用引起的投诉。早值班处理的问题也是一样,也就是要第一时间感知到故障的发生,能够进行快速的一个止血,突出的也是一个敏捷性。


其次就是我们日常的常规值班,我们产品发布后都会有一些的产品不可用的问题、产品难用的咨询以及线上非预期的问题,那么我们会以一个值班群的方式,让客户或者业务方或者合伙合作伙伴都拉到群里,有一些客户在发现了客系统不可用的时候,第一时间会把不可用的问题提到群内,我们在值班的时候就能够及时快速的去判断这个问题是否是变更引起的故障问题。


不管在早值班还是在日常的答疑群里面,我们碰到这些问题的话,都会评估是否有故障的风险,然后都会尽快的成立故障应急小组,执行相应的预案或者计划。



  • 演练压测


演练和压测是预防故障里面非常重要的一个步骤,也就是通过一些常规性的动作模拟用户的大量请求,可以帮助发现系统的漏洞,把系统的不完善的地方全部暴露出来。我们在压测和演练的时候,一般会选在流量低峰期,既能暴露问题,又不会大面积的影响线上的真实客户。


那为什么要频繁演练呢?那是因为我们整个互联网的系统都是经常会有迭代和更新的需求,当我们某一次演练系统没有问题之后,业务可能又发生了大量的变化,很有可能会有新的故障点或者风险点的注入,那么这个时候通过常规化的演练,就可以更早暴露问题。


我们压测和演练都常规化了,每个月至少执行一次压测或者一次演练,压测一般也会选择核心接口以及本个本代里面新增的重要业务接口。在压测期间,我们会关注到对于上下游的业务分的调用以及自身的性能压力,当压测到极限的时候,发现了当内存、CPU、数据库还是外部依赖的超时的时候,我们会停止压测并记录问题,并最终复盘问题,对于相关的不符合预期的问题就进行一个分析和治理。



  • 技术方案评审


对于如此大流量的系统,我们要求所有的稍微大一点的需求变更,我们都要走完整的技术方案评审。因为有时候一个不合理的架构设计会导致故障频繁并且难以根治,架构的优雅性决定了故障的底线是高是低。


技术方案评审除了对于整个业务的ROI(投入产出比)进行一个通晒和判断之外,我们还会要求技术方案有完整的稳定性方案。


这个稳定性的方案一方面是要求对于现有的技术选型,要评估它是否会引入直接的风险点,比如说我们引进了一些新的缓存系统,那么缓存系统的容量能不能符合要求?缓存系统对我们业务保障的SLA又在多少?


除了对于系统方案的调研之外,我们也要求要有配套的保障的监控体系,比如我们这次引入的业务迭代有没有相关的监控和大盘?


其次就是要有业务开关和灰度策略。我们要求所有的核心功能上线都必须要有开关和灰度的方式,能够充分降低业务风险。而实际上表明我们对于这么大流量的情况下的话,用灰度是非常好的一个方式,灰度实际上就是把整个新的功能暴露在一小批用户并且我们去验证这些小批用户的可用性。


我们很多时候都发现我们在刚刚灰都了一批用户的时候,就发现了异常,我们及时的就会回滚和修复,这样就避免了把所有的用户暴露在故障和不可用的功能里面。



  • 故障应急机制


没有完美的系统,哪怕你的代码编写的再好,你的测试再完善,都可能会有遇到一些突发情况。比如非预期的流量、比如底层的网络超时、比如硬盘故障等。


所以我们成立了故障的应急机制。不管是发生了系统的自动告警,还是用户投诉,我们值班的同学或者业务的负责人能够第一时间感知到这些错误,并且能够快速得升级,按照SOP流程成立应急小组并把故障风险上升到指定的层级。


应急小组的形式往往是一个钉钉群,在必要的时候,我们会直接呼起电话会议,把上下游和受影响的团队都会全部拉上,快速的进行一个故障的初步判断以及止血方案的沟通。


所以我们的应急消防要求的特点就是要敏捷,能够快速的对故障进行响应,因为你只要响应的时间提前一分钟止血,客户受影响的时间就短了一分钟。很多大型公司会有保障制度,比如在指定的时间内完成对故障的处理,可以直接降低故障等级,也体现了公司的文化和价值倡导,即出问题不可怕,如果能快速止血问题,就是值得鼓励的行为。


因此我们在整个部门里面也要求做到1-5-15,也就是1分钟感知到故障5分钟定位的问题15分钟解决问题。当然在实际的过程中很难对于所有的故障都做到1-5-15,但是这是我们系统治理要持续追求的目标。



  • 紧急预案


我们的一些核心功能在上线的时候,我们都要求有紧急的降级预案,比如说当我们上线的功能发现了极端不可用的情况下的话,能否快速的止血?比如我们的产品就有一个非常好的全局降级计划,就是我们的服务端接口或者我们依赖方发生了大规模不可用的情况下的话,我们有一个紧急预案就是可以一键降级的缓存,那么客户就能够直接访问他的客户端缓存,这样的话就给了我们留下了很多时间去检验和修复问题。


紧急预案包含有很多方式,比如对于某些接口设置限流,在无法快速解决问题的时候,可以通过限流来保护系统,尽量把影响面降到最低。



  • 复盘


最后就是故障复盘。我们不能期待我们所有的欲望都是完美无缺的,正如系统一样,我们对于故障的认识和故障的处理也是需要反复迭代升级的。我们要求和鼓励复盘文化,不仅仅对影响到真实用户的问题进行复盘,也对潜在的问题进行复盘。


总结


首先我觉得对于一个研发同学来说,故障可能长期来看是不可避免的,但是我们还是要提升自己的对于故障的认知观,因为我们给客户造成了不可用,就是在一定程度上研发工程师的价值,那么我们应该追求写出非常优异的代码,能够写出非常鲁棒的系统,以及在系统出现了不可预期的问题下我们快速的去恢复用户的体验。


最后也不能因噎废食,不能因为怕引起故障就逃避写代码,这相信也不是公司请我们来的原因。而应该大胆创新、小心试错,在出现问题的时候,积极主动响应和治理,并且持续复盘进步,这就是一名优秀的工程师所要追求的素养。




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

简单回顾5年职业生涯,混子前端也要持续前行

基本情况 我是95年前端小菜鸟一只,目前在深圳一家创业公司担任前端小组长,组内两个都是今年刚毕业的小伙,本身自己就是半桶水了,挺怕耽误别人发展的。   写这篇总结主要是回顾一下自己从大学毕业到工作5年来的一些成长经历和心路历程,希望在...
继续阅读 »

基本情况



我是95年前端小菜鸟一只,目前在深圳一家创业公司担任前端小组长,组内两个都是今年刚毕业的小伙,本身自己就是半桶水了,挺怕耽误别人发展的。 




 写这篇总结主要是回顾一下自己从大学毕业到工作5年来的一些成长经历和心路历程,希望在自我回顾的过程中面对真实的自己,继续找到前行的目标和动力。



(PS:不含技术内容,非常水,JYM可以提前溜)


 学习经历



我一直都是一个没什么长远目标的人,喜欢随遇而安,可能也是因为缺乏主见,在高中前两年极尽自由的干了很多想干的事情,逃自习上网打LOL,上课睡觉看小说等等。 第三年的时候感觉周围的人都很努力,在这个氛围下觉得确实也更专注,加上自己还有点小聪明,拼搏了一年,幸运的考上了本省的一所211大学读计科-网络-信安大类。2年基础2年专业课程,因为上了大学少了老师和父母的管教之后放飞自我,经常熬夜打游戏看小说,以至于挂了高数和大物,然后也没认真学到很深的东西,只是为了应付考试,基本没学到太多干货,基本是吃喝玩乐混,毕业前总是想着回5线老家考个公务员事业单位啥的躺平一下。



大学的时候接触过C、JAVA、Python这些技术,但是都仅限于简单的课设要求 



  • 大一的时候加入过学校的社团,接触过一些校园网的技术,写过一些简单的**HTML+CSS+JQ**的简单页面,就是首页轮播图和一些文章管理的,所以当时就埋下了往前端发展的种子吧。

  •  大三暑假那年出了点状况,改变了初衷,所以为了搞钱开始想到一线城市卷一卷。但是由于没有加入导师的实验室做项目,基本没有项目经验,课设作业很多也是抄宿舍大佬的。

  • 大四开学开始就开始整一些简历准备秋招,那会中软国际来学校招聘,当时也没怎么上网冲浪了解,不知道外包公司对未来职业发展影响这么大(后续补充),只是觉得别的校招笔试都挺难的,当时参加过广州多益网络,VIVO一些公司的笔试都没过,这个过了之后能到深圳并且有机会到华为本部工作,觉得是一个不错的机会,当时没有招聘前端的,只有C和JAVA的后端开发岗位,由于自己JAVA不行选择了C,被分配到华为的交换机框式嵌入式相关的开发部门,当时觉得自己反正也是小白,跟着学习应该是可以的,可是事实证明不是钻研的性格的话很难往下发展,都是后话了


 2017~2018




  • 17年2月春节收假后接到电话说节后要到深圳参加青训营,然后开始实习,去到那边之后和来自不同校招的同期大约100+人一起参加青训营,其实就是各业务线负责人讲一下公司的具体项目的运作流程,开发的开发/测试/上线/运维的一些相关内容,为期一周,然后一起去松山湖骑行玩了一天,就被分配导师带领到各自的业务线进行实习了。




  • 我起初是在东莞那边做交换机命令行的嵌入式开发,三方签的6.5K,实习2.5K不包吃住,所幸东莞租房便宜,单间基本500左右,华为的嵌入式开发慢慢迁移过松山湖那边了,机房需要场地大,在深圳成本高。




  • 我们这边基本是做华为的外包项目,不在华为本部,主要就是开发一些框式交换机的命令行和状态监控相关的一些东西,根据业务需求开发某个模块,由华为SE写文档,MDE细化,我们根据切分的业务模块成为小owner,随后拉相关的人开会澄清,确定周期之后进入开发,刚开始接触项目代码的时候,感觉头非常大,因为嵌入式C的开发和自己在校学习的差别巨大,很多钩子根本不懂是哪块代码引入的,而且因为我们是外包,核心的模块我们是没有权限查看代码的,只能够看的是自己业务模块的仓库,开始都是导师指导我们哪块有做过类似的,让我们把那个函数CV过去,改一些逻辑这样子,开始基本上一天才能写两个功能函数,提交代码的时候git也要由华为的人审核代码才能合入,然后由CI/CD集成生成的软件包,还要通过网口上传到设备,老一些的只能通过烧录的方式,所以开发快但是验证很麻烦;但是还是掌握了git的基本命令和团队开发的基本流程




  • 毕业后直到18年初每天基本都是加班到10点/11点的样子,感觉自己对于C和嵌入式的掌握还是不够,并且没有太大的发展兴趣,而且得知中软基本上招外包很少给涨薪,基本上第二年来的都是倒挂的,毕竟只需要你来做简单的功能模块开发,然后后续相同的类似功能都交给你,比较难/核心的需求基本上都是华为本部做,重复/机械的业务才会外包到中软/软通一类的公司。所以当时就谋划着润,可是又不太想继续搞嵌入式,所以就内部转岗积累一些前端的项目经验,当时面试原型链/闭包之类的都没答上,所以只能平薪到了一个部门做前端开发,当时项目组用的angular1和echarts开发,从18年3~12月就基本是在做中国铁塔某分公司的系统,重新开始做回前端,开始学习到ES6和一些组件开发的思想,彼时vue和react已经比较流行了,但是项目组没有使用,而自己也是比较佛系的人,下了班就是跟兄弟们开黑打本,当时沉迷DNF基本周末都在打乌龟卢克刷SS什么的,也错过了一段飞速发展期




  • 随着当初一起进中软的同学们一个个跳槽到其他公司,薪资都上了10K以上的时候,非常羡慕,但是整简历出去面试的时候,因为大部分公司都是用vue/react,所以当时面试情况不太乐观,决定停一段时间好好学习和复习面试,于是18年底选择了裸辞,当时在出租屋好好浪了一段时间推古剑,然后才开始学习vue,都是在网上一些课程和文档,然后git根据原项目整一些demo这样,然后去成都找了波发小,他校招进的中国工商银行,宇宙行要分配到成都当1年半电话客服,去那边玩了几天,逛了锦里,武侯祠,看了大熊猫,吃了好几顿火锅串串,成都真的是一个很美丽的城市,本来还想去川西玩玩,结果快过年就先回家了~ 其实想想成都还是一座蛮适合程序员发展的城市,和杭州一样是新兴的准一线




 2019~2022




  • 年后进行面试,因为中软的外包工作经历,过了面试也会被这个为由压薪资,说外包2年等于半年之类的,当时贼气,感觉如果有点选择的话刚毕业的同学们少选外包,除非钱给的足,而且基本上是入职即巅峰。




  • 19年就是进入现在这家创业公司,是做工业互联网相关的自研系统,终于薪资涨到了11K,不过是大小周,但是当时公司的技术栈是vue,而且有个5年的老哥带,所以就决定留下,开始上手时也因为都没怎么用过,element,axios,vuex以及很多客户定制的内容,所以那段时间也是飞速进步的一段时间,那位老哥其实vue的使用时间也不长,但是当时确实教了我不少关于vue的相关知识,包括vuex,指令等等。还是很感谢他的,入职后我就被老板让弄看板相关的开发,就是很多echarts图表展示,那段时间啃**echarts**文档很勤,也在社区找了很多custom的例子和特效来弄,痛并快乐着吧。结果7月的时候老哥和老板吵了一架,当场就和我交接了,然后我就莫名其妙成了唯一一个前端,开始整体维护公司的前端项目,包括后台PC,小程序(原生)这些,痛并快乐着,但是确实是磨练人,小公司的好处是一个人又当开发又当测试又兼UI,当时公司的项目也是外包某个公司用vueCLI+webpack3搭的,基本没有内部组件,是一个很粗糙的项目,当时感觉好多功能用element的模块不满足业务场景,包括大数据table和tree会导致卡顿等一系列问题,所以在论坛上找了pl-table,学习到了虚拟列表和可编辑表格的一些处理方式,才算慢慢走上了前端开发的正规,第一年挺潦草的度过了。




  • 20年过年时爆发了疫情,为了怕不能正常回深办公,2月底就提前回深了,3月在家办公了一个多月,当时基本是开会腾讯视频,然后根据客户定制的功能模块用墨刀画UI,然后自己进行开发,但是那段时间我们系统的定位不对,跟很多同行竞争中丢单,所以老板和合伙人也很焦急,随后我们老大(据说是TX10000以内工号的,但是感觉他之前做游戏的,主要用的C++,对现在系统用的Java微服务架构不了解,后来找前同事高级架构师给我们系统重新设计了架构,又进行了一波重构,然后老板又从IBM拉了一位资深顾问,带技术团队的来给我们重新设计了UI和交互,然后从20年底开始进入重构,本来顾问大佬问我能不能用AntDesign,由于当时对vue的支持不行,又不太想切换到react,所以就回绝了,又错失一个成长的机会。20年基本上因为已经习惯了现有的技术,基本上需求都是能够及时完成,所以又进入了舒适圈,除非遇到需要的时候才会主动去搜索相关的信息,所以导致了现在的焦虑与迷茫,这是后话了,可能自己一开始就不是一个合格的程序员吧。哦我们公司每年会给小涨薪1K,这年达到13K。




  • 21年由于重构项目需要扩充人手,我面试了一些人,招了个2年多的开发,和他一起用3个月完成了原系统的重构,这年因为很多客户需要定制开发和类excel表格录入,所以我们用了**handsonTable**这个三方完成,但是这个二次开发不像element等组件好改,基本上是要使用到JSX的语法,所以也“被迫”的跳出舒适区;因为需要打印,又学习了不少打印相关的三方组件,最终还是选择了无预览的**LODOP**,公司业务也开始好转,基本上能够达到盈亏平衡。此时薪资涨到14K,但是相比于同龄,不少同学多次跳槽之后已经有25K往上,虽然当前创业公司薪资少,但是确实有给到我一些股份,并且氛围相对轻松,所以暂时也还没有动的想法,但是感觉到技术已经在止步了,然后开始接触到掘金,看了同龄人的一些技术分享和B站的一些模拟面试,感觉自己和他们比在知识体系上差距有些大,不过彼时已经有了萌生退意的想法,想要回老家省会发展,然后年底考了一个公安厅的事业单位,笔试过了,3人进入面试,最终结构化面试没发挥好,比第一差了0.01分,无缘编制,就只能想想看怎么卷了。




  • 22年初立了一些flag,想要拼一拼大厂,但是组内另一个前端因为某个小程序新需求和老大吵了一大架,然后当天下午就和我交接了T-T,本来还想着他顶我,我好润的,然后今年经济下行的情况下,公司业务居然还有增长,导致了我又变得忙了起来,下班基本就10点了,完全没有学习的动力和精力,前半年就这么过了,6月的时候改了简历,想着去外面尝试一下看看行情如何,结果也一直没有去面试,可能也是还没下定决心改变吧,所以总想着拖延一下,加上公司在人员流失的情况下,为了稳住老员工,开始给我们按项目收款的比例获得奖金,所以薪资有了一些涨幅,所以感觉自己有点像处在温水里的青蛙,因为这个公司也创业4年了,目前看来市场还不够认可,只能算是维持在一个状态,但是自己也慢慢走到了成家立业的年龄,父母的身体也慢慢变差,疫情以来经历了外公的离世,以及好友父亲由于癌症离开,感到得过且过有点不太好,所以想要改变下现有的状态,不要到周末节假日就报复性熬夜,少玩游戏,少追剧,多花一些时间重塑自己的职业技能,好好掌握未熟练的浏览器、JS、工程化、TS、把行业新出的内容应用到现在的项目中,提高开发效率的同时也能进行学校提升。




 回顾总结



回看自己这5年,其实大多数时候还是缺乏思考,没有做好职业规划,所以目前虽然年限到了但是感觉技术水平远没有达到同龄人的水准,当然可能与工作中虽然有记笔记,但是没有整理总结输出,所以学习效果不佳吧。 也可能是大家都比较卷吧,毕竟行业如此,没有人能独善其身,既然决定往下走,那么也只能把缺失的东西找补回来,最近在看《认知觉醒》,里面提到提到人多数时候被本能和情绪控制习惯于做简单不思考的事,缺乏耐心坚持长期有益的事情,颇有感悟,写下此篇回顾也是为了直面自己的过去,然后好好复盘与修正不足,为了变成更好的自己提供一些借鉴,加油吧!


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

再学http-为什么文件上传要转成Base64?

web
1 前言 最近在开发中遇到文件上传采用Base64的方式上传,记得以前刚开始学http上传文件的时候,都是通过content-type为multipart/form-data方式直接上传二进制文件,我们知道都通过网络传输最终只能传输二进制流,所以毫无疑问他们本...
继续阅读 »

1 前言


最近在开发中遇到文件上传采用Base64的方式上传,记得以前刚开始学http上传文件的时候,都是通过content-type为multipart/form-data方式直接上传二进制文件,我们知道都通过网络传输最终只能传输二进制流,所以毫无疑问他们本质上都是一样的,那么为什么还要先转成Base64呢?这两种方式有什么区别?带着这样的疑问我们一起来分析下。


2 multipart/form-data上传


先来看看multipart/form-data的方式,我在本地通过一个简单的例子来查看http multipart/form-data方式的文件上传,html代码如下


<!DOCTYPE html>
<html>
<head>
<title>上传文件示例</title>
<meta charset="UTF-8">
<body>
<h1>上传文件示例</h1>
<form action="/upload" method="POST" enctype="multipart/form-data">
<label for="file">选择文件:</label>
<input type="file" id="file" name="file"><br>
<label for="tx">说明:</label>
<input type="text" id="tx" name="remark"><br><br>
<input type="submit" value="上传">
</form>
</body>
</html>

页面展示也比较简单


image.png


选择文件点击上传后,通过edge浏览器f12进入调试模式查看到的请求信息。

请求头如下
image.png
在请求头里Content-Type 为 multipart/form-data; boundary=----WebKitFormBoundary4TaNXEII3UbH8VKo,刚开始看肯定有点懵,不过其实也不复杂,可以简单理解为在请求体里要传递的参数被分为多部份,每一部分通过分解符boundary分割,就比如在这个例子,表单里有file和remark两个字段,则在请求体里就被分为两部分,每一部分通过boundary=----WebKitFormBoundary4TaNXEII3UbH8VKo来分隔(实际上还要加上CRLF回车换行符,回车表示将光标移动到当前行的开头,换行表示一行文本的结束,也就是新文本行的开始)。需要注意下当最后一部分结尾时需要加多两个"-"结尾。

我们继续来看请求体


image.png
第一部分是file字段部分,它的Content-Type为image/png,第二部分为remark字段部分,它没有声明Content-Type,则默认为text/plain纯文本类型,也就是在例子中输入的“测试”,到这里大家肯定会有个疑问,上传的图片是放在哪里的,这里怎么没看到呢?别急,我猜测是浏览器做了特殊处理,请求体里不显示二进制流,我们通过Filder抓包工具来验证下。


image.png
可以看到在第一部分有一串乱码显示,这是因为图片是二进制文件,显示成文本格式自然就乱码了,这也证实了二进制文件也是放在请求体里。后端使用框架springboot通过MultipartFile接受文件也是解析请求体的每一部分最终拿到二进制流。


@RestController
public class FileController {
// @RequestParam可接收Content-Type 类型为:multipart/form-data 
// 或 application/x-www-form-urlencoded 请求体的内容
@PostMapping("/upload")
public String upload(@RequestParam("file") MultipartFile file) {
return "test";
}
}

到此multipart/form-data方式上传文件就分析完了,关于multipart/form-data官方说明可参考 RFC 7578 - Returning Values from Forms: multipart/form-data (ietf.org)


3 Base64上传


在http的请求方式中,文件上传只能通过multipart/form-data的方式上传,这样一来就会有比较大的限制,那有没其他方式可以突破这一限制,也就是说我可以通过其他的请求方式上传,比如application/json?当然有,把文件当成一个字符串,和其他普通参数没什么两样,我们可以通过其他任意请求方式上传。如果转成了字符串,那上传文件就比较简单了,但问题是我们怎么把二进制流转成字符串,因为这里面可能会有很多“坑”,业界一般的做法是通过Base64编码把二进制流转成字符串,那为什么不直接转成字符串而要先通过Base64来转呢?我们下面来分析下。


3.1 Base64编码原理


在分析原理之前,我们先来回答什么是Base64编码?首先我们要知道Base64只是一种编码方式,并不是加解密算法,因此Base64可以编码,那也可以解码,它只是按照某种编码规则把一些不可显示字符转成可显示字符。这种规则的原理是把要编码字符的二进制数每6位分为一组,每一组二进制数可对应Base64编码的可打印字符,因为一个字符要用一个字节显示,那么每一组6位Base64编码都要在前面补充两个0,因此总长度比编码前多了(2/6) = 1/3,因为6和8最小公倍数是24,所以要编码成Base64对字节数的要求是3的倍数(24/8=3字节),对于不足字节的需要在后面补充字节数,补充多少个字节就用多少个"="表示(一个或两个),这么说有点抽象,我们通过下面的例子来说明。

我们对ASCII码字符串"AB\nC"(\n和LF都代表换行)进行Base64编码,因为一共4字节,为了满足是3的倍数需要扩展到6个字节,后面补充了2个字节。


image.png


表3.1


转成二级制后每6位一组对应不同颜色,每6位前面补充两个0组成一个字节,最终Base64编码字符是QUIKQw==,Base64编码表大家可以自行网上搜索查看。


image.png
我们通过运行程序来验证下


image.png
最终得出的结果与我们上面推理的一样。


3.2 Base64编码的作用


在聊完原理之后,我们继续来探讨文件上传为什么要先通过Base64编码转成字符串而不直接转成字符串?一些系统对特殊的字符可能存在限制或者说会被当做特殊含义来处理,直接转成普通字符串可能会失真,因此上传文件要先转成Base64编码字符,不能把二进制流直接字符串。


另外,相比较multipart/form-data Base64编码文件上传比较灵活,它不受请求类型的限制,可以是任何请求类型,因为最终就是一串字符串,相当于请求的一个参数字段,它不像二进制流只能限定multipart/form-data的请求方式,日常开发中,我们用的比较多的是通过apllication/json的格式把文件字段放到请求体,这种方式提供了比较便利的可操作性。


4 总结


本文最后再来总结对比下这两种文件上传的方式优缺点。

(1)multipart/form-data可以传输二进制流,效率较高,Base64需要编码解码,会耗费一定的性能,效率较低。

(2)Base64不受请求方式的限制,灵活度高,http文件二进制流方式传输只能通过multipart/form-data的方式,灵活度低。

因为随着机器性能的提升,小文件通过二进制流传输和字符串传输,我们对这两种方式时间延迟的感知差异并不那么明显,因此大部分情况下我们更多考虑的是灵活性,所以采用Base64编码的情况也就比较多。


作者:初心不改_1
来源:juejin.cn/post/7251131990438264889
收起阅读 »

Web的攻击技术: 别让我看到你网站的缺陷,不然你看我打不打你🍗🍗🍗

简单的 HTTP 协议本身并不存在安全性问题,因此协议本身几乎不会成为攻击的对象。应用 HTTP 协议的服务器和客户端,已经运行在服务器上的 Web 应用等资源才是攻击目标。 在客户端即可篡改请求 在 Web 应用中,从浏览器那接收到的 HTTP 请求的全部内...
继续阅读 »

简单的 HTTP 协议本身并不存在安全性问题,因此协议本身几乎不会成为攻击的对象。应用 HTTP 协议的服务器和客户端,已经运行在服务器上的 Web 应用等资源才是攻击目标。


在客户端即可篡改请求


Web 应用中,从浏览器那接收到的 HTTP 请求的全部内容,都可以在客户端自由地变更、篡改。所以 Web 应用可能会接收到与预期数据不相同的内容。


HTTP 请求报文内加载攻击代码,就能立发起对 Web 应用的攻击,通过 URL 查询字段或表单、HTTP 首部、Cookie 等途径吧攻击代码传入,若这时 Web 应用存在安全漏洞,那内部信息就会遭到窃取,或被攻击者拿到管理权限。
20230701072440


针对 Web 应用的攻击模式


Web 应用的攻击模式有以下两种:



  • 主动攻击;

  • 被动攻击;


以服务器为目标的主动攻击


主动攻击是指攻击者通过直接访问 Web 应用,把攻击代码传入的模式,由于该模式是直接针对服务器上的资源进行攻击,因此攻击者能够访问到那些资源。


主动攻击模式里面具有代表性的攻击是 SQL 注入攻击和 OS 命令注入攻击。


20230701072818


以服务器为目标的被动攻击


被动攻击是指利用圈套策略执行攻击代码的攻击模式,在被动攻击过程中,攻击者不直接对目标 Web 应用访问发起攻击。


被动攻击通常的攻击模式如下所示:



  1. 攻击者诱使用户触发已设置好的陷阱,而陷阱会启动发送已嵌入攻击代码的 HTTP 请求;

  2. 当用户不知不觉中招之后,用户的浏览器或邮件客户端会触发这个陷阱;

  3. 中招后的用户浏览器会把含有攻击代码的 HTTP 请求发送给作为攻击目标的 Web 应用,运行攻击代码;

  4. 执行完攻击代码,存在安全漏洞的 Web 应用会成为攻击者的跳板,可能导致用户所持的 Cookie 等个人信息被窃取,登录状态中的用户权限遭恶意滥用等后果。


被动攻击模式中具有代表性的攻击是跨站脚本攻击和跨站点请求伪造。
20230701074804


利用被动攻击,可发起对原本从互联网上无法直接访问的企业内网等网络的攻击。只要用户踏入攻击者预先设好的陷阱,在用户能够访问到的网络范围内,即使是企业内网也同样会受到攻击。


20230701075024


因输出值转移不完全引发的安全漏洞


实施 Web 应用的安全对策可大致分为以下两部分:



  • 客户端的验证;

  • Web 应用端的验证:

    • 输入值验证;

    • 输出值转义;




20230701075829


因为 JavaScript 代码可以在客户端随便修改或者删除,所以不适合将 JavaScript 验证作为安全的方法策略。保留客户端验证只是为了尽早地辨识输入错误,起到提高 UI 体验的作用。


输入值验证通常是指检查是否是符合系统业务逻辑的数值或检查字符编码等预防对策。


从数据库或文件系统、HTML、邮件等输出 Web 应用处理的数据之际,针对输出做值转义处理是一项至关重要的安全策略。当输出值转义不完全时,会因触发攻击者传入的攻击代码,而给输出对象带来损害。


跨站脚本攻击


跨站脚本攻击(Cross-Site Scripting,XSS)是指通过存在安全漏洞的 Web 网站注册用户的浏览器内运行非法的 HTML 标签或 JavaScript 进行的一种攻击。动态创建的 HTML 部分有可能隐藏这安全漏洞。就这样,攻击者编写脚本设下陷阱,用户在自己的浏览器上运行时,一不小心就会受到被动攻击。


跨站脚本攻击有可能造成一下影响:



  • 利用虚假输入表单骗取用户个人信息;

  • 利用脚本窃取用户的 Cookie 值,被害者在不知情的情况下,帮助攻击者发送恶意请求;

  • 显示伪造的文章或图片;


跨站脚本攻击案例


在动态生成 HTML 处发生


下面以编辑个人信息页面为例讲解跨站脚本攻击,下放界面显示了用户输入的个人信息内容:
20230701090312


确认姐妹按原样显示在编辑解密输入的字符串。此处输入带有山口伊朗这样的 HTML 标签的字符串。


那如果我把输入的内容换成一段 JavaScript 代码呢,阁下又该如何应对?


<script>
alert("落霞与孤鹜齐飞,秋水共长天一色!");
</script>

XSS 是攻击者利用预先设置的陷阱触发的被动攻击


跨站脚本攻击属于被动攻击模式,因此攻击者会事先布置好用于攻击的陷阱。


下图网站通过地址栏中 URI 的查询字段指定 ID,即相当于在表单内自动填写字符串的功能。而就在这个地方,隐藏着可执行跨站脚本攻击的漏洞。
20230701091251


充分熟知此处漏洞特点的攻击者,于是就创建了下面这段嵌入恶意代码的 URL。并隐藏植入事先准备好的欺诈邮件中或 Web 页面内,诱使用户去点击该 URL


浏览器打开该 URI 后,直观感觉没有发生任何变化,但设置好的脚本却偷偷开始运行了。当用户在表单内输入 ID 和密码之后,就会直接发送到攻击者的网站,导致个人登录信息被窃取。


之后,ID 及密码会传给该正规网站,而接下来仍然是按正常登录步骤,用户很难意识到自己的登录信息已遭泄露。


除了在表单中设下圈套之外,下面那种恶意构造的脚本统一能够通过跨站脚本攻击的方式,窃取到用户的
Cookie 信息:


const cookie = document.cookie;

执行上面这段 JavaScript 程序,即可访问到该 Web 应用所处域名下的 Cookie 信息,然后这些信息会发送至攻击者的 Web 网站,记录在它的登录日志中,攻击者就这样窃取到用户的 cookie 信息了。


React 中通过 JSX 语法转义来防止 XSS。在 JSX 语法中,可以通过花括号 {} 插入 JavaScript 表达式。JSX 语法会自动转义被插入到 HTML 标签之间的内容。这意味着任何用户输入的内容都会被转义,以防止恶意脚本的执行。在转义过程中,React 会将特殊字符进行转换,例如将小于号 < 转义为 <、大于号 > 转义为 >、引号 " 转义为 " 等。这样可以确保在渲染时,用户输入的内容被当作纯文本处理,而不会被解析为 HTML 标签或 JavaScript 代码。


SQL 注入攻击


会执行非法 SQL 的 SQL 注入攻击


SQL 注入是指针对 Web 应用使用的数据库,通过运行非法的 SQL 而产生的攻击。该安全隐患有可能引发极大的威胁,有时会直接导致个人信息及机密信息的泄露。


SQL 注入攻击有可能会造成以下等影响:



  • 非法查看或篡改数据库内的数据;

  • 规避认证;

  • 执行和数据库服务器业务关联的程序等;


SQL 注入攻击案例



  • 登录绕过攻击: 攻击者可以在登录表单的用户名和密码字段中插入恶意的 SQL 代码。如果应用程序未对输入进行正确的验证和过滤,攻击者可以通过在用户名字段中输入 ' OR '1'='1 的恶意输入来绕过登录验证,使得 SQL 语句变为:SELECT \* FROM users WHERE username = '' OR '1'='1' AND password = '<输入的密码>',从而成功登录到系统中;

  • 删除或修改数据攻击: 攻击者可以通过注入恶意的 SQL 代码来删除或修改数据库中的数据。例如,攻击者可以在一个表单的输入字段中插入 '; DROP TABLE users;-- 的恶意输入。如果应用程序未正确处理这个输入,攻击者可以成功删除用户表(假设表名为 "users")导致数据丢失;


OS 命令注入攻击


OS 命令注入攻击是指通过 Web 应用,执行非法的操作系统命令达到攻击的目的。只要在能调用 Shell 函数的地方就有存在被攻击的风险。


可以从 Web 应用中通过 Shell 来调用操作系统命令,如果调用 Shell 是存在疏漏,就可以执行插入的非法 OS 命令。


OS 命令注入攻击可以向 Shell 发送命令,让 WindowsLinux 操作系统的命令行启动程序。也就是说,通过 OS 注入攻击可执行 OS 上安装着的各种程序。


OS 注入攻击案例



  • 文件操作攻击: 攻击者可以在应用程序的输入字段中插入恶意的操作系统命令来执行文件操作。例如,如果应用程序在文件上传过程中未对输入进行适当的验证和过滤,攻击者可以在文件名字段中插入 '; rm -rf / ;-- 的恶意输入。如果应用程序在执行文件操作时没有正确处理这个输入,攻击者可能会删除服务器上的所有文件;

  • 远程命令执行攻击: 攻击者可以通过注入恶意的操作系统命令来执行远程系统命令。例如,如果应用程序在一个输入字段中插入 ; ping <恶意 IP 地址> ; 的恶意输入,而没有进行正确的输入验证和过滤,攻击者可以利用该注入漏洞执行远程命令,对目标系统进行攻击或探测;


HTTP 首部注入攻击


HTTP 首部注入攻击是指攻击者通过在响应首部字段内插入换行,添加任意响应首部或主体的一种攻击。属于被动攻击模式。


HTTP 首部注入攻击有可能造成以下一些影响:



  • 设置任何 Cookie 信息;

  • 重定向值任意 URL;

  • 显示任意的主体;


HTTP 首部注入攻击案例


以下是一些 HTTP 首部注入攻击的案例,展示了攻击者是如何利用该漏洞进行攻击的:



  • 重定向攻击: 攻击者可以在 HTTP 响应的 Location 首部中插入恶意 URL,从而将用户重定向到恶意网站或欺骗性的页面。如果应用程序未正确验证和过滤用户输入,并将其直接用作 Location 首部的值,攻击者可以在 URL 中插入换行符和其他特殊字符,添加额外的首部字段,导致用户被重定向到意外的位置;

  • 缓存投毒攻击: 攻击者可以在 HTTP 响应的 Cache-Control 或其他缓存相关首部中插入恶意指令,以欺骗缓存服务器或浏览器,导致缓存数据的污染或泄漏。攻击者可以通过注入换行符等特殊字符来添加额外的首部字段或修改缓存指令,绕过缓存机制或引发信息泄露;

  • HTTP 劫持攻击: 攻击者可以在 HTTP 响应的 LocationRefresh 首部中插入恶意 URL,将用户重定向到恶意网站或欺骗性页面,从而劫持用户的会话或执行其他攻击。通过在响应中插入恶意的 LocationRefresh 值,攻击者可以修改用户的浏览器行为;

  • XSS 攻击: 攻击者可以在 HTTP 响应的 Set-Cookie 或其他首部字段中插入恶意脚本,以执行跨站脚本攻击。如果应用程序未正确过滤和转义用户输入,并将其插入到首部字段中,攻击者可以通过注入恶意代码来窃取用户的会话标识符或执行其他恶意操作;


因会话管理疏忽引发的安全漏洞


会话管理是用来管理用户状态的必备功能,但是如果在会话管理上有所疏忽,就会导致用户的认证状态被窃取等后果。


会话劫持


会话劫持是指攻击者通过某种手段拿到了用户的会话 ID,并非法使用此会话 ID 伪装成用户,达到攻击的目的:
20230701104716


具备人中功能的 Web 应用,使用会话 ID 的会话管理机制,作为管理认证状态的主流方式。会话 ID 中记录客户端的 Cookie 等信息,服务端将会话 ID 与认证状态进行一对一匹配管理。


下面列举了几种攻击者可获得会话 ID 的途径:



  • 通过非正规的生成方法推测会话 ID;

  • 通过窃听或 XSS 攻击盗取会话 ID;

  • 通过会话固定攻击强行获取会话 ID;


会话劫持攻击案例


下面我们以认证功能为例讲解会话劫持。这里的认证功能通过会话管理机制,会将成功认证的用户的会话 ID,保存在用户浏览器的 Cookie 中。
20230701105419


攻击者在得知该 Web 网站存在可跨站攻击的安全漏洞后,就设置好用 JavaScript 调用 document.cookie 以窃取 cookie 信息的陷阱,一旦用户踏入陷阱访问了该脚本,攻击者就能获取含有会话的 IDCookie


攻击者拿到用户的会话 ID 后,往自己的浏览器的 Cookie 中设置该会话 ID,即可伪装成会话 ID 遭窃的用户,访问 Web 网站了。


会话固定攻击


对一切去目标会话 ID 为主动攻击手段的会话劫持而言,会话固定攻击 攻击会强制用户使用指定的会话 ID,属于被动攻击。


会话固定攻击案例


下面我们以认证功能为例讲解会话固定攻击,这个 Web 网站的认证功能,会在认证前发布一个会话 ID,若认证成功,就会在服务器内改变认证状态。


20230701152056


攻击者准备陷阱,先访问 Web 网站拿到会话 ID,此刻,会话 ID 在服务器上的记录仪仍是未认证状态。


攻击者设计好强制用户使用该会话 ID 的陷阱,并等待用户拿着这个会话 ID 前去认证,一旦用户触发陷阱并完成认证,会话 ID 服务器上的状态(用户 A 已认证) 就会被记录下来。


攻击者估计用户差不多已触发陷阱后,再利用之前这个会话 ID 访问网站,由于该会话 ID 目前已是用户 A 已认证状态,于是攻击者作为用户 A 的身份顺利登陆网站。


会话固定攻击预防措施


会话固定攻击利用了应用程序在身份验证和会话管理过程中未正确处理会话标识符的漏洞。为了防止会话固定攻击,开发人员可以采取以下措施:



  1. 生成随机、唯一的会话标识符,并在用户每次登录或创建新会话时重新生成;

  2. 不接受用户提供的会话标识符,而是通过服务器生成并返回给客户端;

  3. 在身份验证之前和之后,对会话标识符进行适当的验证和验证机制;

  4. 设置会话管理策略,包括会话超时时间和注销会话的方式;


通过采取这些预防措施,可以减少会话固定攻击的风险,并提高应用程序的安全性。


跨站点请求伪造


跨站点伪造请求(Cross-Site Require Forgeries,CSRF)攻击是指攻击者通过设置好的陷阱,强制用户对已完成认证的用户进行非预期的个人信息或设定信息等某些状态更新,属于被动攻击。


跨站点请求伪造有可能会造成一下等影响:



  • 利用已通过认证的用户权限更新设定信息等;

  • 利用已通过认证的用户权限购买商品;

  • 利用已通过认证的用户权限在评论区发表言论;


跨站点伪造请求的攻击案例


下面以一个网站的登录访问功能为例,讲解跨站点请求伪造,如下图所示:
20230701160321



  1. 当用户输入账号信息请求登录 A 网站;

  2. A 网站验证用户信息,通过验证后返回给用户一个 cookie;

  3. 在未退出网站 A 之前,在同一浏览器中请求了黑客构造的恶意网站 B;

  4. B 网站收到用户请求后返回攻击性代码,构造访问 A 网站的语句;

  5. 浏览器收到攻击性代码后,在用户不知情的情况下携带 cookie 信息请求了 A 网站。此时 A 网站不知道这是由 B 发起的。那么这时黑客就可以为所欲为了!!!


这首先必须瞒住两个条件:



  • 用户访问站点 A 并产生了 Cookie;

  • 用户没有退出 A 网站同时访问了 B 网站;


CSRF 攻击的防御


当涉及到跨站伪造请求的防御时,一下是一些防御方法和实践:




  1. 验证来源和引用检查:



    • 服务器端应该验证每个请求的来源 Referer 字段和源 Origin 字段以确保请求来自预期的域名或网站。如果来源不匹配,服务器应该拒绝请求。Referer 头部并不是 100% 可靠,因为某些浏览器或网络代理可能会禁用或篡改该字段。因此,Origin 字段被认为是更可靠的验证来源的方式;




  2. CSRF Token:



    CSRF 令牌是一个随机生成的值,嵌入到表单或请求参数中,与用户会话相关联。它的目的是验证请求的合法性,确保请求是由预期的用户发起的,而不是由攻击者伪造的。




    • 在每个表单和敏感操作的请求中,包括一个 CSRF 令牌;

    • 令牌可以作为隐藏字段 input type="hidden" 或请求头,例如 X-CSRF-Token 的一部分发送;

    • 在服务器端,验证请求中的令牌是否与用户会话中的令牌匹配,以确保请求的合法性;




  3. 验证请求的方法: 某些敏感操作应该使用 POSTPUTDELETE 等非幂等方法,而不是 GET 请求。这样可以防止攻击者通过构造图片或链接等 GET 请求来触发敏感操作;




  4. 敏感操作的二次确认: 对于一些敏感操作,例如修改密码、删除账户等,可以在用户执行操作前要求二次确认,以确保用户的意图和授权;




综合采取上述防御措施,可以有效减少跨站伪造请求攻击的风险。然而,没有单一的解决方案可以完全消除跨站伪造请求的威胁,因此建议在开发过程中将安全性作为一个关键考虑因素,并进行全面的安全测试和审查。


参考文献


书籍: 图解HTTP


总结


没有总结,总结个屁,不上网就是

作者:Moment
来源:juejin.cn/post/7251158799318057015
最安全的......

收起阅读 »

程序员的努力有意义吗?

最近,在小灰的知识星球上,有个小伙伴问了一个蛮有意思的问题: 这个问题看起来有些复杂,其实可以归纳为一句话: IT技术更新换代很快,如果我们花费很多年去学习技术,有一天旧技术被淘汰,新技术成为主流,那我们是不是就白学了?我们程序员的努力还有什么意义呢? 不得...
继续阅读 »

最近,在小灰的知识星球上,有个小伙伴问了一个蛮有意思的问题:



这个问题看起来有些复杂,其实可以归纳为一句话:


IT技术更新换代很快,如果我们花费很多年去学习技术,有一天旧技术被淘汰,新技术成为主流,那我们是不是就白学了?我们程序员的努力还有什么意义呢?


不得不说,这个问题困扰着很多程序员,小灰自己也常常在思考。


那么,程序员该不该努力钻研技术的?今天小灰来说一说自己的想法。


先说结论,程序员的努力当然是有效的。


无论是程序员,还是其他大多数凭本事吃饭的职业,个人的成就主要取决于四个因素:努力选择天赋运气。其中天赋和运气是不可控的,因此我们这里只谈论努力和选择。


那么,我们应该选择什么样的方向去努力钻研呢?


在职场上,我们需要掌握的技能有三类:一类是应用技能,一类是底层技能,一类是通用技能


应用技能,就是可以直接用来做事情赚钱的技能,比如Go语言、MySQL技术、Spring框架等等。掌握了这些技能,你可以开发项目,在近期为公司创造价值。但是,这些技术难免会有时效性,很可能过一段时间就不再流行。


举个例子,十几年前兴盛一时的Delphi语言,因为市场的转变,现在已经很少有人使用了。


底层技能,对于程序员行业来说,包括操作系统原理、算法与数据结构,设计模式、架构理论等等。这些技能在短时间内无法让你快速提高生产力,但是却可以让你在职业发展的中长期受益。而且,这些技能的有效期很长,在可见的未来,在程序员的各个细分领域里,一直都有用。


举个例子,无论你是做Java,做Python,做C++,亦或是做前端,算法和数据结构的理念都是互通的。


通用技能,包括沟通能力,情绪控制能力,团队管理能力等等。这些技能不仅对程序员有用,哪怕有一天你不当程序员了,甚至你退休以后,都能给你的工作和生活带来一定的好处。


比如,当你从程序员转行做了产品经理,沟通能力依然能派上用场;当你谈恋爱结婚,未来教育孩子的时候,情绪控制能力同样可以派上用场。


因此,我们在选择某一方向去努力的时候,切记不要一味追逐流行的新技术,那样只会让我们疲于奔命。我们需要在学习应用技能的同时,不断加深底层技能和通用技能的提升,为更远的将来打好基础,全方位进行提升。


当你长期坚持在正确的方向上努力,或许一年两年看不出效果,但经过五年十年,你和同行的能力差距会变得非常显著,而且不会因为技术的更新换代而改变。


比如说,你用Go语言工作了10年,有一天Go语言没人用了,Come语言成了主流语言。那你损失的只是这一项应用技能,而你这些年的底层技能、通用技能并没有白积累。


当你和职场新人一起学习Come语言的时候,你大概率比他学习得更快,因为编程技术之间多少会有一些相通性。


再加上你的算法和设计功底,良好的沟通和管理能力,你一定比新人更有价值。


不过话又说回来,随着人的年龄增长,你一定会有家庭的牵绊、体力的下降等问题,让你在职场上的竞争力有所下降。但这些和工作能力没有关系,并不在我们今天的讨论范围内。


程序员的努力有意义吗?


选择正确的方向,兼顾应用技能、底层技能、通用技能的提升,那我们的努力就必然是有意义的。


小灰的回答就到这里,如果这篇文章对你有所

作者:程序员小灰
来源:juejin.cn/post/7251501954157215800
帮助,欢迎点赞哦~~

收起阅读 »

API接口对于企业数字化转型有哪些重要意义?

当前,有很多企业都在加速进行数字化转型,随着AI等新技术的不断发展,企业的交付和数据管理能力已经无法满足需求。以数据为例,不少企业内部存在数据难题无法解决,一方面数据孤岛使得数据分散且多个业务系统之间难以打通,业务衔接不够顺畅,导致用户使用系统会更加复杂,数字...
继续阅读 »

当前,有很多企业都在加速进行数字化转型,随着AI等新技术的不断发展,企业的交付和数据管理能力已经无法满足需求。以数据为例,不少企业内部存在数据难题无法解决,一方面数据孤岛使得数据分散且多个业务系统之间难以打通,业务衔接不够顺畅,导致用户使用系统会更加复杂,数字化作用失效;另一方面数据共享开放的需求明显,但是数据安全无法保障,部分企业客户倾向于线下提供数据进行共享,会导致数据无法实时更新而且无法控制数据的流向。这两大问题既无法提高企业的业务效能,也影响到数据的安全。因此企业迫切需要进行数字化转型,才能跟上数字化浪潮的发展。

这其中最关键的方式就是使用API接口服务,那么API是如何赋能企业数字化转型的呢?


首先,不少企业内部都储存了海量的高价值数据,API能够帮助打破数据孤岛怪圈,让数据得到有效利用,开发人员能自由访问、组合数字资产,最终实现整体的协同效果,企业数据管理环境的复杂性得到了解决。
其次,API可以构造多个业务相关的接口服务与交付,企业的交付周期大大缩短,同时因为减少了代码量,开发效率得到有效提升,企业内部快速实现了降本增效。
最后,API开放平台能够实现IT资产和运维可视化以及IT资产的安全管控, 促进生态系统的形成,同时开发人员能够更方便地进行实验,创新并响应不断变化的客户需求。

数聚变平台打造了一个深耕新能源领域的API生态平台,目前已经覆盖了数据采集转发、数据集成共享、数据要素开放流通、企业数字化咨询和API全生命周期管理等多个功能模块,有效助力企业实现数字化转型。

收起阅读 »

Flutter如何实现IOC与AOP

在Flutter中实现IOC(Inversion of Control,控制反转)与AOP(Aspect-Oriented Programming,面向切面编程)之前,让我们先来了解一下这两个概念。 IOC(控制反转) 是一种设计原则,它将应用程序的控制权从应...
继续阅读 »

在Flutter中实现IOC(Inversion of Control,控制反转)与AOP(Aspect-Oriented Programming,面向切面编程)之前,让我们先来了解一下这两个概念。


IOC(控制反转) 是一种设计原则,它将应用程序的控制权从应用程序本身转移到外部框架或容器。传统上,应用程序会自己创建和管理对象之间的依赖关系。而在IOC中,对象的创建和管理被委托给一个专门的框架或容器。框架负责创建和注入对象,以实现松耦合和可扩展的架构。通过IOC,我们可以将应用程序的控制流程反转,从而实现更灵活、可测试和可维护的代码。


AOP(面向切面编程) 是一种编程范式,用于将横切关注点(如日志记录、事务管理、性能监控等)从应用程序的主要业务逻辑中分离出来。AOP通过在特定的切入点上织入额外的代码(称为切面),从而实现对这些关注点的统一管理。这种分离和集中的方式使得我们可以在不修改核心业务逻辑的情况下添加、移除或修改横切关注点的行为。


对于Java开发者来说,IOC和AOP可能已经很熟悉了,因为在Java开发中有许多成熟的框架,如Spring,提供了强大的IOC和AOP支持。


在Flutter中,尽管没有专门的IOC和AOP框架,但我们可以利用语言本身和一些设计模式来实现类似的功能。


接下来,我们可以探讨在Flutter中如何实现IOC和AOP的一些常见模式和技术。无论是依赖注入还是横切关注点的管理,我们可以使用一些设计模式和第三方库来实现类似的效果,以满足我们的开发需求


1. 控制反转(IOC):


依赖注入(Dependency Injection):依赖注入是一种将依赖关系从组件中解耦的方式,通过将依赖项注入到组件中,实现控制反转的效果。在Flutter中,你可以使用get_it库来实现依赖注入。下面是一个示例:


import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';

class UserService {
String getUser() => 'John Doe';
}

class GreetingService {
final UserService userService;

GreetingService(this.userService);

String greet() {
final user = userService.getUser();
return 'Hello, $user!';
}
}

void main() {
// 注册依赖关系
GetIt.instance.registerSingleton<UserService>(UserService());
GetIt.instance.registerSingleton<GreetingService>(
GreetingService(GetIt.instance<UserService>()),
);

runApp(MyApp());
}

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
final greetingService = GetIt.instance<GreetingService>();

return MaterialApp(
title: 'IOC Demo',
home: Scaffold(
appBar: AppBar(title: Text('IOC Demo')),
body: Center(child: Text(greetingService.greet())),
),
);
}
}


在上述示例中,我们定义了UserServiceGreetingService两个类。GreetingService依赖于UserService,我们通过依赖注入的方式将UserService注入到GreetingService中,并通过get_it库进行管理。


2. 面向切面编程(AOP):


在Flutter中,可以使用Dart语言提供的一些特性,如Mixin和装饰器(Decorator)来实现AOP。


Mixin:Mixin是一种通过将一组方法和属性混入到类中来实现代码复用的方式。下面是一个示例:


import 'package:flutter/material.dart';

mixin LogMixin<T extends StatefulWidget> on State<T> {
void log(String message) {
print('[LOG]: $message');
}
}

class LogButton extends StatefulWidget {
final VoidCallback onPressed;

const LogButton({required this.onPressed});

@override
_LogButtonState createState() => _LogButtonState();
}

class _LogButtonState extends State<LogButton> with LogMixin {
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
log('Button clicked');
widget.onPressed();
},
child: Text('Click Me'),
);
}
}

void main() {
runApp(MyApp());
}

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'AOP Demo',
home: Scaffold(
appBar: AppBar(title: Text('AOP Demo')),
body: Center(child: LogButton(onPressed: () => print('Button pressed'))),
),
);
}
}



在上面的示例中,我们定义了一个LogMixin,其中包含了一个log方法,用于记录日志。然后我们在_LogButtonState中使用with LogMixin将日志记录功能混入到_LogButtonState中。每次按钮被点击时,会先打印日志,然后调用传入的回调函数。


装饰器:装饰器是一种将额外行为添加到方法或类上的方式。下面是一个示例:


void logDecorator(Function function) {
print('[LOG]: Method called');
function();
}

@logDecorator
void greet() {
print('Hello, world!');
}

void main() {
greet();
}

在Flutter中,虽然没有专门的IOC(控制反转)和AOP(面向切面编程)框架,但我们可以利用一些设计模式和技术来实现类似的效果。


对于IOC,我们可以使用依赖注入(Dependency Injection)的方式实现。依赖注入通过将依赖项注入到组件中,实现了控制反转的效果。在Flutter中,可以借助第三方库如get_itkiwi来管理依赖关系,将对象的创建和管理交由依赖注入框架处理。


在AOP方面,我们可以使用Dart语言提供的Mixin和装饰器(Decorator)来实现类似的功能。Mixin是一种通过将一组方法和属性混入到类中的方式实现代码复用,而装饰器则可以在不修改被装饰对象的情况下,添加额外的行为或改变对象的行为。


通过使用Mixin和装饰器,我们可以在Flutter中实现横切关注点的管理,例如日志记录、性能监测和权限控制等。通过将装饰器应用于关键的方法或类,我们可以在应用程序中注入额外的功能,而无需直接修改原始代码。


需要注意的是,以上仅为一些示例,具体实现方式可能因项目需求和个人偏好而有所不同。在Flutter中,我们可以灵活运用设计模式、第三方库和语言特性,以实现IOC和AOP的效果,从而提升代码的可维护性、可扩展性和重用性。


总结而言,尽管Flutter没有专门的IOC和AOP框架,但我们可以借助依赖注入和装饰器等技术,结合常见的设计模式,构建灵活、可测试和可维护的应用程序。这些技术和模式为开发者提供了良好的开发体验和代码结构。


希望对您有所帮助谢谢!!

作者:北漂十三载
来源:juejin.cn/post/7251032736692600869

收起阅读 »

算法基础:归并排序

上一篇文章介绍了什么是分治思想,今天就来看一下它其中一个继承人-- 归并排序,本章主要介绍归并排序的原理,以及对一个实际问题进行编码。 学习的内容 1. 什么是归并排序 比如我们拿到一个数组,如果想使用归并排序,应该怎么做呢?首先我们将数组从中间切分,分成左...
继续阅读 »

上一篇文章介绍了什么是分治思想,今天就来看一下它其中一个继承人-- 归并排序,本章主要介绍归并排序的原理,以及对一个实际问题进行编码。


学习的内容




1. 什么是归并排序


比如我们拿到一个数组,如果想使用归并排序,应该怎么做呢?首先我们将数组从中间切分,分成左右两个部分,然后对左半部分和右半部分进行排序,两边部分又可以继续拆分,直至子数组中只剩下一个数据位置。


然后就要将拆分的子数组进行合并,合并的时候会涉及到两个数据进行比较,然后按照大小进行排序,以此往上进行合并。


拆分过程


image.png
合并过程


image.png


从上面我们可以看出,我们最终将大的数组拆分成只有单个数据的数组,然后进行合并,在合并过程中比较两个长度为1的数组,进行排序合并成新的子数组,然后依次类推,直至全部排序完成,也就意味着原数组排序完成。


2.代码示例


public class Solution {
   public static void main(String[] args) {
       int[] arr = {1,4,3,2,11};
       sortArray(arr);
       System.out.println(arr);
  }

   public static int[] sortArray(int[] nums) {
       quickSort(nums, 0, nums.length - 1);
       return nums;
  }

   private static void quickSort(int[] nums, int left, int right) {
       if (left >= right) {
           return;
      }
       int partitionIndex = getPartitionIndex(nums, left, right);
       quickSort(nums, left, partitionIndex - 1);
       quickSort(nums, partitionIndex + 1, right);
  }

   private static int getPartitionIndex(int[] nums, int left, int right) {
       int pivot = left;
       int index = pivot + 1;
       for (int i = index; i <= right; i++) {
           if (nums[i] < nums[pivot]) {
               swap(nums, i, index);
               index++;
          }
      }
       swap(nums, pivot, index - 1);
       return index - 1;
  }

   private static void swap(int[] nums, int i, int j) {
       int temp = nums[i];
       nums[i] = nums[j];
       nums[j] = temp;
  }
}




总结


本章简单分析了归并排序的原理以及分享了一个实际案例,无论是归并还是归并算法,对理解递归还是很有帮助的,之前总是靠着想递归流程,复杂点的绕着绕着就晕了,后面会再看一下快速排序,他和本文提到的归并排序都是分治思想,等说完快排,再

作者:花哥编程
来源:juejin.cn/post/7250404077712048165
一起对比两者的区别。

收起阅读 »

如果你的同事还不会配置commit提交规范,请把这篇文章甩给他

前言 首先问问大家在日常工作中喜欢哪种commit提交? git commit -m "代码更新" git commit -m "解决公共样式问题" git commit -m "feat: 新增微信自定义分享" 如果你是第三种,那我觉得你肯定了解过co...
继续阅读 »

前言


首先问问大家在日常工作中喜欢哪种commit提交?


git commit -m "代码更新"

git commit -m "解决公共样式问题"

git commit -m "feat: 新增微信自定义分享"

如果你是第三种,那我觉得你肯定了解过commit提交规范,可能是刷到过同类文章也可能是在工作中受到的要求


我自己是在刚出来实习的一家公司了解到的,依稀记得“冒号要用英文的,冒号后面要接空格...”


虽然我一直保持这种习惯去提交代码,但是后面遇到的同事大部分都是放飞自我的提交,看的我很难受


因此这篇文章就教还不会配置的小伙伴如何配置被业界广泛认可的 Angular commit message 规范以及呼吁大家去使用。


先来了解下commit message的构成


<type>(<scope>): <subject>
<BLANK LINE>
<body>
<BLANK LINE>
<footer>

对应的commit记录如下图


微信截图_20230608114515.png




  • type: 必填 commit 类型,有业内常用的字段,也可以根据需要自己定义



    • feat 增加新功能

    • fix 修复问题/BUG

    • style 代码风格相关无影响运行结果的

    • perf 优化/性能提升

    • refactor 重构

    • revert 撤销修改

    • test 测试相关

    • docs 文档/注释

    • chore 依赖更新/脚手架配置修改等

    • workflow 工作流改进

    • ci 持续集成

    • types 类型定义文件更改

    • wip 开发中

    • undef 不确定的分类




  • scope: commit 影响的范围, 比如某某组件、某某页面




  • subject: 必填 简短的概述提交的代码,建议符合 50/72 formatting




  • body: commit 具体修改内容, 可以分为多行, 建议符合 50/72 formatting




  • footer: 其他备注, 包括 breaking changes 和 issues 两部分




git cz使用


只需要输入 git cz ,就能为我们生成规范代码的提交信息。


一、安装工具


npm install -g commitizen // 系统将弹出上述type、scope等来填写
npm install -g cz-conventional-changelog // 用来规范提交信息

ps:如果你是拉取别人已经配置好git cz的项目,记得也要在自己环境安装


然后将cz-conventional-changelog添加到package.json中


commitizen init cz-conventional-changelog --save --save-exact

微信截图_20230608155514.png


二、使用git cz提交


安装完第一步的工具后,就可以使用git cz命令提交代码了


微信图片_20230608092741.png


微信图片_20230608092732.png


如图,输入完git cz命令后,系统将会弹出提交所需信息,只需要依次填写就可以


commitlint使用


如果你不想使用git cz命令去提交代码,还是习惯git commit的方式去提交


那么接下来就教大家怎么在git commit命令或者vscode工具中同样规范的提交代码


一、安装工具


npm install --save-dev husky
npm install --save-dev @commitlint/cli
npm install --save-dev @commitlint/config-conventional

二、配置



  • 初始化husky


npx husky install


  • 添加hooks


npx husky add .husky/commit-msg 'npx --no -- commitlint --edit \$1'


  • 在项目根目录下创建commitlint.config.js,并配置如下


module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-case': [2, 'always', ['lower-case', 'upper-case']],
'type-enum': [2, 'always',[
'feat', // 增加新功能
'fix', // 修复问题/BUG
'style', // 代码风格相关无影响运行结果的
'perf', // 优化/性能提升
'refactor', // 重构
'revert', // 撤销修改
'test', // 测试相关
'docs', // 文档/注释
'chore', // 依赖更新/脚手架配置修改等
'workflow', // 工作流改进
'ci', // 持续集成
'types', // 类型定义文件更改
'wip', // 开发中
'undef' // 不确定的分类
]
]
}
}

三、验证


没配置前能直接提交


微信图片_20230608092753.png


配置之后就会规范提交


微信图片_20230608092757.png


总结


以上两种方式都很简单,几个步骤下来就可以配置好,希望大家都能

作者:这货不是狮子
来源:juejin.cn/post/7243451555930898469
养成一个开发好习惯~

收起阅读 »

Android 内存治理之线程

1、 前言   当我们在应用程序中启动一个线程的时候,也是有可能发生OOM错误的。当我们看到以下log的时候,就说明系统分配线程栈失败了。 java.lang.OutOfMemoryError: pthread_create (1040KB stack) fa...
继续阅读 »

1、 前言


  当我们在应用程序中启动一个线程的时候,也是有可能发生OOM错误的。当我们看到以下log的时候,就说明系统分配线程栈失败了。


java.lang.OutOfMemoryError: pthread_create (1040KB stack) failed: Out of memory


这种情况可能是两种原因导致的。



  • 第一个就是系统的内存不足的时候,我们去启动一个线程。

  • 第二种就是进程内运行的线程总数超过了系统的限制。



  如果是内存不足的情况,需按照堆内存治理的方式来进行解决,检查应用内存泄漏问题并优化,此情况不作为本次讨论的重点。

  本次主要讨论进程内运行的线程总数超过了系统的限制所导致的情况。出现此情况时,我们就需要通过控制并发的线程总数来解决这个问题。


  想要控制并发的线程数。最直接的一种方式就是利用回收的思路,也就是让我们的线程通过串行的方式来执行;一个线程执行完毕之后,再启动下一个线程。这样就能够让并发的线程总数达到一个可控的状态。

  另外一种方式就是通过复用来解决,让同一个线程的实例可以被反复的利用,只创建较少的线程实例,就能完成大量的异步操作。


2、异步任务的方式对比


  对比一下,在安卓平台我们比较常用的开启异步任务的方式中,有哪些是更加有利于我们进行线程总数的控制的。


开启异步任务的方式特点
Thread.start()并行,难以管理
HandlerThread带消息循环的线程,线程内部串行任务(线程复用)
AsyncTask轻量级,串行(3.0以上),可以结合线程池使用
线程池可管理并发数,池化复用线程
Kotlin协程简化异步编程代码,复用线程,提高并发效率
##### 2.1 Thread

  从最简单的直接创建Thread的实例的方式来说起。在Java中这种方式虽然是最简单的去开启一个线程的方式,但是在实际开发中,一旦我们通过这种方式去自己创建 Thread 类的实例,并且调用 start 来开启一个线程的话,所开启的线程会非常的难以调度和管理。这种线程也就是我们平时所说的野线程。所以我们最好不要直接的创建thread类的实例。


2.2 HandlerThread

public class HandlerThread extends Thread { }

  HandlerThread是Thread类的子类,对Thread做了很多便利的封装。它有自己的Loop,它能够进行消息循环,所以就能够做到通过Handler执行异步任务,也能够做到在不同的线程之间,通过Handler进行现成的通讯。我们可以利用Handler的post操作,让我们在一个线程内部串行的执行多个异步任务。从内存的角度来说,也就相当于对线程进行了复用。


2.3 AsyncTask

  AsyncTask是一个相对更加轻量级,专门为了完成执行异步任务,然后返回UI线程更新UI的操作而设计的。对于我们来说,AsyncTask更像是一个任务的概念,而不是一个线程的概念。我们不需要把它当做一个线程去理解。 AsyncTask的本质,其实也是对线程和Handler的封装。



  • Android 1.6前,串行执行,原理:一个子线程进行任务的串行执行;

  • Android 1.6到2.3,并行执行,原理:一个线程数为5的线程池并行执行,但如果前五个任务执行时间过长,会堵塞后续任务执行,故不适合大量任务并发执行;

  • Android 3.0后,串行执行,原理:全局线程池进行串行处理任务;


到了Android 3.0以上版本,默认是串行执行的,但是可以结合线程值来实现有限制的并行。也可以达到一个限制线程总数的目的。


2.4 线程池

  Java语言本身也为我们提供了线程池。线程池的作用就是可以管理并发数,并且能够持续的去复用线程。如果在一个应用内部的全部异步操作,全部都采用线程池的方式来开启的话,那么我们就能够管理我们所有的异步任务了。这样一来,能够大大的降低线程治理的成本。


2.5 Kotlin协程

  在Kotlin中还引入了协程的概念。协程给传统的Java的异步编程带来最大的改变,就是能够让我们更加优雅的去实现异步任务。我们前面所说的这几种异步任务的执行方式,都需要我们额外的去写大量的样本代码。而Kotlin协程就能够做到让我们用写同步代码的方式去写异步代码。


  在语法的层面上,协程的另一个优势就是性能方面。协程能够帮助我们用更少的线程去执行更多的并发任务。同样也降低了我们治理内存的成本。从治理内存的角度来说,用线程池接管线程或者采用协程都是很好的方式。

作者:大神仙
来源:juejin.cn/post/7250357906712854589

收起阅读 »

uniapp开发项目——问题总结

前言 之前使用过uniapp开发微信小程序,但是没有遇到需要兼容H5页面的。因此在使用uniapp开发微信小程序和H5的过程中,遇到了好些问题。 1. button按钮存在黑色边框 使用button标签,在手机上查看存在黑色的边框,设置了border: non...
继续阅读 »

前言


之前使用过uniapp开发微信小程序,但是没有遇到需要兼容H5页面的。因此在使用uniapp开发微信小程序和H5的过程中,遇到了好些问题。


1. button按钮存在黑色边框


使用button标签,在手机上查看存在黑色的边框,设置了border: none;也没有效果。


原因:uniapp的button按钮使用了伪元素实现边框


解决方法: 设置button标签的伪元素为display:none或者boder:none;


button:after{
boder:none;
}

2. 配置反向代理,处理跨域


微信小程序没有跨域问题,如果当前小程序还没有配置服务器域名出现无法请求接口,只需要在微信开发工具勾选不校验合法域名,就可以请求到了


在本地开发环境中,H5页面在浏览器中调试,会出现跨域问题。如果后端不处理,前端就需要配置反向代理,处理跨域


a. 在manifest.json的源码视图中,找到h5的配置位置,配置proxy代理


image.png


注: "pathRewrite"是必要的,告诉连接要使用代理


b.在请求接口中使用


// '/api'就是manifest.json文件配置的devServer中的proxy
uni.request({
url: '/api'+ '接口url',
...
})

c. 配置完,需要重启项目


3. 使用uni.uploadFile()API,上传图片文件


在微信小程序使用该API上传图片没问题,但是在H5页面实现图片上传,后台始终不能获取到上传的文件。


一开始使用uni.chooseImage()API实现从本地相册选择图片或使用相机拍照,成功之后可以返回图片的本地文件路径列表(tempFilePaths)和图片的本地文件列表(tempFiles,每一项是一个 File 对象)


tempFilePaths 在微信小程序中得到临时路径图片,而在浏览器中得到 blob 路径图片。微信小程序使用uni.uploadFile()上传该临时路径图片,可以成功上传,但是H5无法成功(浏览器中的传值方式会显示为payload,不是文件流file)


image.png


f994e37fce7a5d62763f1c015b9553f.png



可能原因:



  1. 使用 uni.uploadFile() 上传 blob 文件给服务端,后端无法获取到后缀名,进而上传失败。


b. uni.uploadFile()上传的文件格式不正确



解决方法:


在H5中上传tempFiles文件,而不是tempFilePaths,并更改uni.uploadFile()上传的格式


H5


image.png


微信小程序


image.png


4. 打包H5


问题:打包出来,部署到线上,页面空白,控制台preview中展示please enable javascript tocontinue


原因:uniapp的打包配置存在问题


解决方法:


a. web配置不选择路由模式、运行的基础路径也不填写(一开始都写了)


image.png


b. "pathRewrite"设置为空(不知道为啥,可能是不需要配置代理了,网站和接口是同一域名)


"proxy" : {
"/api" : {
"target" : "xxx",
"changeOrigin" : true,
"secure" : true,
"pathRewrite" : {}
}
}



注: 之前接口中的'/api'也需要取消


作者:sherlockkid7
来源:juejin.cn/post/7250284959221809209

收起阅读 »

你的密码安全吗?这三种破解方法让你大开眼界!

密码破解,是黑客们最喜欢的玩具之一。当你用“123456”这类简单密码来保护你的账户时,就像裸奔一样,等待着黑客的攻击。所以,今天我们就来聊聊密码破解知识,看看那些常见的密码破解方法,以及如何防范它们。 1、暴力破解 首先,我们来介绍一下最简单、最暴力的密码破...
继续阅读 »

密码破解,是黑客们最喜欢的玩具之一。当你用“123456”这类简单密码来保护你的账户时,就像裸奔一样,等待着黑客的攻击。所以,今天我们就来聊聊密码破解知识,看看那些常见的密码破解方法,以及如何防范它们。


1、暴力破解


首先,我们来介绍一下最简单、最暴力的密码破解方法——暴力破解。


什么是暴力破解密码呢?


简单来说,就是 攻击者通过穷举所有可能的密码组合来尝试猜测用户的密码。如果你的密码太简单或者密码空间较小,那么暴力破解密码的成功几率会增加。


暴力破解密码的一般步骤:


step1:确定密码空间


密码空间是指所有可能的密码组合。密码空间的大小取决于密码的长度和使用的字符集。例如,对于一个只包含数字的4位密码,密码空间就是从0000到9999的所有组合。


step2:逐个尝试密码


攻击者使用自动化程序对密码空间中的每个密码进行逐个尝试。这可以通过编写脚本或使用专门的密码破解工具来实现。攻击者从密码空间中选择一个密码并将其用作尝试的密码。


step3:比对结果


对于每个尝试的密码,攻击者将其输入到目标账户进行验证。如果密码正确,那么攻击者成功破解了密码。否则,攻击者将继续尝试下一个密码,直到找到匹配的密码为止。


那么我们该如何防范暴力破解呢?


方案1:增强密码策略


增强密码策略,即选择强密码。强密码应该包括足够的长度、复杂的字符组合和随机性,以增加密码空间的大小,从而增加破解的难度。比如说,“K3v!n@1234”这样的密码就比“123456”要强得多。


方案2:登录尝试限制


限制登录尝试次数,例如设置最大尝试次数和锁定账户的时间。


方案3:双因素身份验证


我们还可以引入双因素身份验证,要求用户提供额外的验证信息,如验证码、指纹或硬件令牌等。


通过综合使用这些安全措施,我们可以大大减少暴力破解密码的成功几率,并提高账户和系统的安全性。


2、彩虹表攻击


接下来,我们来介绍一种更加高级、更加可怕的密码破解方法——彩虹表攻击。


彩虹表攻击是一种 基于预先计算出的哈希值与明文密码对应关系的攻击方式


攻击者通过预先计算出所有可能的哈希值与对应的明文密码,并将其存储在一个巨大的“彩虹表”中。


当需要破解某个哈希值时,攻击者只需要在彩虹表中查找对应的明文密码即可。


攻击者生成彩虹表时需要耗费大量的计算和存储资源,但一旦生成完成,后续的密码破解速度就会非常快。


彩虹表攻击的基本原理如下:


step1:生成彩虹表


攻击者事先生成一张巨大的彩虹表,其中包含了输入密码的哈希值和对应的原始密码。彩虹表由一系列链条组成,每个链条包含一个起始密码和相应的哈希值。生成彩虹表的过程是耗时的,但一旦生成完成,后续的破解过程会变得非常快速。


step2:寻找匹配


当攻击者获取到被保护的密码哈希值时,他们会在彩虹表中搜索匹配的哈希值。如果找到匹配,就意味着找到了原始密码。


step3:链表查找


如果在彩虹表中没有找到直接匹配的哈希值,攻击者将使用哈希值在彩虹表中进行一系列的链表查找。他们会在链表上依次应用一系列的哈希函数和反向函数,直到找到匹配的密码。


那如何防范呢?


方案1:盐值(Salt)


使用随机盐值对密码进行加密。盐值是一个随机的字符串,附加到密码上,使得每次生成的哈希值都不同。这样即使相同的密码使用不同的盐值生成哈希,也会得到不同的结果,使得彩虹表无效。


方案2:迭代哈希函数


多次迭代哈希函数是指对原始密码进行多次连续的哈希运算的过程。


通常情况下,单次哈希函数的计算速度是相当快的,但它可能容易受到彩虹表等预先计算的攻击。为了增加密码的破解难度,我们可以通过多次迭代哈希函数来加强密码的安全性。


在多次迭代哈希函数中,原始密码会被重复输入到哈希函数中进行计算。每次哈希运算的结果会作为下一次的输入,形成一个连续的链式计算过程。例如,假设初始密码为 "password",哈希函数为 SHA-256,我们可以进行如下的多次迭代哈希计算:



  1. 首先,将初始密码 "password" 输入 SHA-256 哈希函数中,得到哈希值 H1。

  2. 将哈希值 H1 再次输入 SHA-256 哈希函数中,得到哈希值 H2。

  3. 将哈希值 H2 再次输入 SHA-256 哈希函数中,得到哈希值 H3。

  4. 以此类推,进行多次迭代。


通过多次迭代哈希函数,我们可以增加密码破解的难度。攻击者需要对每一次迭代都进行大量的计算,从而大大增加了密码破解所需的时间和资源成本。同时,多次迭代哈希函数也提供了更高的密码强度,即使原始密码较为简单,其哈希值也会变得复杂和难以预测。


需要注意的是,多次迭代哈希函数的次数应根据具体的安全需求进行选择。次数过少可能仍然容易受到彩虹表攻击,而次数过多可能会对系统性能产生负面影响。因此,需要在安全性和性能之间进行权衡,并选择适当的迭代次数。


方案3:长度和复杂性要求


要求用户选择强密码,包括足够的长度、复杂的字符组合和随机性,以增加彩虹表的大小和密码破解的难度。


3、字典攻击


最后,我们来介绍一种基于字典的密码破解方法——字典攻击。


字典攻击是 通过使用一个包含常见单词和密码组合的字典文件来尝试破解密码(这文件就是我们常说的字典)。这种方法比暴力破解要高效得多,因为它可以根据常见密码和单词来进行尝试。


如果你使用了常见单词或者简单密码作为密码,那么字典攻击很有可能会成功。


以下是字典攻击的一般步骤:


step1:收集密码字典


攻击者会收集各种常见密码、常用字词、常见姓名、日期、数字序列等组成的密码字典。字典可以是公开的密码列表、泄露的密码数据库或通过爬取互联网等方式获得。


step2:构建哈希表


攻击者会对密码字典中的每个密码进行哈希运算,并将明文密码与对应的哈希值构建成一个哈希表,方便后续的比对操作。


step3:逐个比对


攻击者使用字典中的密码与目标账户的密码进行逐个比对。对于每个密码,攻击者将其进行哈希运算,并与目标账户存储的哈希值进行比较。如果找到匹配的哈希值,那么密码就被破解成功。


字典攻击的成功取决于密码的强度和字典的质量。


如果用户使用弱密码或常见密码,很容易受到字典攻击的威胁。为了抵御字典攻击,用户应该选择强密码,包括使用足够的长度、复杂的字符组合和随机性,以增加密码的猜测难度。而系统设计者可以使用前文介绍的方式来防止密码被破解,如密码加盐和限制登录尝试次数等。


好啦,今天的分享就到这里啦!希望大家都能保护好自己的账户安全,不要成

作者:陈有余Tech
来源:juejin.cn/post/7250866224429563941
为黑客攻击的目标哦!

收起阅读 »

为啥你的tree的checkbox隐藏的这么艰难

web
场景: 近期在实现一个基于element-ui 的 Tree 组件的场景, 产品要求, 部门的数据,都不要checkbox, 只有节点值为 员工 才显示,而且还要部分员工的checkbox 禁用 element-ui 的 tree 还不支持特定节点的check...
继续阅读 »

场景:


近期在实现一个基于element-ui 的 Tree 组件的场景, 产品要求, 部门的数据,都不要checkbox, 只有节点值为 员工 才显示,而且还要部分员工的checkbox 禁用


element-ui 的 tree 还不支持特定节点的checkbox隐藏功能, 网上大多采用 class 的方式,将第一层的checkbox进行了隐藏, 但是不满足我们的要求


规则:



  • 第一层节点不显示checkbox

  • 后续任意子节点,如果数据为部门 则也不显示 checkbox

  • 后端返回的部分数据,如果人员符合特定规则(根据自己场景来即可),则表现为 禁用 checkbox


实现


数据
treeData.js


export default [
{
"id":1,
"label":"一级 1-是部门",
"depType":1,
"disabled":false,
"children":[
{
"id":4,
"label":"二级 1-1-是部门",
"depType":1,
"disabled":false,
"children":[
{
"id":9,
"label":"三级 1-1-9",
"disabled":false
},
{
"id":25,
"label":"三级 1-1-25",
"disabled":false
},
{
"id":27,
"label":"三级 1-1-27",
"disabled":false
},
{
"id":30,
"label":"三级 1-30",
"disabled":false
},
{
"id":10,
"label":"三级 1-1-2 是部门",
"depType":5,
"disabled":false
}
]
}
]
},
{
"id":2,
"label":"一级 2 部门",
"depType":1,
"disabled":false,
"children":[
{
"id":5,
"label":"二级 2-1 张三",
"disabled":false
},
{
"id":6,
"label":"二级 2-2 李四",
"disabled":false
}
]
},
{
"id":3,
"label":"一级 3 部门",
"depType":1,
"disabled":false,
"children":[
{
"id":7,
"depType":1,
"label":"二级 3-1 王武",
"disabled":false
},
{
"id":8,
"label":"二级 3-2 赵柳",
"disabled":false
}
]
}
]

上述数据,有的有 deptType字段 ,有的节点没有, 这其实是业务场景的特殊规则,有deptType的认为这个节点为部门节点,没有的则为 员工


<template>
<div>
<el-tree
node-key="id"
show-checkbox
:data="treeData"
:render-content="renderContent"
class="tree-box"
@node-expand='onNodeExpand'
>
</el-tree>
<div>

<ul>
<li>一开始的数据结构必须都有 disabled字段, 默认不禁用,设置为 false 否则会出现视图的响应式延迟问题</li>
<li>是否禁用某个节点,根据renderContent 里面的规则来的, 规则是, 只要是部门的维度,就禁用 设置 data.disabled= true</li>
<li>tree的第一层节点隐藏,是通过js控制的</li>
</ul>
</div>
</div>

</template>

<script>
import treeData from './treeData.js'

export default {
name: 'render-content-tree',
data() {
return {
treeData
}
},
mounted() {
let nodes = document.querySelector('.tree-box')
let children = nodes.querySelectorAll('.el-tree-node')

for(let i=0; i< children.length; i++) {
children[i].querySelector('.el-checkbox').style.display = 'none'
}

// 第一层不要checkbox
// 后续根据规则来
},

methods: {
renderContent(h, { node, data, store }) {
// console.log(node, data)

// 如果不是一级节点,并且符合数据的特定要求,比如这里是 id 大于27 的数据,禁用掉
if (node.level !== 1 && data.id > 27) {
data.disabled = true
}

return h('div',
{
// 如果是部门,就将所有的 checkbox 都隐藏
class: data.depType === undefined ? '' : 'dept-node'
},
data.label)
},

setDeptNodeHide() {
let deptNodes = document.querySelectorAll('.dept-node')

for(let i=0; i<deptNodes.length; i++) {
let checkbox = deptNodes[i].parentNode.querySelector('.el-checkbox')

checkbox.style.display = 'none'
}
},

onNodeExpand(data, node, com) {
// console.log(data);
// console.log(node);
// console.log(com);

this.$nextTick(() => {
this.setDeptNodeHide()
})
}
}
}
</script>

image.png


节点初次渲染的效果.png




展开后的效果


image.png


部门节点没有checkbox, 符合特定规则的c

作者:知了清语
来源:juejin.cn/post/7250040492162433081
heckbox 禁用

收起阅读 »

Vue3 如何开发原生(安卓,ios)

Vue3 有没有一款好用的开发原生的工具 1.uniapp 我个人认为uniapp 适合开发小程序之类的,用这个去开发原生应用会存在一些问题 性能限制:由于 Uniapp 是通过中间层实现跨平台,应用在访问底层功能时可能存在性能损失。与原生开发相比,Uni...
继续阅读 »

Vue3 有没有一款好用的开发原生的工具


1.uniapp 我个人认为uniapp 适合开发小程序之类的,用这个去开发原生应用会存在一些问题




  • 性能限制:由于 Uniapp 是通过中间层实现跨平台,应用在访问底层功能时可能存在性能损失。与原生开发相比,Uniapp 在处理大规模数据、复杂动画和高性能要求的应用场景下可能表现较差。




  • 平台限制:不同平台有着各自的设计规范和特性,Uniapp 在跨平台时可能受到一些平台限制。有些平台特有的功能或界面设计可能无法完全实现,需要使用特定平台的原生开发方式来解决。




  • 生态系统成熟度: 相比于原生开发,Uniapp 的生态系统相对较新,支持和资源相对有限。在遇到问题时,可能难以找到完善的解决方案,开发者可能需要花费更多的时间和精力来解决问题。




  • 用户体验差异: 由于不同平台的设计规范和用户习惯不同,使用 Uniapp 开发的应用在不同平台上的用户体验可能存在差异。开发者需要针对每个平台进行特定的适配和调优,以提供更好的用户体验。




  • 功能支持限制: Uniapp 尽可能提供了跨平台的组件和 API,但某些特定平台的功能和接口可能无法完全支持。在需要使用特定平台功能的情况下,可能需要使用原生开发或自定义插件来解决。




  • uni 文档 uniapp.dcloud.net.cn/




2.react 拥有react native 开发原生应用 Vue无法使用 http://www.reactnative.cn/


3.Cordova cordova.apache.org/ 支持原生html js css 打包成 ios android exe dmg


4.ionic 我发现这个框架支持Vue3 angular react ts 构建Android iOS 桌面程序 这不正合我意 ionicframework.com/docs


前置条件


1.安装 java 环境 和 安卓编辑器sdk



安装完成检查环境变量


image.png


image.png


image.png


检查安卓编辑器的sdk 如果没安装就装一下


image.png


image.png


image.png


ionic


npm install -g @ionic/cli

初始化Vue3项目


安装完成后会有ionic 命令


ionic start [name] [template] [options]
# 名称 模板 类型为vue项目
ionic start app tabs --type vue

image.png


npm install #安装依赖

npm run dev 启动测试

image.png


启动完成后自带一个tabs demo


image.png


运行至android 编辑器 调试


npm run build
ionic capacitor copy android

注意检查


image.png


如果没有这个文件 删除android目录 重新执行下面命令


ionic capacitor copy android

预览


ionic capacitor open android

他会帮你打开安卓编辑器


如果报错说丢失sdk 注意检查sdk目录


image.png.


等待编译


image.png


点击上面绿色箭头运行


image.png


热更新


如果要热更新预览App 需要一个安卓设备


一直点击你的版本号就可以开启开发者模式


bd36c9f72990ae5cf2275e7690c7f354.jpg


开启usb调试 连接电脑


8f1085f12207c5107d39dd8d193dadfb.jpg


ionic capacitor run android -l --external

选择刚才的安卓设备


image.png


成功热更新


image.png


20c29c088e7f4f152fe1af0adbc4035f.jpg


作者:小满zs
来源:juejin.cn/post/7251113487317106745
收起阅读 »

从底层理解CAS原语

CAS
什么是硬件同步原语? 为什么硬件同步原语可以替代锁呢?要理解这个问题,你要首先知道硬件同步原语是什么。 硬件同步原语(Atomic Hardware Primitives)是由计算机硬件提供的一组原子操作,我们比较常用的原语主要是CAS和FAA这两种。 CAS...
继续阅读 »

什么是硬件同步原语?


为什么硬件同步原语可以替代锁呢?要理解这个问题,你要首先知道硬件同步原语是什么。


硬件同步原语(Atomic Hardware Primitives)是由计算机硬件提供的一组原子操作,我们比较常用的原语主要是CAS和FAA这两种。


CAS(Compare and Swap),它的字面意思是:先比较,再交换。我们看一下CAS实现的伪代码:

<< atomic >>
function cas(p : pointer to int, old : int, new : int) returns bool {
if *p ≠ old {
return false
}
*p ← new
return true
}


它的输入参数一共有三个,分别是:



  • p: 要修改的变量的指针。

  • old: 旧值。

  • new: 新值。


返回的是一个布尔值,标识是否赋值成功。


通过这个伪代码,你就可以看出CAS原语的逻辑,非常简单,就是先比较一下变量p当前的值是不是等于old,如果等于,那就把变量p赋值为new,并返回true,否则就不改变变量p,并返回false。


这是CAS这个原语的语义,接下来我们看一下FAA原语(Fetch and Add):

<< atomic >>
function faa(p : pointer to int, inc : int) returns int {
int value <- *location
*p <- value + inc
return value
}


FAA原语的语义是,先获取变量p当前的值value,然后给变量p增加inc,最后返回变量p之前的值value。


讲到这儿估计你会问,这两个原语到底有什么特殊的呢?


上面的这两段伪代码,如果我们用编程语言来实现,肯定是无法保证原子性的。而原语的特殊之处就是,它们都是由计算机硬件,具体说就是CPU提供的实现,可以保证操作的原子性。


我们知道, 原子操作具有不可分割性,也就不存在并发的问题。所以在某些情况下,原语可以用来替代锁,实现一些即安全又高效的并发操作。


CAS和FAA在各种编程语言中,都有相应的实现,可以来直接使用,无论你是使用哪种编程语言,它们底层的实现是一样的,效果也是一样的。


接下来,还是拿我们熟悉的账户服务来举例说明一下,看看如何使用CAS原语来替代锁,实现同样的安全性。


CAS版本的账户服务


假设我们有一个共享变量balance,它保存的是当前账户余额,然后我们模拟多个线程并发转账的情况,看一下如何使用CAS原语来保证数据的安全性。


这次我们使用Go语言来实现这个转账服务。先看一下使用锁实现的版本:

package main

import (
"fmt"
"sync"
)

func main() {
// 账户初始值为0元
var balance int32
balance = int32(0)
done := make(chan bool)
// 执行10000次转账,每次转入1元
count := 10000

var lock sync.Mutex

for i := 0; i < count; i++ {
// 这里模拟异步并发转账
go transfer(&balance, 1, done, &lock)
}
// 等待所有转账都完成
for i := 0; i < count; i++ {
<-done
}
// 打印账户余额
fmt.Printf("balance = %d \n", balance)
}
// 转账服务
func transfer(balance *int32, amount int, done chan bool, lock *sync.Mutex) {
lock.Lock()
*balance = *balance + int32(amount)
lock.Unlock()
done <- true
}


这个例子中,我们让账户的初始值为0,然后启动多个协程来并发执行10000次转账,每次往账户中转入1元,全部转账执行完成后,账户中的余额应该正好是10000元。


如果你没接触过Go语言,不了解协程也没关系,你可以简单地把它理解为进程或者线程都可以,这里我们只是希望能异步并发执行转账,我们并不关心这几种“程”他们之间细微的差别。


这个使用锁的版本,反复多次执行,每次balance的结果都正好是10000,那这段代码的安全性是没问题的。接下来我们看一下,使用CAS原语的版本。

func transferCas(balance *int32, amount int, done chan bool) {
for {
old := atomic.LoadInt32(balance)
new := old + int32(amount)
if atomic.CompareAndSwapInt32(balance, old, new) {
break
}
}
done <- true
}


这个CAS版本的转账服务和上面使用锁的版本,程序的总体结构是一样的,主要的区别就在于,“异步给账户余额+1”这一小块儿代码的实现。


那在使用锁的版本中,需要先获取锁,然后变更账户的值,最后释放锁,完成一次转账。我们可以看一下使用CAS原语的实现:


首先,它用for来做了一个没有退出条件的循环。在这个循环的内部,反复地调用CAS原语,来尝试给账户的余额+1。先取得账户当前的余额,暂时存放在变量old中,再计算转账之后的余额,保存在变量new中,然后调用CAS原语来尝试给变量balance赋值。我们刚刚讲过,CAS原语它的赋值操作是有前置条件的,只有变量balance的值等于old时,才会将balance赋值为new。


我们在for循环中执行了3条语句,在并发的环境中执行,这里面会有两种可能情况:


一种情况是,执行到第3条CAS原语时,没有其他线程同时改变了账户余额,那我们是可以安全变更账户余额的,这个时候执行CAS的返回值一定是true,转账成功,就可以退出循环了。并且,CAS这一条语句,它是一个原子操作,赋值的安全性是可以保证的。


另外一种情况,那就是在这个过程中,有其他线程改变了账户余额,这个时候是无法保证数据安全的,不能再进行赋值。执行CAS原语时,由于无法通过比较的步骤,所以不会执行赋值操作。本次尝试转账失败,当前线程并没有对账户余额做任何变更。由于返回值为false,不会退出循环,所以会继续重试,直到转账成功退出循环。


这样,每一次转账操作,都可以通过若干次重试,在保证安全性的前提下,完成并发转账操作。


其实,对于这个例子,还有更简单、性能更好的方式:那就是,直接使用FAA原语。

func transferFaa(balance *int32, amount int, done chan bool) {
atomic.AddInt32(balance, int32(amount))
done <- true
}


FAA原语它的操作是,获取变量当前的值,然后把它做一个加法,并且保证这个操作的原子性,一行代码就可以搞定了。看到这儿,你可能会想,那CAS原语还有什么意义呢?


在这个例子里面,肯定是使用FAA原语更合适,但是我们上面介绍的,使用CAS原语的方法,它的适用范围更加广泛一些。类似于这样的逻辑:先读取数据,做计算,然后更新数据,无论这个计算是什么样的,都可以使用CAS原语来保护数据安全,但是FAA原语,这个计算的逻辑只能局限于简单的加减法。所以,我们上面讲的这种使用CAS原语的方法并不是没有意义的。


另外,你需要知道的是,这种使用CAS原语反复重试赋值的方法,它是比较耗费CPU资源的,因为在for循环中,如果赋值不成功,是会立即进入下一次循环没有等待的。如果线程之间的碰撞非常频繁,经常性的反复重试,这个重试的线程会占用大量的CPU时间,随之系统的整体性能就会下降。


缓解这个问题的一个方法是使用Yield(), 大部分编程语言都支持Yield()这个系统调用,Yield()的作用是,告诉操作系统,让出当前线程占用的CPU给其他线程使用。每次循环结束前调用一下Yield()方法,可以在一定程度上减少CPU的使用率,缓解这个问题。你也可以在每次循环结束之后,Sleep()一小段时间,但是这样做的代价是,性能会严重下降。


所以,这种方法它只适合于线程之间碰撞不太频繁,也就是说绝大部分情况下,执行CAS原语不需要重试这样的场景。


总结


本文讲述了CAS和FAA这两个原语。这些原语,是由CPU提供的原子操作,在并发环境中,单独使用这些原语不用担心数据安全问题。在特定的场景中,CAS原语可以替代锁,在保证安全性的同时,提供比锁更好的性能。


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

一文看懂什么是fork/join

什么是Fork/Join Fork/Join 是JUC并发包下的一个并行处理框架,实现了ExecutorService接口的多线程处理器,它专为那些可以通过递归分解成更细小的任务而设计,最大化的利用多核处理器来提高应用程序的性能。 Fork/Join的运行流程...
继续阅读 »

什么是Fork/Join


Fork/Join 是JUC并发包下的一个并行处理框架,实现了ExecutorService接口的多线程处理器,它专为那些可以通过递归分解成更细小的任务而设计,最大化的利用多核处理器来提高应用程序的性能。


Fork/Join的运行流程大致如下所示:


需要注意的是,图里的次级子任务可以一直分下去,一直分到子任务足够小为止,这里体现了分而治之(divide and conquer) 的算法思想。


工作窃取算法


工作窃取算法指的是在多线程执行不同任务队列的过程中,某个线程执行完自己队列的任务后从其他线程的任务队列里窃取任务来执行。


工作窃取流程如下图所示:


值得注意的是,当一个线程窃取另一个线程的时候,为了减少两个任务线程之间的竞争,我们通常使用双端队列来存储任务。被窃取的任务线程都从双端队列的头部拿任务执行,而窃取其他任务的线程从双端队列的尾部执行任务。


另外,当一个线程在窃取任务时要是没有其他可用的任务了,这个线程会进入阻塞状态以等待再次“工作”。


Fork/Join 实践


前面说Fork/Join框架简单来讲就是对任务的分割与子任务的合并,所以要实现这个框架,先得有任务。在Fork/Join框架里提供了抽象类ForkJoinTask来实现任务。


ForkJoinTask


ForkJoinTask是一个类似普通线程的实体,但是比普通线程轻量得多。


fork()方法:使用线程池中的空闲线程异步提交任务

public final ForkJoinTask<V> fork() {
Thread t;
// ForkJoinWorkerThread是执行ForkJoinTask的专有线程,由ForkJoinPool管理
// 先判断当前线程是否是ForkJoin专有线程,如果是,则将任务push到当前线程所负责的队列里去
if ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread)
((ForkJoinWorkerThread)t).workQueue.push(this);
else
// 如果不是则将线程加入队列
// 没有显式创建ForkJoinPool的时候走这里,提交任务到默认的common线程池中
ForkJoinPool.common.externalPush(this);
return this;
}

其实fork()只做了一件事,那就是把任务推入当前工作线程的工作队列里。


join()方法:等待处理任务的线程处理完毕,获得返回值。


我们看下join()的源码:

public final V join() {
int s;
// doJoin()方法来获取当前任务的执行状态
if ((s = doJoin() & DONE_MASK) != NORMAL)
// 任务异常,抛出异常
reportException(s);
// 任务正常完成,获取返回值
return getRawResult();
}

/**
* doJoin()方法用来返回当前任务的执行状态
**/
private int doJoin() {
int s; Thread t; ForkJoinWorkerThread wt; ForkJoinPool.WorkQueue w;
// 先判断任务是否执行完毕,执行完毕直接返回结果(执行状态)
return (s = status) < 0 ? s :
// 如果没有执行完毕,先判断是否是ForkJoinWorkThread线程
((t = Thread.currentThread()) instanceof ForkJoinWorkerThread) ?
// 如果是,先判断任务是否处于工作队列顶端(意味着下一个就执行它)
// tryUnpush()方法判断任务是否处于当前工作队列顶端,是返回true
// doExec()方法执行任务
(w = (wt = (ForkJoinWorkerThread)t).workQueue).
// 如果是处于顶端并且任务执行完毕,返回结果
tryUnpush(this) && (s = doExec()) < 0 ? s :
// 如果不在顶端或者在顶端却没未执行完毕,那就调用awitJoin()执行任务
// awaitJoin():使用自旋使任务执行完成,返回结果
wt.pool.awaitJoin(w, this, 0L) :
// 如果不是ForkJoinWorkThread线程,执行externalAwaitDone()返回任务结果
externalAwaitDone();
}

我们在之前介绍过说Thread.join()会使线程阻塞,而ForkJoinPool.join()会使线程免于阻塞,下面是ForkJoinPool.join()的流程图:


RecursiveAction和RecursiveTask


通常情况下,在创建任务的时候我们一般不直接继承ForkJoinTask,而是继承它的子类RecursiveAction和RecursiveTask。


两个都是ForkJoinTask的子类,RecursiveAction可以看做是无返回值的ForkJoinTask,RecursiveTask是有返回值的ForkJoinTask。


此外,两个子类都有执行主要计算的方法compute(),当然,RecursiveAction的compute()返回void,RecursiveTask的compute()有具体的返回值。


ForkJoinPool


ForkJoinPool是用于执行ForkJoinTask任务的执行(线程)池。


ForkJoinPool管理着执行池中的线程和任务队列,此外,执行池是否还接受任务,显示线程的运行状态也是在这里处理。


我们来大致看下ForkJoinPool的源码:

@sun.misc.Contended
public class ForkJoinPool extends AbstractExecutorService {
// 任务队列
volatile WorkQueue[] workQueues;

// 线程的运行状态
volatile int runState;

// 创建ForkJoinWorkerThread的默认工厂,可以通过构造函数重写
public static final ForkJoinWorkerThreadFactory defaultForkJoinWorkerThreadFactory;

// 公用的线程池,其运行状态不受shutdown()和shutdownNow()的影响
static final ForkJoinPool common;

// 私有构造方法,没有任何安全检查和参数校验,由makeCommonPool直接调用
// 其他构造方法都是源自于此方法
// parallelism: 并行度,
// 默认调用java.lang.Runtime.availableProcessors() 方法返回可用处理器的数量
private ForkJoinPool(int parallelism,
ForkJoinWorkerThreadFactory factory, // 工作线程工厂
UncaughtExceptionHandler handler, // 拒绝任务的handler
int mode, // 同步模式
String workerNamePrefix) { // 线程名prefix
this.workerNamePrefix = workerNamePrefix;
this.factory = factory;
this.ueh = handler;
this.config = (parallelism & SMASK) | mode;
long np = (long)(-parallelism); // offset ctl counts
this.ctl = ((np << AC_SHIFT) & AC_MASK) | ((np << TC_SHIFT) & TC_MASK);
}

}

WorkQueue
双端队列,ForkJoinTask存放在这里。


runState
ForkJoinPool的运行状态。SHUTDOWN状态用负数表示,其他用2的幂次表示。


当工作线程在处理自己的工作队列时,会从队列首取任务来执行(FIFO);如果是窃取其他队列的任务时,窃取的任务位于所属任务队列的队尾(LIFO)。


ForkJoinPool与传统线程池最显著的区别就是它维护了一个工作队列数组(volatile WorkQueue[] workQueues,ForkJoinPool中的每个工作线程都维护着一个工作队列)。


Fork/Join的使用


上面我们说ForkJoinPool负责管理线程和任务,ForkJoinTask实现fork和join操作,所以要使用Fork/Join框架就离不开这两个类了,只是在实际开发中我们常用ForkJoinTask的子类RecursiveTask 和RecursiveAction来替代ForkJoinTask。


下面我们用一个计算斐波那契数列第n项的例子来看一下Fork/Join的使用:


斐波那契数列数列是一个线性递推数列,从第三项开始,每一项的值都等于前两项之和:


1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89······


如果设f(n)为该数列的第n项(n∈N*),那么有:f(n) = f(n-1) + f(n-2)。

public class FibonacciTest {

class Fibonacci extends RecursiveTask<Integer> {

int n;

public Fibonacci(int n) {
this.n = n;
}

// 主要的实现逻辑都在compute()里
@Override
protected Integer compute() {
// 这里先假设 n >= 0
if (n <= 1) {
return n;
} else {
// f(n-1)
Fibonacci f1 = new Fibonacci(n - 1);
f1.fork();
// f(n-2)
Fibonacci f2 = new Fibonacci(n - 2);
f2.fork();
// f(n) = f(n-1) + f(n-2)
return f1.join() + f2.join();
}
}
}

@Test
public void testFib() throws ExecutionException, InterruptedException {
ForkJoinPool forkJoinPool = new ForkJoinPool();
System.out.println("CPU核数:" + Runtime.getRuntime().availableProcessors());
long start = System.currentTimeMillis();
Fibonacci fibonacci = new Fibonacci(40);
Future<Integer> future = forkJoinPool.submit(fibonacci);
System.out.println(future.get());
long end = System.currentTimeMillis();
System.out.println(String.format("耗时:%d millis", end - start));
}
}

上面例子的输出:



  • CPU核数:4

  • 计算结果:102334155

  • 耗时:9490 ms

  • 需要注意的是,上述计算时间复杂度为O(2^n),随着n的增长计算效率会越来越低,这也是上面的例子中n不敢取太大的原因。


总结


并不是所有的任务都适合Fork/Join框架,比如上面的例子任务划分过于细小反而体现不出效率。因为Fork/Join是使用多个线程协作来计算的,所以会有线程通信和线程切换的开销。


如果要计算的任务比较简单,直接使用单线程会更快一些。但如果要计算的东西比较复杂,计算机又是多核的情况下,就可以充分利用多核CPU来提高计算速度。


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

Kotlin的一些细节与技巧

kotlin在使用过程中有些有着一些小细节需要注意,搞清楚这些能够更加高效的使用Kotlin,下面来介绍一下。 查看字节码 kotlin本质上在编译之后还是会跟java一样生成字节码,as的工具自带查看字节码工具,能让我们看到一些舒服的kotlin操作背后的秘...
继续阅读 »

kotlin在使用过程中有些有着一些小细节需要注意,搞清楚这些能够更加高效的使用Kotlin,下面来介绍一下。


查看字节码


kotlin本质上在编译之后还是会跟java一样生成字节码,as的工具自带查看字节码工具,能让我们看到一些舒服的kotlin操作背后的秘密 image.png 点击生成文件的Decompile 能看到kotlin文件从字节码到java代码后的结果,不过可读性并不是很好 image.png


扩展方法的小坑


Kotlin提供了扩展方法和扩展属性,能够对一些我们无法修改源码的类,增加一些额外的方法和属性 一个很简单的例子,String是JDK提供的类,我们没有办法直接修改它的源码,但是我们又经常会做一些判空、判断长度的操作,在以往使用Java的时候,我们会使用TextUtils.isEmpty来判断,但是有了Kotlin之后,我们可以像下面这样,给String定义一个扩展方法,之后在方法体中,使用this就可以方法到当前的String对象,从而实现**「看起来」**为这个类新增了一个方法的效果,如下所示

fun main() {
    "".isEmpty()
}

fun String.isEmpty(): Boolean {
    return this.length > 0
}

这实际上是Kotlin编译器的魔法,最终它在调用时还是以一个方法的形式,「所以扩展方法并没有真正的为这个类增加新的方法」,而只是让你在写代码时可以像调用方法一样调用工具类,来增加代码的可读性,看下它的字节码 image.png 了解这一原理之后,我们就可以理解在一些特殊case下,Kotlin的扩展为什么表现的有点不符合预期,



  • 扩展类中一样签名的方法,将无效
class People {
    fun run() = println("people run")
}

fun People.run() = println("extend run")

fun main() {
    val people = People()

    people.run()
}
//people run

因为从底层来看,类People自己的方法和扩展方法,方法签名是一样的,Kotlin编译器发现本身有这个方法了,就不会再给你做扩展方法的调用



  • 扩展方法跟随声明时候类型
open class Fruit {
}

class Apple : Fruit() {

}


fun Fruit.printSelf() = println("Fruit")

fun Apple.printSelf() = println("Apple")

fun main() {
    val fruit = A()
    fruit.printSelf()
 //注意这里
    val apple1: Fruit = Apple()
    apple1.printSelf()

    val apple2 = Apple()
    apple2.printSelf()
}
// 输出结果是
// Fruit
// Fruit
// Apple

但是第二个的输出结果却是Fruit,把apple的类型声明成了Fruit,虽然它是一个Apple的实例,但Kotlin编译器又不知道你运行时到底是什么,你声明是Fruit,就给你调用Fruit的扩展方法。


inline来帮你性能优化


在高阶函数在调用时总会创建新的Function对象,当被频繁调用,那么就会有大量的对象被创建,除此之外还可能会有基础类型的装箱拆箱问题,不可避免的就会导致性能问题,为此,「Kotlin为我们提供了inline关键字」。 inline的作用**,内联**,通过inline,我们可以把**「函数调用替换到实际的调用处」**,从而避免Function对象的创建,进一步避免性能损耗,看下代码以及 image.png main方法的调用不再直接调用foo函数,而是把foo函数的函数体直接拷贝了过来进行调用, 不过也不能滥用inline,因为inline是在编译时进行代码的替换,那么就意味着你inline的函数体里的代码,会被替换到每一个调用的地方,从而导致字节码的膨胀,如果对产物对产物大小有严格的要求,需要关注下这个副作用。


借助reified来实现真泛型


在java中我们都知道由于编译时的类型擦除,JVM的泛型其实都是假泛型,如下的代码在编译时往往会报错

fun <T> foo() {
    println(T::class.java) // 会报错
}

但是Kotlin为我们提供了**「reified关键字」,通过这个关键字,我们就可以让上面的代码成功编译并且运行,不过还需要「搭配inline关键字」**

inline fun <reified T> fooReal() {
    println(T::class.java)
}

由于inline会把函数体替换到调用处,调用处的泛型类型一定是确定的,那么就可以直接把泛型参数进行替换,从而达成了「真泛型」的效果,比如使用上面的fooReal

fooReal<String>()
//调用它的打印方法时 替换为String类型
println(String::class.java)

Lateinit 和 by lazy的使用场景


这两个经常会被使用到用来实现变量的延迟初始化,不过二者还是有些区别的



  • lateinit


在声明变量时不知道它的初始值是多少,依赖后续的流程来赋值,可以节省变量判空带来的便利。不过需要确保后续是会对其赋值的,不然会有异常出现



  • lazy


**「一个对象的创建需要消耗大量的资源,而我不知道它到底会不会被用到」**的场景,并且只有在第一次被调用的时候才会去赋值。

fun main() {
    val lazyTest by lazy {
        println("create lazyTest instance")
    }

    println("before create")
    val value = lazyTest
    println("after create")
}
// before create
// create lazyTest instance
// after create

Sequence来提高性能


Kotlin为我们提供了大量的集合操作函数来简化对集合的操作,比如filter、map等,但是这些操作符往往**「伴随着性能的损耗」**,比如如下代码

fun main() {
    val list = (1..20).toList()

    val result = list.filter {
        print("$it ")
        it % 2 == 0
    }.also {
        println()
    }.filter {
        print("$it ")
        it % 5 == 0
    }
    println()
    println(result.toString())
}
// 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 
// 2 4 6 8 10 12 14 16 18 20 
// [10, 20]

可以看出,我们定义了一个1~20的集合,然后通过两次调用**「filter」**函数,来先筛选出集合中的偶数,再筛选出集合中的5的倍数,最后得到结果10和20,让我们看下这个舒服的fliter操作符的实现

public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> {
    return filterTo(ArrayList<T>(), predicate)
}

可以看到,每次filter操作都会创建一个新的集合对象,如果你的操作次数很多并且你的集合对象很大,那么就会有额外的性能开销 「如果你对集合的操作次数比较多的话,这时候就需要Sequence来优化性能」

fun main() {
    val list = (1..20).toList()

    val sequenceResult = list.asSequence()
        .filter {
            print("$it ")
            it % 2 == 0
        }.filter {
            print("$it ")
            it % 5 == 0
        }
    
    val iterator = sequenceResult.iterator()
    iterator.forEach {
        print("result : $it ")
    }
}

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

对于Sequence,由于它的计算是惰性的,在调用filter的时候,并不会立即计算,只有在调用它的iterator的next方法的时候才会进行计算,并且它并不会像List的filter一样计算完一个函数的结果之后才会去计算下一个函数的结果,「而是对于一个元素,用它直接去走完所有的计算」。 在上面的例子中,对于1,它走到第一个filter里面,不满足条件,直接就结束了,而对于5,它走到第一个filter里面,符合条件,这个时候会继续拿它去走第二个filter,不符合条件,就返回了,对于10,它走到第一个filter里面,符合条件,这个时候会继续拿它去走第二个filter,依然符合条件,最终就被输出了出来


Unit与void的区别


在Kotlin中,如果一个方法没有声明返回类型,那么它的返回类型会被默认设置为**「Unit」,但是「Unit并不等同于Java中的void」**关键字,void代表没有返回值,而Unit是有返回值的,如下

fun main() {
    val foo = foo()
    println(foo.javaClass)
}

fun foo() {

}

// 输出结果:class kotlin.Unit

继续跟进下看看Unit的实现

public object Unit {
    override fun toString() = "kotlin.Unit"
}

在Kotlin中是函数作为一等公民,而不是对象。这一个特性就决定了它可以使用函数进行传递和返回。因此,Kotlin中的高阶函数应用就很广。高阶函数至少就需要一个函数作为参数,或者返回一个函数。如果我们没有在明明函数声明中明确的指定返回类型,或者没有在Lambda函数中明确返回任何内容,它就会返回Unit。 比如 如下实现实际是相同的

fun funcionNoReturnAnything(){
 
}
fun funcionNoReturnAnything():Unit{
 
}

或者是在lambda函数体中最后一个值会作为返回值返回,如果没有明确返回,就会默认返回Unit

view.setOnclickListener{
 
}
view.setOnclickListener{
 Unit 
}

Kotlin的包装类型


kotlin是字节码层面跟java是一样的,但是java中在基础类型有着 **「原始类型和包装类型」**的区别,比如int和Integer,但是在kotlin中我们只有Int这一种类型,那么kotlin编译器是如何做到区分的呢?先看一段kotlin代码以及反编译java之后的代码 image.png 可以看出



  • 对于不可空的基础类型,Kotlin编译器会自动为我们选择使用原始类型,而对于可空的基础类型,Kotlin编译器则会选择使用包装类型

  • 对于集合这种只能传包装类的情况,不论你是传可空还是不可空,都会选择使用包装类型


老生常谈run、let、also、with


run、let、apply、also、with都是Kotlin官方为我们提供的高阶函数,通常对比着4个操作符,



  1. 差异


我们关注receiver、argument、return之间的差异,如图所示 image.png



  1. 场景


image.png 简而言之



  • **「run」**适用于在顶层进行初始化时使用

  • **「let」**在被可空对象调用时,适用于做null值的检查,let在被非空对象调用时,适用于做对象的映射计算,比如说从一个对象获取信息,之后对另一个对象进行初始化和设置最后返回新的对象

  • **「apply」**适用于做对象初始化之后的配置

  • **「also」**适用于与程序本身逻辑无关的副作用,比如说打印日志等


==和===


在Java中我们一般使用==来判断两个对象的引用是否相等,使用equals方法来判断两个**「对象值」**是否相等 但是在Kotlin中,==和equals是相等的用来判断值,使用===来判断两个对象的引用是否相等


高阶函数


kotlin中一等公民是函数,函数也可以作为另一个函数的入参或者返回值,这就是高阶函数。 不过JVM本身是没有函数类型的,那Kotlin是如何实现这种效果的呢?先看段kotlin代码以及反编译了java的代码,一切就一目了然 image.png 我们可以看到,最终foo方法传入的类型是一个Function0类型,然后调用了Function0的invoke方法,继续看下Function0类型

public interface Function0<out R> : Function<R> {
    /** Invokes the function. */
    public operator fun invoke(): R
}

看来魔法就在这里 也就是说如下的两种写法也是等价的

//kotlin
fun main() {
    foo {
        println("foo")
    }
}
//java
public static void main(String[] args) {
    foo(new Function0<Unit>() {
        @Override
        public Unit invoke() {
            System.out.println("foo");
            return Unit.INSTANCE;
        }
    });
}

到这里是不是对高阶函数有着更深刻的认识了呢 Kotlin的高阶函数本质上是通过对函数的抽象,然后在运行时通过创建Function对象来实现的。


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

Android单元测试

本文主要内容 1、单元测试介绍 2、java单元测试 3、android单元测试 4、常用方法介绍 1、单元测试介绍 单元测试,是指对软件中的最小可测试单元进行检查和验证。 在Java中,最小单元可以是类也可以是方法,比如刚刚开发完成一个下载的方法,此时可...
继续阅读 »

本文主要内容



  • 1、单元测试介绍

  • 2、java单元测试

  • 3、android单元测试

  • 4、常用方法介绍


1、单元测试介绍


单元测试,是指对软件中的最小可测试单元进行检查和验证。


在Java中,最小单元可以是类也可以是方法,比如刚刚开发完成一个下载的方法,此时可以用单元测试其是否ok。如果不用单元测试,用手写代码调用的方式,则工作量会较大。


使用Android studio进行单元测试,一共有两种类型,一种就是普通的java单元测试,另一种就是android单元测试,android单元测试包括对ui测试,activity的相关方法进行测试等等,需要context参数


image.png


进行单元测试需要引入对应的依赖。

testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
androidTestImplementation 'com.android.support.test:rules:1.0.2'

前面3个依赖包,在创建工程的时候会默认加进来,最后一个貌似不会默认添加,需要手动添加。最后一个依赖包与activity相关的单元测试有关。


2、java单元测试


以一个最简单的例子,计算器为例:

public class Util {

public static int add(int a, int b){
return a + b;
}

public int addInt(int a, int b){
return a + b;
}
}

Util类中有一个静态方法,一个非静态方法,都是简单的相加逻辑。接下来,可以右键选中方法,然后点击goto选项,生成对应的单元测试文件。





最后一步中可以选择为当前类中的哪些方法添加单元测试,也可以勾选before和after两个选项,顾名思义,before和after方法分别在单元测试前后调用,我们可以在这两个方法中做一些事情,例如初始化、回收等等。

public class UtilTest {

Util util;

@Before
public void setUp() throws Exception {
util = new Util();
System.out.println("sutup");
}

@After
public void tearDown() throws Exception {
System.out.println("tearDown");
}

@Test
public void add() {
assertEquals(2,Util.add(1, 1));
}

@Test
public void addInt() {
assertEquals(2, util.addInt(1,1));
}
}

Util类中,写了一个静态方法和非静态方法,其实就是为了演示 setUp 方法的作用,如果在单元测试中需要初始化一些类,则可以在 setUp 中初始化,在测试方法中使用已经初始化过的实例即可。


Java单元测试运行依赖于 JVM,执行单元测试方法非常简单,右键单元测试文件执行即可,也可以选择某个方法,只执行这一个方法。


3、android单元测试


Android单元测试,它依赖于Android的执行环境,也就是需要在android机器上运行。与java单元测试相比,它有一点点的不同。


前一章中讲过java单元测试,提到了 before 和 after 这两个选项,有点类似于切面编程,可以在其中做一些初始化的动作。但android中最常用的是activity,如何在activity中也添加一些周期回调函数呢?

@Rule
public ActivityTestRule rule = new ActivityTestRule(MainActivity.class){
@Override
protected Intent getActivityIntent() {
Intent intent = new Intent();
intent.putExtra("data","world");
return intent;
}

@Override
protected void beforeActivityLaunched() {
super.beforeActivityLaunched();
Log.i("okunu","before");
}
};

通过如上方式添加activity相关的单元测试周期回调函数。


getActivityIntent ,顾名思义,对启动activity的intent进行测试封装,上例中就添加了相关的参数。值得注意的是,为何 intent 中没有添加 action 呢?我猜想就是 ActivityTestRule 对象已经与MainActivity相关联了,它就是要去启动MainActivity的,加不加action都无所谓了。这里也隐含了另一层意思,要对某个activity相关的任何方法进行单元测试,都要添加与之相关联的ActivityTestRule 对象。


beforeActivityLaunched ,就是在activity启动之前执行的函数


本例中,有一个EditText,TextView和一个Button,点击Button,将EditText中的文字显示到TextView,同时也会接收Intent中的相关参数,显示在TextView中

public class MainActivity extends AppCompatActivity {

String mData;
TextView text;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mData = getIntent().getStringExtra("data");
text = (TextView)findViewById(R.id.text);
text.setText(mData != null ? mData : "");
}

public void sayHello(View view){
EditText edit = (EditText)findViewById(R.id.edit);
String str = "hello " + mData + " " + edit.getText().toString() + " !";
text.setText(str);
}
}

它的单元测试类依然可以和第2节一样生成,我们看看详细代码:

@RunWith(AndroidJUnit4.class)
public class MainActivityTest {

@Rule
public ActivityTestRule rule = new ActivityTestRule(MainActivity.class){
@Override
protected Intent getActivityIntent() {
Intent intent = new Intent();
intent.putExtra("data","world");
return intent;
}

@Override
protected void beforeActivityLaunched() {
super.beforeActivityLaunched();
Log.i("okunu","before");
}
};

Context appContext;
@Before
public void setUp() throws Exception {
Log.i("okunu","setUp");
appContext = InstrumentationRegistry.getTargetContext();
}

@After
public void tearDown() throws Exception {
Log.i("okunu","tearDown");
}

@Test
public void sayHello() {
onView(withId(R.id.edit)).perform(typeText("jim"), closeSoftKeyboard()); //line 1
onView(withText("hello")).perform(click()); //line 2
String expectedText = "hello " + "world " + "jim" + " !";
onView(withId(R.id.text)).check(matches(withText(expectedText))); //line 3
}
}

注意,context是可以获取的。另外最重要的就是理解这几个生命周期回调函数的作用。可以在setUp函数中获取context,如果与activity启动相关的要改动,则在ActivityTestRule类中修改即可。


4、常用方法介绍


在android单元测试中需要获取到某个view,如何获取呢?



  • withText:通过文本来获取对象,如:ViewInteraction save = onView(withText(“保存”)) ;

  • withId:通过id来获取对象,如:ViewInteraction save = onView(withId(R.id.save)) ;


通过文本获取,如上例,如果某个view上的文本是“保存”,则返回此view。通过id获取就比较容易理解了,建议使用id方式。


那么对view操作的接口又有哪些呢?


使用方式是onView(…).perform() 。也可以执行多个操作在一个perform中如:perform(click(),clearText()) 。所有的操作都有一个前提 ———— 就是要执行的view必须在当前界面上显示出来(有且可见)。

方法名含义
click()点击view
clearText()清除文本内容
swipeLeft()从右往左滑
swipeRight()从左往右滑
swipeDown()从上往下滑
swipeUp()从下往上滑
click()点击view
closeSoftKeyboard()关闭软键盘
pressBack()按下物理返回键
doubleClick()双击
longClick()长按
scrollTo()滚动
replaceText()替换文本
openLinkWithText()打开指定超链

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

写了个程序员真香的简历制作工具

看效果 不废话直入主题,这里是编辑页面,你可以来尝试一下 用法 左边写Markdown内容,右边显示渲染效果,纯前台数据,不涉及数据库 扩展语法 Markdown本身并没有实现多列布局,以及针对简历布局的需要,所以我自己写了个插件来支持这些语法并实现Mark...
继续阅读 »

看效果


不废话直入主题,这里是编辑页面,你可以来尝试一下


image.png


用法


左边写Markdown内容,右边显示渲染效果,纯前台数据,不涉及数据库


扩展语法


Markdown本身并没有实现多列布局,以及针对简历布局的需要,所以我自己写了个插件来支持这些语法并实现Markdown的一些常用语法(只实现了常用的),比如多列布局、图标、个人信息、主体内容布局语法


多列布局


实现一个三列布局,原理就是flex

::: start
第1列
:::
第2列
:::
第3列
::: end

多列布局显示效果


image.png


图标语法


下面的语法将被解析为<i></i>,可以通过下面即将介绍的自定义CSS来对其进行样式设置,感兴趣可以自行尝试一下.

icon:github

图标显示效果


image.png


其他语法


更多可以查看这里的语法助手


工具栏


为了更自由的定制化需求,暴露了一些工具提供使用,这里以编写CSS为例,写的CSS可以直接作用在简历模板上


image.png


编写CSS这里所有的样式都需要写在.jufe容器下面,以防影响到其他节点的样式


image.png


设置后的效果


image.png



其他小功能比如自定义主题配色、自定义字体颜色、调节模板边距等可以自行尝试一下,就不过多赘述了,都是比较实用的功能



两种编辑模式


提供了两种编辑模式,markdown模式内容模式,提供内容模式主要就是给一些不熟悉 Markdown的同学使用,那这里就介绍一下内容模式吧


点击切换编辑模式(左侧右侧工具栏都有提供切换按钮,这里使用左侧的)


image.png


这样就切换到了内容模式,这两种模式之间的数据是同步的,可以自由切换


image.png



内容模式本质就是一个富文本编辑器模式,就类似写word一样就可以了,比如想修改图标,直接点击想修改的图标就会弹出图标选择框,点击想替换的图标后直接就完成了替换,这个感兴趣的自行尝试吧



模板


模板目前有十几个,都随便用,没事的时候就会更新一下


image.png


其他


其实这个工具还有很多功能都没介绍,因为全部写下来的话篇幅会太大,感兴趣可以自己去尝试,都是些比较实用的功能


仓库


GitHub Repo
    Gitee Repo


感兴趣可以点个Star,感谢支持,如果你有不错的Idea,欢迎给仓库贡献代码.


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

我本可以忍受黑暗,如果我未曾见过光明

老文章? 这篇文章大体结构早已在我语雀里写完了很久很久~~~ 因为这篇文章写的时候太过于冲劲十足,太过于理想主义,但是反顾现实我当时正在经历考试挂科,没错,就是你理解的大三挂科了(这也就意味着我开学要经历补考,如果没过的话,可能大四不能实习,还要和下一届同学...
继续阅读 »

老文章?


这篇文章大体结构早已在我语雀里写完了很久很久~~~


假期就有构思了,现在埋坑


因为这篇文章写的时候太过于冲劲十足,太过于理想主义,但是反顾现实我当时正在经历考试挂科,没错,就是你理解的大三挂科了(这也就意味着我开学要经历补考,如果没过的话,可能大四不能实习,还要和下一届同学一起上课,而且下一届还是我带的班级,想想那种感觉“咦,武哥你怎么在这上课”而我,内心qs:杀了我把,太羞辱了,脚指头已经扣除一套四合院了)


朋友问我成绩,当时孩子都傻了


所以这段时间我正在经历自我内耗,就向是欠了谁东西,到了deadline,到了审判的日子才能释怀!也至于最近心理一直在想着这个事情,导致最近焦虑的一批,最近几天自己都不正常了,但是终于结束了~~~(非常感谢老师)



言归正传


好了好了,又跑题了,书归正题,你可能会疑惑我为什么用这个标题,难道我经历了什么涩会黑暗,被潜规则,被PUA......(给你个大逼斗子,停止瞎想,继续向下看)



这篇文章灵感来源于我很喜欢的B站一位高中语文老师讲解《琵琶行》,突然我被这个短短 3分51秒的视频搞得愣住了,直接神游五行外,大脑开始快速的回顾自己最近的生活~~~(再次表白真的很爱这摸温柔的语文老师,他的课真的让我感觉到什么叫“腹有诗书气自华”)

视频链接:https://www.bilibili.com/video/BV1bW4y1j7Un/

最爱的语文老师


其实人生当中很残忍的一个事儿是什么呢?就是你一直以为未来有无限可能的时候,就像琵琶女觉得她能够过上那样的生活一直下去。一直被“五陵年少争缠头”,一直被簇拥着的时候,突然有一天你意识到好像这辈子就只能这样,就只能去来江头守空船,守着这一这艘空船,默默的度过慢慢的长夜。
就是如果如果你不曾体验过那样的生活,你会觉得好像“我”最终嫁给了一个商人,然后至少衣食不愁,至少也能活得下去,好像也还算幸福。但是如果我曾经经历过那样的生活,我此刻内心多多少少是有些不甘的。


很喜欢的一幅油画


亦或者是像白居易,如果他是从平民起身,然后一直一步一步做到了江州司马可能觉得也还是不错,但是你要知道他在起点就是在京城为官,所以这里其实是有很明显的,一种落差。那也同样,如果此刻你回到我们说所有的文学都是在读自己,你想想看你自己,此刻你可能没有这种感觉。


30公里鲜啤



哈哈哈,兄弟们不要emo啊,让我们珍惜当下,还是那句话,我们还年轻,谁都不怕。(但是遇到刀枪棍棒还是躲一躲呀,毕竟还是血肉之躯)



其实反思反思人生中最大的挑战,就是接受自己生来平凡。自己没有出色的外表,我也没有过人的才华,我可能也少了些许少年时的锐意。但是这个emo点我并不care,因为我还在拥有选择的阶段,我也在尝试探索不一样的人生,这也许就是喜欢记录生活和写下灵机一动时候想法的意义。但是也就向UP主@peach味的桃子记录自己第44次开学,也是最后一次开学表达自己点点滴滴,也同样是不同的感受;我们同样有应届生的迷茫,但是想想也没什么可怕,还在学习,还在向目标奔跑,也还在享受校园生活~~~


打卡老馆子-群乐饭店


啊呀,好像又唠跑偏了,就是说我对这个视频那么的不一样,尤其是这个主题,因为自己的寒假的实习给我带来了新的视野,哦不,应该是旷野,很有幸能去华为在我们省份的办事处,又被出差派往华为在一个某市分部工作了半个月。这短短的实习经历,让我在大三这个迷茫的时期多了份坚定,在这个期间和大佬们一起工作,真的看到了人家的企业文化和那种行动力,最主要被军团的大佬们很牛掰技术折服,在相处这段时间真的知道了什么是向往的生活,这个学历门槛迈过去,你将会迎来什么样的明天~~~


(谁说我去卖手机去了,我揍他啊[凶狠])


游客打卡照


所以我可能对之前年终总结看法有了些改变,我之前年终总结写到,薪资又不会增加多少,浪费三年那不纯属XX嘛,没错,今天我被打脸了,为我之前的幼稚想法感到可笑;写到这里脑子已经开始疼了,最近甲流,朋友圈注意身体,这个东西真的会影响我们的战斗力,好吧,这也只是一个随想录,留点内容给年中总结,要不到时候就词穷了,哈哈~~


很nice的江景房


近期反思


其实每个人的出发点不一样不能一概而论,就向我自己出发,一个来自十八线农村的孩子,父母通过自己一代人的努力从农村到乡镇,而我就通过自己的求学之路一直到,貌似能够在这个省份的省会立足,这也就是我能做的进步,不管怎么说,我们都是从自身出发,其实谈到这个问题,我自身也很矛盾,小城市就真的不好吗,人的一生除了衣食无忧,在向下追求的不就是快乐,如果真的能和一个爱的人,在做一些自己喜欢做的事情,难道不就是“人生赢家”,城市在这种维度下考虑貌似也不重要~~(如果你想diss这种想法,没有考虑子女的教育问题,其实我想到了,但是我目前的年龄和所处的位置吧,感觉很片面,所以就不对这个点展开讨论了)


过度劳累,小酌一杯


回复问题


有人怕别人看到自己以往的文章写的很幼稚,就不想写了,我有不同的看法,只有看到曾经的对事情的看法和处理方式幼稚了,才能证明自己的成长呀,谁能一下子从孩子成为一个大人!(但是某些时候谁还是不是一个孩子[挑眉])



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

职业发展应该思考什么

没有一种适合所有人的职业规划。因为每个人对职业的诉求不一样。 这里谈谈我思考的维度。 一、 生活。 工作服务于生活,你想过什么样的生活呢?短期你会因为挣钱不挑活儿,但长期你一定要从生活出发考量职业,不要成为工作的奴隶,本末倒置。 提前想明白你对生活的诉求是什...
继续阅读 »

没有一种适合所有人的职业规划。因为每个人对职业的诉求不一样。


这里谈谈我思考的维度。


一、 生活。


工作服务于生活,你想过什么样的生活呢?短期你会因为挣钱不挑活儿,但长期你一定要从生活出发考量职业,不要成为工作的奴隶,本末倒置。



  1. 提前想明白你对生活的诉求是什么,提早布局。

  2. 尽早确定你要在哪个城市定居,你的孩子要在哪个城市上学


二、 你有认真想过自己适合干什么吗?


职业可以划分为三类:



  1. 生产商品:设计师、产品、程序员、QA、按摩师、理发(服务本身也是商品)

  2. 链接关系:销售、中介

  3. 资源分配:管理


大多数人的职业属于前两类,要么直接创造价值,要么通过链接商品、间接创造价值。


很多朋友的职业发展是,找到什么工作就干什么,公司安排干什么就干什么,随波逐流。运气好跟着业务一起晋升,运气不好到了中年举步维艰。


先发散,假设你什么都不会,你想去做什么。然后收敛,选出可行的方向,你不一定非要在程序员这个行业死磕的。


往小了说,技术类有很多细分类别,有没有更适合你的领域。长期从事你并不喜欢的工作,会被职业磨得毫无生气,苟延残喘。


很多人的顾虑是,我不会这个,别人要我吗?这些对年轻人来说,都不是问题。


我见过最多情况是,等着工作的安排,把成长完全寄托于工作。能做到需要什么学什么就很不错了。


少有人能把成长和工作划分开,独立规划自己的职业,设置学习计划并严格执行。


3. 人脉。


职业的每个阶段,使命不一样,实现的路径也不同。


30多岁跳槽,基本靠朋友互推,这个阶段的人脉靠你20多岁来积累,有些人从来不出去交流,不向别人展示你擅长什么,到了三十岁时籍籍无名。


一次分享,一次沙龙,一次愉快的合作,长期维护的博客都是向外展示自己,积累人脉。


重要的岗位上,老板是否用你,排在第一看的是"信任关系",你是老板你难道不优先用嫡系么?卧榻之侧岂容他人鼾睡。


为什么狗是人类最喜欢的宠物--忠诚。


很多人三十多岁进退维谷,都是年轻时候不作为埋下的雷,没有办法,人生就是这样,我们终究要为自己的选择负责。


4. 聚焦。


职场和高考最大的区别在于:


基础教育强调通识性,学校要求你全面发展,偏科的劣势非常明显。


职场是流水线,用的是专才,专业的优势非常明显。


职场人的成长路线是,先集中力量聚焦一点,打穿打透,以此立足,之后再迁移拓展。越早在一个点上打穿,就为你后面的发展争取了更多的时间。


非常忌讳什么都干,什么都不精通,这意味着你所有的工作都可以轻松交接给别人。


建议:不要什么活儿都接,尽量让工作收敛到一个可持续发展的领域。 你不能赌哪个人能混好提前去抱大腿,对自己下注最保险。


不要仅因为晋升或取悦老板,啥事都冲在前线,尤其是做大量沟通协调、低价值的工作。


沟通能力对程序员来说是锦上添花,是突破瓶颈需要的能力,但大部分人还远没到这个层次,混的不好是基础的专业能力不过关,


你敢自信的说,自己精通哪一门技术吗?


输入 & 阅读


《纳瓦尔宝典》一书中,把人分成两类,一类是读书的人,一类是不读书的人。


有些朋友知道要多思考,但是"脑子很空",思维打不开。


原因是输入太少,小学只能解决最初级的加减乘除计算,复杂的空间几何要到中学阶段,更复杂的近现代数学(微积分、线性代数、概率论)要到大学阶段。


知识储备决定思维、格局的边界。


有个原始部落里民风很淳朴没有偷盗事件,原因是他们的语言没有"盗窃"这个词,因此他们就没有进化出这种意识行为。


你的知识储备决定你思维进化的程度。


要在年轻的时候系统的阅读,构建自己各个领域的通识基础,逐步融会贯通。每一门学科都是一门逻辑自洽的完整体系,都能为你打开一扇看世界窗户。横看成岭侧成峰,远近高低各不同。


趁年轻,多读一点有难度的,信息密度高的书,越往后阅读越轻松。


建议,每个月至少读一本书,一年12本。达不到这个量,很难有质的跃迁。


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

一个前端实习生在蔚来的成长小结

一、聊聊工作氛围 & 个人成长 1. 这是我的期许 “所谓前途,不过是以后生活的归途。” 这是我人生中的第一段技术实习,之前并不是一心做技术的,所以为了探索自己喜欢的事情在某小公司做过翻译实习,并且在交行做过金融实习(其实就是纯打杂)。 我很喜欢这样...
继续阅读 »

一、聊聊工作氛围 & 个人成长


1. 这是我的期许


“所谓前途,不过是以后生活的归途。”


这是我人生中的第一段技术实习,之前并不是一心做技术的,所以为了探索自己喜欢的事情在某小公司做过翻译实习,并且在交行做过金融实习(其实就是纯打杂)。


image.png


我很喜欢这样一段话: “我曾以为我的23岁会手提皮包西装革履,但我还是穿着休闲裤,带着十几岁的意气行事,幼稚又成熟;我曾以为我的23岁会性格外向,做事圆滑,但我连最简单的亲情都处理不好;我曾以为我的23岁会和喜欢的人看山河大海落日余晖,但没想道周围的人谈婚论嫁都近在眼前,我还在路上找自己。”


我一直在探索着自己的边界,在能闯能疯的年纪反复横跳,寻找着自己的热爱与期许。在真正从事这个行业之后,我发现了我对于这个岗位的喜爱,当你看着一个个实际的视图出现于自己的手中,你会有一种莫名其妙的成就感,这种感觉难以描述的程度就好像你要向一个完全不看vtuber的人描述你对嘉然的喜爱。


2. 工作氛围:这里是一个乌托邦(适合摸鱼学习的好地方!)


说实话,我最开始预期是每天九点来上班,九点下班的(因为看学长们实习都好辛苦的样子)。


来了之后发现完全不是,每天十点上班,六点下班(我当然是准点跑路)



实习两个月左右的时候接的一个需求,第一天是另一个前端实习生来搞,后来他要跑路,leader就把活给我了。


周四,后端六点把接口给另一个前端实习生。


另一个前端实习生:“明天再说”


周五我来接这个活,我边画页面边让他加字段。


然后提完了,六点他给我改好的接口,让我看看有没问题


我:“下周再说”。


后端:“前端是不是,都很快乐啊[流泪]”



image.png


最开始因为我对 react 不是特别熟悉,leader 让我看着组内文档学了半个月,才开始了第一个需求。


leader 没有给我指定 mentor,所以当我有问题的时候,我看组内谁没开会(或者有时间)就会去问,都能得到很耐心的解答,这点来说还是很有安全感的。


然后每天都会跟着老板和大老板一起去吃饭,有时听他们说说自己的事情,有时听听他们对某个语言的看法,也算有不少收获。


值得一提的是刚入职三天部门就开始团建了,从周五下午五点玩到了第二天凌晨两点,炫了一只烤全羊,然后就开始电玩篮球各种 happy,后面玩狼人杀我次次狼人,大老板也总觉得我是狼人,我次次和他对着刚(乐)



马上就要第二次团建了,可惜参加不了呜呜呜



在团建上 leader 说我是从五个面试感觉都 ok 的人里面选出来的(当时我超惊喜的)


还有几件有趣的事情值得一提



第一件事情是中午和 leader 散步,他说:“你干了两个月这里的情况也看到,很难接触到同龄的小姐姐的,找对象的优先级应该要提高了。”


我:“说的对说的对。”


当时我心里就暗暗想着,这是我不想找吗?这tm是我找不到啊(悲)


第二件事情是我有事开了自己的热点,热点的名字叫:“要失业了咋办呐。


被同事发到了前端大群里。


同事:“这是谁的啊?”


我:“是实习生的(悲)”



3. 个人成长:“不卑不亢,低调务实”


最开始入职当然会担心一些七的八的,诸如这样说会不会不太客气,这样搞会不会让老板不爽,后来和老板还有大老板一起吃饭之后发现他们人都挺随和的,没什么架子,他们更多的关心的是这件事情做的怎么样。


大老板曾经在周会上说:“这个事情可以做的慢一些,这是能力上的问题,这个可以商量,但是如果到了约定的日期没有交付,这就有问题了。 ”这个是说的务实。


然后就是为人处事方面了,自己有时候挺跳脱的,没有什么边界感,在实习和他们一起吃饭的时候我就回默默的听着,有些问题大家都不会问,算是看着看着就成长了。


回校远程的时候我写了这样一段话:



去打工吧,去打上海冬夜准时下班,踩雪走回家的工。


去打工吧,去打一边聊天一边发现,这个产品也是清华✌️的工。


去打工吧,去打测试前一天,人都走光了,mentor陪我赶工到半夜的工。


去打工吧,去打部门团建,大leader带我们玩狼人杀到凌晨两点,超级尽兴的工。


冴羽曾在一次读书会上分享:“开眼界就像开荤一样,只有见过了才会产生饥饿感。”


打工虽然让我变成了稍不注意就会摆烂的成年人,但大平台汇聚了很多丰富有趣的同事,让我看到了截然不同的经历与一波三折的人生。


不知道是不是部门的原因,我这边总是十六五准点上下班。


我现在依然处于打工真香的阶段,不用早起,不用日复一日的和同龄人卷同一件事,身边的人年岁不同,人生阶段也不相同,卷不到一起去。


我还在路上~



image.png


4. 代码方面 learning


说实话看到组内项目的时候体会到了不少的震撼,看着组内的项目之后真的就感觉自己写的东西和玩具一样,每次写完项目,都会兴冲冲的找组内的哥哥姐姐帮忙 CR,然后 CR 出一堆问题,自己在一个一个的修改,把这些规范点记周报上,总之就是学到了很多很多。


timeLine 大概是这样的



  • 前两周熟悉 react 写小 demo

  • 然后以两周一个需求的速度给咱活干~


记得第二次写完一个稍微有点复杂的需求,带着我做这个需求的 mentor 还夸了我一波(骄傲)


5. 对于技术和业务的想法


大leader组织组内 vau 对齐的时候我仔细的听了听,我们的很多东西都需要落地,相比来说技术只是一个实现的手段,并不是做这个的目的。


但怎么说呢,我个人还是对技术本身抱有很大的期许的,希望自己能够变得很厉害,参与到很多的开源项目中,我坚信代码可以改变世界。


二、展望未来



实习不去字节,就像读四大名著不看红楼梦,基督徒不看圣经,学相对论不知道爱因斯坦,看vtuber不看嘉然今天吃什么,这个人的素养与精神追求不足,成了无源之水,无本之木。他的格局就卡在这里了,只能度过一个相对失败的人生!




  • 话是这么说啦,但最后还是没有成功去到字节,但是我是字节不折不扣的舔狗,后面再看吧。

  • 字节给我发面试一定是喜欢我(普信)


下面这段是之前写的



离开的契机也很简单,我在小红书实习的同学跑路了,然后要找继任,顺手把我的简历投过去了,然后我顺手面了一下小红书,小红书顺手给我发了个Offer(bushi,然后就去小红书了。



image.png


小红书确实Offer了,但是老板和我约谈了很久,我决定继续远程实习,在这篇文章发布的当天,我已经实习了 一百四十天,我相信,我的旅途还在继续。


image.png


三、写在最后


不知不觉就实习快半年了啊


我真的非常感谢遇到的leader和同事,感恩遇到的每一位愿意拉我一把的人。


在这段时间里学到了好多一个人学习学不到的东西啊。


那么这就是我在蔚来的实习小结啦!


感谢阅读~


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

日本,我劝你别来

大家好,我小j,很久不见,这几个月摸鱼摸得过分了,一直没找到新想法来写文章,正巧最近有很多群友交流疑问求职问题,这边我就写一篇干货,以我自身经历以及和来日本很久的朋友交流中总结下日本的程序员工作相关的一些话题 在讲这个话题之前,首先要引入很多概念,当然不愿意看...
继续阅读 »

大家好,我小j,很久不见,这几个月摸鱼摸得过分了,一直没找到新想法来写文章,正巧最近有很多群友交流疑问求职问题,这边我就写一篇干货,以我自身经历以及和来日本很久的朋友交流中总结下日本的程序员工作相关的一些话题


在讲这个话题之前,首先要引入很多概念,当然不愿意看也跟我没关系,可以自己选择折叠和跳段


日本的雇佣情况




  • 正社员


    正社员是最常见的日本公司雇佣情况,具体表现是入职合同没有工作期限或者期限在退休年龄之后,一般来说正社员是大部分日本公司的正式雇员,公司需要帮助其按时缴纳厚生年金和医疗保险的费用,同时也会享受公司的全部福利。


    根据日本法律的情况,除非自己离职,或者沟通选择离职,一般公司无法以一般理由开除正社员。同时推特、谷歌、indeed等外企在裁员时日本分部也是裁员最少,赔偿金月数最高的地方。


    极端来说,大部分日本公司或者外企都要脸,在日本法律的要求下,不会像国内一样大量的裁员关闭部门,基本上都是软磨硬泡砸钱沟通让你走,如果你求助工会赖着不走,公司也没有什么很好的处理方法,身边有例子沟通裁员后赖到现在还没走。


    当然,公司宣布倒闭的话,也没有法律和脸面的说法了。


    简单来说,可以类比国内带编制的国企职工




  • 劳务派遣


    劳务派遣是日本第二常见的雇佣情况,具体可类比国内的外包开发,甲方有开发需求时,向派遣公司提出要人要求,派遣公司的雇员通过甲方面试后进入甲方的公司进行开发。


    综合来说派遣是水最深的地方,你的求职体验跟要你的甲方,雇佣你的派遣公司乃至跟你同期进去的同事息息相关,任何一方恶心到你都会极大的影响你的工作体验。能否remote,是否需要穿正装,都根据不同甲方的要求各异。


    但是相对而言,派遣也是自由性很大的职业,一旦自己觉得不爽了就可以跟派遣公司申请换现场,只要能通过面试就可以进入新的现场工作。同时因为自己不是正式员工,也不需要关心公司的制度要求,到点下班,根本不care其他人的情况。由于日本派遣制度的成熟,很多传统大手有大量任务需要交给派遣完成,所以只要日语ok,技术尚可,找到新现场也不是难事,这也是很多人喜欢一直在派遣的原因。




  • 契约雇佣


    这种雇佣形式和国内类似,由于某些原因,公司没有按正社员的形式雇佣你,而是签一年、三年等时间的定期合同,到期需要公司续签。




  • 项目委托


    这种形式常见于freelancer和部分派遣公司,雇佣的目的是为了完成某个项目,而不是缺人辅助开发各种系统,俗称一锤子买卖,交付完成之后就相当于完成合同,之后可能会继续交付项目亦或是直接拜拜




日本的公司类型




  • 日资公司


    日资公司就是常见的真日本人担任法人的公司,俗称日企,其中日企也可以分为两类




    • 传统日企


      传统日企整体公司以日本人为主,主要沟通语言为日语,或多或少会带有一些日企味道,根据公司要求,可能会要求穿正装,年功序列,熬年限等等情况。


      例子有:大部分日本本土公司,如银行,制造业




    • 现代日企


      有许多日本本土互联网公司吸收了大量外国雇员,如乐天、line、Woven Planet、paypay等公司,这些公司大部分沟通语言为英语,整体比较年轻化,管理比较自由,整体和国内公司的情况类似。






  • 外资公司


    外资企业就是我们常说的外企,比如amazon、google、apple、微软等公司,整体风格就跟其他国家的外企类似,主要语言为英语,面试要求较高,相对应工资也会比较高。




  • 华人公司


    华人公司就是中国人在日本开办的互联网业务公司,整体风格千差万别,无法具体定论




  • 派遣公司


    正如上面提到的劳务派遣情况,当甲方缺干活的人,又不想招聘自己的员工时(招聘是一个非常费钱的过程),就向派遣公司要人,派遣公司提供自己的员工去面试,面试通过则进入甲方工作。


    派遣公司有几个比较大的问题:



    1. 跟华人公司一样,派遣公司好不好,良不良心完全看老板的风格,除了base这种大型上市派遣公司以外,大部分派遣公司都比较夫妻店一言堂,所以完全看老板是不是一个有良心的人。

    2. 派遣并不包找到现场,所以一旦你因为语言或者技术的问题而久久没有公司接收你的时候,你只能根据合同可能能收到一些待机费,待机太久可能会有辞退的风险。

    3. 派遣也会按正社员、非正社员招人,一般是正社员的工资会比非正社员低一些,但是公司会帮正社员提交社保,非正社员需要自己提交社保,年底统计会更麻烦一些。

    4. 由于2022年小红书、知乎、v2ex中介的发力,引来了大批量无日语,有(无)技术的国人来到日本,导致目前派遣现场非常卷,基本都要求有一些日语能力,能看懂式样书,稍微做一些技术。请不要轻信中介的说法,无技术无语言,在日本是很难找到工作的。小红书上有大量派遣员工找不到现场待业的文章。

    5. 请拒绝一切派遣公司要求原件的要求,办理在留签证不需要任何原件,打印件即可,一旦交付原件以后可能会被提不合理要求




个人建议


总的来说,要先找交厚生年金的正社员工作,然后是每个月都有全额工资拿的契约社员,最后才考虑派遣公司。


华人公司因为聊得太少,暂时没有添加进排序里


高薪外企/大手日企 > 低薪外企/日企小公司 > 派遣正社员 > 派遣契约社员


个人是非常不建议入职派遣的,因为鱼龙混杂,如果你日语不好初来乍到很容易被骗被坑,但是你非说我就为了留在日本拿签证或者我能力暂时不足以进其他情况的公司那我也没法拦你


记住,一旦遇到工作上的违法事情或者パワハラ要记得找hellowork来谋求自己的合法权益


面试要求


一定要掌握一门外语


一般来说日本传统日企要求不高,日本公司不像国内公司一样八股乱飞,妖魔横行,更多是要求日语好,通过交流来确认你确实有解决问题的能力,之后就是看重behavior question,考察你的合作能力,


外企和国际化的日企会接轨国外的考察方式,一般一面是hr交流看你的意愿,之后进行一轮oa,之后技术面试会考察算法、系统设计、bq和交流能力


华人和派遣无法一概而论


如何准备面试


面试之前请确定你的目标公司,因为传统日企和其他公司有着决定性的差别。


每个人的情况不同,以下只给出一些对应的网站


语言:日语/英语做到交流无碍,可以没证书,但是起码交流大家都听的懂(乐天要求托业800分以上证书)


算法:leetcode


bq:彻底击碎行为问题


system design: 系统设计面试题精选


etc.


怎么找到公司


除了google、indeed、amazon、apple、微软等大外企直接在官网找招聘入口之外,大部分公司都会在以下几种途径进行招聘



  1. 内推

  2. linkedln

  3. tokyo-dev、japan-devgreen-japanindeeddodaopenworkforkwellfindylapras等求职网站

  4. 猎头


当准备好一份英文简历后,挂在linkedln上,关注一些目标公司,经常会有猎头或hr邀请。


怎么区分派遣公司


说句实话,目前小红书、知乎、v2ex上招人的大多都是派遣公司,大部分自社业务的公司较少会直接在这些平台发帖招人,如果你拿不准这个公司是否是派遣公司,可以依照以下几个方法区分:



  1. 查询该公司的linkedln首页,一般linkdln有信息,有员工的的较少为派遣公司

  2. 查询该公司主页,如果没有明确说出自己产品或者包含提供技术人才、提供解决方案等关键词,大概率为派遣公司

  3. 如果该公司没有母公司,且在谷歌上信息很少,大概率为派遣公司


选择公司需慎重


Q&A


日本卷不卷


事实上,日本之前确实很卷,毕竟是社畜这个词的产生地,但是安倍上台后2015年发生了电通事件后立法限制了加班时间,我正规公司的正社员程序员极少存在加班现象,周末不oncall,假期不上班是常态。


日本工资低不低


说实话,低,大部分日企开发天花板远低于中国,胜在一个稳定。日本的程序员工作属于一个普通工种,很多本国人不愿意开发,所以提供了大量岗位吸收国外的开发。


薪资参考:


Untitled (5).png
具体查询薪资网站:opensalary.jp/en/roles/so…


不会日语能否在日本生活


首先给出结论:可以。


要相信人的理解能力,除了银行卡需要日语(入职后可以找同事陪同)之外,其他的不是有中文服务就是可以网上申请。


在日本生活会不会比国内好


不一定,因为工资低,物价不便宜,所以在日本能否享受你在中国的生活水平因人而异。


日本买房多少钱


suumo


有身份可以做到8-10倍年收,0首付,0.5-0.3%利率,有房产税,一年大概1.2%,之后折旧会降低


日本是否值得来


长远角度,日本经济处于要完未完的状态,长期停滞,物价上涨,工资比不上物价的涨幅,而且岛国环境,依附于美国势力,长期发展肯定是远不如国内的,但是生活不止宏大叙事,过好自己的生活,享受安逸才最关键,所以值不值得来,就看你看中什么了


日本生活相关


跟本篇内容无关,之后再说(咕了)


是否适合出国


请你先想好出国的风险,以及作通家里的思想工作之后,再考虑出国的决定,希望你想出国只是想看看异国的文化和工作体验,而不是当个键政小鬼只会润润润。


Untitled (1).jpeg


以上


对年轻人应届生的小建议


目前国内确实经济处于不良好的阶段,大厂裁员事件频发,hc逐年降低,应届生比往年更难以拥有满意体面的offer,以我的经验对应届生可以写几条建议



  1. 除了希望大学期间打好基本功,多参与项目之外,也建议可以读研等方法跳过经济周期,等待几年后的复苏。

  2. 在求职道路中,除了私企国企公务员等项目,确实也可以多考虑外企乃至出国工作的机会,不要停止英语学习,这样路子会更多更广。

  3. 减少键政,少看知乎等键政乐子人聚居地,天天看帖子只会形成信息茧房,充满你满意的观点的帖子,但是在你阅读帖子的时候,只有发泄情绪浪费时间,年轻人压力大找地方发泄无可厚非,但是只会谩骂指责并不能对你有任何帮助,除非你渴望机械降神,否则还是希望多用时间去充实自己,提高自己。少键政,少参与骂战,少乐少甜,都是浪费时间。

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

《我的程序人生》

《我的程序人生》 在这个充满挑战和机遇的数字时代,我回首过去,分享我的程序生涯。这是一个展示我职业计划、职场成长、掌握的新技能、薪资增幅、阅读的书籍、硬件装备、半路出道、良师益友以及职场经历的机会。 职业计划是我程序人生的指南针。早年,我对计算机科学的热情引领...
继续阅读 »

《我的程序人生》


在这个充满挑战和机遇的数字时代,我回首过去,分享我的程序生涯。这是一个展示我职业计划、职场成长、掌握的新技能、薪资增幅、阅读的书籍、硬件装备、半路出道、良师益友以及职场经历的机会。


职业计划是我程序人生的指南针。早年,我对计算机科学的热情引领我进入这个领域。我设定了远大的目标,希望成为一位卓越的软件工程师。通过不断学习和实践,我逐渐掌握了多种编程语言和技术,包括Python、Java、JavaScript等。这些技能为我在职场上的发展奠定了坚实的基础。


职场成长是我程序人生中最宝贵的财富之一。我从最初的实习生开始,逐步晋升为项目经理。在每个岗位上,我都努力追求卓越,并学会了如何与团队成员合作,如何管理时间和资源。这些经验让我不断成长,成为一个全面发展的专业人士。


掌握新技能是我程序人生中不可或缺的一部分。我不断追求学习和进步,通过参加培训课程、在线教育平台和技术社区来拓宽自己的知识面。我学习了人工智能、数据科学、云计算等前沿技术,并将其应用于实际项目中。这些新技能不仅提高了我的竞争力,还让我能够为公司带来更多的价值。


薪资增幅是我程序人生中的一项重要衡量指标。随着经验的积累和技能的提升,我的薪资水平也得到了显著提高。然而,对我而言,薪资并不仅仅是一个数字,更是对我努力工作和不断进步的认可。它激励着我继续追求卓越,并为自己设定更高的目标。


阅读的书籍是我程序人生中的灵感之源。我相信知识改变命运,因此我不断阅读各种技术书籍和领导力书籍。这些书籍开阔了我的视野,激发了我的创造力,并帮助我更好地理解技术和人际关系的复杂性。每一本书都为我带来了新的启示,让我在程序人生的道路上不断前行。


硬件装备是我程序人生中的得力助手。一台高性能的电脑、一款舒适的键盘和一台高清显示器都让我的工作更加高效和愉悦。这些工具不仅是我工作的必备品,也是我对技术的热爱和追求的象征。


半路出道是我程序人生中的一段特殊经历。曾经,我在一个完全不同的行业工作,但内心深处的激情让我决定转行进入程序领域。这段经历让我更加珍惜我现在的职业,并以一种全新的视角看待问题和挑战。


在程序人生的道路上,我结识了许多良师益友。他们是那些与我分享知识、经验和智慧的人。通过与他们的交流和合作,我不仅学到了更多的技术和职场技巧,也收获了珍贵的友谊和支持。他们的存在让我感到坚定和勇敢,让我相信自己可以克服任何困难。


职场经历中也有奇葩不愉快的时刻。这些经历教会了我如何处理冲突、管理压力和保持专业。它们让我更加坚韧,更加懂得珍惜每一个机会。


回顾我的程序人生,我感到由衷的自豪和满足。我从一个对计算机科学充满热情的年轻人成长为一名有实力和经验的程序员。我相信,未来的道路上还有更多的挑战和机遇等待着我。我会继续努力学习,不断进步,为自己的程序人生书写更加辉煌的篇章!

收起阅读 »

为什么强烈不建议使用继承

这两天有空的时候看了下 继承和复合如何选择这个知识点,其实之前开发的时候遇到类似的问题我是无脑继承的,也没有考虑这么多,因为这些新增的父子类,都是在包内使用,而且父子类基本都是我们同一个开发人员,所以一般不会有什么意外情况。 但是如果我们要开发新的类,这个类需...
继续阅读 »

这两天有空的时候看了下 继承和复合如何选择这个知识点,其实之前开发的时候遇到类似的问题我是无脑继承的,也没有考虑这么多,因为这些新增的父子类,都是在包内使用,而且父子类基本都是我们同一个开发人员,所以一般不会有什么意外情况。


但是如果我们要开发新的类,这个类需要对外开放,有很多模块会来继承我们的类(或者我们会继承第三方提供的公共类),这个时候就需要很小心的设计了,如果小伙伴们觉得以后不会接到这样的需求,其实就不用继续看的,下面内容还是有点枯燥无聊的🤣。


学习的内容




1. 继承(Inheritance)是什么


继承其实不用过多的去解释,因为大家都是非常熟悉的,它和封装(encapsulation)抽象(abstraction)多态(polymorphism) 组成面向对象编程的(Object-Oriented Programming)主要特征。


image.png



  • 代码示例
//父类
public class Animal {
   //名称
   protected String name;
   //种类
   String species;

   protected String getName() {
       return name;
  }
   protected void setName(String name) {
       this.name = name;
  }
   protected String getSpecies() {
       return species;
  }
   protected void setSpecies(String species) {
       this.species = species;
  }
}


//子类
public class Birds extends Animal {
   //翅膀长度
   protected String wingSize;

   protected String getWingSize() {
       return wingSize;
  }
   protected void setWingSize(String wingSize) {
       this.wingSize = wingSize;
  }
}

总结:


继承的优点:



  1. 子类可以复用父类的代码,继承父类的特性,可以减少重复的代码量

  2. 父子类之前结构层次更加清晰


继承的缺点:



  1. 父子类之间属于强耦合性,一旦父类改动(比如增加参数),很可能会影响到子类,这就导致代码变得脆弱

  2. 如果子类新增一个方法,但是后续父类升级之后,和子类的方法签名相同返回类型不同,这会导致子类编译失败

  3. 会破坏封装性

  4. 不能进行访问控制


缺点第三条的解释:


下面是新建了一个集成HashSet的类,主要目的是想统计这个实例一共添加过多少次元素



  • addAll:批量增加数据

  • add:单个数据增加


最终统计出来的结果是 4 ,只是因为 super.addAll(c) 最终会调用add方法,也就导致重复计数了。


出现这种情况是因为我们在编写子类逻辑时不清楚父类方法的实现细节,从而造成了错误,即使我们把add中的addCount++ 删除,也同样不能保证父类的逻辑会不会变动,这样就会导致子类非常脆弱且不可控,简单总结就是子类依赖了父类的实现细节,所以这就是为什么会说破坏了封装性。



封装性:将数据和行为结合,形成一个整体,使用者不用了解内部细节,只能通过对外提供的接口进行访问

@Slf4j
public class DestroyInheritance<E> extends HashSet<E> {
   private int addCount = 0;


   @Override
   public boolean add(E o) {
       addCount++;
       return super.add(o);
  }

   @Override
   public boolean addAll(Collection<? extends E> c) {
       addCount += c.size();
       return super.addAll(c);
  }

   public int getAddCount() {
       return addCount;
  }

   public static void main(String[] args) {
      //测试类,使用addAll批量增加
       DestroyInheritance<String> item = new DestroyInheritance<>();

       String[] arr = new String[]{"s","a"};
       item.addAll(Arrays.asList(arr));
       log.info("count:{}",item.getAddCount());
  }
}

2.复合是什么


复合从字面意思上也是可以理解的,就是将多个实例的行为和特征组合成一个新的类,简单理解就是新增的这个类,它拥有其他一个或者多个类的特征,比如家用汽车有轮子、底盘、发动机等等组件组成,那车子这个类就包含了轮子类和底盘类这些属性。


image.png


看一下下面这段代码,Car是一个使用了复合的类,他包含了引擎类Engine和轮胎类Tyre,那为什么要这样写呢,我的想法是有下面几点:



  1. 在doSomething方法中,我无需全部继承引擎类或者轮胎类,只需要根据实际情况调用某些方法即可,减少了之前对父类的严重依赖,造成的耦合性影响

  2. 引擎类只需要提供个别公共的方法给Car类使用,不需要完全暴露其内部细节,也不用担心会出现类似addAll最终调用add的问题



  • 代码示例
import lombok.Data;
import lombok.extern.slf4j.Slf4j;

/**
* 汽车类
**/
@Slf4j
public class Car {
   //引擎类实例
   private final Engine engine;
//轮胎类实例
   private final Tyre tyre;

   public Car(Engine engine,Tyre tyre) {
       this.engine = engine;
       this.tyre = tyre;
  }

   public void doSomething(){
       //自定义逻辑
       engine.setBrand("坏牌子轮胎");
  }

   public String getEngineBrand(){
       //返回轮胎名称
      return engine.getBrand();
  }

   public static void main(String[] args) {
       Engine engine = new Engine();
       engine.setBrand("好牌子引擎");
       engine.setPower("250");

       Tyre tyre = new Tyre();
       tyre.setBrand("好牌子轮胎");
       tyre.setSize("50cm");

       Car car = new Car(engine, tyre);
       car.doSomething();
       log.info("轮胎名称:{}",car.getEngineBrand());
  }
}

/**
* 引擎类
*/
@Data
class Engine{
   private String brand;

   private String power;
}
/**
* 轮胎类
*/
@Data
class Tyre{
   private String brand;

   private String size;
}


3.继承和复合如何选择


image.png


说了半天,那究竟是用复合还是用继承呢,我觉得最重要的一点我觉得是要搞清楚类之间的关系,对于继承而言,它是 "is-a" 的关系,是对事情的一种比如:人是动物、华为mate60是手机,只是对于动物、手机这种是更为抽象的事物,人和华为是对其类的衍生。


复合则是 "has-a" 的关系,比如:健康的人有两只眼睛、家用汽车有四个轮子,对于这种情况而言,我们就需要用到复合。


继承和复合并非是绝对的好与坏,而是我们要结合实际情况,如果是is-a关系,只有当子类和父类非常明确存在这种关系时,我们可以使用继承,并且在代码设计时,一定要考虑日后可能出现的继承问题及后续代码的升级迭代,不然很可能出现令人崩溃的后续问题;而如果某一个对象只是新增类中的一个属性时,我们就要使用复合来解决问题。




总结


写这篇文章也搜索了一些其他博主的文章,总体的感受就是国内主流博客上相关的文章并不多,大家好像并不关心这个点😑,搜到有几篇还都是直接抄书放上去(而且长得都一模一样),很是郁闷,最后还是去跳出去看了几篇别人的文章,感觉还是有很多点还是得仔细琢磨琢磨,后面再继续学习学习。


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

挂起函数的返回值

返回值类型 协程中挂起函数的返回值类型是 Object,无论代码中写的是什么。 我们写的协程代码编译成 Java 代码后,挂起函数的返回值类型就会被修改成 Object,如下:// 定义一个挂起函数,其返回值类型是 Int private suspend ...
继续阅读 »

返回值类型



协程中挂起函数的返回值类型是 Object,无论代码中写的是什么。



我们写的协程代码编译成 Java 代码后,挂起函数的返回值类型就会被修改成 Object,如下:

// 定义一个挂起函数,其返回值类型是 Int
private suspend fun test2(): Int {...}

// javap -v 反编译对应的 class 文件
.method private final test2(Lkotlin/coroutines/Continuation;)Ljava/lang/Object;

通过反编译可以看到,挂起函数额外多了个 Continuation 类型的参数,返回值类型也变成了 Object。关于前者它是协程能恢复的关键,是协程底层原理的基础知识,此处忽略。对于后者是本文重点。


返回值类型被修改的原因



调用到挂起函数时会返回特殊对象 COROUTINE_SUSPENDED,最终也会返回自己定义的返回值。



一个挂起函数会被调用多次,当它执行到另一个挂起函数时会返回 COROUTINE_SUSPENDED 给调用者。执行到函数最后时,它会返回该返回的值给调用者。因此,挂起函数会返回两种类型的数据,所以返回结果型只能是 Object 类型。


验证


为验证上面结论,以下面代码为例说明

private suspend fun test2(): Int {
// withContext 是挂起函数
val a = withContext(Dispatchers.IO) {
delay(100)
1
}
return 1 + a
}

首先通过 as 自带的 show kotlin bytecode 查看上述代码对应的 java 代码,如下


Xnip2023-06-28_15-44-46.png


关于 if 判断是否成立,可以直接反编译生成的 apk,向 apk 中插入代码,可以发现它和 var5 是同一个对象,所以 if 判断成立,因此此时 test() 返回的是 COROUTINE_SUSPENDED。


现在确定下上图中的 $continuation 到底是什么类型,反编译 apk 查看 smali 代码,可以看到 $continuation 其实是 MainActivity$test2$1 类型。

// test2 定义在 MainActivity 类中,所以生成的内部类都是 MainActivity$ 开头

new-instance v0, Lcom/example/demo/MainActivity$test2$1;
invoke-direct {v0, p0, p1}, Lcom/example/demo/MainActivity$test2$1;-><init>(Lcom/example/demo/
MainActivity;Lkotlin/coroutines/Continuation;)V
:goto_0
move-object p1, v0
.local p1, "$continuation":Lkotlin/coroutines/Continuation;

MainActivity$test2$1 继承 ContinuationImpl,最核心代码是它的 invokeSuspend(),对应的 smali 代码如下,看懂它的代码有助于我们理解 test2() 第二次执行逻辑:

.method public final invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
.locals 2
// 将 p1 赋值给 p0 的 result 中
// p0 是当前对象。invokeSuspend() 非 static 函数,默认有一个参数 this,即 p0
// 这句代码就是:将参数赋值给当前对象的 result 字段
iput-object p1, p0, Lcom/example/demo/MainActivity$test2$1;->result:Ljava/lang/Object;

// v0 = p0.label。即将当前对象的 label 赋值给 v0
iget v0, p0, Lcom/example/demo/MainActivity$test2$1;->label:I

// v1 = Int.MIN_VALUE
const/high16 v1, -0x80000000

// v0 与 v1 或运算,并将结果存储至 v0
or-int/2addr v0, v1
// 将 v0 赋值给 this.label
iput v0, p0, Lcom/example/demo/MainActivity$test2$1;->label:I

// this$0 是 jvm 中内部类添加的一个字段,用于表示外问类的引用,此处即 MainActivity 对象
// 这句话就是将 MainActivity 赋值给 v0
iget-object v0, p0, Lcom/example/demo/MainActivity$test2$1;->this$0:Lcom/example/demo/MainActivity;

// 用 v1 指向当前对象,即 v1 = this
move-object v1, p0
// 判断 v1 是不是 instanceof Continuation,肯定成立
check-cast v1, Lkotlin/coroutines/Continuation;

// 调用 MainActivity 的静态方法 access$test2,同时传入参数 MainActivity 实例
// 以及当前类对象
invoke-static {v0, v1}, Lcom/example/demo/MainActivity;->access$test2(Lcom/example/demo/MainActivity;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;

// 将上面 access$test2() 执行结果赋值给 v0
move-result-object v0

// 返回 v0,也就是返回 access$test2() 的执行结果
return-object v0
.end method

在这段代码的最开始会将参数赋值给对象的 result 属性,结合验证一节中的截图 $result 字段,看一下它的赋值,就可以明白为啥 $result 取到的是挂起函数的返回值了。


上面代码提到了 MainActivity 的静态方法 access$test2 方法,看一眼,代码更简单:

.method public static final synthetic access$test2(Lcom/example/demo/MainActivity;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
.locals 1
.param p0, "$this" # Lcom/example/demo/MainActivity;
.param p1, "$completion" # Lkotlin/coroutines/Continuation;

.line 16
// 直接执行 MainActivity 的 test2() 方法
invoke-direct {p0, p1}, Lcom/example/demo/MainActivity;->test2(Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
// 同时将 test2() 的返回值直接返回
move-result-object v0

return-object v0
.end method

目前可知 test2() 由 invokeSuspend() 调用的,那该方法是由谁调用的呢?根据协程的基础知识可知,协程的恢复都是由它的 resumeWith() 开始的,该方法定义在 BaseContinuationImpl 中,如下:


Xnip2023-06-28_18-52-11.png


上图中会调用 invokeSuspend(),也就是调用本节分析的 invokeSuspend() 方法,最终会执行到 test2() 方法,拿到 test2() 的最终返回值。结合 while 死循环,最终会执行到 test3() 后面的步骤。


以上就是协程的挂起恢复流程,也说明了挂起函数的返回值为啥是 Object。


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

Android面试经:Broadcast需要注意哪些细节

前言 在android中,广播也是我们经常使用的组件,但是因为大部分使用场景简单,所以关注不多,今天就让我们来探索一下Broadcast。 注册 这个是常识了,两种注册方式:静态注册(menifast)和动态注册,不展开说了。 这里注意动态注册后,我们一般会手...
继续阅读 »

前言


在android中,广播也是我们经常使用的组件,但是因为大部分使用场景简单,所以关注不多,今天就让我们来探索一下Broadcast。


注册


这个是常识了,两种注册方式:静态注册(menifast)和动态注册,不展开说了。


这里注意动态注册后,我们一般会手动进行注销,不过如果没有手动注销,当context对象被销毁时,Broadcast会自动注销,但是我们还是及时注销释放资源。


线程及ANR


默认Broadcast都是运行在主线程,而且android对它的运行(onReceive)有一个时间限制——10秒,即ANR时间,所以不要在onReceive执行耗时操作。


但是Broadcast其实可以运行在其他线程(这时候就没有时间限制了),但是必须是动态注册才可以,Context的registerReceiver其实是一系列函数,其中就有

public abstract Intent registerReceiver(BroadcastReceiver receiver,
IntentFilter filter, @Nullable String broadcastPermission,
@Nullable Handler scheduler)

这里可以传入一个Handler,这个Handler就可以是其他线程创建的,这样就可以在其他线程运行Broadcast。


官方说明如下:



This method is always called within the main thread of its process, unless you


explicitly asked for it to be scheduled on a different thread using
{@link android.content.Context#registerReceiver(BroadcastReceiver, IntentFilter, String, android.os.Handler)}. When it runs on the main
thread you should
never perform long-running operations in it (there is a timeout of
10 seconds that the system allows before considering the receiver to
be blocked and a candidate to be killed). You cannot launch a popup dialog
in your implementation of onReceive().



那么既然onReceive中不能执行耗时操作,我们是否可以在onReceive中开启一个新的线程来处理?


在onReceive中开启新的线程,因为与其生命周期有关,所以下面与生命周期一起来说。


生命周期


Broadcast生命周期很简单,只有onReceive,当它在执行onReceive时是活跃状态,当执行完成则处于失活状态。根据网上资料:


拥有一个活跃状态的广播接收器的进程被保护起来而不会被杀死,但仅拥有失活状态组件的进程则会在其它进程需要它所占有的内存的时候随时被杀掉。 


而根据Broadcast的官方文档,当onReceive执行完这个Broadcast对象不再是alive状态,所以可以随时被回收销毁。所以不能在onReceive中进行异步操作,即开启新的线程,因为当onReceive执行完处于失活状态,它和这个新的线程可能随时被销毁,导致不可预计的程序问题。如果想在onReceive中执行一些异步操作,那么可以使用JobService,或者service。官方文档如下:



If this BroadcastReceiver was launched through a <receiver> tag,
then the object is no longer alive after returning from this
function.
This means you should not perform any operations that
return a result to you asynchronously. If you need to perform any follow up
background work, schedule a {@link android.app.job.JobService} with
{@link android.app.job.JobScheduler}.


If you wish to interact with a service that is already running and previously
bound using {@link android.content.Context#bindService(Intent, ServiceConnection, int) bindService()},
you can use {@link #peekService}.



所以说当Broadcast执行完onReceive后就可以随时被销毁了,当然动态注册不一样,因为它是手动创建的,所以还需要关心它的引用可达性。


同时,Broadcast的创建也一样,动态注册的时候我们手动创建,所以是一个对象。


而静态注册的时候,应该与activity等组件类似,(binder机制中)先通过intent条件查找创建Broadcast对象,经过测试每次都是重新创建。比如我们在menifast中静态注册一个Broadcast,然后通过一个按钮发送这个广播,在Broadcast的onReceive中打印自己的对象的toString,发现每次点击都是一个新的对象来执行。所以给Broadcast设置类变量,防止重复接收不会起作用,因为每次都是一个新的对象。


如果在onReceive中执行耗时操作,如下:

public class MyBroadcast extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Log.e("ssss", this.toString() + ":start");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.e("ssss", this.toString() + ":end");
}
}

再反复点击按钮发送广播,就会发现这些广播会按顺序执行,当上一个执行完才开始执行下一个(因为是在一个线程中)。


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

四个有用的Android开发技巧,又来了

大家好,本篇文章会继续给大家分享一些Android常见的开发技巧,希望能对读者有所帮助。 一. 通过堆栈快速定位系统版本 这个地方主要分享大家两个个技巧,通过问题堆栈简快速定位当前系统版本: 1. 快速区分当前系统版本是Android10以下,还是Androi...
继续阅读 »

大家好,本篇文章会继续给大家分享一些Android常见的开发技巧,希望能对读者有所帮助。


一. 通过堆栈快速定位系统版本


这个地方主要分享大家两个个技巧,通过问题堆栈简快速定位当前系统版本:


1. 快速区分当前系统版本是Android10以下,还是Android10及以上;


首先Android10及以上引入了一个新的服务Service:ActivityTaskManagerService,将原本ActivityMangerService原本负责的一些职能拆分给了前者,所以当你的问题堆栈中出现了ActivityTaskManagerService相关的字眼,那肯定是Android10及以上了



大家在Android9及以下的源码中是找不到这个类的。


2. 快速区分当前系统版本是Android12以下,还是Android12及以上;


这个就得借助Looper了,给大家看下Android12上Looper的源码:



Looper分发消息的核心方法loop(),现在会转发给loopOnce()进行处理,这个可是Android12及以上特有的,而Looper又是Android处理消息必要的一环,是咱们问题堆栈的源头祖宗,类似于下面的:



所以这个技巧相信还是非常有必要的:当你从问题堆栈中一看有loopOnce() 这个方法,那必定是Android12无疑了。


二. 实现按钮间距的一种奇特方式


最近看了一个新的项目代码,发现该项目实现按钮之间、按钮与顶部底部之间间距实现了,用了一种我之前没了解过的方式,于是这里分享给大家瞧瞧。


这里就以TextView和屏幕顶部间设置间距为例,初始的效果如下:



接下来我们来进行一步步改造:


1. 首先TextView是有一个自定义的xml背景:

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:height="70dp"
android:gravity="center_vertical">
<shape>
<solid android:color="#ff0000" />
</shape>
</item>
</layer-list>

核心就是定义了android:heightandroid:gravity这两个属性,来确保我们自定义背景在组件中的高度及居中位置。


2. 其次将布局中TextView的属性调整下:




  1. 首先height属性一定要调整为wrap_content保证最后TextView按钮的高度的测量最终取minHeight设置的属性值和背景设置的高度这两者的最大值




  1. 其次还要设置minHeight最小高度属性,注意一定要比背景设置的高度值大,保证能和屏幕顶部产生边距效果;




  1. 最后要设置字体的位置为垂直居中,保证字体位置和背景不发生错位


经过上面处理,效果就出来了:



其实上下空白的部分都是属于TextView,设置点击事件也会被响应,这算是其中的缺点之一,当前也可能在业务场景中认为这是一种合理表现。


上面实现的逻辑和TextView的测量逻辑密不可分,感兴趣的同学可以看下这块代码,这里就不带大家进行一一分析了:




三. logcat快速查看当前跳转的Activity类信息


忘了是在哪里看到的了,只要日志过滤start u0,就可以看到每次跳转的Activity信息,非常的有帮助,既不需要改动业务层,也不需要麻烦的安装一些插件啥的。


使用时记得将logcat右边的过滤条件置为,否则你就只能在左边切换到系统进程去看了:


这里我们演示下效果:


1. 跳转到Google浏览器



logcat界面会输出:



会打印一些跳转到包名类名等相关信息。


2. 跳转到系统设置界面



logcat输出:



可以说start u0还是相当好用的。


四. 项目gradle配置最好指向同一本地路径


最近开发中经常存在需要一次性检索多个项目的场景,而这样项目的gradle版本都是相同的,没啥区别。但每打开一个项目就得重新走一遍gradle下载流程,下载速度又是蜗牛一样的慢。


所以强烈建议大家,本地提前准备好几个gradle版本,然后通过设置将项目的gradle指向本地已存在好的gradle:



这样项目第一次打开的速度将是非常快的,而且按道理来说相同gradle版本的项目指向同一本地路径,也可以实现缓存共享。猜的


如果项目好好的编译运行着,突然没网了,可能会提示一些找不到依赖库资源啥的,其实你本地都已经缓存好依赖库资源了,只需要设置下off-mode,不走网络直接通过本地资源编译运行即可



总结


本篇文章主要是介绍了Android开发一些技巧,感觉都是项目中挺常用到的,算是我最近一个月收获的吧,后续准备研究研究compose了,毕竟看到大家们都在搞这个,羡慕的口水都流了一地了哈哈。


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

个人职业复盘-我的路

概述 从2009年毕业开始工作以来,至今已是第14个年头了,在这段漫长的职业生涯里,我干过好多件蠢事,也做对了一些事。 目前在一家公司,担任后端开发负责人,管着一个15人的技术团队。对我而言,这个是我当前的职业生涯里最亮眼的职位了。虽然团队不大,但是要对整个...
继续阅读 »

概述


从2009年毕业开始工作以来,至今已是第14个年头了,在这段漫长的职业生涯里,我干过好多件蠢事,也做对了一些事。


目前在一家公司,担任后端开发负责人,管着一个15人的技术团队。对我而言,这个是我当前的职业生涯里最亮眼的职位了。虽然团队不大,但是要对整个IT部门的系统稳定性负责,要承担的责任还是非常大的。


只要在IT公司做过几年程序员的都应该知道,要想走技术管理的路,并不是那么容易的,这里有运气的成分,也需要自己实力的加持。这篇文章就是想把自己如何走上技术管理之路的经历分享出来,供大家参考,为广大程序员尽一点绵薄之力。


文章的大纲分为如下几个部分:



  • 毫无成长的前五年;

  • 职业生涯转折点:进入了唯品会;

  • 第一次当技术组长;

  • 第一次当技术经理;

  • 第一次当后端技术负责人。


期间到底如何打怪升级的,请看下面的章节的详细内容。


毫无成长的前五年


前五年可以总结成一句话:



像一只无头苍蝇一样,到处乱撞,但一事无成。



这五年,待过了4家【传统行业的公司】,基本上一年一跳,其中两家还是外包公司。当时跳槽的原因只有一个,哪里钱多就往哪里去,就算只是涨幅2、3千块钱,我也去。完全没有任何的职业计划,现在回想起来了,已能知道为啥当时自己会那么做,原因就是:



没有良师益友的指导。



就是说没人帮你,没人带你,告知你应该如何珍惜前五年,如何做职业规划。人很容易困在自己的圈子里,认知很难突破的,如果没有高手帮忙拉你一下的话,你可能10年都是那个样子,一直碌碌无为,一直没有方向。


但毕竟工作五年了,为啥说毫无成长呢? 这里有几个原因。



  • 在外包公司(去过两家外包公司,待了两年多一些)的经历,基本可以忽略。因为做的东西都是企业级管理的后端系统,且也很难接触到真正的核心系统,也没有针对C端用户的大流量系统(就算有,也轮不到你来做),真的就是按照产品的需求做出来就行。团队不会严格要求你必须得有技术方案、技术设计,因为这种项目对技术的要求本身就很低。因此需求一来,火急火燎的,马上开工就行;

  • 虽然也待过两家非外包的公司,但是公司没什么名气,对技术人的要求也不高,还是需求一来,马上开工的那种状态。如何做设计,如何提升代码质量,代码如何写才是好代码,没人告诉你。


没做过好项目,周围也没遇到过高手,加上自己也没啥天赋,比较笨,就一直浑浑噩噩度过了五年。我是属于需要有人指导一下,才能开窍的那种。


经验教训:



在传统公司里做程序员,不太容易遇到好公司好项目好团队和好的人,你可能还自我感觉良好,觉得自己做的不错,但是在互联网的HR和面试官看来,你可能一文不值。这个时候的你,应该赶紧去找一下好公司,突破上去,上到一个新台阶。



职业生涯转折点:进入了唯品会


14年结婚后不久,老婆语重深长的说:



准备屡战屡败,屡败屡战,去互联网公司,把履历洗白一下,不然你以后的路会越来越难走的。



我当时是挺不屑的,她懂啥呀。但是听了她的分析后,我被说服了。



  • 工作五年了,履历毫无亮点。没在有名气的公司待过,也没做过有大流量的项目,还是一个普普通通的程序员;

  • 再过几年,就七八年工作经验了,但是你的技术跟年龄,完全不匹配呀。


可以说是当头一棒了,确实不能在这样下去。于是赶紧更新了简历,开始投递广州的一些互联网公司,开始走上了互联网公司求职之路。如上文的描述,我一直以为我的技术还是相当不错的,像Spring、Hibernate、Struts用的很熟练,但是去面了几次后,我才知道跟别人差距有多大。面试官一上来都是问JVM、高并发、高可用等一些技术点,我全都不会。


当时我是屡战屡败,屡败屡战。记得还连续去了广州一家知名互联网公司面试过4次,都没通过,每次都被打击的死死的。虽说每次都失败,但是也注意到了互联网公司到底都用一些什么技术,后面认真的准备了半年,啃了N本书,看了N个技术博客,写了N个技术Demo。终于在2015年成功进入了广州唯品会。虽然是降工资,降了两个等级过去的(我当时资深开发,但是去了唯品会,就变成普通开发,连高级开发都没有),但是我仍然愿意去。因为进入一家比较有名的互联网公司,当时对我来说实在太重要了。


2015年,唯品会刚好开始进行核心系统大重构,我刚好在公司的重构部完整的经历了整个过程,参与了几个核心系统的重构工作(其中一个商品项目,在唯品会2016年的大促里,流量是最高的,真的是百万级别的瞬时流量),都是从零开始弄的项目。整个过程下来,自己的JAVA技术、系统设计和架构、以及技术认知,都提高了好几个档次。也终于见识了,一个高并发、高可用的系统是怎么弄出来了,要经历过一些什么,要注意一些什么。记得在由于体会越来越多,也陆续写了很多博客,后来还成了CSDN技术博客专家。


期间由于表现还不错,晋升到高级开发(P3),刚晋升后不久,我就跟原来的leader提出说,能不能让自己带一个小团队,锻炼一下带队的能力。得到的回复是:



团队暂时没有这样的坑位。



但是带团队的经历对我来说是非常重要的,因为我当时的年龄已经不小了,技术上也没有什么特别的优势,但是项目管理的潜力还不错。最后没有办法,我只能提离职申请,离开待了快3年的唯品会。当时的离职不是一时冲动的,而是职业发展所需。



  • 年纪也30好几了,技术天赋也一般,走技术专家不合适我;

  • 对项目管理非常有兴趣也有一定的潜力;

  • 当时的团队暂时没有坑位;


基于上面几点,我又开始走上了寻求技术管理岗的求职之路。


经验教训:




  • 早期不要毫无目的的频繁换工作,很多中大型公司很在意这个的,会因为你频繁换工作而把你刷掉。因为职业发展所需才去跳槽才是对的;




  • 如果能进入到一家好的互联网公司,最好是去核心部门,学到的东西才多,成长才足够快;




  • 只要去过一家比较有名气的互联网公司,是能将你不好履历洗白的。这个对后面找工作非常有帮助;




  • 找一个合适的靠谱的女人结婚,对你的影响是一生的。




第一次当技术组长


以我当时的实力和背景,去大厂带团队是不可能的,只能去一些中小公司,但是这个没关系,只要能让我带团队就行,先入门先。


刚好当时唯品会有几个认识的同事离职了,去了深圳的一些创业公司做技术负责人或者架构师。当时有3家公司可以选择,也都面试通过了(有熟人推荐和介绍,确实成功率高很多)。但是只有其中一家有机会带一个7人小团队,加上当时的推荐人在那边是技术负责人,对技术这块有完全的话语权,我就果断了选择了这家。


这家创业公司是做小程序电商的,是做这块业务的头部公司,业务发展非常快。基本上所有的业务项目都是倒排期的,技术团队是没有任何的反驳的余地的。



经常出现大老板对外说,我们什么时候推出什么样的产品,而我和我带的小团队,一行代码都还没写呢?



因此如何按时交付倒排期大项目,成为当时我面临的最大挑战。当时是小团队,我也是参与写大量代码的(必须一直在一线),但同时得兼顾如下几个角色:



  • 架构师,主要是关键系统的设计和核心模块的开发;

  • 项目管理者,负责与产品和业务方沟通,并制定详细的项目计划,让大家能依计划行事;

  • 管理者,主要是团队的成长、绩效这块。


那刚开始顶得住吗? 肯定是顶不住呀,我经常因为赶工,睡公司沙发。能力还没修炼到家,那就只能拼时间了。


这个节奏一直持续接近两年,非常的辛苦,但是成长也是有的。具体如下:



  • 正儿八经的带过团队了,团队虽小,但也算五脏俱全了。团队管理遇到的一些问题也基本遇到了,这个就是我当时想要的,因为后续再去找工作的时候,我就是实打实的有带团队的经验了;

  • 项目管理能力得到飞速的提升,毕竟要经常应付紧急大项目。项目管理能力是技术管理者的一项重要的横向能力,是一定要懂的;

  • 在创业公司,业务交付是最重要的,技术的长期规划是不重要的。老板都不知道某块业务上线后又没有用呢,你IT团队操心个什么技术的长期价值呢?

  • 在创业公司带团队,自己的硬实力一定要有,随时能在一线大量的写代码;

  • 人脉对找工作极其关键,我能快速的入职这家公司并带一个小团队,也是因为在唯品会累积了一些人脉。他们有好的去向,你是可以跟着去的。


第一次当技术经理


后来这家创业公司业务发展不行了,开始裁员了,我也不得不考虑一下自己的将来,于是又借助人脉,去了一家餐饮公司,当时这家公司的CTO以前也是认识的,简单唠叨几句后,很快就接到了该公司的面试通知,由于自己有唯品会的重构经历也有创业公司的带队经历,整个面试过程还是很顺利的,拿到了一个技术经理的职位,负责中后台业务,团队是13人。为了能快速的融入这家公司的IT部门,我当时做了几件事情。



  • 通过一对一沟通,了解团队的瓶颈;

  • 积极的去一线处理线上问题,以便快速熟悉业务和了解应用系统的情况,同时也能让更多的人认识到我;

  • 大重构;


记得刚进入这家公司没几天,上级就提到,我负责的团队的情况不是很乐观,项目经常延期且跟其他职能团队配合的也不太好,让我想办法解决一下。言外之意就是说,这个团队不好管,让我多花点心思。由于之前已经有两年的带团队经历,深知跟团队成员一对一沟通的重要性,尤其是跟核心员工。


于是便找了团队的两个老员工,请他们吃饭,顺便了解团队的情况。当时其中一个员工情绪稍微有点激动:



团队负责的核心应用,大部分还是PHP写的,但是PHP开发只有3个,业务需求又多,根本忙不过来。而JAVA开发基本又没事干。



基本是一针见血了,这个是团队的真正问题所在。忙的人忙的要死,不忙的又闲的无聊。事后我跟上级反馈了这个问题并提出了解决思路:



必须进行核心系统的重构,除了提高稳定性外,也能极大的提高业务需求交付速度;



在得到上级的批准后,便紧锣密鼓的操办起重构来,那会的阻力很大,因为产品负责人觉得这样会阻碍业务需求的交付,而测试负责人则反馈,重构期间,测试人员得两边测试,极大的加大了测试工作量。没有办法,我只能把问题上升上去,直到CTO那边。最终CTO拍板,必须重构,并让我列出详细的重构计划。


于是乎中后台技术团队足足花了10多个月,把核心的应用全部从PHP转成JAVA。期间也出现了很多线上问题,但由于有完善的灰度和回滚方案,都在5分钟内恢复正常。我因为这个重构项目,在次年晋升为高级技术经理,而我的上司则晋升为技术总监,记得当时他去晋升的时候,CEO还特意说了句:系统最近稳定了很多。


期间为了能快速熟悉业务,我做了一件很疯狂的事:



我个人在线值班3个月,并作为线上问题的对接人。



无论是线下门店的问题还是线上的app和小程序问题,都可以直接找我。这个当然会占用我很大的一部分时间,但我认为值得,理由如下:



带着问题去询问或者查看代码,效率是非常高的,因为问题非常明确,就摆在那,你压根就不用去想,从哪个地方切入去熟悉业务,直接把当前的问题解决掉,就能了解到一点业务,通过长期的坚持的处理一个一个的问题,慢慢的从点到线到面的了解整体的业务。随着解决的问题越来越多,你会越来越了解当前的业务应用,有哪些模块,都是负责什么的,当前是怎么玩的,哪些是关键的地方绝对不能出问题的,哪些又是强依赖第三方的,渐渐的会对应用有个整体的认识。当然也会知道系统哪些地方需求去打补丁,哪些地方需要去做监控。业务熟悉了除了有助于自己快速的融入到团队里去,还可以为自己后续的任务合理分配和系统有效规划打好基础,当然最重要的还是有助于与团队内部、外部、上司进行良好的沟通,如果业务都不熟悉的话,很多事情都无法直接跟你谈。



没有这段处理线上问题的经历,我是无法很好的推动重构的。因为业务和周围的人你都不熟悉,别人也不了解你。


在这家公司除了做重构之外,也交付了很多能给公司带来GMV的业务需求,上司和CTO也都比较满意,基本上每年都给我涨了不少工资。在CTO眼中,能帮大忙的,钱就尽量给够。那几年公司发展的不错,中后台技术团队的业绩也很不错,每年的蛋糕分的都比较多,团队的人都很开心。


经验分享:



  • 要去做有挑战有难度的事情,只要做成了,团队就容易拿到好的成绩,对团队的稳定性是极其有用的。一定要从团队的角度出发来考虑问题,像当时如果没有做好重构项目,我估计团队就会散掉了;

  • 跟着你一个看好你支持你的上级以及CTO,超级重要。他会信任你保护你并授权给你,让你做重要的项目,让你出成绩;

  • 稳定性是技术团队保命用的,如果系统不稳定,随时会被干掉的;

  • 空降兵,一定不要急着去改变团队现状,一定要先融入,了解团队的人和事,找到那几个关键的问题,然后全力推动去解决;

  • 项目管理能力很重要,因为大老板很看重这个能力。能按期按节奏交付系统,是很重要的。作为技术管理者,必须掌握这个技能;


我在这家公司待了也2年半。


第一次当后端技术负责人


在2022年2月,我又跳槽去了一家公司,当时离开上家公司是没有办法的事情,因为原CTO离职了,公司新招了一个技术总裁,哇去,带来了N多老部下。几乎所有的核心老员工都需要走,包括我。


当时我心里就想着一个事,CTO是否稳定太重要了。


因此从那家餐饮公司离职后,陆续去面试了几家,每每到最后,我都会问面试官,CTO的背景是什么,是否稳定,因为我吃过亏了。


当时选择这家公司有如下几个原因:



  • 这个行业的资本很活跃,一直在投资公司。也即是资本看好这个行业,赛道足够大;

  • 公司是这个行业的头部公司;

  • 我应聘的职位是后端技术负责人,责任变得更加大了,同时title也升级了;

  • CTO背景非常好;


这次我没有借助人脉,完全靠自己的判断和职业诉求去找工作。对团队内部的情况并不了解,里面一个人都不认识。相当于是进入了一个完全陌生的环境。因此也踩了不少坑,导致在早期,我在这家公司,过的极其的辛苦。


首先是leader层以及技术总监的技术专业水平,强到完全超乎我的想象,他们在技术架构、技术规划、技术管理、项目管理,有一套极其严格的规范,且是他们在上家公司严格经过考验的。这个导致我很不习惯,因为有一堆的方法论在卡着你,我在前两家创业公司里,做事的时候,有时候是有点野路子的,当然这个也是被逼的,因为当时业务方压的太紧了。


但在这家公司里,业务需求是能停一停的。可以专注的去做技术项目,提高系统的稳定性。大家应该知道,如果纯做技术项目,对你的技术能力要求是非常高的,但我已经有两年多没写代码了,刚开始完全适应不了。


另外呢,他们的要求又极其的高,像技术方案、代码质量,code reivew,压测,技术架构合理性,没按照要求来做的,通通打回重搞。再加上我的上级是细节控,一路盯着你,哎,搞得我压力山大。我曾经一度怀疑自己,我还合适待在这家公司吗? 怎么感觉之前累积的东西,在这里完全没用了。


后面压力实在太大了,只能多次的敞开的跟上级和CTO聊,慢慢的,自己的心态才缓过来。那会我开始明白了一些事情。



  • 勇敢的承认自己的不足,不是什么丢人的事情。一定要敞开的跟老板聊,老板虽然严格,但是他会帮你的;

  • 心态要好,有些人的实力就是比你强大很多倍,在他眼中很容易做成的事情,你可能一直都无法做好或者说需要花费很大的精力才能完成。要能接受这种差距,并努力提高自己,慢慢减少差距;

  • 有些事情真的急不来,解决它就是需要花时间的。


在这家公司经历了12个月的磨练后,哇去,我自己成长真的是飞快,而且成长质量非常高,比以往任何一家公司都高。毕竟跟着一波高人做事情,成长是最好最快的。


当然期间你得能挨过去,中间我其实有离开的想法,但是后面还是坚持了下去,到目前为止,基本上迈过了这个坎了。


那么在这家公司我学习到哪些技能?



  • 对团队要高要求,团队成长才足够快。中间如果有人适应不了的,那就请他走;

  • 如何用最标准的方式,准备一场大促;

  • 如何管理自己的时间。这个是我在这家公司才真正掌握到的,做管理的,其实有一条暗线,那就是管理自己的时间。如果做的不好,你可能一路都被别人牵着走;

  • 如何高质量的从0到1,搭建应用系统。以前我都是短平快的把事情做成,在这边,学习到如何系统性、体系化、长期化的去思考和解决问题;

  • 做管理的,如果自己都没重点,那团队也就没有重点,这个是很危险的事情;

  • 如何从0到1的搭建研发流程体系。


小结一下的话,就是到目前为止,我学习到了一些技术总监才懂的技能。有一种从八路军到正规军的转变的感觉。


总结


提几个重点吧。



  • 跳槽要根据自己的职业规划来,每次跳槽都是往上跳。呈上升的趋势,不要搞降级跳,平级跳。我这边在后面几年就是按照这个思路来的。传统公司-->唯品会高级开发-->技术组长-->技术经理-->后端开发负责人;

  • 每次跳到一个新的台阶,你都会累到半死,只要你能挨过去,就能得到飞速的成长;

  • 跟着高手才能学习到好东西,要注意成长的质量;

  • 勇敢的承认自己的不足,不是什么丢人的事情;

  • 心态要好,要能接受差距,并努力提高自己,慢慢减少差距;

  • 团队管理,必须在实战中体会和掌握。

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

面试必问的三个问题,你怎么回答

现在已是四月中旬,企业招聘也快到了落幕时分。 相信参加校招的同学们都或多或少参加过面试。 今天学长就和大家盘点一下咱们在面试过程中一定会被问的问题。 01 请介绍一下自己 自我介绍是面试中非常关键的一步。很多面试官第一个问题都会问:能否请您做一下自我介绍? 因...
继续阅读 »

现在已是四月中旬,企业招聘也快到了落幕时分。


相信参加校招的同学们都或多或少参加过面试。


今天学长就和大家盘点一下咱们在面试过程中一定会被问的问题。


01 请介绍一下自己


自我介绍是面试中非常关键的一步。很多面试官第一个问题都会问:能否请您做一下自我介绍?


因为在自我介绍的时候,面试官通过自我介绍,可以考察到他们的语言表达能力、应变能力和岗位的胜任能力。


一般来讲,自我介绍的时间一般控制在 2-3 分钟,如果面试官有严格的时间要求注意不要超时。


那么,在如此短的时间内,求职者该如何彰显出自己呢?应该说些什么?应该怎么去说?


准备好一段简洁而生动的自我介绍,突出自己的优势和特点,并结合应聘的工作职责进行陈述。


自我介绍没有技巧,最重要的就是真诚二字。


可以美化自己的经历但一定要基于事实,保证后续如果问到相关的问题能对答如流,而不是自己给自己挖坑。


最好是提前练习自我介绍,不要在面试的第一关就出现紧张忘词的情况,也不要过于死板。


学长为大家准备了一个自我介绍的模板,仅供参考!


首先,我有丰富的项目经历。比如我曾经在某公司实习,参与多个项目。


其次,我的协同合作能力也不错。比如我在校期间参加某某活动,对接多个部门。


最后,我有很强的专业技能。在实习和项目中有很强的动手能力,学习能力较强,能很快上手。


02 你对我们公司有什么了解?


在面试前,通过查看公司官方网站、新闻文章等信息,掌握公司的背景信息,对公司有基本的了解。


从你在调查收集的信息中,你可以选择在回答这个问题时应该提到的关键细节。


你需要特别注意那些单位重要的成就或他们即将发布的产品。


你可以重点谈谈公司的成就、公司历史上的重要里程碑。


以及与你的职业目标或个人目标相一致的品牌使命和价值观。


此外,你必须用一种真诚的方式表达你对加入公司的渴望。


03 说说你的缺点


有的同学可能想皮一下:我的缺点就是没有缺点。


面试官 Belike:


image.png


人无完人,每个人都会有大大小小的缺点。说点真实的小缺点可以适当加分,说明你能认识到自己的缺点。


HR 想通过这个问题看到你解决问题的能力,以及你个人为缺点的改正做了哪些努力。


说自己的缺点,要学会避重就轻。


假如你面试开发岗位,你可以说自己的领导力没有那么强。


要注意的是,这些问题都没有标准答案。


总之,在回答问题时,需要结合自己的经验和公司的实际情况作出恰当的回答。


同时要注意自己的表达方法和语言表达,保持积极、开放和真实的态度。


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

“丑小鸭”真的能变成“白天鹅”吗?

周末到了,忙碌了5天的小伙伴们,估计也没啥心思再看技术文章了吧。其实总写算法和源码解析的文章,我也有些腻了,今天的文章随便聊聊吧。 关于今天文章的主题,其实很多人都迷茫过,我也曾迷茫彷徨,尤其是看过《最强大脑》之后,更觉得自己平庸得简直无药可救了,怎么聪明人那...
继续阅读 »

周末到了,忙碌了5天的小伙伴们,估计也没啥心思再看技术文章了吧。其实总写算法和源码解析的文章,我也有些腻了,今天的文章随便聊聊吧。


关于今天文章的主题,其实很多人都迷茫过,我也曾迷茫彷徨,尤其是看过《最强大脑》之后,更觉得自己平庸得简直无药可救了,怎么聪明人那么厉害?我这完全没有跟这些人拼下去的资本和机会啊!


同样在学的时候,很奇怪的是,班级里总是会有胖胖憨憨的同学,总是会有打架很厉害的同学,也同时大概率都会有那种一上课就趴桌子睡觉,然后一考试就名列前茅的“奇才”。我高中就有这样的同学,我们戏称他为“觉主”(睡觉的“觉”,谐音教主)。


那么,面对着这些天才和大牛,我们怎么办呢?**总不能束手就擒,任其宰割吧!**当然不能这样了,一句话“凭什么!”。


为了能找到解决的办法,我们可以从问题的产生原因出发。以我们软件研发人员为例:技术团队里的牛人太多了,有的人架构思想深厚,有的人编码速度飞快,有的算法思维缜密,而反观自己,貌似都很平平,然后越发自卑,没有破解之道。那么,其实我们进入了“小巷思维”,即:我们用一维视角去看待问题了


一维是什么?简单说,就是一条线!比如:技术水平的高低,就是一条线。我们发现自己拼劲了全力,依然“只能用自己的极限去挑战对方的怠速”,拼不赢,怎么办?那么我们就增加自己的二维能力,而此时我们的能力就是一个面。


比如,我默默提升自己的英语能力。好!你就拥有了英语这个二维能力了。加上这个能力后,自己还是不能在人群中脱颖而出,怎么办?我们增加自己的三维能力。比如,学习演讲技巧,可以在公司内外进行技术宣讲。或者提升管理能力,让自己成为技术+管理的复合型人才……那好,我们现在的能力就从一个面,提升为了一个三维体了
file
还是没有感触?没关系。我们来看一下周围的世界,有太多太多采用“多维竞争”的例子了。以这几年我国新兴的国产品牌新能源车为例,除了汽车的基本功能属性外,我们跟你拼大屏幕、拼屏幕多、拼座椅舒适、拼车载KTV、拼车载影院、拼全方位的客户服务、拼车子长、拼车子宽……


在以上这些多维度的加持下,不得不说,当电车突破了发动机和变速箱的技术壁垒之后,我们的国产新能源车竞争力越来越高了。甚至已经让BBA等国际车企们措手不及了(看看宝马i3、奥迪e-tron、奔驰EQC等等合资或外企的新能源车销售量有多低、优惠折扣有多大就知道了)。
file
所以,归根结底,我们不要只盯着某个一维空间上自己又多“弱小”,而应该发掘自己在多维上面的优势。要谨记这句话——“天生我材必有用”,不要放弃自己,不要让自己躺平,要“支棱”起来。我们不是鄙视平凡——我们努力了,但是没有获得好的结果,迫使自己终归平凡,这种平凡是那种“凡事全力争取,结果顺其自然”的平凡,是自己释怀自己的平凡;而不去奋斗,不去争取,好吃懒做而使得自己平凡,这种才是真的会让周围人鄙视的平凡了。


多维度提升自己的能力,不要轻易放弃自己,牢记能力提升复利曲线,我们需要做的只是持续成长,当积累终归由量变转化为质变的时候,终会感叹——“回首向来萧瑟处,也无风雨也无晴”。


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

如何避免简历石沉大海!

背景 疫情当下,金三银四已经失去了原本的辉煌。在投了一段时间简历,仅收获寥寥几份回复,甚至自己看好的岗位始终没得到沟通消息后陷入迷茫。 这岗位是假的吧...... 唉~环境真的太差了...... 其实也不全归咎于环境,企业也还在招人,岗位还是有不少的。说直白一...
继续阅读 »

背景


疫情当下,金三银四已经失去了原本的辉煌。在投了一段时间简历,仅收获寥寥几份回复,甚至自己看好的岗位始终没得到沟通消息后陷入迷茫。


这岗位是假的吧......


唉~环境真的太差了......


其实也不全归咎于环境,企业也还在招人,岗位还是有不少的。说直白一些,只不过是你不合适,或者说你的简历不合适而已。


之前总是听人在强调简历的重要性,但是没有特别深刻的认识,最近因为人力不足只能自己去发布岗位,筛简历,参与了简历筛选工作后才发现:哦,简历原来这么重要!!!(痛只有在自己身上,才知道有多痛!别人说的很难让自己重视!)


关于在线简历


在线简历一定要认真写!在线简历一定要认真写!在线简历一定要认真写!


大家都知道,注册了招聘网站后就让你填写在线简历


很多人可能都没太关注,随便填一下就去看职位了。找到自己感兴趣(我觉得我可以)的岗位后,就发起了沟通。


企业微信截图_16806604679636.png


或者,过了一段时间准备找工作了,只认真更新了自己的附件简历,忽略了在线简历。


如果你有这种情况,那么你就得抓紧更新一下自己的在线简历吧!!




原因: 在招聘平台,以BOSS ZP为例,求职者向职位发布者发起沟通后,职位发布者仅能查看在线简历!!! 而且在线简历在一开始真的巨多, 也就是说,你在线简历随便写写,没有抓住发布者眼球,那就只能先拜拜了。


当然,有些硬性要求也会导致筛选直接被否掉,比如学历,稳定性......


关于沟通


前边我们有提到,找到自己感兴趣(我觉得我可以)的岗位后,就发起了沟通。这个沟通,真的就只是一句话的沟通,在简历太多的情况下,很容易被忽略,要吸引发布者查看自己简历其实很简单:多发几条,让他知道你真的有关注,比如:


image.png


如果能够介绍一下自己与岗位的匹配点那就更好了:


image.png


关于简历内容


此时不出意外,岗位发布者就要点开你的在线简历查看了。这个时候内容为王,一定要在短时间内,让他觉得OK,可以聊一聊。


关于简历怎么写,已经有很多的文章说明了,这里就不多说。简单提两点:


工作经历部分,一定要简洁,还要能体现价值
项目经验部分,还是要简洁,挑重点,体现价值和亮点,当然相对工作经历来说要详细。 写的太多真的很难抓住重点。所以这里也可以根据情况,针对岗位单独做一份简历,重点突破。




做到前面三个步骤,基本上自己的简历不会被简历海淹没掉,有合适的岗位肯定是能够收到面试邀请的。至于面试的事情,那就后续再聊。


最后,祝愿找工作的人都能拿到满意的offer。


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

大龄,掘金,疫情,酒店,转型,前端满两年,搞公司后端两个月,年后离职还是继续等待?

大家好,我是 那个曾经的少年回来了。10年前我也曾经年轻过,如今已步入被淘汰的年龄,但现在幡然醒悟,所以活在当下,每天努力一点点,来看看2024年的时候自己会是什么样子吧,2024年的前端又会是什么样子,而2024年的中国乃至全球又会变成什么样子,如果你也有想...
继续阅读 »

大家好,我是 那个曾经的少年回来了。10年前我也曾经年轻过,如今已步入被淘汰的年龄,但现在幡然醒悟,所以活在当下,每天努力一点点,来看看2024年的时候自己会是什么样子吧,2024年的前端又会是什么样子,而2024年的中国乃至全球又会变成什么样子,如果你也有想法,那还不赶紧行动起来。期待是美好的,但是更重要的是要为美好而为之奋斗并付诸于行动。



喜欢的可以到创作者榜单点点我,估计也没几个人点我哈哈,自己点自己嘞


1、前言


就跟随着标题一个一个的来总结一下自己的2022吧,绝望中透露着一丝的希望,让我不得不在逆境中重生,寻找新的出路。


2、欠薪6个月


今年上了12个月的班,但是呢不算12月的工资,竟然还有6个月的工资没发,公司确实欠薪了,而且也非常的难受。怎么办呢?我自己也不清楚,过完年再说吧,希望年前最后一个月还能发点工资吧。


3、大龄


88年大龄前端:转行前端不到两年|2022年年中总结


这是我在2022年年中的时候总结的文章,那个时候计划2022年下半年输出大概16篇文章,而我下半年真正输出了46篇文章,当然其中有一部分是在我脚骨折只能在家卧床的时候写的,所以从时间上来看有一些水分,但是从完成任务的角度我还是超额完成的,我对自己的表现非常满意,哈哈哈。


大龄也许就是一个分水岭,有的人踏过去了,也有的人就此放弃了,还有的人根本不当回事,那么你又是哪一种呢?


大龄,没学历,没背景,没资源就只能躺平吗?反正我觉得如果真躺平了,那就是平了,而我选择了继续努力,每天保持不断的学习努力有所成长,就会得到满足,,哪怕一点点,也经得起长时间的积累。


4、掘金



  • 收获最多的地方
    1bed61531924d964bbf75dd5d12911f.jpg


这里应该是收获最多的地方,55篇这放在任何时候想都不敢想,万万没想到竟然能输出这么多,而且还收获了掘金非常多的礼物,在此感谢掘金,感谢川哥https://juejin.cn/user/1415826704971918, 不用想肯定是你认识的那个若川视野。


61da0551e864447baa877f208eb0f43.jpg


这里的礼物只是一部分,还有另外一部分,什么背包帽子,等等的每次收到都非常的开心。


324f7d177af92efe44023043cd25583.jpg


这个创作先锋将我个人还是非常的意外,也是不经意间老婆收到的快递,简直开心到起飞。



  • 去年在掘金的阅读


image.png


2021年一年可以说是入门前端,和众多刚毕业以及毕业一两年的前端的道友们一起在这里不断的收获,这里我个人点赞(共683篇)的文章大多都是研读的文章。



  • 今年在掘金的阅读


9e851faeebda2eed0f7e074f72d93d3.jpg


同时依靠掘金我的github也竟然有了200多的小星星,实属难得


image.png


这里顺便提一下极客时间的学习


0e79faf2e59a08ba062182d24596aed.jpg


212ec2c1481895c931dd57c9f9cbee8.jpg


只能说尽力学对自己有用的,充实自己,其实很多篇我都是反复看,看的自己明明白白的。不过确实也收获到了知识。


2022年一年可以说是入门后的腾飞,不断在掘金的引领下,让我在自我思考的摸索中寻找到坚定的方向。同时在川哥的带领下我也能看懂一点牛逼开源项目的源码了,这真的可以说是比较大的突破了。同时可以发现2022年的阅读量会更大一些,由于自己也会进行输出,在输出的过程中其实更需要对知识进行再三确认。


5、疫情,酒店,转型




  • 万万没想到就在现在此时此刻,全国所有人正在经历着,或者自己的至亲正在经历着,又或者自己身边的人正在经历着“鼻子封水泥、喉咙吞刀片、内脏咳出胸、”等症状,本来这篇文章准备在12月23日发出来的,但早上一醒来就进入炼狱般的状态了,昨天一天在头痛和发烧中度过的。




  • 由于公司主营业务便是服务于酒店业务,公司在2020年和2021年的收入有所影响,但总体可控影响不大。但是时间节点来到2021年年底以及2022年的全年,各种突发情况,慢慢的让公司的收入锐减。




  • 同时公司在2020年也有了初步的判断,需要拓展业务,才有了新的业务赛道,可能是由于决策和对新赛道的陌生,也使得前期大幅投入迟迟达不到预期,迟迟也没有收入,公司也由360多人,一度减员到8月份低谷时期,总人数不到80吧。




6、前端满两年




  • 从2020年9月25日入职公司,开始接触vue2,然后着手公司pc端:vue2+elementui,微信端h5:vue2+vant, 然后android app webview嵌套 vue2+vant,期间也接触了一个react项目




  • 2021年年初开始走上,vite+vue3+echarts大屏项目,相对于熟悉了解了vue2后,直接用vue2的语法来写是没问题的,然后慢慢的也在学习vue3+setup的语法,也将某些组件进行了转换




  • 2021年4月开始一个新的pc项目,采用了qiankun微前端,主应用使用vite+vue3,其他子应用采用vuecli+vue3 + element-plus,刚使用qiankun时,还是遇到了一些问题




  • pc端项目经过几个月的时间,陆续稳定上线,然后期间封装了pc端的json form表单生成器和json table列表生成器,这两个组件节省了很多PC端重复的工作,以及bug修改,感觉封装出来还是有点成就感的,我的前端兄弟都觉得非常的nice。




  • 搞pc期间还接触了leaflet、leaflet-geoman来给地图打点或者画区域,上手略有难度,但经过几天的摸索熟悉后,能够磕磕绊绊的将需要的功能实现出来了,使用过后感觉这个类库的功能还是非常强大的。




  • 2021年年底开始在原有android app webview的基础上增加新的功能,考虑到对vue3以及qiankun的熟悉,准备添加一个子应用,使用vue3+vant的模式来处理新增的业务功能




  • 此时可着手两个组件的封装,一个当然还是json form表单生成器的,逻辑上跟pc组件是类似的,只是换了一套vant的组件。另外一个相当于pc端的table列表,但是在移动端的h5当中每个列表的样式可能不同,就单独提取了一个模板,加速充血了一波,待组件稳定后,其实大致到了2022年的3月份了。




  • 2022年4月份的时候公司有一个专门数据采集的项目,最终要的功能便是用到了根据json生成form表单的并且对接通用接口,json的生成也是通过页面进行配置。其中难度比较大的便是数据的联动控制显示隐藏,以及数据校验、正则匹配、以及将部分js代码通过界面去编写,前端解析json后再动态执行js代码也是一个不小的难点。




  • 另外一个突破便是将vant 列表数据模板,做了两个通用的,根据SQL配置 接口返回通用的数据结构列表,去匹配模板列表。其实这里也有思考通过后台配置,拖拽元素实现列表的一行数据样式展示,但是在渲染的时候我是根据屏幕宽高比去进行等比的展示,但是发现样式会有所变形,主要是通过transform: scale(0.9) 计算出比例,然后填充数值,我猜测可能是我实现的方式还存在问题,等有时间再来看看,主要是我觉得这个思路好像是没问题的。




  • 期间5、6月份开始解决vue3 移动端中 列表到详情再返回列表,并且要记录当时的位置的问题,其实解决起来还是蛮麻烦的,当时查阅资料或者水平还不够,没能实现,但是线上的问题又必须要解决,于是硬着头皮看了一下vue3 keppalive组件的源码,其实还是看了蛮久的,看完解决完问题后,我还专门写了一篇小文,一不小心算是上了掘金的头条,真的非常开心。




  • 同时解决微信小程序中嵌套webview场景中的一些小问题,最主要的一个问题其实微信中打开h5页面,如果有使用到localstorage或者cookie,再在微信小程序中嵌套h5页面,那么会存在脏读的问题。我是通过根据window.navigator.userAgent.toLowerCase() 先判断其中是否包含 'miniprogram',有则代表是在微信小程序中,再判断是否包含'micromessenger',有则代表是在微信环境中,这样针对每个环境去设置不同的key,然后在当前环境中使用当前的key就不会产生冲突了。




  • 2022年7月份意外脚骨折在家里呆了三个周吧,然后上下班打车两个月终于摆脱拐杖,不得不说真的是伤筋动骨100天呢。




  • 2022年8月和9月正常开始迭代新的需求和项目的bug修复,期间有指出有新的项目要开始了。由于自己自身的尴尬(原先前端由我来管理的,但是骨折期间和之后发生了一些令人不悦的事情,没办法我直接提出交出去吧),自己也不能闲下来,于是开始新项目的准备,前端我可以干,有时间了也开始参与后端的代码。




7、后端两个多月的时间了(从2022年10月至今)


之前使用过.net framework,而公司有个项目正好使用的是.net core,所以上手难度相对较小但由于很久没用,区别还是有的,,最大的区别当然就是跨平台了。于是在今年10月份开始接触.net core,这两个多月的时间下来对公司后端代码也算是有了更加深入的了解。之前的两年时间算是全部都花在了前端代码里。从我现在的角度来看后端,其实思路相对来说也非常的明确。




  • 熟悉操作linux常用的各种命令,因为要发布测试上线,服务器都是linux




  • 熟悉基础的后端代码,然后能够独立的实现CRUD增删改查




  • 熟悉mysql的基本操作,由于数据量比较大,所以对索引的使用也上了一个台阶,要不然严重影响接口的响应时间




  • 当然还有其他的但是目前来看还只算是皮毛,有待进一步的加强学习




8、年后离职还是继续等待?


关于这个问题其实自己思考过了,看年后一两个月的情况就可以快速决定了。没办法,从现在开始只能说我要时刻准备着,时刻准备让自己拥有更多的技能,能够让自己变得更加强大。


9、2023年计划


没有目标一切都将是空谈,给自己制定一个切实有效的目标,那么到了来年,可以跟随时间和需求的变化,再随时调整目标。


关于前端计划




  • 继续攻坚前端工程化




  • 继续攻坚前端组件的封装




  • 继续攻坚react的使用和深入,公司项目主要是vue3,自己玩无用武之地




关于后端计划




  • 微服务架构模式学习深入




  • 消息队列在项目各场景中灵活运用,比如先攻克一个rabbitmq




  • redis在项目中发挥桥梁的作用




  • mysql数据库如何在项目中发挥护城墙的作用,把好最后一道关卡




  • 项目整个架构相关的学习实战




所以最后争取吧,一年36篇小作文,也就是每个月三篇,目标不算远大,但好好的去完成也需要一些精力,关键是要对当前的自己要有用处。


10、总结




  • 35岁真的会被毕业吗?而且是会被永久毕业吗?如果身边的朋友、同学、又或者是同学的朋友、同事的朋友等等真的是大批量的都被毕业了,那么我才会觉得风险是真的来了。




  • 现在就是时刻准备着可能要发生的事情,企业如果真不行了,或者自己真的想换工作了,就提前准备不就完事了。




  • 说真的每天时间就那么有限,自从你有了家,有了娃,时间就如白驹过隙




  • 没什么负面情绪,如果有的话就转化为正面动力吧




  • 浅层的学习靠输入,深层的学习靠输出:通过几期的学习源码,能深刻感受到自己看一遍和写一遍真的是非常不一样




  • 兄弟们加油吧,也许在疫情的催化下底层人民过的将会更加艰苦,多关照一下家里的老年人




  • 在疫情的催化下我们也要重新考虑一下我们的工作和生活方式了




  • 喜欢的可以到创作者榜单点点我,估计也没几个人点我哈哈,自己点自己嘞


作者:那个曾经的少年回来了
链接:https://juejin.cn/post/7181095134758387773
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

既当产品又当研发,字节大哥手把手带我追求极致

在学校的时候,计算机相关专业的同学应该都或多或少都被“大作业”折磨过,没有为“大作业”熬过夜通过宵的大学生活可以说是不完整的。步入公司后才发现,校园里的“大作业”就像玩具一样,需求明确、解决方案明确、最终产品效果明确、甚至还有前人的作品可以参考,而在公司里要做...
继续阅读 »

在学校的时候,计算机相关专业的同学应该都或多或少都被“大作业”折磨过,没有为“大作业”熬过夜通过宵的大学生活可以说是不完整的。步入公司后才发现,校园里的“大作业”就像玩具一样,需求明确、解决方案明确、最终产品效果明确、甚至还有前人的作品可以参考,而在公司里要做的东西,上面说的特点至少有一个不具备,甚至通通不具备。


而我在字节实习的过程中,所经手的恰恰就是这么一个需求不明确、解决方案不明确、最终产品效果不明确的项目。整个过程中有过焦头烂额毫无进展的时刻也有过欲哭无泪的时刻,还好有我的mentor带着我一路披荆斩棘、过关斩将。


首先和大家讲一下项目背景,当时我在的组是视频会议移动端,经历了近三年大流感的洗礼,相信大家对于视频会议中可能遇到的各种问题如数家珍,包括但不限于没声了、没音了、没画面了、画面卡顿、画面不清晰、画面和语音不同步、同步屏幕时闪退等等等等。作为一个服务企业级的B端产品,出现以上问题时就可能会投诉,然后经过客户成功部门转手到运营再转手到研发这里,研发就需要判断分析一下究竟是我们产品的原因、还是客户本身设备的问题、或者是第三方环境的因素,当用户的量级上来后,这样的客诉就会很多,会严重占用oncall的研发人员的时间以及精力。


我的mentor,一个专注于解决问题、避免重复劳动的人,一个字节范我觉得有E+的人,一个虽然身处移动端但是前后端甚至网络也都会的人,觉得这样很不OK,应该有个工具,能够自动的分析出来客户究竟遇到了什么问题,分析不出来的再找研发进行排查。没有这个工具也不影响业务开发的进展,所以整个项目并不存在时间上的紧迫性,但是呢,有这个工具做出来后肯定会大大降低研发的开发时间,所以项目的必要性还是有的。于是,我作为刚入职的实习新人,这个项目就交给我来做了。


而我,一个还没有从校园中完全出来的新兵蛋子,说实话面对这样的场面是一脸懵逼的,对于要做啥、要怎么做可以说是一无所知,我的mentor在我入职后,让我先了解了解背景,第一周就带着我oncall了,让我知道都可能有样的客诉,手把手给我演示他们正常的排查问题的方式。先了解客户反馈的情况,然后捞出来客户对应时间的设备信息以及设备日志。


说实话,作为一个新人,或者说我本身对于项目有一种畏难心理,碰到一点难题就总是想着往后拖,或者摆烂先不管然后就搁置在一边不想再问津了,但是我的mentor是一个有着坚定信念的人,差不多就是见山开山,见水架桥这种,遇到问题会主动找到相关人员一起解决,可以说就是有那种主人翁,项目owner的意识。于是,我就跟在他的后面,和整个团队的不同角色沟通他们遇到问题时排查的思路,试图总结出来一种通用的流程。在过程中,难免有许多困难,我的第一反应是退缩,但是导师的第一反应是拉会拉上相关人员一起讨论,看看用什么方式可以解决。比如在如何确定设备日志和故障表现的映射关系时,先后调研了多种方式看看相关团队有没有类似的做法以及他们最后实现的效果,包括大数据机器学习、代码状态流转图、自定义规则引擎等多种方式,最后调研后决定采用自定义规则引擎的方式。在实现需求的时候,需要其他团队协作时,他总是直接向前提出自己的需求,而我向一个陌生人发消息之前总要做一些心理建设,总是在担心些什么、害怕些什么,而事实上大家都是打工人,谁也不比谁厉害多少,对方不配合那就拉+1进群一起看看,解决不了就向上暴露问题。


于是,导师披荆斩棘在前,我在后面跟着实现他的设想。我们很快就做出来了第一个版本。通过Python自动化下载设备日志,然后正则匹配筛选出含有特定标记的日志,并对他们的出现频率次数做出判断。因为Python是解释型的语言,所以可以把规则直接写成python语言,用eval或者exec函数进行执行。第一个版本做出来后,导师又积极的带着我去给其他人宣传我们的这个工具。然后根据他们的反馈继续进行相关改进,最后我离职前实现的效果就是@ 一个群里的机器人,告诉他出现问题的ID,他就能自动化的拉下来日志进行排查,然后告诉你他分析的结果,整个交互非常的方便。


一个成功的项目必须要有一个负责的owner,我的导师就向我展示了一个优秀的owner是如何一步步解决问题、排除项目中的难关,如今我也正式成为一名打工人,希望我也能早日如他一般自如的面对工作。


我是日暮与星辰之间,出道两年半的Java选手,相信时间的力量,一起成为更好的自己!


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

思考 | 公司活力

经常看到一些互联网公司,尤其是创业公司,宣传自己的年轻员工占比高,公司充满活力。久而久之,大家都开始习惯这套说辞,认为一家公司有没有活力的标志就是年轻人多不多。可是当我们说一家公司充满活力时,到底在说什么? 我们是在说这家公司员工的体力充沛么?肯定不是,毕竟我...
继续阅读 »

经常看到一些互联网公司,尤其是创业公司,宣传自己的年轻员工占比高,公司充满活力。久而久之,大家都开始习惯这套说辞,认为一家公司有没有活力的标志就是年轻人多不多。可是当我们说一家公司充满活力时,到底在说什么?


我们是在说这家公司员工的体力充沛么?肯定不是,毕竟我们不用比拼打螺丝的速度,也不用比拼抡大锤的力度。再说,抡大锤,小年轻们不一定比得过中年大叔呢。


我们是在说这家公司员工满面红光、笑容可掬么?那也不是,毕竟要论这两项,高速收费口的工作人员早把各位杀的片甲不留。


我们是在说这家公司盈利颇丰么?那更不是,毕竟谁也不会把中烟草和活力二字扯上关系。可是若论赚钱能力,互联网的各位在中烟草面前都是弟弟。


那我们到底在说什么?


想必各位都看到今年上半年AI圈的迅猛增势,这些公司中有创业公司,譬如OpenAI和Midjourney,也有巨头公司,譬如Nvidia和Google。它们都可以称得上充满活力,原因并非它们拥有年轻的员工,而是它们一直做着创新突破的工作,是真正把大家带入更好未来的公司。


所以当我们说一家公司充满活力时,并不是想说它的员工充满活力,而是它的产品、它的创新工作充满活力。


事实上,Nvidia和Google这类公司中,大龄工程师并不少,而且多数都是研发的主力。举个例子,在我接触过的Google工程师中(Runtime和ART团队居多),多数都拥有10年以上的工作经历。那这是否说明大龄工程师更具优势?难道国内的35岁理论要失效了?


问题的关键其实不在于工程师的年纪,而是工程师是否优秀。一个优秀的工程师25岁时表现优秀,等到50岁时就会变得卓越。而一个平庸的工程师,不论25岁还是50岁,都不会有多少突破性的工作。这就好比,不是老人坏了,而是坏人老了。你非要拿着年龄去衡量工程师的水平,多半会徒劳无功。


那这么说,年龄不是标准?其实年龄也是标准。因为优秀的工程师终归是少数,刚工作的时候泥沙俱下,分不清好坏。工作愈久,大浪淘沙的效应就愈明显。最终,那些少数优秀的大龄工程师都身居了要位,而剩下的那些,则频繁地活跃在求职市场。


国内职场令人吊诡的一点是,优秀的人才最终都走向了管理的位置,或者说,如果你走不到管理的位置,就不会被认定为优秀。这在技术领域其实是有严重问题的。专业人才得不到足够的重视和认可,站在背后指手画脚的人却被捧上了天,搞得大家都去研究PPT,学习向上管理,却忽略了创新不同于打仗,它主要来自于直接的实践,而非稳坐后方的指挥。


不过现实的残酷在于,创新只属于一小撮人。大多数的工作谈不上创新,只是时间和精力的耗散。不然为什么会有“人力资源”这个词?当把创新或者人的主观能动性从工作中剥离后,人便是资源,和矿产无异,而管理者的高贵就体现在如何支配这些资源。


我希望有一天,50岁的人也可以写代码,写出很漂亮的代码。毕竟,我们都有50岁的一天。


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

被一个问题卡了近两天,下班后我哭了。。。

写在前面 好像很久没有更文了,感觉有很多想写的,但却又不知道该写些什么了。。。 近阶段,整个人的状态都好,本计划这月想给自己充电,做一些自己想做的事,结果真的就是事与愿违吧。 好像每个人都一样,都是为了生活而疲于奔命,依然忙碌于各种事情之间。 整个过程 没经过...
继续阅读 »

写在前面


好像很久没有更文了,感觉有很多想写的,但却又不知道该写些什么了。。。


近阶段,整个人的状态都好,本计划这月想给自己充电,做一些自己想做的事,结果真的就是事与愿违吧。


好像每个人都一样,都是为了生活而疲于奔命,依然忙碌于各种事情之间。


整个过程


没经过深思熟虑的计划制定


两周前,组内同事想让我帮忙做冒烟测试脚本,原因是因为每次发版测试的时间耗时特别长,所以在结束批量测试工具的开发工作后,我便主动和领导请缨做冒烟测试脚本的开发工作。



和领导说,脚本开发需要5天,整个冒烟测试每次需要大约5分钟!



领导听完很吃惊,我自己说应该差不多吧。


迷之自信?


可能很多同学也会和我的领导一样吃惊,为什么?


系统发版后的回归测试,就测试场景和流程来看,工作量肯定不小,姑且不说技术问题,就业务流程的梳理就很费时间了。


而我却说整个过程只需要五天,可见我是多想证明自己了


其实不然,我自己还是有一些考量的,才说出五天,原因有两个:



  • 因为信任,所以备受期待,同事信任我,真的感觉自己被需要,并且想为团队贡献出一份自己的力量;

  • 因为之前做过测试环境的性能测试脚本,以为很多接口可以直接拿来就用(我天真了,因为改了不少,需要重做)。


理性永远在给感性收拾烂摊子


整个系统总共6个测试流程,也就是说我每天要完成1.2个流程的脚本开发。


我特别喜欢现在团队的氛围,第一天到下班点时,差一个模块就完成了一个流程。


所以在责任心的驱使下,心想加个班吧,今天能赶出来这个模块,明天其他的流程就能复用了。


一切看似很好,也正是这个模块把我彻底卡住了,我遇到了一个让我很抓狂的问题:



打个类比,比如发起申请接口,申请成功了,到领导审批,点击同意的时候报错,而发起申请这个接口却不报错,你在页面同样的操作,领导同意却是正常好用的。



被问题卡住,心态开始崩盘


这个问题,我反复查了近两天......


这期间我积极的找开发同事帮忙排查问题,并确认是否是我的入参不对导致节点数据不正确。


由于开发同事比较忙,能帮我排查问题的时间有限,所以只有在开发稍微有点时间,才能帮忙排查联调。


也正因为开发同事的尽心尽力帮忙,几次下来,让我感觉离问题根源好像又进了一步。


也知道为什么不能审批了,因为虽然请求成功了,但是没走业务逻辑,导致部分数据还是默认值,所以审批报错。


关于入参的排查,暂时告一段落了,因为数据状态不对,无法进行审批,意味着还是没有解决问题。


到这已经是第三天了,一个流程都没整完,感觉整个人都不好了,心态有点崩了......


于是向领导说明原因,领导了解后,并说先把耗时最长的做完,虽然没那么大压力,但是心里还是有些深深地自责。


我还是没忍住,终于哭了出来......


距离周五晚上发版测试还有两天,这个问题不解决,怎么也说不过去,心里一直憋着这个劲特别难受。


当时的想法,真的是谁能帮帮我,帮帮我行么?


但是我也不知道该找谁帮忙,谁又能帮助我?


为什么?说是业务问题吧?还不算?技术问题吧,入参还查不出来啥问题?真的就是进退两难!


因为开发太忙,实在没时间,暂时也没想到什么好的解决办法,我就先下班回了家。


把车停好后,习惯性地给女友打了电话,那天还是我的生日,再加上那阶段烦心事特别多,说着说着我哭了出来,突然感觉好无助而且很没用,最后彻底哭了出来,为什么就那么难?


我以为我很颓废,今天我才知道,原来我早废了。


因为烦心事特别多,导致整个人都不好了,哭出来后,感觉真的很舒服,而且整个人平和了许多。


没人能教你,只有自己能拯救自己


回到家后,搭建好环境,改用工具进行测试,使用jmeter+fiddler抓包开始,重新调接口来模拟测试,结果居然成功了,真的很意外,难道是我代码写的有问题?


第二天上班,我把自己代码接口调用及入参与昨天做好的jmeter脚本一一对照,发现入参一模一样,这让我产生了怀疑,是我封装的工具类有问题?


我代码走的HTTP协议,而jmeter脚本是HTTPS协议才成功的。


这让我想到,可能我的httpclient需要走HTTPS协议请求会让接口调用后,数据应该会正常显示吧。


有了思路,就开始找httpclient如何进行HTTPS请求的相关文章。


经过一番搜索,找到的重点都是围绕使用ssl和根证书的使用的代码片段,我又对httpclient底层封装进行改造,改造完再次使用封装工具类调用接口,结果还是数据状态不对,我真的彻底绝望了。


于是,我又去找到了强哥(我北京的同事),强哥说你干嘛自己封装,用hutool呀。


我照着强哥的思路,又去照着hutool中的工具类,开始写demo,逐一调用接口,结果竟然成功了,这让我欣喜若狂,真的好用。


于是,我对写好的demo,再次进行封装,也就是hutool中的工具类封装,封装好后,再次使用封装好的工具类调用,结果数据状态又不对了。


我真的服了,这是玩我吗?分开就好使,封装就不行。


有的同学说了,应该是你封装的有问题,那为什么其他模块都好用,就这个模块不行?


后来,我灵机一动,那就都对分开可用这部分代码进行简单封装,保证流程跑通就行,算是退而求其次的解决方法,虽然,它很low,但是能用。


也正因为这个临时解决方案,助力我在周五发版前成功的让同事用上了,一个流程的冒烟测试,跑完这一个流程仅需113秒,比手动回归快了近10倍的时间。


写在最后


整个过程让我记忆深刻,在此特别记录一下,真的是头一次被问题卡的这么难受,那种既生气,又干不掉难题的感觉,太难受了!


你有被难题阻塞,一直无法继续下去的情况吗?欢迎文末给我留言哦!


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

PC网站如何实现微信扫码登录

不管你运营什么类型的网站,用户注册都是很重要的一个环节,用户注册的方式也是很多的,比如邮箱注册、手机号注册、第三方授权登录等。其中,第三方授权登录是最常用的一种方式,微信扫码登录是其中的一种,但是微信扫码登录的实现方式有很多种,比如公众号扫码,小程序扫码,网页...
继续阅读 »

不管你运营什么类型的网站,用户注册都是很重要的一个环节,用户注册的方式也是很多的,比如邮箱注册、手机号注册、第三方授权登录等。其中,第三方授权登录是最常用的一种方式,微信扫码登录是其中的一种,但是微信扫码登录的实现方式有很多种,比如公众号扫码,小程序扫码,网页扫码等。


本文将介绍一种简单的实现方式。


技术栈



  • 后端:NodeJs / 企业级框架 Egg.js

  • 前端:Vue

  • 微信小程序:uni-app

  • 数据库:MySQL


实现思路



  1. PC 端网站生成一个二维码,定时 3s 轮询请求接口,判断用户是否扫码,如果扫码,则返回用户的微信信息。

  2. 用户微信扫码后,会跳转到微信小程序,小程序打开点击注册按钮,会获取到用户的微信信息,然后将用户信息发送到后端。

  3. 后端接收到用户信息后,判断用户是否已经注册,如果已经注册,则直接登录,如果没有注册,则将用户信息 openid 和 mobile 保存到数据库中,新建用户,生成一个 token,返回给 PC 端,展示用户登录成功。

  4. 微信小程序展示用户扫码成功。


实现步骤




  • 需要申请一个微信小程序,用于扫码登录,申请地址:mp.weixin.qq.com/




  • 建表






  • PC 端网站生成二维码



实现效果如下:





  • 微信小程序扫码登录








  • 后端接口实现


路由:app/router.js




  • 生成带唯一 scene 参数的小程序码


app/controller/login.js




收起阅读 »

gradle 实用技巧

前言 总结一些日常开发中非常有用的 gradle 脚本、自定义功能实现。 实现 以下实现基于 AGP 8.0.2 版本 ,AGP 的 API 隔三岔五就会迎来一波破坏性的变更,导致脚本和插件无法使用,因此这里需要关注一下版本。 输出打包后 apk 文件路径及 ...
继续阅读 »

前言


总结一些日常开发中非常有用的 gradle 脚本、自定义功能实现。


实现


以下实现基于 AGP 8.0.2 版本 ,AGP 的 API 隔三岔五就会迎来一波破坏性的变更,导致脚本和插件无法使用,因此这里需要关注一下版本。


输出打包后 apk 文件路径及 apk 大小。


Android Studio 最新版本 Run 之后,每次输出的 apk 并没有在这 app/build/outputs 文件夹下(不知道 Android 官方是出于什么考虑要更改这个路径),而是移动到了 build\intermediates\apk\{flavor}\debug\ 目录下。为了方便后续快速找到每次运行完成的 apk ,可以在每次打包后输出 apk 文件路径及大小,从而可以关注一下日常开发过程中自己
的 apk 体积大概是一个什么样的范围。


static def getFileHumanSize(length) {
def oneMB = 1024f * 1024f
def size = String.valueOf((length / oneMB))
def value = new BigDecimal(size)
return value.setScale(2, BigDecimal.ROUND_HALF_UP)
}
/**
* 打包完成后输出 apk 大小*/
android {
applicationVariants.all { variant ->
variant.assembleProvider.configure() {
it.doLast {
variant.outputs.forEach {
logger.error("apk fileName ==> ${it.outputFile.name}")
logger.error("apk filePath ==> ${it.outputFile}")
logger.error("apk fileSize ==> ${it.outputFile.length()} , ${getFileHumanSize(it.outputFile.length())} MB")
}
}
}
}
}

apk fileName ==> app-huawei-global-debug.apk
apk filePath ==> D:\workspace\MinApp\app\build\intermediates\apk\huaweiGlobal\debug\app-huawei-global-debug.apk
apk fileSize ==> 11987818 , 11.43 MB

可以看到 apk 的路径在 build/intermediates 目录下。当然,我们可以通过下面的方法修改这个路径,定义成我们习惯的路径。


gradle 自定义功能的模块化


日常开发中,会有很多关于 build.gradle 的修改和更新。日积月累,build.gradle 的内容越来越多,代码几乎要爆炸了。其实,可以用模块化的思路将每一个小功能单独抽取出来,这样不仅可以减少 build.gradle 的规模,同时小功能可以更加容易的复用。


比如上面定义的输出打包后 apk 文件路径及 apk 大小的功能,我们就可以把他定义在 report_apk_size_after_package.gradle 这样一个文件中,然后在要使用的 build.gradle 中导入即可。


比如我们要在 app module 中使用这个功能,那么就可以直接在其 build.gradle 文件中按照相对路径引入即可。


gradle_dep.png


apply from: file("../custom-gradle/report_apk_size_after_package.gradle") // 打包完成后输出 apk 大小


修改 release 包的输出路径及文件名


输出 apk 后改名的需求,应该已经很普遍了。在最终输出的 apk 文件中,我们可以追加一些和代码相关的信息,方便通过 apk 文件名迅速确定一些内容。


def getCommit() {
def stdout = new ByteArrayOutputStream()
exec {
commandLine "git"
args "rev-parse", "--short", "HEAD"
standardOutput = stdout
}
return stdout.toString().trim()
}

def getBranch() {
def stdout = new ByteArrayOutputStream()
exec {
commandLine "git"
args "rev-parse", "--abbrev-ref", "HEAD"
standardOutput = stdout
}
return stdout.toString().trim()
}

def gitLastCommitAuthorName() {
return "git log -1 --pretty=format:'%an'".execute(null, rootDir).text.trim().replaceAll("\'", "")
}

def gitLastCommitAuthorEmail() {
return "git log -1 --pretty=format:'%ae'".execute(null, rootDir).text.trim().replaceAll("\'", "")
}


android {
def i = 0
applicationVariants.all { variant ->
if (variant.assembleProvider.name.contains("Debug")) {
// 只对 release 包生效
return
}

// 打包完成后复制到的目录
def outputFileDir = "${rootDir.absolutePath}/build/${variant.buildType.name}/${variant.versionName}"
//确定输出文件名
def today = new Date()
def path = ((project.name != "app") ? project.name : rootProject.name.replace(" ", "")) + "_" + variant.flavorName + "_" + variant.buildType.name + "_" + variant.versionName + "_" + today.format('yyyy_MM_dd_HH_mm') + "_" + getBranch() + "_" + getCommit() + "_" + gitLastCommitAuthorName() + ".apk"
println("path is $path")
variant.outputs.forEach {
it.outputFileName = path
}
// 打包完成后做的一些事,复制apk到指定文件夹
variant.assembleProvider.configure() {
it.doLast {
File out = new File(outputFileDir)
copy {
variant.outputs.forEach { file ->
copy {
from file.outputFile
into out
}
}
}
}
}
}
}

打 release 包后的日志


let me do something after assembleHuaweiGlobalRelease
apk fileName ==> MiniApp_huaweiGlobal_release_1.0.0_2306292226_2023_06_29_22_26_master_b0c6937_rookie.apk
apk filePath ==> D:\workspace\MinApp\app\build\outputs\apk\huaweiGlobal\release\MiniApp_huaweiGlobal_release_1.0.0_2306292226_2023_06_29_22_26_master_b0c6937_rookie.apk
apk fileSize ==> 4959230 , 4.73 MB

通过上面的日志,可以看到 MiniApp_huaweiGlobal_release_1.0.0_2306292226_2023_06_29_22_26_master_b0c6937_rookie.apk 包含了 ProjectName、flavor、debug/release、打包时间、分支、commitId 即最后一个 commitor 邮箱这些信息。通过这样的信息,可以更加方便快速的定位问题和解决问题。


妙用 flavor 实现不同的功能


使用 flavor 可以定制代码的不同功能及组合。不用把所有内容一锅乱炖似的放在一起搞。比如 MiniApp 随着演示代码的增多,已经逐渐丧失了 Mini 的定位,Apk 大小已经来到了 22 MB之多。究其原因,就是把所有代码验证和功能都放在一起导致的,音视频、compose、C++ 代码全都混在一起。部分功能不常用,但是每次为了验证一部分小功能,却要连带编译这些所有功能,同时打出的 apk 包体积也变大了,从编译到安装,无形中浪费了很多时间。


因此,可以通过 flavor 将一些不常用的功能,定义到不同的 flavor 中,真正需要的时候,编译相应 flavor 的包即可。


首先我们可以从 type 维度定义两个 flavor


    flavorDimensions "channel", "type"
productFlavors {
xiaomi {
dimension "channel"
}
oppo {
dimension "channel"
}
huawei {
dimension "channel"
}

global {
dimension "type"
}
local {
dimension "type"
}
}

在 type 维度,我们可以认为 global 是功能完整的 flavor,而 local 是部分功能缺失的 flavor 。那么具体缺失哪些功能呢?这就要从实际情况出发了,比如产品定义,代码架构及模块组合之类的。回到 Mini App 中,我们使用不同 flavor 的目标就是通过减少非常用功能模块,获得一个体积相对较小的 apk. 因此,可以做如下配置。


    if (source_code.toBoolean()) {
globalImplementation project(path: ':thirdlib')
} else {
globalImplementation 'com.engineer.third:thirdlib:1.0.0'
}
globalImplementation project(path: ':compose')
globalImplementation 'com.github.CarGuo.GSYVideoPlayer:GSYVideoPlayer:v8.1.8-release-jitpack'

如上我们只在 global 这个 flavor 依赖 thirdlib, compose, GSYVideoPlayer 这些组件。这样 local flavor 就不会引入这些组件,那么就会带来一个问题,local flavor 编译的时候没有这些组件的类,会出现找不到类的情况。


class_missing.png


对于这种情况,我们可以在项目 src 和 main 同级的目录下,创建 local 文件夹,然后在其内部按照具体 Class 文件的路径创建相应的类即可。


package com.engineer.compose.ui

import com.engineer.BasePlaceHolderActivity

/**
* Created on 2022/7/31.
* @author rookie
*/

class MainComposeActivity : BasePlaceHolderActivity()



package com.engineer.third

import com.engineer.BasePlaceHolderActivity

/**
* Created on 2022/7/31.
* @author rookie
*/

class CppActivity : BasePlaceHolderActivity()

package com.engineer

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.engineer.android.mini.ext.toast

/**
* Created on 2022/8/1.
* @author rookie
*/

open class BasePlaceHolderActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
"please use global flavor ".toast()
finish()
}
}

local_flavor.png


这里的思路其实很简单,类似于 leanCanary ,就是在不需要这个功能的 flavor 提供空实现,保证编译可以正常通过即可。缺什么类,建按照类的完整路径创建相应的 Class 文件,这样既保证了编译可以通过,同时在不需要次功能的 flavor 又减少无冗余的代码。


flavor 扩展


其实顺着上面的思路,基于不同 flavor 我们可以做更多的事情。基于 Java 虚拟机的类加载机制限制,相同的类只能有一个,因此我们无法做的事情是,通过 flavor 创建同名的类,去覆盖或重写其他 flavor 的逻辑,这种在编译阶段(其实是在创建同名类的阶段)就会报错。


但是,有些功能是可以被覆盖和定制的。比如包名、App 的名称、icon 之类。这些配置可以通过 AndroidManifest.xml/gradle 进行配置。可以在 local 目录下创建这个 flavor 特有的一些资源文件,这样就可以实现基于 flavor 的产品功能定制了。


比如最简单的修改 applicationId


        global {
dimension "type"
}
local {
dimension "type"
applicationId "com.engineer.android.mini.x"
}

这样,local 和 global 就有了各自不同的 applicationId, 这两种不同 flavor 的包就可以安装在同一台设备了。当然,现在这两个包的 label 和 icon 都是一样的,完全看不出区别。这里就可以利用 flavor 各自的文件夹,来定制各类资源和命名了。


flavor 过滤


不同维度的 flavor 会导致最终的 variant 变多。比如定义 product、channel、type 这些几个 dimension 的之后,后续新增的 flavor 会以乘积的形式增长,但是有些 flavor 又是我们不需要的,这个时候我们就可以过滤掉某些不需要的 flavor 。


比如以上面定义的 channel,type 这两个维度为例,在这两个维度下分别又扩展了 xiaomi/opop/huawei,global/local 这些 flavor 。按照规则会有 2x3x2=12 种 flavor,但实际情况可能不需要这么多,为了减少编译的压力,提升代码的可维护性,我们可以对 flavor 进行过滤。


    variantFilter { variant ->
println "variant is ${variant.flavors*.name}"
def dimens = variant.flavors*.name
def type = dimens[1]
def channel = dimens[0]
switch (type) {
case "global":
if (channel == "xiaomi") {
setIgnore(true)
}
break
case "local":
if (channel == "oppo") {
setIgnore(true)
}
break
}
}

这样我们就成功的过滤掉了 xiaomiGlobal 和 oppoLocal 的 flavor ,一下子就去掉了 4 个 flavor 。


基于现有 task 定制任务


再回顾一下上面的 修改 release 包的输出路径及文件名 的代码实现,我们是在打包完成之后进行了 apk 文件的重命名。


        // 打包完成后做的一些事,复制apk到指定文件夹
variant.assembleProvider.configure() {
it.doLast {
File out = new File(outputFileDir)
copy {
variant.outputs.forEach { file ->
copy {
from file.outputFile
into out
}
}
}
}
}

这里的 doLast 就是说,无论是否需要,每次都会在 assemble 这个 task 完成之后做一件事。这样在某些情况下显得非常的不灵活,尤其是当 doLast 闭包中要做的事情非常繁重的时候。这里的 copy 操作显然是比较轻量的,但是换做是其他操作,比如 apk 安全加固等操作,并不是每次必然需要的操作。这种情况下,就需要我们换一种方式去实现相应的逻辑了。


我们就以加固为例,一般情况下,我们需要对各个版本的 release 包进行加固。因此,我们可以基于现有的 assembleXXXRelease 这个 task 展开。


android {
applicationVariants.all { variant ->
if (variant.assemble.name.contains("Debug")) {
// 只对 release 包生效
return
}

def taskPrefix = "jiagu"
def groupName = "jiagu"
def assembleTask = variant.assembleProvider.name
def taskName = assembleTask.replace("assemble", taskPrefix)
tasks.create(taskName) {
it.group groupName
it.dependsOn assembleTask
variant.assembleProvider.configure() {
it.doLast {
logger.error("let me do something after $assembleTask")
}
}
}
}
}

添加上面的代码后,再执行一下 gradle sync ,我们就可以看到新添加的 jiagu 这个 group 和其中的 task 了。


jiagu.png


这里使用创建 task 的一种方式,使用 createdependsOn ,动态创建 task,并指定其依赖的 task 。


这样当我们执行 ./gradlew jiaguHuaweiLocalRelease 时就可以看到结果了。


> Task :app:assembleHuaweiLocalRelease
let me do something after assembleHuaweiLocalRelease

>
Task :app:jiaguHuaweiLocalRelease

BUILD SUCCESSFUL in 56s
82 actionable tasks: 12 executed, 70 up-to-date

可以看到我们自定义的 task 已经生效了,会在 assembleXXXRelease 这个 task 完成之后执行。


关于 gradle 的使用,可以说是孰能生巧,只要逐渐熟悉了 groovy 的语法和 Java 语法之间的差异,那么就可以逐渐摸索出更多有意思的用法了。


本文源码可以参考 Github MiniApp


小结


可以看到基于 gradle 构建流程,我们仅仅通过编写一些脚本,可以做的事情还是很多的。但是由于 groovy 语法过于灵活,不像 Java 那样有语法提示,因此尝试一些新的语法时难免不知所措。面对这种情况,去看他的源码就好。通过源码,我们就可以知道某个类有哪

作者:IAM四十二
来源:juejin.cn/post/7250071693543145529
些属性,有哪些方法。

收起阅读 »

手撸一个私信功能

web
前言 几年前的项目里写了一pc版的私信功能,使用的版本和代码比较老了, 这篇文章就直接粘了之前的代码简单的改了改,说明一下问题; 主要就是写一下这个功能如何下手,思想,以及界面如何整,消息怎么发等; 也只是截取了当时项目里私信的一部分功能,这个完全可以说明问题...
继续阅读 »

前言


几年前的项目里写了一pc版的私信功能,使用的版本和代码比较老了,

这篇文章就直接粘了之前的代码简单的改了改,说明一下问题;

主要就是写一下这个功能如何下手,思想,以及界面如何整,消息怎么发等;

也只是截取了当时项目里私信的一部分功能,这个完全可以说明问题了;


效果


界面大概是这样的
image.png


整体动态效果是这样的


test 00_00_00-00_00_30~1.gif


test1 00_00_00-00_00_30.gif


说下大致思路吧


首先是把界面分成左边和右边,左边占少一部分,是朋友目录界面;

右边占多一点,右边是聊天的详情界面;

点击左边对应的那个人,右边就会出现本人跟点击的那个人的聊天详情;


左边人员目录的思路


左边的人员目录和显示的头像,最新的一条消息还有时间,这些都是后端返给前端的;

前端把数据展现出来就行,

时间那里可以根据公司需求以及后端返回的格式转成前天,刚刚等根据需求而定;

我这块时间项目中是有分开前天,昨天,刚刚的,

只不过这里就自己造的数据时间随便写的;

当然这里数据多的时候,可做成虚拟滚动效果;
每个人头像那个红色是消息数量,当读完消息时,就恢复成剩下的消息数量;


右边聊天详情的思路


右边是左边点击对应的聊天人员时,

拿这个人的id之类的数据去请求后端,拿对应的聊天详情数据;

最下面的显示的是最新的聊天信息,后端给的排序不对,可自己反转去排序;

这里也做成虚拟滚动;

最上面显示的那个名称是当前和谁聊天的那个人的昵称;


image.png


聊天界面里也显示的是时间,昵称,头像,聊天信息内容,

时间也需要分昨天,前天,刚刚等。。。


发送消息的思路


我这里也做了按键和点击按钮两种方式;

按键就是在代码里添加一个键盘的监听事件就可;


    var footerTarget = document.getElementById('footer');
footerTarget.addEventListener('keydown', this.footerKeydown);

Enter按键是13;



//底部keydown监听事件
footerKeydown = (e) => {
if (e?.keyCode === 13) {
this.handleSubmit();
}
};

发送消息界面其实就是个表单,做成那个样子就可以啦;

发送消息时,调用后端接口,把这条消息添加在消息数据后面就可;


结尾


只是简单写下思路就已经写这么多了;

代码后面有空给粘上;

由于我是临时把几年前的代码拿出来粘的,

为了显示效果,数据也是自己造的,

一些时间呀以及显示,已读信息的数量呀以及其他一些细节都没有管,

实际项目中直接对应接口嘛,

所以这里就只是随便

作者:浅唱_那一缕阳光
来源:juejin.cn/post/7250029035744149541
改改说明一下问题哈;

收起阅读 »

什么是布隆过滤器?在php里你怎么用?

布隆过滤器(Bloom Filter)是一种用于快速判断一个元素是否属于某个集合的概率型数据结构。它基于哈希函数和位数组实现,可以高效地检索一个元素是否存在,但不提供元素具体的存储和获取功能。 布隆过滤器原理 上面的思路其实就是布隆过滤器的思想,只不过因为 ...
继续阅读 »

布隆过滤器(Bloom Filter)是一种用于快速判断一个元素是否属于某个集合的概率型数据结构。它基于哈希函数和位数组实现,可以高效地检索一个元素是否存在,但不提供元素具体的存储和获取功能。


image.png


布隆过滤器原理


上面的思路其实就是布隆过滤器的思想,只不过因为 hash 函数的限制,多个字符串很可能会 hash 成一个值。为了解决这个问题,布隆过滤器引入多个 hash 函数来降低误判率。


下图表示有三个 hash 函数,比如一个集合中有 x,y,z 三个元素,分别用三个 hash 函数映射到二进制序列的某些位上,假设我们判断 w 是否在集合中,同样用三个 hash 函数来映射,结果发现取得的结果不全为 1,则表示 w 不在集合里面。


image.png


布隆过滤器处理流程


布隆过滤器应用很广泛,比如垃圾邮件过滤,爬虫的 url 过滤,防止缓存击穿等等。下面就来说说布隆过滤器的一个完整流程,相信读者看到这里应该能明白布隆过滤器是怎样工作的。


第一步:开辟空间


开辟一个长度为 m 的位数组(或者称二进制向量),这个不同的语言有不同的实现方式,甚至你可以用文件来实现。


第二步:寻找 hash 函数


获取几个 hash 函数,前辈们已经发明了很多运行良好的 hash 函数,比如 BKDRHash,JSHash,RSHash 等等。这些 hash 函数我们直接获取就可以了。


第三步:写入数据


将所需要判断的内容经过这些 hash 函数计算,得到几个值,比如用 3 个 hash 函数,得到值分别是 1000,2000,3000。之后设置 m 位数组的第 1000,2000,3000 位的值位二进制 1。


第四步:判断


接下来就可以判断一个新的内容是不是在我们的集合中。判断的流程和写入的流程是一致的。


在PHP中如何使用?


在PHP中,可以使用BloomFilter扩展库或自行实现布隆过滤器。下面我将介绍两种方法。


1. 使用BloomFilter扩展库:


PHP中有一些第三方扩展库提供了布隆过滤器的功能。其中比较常用的是phpbloomd扩展,它提供了对布隆过滤器的支持。你可以按照该扩展库的文档进行安装和使用。


示例代码如下:


// 创建一个布隆过滤器
$filter = new BloomFilter();

// 向过滤器添加元素
$filter->add("element1");
$filter->add("element2");
$filter->add("element3");

// 检查元素是否存在于过滤器中
if ($filter->has("element1")) {
echo "Element 1 may exist.";
} else {
echo "Element 1 does not exist.";
}


2. 自行实现布隆过滤器:


如果你不想使用第三方扩展库,也可以自行实现布隆过滤器。下面是一个简单的自实现布隆过滤器的示例代码:


class BloomFilter {
private $bitArray;
private $hashFunctions;

public function __construct($size, $numHashFunctions) {
$this->bitArray = array_fill(0, $size, false);
$this->hashFunctions = $numHashFunctions;
}

private function hash($value) {
$hashes = [];
$hash1 = crc32($value);
$hash2 = fnv1a32($value);

for ($i = 0; $i < $this->hashFunctions; $i++) {
$hashes[] = ($hash1 + $i * $hash2) % count($this->bitArray);
}

return $hashes;
}

public function add($value) {
$hashes = $this->hash($value);

foreach ($hashes as $hash) {
$this->bitArray[$hash] = true;
}
}

public function has($value) {
$hashes = $this->hash($value);

foreach ($hashes as $hash) {
if (!$this->bitArray[$hash]) {
return false;
}
}

return true;
}
}

// 创建一个布隆过滤器
$filter = new BloomFilter(100, 3);

// 向过滤器添加元素
$filter->add("element1");
$filter->add("element2");
$filter->add("element3");

// 检查元素是否存在于过滤器中
if ($filter->has("element1")) {
echo "Element 1 may exist.";
} else {
echo "Element 1 does not exist.";
}


无论是使用扩展库还是自行实现,布隆过滤器在处理大规模数据集合时可以提供高效的元素存在性检查功能,适用于需要快速判断元素是否属于某个集合的场景。


作者:Student_Li
来源:juejin.cn/post/7249933985562984504
收起阅读 »

我工作中用到的性能优化全面指南

web
在Web开发中,Web的性能优化是一个重要的话题。无论是页面加载速度,用户体验,或者是程序运行效率,都与Web的性能优化息息相关。 最小化和压缩代码 在构建过程中,为了减少文件的大小和加载时间,通常会对JavaScript代码进行最小化和压缩处理。这包括移除...
继续阅读 »

在Web开发中,Web的性能优化是一个重要的话题。无论是页面加载速度,用户体验,或者是程序运行效率,都与Web的性能优化息息相关。



最小化和压缩代码


在构建过程中,为了减少文件的大小和加载时间,通常会对JavaScript代码进行最小化和压缩处理。这包括移除不必要的空格、换行、注释,以及缩短变量和函数名。工具如UglifyJS和Terser等可以帮助我们完成这个任务。


// 原始代码
function hello(name) {
let message = 'Hello, ' + name;
console.log(message);
}

// 压缩后的代码
function hello(n){var e='Hello, '+n;console.log(e)}

利用浏览器缓存


浏览器缓存是提升Web应用性能的一个重要手段。我们可以将一些经常用到的、变化不大的数据存储在本地,以减少对服务器的请求。例如,可以使用localStorage或sessionStorage来存储这些数据。


// 存储数据
localStorage.setItem('name', 'John');

// 获取数据
var name = localStorage.getItem('name');

// 移除数据
localStorage.removeItem('name');

// 清空所有数据
localStorage.clear();

避免过度使用全局变量


全局变量会占用更多的内存,并且容易导致命名冲突,从而降低程序的运行效率。我们应尽量减少全局变量的使用。


// 不好的写法
var name = 'John';

function greet() {
console.log('Hello, ' + name);
}

// 好的写法
function greet(name) {
console.log('Hello, ' + name);
}

greet('John');

使用事件委托减少事件处理器的数量


事件委托是将事件监听器添加到父元素,而不是每个子元素,以此来减少事件处理器的数量,并且提升性能。


document.getElementById('parent').addEventListener('click', function (event) {
if (event.target.classList.contains('child')) {
// 处理点击事件...
}
});

好的,下面我会详细解释一下这些概念以及相关的示例:


async 和 defer


asyncdefer 是用于控制 JavaScript 脚本加载和执行的 HTML 属性。



  • async 使浏览器在下载脚本的同时,继续解析 HTML。一旦脚本下载完毕,浏览器将中断 HTML 解析,执行脚本,然后继续解析 HTML。


<script async src="script.js"></script>


  • defer 也使浏览器在下载脚本的同时,继续解析 HTML。但是,脚本的执行会等到 HTML 解析完毕后再进行。


<script defer src="script.js"></script>

在需要控制脚本加载和执行的时机以优化性能的场景中,这两个属性是非常有用的。


防抖和节流


throttle(节流)和 debounce(防抖)。



  • throttle 保证函数在一定时间内只被执行一次。例如,一个常见的使用场景是滚动事件的监听函数:


function throttle(func, delay) {
let lastCall = 0;
return function(...args) {
const now = new Date().getTime();
if (now - lastCall < delay) return;
lastCall = now;
return func(...args);
};
}

window.addEventListener('scroll', throttle(() => console.log('Scrolling'), 100));


  • debounce 保证在一定时间内无新的触发后再执行函数。例如,实时搜索输入的监听函数:


function debounce(func, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func(...args), delay);
};
}

searchInput.addEventListener('input', debounce(() => console.log('Input'), 300));

利用虚拟DOM和Diff算法进行高效的DOM更新


当我们频繁地更新DOM时,可能会导致浏览器不断地进行重绘和回流,从而降低程序的性能。因此,我们可以使用虚拟DOM和Diff算法来进行高效的DOM更新。例如,React和Vue等框架就使用了这种技术。


// React示例
class Hello extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}

ReactDOM.render(<Hello name="John" />, document.getElementById('root'));

避免长时间运行的任务


浏览器单线程的运行方式决定了JavaScript长时间运行的任务可能会阻塞UI渲染和用户交互,从而影响性能。对于这类任务,可以考虑将其分解为一系列较小的任务,并在空闲时执行,这就是“分片”或者“时间切片”的策略。


function chunk(taskList, iteration, context) {
requestIdleCallback((deadline) => {
while (deadline.timeRemaining() > 0 && taskList.length > 0) {
iteration.call(context, taskList.shift());
}

if (taskList.length > 0) {
chunk(taskList, iteration, context);
}
});
}

chunk(longTasks, (task) => {
task.execute();
}, this);

虚拟列表(Virtual List)


当我们在页面上渲染大量的元素时,这可能会导致明显的性能问题。虚拟列表是一种技术,可以通过只渲染当前可见的元素,来优化这种情况。



虚拟列表的等高方式实现:



// 列表项高度
const ITEM_HEIGHT = 20;

class VirtualList {
constructor(container, items, renderItem) {
this.container = container;
this.items = items;
this.renderItem = renderItem;

this.startIndex = 0;
this.endIndex = 0;
this.visibleItems = [];

this.update();

this.container.addEventListener('scroll', () => this.update());
}

update() {
const viewportHeight = this.container.clientHeight;
const scrollY = this.container.scrollTop;

this.startIndex = Math.floor(scrollY / ITEM_HEIGHT);
this.endIndex = Math.min(
this.startIndex + Math.ceil(viewportHeight / ITEM_HEIGHT),
this.items.length
);

this.render();
}

render() {
// 移除所有的可见元素
this.visibleItems.forEach((item) => this.container.removeChild(item));
this.visibleItems = [];

// 渲染新的可见元素
for (let i = this.startIndex; i < this.endIndex; i++) {
const item = this.renderItem(this.items[i]);
item.style.position = 'absolute';
item.style.top = `${i * ITEM_HEIGHT}px`;
this.visibleItems.push(item);
this.container.appendChild(item);
}
}
}

// 使用虚拟列表
new VirtualList(
document.getElementById('list'),
Array.from({ length: 10000 }, (_, i) => `Item ${i}`),
(item) => {
const div = document.createElement('div');
div.textContent = item;
return div;
}
);


优化循环


在处理大量数据时,循环的效率是非常重要的。我们可以通过一些方法来优化循环,例如:避免在循环中进行不必要的计算,使用倒序循环,使用forEach或map等函数。


// 不好的写法
for (let i = 0; i < arr.length; i++) {
console.log(arr[i]);
}

// 好的写法
let length = arr.length;
for (let i = 0; i < length; i++) {
console.log(arr[i]);
}

// 更好的写法
arr.forEach(function (item) {
console.log(item);
});

避免阻塞UI


JavaScript的运行是阻塞UI的,当我们在进行一些耗时的操作时,应尽量使用setTimeout或Promise等异步方法,以避免阻塞UI。


setTimeout(function () {
// 执行耗时的操作...
}, 0);

使用合适的数据结构和算法


使用合适的数据结构和算法是优化程序性能的基础。例如,当我们需要查找数据时,可以使用对象或Map,而不是数组;当我们需要频繁地添加或移除数据时,可以使用链表,而不是数组。


// 使用对象进行查找
var obj = { 'John': 1, 'Emma': 2, 'Tom': 3 };
console.log(obj['John']);

// 使用Map进行查找
var map = new Map();
map.set('John', 1);
map.set('Emma', 2);
map.set('Tom', 3);
console.log(map.get('John'));

避免不必要的闭包


虽然闭包在某些情况下很有用,但是它们也会增加额外的内存消耗,因此我们应该避免不必要的闭包。


// 不必要的闭包
function createFunction() {
var name = 'John';
return function () {
return name;
}
}

// 更好的方式
function createFunction() {
var name = 'John';
return name;
}

避免使用with语句


with语句会改变代码的作用域,这可能会导致性能问题,因此我们应该避免使用它。


// 不好的写法
with (document.getElementById('myDiv').style) {
color = 'red';
backgroundColor = 'black';
}

// 好的写法
var style = document.getElementById('myDiv').style;
style.color = 'red';
style.backgroundColor = 'black';

避免在for-in循环中使用hasOwnProperty


hasOwnProperty方法会查询对象的整个原型链,这可能会影响性能。在for-in循环中,我们应该直接访问对象的属性。


// 不好的写法
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
console.log(key + ': ' + obj[key]);
}
}

// 好的写法
for (var key in obj) {
console.log(key + ': ' + obj[key]);
}

使用位操作进行整数运算


在进行整数运算时,我们可以使用位操作符,它比传统的算术运算符更快。


// 不好的写法
var half = n / 2;

// 好的写法
var half = n >> 1;

避免在循环中创建函数


在循环中创建函数会导致性能问题,因为每次迭代都会创建一个新的函数实例。我们应该在循环外部创建函数。


// 不好的写法
for (var i = 0; i < 10; i++) {
arr[i] = function () {
return i;
}
}

// 好的写法
function createFunction(i) {
return function () {
return i;
}
}

for (var i = 0; i < 10; i++) {
arr[i] = createFunction(i);
}

使用Web Worker进行多线程处理


JavaScript默认是单线程运行的,但我们可以使用Web Worker来进行多线程处理,以提升程序的运行效率。


// 主线程
var worker = new Worker('worker.js');

worker.onmessage = function (event) {
console.log('Received message ' + event.data);
}

worker.postMessage('Hello Worker');

// worker.js
self.onmessage = function(event) {
console.log('Received message ' + event.data);
self.postMessage('You said: ' + event.data);
};

使用WebAssembly进行性能关键部分的开发


WebAssembly是一种新的编程语言,它的代码运行速度接近原生代码,非常适合于进行性能关键部分的开发。例如,我们可以用WebAssembly来开发图形渲染、物理模拟等复杂任务。


// 加载WebAssembly模块
WebAssembly.instantiateStreaming(fetch('module.wasm'))
.then(result => {
// 调用WebAssembly函数
result.instance.exports.myFunction();
});

使用内存池来管理对象


当我们频繁地创建和销毁对象时,可以使用内存池来管理这些对象,以避免频繁地进行内存分配和垃圾回收,从而提升性能。


class MemoryPool {
constructor(createObject, resetObject) {
this.createObject = createObject;
this.resetObject = resetObject;
this.pool = [];
}

acquire() {
return this.pool.length > 0 ? this.resetObject(this.pool.pop()) : this.createObject();
}

release(obj) {
this.pool.push(obj);
}
}

var pool = new MemoryPool(
() => { return {}; },
obj => { for (var key in obj) { delete obj[key]; } return obj; }
);

使用双缓冲技术进行绘图


当我们需要进行频繁的绘图操作时,可以使用双缓冲技术,即先在离屏画布上进行绘图,然后一次性将离屏画布的内容复制到屏幕上,这样可以避免屏幕闪烁,并且提升绘图性能。


var offscreenCanvas = document.createElement('canvas');
var offscreenContext = offscreenCanvas.getContext('2d');

// 在离屏画布上进行绘图...
offscreenContext.fillRect(0, 0, 100, 100);

// 将离屏画布的内容复制到屏幕上
context.drawImage(offscreenCanvas, 0, 0);

使用WebGL进行3D渲染


WebGL是一种用于进行3D渲染的Web标准,它提供了底层的图形API,并且能够利用GPU进行加速,非常适合于进行复杂的3D渲染。


var canvas = document.getElementById('myCanvas');
var gl = canvas.getContext('webgl');

// 设置清空颜色缓冲区的颜色
gl.clearColor(0.0, 0.0, 0.0, 1.0);

// 清空颜色缓冲区
gl.clear(gl.COLOR_BUFFER_BIT);

使用Service Workers进行资源缓存


Service Workers可以让你控制网页的缓存策略,进一步减少HTTP请求,提升网页的加载速度。例如,你可以将一些不常变化的资源文件预先缓存起来。


// 注册一个service worker
navigator.serviceWorker.register('/service-worker.js').then(function(registration) {
console.log('ServiceWorker registration successful with scope: ', registration.scope);
}).catch(function(error) {
console.log('ServiceWorker registration failed: ', error);
});

// service-worker.js
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open('my-cache').then(function(cache) {
return cache.addAll([
'/style.css',
'/script.js',
// 更多资源...
]);
})
);
});

self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request).then(function(response) {
return response || fetch(event.request);
})
);
});

使用内容分发网络(CDN)


你可以将静态资源(如JavaScript、CSS、图片等)上传到CDN,这样用户可以从离他们最近的服务器下载资源,从而提高下载速度。


<!-- 从CDN加载jQuery库 -->
<script src="https://cdn.example.com/jquery.min.js"></script>

使用HTTP/2进行资源加载


HTTP/2支持头部压缩和多路复用,可以更高效地加载资源。如果你的服务器和用户的浏览器都支持HTTP/2,那么你可以使用它来提高性能。


// 假设我们有一个HTTP/2库
var client = new Http2Client('https://example.com');

client.get('/resource1');
client.get('/resource2');

使用Web Socket进行数据通信


如果你需要频繁地与服务器进行数据交换,可以使用Web Socket,它比HTTP有更低的开销。


var socket = new WebSocket('ws://example.com/socket');

socket.addEventListener('open', function() {
socket.send('Hello, server');
});

socket.addEventListener('message', function(event) {
console.log('Received message from server: ' + event.data);
});

使用Progressive Web Apps(PWA)技术


PWA可以让你的网站在离线时仍然可用,并且可以被添加到用户的主屏幕,提供类似于原生应用的体验。PWA需要使用Service Workers和Manifest等技术。


// 注册Service Worker
navigator.serviceWorker.register('/service-worker.js');

// 检测是否支持Manifest
if ('manifest' in document.createElement('link')) {
var link = document.createElement('link');
link.rel = 'manifest';
link.href = '/manifest.json';
document.head.appendChild(link);
}

使用WebRTC进行实时通信


WebRTC是一种提供实时通信(RTC)能力的技术,允许数据直接在浏览器之间传输,对于需要实时交互的应用,如视频聊天、实时游戏等,可以使用WebRTC来提高性能。


var pc = new RTCPeerConnection();

// 发送offer
pc.createOffer().then(function(offer) {
return pc.setLocalDescription(offer);
}).then(function() {
// 发送offer给其他浏览器...
});

// 收到answer
pc.setRemoteDescription(answer);

使用IndexedDB存储大量数据


如果你需要在客户端存储大量数据,可以使用IndexedDB。与localStorage相比,IndexedDB可以存储更大量的数据,并且支持事务和索引。


var db;
var request = indexedDB.open('myDatabase', 1);
request.onupgradeneeded = function(event) {
db = event.target.result;
var store = db.createObjectStore('myStore', { keyPath: 'id' });
store.createIndex('nameIndex', 'name');
};
request.onsuccess = function(event) {
db = event.target.result;
};
request.onerror = function(event) {
// 错误处理...
};

使用Web Push进行后台消息推送


Web Push允许服务器在后台向浏览器推送消息,即使网页已经关闭。这需要在Service Worker中使用Push API和Notification API。


// 请求推送通知的权限
Notification.requestPermission().then(function(permission) {
if (permission === 'granted') {
console.log('Push notification permission granted');
}
});

// 订阅推送服务
navigator.serviceWorker.ready.then(function(registration) {
registration.pushManager.subscribe({ userVisibleOnly: true }).then(function(subscription) {
console.log('Push subscription: ', subscription);
});
});

// 在Service Worker中接收和显示推送通知
self.addEventListener('push', function(event) {
var data = event.data.json();
self.registration.showNotification(data.title, data);
});

通过服务器端渲染(SSR)改善首次页面加载性能


服务器端渲染意味着在服务器上生成HTML,然后将其发送到客户端。这可以加快首次页面加载速度,因为用户可以直接看到渲染好的页面,而不必等待JavaScript下载并执行。这对于性能要求很高的应用来说,是一种有效的优化手段。


// 服务器端
app.get('/', function(req, res) {
const html = ReactDOMServer.renderToString(<MyApp />);
res.send(`<!DOCTYPE html><html><body>${html}</body></html>`);
});

利用HTTP3/QUIC协议进行资源传输


HTTP3/QUIC协议是HTTP/2的后续版本,采用了全新的底层传输协议(即QUIC),以解决HTTP/2中存在的队头阻塞(Head-of-line Blocking)问题,从而进一步提高传输性能。如果你的服务器和用户的浏览器都支持HTTP3/QUIC,那么可以考虑使用它进行资源传输。


使用Service Worker与Background Sync实现离线体验


通过Service Worker,我们可以将网络请求与页面渲染解耦,从而实现离线体验。并且,结合Background Sync,我们可以在用户离线时提交表单或同步数据,并在用户重新联网时自动重试。


// 注册Service Worker
navigator.serviceWorker.register('/sw.js');

// 提交表单
fetch('/api/submit', {
method: 'POST',
body: new FormData(form)
}).catch(() => {
// 如果请求失败,使用Background Sync重试
navigator.serviceWorker.ready.then(reg => {
return reg.sync.register('sync-submit');
});
});

// 在Service Worker中监听sync事件
self.addEventListener('sync', event => {
if (event.tag === 'sync-submit') {
event.waitUntil(submitForm());
}
});

使用PostMessage进行跨文档通信


如果你的应用涉及到多个窗口或者iframe,你可能需要在他们之间进行通信。使用postMessage方法可以进行跨文档通信,而不用担心同源策略的问题。


// 父窗口向子iframe发送消息
iframeElement.contentWindow.postMessage('Hello, child', 'https://child.example.com');

// 子iframe接收消息
window.addEventListener('message', function(event) {
if (event.origin !== 'https://parent.example.com') return;
console.log('Received message: ' + event.data);
});

使用Intersection Observer进行懒加载


Intersection Observer API可以让你知道一个元素何时进入或离开视口,这对于实现图片或者其他资源的懒加载来说非常有用。


var images = document.querySelectorAll('img.lazy');

var observer = new IntersectionObserver(function(entries, observer) {
entries.forEach(entry => {
if (entry.isIntersecting) {
var img = entry.target;
img.src = img.dataset.src;
observer.unobserve(img);
}
});
});

images.forEach(img => {
observer.observe(img);
});

利用OffscreenCanvas进行后台渲染


OffscreenCanvas API使得开发者可以在Web Worker线程中进行Canvas渲染,这可以提高渲染性能,尤其是在进行大量或者复杂的Canvas操作时。


var offscreen = new OffscreenCanvas(256, 256);
var ctx = offscreen.getContext('2d');

// 在后台线程中进行渲染...

利用Broadcast Channel进行跨标签页通信


Broadcast Channel API提供了一种在同源的不同浏览器上下文之间进行通信的方法,这对于需要在多个标签页之间同步数据的应用来说非常有用。


var channel = new BroadcastChannel('my_channel');

// 发送消息
channel.postMessage('Hello, other tabs');

// 接收消息
channel.onmessage = function(event) {
console.log('Received message: ' + event.data);
};

使用Web Cryptography API进行安全操作


Web Cryptography API 提供了一组底层的加密API,使得开发者可以在Web环境中进行安全的密码学操作,例如哈希、签名、加密、解密等。


window.crypto.subtle.digest('SHA-256', new TextEncoder().encode('Hello, world')).then(function(hash) {
console.log(new Uint8Array(hash));
});

使用Blob对象进行大型数据操作


Blob对象代表了一段二进制数据,可以用来处理大量的数据,比如文件。它们可以直接从服务端获取,或者由客户端生成,这对于处理大型数据或者二进制数据很有用。


var fileInput = document.querySelector('input[type="file"]');
fileInput.addEventListener('change', function(event) {
var file = event.target.files[0];
var reader = new FileReader();
reader.onload = function(event) {
var contents = event.target.result;
processContents(contents);
};
reader.readAsArrayBuffer(file);
});

使用Page Visibility API进行页面可见性调整


Page Visibility API提供了一种方式来判断页面是否对用户可见。利用这个API,你可以在页面不可见时停止或减慢某些操作,例如动画或视频,从而节省CPU和电池使用。


document.addEventListener('visibilitychange', function() {
if (document.hidden) {
pauseAnimation();
} else {
resumeAnimation();
}
});

使用WeakMap和WeakSet进行高效的内存管理


在处理大量数据时,如果不小心可能会产生内存泄漏。WeakMap和WeakSet可以用来保存对对象的引用,而不会阻止这些对象被垃圾回收。这在一些特定的应用场景中,例如缓存、记录对象状态等,可能非常有用。


let cache = new WeakMap();

function process(obj) {
if (!cache.has(obj)) {
let result = /* 对obj进行一些复杂的处理... */
cache.set(obj, result);
}

return cache.get(obj);
}

使用requestAnimationFrame进行动画处理


requestAnimationFrame能够让浏览器在下一次重绘之前调用指定的函数进行更新动画,这样可以保证动画的流畅性,并且减少CPU的使用。


function animate() {


// 更新动画...
requestAnimationFrame(animate);
}

requestAnimationFrame(animate);

使用CSS3动画替代JavaScript动画


CSS3动画不仅可以提供更好的性能,还可以在主线程之外运行,从而避免阻塞UI。因此,我们应该尽可能地使用CSS3动画替代JavaScript动画。


@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}

.myDiv {
animation: fadeIn 2s ease

-in-out;
}

避免回流和重绘


回流和重绘是浏览器渲染过程中的两个步骤,它们对性能影响很大。优化的关键在于尽可能减少触发回流和重绘的操作,例如一次性修改样式,避免布局抖动等。


var el = document.getElementById('my-el');
el.style.borderLeft = '1px';
el.style.borderRight = '2px';
el.style.padding = '5px';
// 尽量避免上面的写法,以下为优化后的写法
el.style.cssText += 'border-left: 1px; border-right: 2px; padding: 5px;';

使用CSS3硬件加速提高渲染性能


使用 CSS3 的 transform 属性做动画效果,可以触发硬件加速,从而提高渲染性能。


element.style.transform = 'translate3d(0, 0, 0)';

避免使用同步布局


同步布局(或强制布局)是指浏览器强制在 DOM 修改和计算样式之后,立即进行布局。这会中断浏览器的优化过程,导致性能下降。一般出现在连续的样式修改和读取操作之间。


let div = document.querySelector('div');

// 写样式
div.style.width = '100px';
// 读样式,导致同步布局
let width = div.offsetWidth;
// 再写样式
div.style.height = width + 'px'; // 强制布局

为避免这个问题,可以将读操作移到所有写操作之后:


let div = document.querySelector('div');

// 写样式
div.style.width = '100px';
// 写样式
div.style.height = '100px';

// 读样式
let width = div.offsetWidth;

使用ArrayBuffer处理二进制数据


ArrayBuffer 提供了一种处理二进制数据的高效方式,例如图像,声音等。


var buffer = new ArrayBuffer(16);
var int32View = new Int32Array(buffer);
for (var i = 0; i < int32View.length; i++) {
int32View[i] = i * 2;
}

利用ImageBitmap提高图像处理性能


ImageBitmap对象提供了一种在图像处理中避免内存拷贝的方法,可以提高图像处理的性能。


var img = new Image();
img.src = 'image.jpg';
img.onload = function() {
createImageBitmap(img).then(function(imageBitmap) {
// 在这里使用 imageBitmap
});
};
作者:linwu
来源:juejin.cn/post/7249991926307864613

收起阅读 »

我看UI小姐姐就是在为难我这个切图仔

web
前言 改成这个样子 咱也不懂啊,这样更好看了吗,只能照着改了,谁让我只是个卑微的切图仔呢. 实现过程 刚开始我觉得很简单嘛,封装一个组件,用它包裹表单元素,比如Input、 Select、DatePicker等,然后修改css样式,把表单元素的bord...
继续阅读 »

前言



image.png


改成这个样子


image.png


咱也不懂啊,这样更好看了吗,只能照着改了,谁让我只是个卑微的切图仔呢.


image.png


实现过程


刚开始我觉得很简单嘛,封装一个组件,用它包裹表单元素,比如Input、 Select、DatePicker等,然后修改css样式,把表单元素的border干掉,给外面的组件加上border不就搞定了,看起来也不是很复杂的样子.第一版长这样


image.png


发现问题了嘛,select下拉选项的宽度和表单元素不一样长,当然我觉得问题不大能用就行,但是在ui眼里那可不行,必须要一样长,不然不好看.
好吧,在我的据理力争下,我妥协啦,开始研究下一版.


image.png


在第一版的基础上我发现只有Select有这个问题,那就好办了,针对它单独处理就行了,解决方法思考了3种:



  • 第一种就是antd的Select可以设置dropdownStyle,通过获取父元素的宽度来设置下拉菜单的宽度,以此达到等长的目的

  • 第二种就是通过设置label元素为绝对定位,同时设置Select的paddingLeft

  • 还有一种就是通过在Select里添加css伪元素(注意这种方法需要把content里的中文转成unicode编码,不然可能会乱码)


最终我采用的是第二种方法,具体代码如下


import React, { CSSProperties, PropsWithChildren, useMemo } from 'react';
import { Form, FormItemProps, Col } from 'antd';
import styles from './index.module.less';

interface IProps extends FormItemProps {
label?: string;
style?: CSSProperties;
className?: string;
isSelect?: boolean;
noMargin?: boolean;
col?: number;
}
export const WrapFormComponent = ({ children, className, isSelect, style, col, noMargin = true, ...props }: PropsWithChildren<IProps>) => {
const labelWidth = useMemo(() => {
if (!isSelect || !props.label) return 11;
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
context!.font = '12px PingFang SC';
const metrics = context!.measureText(props.label);
return metrics.width + (props.colon === undefined || props.colon ? 10 : 0) + 11;
}, [isSelect, props.label, props.colon]);
return (
<Col span={col}>
<Form.Item
style={{ '--label-length': labelWidth + 'px', marginBottom: noMargin ? 0 : '16px', ...style } as CSSProperties}
className={`${styles['wrap-form']} ${isSelect ? styles['wrap-form-select'] : ''} ${className || ''}`}
{...props}
>

{children}
</Form.Item>
</Col>

);
};


less代码


.wrap-form {
padding: 0 !important;
padding-left: 11px !important;
border: 1px solid #c1c7cd;
border-radius: 4px;

:global {
.ant-form-item-label {
display: inline-flex !important;
align-items: center !important;
}

.ant-form-item-label > label {
height: auto;
color: #919399;
font-weight: 400;
}

.ant-picker {
width: 100%;
}

.ant-input,
.ant-select-selector,
.ant-picker,
.ant-input-number {
border: none;
border-color: transparent !important;
}

.ant-input-affix-wrapper {
background: none;
border: none;
}
}
}

.wrap-form-select {
position: relative;
padding: 0 !important;

:global {
.ant-form-item-label {
position: absolute;
top: 50%;
left: 11px;
z-index: 1;
text-align: left;
transform: translateY(-50%);
}

.ant-select-selector {
padding-left: var(--label-length) !important;
}

.ant-select-selection-search {
left: var(--label-length) !important;
}

.ant-select-multiple .ant-select-selection-search {
top: -2px;
left: 0 !important;
margin-left: 0 !important;
}

.ant-select-multiple .ant-select-selection-placeholder {
left: var(--label-length) !important;
height: 28px;
line-height: 28px;
}
}
}

最后就变成这样了,完美解决,这下ui总不能挑刺儿了吧.


image.png

收起阅读 »

这几个自学前端/编程的苦,你千万不要吃

今天我给你们分享一些前端自学路上,你会吃的苦,以及如何避免和解决。 这些让你吃苦的地方,是造成你前端学不好、并且学不下去的根本原因。 首先是, 一、学习的苦 一般建议你自学的人,会告诉你,自己到B站找个系统课,然后跟着学就行了。 这样跟你说的人,不是蠢就是坏。...
继续阅读 »

今天我给你们分享一些前端自学路上,你会吃的苦,以及如何避免和解决。


这些让你吃苦的地方,是造成你前端学不好、并且学不下去的根本原因。


首先是,


一、学习的苦


一般建议你自学的人,会告诉你,自己到B站找个系统课,然后跟着学就行了。


这样跟你说的人,不是蠢就是坏。


你这样做的结果,只会导致,你要学的内容非常多,并且又臭又长。


有很多人看到这么多要学的,第一想法就是放弃。


而一些有想法的呢,愿意逼着自己啃下去,大部分的结果是,看了好几十集就看不下去了。


我有学生就是这样,整个大一下学期,一直在B站看某个机构的前端课,看了八十多集就看不下去了,也不知道学到哪里才有成效。


我过去也是这样学的,因为没办法。


那时候的我,不知道怎么样的方式才是更好、更有效的,我只能逼自己把所有的先看一遍。


很多自学上岸的人,也都是这样做的,所以他们就算给你建议,也只会给你这一个。


我不清楚你们的学习效果如何啊,就我这么多年看到的情况来说,我见过非常多的人,


这里面有自学的,有培训出来的,但是哪怕他们工作了四五年,甚至有些七八年,依旧没有学明白。


什么叫没学明白?我给你举个例子。


HTML、CSS这两个东西,你去网上找,很多人告诉你要学一个月,实际上,这部分我总共就花了 10 分钟。


并且你学了这么久,让你自己去写东西,很多人依旧没思路,但是我这十分钟掌握的内容,我至今用了快十年,就没有搞不定的页面。


你学得时候理解困难,学完了又做不出东西,自然你就觉得学前端是一件很难的事情,你在这上面吃的苦越多,你越抗拒做这件事情。你越抗拒,自然就越做不好。


这部分如果继续展开讲,又有许多内容,我们留到下次再说。


接着我们来看你会吃的第二个苦,


二、拿不到结果的苦


学编程已经是最容易拿到结果的一个了,前端更是如此。


在计算机的世界里,你永远可以相信,只要你做对了,结果就是对的,如果结果不对,那一定是有某个地方你没做对。


并且计算机不需要什么细节操作,不会因为你打字打得快,键盘敲得响,你就能做对了。


真正有用的是,你能写出代码,完成你要的效果。你打字打得慢,只会影响你代码敲完的速度,但不影响你代码能够完成的效果,能明白我的意思吗?


但是对于刚开始学习前端的你来说,想要把代码写对,是一件非常不容易的事情。我没说它难啊,我说的是非常不容易。


这里的把代码写对,还不涉及到代码的逻辑,哪怕是你单纯的跟着视频里的代码去敲,也是一件不容易做对的事情。


我之前帮学员排查问题,就是单纯把该写“;”的地方,写成了“,”,“methods”写成了“method”。她自己看了一个小时多没整出来,我看一眼就解决了。


不要觉得这个问题有多简单啊,对于有经验的人,和正在听我讲的你来说,这是很容易发现的问题,但是对于正在学习的小白来说,是很难发现这些情况的。


我上大学的时候,有一次课程设计就是这样,我不小心把字母n,打成了字母b,坐在教室里,我找了将近一个上午没有找出原因。


这些让你得不到正确结果的情况,会打击你的学习兴致,还是那句话,越吃苦,越抗拒,越抗拒,越得不到结果。


这里呢,我给你们一个解决办法,那就是,


学会“一比一模仿”。能够完完整整的把你看的内容,给复刻出来。


这个方法,对于正在学习的你来说,就已经足够了。


那些你觉得厉害的人,也都是这么过来的。不要怀疑,现实的真相就是如此。


三、认知缺乏的苦


许多自学出来的人都觉得,别人都能像他一样,能解决自己遇到的任何问题。


我的学习能力,相比于大多数人来说,算是足够优秀的了,如果你关注我的时候够长,你就知道我没在说胡话。


但即使是学习能力优秀如我,依旧有不知道怎么解决的问题。


我曾经在大二的寒假,认为我只要努力学好 Java,就能跟着别人做项目。


于是我花了 3 天时间,每天坐在电脑前看视频 16~18 个小时,出了上厕所,就没离开过,包括吃饭。


你知道后面怎么了吗?


我迷茫了,下一步该做什么,我完全没思路。然后我就“摆烂”了一个星期,看了一周的火影忍者。


作为已经经历过这些阶段的人来说,事情已经变得很简单了。


就好比现在的我,如果回到那时候,我会主动去联系之前找过的老师,问他能不能跟着做项目了,或者下一步我该做什么。


但是身处那个阶段的我,是想不到可以这么做的。


这样的苦,是属于认知层面的苦。而认知这个东西,当你没跳出自己的认知范围时,你是不知道外面的世界是怎样的。


正在看这篇内容的你,已经比当初的我要优秀了,至少能想到去找解决方案,至少能刷到我这篇内容。


解决办法我也说了,


去网上、去相关的群里问;去搜相关的问题;等系统给你推荐优质的内容,都是一种方法。


四、结语


现在回过头来看,这些苦都不是什么大事情,熬一熬也能过去。所以这么多的人,才会推荐你说,自学前端就够了。


但是现在的我看法不一样了。


哪怕是回到过去,我也依旧会努力认识一个或多个优秀的前辈,向他们汲取经验。


这些苦我都吃过了,我知道能熬过去,但是这个熬过去的这部分时间,我本可以用来做更多有意义的事情。


如果你正在学习前端开发,我建议你一定要找一个,你信得过的前辈。


不要去那种“新人交流群”,那没意义,只不过是一群人在抱团取暖罢了。


要找就找一个高手,那种你的问题能够一点就透的,并且还能言简意赅的给你讲明白的。


我见过太多在群里交流了半天,得不到一个确定的解的。讨论了半天,远不如一位高手,

作者:Wetoria
来源:juejin.cn/post/7249943229289267258
一两句话就能讲明白。

收起阅读 »

档案真的很重要,它关乎你的未来,请一定保管好

看到那个妈妈拆那个学生的档案,只能一声叹息。 档案的用途 档案对你升学、考公、考编、办理退休等都非常重要,没有档案,你基本办不了。打个不恰当的比喻,你的档案就相当于是你的蘑菇云,可以不用,但必须保证它完好无损,放在它该放的地方。等用的时候你再折腾,晚了。 大学...
继续阅读 »

看到那个妈妈拆那个学生的档案,只能一声叹息。


档案的用途


档案对你升学、考公、考编、办理退休等都非常重要,没有档案,你基本办不了。打个不恰当的比喻,你的档案就相当于是你的蘑菇云,可以不用,但必须保证它完好无损,放在它该放的地方。等用的时候你再折腾,晚了。


大学毕业档案处置


你毕业的时候,有的学校会把你的档案直接发到你的生源地,有的学校会把档案交到你手里。当你手里拿到你档案的那一刻起,你的档案就是你现在最最最重要的事情,没有什么事情比他更重要,你必须确保它的完好无损。


如果你是大学生,你们老师让把档案存哪你就抓紧存哪去,比如你们当地人才市场、人社局,别磨蹭。档案在你手里放的时间太长会变成死档,很多地方是不接收死档的。如果以后你有需要,到时候你再着急,没用了。


高中毕业档案处置


如果你是高中生,拿到档案后不要随便一扔,一定要找个合适的地方保管好了。跟家里人说清楚这个东西谁都别碰,这个东西一旦损坏你就毁了,最好能找个地方把它锁好,等你上大学的时候,把它交给学校。


损坏后能修补吗


档案万一损坏了,你可以试着联系下发给你档案的地方,看看能不能重新密封。说实话,难!


当时给我们发档案的时候,我们老师跟我们说,如果你的档案万一被拆了,理论上你可以拿着你的档案找学校给你重新密封什么的,但这只是理论上。实际非常难,几乎就是不可能,跟你档案相关的所有单位都要核对一遍、盖章,他给你核对,他是要担责的,你的档案万一有问题,他怎么办?所以你千万不要想着我拆了咋了,再密封下不就行了

作者:程序员黑黑
来源:juejin.cn/post/7249930189181993020
,明确的告诉你不行。

收起阅读 »

附件类文件存储在环信和不存储在环信时的两种实现方式

场景一: 附件类文件存储在环信服务器使用环信EMFileMessageBody#getLocalUrl判断有没有本地文件; EMImageMessageBody imgBody = (EMImageMessageBody) message.getBody();...
继续阅读 »

场景一: 附件类文件存储在环信服务器

使用环信EMChatManager#downloadAttachment下载附件方案
(本篇文章以图片消息为例,其他附件类消息类似):

一、 通过EMFileMessageBody#getLocalUrl判断有没有本地文件;


EMImageMessageBody imgBody = (EMImageMessageBody) message.getBody();
//本地文件的资源路径
String imageLocalUrl = imgBody.getLocalUrl();



1 、 如果存在本地路径,直接使用本地文件;(本案例使用的Glide)

      Glide.with(this).load(imageLocalUrl).into(image);


2. 如果不存在本地文件,调用EMChatManager#downloadAttachment下载,下载成功后展示图片;(本案例使用的Glide)

EMMessage msg = EMClient.getInstance().chatManager().getMessage(msgId);
EMCallBack callback = new EMCallBack() {
public void onSuccess() {
EMLog.e(TAG, "onSuccess" );
runOnUiThread(new Runnable() {
@Override
public void run() {
Uri localUrlUri = ((EMImageMessageBody) msg.getBody()).getLocalUri();
Glide.with(mContext)
.load(localUrlUri)
.apply(new RequestOptions().error(default_res))
.into(image);
}
});
}

public void onError(final int error, String message) {
EMLog.e(TAG, "offline file transfer error:" + message);
}

public void onProgress(final int progress, String status) {
EMLog.d(TAG, "Progress: " + progress);
}
};
msg.setMessageStatusCallback(callback);
EMClient.getInstance().chatManager().downloadAttachment(msg);


二、 如果对本地存储的路径有特殊要求:

1 可以先通过EMFileMessageBody#setlocalUrl去修改路径;
2 然后再调用EMChatManager#downloadAttachment下载(下载操作可以参考上边);

EMImageMessageBody imgBody = (EMImageMessageBody) message.getBody();
//本地文件的资源路径
imgBody.setLocalUrl(localUrl);


场景二: 附件类文件存储在自己服务器

一、发送自定义消息时,携带文件存储的url;

EMMessage customMessage =EMMessage.createSendMessage(EMMessage.Type.CUSTOM);
// `event` 为需要传递的自定义消息事件,比如礼物消息,可以设置:
String event = "gift";
EMCustomMessageBody customBody = new EMCustomMessageBody(event);
// `params` 类型为 `Map`
Map params = new HashMap<>();
params.put("imageUrl","服务器的图片url");
customBody.setParams(params);
customMessage.addBody(customBody);
// `to` 指另一方环信用户 ID(或者群组 ID,聊天室 ID)
customMessage.setTo(to);
// 如果是群聊,设置 `ChatType``GroupChat`,该参数默认是单聊(`Chat`)。
customMessage.setChatType(chatType);
EMClient.getInstance().chatManager().sendMessage(customMessage);


二 、接收消息时,解析字段获取到url,进行下载;


@Override
public void onMessageReceived(List messages) {
super.onMessageReceived(messages);
for (EMMessage message : messages) {
EMCustomMessageBody emCustomMessageBody = (EMCustomMessageBody) message.getBody();
Map params = emCustomMessageBody.getParams();
String imageUrl = params.get("imageUrl");
}
}

收起阅读 »