注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

Android组件化 这可能是最完美的形态吧?

Android组件化的几种方式 一. 前言 Android开发为什么要组件化,有什么好处?可以看看之前的文章。 组件化的过程中其实都大同小异。结构与功能分为不同的层级: 各模块的跳转和业务通信通过路由转发: 这里讲一下常用的两种方案 二. 修改配置文件的...
继续阅读 »

Android组件化的几种方式


一. 前言


Android开发为什么要组件化,有什么好处?可以看看之前的文章


组件化的过程中其实都大同小异。结构与功能分为不同的层级:


各模块的跳转和业务通信通过路由转发:


这里讲一下常用的两种方案




二. 修改配置文件的方案


我们都知道组件Module是分为Application和library的:



  1. application属性,可以独立运行的Android程序,常见的App模块就是Application类型。

  2. library属性,不可以独立运行,一般是程序依赖的库文件。


那么我们就可以在跟gradle文件中配置,指定当前模块是否需要独立运行。


    isNewsFeedModule = true
isProfileModule = true
isPartTimeModule = true
isPromotionModule = true
isWalletModule = true
isYYPayModule = true
isYYFoodModule = true
isRewardsModule = true
isResumeModule = true
isFreelancerModule = true
复制代码

在指定的模块如NewsFeed模块中配置是否需要独立运行:


if (isNewsFeedModule.toBoolean()) {
apply plugin: 'com.android.application'
} else {
apply plugin: 'com.android.library'
}
复制代码

一个独立运行的application都是要有指定的appid的,那我们也得指定:


    defaultConfig {
(!isNewsFeedModule.toBoolean()){
applicationId "com.mygroup.newsfeed"
}
}
复制代码

还有可能独立运行和依赖库的方式,它们的清单文件有差异导致不同,那么还得指定清单文件的路径:


  sourceSets {
main {
if (isModule.toBoolean()) {
manifest.srcFile 'src/main/module/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
}
}
}
复制代码

最后,如果NesFeed模块是独立运行的,那么App模块不可能依赖一个Application吧。所以App的Build.gradle中也得修改:


   if (isNeedHomeModule.toBoolean ()){
implementation project (':newsfeed')
}
复制代码

这样每一次想修改对应的模块的时候,就去根目录配置文件修改,然后build之后就能生效。这应该是大多数开发者惯用的组件化方式了吧。




三. 使用框架来实现配置的升级


其实关于配置,关于ApplicationId,清单文件和application与library的判断,都是有迹可循,可以使用代码代替的,由此出现了不少组件化的框架来替我们完成重复的工作。


比较出名的如JIMU。再比如另一个比较火的组件化框架DDComponent,他们替你完成了很大一部分的工作。你只需要引用它的插件


apply plugin: 'com.dd.comgradle'
复制代码

指定他独立运行的applicationName就能实现组件化了


combuild {
applicationName = 'com.luojilab.reader.runalone.application.ReaderApplication'
isRegisterCompoAuto = true
}
复制代码

其中还自带路由,可谓是方便到家了。


但是一些痛点是,他们基于Gradle插件生成代码,由于AGP7的api有变动,有可能升级到AGP7之后出现问题。还有就是多模块的组合测试不方便,比如我想测试NewsFeed,这个模块中关联了很多Profile模块的东西,那我单独测试就要引入这2个组件,但是他们是平级的。也导致测试不方便,只能运行主app模块来测试。




四. 自定义单独的独立运行模块


我们不使用框架,直接把全部的模块都设置为library,由app模块依赖,我们单独的建立runalone的application类型模块,可以单独的调试ProFile模块 ,也可以添加NewsFeed和Profile模块一起测试。


由于app模块没有依赖runalone的模块,所以对应apk的大小和性能也没有影响,可以说单独用于调试是很方便的。


结构如下:


settings.gradle:


include ':app',
':cs_router',
':cs_baselib',
':cs_cptServices',

':cpt_auth',
':cpt_main',
':cpt_parttime',
':cpt_newsfeed',
':cpt_im',
':cpt_ewallet',
':cpt_profile',

':cs_ninegrid',

':lib_xpopup',

':standalone:parttimerunalone',
':standalone:authrunalone',
':standalone:ewalletrunalone',
':standalone:newsfeedrunalone',
':standalone:profilerunalone'
复制代码

优势:



  1. 同样实现了组件化隔离

  2. 不需要修改配置反复编译

  3. 不需要导入第三方库导致开发成本和容错率提高

  4. 方便不同平级的模块组合调试




内部路由功能的实现:


一些框架都是自带的路由,其实思想都是和ARouter差不多。其他单独的组件化框架也有很多,例如app-joint。另一种方案就是大家耳熟能详的ARouter


推荐大家使用Arouter,理由还是和上面一样,由gradle生成的代码有风险,AMS生成过程中依赖APG的api,一旦api有变动就无法使用。有可能升级到AGP7之后出现问题。


主要代码如下:


public class ARouterPath {

//App模块路由服务Path
public static final String PATH_SERVICE_APP = "/app/path/service";

//Auth模块路由服务Path
public static final String PATH_SERVICE_AUTH = "/auth/path/service";
//登录页面
public static final String PATH_AUTH_PAGE_LOGIN = "/auth/page/login";

//Main模块路由服务Path
public static final String PATH_SERVICE_MAIN = "/main/path/service";
//首页Main页面
public static final String PATH_MAIN_PAGE_MAIN = "/main/page/main";

//Wallet模块路由服务Path
public static final String PATH_SERVICE_WALLET = "/wallet/path/service";

//IM模块路由服务Path
public static final String PATH_SERVICE_IM = "/im/path/service";

//NewsFeed模块路由服务Path
public static final String PATH_SERVICE_NEWSFEED = "/newsfeed/path/service";

//PartTime模块路由服务Path
public static final String PATH_SERVICE_PARTTIME = "/parttime/path/service";

//Profile模块路由服务Path
public static final String PATH_SERVICE_PROFILE = "/profile/path/service";

//Service模块路由服务Path
public static final String PATH_SERVICE_SERVER = "/service/path/service";

}
复制代码

全局保管每个组件的Serivce对象


object YYRouterService {

var appComponentServer: IAppComponentServer? = ARouter.getInstance().navigation(IAppComponentServer::class.java)

var authComponentServer: IAuthComponentServer? = ARouter.getInstance().navigation(IAuthComponentServer::class.java)
...
}
复制代码

定义接口:


interface IAppComponentServer : IProvider {

fun initSMS(): IAppComponentServer

//Firebase短信服务-发送短信
fun sendSMSCode(
activity: Activity, phone: String,
sendAction: ((isSuccess: Boolean) -> Unit)?,
verifyAction: ((isSuccess: Boolean) -> Unit)?
)

//Firebase短信服务-验证短信
fun verifySMSCode(activity: Activity, code: String, verifyAction: ((isSuccess: Boolean) -> Unit)?)

fun gotoLoginPage()
}
复制代码

ARouter注解标注服务


@Route(path = ARouterPath.PATH_SERVICE_APP, name = "App模块路由服务")
class AppComponentServiceImpl : IAppComponentServer {

override fun initSMS(): IAppComponentServer {
return this
}

override fun sendSMSCode(
activity: Activity, phone: String, sendAction: ((isSuccess: Boolean) -> Unit)?, verifyAction: ((isSuccess: Boolean) -> Unit)?
) {

}

override fun verifySMSCode(activity: Activity, code: String, verifyAction: ((isSuccess: Boolean) -> Unit)?) {

}

override fun gotoLoginPage() {
LoginActivity.startInstance()
}

override fun init(context: Context?) {
}
}
复制代码

当然ARouter默认的页面导航也是能做的


@Route(path = ARouterPath.PATH_MAIN_PAGE_MAIN)
@AndroidEntryPoint
class MainActivity : YYBaseVDBActivity<MainViewModel, ActivityMainBinding>() {

companion object {
fun startInstance() {
val intent = Intent(commContext, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
commContext.startActivity(intent)
}
}
...
}
复制代码

至于为什么使用的是IProvide的方式来定义,是因为便于管理,每一个组件自己需要提供的服务或跳转由组件自己定义。没有完全的通过Activity的跳转来搭建路由,有可能你的应用不是基于Activity构建的呢?


基于单Activity+Fragment的构架的话,使用IProvide的方式也不会有影响。比如我们的项目就是把UI也组件化了,每一个组件都是Activity+多Fragment,总共8个组件就只有8个主要的Activity。


感谢看到这里,如果有不同意见,欢迎评论区讨论。

如果觉得不错还请点赞关注。后面可能会讲单Activity+多Fragment的几种方式。

好了,到处完结!



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

听说Jetpack WorkManager很难用?快来看这篇~

1、什么是WorkManager 按照官方描述,WorkManager 是适合用于持久性工作的推荐解决方案。如果工作始终要通过应用重启和系统重新启动来调度,便是持久性的工作。由于大多数后台处理操作都是通过持久性工作完成的,因此 WorkManager 是适用于...
继续阅读 »

1、什么是WorkManager


按照官方描述,WorkManager 是适合用于持久性工作的推荐解决方案。如果工作始终要通过应用重启和系统重新启动来调度,便是持久性的工作。由于大多数后台处理操作都是通过持久性工作完成的,因此 WorkManager 是适用于后台处理操作的主要推荐 API。


2、任务类型


WorkManager任务类型分为立即运行、长期运行和延期执行,使用方式与周期关系如下所示:






















立即一次性OneTimeWorkRequest 和 Worker。如需处理加急工作,请对 OneTimeWorkRequest 调用 setExpedited()。
长期运行一次性或定期任意 WorkRequest 或 Worker。在工作器中调用 setForeground() 来处理通知。
可延期一次性或定期PeriodicWorkRequest 和 Worker。

接下来来看具体的使用方法。


3、入门使用


3.1 添加依赖库


本文代码使用Kotlin编写,所以这里仅引入Kotlin相关的库即可,在build.gradle中添加代码如下所示:


def work_version = "2.7.1"
implementation "androidx.work:work-runtime-ktx:$work_version"

如果使用的是Java语言该如何引用呢?听我的,放弃吧~


3.2 定义工作Worker


这里我们以上传日志文件任务为例,新建UploadLogWorker类,继承自Worker,代码如下所示:


class UploadLogWorker(context: Context, workerParams: WorkerParameters) :
Worker(context, workerParams) {

override fun doWork(): Result {
Log.d("打印线程", Thread.currentThread().name)
return Result.success()
}
}

继承自Worker的类需要重写doWork方法,我们可以在这个方法中执行具体的任务,这里为了有演示结果打印出线程的名称。


Result用于返回任务的执行结果Result.success表示执行成功;Result.failure、Result.retry则分别表示执行失败和失败后尝试重试。


3.3 创建任务请求WorkRequest


这里我们创建一个一次性的执行任务,代码如下所示:


val uploadLogWorkerRequset: WorkRequest = OneTimeWorkRequestBuilder<UploadLogWorker>()
.build()

3.4 将任务提交系统


创建好任务之后,就可以将任务提交系统,执行请求,代码如下所示:


WorkManager.getInstance(this).enqueue(uploadLogWorkerRequset)

运行App,运行结果如下图所示。



3.5 为任务传递参数


许多时候我们在执行任务的时候是需要参数的,比如上传日志文件我们要知道日志文件的路径或者其他参数,我们怎么样将参数传递给Worker呢?


我们可以通过WorkRequest的setInputData方法来设置参数,代码如下所示:


val uploadLogWorkerRequset: WorkRequest = OneTimeWorkRequestBuilder<UploadLogWorker>()
.setInputData(workDataOf( "filePath" to "file://***" , "fileName" to "log.txt" ))
.build()

这里我们传递了文件路径filePath和文件名fileName,在Worker通过getInputData方法接受,比如我们在doWork中接受参数并打印。代码如下所示:


override suspend fun doWork(): Result {
val filePath = inputData.getString( "filePath" )
val fileName = inputData.getString( "fileName" )
Log.d( "接受的参数" , " $fileName : $filePath " )
return Result.retry()
}

运行程序,打印如下图所示。



这样我们就完成了一个最简单的WorkManager使用案例。接着我们来进一步的探索。


4、执行加急工作你所需要知道的


从 WorkManager 2.7 开始,我们可以调用setExpedited方法来告诉系统,我这个任务是加急任务,请尽快执行。修改代码如下所示:


val uploadLogWorkerRequset: WorkRequest = OneTimeWorkRequestBuilder<UploadLogWorker>()
.setExpedited(OutOfQuotaPolicy. RUN_AS_NON_EXPEDITED_WORK_REQUEST )
.build()

setExpedited方法中的OutOfQuotaPolicy参数有两个枚举值,含义如下所示。



















枚举值含义
RUN_AS_NON_EXPEDITED_WORK_REQUEST当系统无法为任务加急处理时,任务变成常规任务
DROP_WORK_REQUEST当系统无法为任务加急处理时,删除改任务

所以我们这里声明为RUN_AS_NON_EXPEDITED_WORK_REQUEST即可。再次运行程序。


OK,完美运行???


不过我的手机是Android 12的,为了确保没问题,我们必须在Android 11 或低版本上执行一次。没崩溃,但是任务却没执行,我们看到了错误日志如下图所示。



Emm.. 一堆乱七八糟的,关键信息在这句话


Expedited WorkRequests require a ListenableWorker to provide an implementation for `getForegroundInfoAsync()`

从官方我们获取到了这些信息:在 Android 12 之前,工作器中的 getForegroundInfoAsync()getForegroundInfo() 方法可让 WorkManager 在您调用 setExpedited() 时显示通知。如果您想要请求任务作为加急作业运行,则所有的 ListenableWorker 都必须实现 getForegroundInfo 方法。


如果未能实现对应的 ****getForegroundInfo 方法,那么在旧版平台上调用 setExpedited 时,可能会导致运行时崩溃。


了解到了这些,那我们就来实现getForegroundInfo()方法,修改UploadLogWorker代码如下所示:


class UploadLogWorker(context: Context, workerParams: WorkerParameters) :
    Worker(context, workerParams) {

    override fun doWork(): Result {
        Log.d("打印线程", Thread.currentThread().name)
        setForegroundAsync(getForegroundInfo())
        return Result.success()
    }

    @SuppressLint("RestrictedApi")
    override fun getForegroundInfoAsync(): ListenableFuture<ForegroundInfo> {
        val future = SettableFuture.create<ForegroundInfo>()
        future.set(getForegroundInfo())
        return future
    }


    fun getForegroundInfo(): ForegroundInfo {
        val notificationManager =
            applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel = NotificationChannel(
                "1",
                "hh",
                NotificationManager.IMPORTANCE_HIGH
            )
            notificationManager.createNotificationChannel(channel)
        }

        val notification = NotificationCompat.Builder(applicationContext, "1")
            .setSmallIcon(R.drawable.ic_launcher_background)
            .setContentTitle(applicationContext.getString(R.string.app_name))
            .setContentText("我是一个上传日志的任务")
            .build()
        return ForegroundInfo(1337, notification)
    }


}

再次在Android11 上运行程序,发现打印出了日志,并显示了一个任务通知,如下图所示。



这一点是在执行加急工作时所必须要注意的。


5、协程工作CoroutineWorker


1、将继承类修改为CoroutineWorker


2、实现getForegroundInfo方法,内容与上getForegroundInfo一致


6、定时任务PeriodicWorkRequest


在3.2中我们定义了一次性任务OneTimeWorkRequestBuilder,现在我们将上传日志的这个任务修改为定时任务,代码如下所示:


val uploadLogWorkerRequset: WorkRequest = PeriodicWorkRequestBuilder<UploadLogWorker>(15,TimeUnit.MINUTES)
.build()

这里指定了,定时任务的周期是15分钟一次,可以定义的最短重复间隔就是 15 分钟,这一点开发者在测试的时候需要注意,不能傻傻的等着...,这里我就傻傻的等了15分钟,确保定时任务是可以执行的。


7、工作约束、延迟执行和重试策略


7.1 工作约束


很多情况下,我们需要为任务添加工作约束,比如上传日志的任务肯定是在有网络的条件下进行的,当前支持的约束条件如下所示。



























NetworkType约束运行工作所需的网络类型。例如 Wi-Fi (UNMETERED)。
BatteryNotLow如果设置为 true,那么当设备处于“电量不足模式”时,工作不会运行。
RequiresCharging如果设置为 true,那么工作只能在设备充电时运行。
DeviceIdle如果设置为 true,则要求用户的设备必须处于空闲状态,才能运行工作。在运行批量操作时,此约束会非常有用;若是不用此约束,批量操作可能会降低用户设备上正在积极运行的其他应用的性能。
StorageNotLow如果设置为 true,那么当用户设备上的存储空间不足时,工作不会运行。

比如我们现在为一次性任务添加约束为在链接wifi的情况下执行,首先用Constraints构建一个约束实例可以将多个约束条件放在一起。代码如下所示:


val constraints = Constraints.Builder()
.setRequiresCharging(true)
.build()

这里设置为仅在充电的时候执行。接着为任务构建器添加约束。


val uploadLogWorkerRequset: WorkRequest = OneTimeWorkRequestBuilder<UploadLogWorker>()
.setConstraints(constraints)
.build()

这样一来任务就会在仅充电的时候执行了。


7.2 延迟执行


延迟执行适用于一次性任务和定时任务,但应用在定时任务事仅对第一次执行有效,为啥呢?因为是定时任务呀~


我们为一次性任务设置延迟时间为5秒钟,代码如下所示:


val uploadLogWorkerRequset: WorkRequest = OneTimeWorkRequestBuilder<UploadLogWorker>()
.setConstraints(constraints)
.setInitialDelay( 5 ,TimeUnit.SECONDS)
.build()

运行程序,可以看到5秒钟后,程序才打印了日志,这里就不演示了。


7.3 重试策略


在3.2中定义Work中我们提到了Result.retry可以让任务重试,我们也可以自定义任务的重试策略和退避政策,我们通过具体的例子来解释。


val uploadLogWorkerRequset: WorkRequest = OneTimeWorkRequestBuilder<UploadLogWorker>()
.setBackoffCriteria(
BackoffPolicy.EXPONENTIAL, OneTimeWorkRequest.MIN_BACKOFF_MILLIS,
TimeUnit.MILLISECONDS
)
.build()

最短退避延迟时间设置为允许的最小值,即 10 秒。由于政策为 LINEAR,每次尝试重试时,重试间隔都会增加约 10 秒。例如,第一次运行以 Result.retry() 结束并在 10 秒后重试;然后,如果工作在后续尝试后继续返回 Result.retry(),那么接下来会在 20 秒、30 秒、40 秒后重试,以此类推。


打印日志如下图所示。



我们可以看到,第一次任务失败后延迟了10秒重新执行,第二次延迟了20秒,第三次延迟了40秒...


8、观察工作执行结果


在任务完成后,我可能需要进行更新UI或者业务逻辑操作。我们可以通过注册监听器来观察 WorkInfo 的变化,以根据ID查询WorkInfo状态为例,代码如下所示:


WorkManager.getInstance(this).getWorkInfoByIdLiveData(uploadLogWorkerRequset.id).observe(this){
if (it.state == WorkInfo.State.SUCCEEDED){
Toast.makeText(this,"任务执行成功,更新UI",Toast.LENGTH_LONG).show()
}else{
//任务失败或重试
}
}

除了getWorkInfoByIdLiveData之外还有根据tag、name等查询的转化方法,这里读者可自行查看API。


运行程序,结果如下图所示。



类似的我们还可以通过cancelWorkById等方法来取消任务的执行。这里不做演示了。


9.总结


9.1 特性



  • 在早于 Android 12 的 API 版本中,加急工作都是由前台服务执行的,而从 Android 12 开始,它们将由加急作业 (expedited job) 实现。所以在第4小节中,Android12上并不会显示通知栏

  • WorkManager 只是一个处理定时任务的工具

  • WorkManager 最早兼容到 API 14(Android 4.0)


9.2 注意事项



  • 使用WorkManager注册的周期性任务不能保证一定会准时执行,这并不是bug,而是系 统为了减少电量消耗,可能会将触发时间临近的几个任务放在一起执行,这样可以大幅度地减 少CPU被唤醒的次数,从而有效延长电池的使用时间。

  • WorkManager官方虽然称它可以保证即使在应用退出甚至手机重启的情况下,之前注册的任务仍然将会得到执行。但是在国产手机中是不可能的,因为系统自己做了改动。但是在国产机上测试退出后,再进来也会执行之前的任务。这个时候可能就会有重复的任务执行。

  • 如果任务已经开始执行调用取消任务的方法是无法终止任务的,但是调用取消方法之后,无法再观察到任务结果。

  • 执行一个后台任务,在任务结束前杀死APP,再次进来时之前未完成的任务会从头开始执行,且执行结束后无法收到回调。

  • 任务添加到队列后,未开始执行前,如果是在onDestory中调用取消任务的方法是不可行的,此种情况下下次进来时仍然会有重复任务开始执行。(原生系统、国产机一样)


9.3 在业务中使用需要关注的问题



  • 任务添加到队列后,未开始执行前,如果是在onDestory中调用取消任务的方法是不可行的,此种情况下下次进来时仍然会有重复任务开始执行。


产生原因:cancelWork操作是一个异步操作,调用此操作后取消操作还未执行结束进程便结束了


业务影响: 连续打开关闭多次,会有多个重复的任务执行,且之前的任务无法收到任务进度回调


解决方案: 暂无


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

是时候弃用 buildSrc ,使用 Composing builds 加快编译速度了

为什么要使用复合构建 依赖管理一直是一个优化痛点,从硬编码到ext,再发展到buildSrc,尽管代码量在发展中增长了,但是对于追求更快更干净的构建来说确实进步了不少。但是buildSrc虽然给了我们相对干净的使用方式,但是依然没有解决最核心的速度问题。编译过...
继续阅读 »

为什么要使用复合构建


依赖管理一直是一个优化痛点,从硬编码到ext,再发展到buildSrc,尽管代码量在发展中增长了,但是对于追求更快更干净的构建来说确实进步了不少。但是buildSrc虽然给了我们相对干净的使用方式,但是依然没有解决最核心的速度问题。编译过程中 Gradle 最大的低效就是它的单线程配置阶段,这意味着每个额外的模块都会对构建产生持续的开销,因此我们依然经历着配置时间的线性增长,通常大型项目编译一次,就要去喝杯咖啡。


使用 Gradle 的复合构建工具就避免了在其他构建模式时很容易观察到的配置时间损失,依赖不再是全量编译了。复合构建将大型项目构建分解为更小、更独立的块,这些块可以根据需要独立或一起工作,包含的构建不与复合构建或其他包含的构建共享任何配置。每个包含的构建都是独立配置和执行的。


更详细的对比,请参考大佬的再见吧 buildSrc, 拥抱 Composing builds 提升 Android 编译速度,这里不再赘述。因为找到的相关使用文档均已过时,所以下面就记录下来最新的创建使用方法。


基本使用


创建版本依赖插件 Module


这个步骤可以手动创建,也可以借助 Android Studio 创建。




  • 手动创建




    1. 切换到 Project 视图,创建 version-plugin 文件夹,在 version-plugin 文件夹里创建 src -> main -> java 文件




    2. 在 java 文件夹里创建你的包名文件夹,例如 com -> example -> plugin (不想要包名文件夹的话,这一步可以省略),在 plugin 文件夹里创建两个文件Dependencies.ktVersionPlugin.kt




    3. 在 version-plugin 文件夹下创建build.gradle.kts文件,这里使用 kotlin DSL 更方便




    4. build.gradle.kts里添加所需的插件


      plugins {
      `kotlin-dsl`
      }



    5. 在version-plugin 根目录创建settings.gradle.kts,并添加依赖仓库


      dependencyResolutionManagement {
      repositories {
      google()
      mavenCentral()
      }
      }
      rootProject.name = "version-plugin"
      include (":version-plugin")



    6. 在项目根目录的settings.gradle里添加includeBuild("version-plugin")引入插件


      pluginManagement {
      includeBuild("version-plugin")
      repositories {
      google()
      mavenCentral()
      gradlePluginPortal()
      }
      }
      dependencyResolutionManagement {
      repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
      repositories {
      google()
      mavenCentral()
      }
      }
      rootProject.name = "ComposeBuild"
      include ':app'





  • AS创建




image.png




  1. File -> New -> New Module ,选择 Java or kotlin Library,创建一个 Module




  2. 创建Dependencies.kt文件




  3. 删除 version-plugin 文件夹下的 libs 文件夹




  4. build.gradle转化为build.gradle.kts文件


    plugins {
    `kotlin-dsl`
    }



  5. 在 version-plugin 根目录创建settings.gradle.kts,并添加依赖仓库


    dependencyResolutionManagement {
    repositories {
    google()
    mavenCentral()
    }
    }
    rootProject.name = "version-plugin"
    include (":version-plugin")



  6. 项目根目录settings.gradle里的include ':version-plugin'替换为includeBuild("version-plugin"),为了规范,把它注册在上面的pluginManagement


    pluginManagement {
    includeBuild("version-plugin")
    repositories {
    google()
    mavenCentral()
    gradlePluginPortal()
    }
    }
    dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
    google()
    mavenCentral()
    }
    }
    rootProject.name = "ComposeBuild"
    include ':app'
    //include ':version-plugin'

    完成后的项目目录:
    项目目录




编写插件


Gradle 是一个框架,作为框架,它负责定义流程和规则。而具体的编译工作则是通过插件的方式来完成的,我们要引入插件,而达到获取插件配置的目的。


实现插件类


VersionPlugin.kt中实现插件


package com.example.plugin

import org.gradle.api.Plugin
import org.gradle.api.Project

class VersionPlugin : Plugin<Project> {
override fun apply(target: Project) {
println("VersionPlugin")
}
}

配置依赖


Dependencies.kt中,我项目的依赖库拷贝在这里:


object Versions {
const val composeUi = "1.3.1"
const val composeVersion = "1.2.0"
const val kotlin = "1.8.0"
const val lifecycle = "2.5.1"
const val activityCompose = "1.5.1"
const val composeMaterial3 = "1.0.0-alpha11"
const val junit = "4.13.2"
const val androidxJunit = "1.1.3"
const val espresso = "3.4.0"
}

object Libraries {
// 依赖库
const val coreKtx = "androidx.core:core-ktx:${Versions.kotlin}"
const val lifecycle = "androidx.lifecycle:lifecycle-runtime-ktx:${Versions.lifecycle}"
const val activityCompose = "androidx.activity:activity-compose:${Versions.activityCompose}"
const val composeUi = "androidx.compose.ui:ui:${Versions.composeUi}"
const val composePreview = "androidx.compose.ui:ui-tooling-preview:${Versions.composeVersion}"
const val composeMaterial3 = "androidx.compose.material3:material3:${Versions.composeMaterial3}"
// 测试库
const val junit = "junit:junit:${Versions.junit}"
const val androidxJunit = "androidx.test.ext:junit:${Versions.androidxJunit}"
const val espresso = "androidx.test.espresso:espresso-core:${Versions.espresso}"
const val uiTestJunit4 = "androidx.compose.ui:ui-test-junit4:${Versions.composeVersion}"
const val uiTooling = "androidx.compose.ui:ui-tooling:${Versions.composeVersion}"
const val uiTestManifest = "androidx.compose.ui:ui-test-manifest:${Versions.composeVersion}"

}

注册插件


插件要能被别的 Module 引入,需要注册在插件 Module 的build.gradle.kts


plugins {
`kotlin-dsl`
}
gradlePlugin {
plugins.register("versionPlugin") {
id = "version-plugin"
implementationClass = "com.example.plugin.VersionPlugin"
}
}

使用


在用到的 Module 里添加插件,app 目录下的build.gradle


plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
// 依赖插件
id 'version-plugin'
}

这时候就可以引用插件 Module 里定义的依赖了:


implementation Libraries.coreKtx

扩展


依赖优化


上面一通操作,在使用的时候,并没有方便多少。为了不再一个一个的引入依赖,我们需要写个扩展优化。为了方便操作和提示,建议使用 Kotlin 的 DSL ,首先把build.gradle转为build.gradle.kts


转化前:


import com.example.plugin.Libraries

plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'version-plugin'
}

android {
namespace 'com.example.composingbuilds'
compileSdk 33

defaultConfig {
applicationId "com.example.composingbuilds"
minSdk 24
targetSdk 33
versionCode 1
versionName "1.0"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary true
}
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion '1.1.1'
}
packagingOptions {
resources {
excludes += '/META-INF/{AL2.0,LGPL2.1}'
}
}
}

dependencies {
implementation Libraries.coreKtx
// implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
implementation 'androidx.activity:activity-compose:1.3.1'
implementation "androidx.compose.ui:ui:$compose_version"
implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
implementation 'androidx.compose.material3:material3:1.0.0-alpha11'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version"
}

转化后:


import com.example.plugin.Libraries

plugins {
id("com.android.application")
id("kotlin-android")
id("version-plugin")
}

android {
namespace = "com.example.composingbuilds"
compileSdk = 33

defaultConfig {
applicationId = "com.example.composingbuilds"
minSdk = 23
targetSdk = 33
versionCode = 1
versionName = "1.0"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary = true
}
}

buildTypes {
getByName("release") {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.1.1"
}
packagingOptions {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
}

dependencies {
implementation(Libraries.coreKtx)
implementation(Libraries.lifecycle)
implementation(Libraries.activityCompose)
implementation(Libraries.composeUi)
implementation(Libraries.composePreview)
implementation(Libraries.composeMaterial3)

testImplementation(Libraries.junit)
androidTestImplementation(Libraries.androidxJunit)
androidTestImplementation(Libraries.espresso)
androidTestImplementation(Libraries.uiTestJunit4)
debugImplementation(Libraries.uiTooling)
debugImplementation(Libraries.uiTestManifest)

}

dependencies里面还是需要一个一个的依赖,有时候项目并不是一个 Module 而是多 Module 的状态,每个build.gradle都要写依赖,要简化这个繁琐的过程,就需要把依赖分类集中处理。


在插件 Module 里新建Extension.kt,可以把依赖库分为kotlin、android、compose、test四部分。扩展DependencyHandlerScope


fun DependencyHandlerScope.kotlinProject() {
"implementation"(Libraries.coreKtx)
}


fun DependencyHandlerScope.androidProject() {
"implementation"(Libraries.lifecycle)
}

fun DependencyHandlerScope.composeProject() {
"implementation"(Libraries.activityCompose)
"implementation"(Libraries.composeUi)
"implementation"(Libraries.composePreview)
"implementation"(Libraries.composeMaterial3)
}

fun DependencyHandlerScope.androidTest() {
"testImplementation"(Libraries.junit)
"androidTestImplementation"(Libraries.androidxJunit)
"androidTestImplementation"(Libraries.espresso)
"androidTestImplementation"(Libraries.uiTestJunit4)
"debugImplementation"(Libraries.uiTooling)
"debugImplementation"(Libraries.uiTestManifest)
}

然后修改项目依赖,调用上面的扩展,短短几行就可实现:


dependencies {
kotlinProject()
androidProject()
composeProject()
androidTest()

// implementation(Libraries.coreKtx)
// implementation(Libraries.lifecycle)
// implementation(Libraries.activityCompose)
// implementation(Libraries.composeUi)
// implementation(Libraries.composePreview)
// implementation(Libraries.composeMaterial3)
//
// testImplementation(Libraries.junit)
// androidTestImplementation(Libraries.androidxJunit)
// androidTestImplementation(Libraries.espresso)
// androidTestImplementation(Libraries.uiTestJunit4)
// debugImplementation(Libraries.uiTooling)
// debugImplementation(Libraries.uiTestManifest)

}

插件依赖


上面只优化了dependencies这个闭包,build.gradle.kts依旧很多东西,既然写了一个插件,我们就用插件实现整个配置。


app的build.gradle.kts一共有三个闭包:pluginandroiddependencies,对应插件其实也是现实这三个配置,回到最开始的VersionPlugin中:


class VersionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target){
//配置plugin
//配置android
//配置dependencies
}
}
}

1. 首先实现配置plugin


这个闭包就是引入插件,把原 Module 用到的插件搬过来即可,这里要去掉原先加入的自身插件


//配置plugin
plugins.run {
apply("com.android.application")
apply("kotlin-android")
}

2. 然后实现配置android


这里用到相关依赖,先添加到插件 Module 的build.gradle.kts里:


plugins {
`kotlin-dsl`
}
dependencies {
implementation("com.android.tools.build:gradle:7.3.1")
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.0")
}

然后配置android,把 Module 的build.gradle.kts里的android部分搬过来,唯一需要注意的是,插件里没有kotlinOptions,需要自己写一个扩展:


//配置android
extensions.configure<ApplicationExtension> {
applicationId = "com.asi.composingbuild"
compileSdk=33
defaultConfig {
applicationId="com.asi.composingbuild"
minSdk = 23
targetSdk=33
versionCode=1
versionName="1.0"
testInstrumentationRunner= "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary =true
}
}
buildTypes {
getByName("release") {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}

kotlinOptions{
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.1.1"
}
packagingOptions {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
}

kotlinOptions扩展:


fun CommonExtension<*, *, *, *>.kotlinOptions(block: KotlinJvmOptions.() -> Unit) {
(this as ExtensionAware).extensions.configure("kotlinOptions", block)
}



  1. 首先实现配置dependencies


    //配置dependencies
    dependencies {
    kotlinProject()
    androidProject()
    composeProject()
    androidTest()
    }



    1. 依赖插件


      把 app Module 的build.gradle.kts里的内容都删了,只依赖下刚完成的插件:


      plugins {
      id("version-plugin")
      }

      是不是很清爽的感觉?






多个插件


如果是多 Module 的项目,每个 Module 的依赖会不一样,所以可以在 version-plugin 中编写多个plugin,然后注册id,在不同的 Module 里使用,修改某个依赖,只构建这个 Module 的依赖,达到隔离构建的目的。


复合构建


上面单一 Module 中单独的插件,依赖的库并没有达到隔离构建的目的,如果我们只是更改了composeUi版本,整个依赖都要重新编译。要实现隔离,需要更精细化的拆分,比如把compose部分单独出来。


新建一个ComposePlugin.kt,把原来插件中的关于compose的配置拷贝过来:


class ComposePlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
//配置compose
extensions.configure<ApplicationExtension> {
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = Versions.kotlinCompilerExtensionVersion
}
}
dependencies {
composeProject()
}
}
}
}

插件写完需要注册:


gradlePlugin {
plugins.register("versionPlugin") {
id = "version-plugin"
implementationClass = "com.example.plugin.VersionPlugin"
}
plugins.register("ComposePlugin") {
id = "compose-plugin"
implementationClass = "com.example.plugin.ComposePlugin"
}
}

这里可以优化下写法:


gradlePlugin {
plugins{
register("versionPlugin") {
id = "version-plugin"
implementationClass = "com.example.plugin.VersionPlugin"
}
register("ComposePlugin") {
id = "compose-plugin"
implementationClass = "com.example.plugin.ComposePlugin"
}
}
}

在 app 模块里引入:


plugins {
id("version-plugin")
id("compose-plugin")
}

这样如果修改compose版本,并不会构建别的依赖。


国际惯例上源码


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

来自 2022 的总结,拖延症害死人

停下来,看一看 之前写影评就不喜欢矫揉造作,这篇也就当成自述吧,可能,矫情了点? 高中时候看过一个小短片《二十七八岁》,和现在生活真有些许重合,发现生活中开始计算收支,喜欢健康的生活方式 偶尔感慨着房价、物价的涨的有多快,然后摸了摸自己口袋,发现烟没怎么涨价,...
继续阅读 »

停下来,看一看


之前写影评就不喜欢矫揉造作,这篇也就当成自述吧,可能,矫情了点?


高中时候看过一个小短片《二十七八岁》,和现在生活真有些许重合,发现生活中开始计算收支,喜欢健康的生活方式


偶尔感慨着房价、物价的涨的有多快,然后摸了摸自己口袋,发现烟没怎么涨价,挺好的。


我是一名 Android 开发工程师,但大多数时间做的都是偏前端项目,可能 23 年的 flag 一不小心就定位转行了呢!!!


2022 年,给自己备份存个档,按时间节点的流水账


主题词 一:结婚


作为 Android 打工人,名正言顺经常去设计部门逛逛,和妹子讨论 UI设计问题。


我说:“你不能这样设计,会增加开发复杂度,以及运行性能……”

“真的吗?这样设计真的不行吗”

“对的啊,你看把这些…功能都去掉可不可以”

她看着我说:“可是我觉得这样更好看唉”

对视了会,也不是不能做,内卷就这么开始了。。。


可能我个人魅力(内卷的教养)吸引了妹子,在我的疯狂追势下,成为了我老婆。

你看,谈恋爱容易吗?结婚容易吗?


相信我,你只要兢兢业业的打工,也会拥有的。当然有一丢丢取决于个人魅力(幽默、吃饭、旅游、礼物、关心、头发、骚!!!


如果没能成功,还是记得好好爱自己(🐶)。


主题词 二:买房


做了很多攻略,我觉得22年并不是买房的好时机,但房东说你不买就得给我打工,被逼的我就买了房。


看了西安,看了武汉,最后还是选择了上海,不亏为我的底线


定居后,感觉人生清单里,又划掉一项,想了想剩下的 Todo 事项,又被沉默施法。

说明这真的只是人生中一件事,拉长生活轨迹,还有很多等待着去体验,去感受。


但是,我成了大家眼中,有钱人?

我说你误会太多,他说早该叫老总

我说贷款成房奴,那是资产会升值

我说还贷压力大,老总你还有头发


其实有没有钱呢,心里跟明镜一样儿,不需要华丽的奢侈品、昂贵的汽车、进口的杂七杂八。


从日出到黄昏,三餐四季,柴米油盐酱醋;会一会小伙伴,饮茶小酌,吃喝玩乐大保健(也就只敢写写)。


你说这样的生活需要太大成本吗,不舒服?那就是这样平凡


主题词 三:研究下班精力流转效率


天将降大任于斯人也,我报之以兼职创业。我想过做炸鸡店、临期食品,还想着要不要盘家店,做拉面生意(虽然我不会)。


甚至某鱼上门教写代码也是可以的,三楼贵宾一位?这思想来源于职场焦虑,也是对于以后的职业生涯,思考多一个选择。


来源于朋友的邀请,一起做外包公司,线下线上接活,再招兼职人员开发,我负责进行项目管理,人员外派。


代码能力不行?没行业经验?不重要

只要你张得开嘴,迈得开腿,吃喝陪睡(没有,写飘了),能拿到 10w+ 预算的项目,人还不好找吗?


当然也会遇到很多问题,计算项目成本没经验,没有人力资源池,以及项目中有人跑路,风险管控,本职工作忙,项目涉及技术复杂等等。


诸如此类,很多问题都要想办法去解决,而且行业也是内卷的,转包严重,留给你的也许就是个骨头。


忙了一段时间,再加上生活中一些变动,还是先休息沉淀沉淀,转战幕后,去梳理下后期规划。


主题词 四:宝宝出生


感概今年的生活很充实,小虎妞出生啦,我看了些楚辞、诗经,给小孩选了几个名字觉得一般。


又搬出了 github,找个程序帮我测算名字(周易五行),95 得分我很满意,不是为了迷信,是不像她爹考试只踩及格线。


宝宝很乖,有时半夜 2:00 还和我打招呼,让我陪她说说话


我说你快睡,她说:“爸爸你最好,爸爸你最帅,爸爸我是你的小棉袄……”,你能想象一个新生儿这么能说吗,我也没想到。


我心想:“等你长大了,我拉着你晚上一起,坐着摇椅慢慢聊 Vue、React、Android,谁都别想睡。”


结语


面对了自己22年,和27岁的人生,白给了吗?好像也并没有,也升职加了薪、项目赚了钱、基金亏成狗(累计还是赚)。


23年就不列什么 flag 了,想到什么就立马去做吧,行动力往往比嘴上功夫更重要。


借用之前看过的一本书:“跳舞吧,趁年轻的时候。不要去想为什么要跳舞,尽管跳舞就对了。脚步一旦停了下来,就

作者:抠键盘养你
来源:juejin.cn/post/7208050893055787063
会往另一个世界走。”

收起阅读 »

4个工具,让 ChatGPT 如虎添翼!

让 ChatGPT 如虎添翼! ChatGPT 很好用,其核心是prompt的运用,其回答质量很大程度取决于你的提问质量。 我算是 ChatGPT 的高频用户了,几乎日活。半个月前我在星球写道: 冲了Plus之后,使用更加频繁,说几点ChatGPT近期感受: ...
继续阅读 »

让 ChatGPT 如虎添翼!


ChatGPT 很好用,其核心是prompt的运用,其回答质量很大程度取决于你的提问质量。


我算是 ChatGPT 的高频用户了,几乎日活。半个月前我在星球写道:


冲了Plus之后,使用更加频繁,说几点ChatGPT近期感受:
1、它发挥多大效能取决于你的知识储备,它只能锦上添花,无法雪中送炭
2、提问是门技术,无论是向他人求助,还是之前用搜索引擎,大部分人连提问都搞不定
3、未来ChatGPT必然更加强大,会更好用,但依然无法解决上面俩个问题
4、未来N年他会持续进化,国内做模型大概率邯郸学步
5、真正的机会在应用侧,大面积复制国外各种现有应用,但是肯定也会高出更多玩法
6、这个赛道很长,无论你对它什么态度,可能最终都会影响你,就像之前的搜索引擎和推荐算法


最近我尝试了很多辅助工具,简直如虎添翼,效率大幅提升,这里就推荐几个最好用的吧。


ChatGPT-Shortcut


让生产力加倍的 ChatGPT 快捷指令,按照领域和功能分区,可对提示词进行标签筛选、关键词搜索和一键复制。


newzone.top/chatgpt/


ChatGPT Shortcut 是根据领域和功能划分的 ChatGPT 快捷指令表,可通过标签筛选、关键词搜索和一键复制来使用提示词,旨在简化你的工作流程并提高生产力。即使是初学者,你只需复制提示词,稍加修改后发送给 ChatGPT,就能获得指定输出,让你的生产力加倍!


ChatGPT Shortcut 页面默认显示全部的提示词,页面分为标签区、搜索区和提示词展示区。


AIPRM for ChatGPT


这是一个插件,单击一下,就可以访问精选的 ChatGPT 提示,这些提示是专门为 SEO、SaaS、营销和更多 ChatGPT 的精选提示模板列表而设计的。


插件安装:
chrome.google.com/webstore/de…


官网:http://www.aiprm.com/


AIPRM for ChatGPT 最方便的地方在于能够设定输出语言、文字用语、文字风格,想它风趣一点或是严肃一点,现在可以轻松做到。


ChatPDF



基于 ChatGPT API 的读取 PDF 工具 ChatPDF:可以直接把需要阅读的 pitch deck、白皮书、文档、论文、图书喂给它,让它做总结和提问。



ChatPDF 简直是学习神器,上传 PDF 文件后,可以对它提问任何关于这份 PDF 的问题,非常适合快速提取各种 paper 论文的摘要,也支持中文输出。


http://www.chatpdf.com/


上传pdf即可,然后你就可以基于pdf的内容去提问了



bob-plugin-openai-translator



基于 ChatGPT API 的文本翻译、文本润色、语法纠错 Bob 插件,让我们一起迎接不需要巴别塔的新时代!


github.com/yetone/bob-…


此插件已支持使用 ChatGPT API 对句子进行润色和语法修改,只需要把目标语言选成跟源语言一样即可,全面替代 Grammarly!而且理论上任何语言都可以润色,不只是英语。



目前仅支持macOS用户,非 macOS 用户可以使用我开发的基于 ChatGPT API 的划词翻译浏览器插件 openai-translator 以解燃眉之急。


github.com/yetone/open…



LightGBM中文文档


机器学习统计学,476页


机器学习圣经PRML中文版


经典著作《机器学习:概率视角


作者:机器学习与统计学
来源:juejin.cn/post/7208413354905223227
收起阅读 »

保姆级JAVA对接ChatGPT教程,实现自己的AI对话助手

1.前言 大家好,我是王老狮,近期OpenAI开放了chatGPT的最新gpt-3.5-turbo模型,据介绍该模型是和当前官网使用的相同的模型,如果你还没体验过ChatGPT,那么今天就教大家如何打破网络壁垒,打造一个属于自己的智能助手把。本文包括API K...
继续阅读 »

1.前言


大家好,我是王老狮,近期OpenAI开放了chatGPT的最新gpt-3.5-turbo模型,据介绍该模型是和当前官网使用的相同的模型,如果你还没体验过ChatGPT,那么今天就教大家如何打破网络壁垒,打造一个属于自己的智能助手把。本文包括API Key的申请以及网络代理的搭建,那么事不宜迟,我们现在开始。


2.对接流程


2.1.API-Key的获取


首先第一步要获取OpenAI接口的API Key,该Key是你用来调用接口的token,主要用于接口鉴权。获取该key首先要注册OpenAi的账号,具体可以见我的另外一篇文章,ChatGPT保姆级注册教程



  1. 打开platform.openai.com/网站,点击view API Key,


image.png



  1. 点击创建key


image.png



  1. 弹窗显示生成的key,记得把key复制,不然等会就找不到这个key了,只能重新创建。


image.png


将API Key保存好以备用


2.2.API用量的查看


这里可以查看API的使用情况,新账号注册默认有5美元的试用额度,之前都是18美元,API成本降了之后试用额度也狠狠地砍了一刀啊,哈哈。


image.png


2.3.核心代码实现


2.3.1.pom依赖


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

<modelVersion>4.0.0modelVersion>
<groupId>com.webtapgroupId>
<artifactId>webtapartifactId>
<version>0.0.1version>
<packaging>jarpackaging>

<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.1.2.RELEASEversion>
parent>

<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-thymeleafartifactId>
dependency>
<dependency>
<groupId>nz.net.ultraq.thymeleafgroupId>
<artifactId>thymeleaf-layout-dialectartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-jpaartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-mailartifactId>
dependency>

<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-lang3artifactId>
<version>3.4version>
dependency>
<dependency>
<groupId>commons-codecgroupId>
<artifactId>commons-codecartifactId>
dependency>
<dependency>
<groupId>org.jsoupgroupId>
<artifactId>jsoupartifactId>
<version>1.9.2version>
dependency>

<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>1.2.56version>
dependency>
<dependency>
<groupId>net.sourceforge.nekohtmlgroupId>
<artifactId>nekohtmlartifactId>
<version>1.9.22version>
dependency>
<dependency>
<groupId>com.github.pagehelpergroupId>
<artifactId>pagehelper-spring-boot-starterartifactId>
<version>1.4.1version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
<dependency>
<groupId>org.apache.httpcomponentsgroupId>
<artifactId>httpasyncclientartifactId>
<version>4.0.2version>
dependency>
<dependency>
<groupId>org.apache.httpcomponentsgroupId>
<artifactId>httpcore-nioartifactId>
<version>4.3.2version>
dependency>

<dependency>
<groupId>org.apache.httpcomponentsgroupId>
<artifactId>httpclientartifactId>
<version>4.3.5version>
<exclusions>
<exclusion>
<artifactId>commons-codecartifactId>
<groupId>commons-codecgroupId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>commons-httpclientgroupId>
<artifactId>commons-httpclientartifactId>
<version>3.1version>
<exclusions>
<exclusion>
<artifactId>commons-codecartifactId>
<groupId>commons-codecgroupId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>1.3.1version>
dependency>
<dependency>
<groupId>com.github.ulisesbocchiogroupId>
<artifactId>jasypt-spring-boot-starterartifactId>
<version>2.0.0version>
dependency>

dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>

project>

2.3.2.实体类ChatMessage.java


用于存放发送的消息信息,注解使用了lombok,如果没有使用lombok可以自动生成构造方法以及get和set方法


@Data
@NoArgsConstructor
@AllArgsConstructor
public class ChatMessage {
//消息角色
String role;
//消息内容
String content;
}

2.3.3.实体类ChatCompletionRequest.java


用于发送的请求的参数实体类,参数释义如下:


model:选择使用的模型,如gpt-3.5-turbo


messages :发送的消息列表


temperature :温度,参数从0-2,越低表示越精准,越高表示越广发,回答的内容重复率越低


n :回复条数,一次对话回复的条数


stream :是否流式处理,就像ChatGPT一样的处理方式,会增量的发送信息。


max_tokens :生成的答案允许的最大token数


user :对话用户


@Data
@Builder
public class ChatCompletionRequest {

String model;

List<ChatMessage> messages;

Double temperature;

Integer n;

Boolean stream;

List<String> stop;

Integer max_tokens;

String user;
}

2.3.4.实体类ExecuteRet .java


用于接收请求返回的信息以及执行结果



/**
* 调用返回
*/

public class ExecuteRet {

/**
* 操作是否成功
*/

private final boolean success;

/**
* 返回的内容
*/

private final String respStr;

/**
* 请求的地址
*/

private final HttpMethod method;

/**
* statusCode
*/

private final int statusCode;

public ExecuteRet(booleansuccess, StringrespStr, HttpMethodmethod, intstatusCode) {
this.success =success;
this.respStr =respStr;
this.method =method;
this.statusCode =statusCode;
}

@Override
public String toString()
{
return String.format("[success:%s,respStr:%s,statusCode:%s]", success, respStr, statusCode);
}

/**
*@returnthe isSuccess
*/

public boolean isSuccess() {
return success;
}

/**
*@returnthe !isSuccess
*/

public boolean isNotSuccess() {
return !success;
}

/**
*@returnthe respStr
*/

public String getRespStr() {
return respStr;
}

/**
*@returnthe statusCode
*/

public int getStatusCode() {
return statusCode;
}

/**
*@returnthe method
*/

public HttpMethod getMethod() {
return method;
}
}

2.3.5.实体类ChatCompletionChoice .java


用于接收ChatGPT返回的数据


@Data
public class ChatCompletionChoice {

Integer index;

ChatMessage message;

String finishReason;
}

2.3.6.接口调用核心类OpenAiApi .java


使用httpclient用于进行api接口的调用,支持post和get方法请求。


url为配置文件open.ai.url的值,表示调用api的地址:https://api.openai.com/ ,token为获取的api-key。
执行post或者get方法时增加头部信息headers.put("Authorization", "Bearer " + token); 用于通过接口鉴权。



@Slf4j
@Component
public class OpenAiApi {

@Value("${open.ai.url}")
private String url;
@Value("${open.ai.token}")
private String token;

private static final MultiThreadedHttpConnectionManagerCONNECTION_MANAGER= new MultiThreadedHttpConnectionManager();

static {
// 默认单个host最大链接数
CONNECTION_MANAGER.getParams().setDefaultMaxConnectionsPerHost(
Integer.valueOf(20));
// 最大总连接数,默认20
CONNECTION_MANAGER.getParams()
.setMaxTotalConnections(20);
// 连接超时时间
CONNECTION_MANAGER.getParams()
.setConnectionTimeout(60000);
// 读取超时时间
CONNECTION_MANAGER.getParams().setSoTimeout(60000);
}

public ExecuteRet get(Stringpath, Map headers) {
GetMethod method = new GetMethod(url +path);
if (headers== null) {
headers = new HashMap<>();
}
headers.put("Authorization", "Bearer " + token);
for (Map.Entry h : headers.entrySet()) {
method.setRequestHeader(h.getKey(), h.getValue());
}
return execute(method);
}

public ExecuteRet post(Stringpath, Stringjson, Map headers) {
try {
PostMethod method = new PostMethod(url +path);
//log.info("POST Url is {} ", url + path);
// 输出传入参数
log.info(String.format("POST JSON HttpMethod's Params = %s",json));
StringRequestEntity entity = new StringRequestEntity(json, "application/json", "UTF-8");
method.setRequestEntity(entity);
if (headers== null) {
headers = new HashMap<>();
}
headers.put("Authorization", "Bearer " + token);
for (Map.Entry h : headers.entrySet()) {
method.setRequestHeader(h.getKey(), h.getValue());
}
return execute(method);
} catch (UnsupportedEncodingExceptionex) {
log.error(ex.getMessage(),ex);
}
return new ExecuteRet(false, "", null, -1);
}

public ExecuteRet execute(HttpMethodmethod) {
HttpClient client = new HttpClient(CONNECTION_MANAGER);
int statusCode = -1;
String respStr = null;
boolean isSuccess = false;
try {
client.getParams().setParameter(HttpMethodParams.HTTP_CONTENT_CHARSET, "UTF8");
statusCode = client.executeMethod(method);
method.getRequestHeaders();

// log.info("执行结果statusCode = " + statusCode);
InputStreamReader inputStreamReader = new InputStreamReader(method.getResponseBodyAsStream(), "UTF-8");
BufferedReader reader = new BufferedReader(inputStreamReader);
StringBuilder stringBuffer = new StringBuilder(100);
String str;
while ((str = reader.readLine()) != null) {
log.debug("逐行读取String = " + str);
stringBuffer.append(str.trim());
}
respStr = stringBuffer.toString();
if (respStr != null) {
log.info(String.format("执行结果String = %s, Length = %d", respStr, respStr.length()));
}
inputStreamReader.close();
reader.close();
// 返回200,接口调用成功
isSuccess = (statusCode == HttpStatus.SC_OK);
} catch (IOExceptionex) {
} finally {
method.releaseConnection();
}
return new ExecuteRet(isSuccess, respStr,method, statusCode);
}

}

2.3.7.定义接口常量类PathConstant.class


用于维护支持的api接口列表


public class PathConstant {
public static class MODEL {
//获取模型列表
public static String MODEL_LIST = "/v1/models";
}

public static class COMPLETIONS {
public static String CREATE_COMPLETION = "/v1/completions";
//创建对话
public static String CREATE_CHAT_COMPLETION = "/v1/chat/completions";

}
}

2.3.8.接口调用调试单元测试类OpenAiApplicationTests.class


核心代码都已经准备完毕,接下来写个单元测试测试下接口调用情况。



@SpringBootTest
@RunWith(SpringRunner.class)
public class OpenAiApplicationTests {

@Autowired
private OpenAiApi openAiApi;
@Test
public void createChatCompletion2() {
Scanner in = new Scanner(System.in);
String input = in.next();
ChatMessage systemMessage = new ChatMessage('user', input);
messages.add(systemMessage);
ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder()
.model("gpt-3.5-turbo-0301")
.messages(messages)
.user("testing")
.max_tokens(500)
.temperature(1.0)
.build();
ExecuteRet executeRet = openAiApi.post(PathConstant.COMPLETIONS.CREATE_CHAT_COMPLETION, JSONObject.toJSONString(chatCompletionRequest),
null);
JSONObject result = JSONObject.parseObject(executeRet.getRespStr());
List choices = result.getJSONArray("choices").toJavaList(ChatCompletionChoice.class);
System.out.println(choices.get(0).getMessage().getContent());
ChatMessage context = new ChatMessage(choices.get(0).getMessage().getRole(), choices.get(0).getMessage().getContent());
System.out.println(context.getContent());
}

}


  • 使用Scanner 用于控制台输入信息,如果单元测试时控制台不能输入,那么进入IDEA的安装目录,修改以下文件。增加最后一行增加-Deditable.java.test.console=true即可。


image.png
image.png




  • 创建ChatMessage对象,用于存放参数,role有user,system,assistant,一般接口返回的响应为assistant角色,我们一般使用user就好。




  • 定义请求参数ChatCompletionRequest,这里我们使用3.1日发布的最新模型gpt-3.5-turbo-0301。具体都有哪些模型大家可以调用v1/model接口查看支持的模型。




  • 之后调用openAiApi.post进行接口的请求,并将请求结果转为JSON对象。取其中的choices字段转为ChatCompletionChoice对象,该对象是存放api返回的具体信息。


    接口返回信息格式如下:


    {
    "id": "chatcmpl-6rNPw1hqm5xMVMsyf6PXClRHtNQAI",
    "object": "chat.completion",
    "created": 1678179420,
    "model": "gpt-3.5-turbo-0301",
    "usage": {
    "prompt_tokens": 16,
    "completion_tokens": 339,
    "total_tokens": 355
    },
    "choices": [{
    "message": {
    "role": "assistant",
    "content": "\n\nI. 介绍数字孪生的概念和背景\n A. 数字孪生的定义和意义\n B. 数字孪生的发展历程\n C. 数字孪生在现代工业的应用\n\nII. 数字孪生的构建方法\n A. 数字孪生的数据采集和处理\n B. 数字孪生的建模和仿真\n C. 数字孪生的验证和测试\n\nIII. 数字孪生的应用领域和案例分析\n A. 制造业领域中的数字孪生应用\n B. 建筑和城市领域中的数字孪生应用\n C. 医疗和健康领域中的数字孪生应用\n\nIV. 数字孪生的挑战和发展趋势\n A. 数字孪生的技术挑战\n B. 数字孪生的实践难点\n C. 数字孪生的未来发展趋势\n\nV. 结论和展望\n A. 总结数字孪生的意义和价值\n B. 展望数字孪生的未来发展趋势和研究方向"
    },
    "finish_reason": "stop",
    "index": 0
    }]
    }



  • 输出对应的信息。




2.3.9.结果演示


image.png


2.4.连续对话实现


2.4.1连续对话的功能实现


基本接口调通之后,发现一次会话之后,没有返回完,输入继续又重新发起了新的会话。那么那么我们该如何实现联系上下文呢?其实只要做一些简单地改动,将每次对话的信息都保存到一个消息列表中,这样问答就支持上下文了,代码如下:


List messages = new ArrayList<>();
@Test
public void createChatCompletion() {
Scanner in = new Scanner(System.in);
String input = in.next();
while (!"exit".equals(input)) {
ChatMessage systemMessage = new ChatMessage(ChatMessageRole.USER.value(), input);
messages.add(systemMessage);
ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder()
.model("gpt-3.5-turbo-0301")
.messages(messages)
.user("testing")
.max_tokens(500)
.temperature(1.0)
.build();
ExecuteRet executeRet = openAiApi.post(PathConstant.COMPLETIONS.CREATE_CHAT_COMPLETION, JSONObject.toJSONString(chatCompletionRequest),
null);
JSONObject result = JSONObject.parseObject(executeRet.getRespStr());
List choices = result.getJSONArray("choices").toJavaList(ChatCompletionChoice.class);
System.out.println(choices.get(0).getMessage().getContent());
ChatMessage context = new ChatMessage(choices.get(0).getMessage().getRole(), choices.get(0).getMessage().getContent());
messages.add(context);
in = new Scanner(System.in);
input = in.next();
}
}

因为OpenAi的/v1/chat/completions接口消息参数是个list,这个是用来保存我们的上下文的,因此我们只要将每次对话的内容用list进行保存即可。


2.4.2结果如下:


image.png


image.png


4.常见问题


4.1.OpenAi接口调用不通


因为https://api.openai.com/地址也被限制了,但是接口没有对地区做校验,因此可以自己搭建一个香港代理,也可以走科学上网。


我采用的是香港代理的模式,一劳永逸,具体代理配置流程如下:



  1. 购买一台香港的虚拟机,反正以后都会用得到,作为开发者建议搞一个。搞活动的时候新人很便宜,基本3年的才200块钱。

  2. 访问nginx.org/download/ng… 下载最新版nginx

  3. 部署nginx并修改/nginx/config/nginx.conf文件,配置接口代理路径如下


server {
listen 19999;
server_name ai;

ssl_certificate /usr/local/nginx/ssl/server.crt;
ssl_certificate_key /usr/local/nginx/ssl/server.key;

ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;

ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;

#charset koi8-r;

location /v1/ {
proxy_pass ;
}
}


  1. 启动nginx

  2. 将接口访问地址改为nginx的机器出口IP+端口即可


如果代理配置大家还不了解,可以留下评论我单独出一期教程。


4.2.接口返回401


检查请求方法是否增加token字段以及key是否正确


5.总结


至此JAVA对OpenAI对接就已经完成了,并且也支持连续对话,大家可以在此基础上不断地完善和桥接到web服务,定制自己的ChatGPT助手了。我自己也搭建了个平台,不断地在完善中,具体可见下图,后续会开源出来,想要体验的可以私信我获取地址和账号哈


image.png



作者:王老狮
来源:juejin.cn/post/7208907027841171512
收起阅读 »

虚拟内存优化:线程+多进程优化

在介绍内存的基础知识的时候,我们讲过在 32 位系统上虚拟内存只有 4G,因为有 1G 是给内核使用的,所以留给应用的只有 3G 了。3G 虽然看起来挺多,但依然会因为不够用而导致应用崩溃。为什么会这样呢? 我们在学习 Java 堆的组成时就知道 MainSp...
继续阅读 »

在介绍内存的基础知识的时候,我们讲过在 32 位系统上虚拟内存只有 4G,因为有 1G 是给内核使用的,所以留给应用的只有 3G 了。3G 虽然看起来挺多,但依然会因为不够用而导致应用崩溃。为什么会这样呢?


我们在学习 Java 堆的组成时就知道 MainSpace 会申请 512M 的虚拟内存,LargeObjectSpace 也会申请 512M 的虚拟内存,这就用掉了 1G 的虚拟内存,再加上其他 Space 和段映射申请的虚拟内存,如 bss 段、text 段以及各种 so 库文件的映射等,这样算下来,3G 的虚拟内存就没剩下多少了。


所以,虚拟内存的优化,在提升程序的稳定性上,是一种很重要的方案。虚拟内存的优化手段也有很多,这一章我们主要介绍 3 种优化方案:




  1. 通过线程治理来优化虚拟内存;




  2. 通过多进程架构来优化虚拟内存;




  3. 通过一些“黑科技”手段来优化虚内存。




方案 1 和 2 相对简单但效果更佳,投入产出比最高,也是我们最常用的。而方案 3 是通过多个“黑科技”的手段来完成虚拟内存的优化,这些手段虽然属于“黑科技”,但还是会用到我们学过的 Native Hook 等技术,所以你理解、吸收起来并不会很难。


那今天我们先介绍 方案 1 和 方案 2 ,方案 3 会在下一章节单独介绍,下面就开始这一章的学习吧。


线程治理


首先,为什么治理线程能优化虚拟内存呢?实际上,即使是一个空线程也会申请 1M 的虚拟空间来作为栈空间大小,我们可以分析 Thread 创建的源码来验证这一点。同时,对线程创建的分析,也能让你能更好的理解后面的优化方案。


线程创建流程


当我们使用线程执行任务时,通常会先调用 new Thread(Runnable runnable) 来创建一个 Thread.java 对象的实例,Thread 的构造函数中会将 stackSize 这个变量设置为 0,这个 stackSize 变量决定了线程栈大小,接着我们便会执行 Thread 实例提供的 start 方法运行这个线程,start 方法中会调用 nativeCreate 这个 Native 函数在系统层创建一个线程并运行。


Thread(ThreadGroup group, String name, int priority, boolean daemon) {
……
this.stackSize = 0;
}

public synchronized void start() {
if (started)
throw new IllegalThreadStateException();
group.add(this);
started = false;
try {
nativeCreate(this, stackSize, daemon);
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {

}
}
}

通过上面 Start 函数的源码可以看到,nativeCreate 会传入 stackSize。你可能想问,这个 stackSize 不是决定了线程栈空间的大小吗?但是它现在的值为 0,那前面为什么说线程有 1M 大小的栈空间呢?我们接着往下看就能知道答案了。


我们接着看 nativeCreate 的源码实现(),它的实现类是 java_lang_Thread.cc


static void Thread_nativeCreate(JNIEnv* env, jclass, jobject java_thread, jlong stack_size, jboolean daemon) {
Runtime* runtime = Runtime::Current();
if (runtime->IsZygote() && runtime->IsZygoteNoThreadSection()) {
jclass internal_error = env->FindClass("java/lang/InternalError");
CHECK(internal_error != nullptr);
env->ThrowNew(internal_error, "Cannot create threads in zygote");
return;
}

Thread::CreateNativeThread(env, java_thread, stack_size, daemon == JNI_TRUE);
}

nativeCreate 会执行 Thread::CreateNativeThread 函数,这个函数才是最终创建线程的地方,它的实现在 Thread.cc 这个对象中,并且在这个函数中会调用 FixStackSize 方法将 stack_size 调整为 1M,所以前面那个疑问在这里就解决了,即使我们将 stack_size 设置为 0,这里依然会被调整。我们继续往下分析,看看一个线程究竟是怎样被创建出来的?


void Thread::CreateNativeThread(JNIEnv* env, jobject java_peer, size_t stack_size, bool is_daemon) {
……
// 调整 stack_size,默认值为 1 M
stack_size = FixStackSize(stack_size);
……

if (child_jni_env_ext.get() != nullptr) {
pthread_t new_pthread;
pthread_attr_t attr;
child_thread->tlsPtr_.tmp_jni_env = child_jni_env_ext.get();
CHECK_PTHREAD_CALL(pthread_attr_init, (&attr), "new thread");
CHECK_PTHREAD_CALL(pthread_attr_setdetachstate, (&attr, PTHREAD_CREATE_DETACHED),
"PTHREAD_CREATE_DETACHED");
CHECK_PTHREAD_CALL(pthread_attr_setstacksize, (&attr, stack_size), stack_size);
// 创建线程
pthread_create_result = pthread_create(&new_pthread,
&attr,
Thread::CreateCallback,
child_thread);
CHECK_PTHREAD_CALL(pthread_attr_destroy, (&attr), "new thread");

if (pthread_create_result == 0) {
child_jni_env_ext.release(); // NOLINT pthreads API.
return;
}
}

……
}

在上面简化后的代码中我们可以看到,CreateNativeThread 的源码实现最终调用的是 pthread_create 函数,它是一个 Linux 函数,而 pthread_create 函数最终会调用 clone 这个内核函数。clone 函数会根据传入的 stack 大小,通过 mmap 函数申请一块对应大小的虚拟内存,并且创建一个进程。


int clone(int (*fn)(void * arg), void *stack, int flags, void *arg);

所以,对于 Linux 系统来说,一个线程实际是一个精简的进程。我们创建线程时,最终会执行 clone 这个内核函数去创建一个进程,通过查看官方文档也能看到,Clone 函数实际上会创建一个新的进程(These system calls create a new ("child") process, in a manner similar to fork)。


image.png


这里我就不继续深入介绍 Linux 中线程的原理了,如果你有兴趣可以参考这篇文章 《掌握 Android 和 Java 线程原理》。


除了通过线程的创建流程可以证明一个线程需要占用 1M 大小的虚拟内存,我们还能在 maps 文件中证明这一点,还是拿前面篇章提到的“设置”这个系统应用的 maps 文件为例,也能发现 anno:stack_and_tls 也就是线程的虚拟内存,大小为 1M 左右。


image.png


理解了一个线程会占用 1M 大小的虚拟内存,我们自然而然也能想到通过减少线程的数量和减少每个线程所占用的虚拟内存大小来进行优化。接下来,我们就详细了解一下如何实现这两种方案。


减少线程数量


首先是减少线程的数量,我们主要有 2 种手段:




  1. 在应用中使用统一的线程池;




  2. 将应用中的野线程及野线程池进行收敛。




Java 开发者应该都知道线程池,但有的人认知可能不深。实际上,线程池是非常重要的知识点,需要我们熟悉并能熟练使用的。线程池对应用的性能提升有很大的帮助,它可以帮助我们更高效和更合理地使用线程,提升应用的性能。但这里就不详细介绍线程池的使用了,在后面的章节中我们会深入来讲线程池的使用。如果你不熟悉线程池,那我建议你尽快熟悉起来,这里主要针对如何减少线程数这个方向,介绍一下线程池中线程数量的最优设置。


对于线程池,我们需要手动设置核心线程数和最大线程数。核心线程是不会退出的线程,被线程池创建之后会一直存在。最大线程数是该线程池最大能达到的线程数量,当达到最大线程数后,线程池处理新的任务便当做异常,放在兜底逻辑中处理。那么,这两个线程数设置成多少比较合适呢?这个问题也经常作为面试题,需要引起注意。


线程池可以分为 CPU 线程池和 IO 线程池,CPU 线程池用来处理 CPU 类型的任务,如计算,逻辑等操作,需要能够迅速响应,但任务耗时又不能太久。那些耗时较久的任务,如读写文件、网络请求等 IO 操作便用 IO 线程池来处理,IO 线程池专门处理耗时久,响应又不需要很迅速的任务。因此,对于 CPU 的线程池,我们会将核心线程数设置为该手机的 CPU 核数,理想状态下每一个核可以运行一个线程,这样能减少 CPU 线程池的调度损耗又能充分发挥 CPU 性能。


至于 CPU 线程池的最大线程数,和核心线程数保持一致即可。 因为当最大线程数超过了核心线程数时,反倒会降低 CPU 的利用率,因为此时会把更多的 CPU 资源用于线程调度上,如果 CPU 核数的线程数量无法满足我们的业务使用,很大可能就是我们对 CPU 线程池的使用上出了问题,比如在 CPU 线程中执行了 IO 阻塞的任务。


对于 IO 线程池,我们通常会将核心线程数设置为 0 个,而且 IO 线程池并不需要响应的及时性,所以将常驻线程设置为 0 可以减少该应用的线程数量。但并不是说这里一定要设置为 0 个,如果我们的业务 IO 任务比较多,这里也可以设置为不大于 3 个数量。对于 IO 线程池的最大线程数,则可以根据应用的复杂度来设置,如果是中小型应用且业务较简单设置 64 个即可,如果是大型应用,业务多且复杂,可以设置成 128 个


可以看到,如果业务中所有的线程都使用公共线程池,那即使我们将线程的数量设置得非常宽裕,所有线程加起来所占用的虚拟内存也不会超过 200 M。但现实情况下是,应用中总会有大量地方不遵守规范,独自创建线程或者线程池,我们称之为野线程或者野线程池。那如何才能收敛野线程和野线程池呢?


对于简单的应用,我们一个个排查即可,通过全局搜索 new Thread() 线程创建代码,以及全局搜索 newFixedThreadPool 线程池创建代码,然后将不合规范的代码,进行修改收敛进公共线程池即可。


但如果是一个中大型应用,还大量使用了二方库、三方库和 aar 包等,那全局搜索也不管用了,这个时候就需要我们使用字节码操作的方式了,技术方案还是前面文章介绍过的 Lancet,通过 hook 住 newFixedThreadPool 创建线程池的函数,并在函数中将线程池的创建替换成我们公共的线程池,就能完成对线程池的收敛。


public class ThreadPoolLancet {

@TargetClass("java.util.concurrent.Executors")
@Proxy(value = "newFixedThreadPool")
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
// 替换并返回我们的公共线程池
……
}

@TargetClass("java.util.concurrent.Executors")
@Proxy(value = "newFixedThreadPool")
public static ExecutorService newFixedThreadPool(int nThreads) {
// 替换并返回我们的公共线程池
……
}
}

收敛完了野线程池,那直接使用 new Thread() 创建的野线程又该怎么收敛呢? 对于三方库中的野线程,我们没有太好的收敛手段,因为即使 Thread 的构造函数被 hook 住了,也不能将其收敛到公共线程池中。好在我们使用的三方库大都已经很成熟并经过大量用户验证过,直接使用野线程的地方会很少。我们可以采用 hook 住 Thread 的构造函数并打印堆栈的方式,来确定这个线程是不是通过线程池创建出来的,如果三方库中确实有大量的野线程,那么我们只能将源码下载下来之后手动修改了。


减少线程占用的虚拟内存


在刚才讲解 CreateNativeThread 源码的时候我们讲过,该函数会执行 FixStackSize 方法将 stack_size 调整为 1M。那结合前面各种 hook 的案例,我们很容易就能想到,通过 hook FixStackSize 这个函数,是不是可以将 stack_size 的从 1M 减少到 512 KB 了呢? 当时是可以的,但是这个时候我们没法通过 PLT Hook 的方案来实现了,而是要通过 Inline Hook 方案实现,因为 FixStackSize 是 so 库内部函数的调用,所以只有 FixStackSize 才能实现。


那如果我们想用 PLT Hook 方案来实现可以做到么?其实也可以。CreateNativeThread 是位于 libart.so 中的函数,但是 CreateNativeThread 实际是调用 pthread_create 来创建线程的,而 pthread_create 是位于 libc.so 库中的函数,如果在 CreateNativeThread 中调用 pthread_create ,同样需要通过走 plt 表和 got 表查询地址的方式,所以我们通过 bhook 工具 hook 住 libc.so 库中的 pthread_create 函数,将入参 &attr 中的 stack_size 直接设置成 512KB 即可,实现起来也非常简单,一行代码即可。


static int AdjustStackSize(pthread_attr_t const* attr) {
pthread_attr_setstacksize(attr, 512 * 1024);
}

至于如何 hook 住 pthread_create 这个函数的方法也非常简单,通过 bhook 也是一行代码就能实现,前面的篇章已经讲过怎么使用了,所以这个方案剩下的部分就留给你自己去实践啦。


除了 Native Hook 方案,我们还能在 Java 层通过字节码操作的方式来实现该方案。stack_size 不就是通过 Java 层传递到 Native 层嘛,那我们直接在 Java 层调整 stack_size 的大小就可以了,但在这之前之前,要先看看在 FixStackSize 函数中是如何调整 stack_size 大小的。


static size_t FixStackSize(size_t stack_size) {

if (stack_size == 0) {
stack_size = Runtime::Current()->GetDefaultStackSize();
}

stack_size += 1 * MB;

……

return stack_size;
}

FixStackSize 函数的源码实现很简单,就是通过 stack_size += 1 * MB 来设置 stack_size 的:如果我们传入的 stack_size 为 0 时,默认大小就是 1 M ;如果我们传入的 stack_size 为 -512KB 时,stack_size 就会变成 512KB(1M - 512KB)。那我们是不是只用带有 stackSize 入参的构造函数去创建线程,并且设置 stackSize 为 -512KB 就行了呢?


public Thread(ThreadGroup group, Runnable target, String name,
long stackSize)
{
this(group, target, name, stackSize, null, true);
}

是的,但是因为应用中创建线程的地方太多很难一一修改,而且我们实际不需要这样去修改。前面我们已经将应用中的线程全部收敛到公共线程池中去创建了,所以只需要修改公共线程池中创建的线程方式就可以了,并且线程池刚好也可以让我们自己创建线程,那只需要传入自定义的 ThreadFactory 就能实现需求。


image.pngimage.png

在我们自定义的 ThreadFactory 中,创建 stack_size 为 - 512 kb 的线程,这么一个简单的操作就能减少线程所占用的虚拟内存。


image.png


当我们将应用中线程栈的大小全改成 512 kb 后,可能会导致一些任务比较重的线程出现栈溢出,此时我们可以通过埋点收集会栈溢出的线程,不修改这部分线程的大小即可。总的来说,这是一个容易落地且投入产出比高的方案。


通过上面的方案介绍,我们也可以看到,减少一个线程所占用的虚拟内存的方案很多,可以通过 Native Hook,也可以通过 Java 代码直接修改。我们在做业务或者性能相关的工作时,往往都有多个实现方案,但是我们在敲定最终方案时,始终要选择最简单、最稳定且投入产出比最高的方案。


多进程架构优化


在 Java 堆内存优化中,我们已经讲到了可以通过多进程优化,那对于虚拟内存,我们依然可以通过多进程的架构来优化。比如说,下面这些业务我都建议你放在独立的进程中:




  1. WebView 相关的业务




  2. 小程序相关的业务




  3. Flutter 相关的业务




  4. RN 相关的业务




这些业务都是虚拟内存占用的大户,用独立的进程来承载,会减少很多虚拟内存的占用,也会减少相应的异常情况。并且,将这些业务放在子进程中也很简单,只需要在承载这些业务的 activity 的 mainfest 配置文件中添加 android:process = "子进程名" 即可。需要注意的是,如果我们把业务放在子进程,就没法直接和主进程通信了,需要借助 Binder 跨进程通信的方式来完成。


当然,你还可能会担心把这些业务放在独立进程后,会影响这些业务的启动速度,其实这都可以通过各种优化方案来解决,比如预启动子进程等。在后面速度提升优化的章节中,我们会进行详细讲解。


小结


这一节课我们介绍了两种虚拟内存优化方案,如下图:


image.png


这两种优化方案相对简单,容易落地,投入产出比高。对于一个中小型应用来说,这两个方案几乎能保证 32 位手机上有足够可用的虚拟内存了。如果这两个方案落地后,还是会有因虚拟内存不足导致的应用崩溃问题,我们就需要接着用“黑科技”手段来进行优化了,所以在下一篇文章中,会接着带大家看看有哪些“黑科技”可以用在虚拟内存优化上,它们又能带来什么样的效果!


作者:helson赵子健
来源:juejin.cn/post/7209306358582853688
收起阅读 »

如何取消 script 标签发出的请求

web
问题 之前在业务上有这样一个场景,通过 script 标签动态引入了一个外部资源,具体方式是这样的 const script = document.createElement('script'); script.src = 'xxx'; script.asyn...
继续阅读 »

问题


之前在业务上有这样一个场景,通过 script 标签动态引入了一个外部资源,具体方式是这样的


const script = document.createElement('script');
script.src = 'xxx';
script.async = true;
document.body.appendChild(script);

最近发现在某些情况下需要取消这个请求,因此对取消script标签发出的请求的方法进行研究。


取消请求的几种方式


取消 XMLHttpRequest 请求


// 发送请求
const xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.send();
// 1s后取消请求的两种方法
// a. 设置超时时间属性,在 IE 中,超时属性可能只能在调用 open()方法之后且在调用 send()方法之前设置。
xhr.timeout = 1000;
// b. 利用abort方法
setTimeout(() => {
xhr.abort();
}, 1000);

取消 fetch 请求


fetch请求的取消主要依赖于AbortController对象,当fetch 请求初始化时,我们将 AbortSignal 作为一个选项传递进入请求的选项对象中(下面的 {signal})。


const controller = new AbortController();
fetch(url, { signal: controller.signal });
// 1s后取消请求
setTimeout(() => {
controller.abort();
}, 1000);

取消 axios 请求


取消 axios 请求同样依赖于 AbortController 对象。


const controller = new AbortController();
axios.get(url, { signal: controller.signal });
// 1s后取消请求
setTimeout(() => {
controller.abort();
}, 1000);

取消使用script标签


通过对网上的资料进行整理,并没有发现直接取消 script 标签发起的请
求的方法。并且当请求发出后对 script 进行的操作(如删除 dom 节点)也不会造成影响。那么能不能将 script 发起的请求改为使用以上三种方法之一来实现呢?


改为 fetch 方法


我首先尝试了 fetch 方法。通过使用 fetch 方法对网址进行请求,我发现请求得到的类型是一个 ReadableStream 对象。
image.png
MDN上提供了一种方法可以获取到 ReadableStream 对象中的内容:


fetch('https://www.example.org')
.then((response) => response.body)
.then((rb) => {
const reader = rb.getReader();
return new ReadableStream({
start(controller) {
// The following function handles each data chunk
function push() {
// "done" is a Boolean and value a "Uint8Array"
reader.read().then(({ done, value }) => {
// If there is no more data to read
if (done) {
console.log('done', done);
controller.close();
return;
}
// Get the data and send it to the browser via the controller
controller.enqueue(value);
// Check chunks by logging to the console
console.log(done, value);
push();
});
}
push();
},
});
})
.then((stream) =>
// Respond with our stream
new Response(stream, { headers: { 'Content-Type': 'text/html' } }).text()
)
.then((result) => {
// Do things with result
console.log(result);
});

使用这种方法我就通过 fetch 方法获取到了原来 script 标签请求的内容,也就可以使用 AbortController 来控制请求的取消。


改为 XMLHttpRequest 方法


尝试使用 fetch 方法解决问题之后,我又对 XMLHttpRequest 进行了尝试,发现这种方法更加简便,获取的请求内包含一个 responseText 字段就是我需要的内容,并且在请求未成功或尚未发送的情况下这个值为 null ,也就更方便进行请求是否成功的判断。


结论


对于 script 标签发出的请求我们无法取消,但是我们可以通过其他的方法来达到 script 标签的效果,因为 XMLHttpRequest 已经足够简便,我就没有对 axios 进行尝试,相信也肯定可以达到同样的目标,有兴趣的同学可以尝试一下。


作者:Maaarch
来源:juejin.cn/post/7208092574162419770
收起阅读 »

鹅厂七年半,写在晋升失败的不眠之夜

夜半惊醒 看了下时间,凌晨4:37,辗转反侧。3.7号大领导告诉我这次10升11没通过,这是我知道晋级失败的第4个夜晚,从震惊到否认,到愤怒,到麻木,再到冷静。我的技术能力、思考深度、攻坚能力明明是小组里突出的一个,但在此次技术职级的晋升上,却名落孙山,落在同...
继续阅读 »

夜半惊醒


看了下时间,凌晨4:37,辗转反侧。3.7号大领导告诉我这次10升11没通过,这是我知道晋级失败的第4个夜晚,从震惊到否认,到愤怒,到麻木,再到冷静。我的技术能力、思考深度、攻坚能力明明是小组里突出的一个,但在此次技术职级的晋升上,却名落孙山,落在同组小伙伴之后。技术人员,终究困于技术的窠臼。


工作经历


浑浑噩噩的四年


我毕业就进入鹅厂工作,15年,正是鹅厂大杀四方的一年,至今犹记得当时拿到鹅厂 offer 时的兴奋和同学眼中的艳羡。来了公司后,跟着项目组开始创新项目,一眼望到头的挣不来钱,浑浑噩噩的干了3年,和一起进公司的小伙伴们收入差距越来越大。


好在我的驱力不错,既然挣不到钱,那就好好积累技术能力,提升自己。那段时间,周围没有伙伴,没有对比,没有好的机会,只剩下我一个人慢慢悠悠的学习积累,我以为自己足够努力,没有荒废时间;后来才发现自己其实是井底之蛙,一起入职的小伙伴付出了百倍的努力和数不清的不眠之夜,1年多就已经能 cover 当时的业务了,没过2年就已经提干了。


2018年底,我满怀信心去答辩后台 3.1,没过。我当时还觉得评委装逼,挑刺,后来回头看写的都是s,一点含量都没有。


时来运转


2019年3月,答辩刚结束。部门业务调整,我们被整组划到了一个相对挣钱的业务上,leader 也调整成后台 leader。


新业务还是挣钱,凭着第一个项目积累的技术和项目经验,我得到了新领导的认可,第一年干的轻轻松松就挣到了远远超出之前的收入,和之前累死累活拿温饱线形成鲜明对比,可见大厂里跟对项目是多么的重要。


只是没想到第一年就是巅峰,后面随着贸易战、反垄断,鹅厂形势越来越差,我们业务收入也受到极大影响,人员也越来越冗余。挣的钱少了,分蛋糕的人还多了,可想而知,收入越来越差。


在这里,我成功从8级升到了10级工程师。说起来我们这一届实名惨,第一批毕业要等1年才能晋级,最后一批8升9需要公司范围内通道评审,第一批9升10走BG内评审,第一批10升11走部门内评审,全程试验品,一样没落下。


10升11


去年年底公司晋升改革,10升11评审下放部门,职级和待遇不再挂钩,不仅看重”武功“,还看中”战功“,同时传闻这次晋升名额极少。大领导找我谈话,要不就别去了?我想了想这两年的技术和项目积累,不甘心。


整个中心,就我和同组的小伙伴两个人一起去,我挑的是个一直在做的有技术挑战、持续优化的项目,小伙伴挑的是挣钱的项目。我准备了几个周末,小伙伴临时抱佛脚,结果小伙伴过了,我没过。评委说,我过度设计(优化过头)、没有实战演练容灾。


我和大领导说这个和我预期差太多了,我和小伙伴一起预答辩过,都知道讲的啥,咱是技术评审,我的项目技术含量、架构和挑战明明更好,为啥是我没过?大领导说你是认知偏差,人家讲的确实好。不忿,遂找评委 battle,咱要真按这个说法,能不能对参加评审的项目一致对待,不要双标?且不说我是不是过度设计,凭啥对我的项目就要求容灾演练,对别人的项目就视而不见?评委不语,你的项目对部门价值没产生什么收益和价值。


部门400+人,4个晋升11级名额,大概率是一个中心一个。一个技术评审,糅合了许多超过技术方面的考量,业务挣不挣钱和技术有啥关系?技术好的业务就能挣钱?业务挣钱多的是技术好的原因?


我以前也晋级失败过,但是我认输,因为相对而言,整个BG一起评审,你没过就是你技术差,其他因素影响没有这么大。这次明明是我更有技术深度、更投心思在优化上,却事与愿违。


反思与感悟


反思一下。千头万绪,毛主席说,那是没有抓住主要矛盾。


大的方面上讲:这两年大环境差,公司收入减少,需要降本增效。我要是管理层,也会对内部薪资成本、晋升等要求控制增速;政策制定上,也会将资源更倾斜于现金流业务,控制亏损业务的支出。在这个大的背景下,公司不愿意养“能力强,战功少”的技术骨干了;更愿意养“能力强,战功多”的人员。希望员工把精力都聚焦在业务挣钱上。


部门层面上讲:和公司政策一脉相承。晋升名额少,那紧着挣钱的中心、挣钱的小组、挣钱的个人给。这个意义上讲,职级体系已经不是衡量技术能力的标尺了。你技术能力强,没用。你得是核心,得是领导认可的人。


中心层面上讲:要争取名额,否则怎么团结手底下人干活。而且名额要给业务干活干的最出色的那个人,其他人就往后稍稍。我要是我领导,年前也会劝退我的。毕竟我技术能力虽然强,更多的是问题专家的角色,而不是业务核心。


且预测一下,中心之间肯定进行了各种资源置换。评审估计流于形式。慢说我被挑出来了问题,就是挑不出来问题也得给你薅下来。我就是和小伙伴互换项目去讲,估计还是小伙伴过,到时候评委就该说我“你做这东西有啥难度,我叫个毕业生都能来做”了。此处我想起了一副对联,不太合适,就是很搞笑。“说你行你就行不行也行,说你不行你就不行行也不行”,横批是“不服不行”。



从个人角度讲,付出和收获不成正比,难受吗?那肯定的。但谁让方向一开始就错了呢?这世界不公平的事情多了,不差这一个。



重新出发


综上来说,短期内,不改变发力方向到业务上,后面可以说晋升无望。以前老是觉得自己技术能力强,心高气傲,心思没在业务上,勤于术而懒于道,实在是太过幼稚。做了几年都对很多东西不甚了了。


今后,需要趁着还能拼的时候拼一下,潜心业务。编码和开发只是很小一部分,更要从:



  1. 业务大局出发,实时数据驱动,监控、统计一定要完善,要有看板,否则无法衡量。

  2. 主动探索业务,给产品经理出主意,一起合力把产品做上去做大,提升对业务的贡献,探索推荐、探索推广渠道等等。产品做不上去,个人也没机会发展。

  3. 多做向上管理,和领导、大领导多沟通。做好安排的每一件小事,同时主动汇报,争取重获信任。

  4. 主动承担,做一个领导眼里靠谱放心的人。

  5. 多思考总结,多拔高提升,不要做表面的勤奋工作,看似没有浪费时间,实则每分每秒都在浪费时间。

  6. 多社交,多沟通,多交流,打破技术人员的牢笼。


凡事都有两面性,福兮祸所依,祸兮福所倚。多受挫折、早受挫折不是坏事。


2023,就立个

作者:醉梦星河
来源:juejin.cn/post/7208907027840630840
flag 在这里吧。

收起阅读 »

又干倒一家公司,我悟了

目前这家公司由于是政府项目,好多项目周期过长,未能及时回款,导致资金链断裂,摇摇欲坠,开始裁员。虽然由于项目资源稀缺,需要我,不至于裁到我头上,但是生存压力迫使我看向了外边,很快决定了入职新公司,同事请领导们吃散伙饭,有被裁的大佬,有留下的,有主动辞职的,各自...
继续阅读 »

目前这家公司由于是政府项目,好多项目周期过长,未能及时回款,导致资金链断裂,摇摇欲坠,开始裁员。虽然由于项目资源稀缺,需要我,不至于裁到我头上,但是生存压力迫使我看向了外边,很快决定了入职新公司,同事请领导们吃散伙饭,有被裁的大佬,有留下的,有主动辞职的,各自怀着复杂的心情,聊了几个小时,现总结一下,作为教训:


1. 技术方面



  • 谨慎对待新技术,技术路线要有延续性,做大版本更新,而不是每个项目都尝试新的路线方案。

  • 小项目,以高复用和快速实现为首要目的,不频繁更换框架。


2. 需求方面



  • 以产品或项目的需求导向而不是以技术为纲。

  • 需求方也许不懂要什么,产品去引导,简单到复杂,别把自己带进技术化和复杂化的迷宫。

  • 有些急的项目前期可以快速简单实现,先验证,而不是等原型、设计,错失良机。


3. 人员方面



  • 主要技术领导别追求完美,要控主方向和整体,而不是较真细节。

  • 十个项目一个成,架构师们歇歇吧,杀鸡不兴用牛刀,尽可能简单实现,别整太复杂的框架和路线。

  • 团队内聚,可以将产品、测试长期配置到各开发团队,减少成本矛盾和沟通问题。

  • boss需要及时频繁的与主要领导沟通,定方向,放大权,用人不疑。


4. 企业管理方面



  • 切忌盲目自信,疯狂扩张团队,利润和成本须同步,不能因短期项目招兵买马。

  • 适时而果断拿起手术刀,坚决断臂求生,及时且合规地删减人员,要有人情但不能妇人之仁、拖泥带水,对个体和公司都好。


最后,警惕以上问题,期待自己尽快找到更

作者:Adam289
来源:juejin.cn/post/7208534700223184951
明确的方向,去奋斗!

收起阅读 »

28岁小公司程序员,无车无房不敢结婚,要不要转行?

大家好,这里是程序员晚枫,又来分享程序员的职场故事了~ 今天分享的这位朋友叫小青,我认识他2年多了。以前从事的是土木行业,2年前找我咨询转行程序员的学习路线和职业规划后,通过自学加入了一家创业公司,成为了一名Java开发。 **最近他遇到了新的职业上的困惑,又...
继续阅读 »

大家好,这里是程序员晚枫,又来分享程序员的职场故事了~


今天分享的这位朋友叫小青,我认识他2年多了。以前从事的是土木行业,2年前找我咨询转行程序员的学习路线和职业规划后,通过自学加入了一家创业公司,成为了一名Java开发。


**最近他遇到了新的职业上的困惑,又找我聊了一下,我也没想到好的解决方法,**大家可以一起看一下~下面是沟通的核心内容。


1、他的问题


小青是中原省份省会城市的大专毕业,毕业季就去了帝都实习和工作。后来发现同学中有转行程序员的,薪资很诱惑,所以就找到我咨询如何学习和转行,现在一家帝都创业公司负责后端开发。工资1w出头。


今年已经28岁了,有一个女朋友,最近女方家里催他结婚,他自己也有结婚的意愿。但是考虑到自己人在大城市,无车无房,创业公司的工作也不稳定,以后吃住花销,结婚后养孩子的花销,再看看自己1w多的工资,女朋友做财务,一个月到手不到1w。


双方家里也都是普通家庭,给不了什么实质的资助,靠自己目前的收入根本不敢想象成家后压力有多大。


所以目前非常迷茫, 不知道自己在28岁这个年龄应该怎么办,应不应该成家?应该怎样提高收入?


虽然自己很喜欢程序员这份工作,但是感觉自己学历不好,天花板有限,程序员还能继续干下去吗?


2、几个建议


平时收到后台读者的技术问题或者转行的困惑,我都会尽力给一些详尽的回复。


但是这次听到小青的问题,说实话,我也不知道该说什么。


在28岁这种黄金年龄,想去大城市奋斗一番也是人之常情,但因为现实的生活压力,却不得不面临着选择离开大城市或者转行到自己不喜欢但是更务实的职业里去。


如果想继续留在帝都,我能想到的有以下几个办法:



  • 首先,如果想继续从事程序员工作,努力提高收入。最快的办法就是跳槽,已经工作2年多了,背一背八股文,总结一下项目经验,应该是可以跳槽到一家更好的公司了。

  • 其次,探索另一个副业收入,例如自媒体。因为我自己就是通过在各个平台开通了自媒体账号:程序员晚枫,分享自己的程序员学习经验获得粉丝,进而得到自媒体收入的。小青也可以实事求是的分享一下自己大专毕业从建筑工作转行到程序员的经验,应该也能帮助到别人。

  • 最后,努力提高学历,想继续在程序员这行卷出高收入,趁着年轻,获得一个本科或者本科以上的学历还是很有帮助的。


受限于自己的经验,我只能给出以上几个建议了。


大家还有什么更有效的建议,欢迎在评论区交流~


3、写在最后


说句题外话,很多人都觉得程序员工资高想来转行,但其实程序员和其它行业一样,高收入的只是一小部分,而且那部分人既聪明又努力。


最重要的是,高收入的那部分人里,大部分都不是转行的,而是在一个专业深耕了多年,最终获得了应有的报酬。


无意冒犯,但听完小青的经历,我依然要给大专以下,想转行程序员拿高薪的朋友提个醒:如果不是十分热爱,请务必三思~


作者:程序员晚枫
来源:juejin.cn/post/7209447968218841144
收起阅读 »

项目中多级缓存设计实践总结

缓存的重要性 简而言之,缓存的原理就是利用空间来换取时间。通过将数据存到访问速度更快的空间里以便下一次访问时直接从空间里获取,从而节省时间。 我们以CPU的缓存体系为例: CPU缓存体系是多层级的。分成了CPU -> L1 -> L2 ->...
继续阅读 »

缓存的重要性


简而言之,缓存的原理就是利用空间来换取时间。通过将数据存到访问速度更快的空间里以便下一次访问时直接从空间里获取,从而节省时间。

我们以CPU的缓存体系为例:


图片.png


CPU缓存体系是多层级的。分成了CPU -> L1 -> L2 -> L3 -> 主存。我们可以得到以下启示。



  • 越频繁使用的数据,使用的缓存速度越快

  • 越快的缓存,它的空间越小


而我们项目的缓存设计可以借鉴CPU多级缓存的设计。


关于多级缓存体系实现在开源项目中:github.com/valarchie/A…


缓存分层


首先我们可以给缓存进行分层。在Java中主流使用的三类缓存主要有:



  • Map(原生缓存)

  • Guava/Caffeine(功能更强大的内存缓存)

  • Redis/Memcached(缓存中间件)


在一些项目中,会一刀切将所有的缓存都使用Redis或者Memcached中间件进行存取。

使用缓存中间件避免不了网络请求成本和用户态和内核态的切换。 更合理的方式应该是根据数据的特点来决定使用哪个层级的缓存。


Map(一级缓存)


项目中的字典类型的数据比如:性别、类型、状态等一些不变的数据。我们完全可以存在Map当中。

因为Map的实现非常简单,效率上是非常高的。由于我们存的数据都是一些不变的数据,一次性存好并不会再去修改它们。所以不用担心内存溢出的问题。 以下是关于字典数据使用Map缓存的简单代码实现。


/**
* 本地一级缓存 使用Map
*
* @author valarchie
*/
public class MapCache {
private static final Map<String, List<DictionaryData>> DICTIONARY_CACHE = MapUtil.newHashMap(128);

static {
initDictionaryCache();
}

private static void initDictionaryCache() {

loadInCache(BusinessTypeEnum.values());
loadInCache(YesOrNoEnum.values());
loadInCache(StatusEnum.values());
loadInCache(GenderEnum.values());
loadInCache(NoticeStatusEnum.values());
loadInCache(NoticeTypeEnum.values());
loadInCache(OperationStatusEnum.values());
loadInCache(VisibleStatusEnum.values());

}

public static Map<String, List<DictionaryData>> dictionaryCache() {
return DICTIONARY_CACHE;
}

private static void loadInCache(DictionaryEnum[] dictionaryEnums) {
DICTIONARY_CACHE.put(getDictionaryName(dictionaryEnums[0].getClass()), arrayToList(dictionaryEnums));
}

private static String getDictionaryName(Class<?> clazz) {
Objects.requireNonNull(clazz);
Dictionary annotation = clazz.getAnnotation(Dictionary.class);

Objects.requireNonNull(annotation);
return annotation.name();
}

@SuppressWarnings("rawtypes")
private static List<DictionaryData> arrayToList(DictionaryEnum[] dictionaryEnums) {
if(ArrayUtil.isEmpty(dictionaryEnums)) {
return ListUtil.empty();
}
return Arrays.stream(dictionaryEnums).map(DictionaryData::new).collect(Collectors.toList());
}
}

Guava(二级缓存)


项目中的一些自定义数据比如角色,部门。这种类型的数据往往不会非常多。而且请求非常频繁。比如接口中经常要校验角色相关的权限。我们可以使用Guava或者Caffeine这种内存框架作为二级缓存使用。

Guava或者Caffeine的好处可以支持缓存的过期时间以及缓存的淘汰,避免内存溢出。

以下是利用模板设计模式做的GuavaCache模板类。


/**
* 缓存接口实现类 二级缓存
* @author valarchie
*/
@Slf4j
public abstract class AbstractGuavaCacheTemplate<T> {

private final LoadingCache<String, Optional<T>> guavaCache = CacheBuilder.newBuilder()
// 基于容量回收。缓存的最大数量。超过就取MAXIMUM_CAPACITY = 1 << 30。依靠LRU队列recencyQueue来进行容量淘汰
.maximumSize(1024)
// 基于容量回收。但这是统计占用内存大小,maximumWeight与maximumSize不能同时使用。设置最大总权重
// 没写访问下,超过5秒会失效(非自动失效,需有任意put get方法才会扫描过期失效数据。但区别是会开一个异步线程进行刷新,刷新过程中访问返回旧数据)
.refreshAfterWrite(5L, TimeUnit.MINUTES)
// 移除监听事件
.removalListener(removal -> {
// 可做一些删除后动作,比如上报删除数据用于统计
log.info("触发删除动作,删除的key={}, value={}", removal.getKey(), removal.getValue());
})
// 并行等级。决定segment数量的参数,concurrencyLevel与maxWeight共同决定
.concurrencyLevel(16)
// 开启缓存统计。比如命中次数、未命中次数等
.recordStats()
// 所有segment的初始总容量大小
.initialCapacity(128)
// 用于测试,可任意改变当前时间。参考:https://www.geek-share.com/detail/2689756248.html
.ticker(new Ticker() {
@Override
public long read() {
return 0;
}
})
.build(new CacheLoader<String, Optional<T>>() {
@Override
public Optional<T> load(String key) {
T cacheObject = getObjectFromDb(key);
log.debug("find the local guava cache of key: {} is {}", key, cacheObject);
return Optional.ofNullable(cacheObject);
}
});

public T get(String key) {
try {
if (StrUtil.isEmpty(key)) {
return null;
}
Optional<T> optional = guavaCache.get(key);
return optional.orElse(null);
} catch (ExecutionException e) {
log.error("get cache object from guava cache failed.");
e.printStackTrace();
return null;
}
}

public void invalidate(String key) {
if (StrUtil.isEmpty(key)) {
return;
}

guavaCache.invalidate(key);
}

public void invalidateAll() {
guavaCache.invalidateAll();
}

/**
* 从数据库加载数据
* @param id
* @return
*/
public abstract T getObjectFromDb(Object id);
}

我们将getObjectFromDb方法留给子类自己去实现。以下是例子:


/**
* @author valarchie
*/
@Component
@Slf4j
@RequiredArgsConstructor
public class GuavaCacheService {

@NonNull
private ISysDeptService deptService;

public final AbstractGuavaCacheTemplate<SysDeptEntity> deptCache = new AbstractGuavaCacheTemplate<SysDeptEntity>() {
@Override
public SysDeptEntity getObjectFromDb(Object id) {
return deptService.getById(id.toString());
}
};
}

Redis(三级缓存)


项目中会持续增长的数据比如用户、订单等相关数据。这些数据比较多,不适合放在内存级缓存当中,而应放在缓存中间件Redis当中去。Redis是支持持久化的,当我们的服务器重新启动时,依然可以从Redis中加载我们原先存储好的数据。


但是使用Redis缓存还有一个可以优化的点。我们可以自己本地再做一个局部的缓存来缓存Redis中的数据来减少网络IO请求,提高数据访问速度。 比如我们Redis缓存中有一万个用户的数据,但是一分钟之内可能只有不到1000个用户在请求数据。我们便可以在Redis中嵌入一个局部的Guava缓存来提供性能。以下是RedisCacheTemplate.


/**
* 缓存接口实现类 三级缓存
* @author valarchie
*/
@Slf4j
public class RedisCacheTemplate<T> {

private final RedisUtil redisUtil;
private final CacheKeyEnum redisRedisEnum;
private final LoadingCache<String, Optional<T>> guavaCache;

public RedisCacheTemplate(RedisUtil redisUtil, CacheKeyEnum redisRedisEnum) {
this.redisUtil = redisUtil;
this.redisRedisEnum = redisRedisEnum;
this.guavaCache = CacheBuilder.newBuilder()
// 基于容量回收。缓存的最大数量。超过就取MAXIMUM_CAPACITY = 1 << 30。依靠LRU队列recencyQueue来进行容量淘汰
.maximumSize(1024)
.softValues()
// 没写访问下,超过5秒会失效(非自动失效,需有任意put get方法才会扫描过期失效数据。
// 但区别是会开一个异步线程进行刷新,刷新过程中访问返回旧数据)
.expireAfterWrite(redisRedisEnum.expiration(), TimeUnit.MINUTES)
// 并行等级。决定segment数量的参数,concurrencyLevel与maxWeight共同决定
.concurrencyLevel(64)
// 所有segment的初始总容量大小
.initialCapacity(128)
.build(new CacheLoader<String, Optional<T>>() {
@Override
public Optional<T> load(String cachedKey) {
T cacheObject = redisUtil.getCacheObject(cachedKey);
log.debug("find the redis cache of key: {} is {}", cachedKey, cacheObject);
return Optional.ofNullable(cacheObject);
}
});
}

/**
* 从缓存中获取对象 如果获取不到的话 从DB层面获取
* @param id
* @return
*/
public T getObjectById(Object id) {
String cachedKey = generateKey(id);
try {
Optional<T> optional = guavaCache.get(cachedKey);
// log.debug("find the guava cache of key: {}", cachedKey);

if (!optional.isPresent()) {
T objectFromDb = getObjectFromDb(id);
set(id, objectFromDb);
return objectFromDb;
}

return optional.get();
} catch (ExecutionException e) {
e.printStackTrace();
return null;
}
}

/**
* 从缓存中获取 对象, 即使找不到的话 也不从DB中找
* @param id
* @return
*/
public T getObjectOnlyInCacheById(Object id) {
String cachedKey = generateKey(id);
try {
Optional<T> optional = guavaCache.get(cachedKey);
log.debug("find the guava cache of key: {}", cachedKey);
return optional.orElse(null);
} catch (ExecutionException e) {
e.printStackTrace();
return null;
}
}

/**
* 从缓存中获取 对象, 即使找不到的话 也不从DB中找
* @param cachedKey 直接通过redis的key来搜索
* @return
*/
public T getObjectOnlyInCacheByKey(String cachedKey) {
try {
Optional<T> optional = guavaCache.get(cachedKey);
log.debug("find the guava cache of key: {}", cachedKey);
return optional.orElse(null);
} catch (ExecutionException e) {
e.printStackTrace();
return null;
}
}

public void set(Object id, T obj) {
redisUtil.setCacheObject(generateKey(id), obj, redisRedisEnum.expiration(), redisRedisEnum.timeUnit());
guavaCache.refresh(generateKey(id));
}

public void delete(Object id) {
redisUtil.deleteObject(generateKey(id));
guavaCache.refresh(generateKey(id));
}

public void refresh(Object id) {
redisUtil.expire(generateKey(id), redisRedisEnum.expiration(), redisRedisEnum.timeUnit());
guavaCache.refresh(generateKey(id));
}

public String generateKey(Object id) {
return redisRedisEnum.key() + id;
}

public T getObjectFromDb(Object id) {
return null;
}

}

以下是使用方式:


/**
* @author valarchie
*/
@Component
@RequiredArgsConstructor
public class RedisCacheService {
@NonNull
private RedisUtil redisUtil;

public RedisCacheTemplate<SysUserEntity> userCache;

@PostConstruct
public void init() {
userCache = new RedisCacheTemplate<SysUserEntity>(redisUtil, CacheKeyEnum.USER_ENTITY_KEY) {
@Override
public SysUserEntity getObjectFromDb(Object id) {
ISysUserService userService = SpringUtil.getBean(ISysUserService.class);
return userService.getById((Serializable) id);
}
};
}
}

缓存Key以及过期时间


我们可以通过一个枚举类来统一集中管理各个缓存的Key以及过期时间。以下是例子:


/**
* @author valarchie
*/
public enum CacheKeyEnum {

/**
* Redis各类缓存集合
*/
CAPTCHAT("captcha_codes:", 2, TimeUnit.MINUTES),
LOGIN_USER_KEY("login_tokens:", 30, TimeUnit.MINUTES),
RATE_LIMIT_KEY("rate_limit:", 60, TimeUnit.SECONDS),
USER_ENTITY_KEY("user_entity:", 60, TimeUnit.MINUTES),
ROLE_ENTITY_KEY("role_entity:", 60, TimeUnit.MINUTES),
ROLE_MODEL_INFO_KEY("role_model_info:", 60, TimeUnit.MINUTES),
;

CacheKeyEnum(String key, int expiration, TimeUnit timeUnit) {
this.key = key;
this.expiration = expiration;
this.timeUnit = timeUnit;
}

private final String key;
private final int expiration;
private final TimeUnit timeUnit;

public String key() {
return key;
}

public int expiration() {
return expiration;
}

public TimeUnit timeUnit() {
return timeUnit;
}

}

统一的使用门面


一般来说,我们在项目中设计好缓存之后就可以让其他同事写业务时直接调用了。但是让开发者去判断这个属于二级缓存还是三级缓存的话,存在心智负担。我们应该让开发者自然地从业务角度去选择某个缓存。比如他正在写部门相关的业务逻辑,就直接使用deptCache。
此时我们可以新建一个CacheCenter来统一按业务划分缓存。以下是例子:


/**
* 缓存中心 提供全局访问点
* @author valarchie
*/
@Component
public class CacheCenter {

public static AbstractGuavaCacheTemplate<String> configCache;

public static AbstractGuavaCacheTemplate<SysDeptEntity> deptCache;

public static RedisCacheTemplate<String> captchaCache;

public static RedisCacheTemplate<LoginUser> loginUserCache;

public static RedisCacheTemplate<SysUserEntity> userCache;

public static RedisCacheTemplate<SysRoleEntity> roleCache;

public static RedisCacheTemplate<RoleInfo> roleModelInfoCache;

@PostConstruct
public void init() {
GuavaCacheService guavaCache = SpringUtil.getBean(GuavaCacheService.class);
RedisCacheService redisCache = SpringUtil.getBean(RedisCacheService.class);

configCache = guavaCache.configCache;
deptCache = guavaCache.deptCache;

captchaCache = redisCache.captchaCache;
loginUserCache = redisCache.loginUserCache;
userCache = redisCache.userCache;
roleCache = redisCache.roleCache;
roleModelInfoCache = redisCache.roleModelInfoCache;
}

}

以上就是关于项目中多级缓存的实现。 如有不足恳请评论指出。


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

不同方面浅浅认识一下 Synchronized

Java中的synchronized关键字是一种用于线程同步的机制,它可以确保同一时间只有一个线程能够访问共享资源。本篇博客将从synchronized的基本用法、synchronized锁的对象、synchronized的可重入性、synchronized的...
继续阅读 »

Java中的synchronized关键字是一种用于线程同步的机制,它可以确保同一时间只有一个线程能够访问共享资源。本篇博客将从synchronized的基本用法、synchronized锁的对象、synchronized的可重入性、synchronized的锁粒度、synchronized与锁优化等多个方面来深入介绍Java中synchronized关键字的使用。


一、synchronized的基本用法


synchronized关键字可以应用于方法和代码块上,以下是synchronized的基本用法示例:


1.1 修饰方法


synchronized可以用于修饰方法,使得在调用该方法时,只有一个线程能够执行该方法,其他线程需要等待。


public synchronized void syncMethod() {
// synchronized方法体
}

1.2 修饰代码块


synchronized还可以用于修饰代码块,只有当线程获取到该代码块的锁时才能进入执行,其他线程需要等待。


public void syncBlock() {
synchronized (this) {
// synchronized代码块
}
}

二、synchronized锁的对象


synchronized锁的对象可以是类的实例对象、类对象、任意对象等。以下是synchronized锁的对象示例:


2.1 类的实例对象


当synchronized锁的对象是类的实例对象时,不同的线程对同一个实例对象的同步方法或同步代码块的访问将会被阻塞,因为它们都是使用同一个对象锁。


public synchronized void syncMethod() {
// synchronized方法体
}

public void syncBlock() {
synchronized (this) {
// synchronized代码块
}
}

2.2 类对象


当synchronized锁的对象是类对象时,不同的线程对同一个类的同步方法或同步代码块的访问将会被阻塞,因为它们都是使用同一个类锁。


public static synchronized void syncMethod() {
// synchronized方法体
}

public void syncBlock() {
synchronized (SynchronizedDemo.class) {
// synchronized代码块
}
}

2.3 任意对象


当synchronized锁的对象是任意对象时,不同的线程对同一个对象的同步方法或同步代码块的访问将会被阻塞,因为它们都是使用同一个对象锁。


public synchronized void syncMethod() {
// synchronized方法体
}

public void syncBlock() {
Object lock = new Object();
synchronized (lock) {
// synchronized代码块
}
}

三、synchronized的可重入性


synchronized具有可重入性,即一个线程已经获得了一个对象的锁,再次请求该对象的锁时仍然可以获得该锁。


public synchronized void syncMethod() {
syncMethod2();
// synchronized方法体
}

在上面的代码中,syncMethod2()方法也是一个synchronized方法,当syncMethod()方法调用syncMethod2()方法时,它仍然可以获取到该对象的锁,因为锁是可重入的。


四、synchronized的锁粒度


synchronized锁的粒度大小是需要考虑的一个问题,粒度过大会导致性能下降,粒度过小又可能导致死锁。以下是synchronized锁的粒度示例:


4.1 对象锁


对象锁的粒度最细,它只锁定对象中的一个方法或代码块。


public synchronized void syncMethod() {
// synchronized方法体
}

public void syncBlock() {
synchronized (this) {
// synchronized代码块
}
}

4.2 类锁


类锁的粒度比对象锁大,它锁定的是整个类的同步方法或同步代码块。


public static synchronized void syncMethod() {
// synchronized方法体
}

public void syncBlock() {
synchronized (SynchronizedDemo.class) {
// synchronized代码块
}
}

五、synchronized与锁优化


Java中的synchronized关键字在使用时需要考虑锁的粒度和性能问题,同时也有一些锁优化的技巧可以使用,以下是synchronized与锁优化的示例:


5.1 减小锁粒度


如果使用了对象锁,可以将锁的粒度减小到对象的某个属性上,以减少锁的粒度,从而提高程序的并发性。


public void syncBlock() {
synchronized (lock) {
// synchronized代码块
}
}

public void syncBlock2() {
synchronized (lock.getProperty()) {
// synchronized代码块
}
}

5.2 双重检查锁定


双重检查锁定技术可以减少锁的粒度,提高程序的并发性。


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

在以上代码中,通过双重检查锁定的方式,可以确保Singleton实例只被创建一次,并且提高了程序的并发性能。


5.3 锁消除


在一些情况下,编译器会自动消除一些不必要的锁,以提高程序的并发性能。


public void syncMethod() {
StringBuilder sb = new StringBuilder();
sb.append("a").append("b").append("c");
// sb对象没有被其他线程共享,可以消除锁
synchronized (sb) {
// synchronized代码块
}
}

以上代码中,由于sb对象没有被其他线程共享,因此编译器可以自动消除synchronized。


六、synchronized的局限性和替代方案


synchronized虽然是Java中一个非常重要的同步机制,但它也有一些局限性和替代方案。


6.1 局限性



  • synchronized只能保证单个线程的同步,无法保证多个线程之间的协作;

  • synchronized在多线程并发访问下会导致性能下降;

  • synchronized在死锁等异常情况下可能导致程序出现问题。


6.2 替代方案


Java中有许多替代方案可以代替synchronized实现同步,以下是一些常用的替代方案:



  • Lock:Lock是Java中提供的一个新的同步机制,它可以替代synchronized实现线程同步,具有更好的扩展性和性能;

  • Atomic:Atomic提供了一些原子操作,可以实现一些线程安全的操作;

  • ConcurrentHashMap:ConcurrentHashMap是Java中线程安全的Map,可以实现高并发访问;

  • Semaphore:Semaphore是一种计数信号量,可以用来控制对公共资源的访问。


七、总结


synchronized是Java中非常重要的同步机制,它可以保证多个线程之间的安全协作,但在使用时需要考虑锁的粒度和性能问题,同时也有一些锁优化的技巧可以使用。


除了synchronized之外,Java中还有许多替代方案可以实现线程同步,包括Lock、Atomic、ConcurrentHashMap等。在实际应用中,需要根据具体的场景选择适合的同步机制来保证程序的正确性和性能。会在后续讲解不同锁结构


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

一张架构图让我认识到前端的渺小

前言 大家好这里是阳九,一个文科转码的野路子码农,热衷于研究和手写前端工具. 今天我们不聊前端,咱们来聊聊后端,聊聊架构 目的是正视自己的地位和价值,在寒冬中保持清醒 借用若川大佬的一句话: 所知甚少,唯善学 先别问我到底是前端程序员还是后端程序员,我自己也...
继续阅读 »

前言


大家好这里是阳九,一个文科转码的野路子码农,热衷于研究和手写前端工具.


今天我们不聊前端,咱们来聊聊后端,聊聊架构


目的是正视自己的地位和价值,在寒冬中保持清醒


借用若川大佬的一句话: 所知甚少,唯善学




先别问我到底是前端程序员还是后端程序员,我自己也不知道。当然自己也很想在某个领域精进,但是业务嘛,咱们就是一块砖,哪里需要哪里搬,硬着头皮上呗


最近是在维护公司的业务引擎, 对其进行优化并添加功能,技术栈的话大体来讲, 前端是React+Node BFF,后端是GO (Gin框架+原生)


随着看后端代码的时间越来越长,作为一个切图仔,我越来越觉得恐怖。那是一种,看到了过于庞大的未知世界,并深深觉得自己的认知太少的恐怖。


因为这个项目是定制项目,通俗点来讲就是"改装车",不从头造车但是要改装,这里改改哪里改改,一旦改动的点多了,就需要你把整个项目的逻辑全部理顺。


于是乎我在这两个月里,里里外外看了几万行代码。也是硬着头皮把整个架构梳理了一遍。


先在这里放上一张整理出来的架构图


(当然这里根据原系统魔改了的很多地方,并进行了简化,并修改了名称,防止泄密,模块的大小差不多是以核心逻辑代码量来算的,前端的核心代码量有多少咱们前端er应该都懂)


XXX系统总架构图.jpg


本文目的


想通过一次后端架构分析, 让我们前端人意识到自己的不足与眼界的狭窄,我们前端er需要对一个完整的大型项目有一个整体的认知,意识到自己的不足,才能在这条路上更好的走下去。
不要满足于html拼拼页面,写写样式,做做一些简单的工作。


技术栈介绍


这里先简单介绍一下技术栈, 否则无法理解



  • 前端 React webpack antd redux ... 前端er都懂,以下省略

  • Koa框架 一个node后端框架

  • Gin框架 一个GO后端框架

  • Docker 容器引擎

  • K8S Docker集群管理

  • RabbitMQ 实现AMQP消息队列协议的代理软件,也就是消息队列,用于通信

  • GFS 分布式文件共享系统,用于大量数据访问

  • MongoDB 快读读取用数据库

  • Elastic Search 分布式数据库,进行大批量存储查询

  • SQL 传统关系型数据库

  • MobileSuit 后端框架工厂框架,用于创建独立Gin服务

  • 扩容服务 GO原生实现

  • 引擎 GO原生实现

  • 守护进程 GO原生实现


关于前端


看到左上角我特意标出来的那一小块红色的UI了吗?我们称之为 前端


数据库


mongo DB : 用于小体积数据的快速读取,用作数据中间传输(原生json,使用方便)


Elastic Search : 分布式数据库, 用于大体积,大批量数据存储和快速检索(动辄十几亿条数据,一条json数据几千个字段)


SQL: 用于存储不变的数据,比如国家信息,公司信息等等,重在稳定。


容器化部署


简单介绍一下什么是容器,当然我这里默认大家都懂。 容器提供了程序所需要运行的环境, 并将其独立隔离出来,


Docker:  是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的镜像中


k8s: 是 Google 开源的一个容器集群管理系统


架构中除了守护进程和引擎扩容服务,所有引擎,前后端服务,数据库服务都使用Docker进行容器化,并使用K8S进行统一管理。


image.png


引擎



  • 引擎扩容服务:可以在判断需要并能够提供多个引擎的时候,开启更多的引擎进行处理。

  • 树状引擎结构: 一个任务可能需要多个引擎协同参与, 而且下级引擎依赖上级引擎的结果

  • 上级引擎的结果需要经过调度服务,通过MQ进行消息传递,调度后再传递给下级引擎

  • 最终结果写入数据库


image.png


调度层




  • 任务调度器:提供任务优先级调度,任务状态管理,任务拆分,任务下放等功能




  • 结果处理器; 提供各引擎对应的结果处理解析功能,有大量的数据库查询,结果计算,字符串解析逻辑,体积非常庞大.




  • 当然优先级调度器和引擎结果处理服务是单独运行在Docker中,需要使用MQ和GFS,数据库进行数据交换




image.png


数据聚合层


也就是node写的BFF数据聚合层,将gin框架(gopher层)获取来的数据进行聚合,格式化成前端所需的数据结构,并提供前端接口。这里就不赘述了


gopher服务层


提供主体服务, 数据库IO等等,体量最大。提供了各种处理模块,接口等。


架构也是一个简单的类node三层架构,


(Router - controller - Service)
带上validator层和数据库操作层(与node中的Model层区别不大)


image.png


守护进程


原生GO写的守护进程,一个部署时直接运行在机器某端口的进程, 主要功能有3个


创建 - 监视 - 代理



  • 它将整个系统包裹起来, 用于监视各个容器的运行情况,

  • 提供了一个用于自动注册Gin框架路由的上级自研框架MobileSuit,系统的每个服务都会通过MS框架进行启动,生成一个Gin路由实例。 并挂载到总路由树上。


image.png



  • 守护进程包裹了所有的服务, 系统各个服务发出的请求都会首先被代理到守护进程上,守护进程将其统一拦截下来, 方便之后的统一请求代理处理。


image.png


对前端人和自己的话


不知道小伙伴们看到我整理出来的架构有什么看法?有没有认识到前端的渺小.
我们一定要正视自己的地位,在寒冬中保持清醒


再聊聊,为什么很多小伙伴会觉得 前端已死?


我想说的是,对比起后端,前端人在几年内吃了太多的互联网红利。这个行业可能需要自我净化,提升整体素质。


我见过包装3年实际一年,连vscode调试都不会的前端人拿很高的月薪。


也见过对算法,原理,底层,优化,都很熟悉的3-4年后端的人拿的不比我这个小外包高多少。


我们前端人一定要明白,普通前端的可替代性实在太强太强。


要么我们深入业务,要么我们深入原理。


对于真正学习计算机科学的人来说,什么webpack代码构建,babel编译,什么react链表结构,vue模板编译,这些看上去比较底层的东西,在他们眼里可能只是基本功,可能只是常识。


如果不深入原理,那么最终真的只能“前端已死”。



  • 想想在刚入行的时候,读了一下某开源工具的源码,我的反应是


“哇这架构好神奇,居然将三层类层层嵌套” “哇一个参数居然能通过观察者模式传三层”



  • 想想在刚入行的时候,看了一下react渲染的原理


"哇他们真聪明,居然能想到将大任务分成多个5ms小任务,运行在浏览器每一帧上,阻止卡顿"


跟一个后端/硬件的朋友讨论,他跟我说"这不是常识吗?调操作系统底层,5ms任务给你掐了"


现在看来,这不过是基础罢了。但很多前端er,连这些都搞不明白,就像原来的我一样。


毕竟,即便是深入了前端的原理,可能也只是到达了软件开发的基本水平吧。


还是借用那句话吧。
所知甚少,唯善学。


前端不会死的,它只是停止了狂奔。


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

Android Framework源码面试——Activity启动流程

面试的时候,面试官经常同你随便侃侃Activity的启动模式,但Activity启动牵扯的知识点其实很多,并非能单单用四个启动模式就能概括的, 默认的启动模式的表现会随着Intent Flag的设置而改变,因此侃Activity启动模式大多走流程装逼,最多结合...
继续阅读 »

面试的时候,面试官经常同你随便侃侃Activity的启动模式,但Activity启动牵扯的知识点其实很多,并非能单单用四个启动模式就能概括的,


默认的启动模式的表现会随着Intent Flag的设置而改变,因此侃Activity启动模式大多走流程装逼,最多结合项目遇到的问题,随便刁难一下面试者,并不太容易把控,也许最后,面试官跟面试者的答案都是错了,


比如在Service中必须通过设置FLAG_ACTIVITY_NEW_TASK才能启动Activity,这个时候启动Activit会有什么样的表现呢?就这一个问题,答案就要分好几个场景:




  • ActivitytaskAffinity属性的Task栈是否存在

  • 如果存在,要看Activity是否存已经存在于该Task

  • 如果已经存在于该taskAffinityTask,要看其是不是其rootActivity

  • 如果是其rootActivity,还要看启动该ActivityIntent是否跟当前intent相等



不同场景,所表现的行为都会有所不同,再比如singleInstance属性,如果设置了,大家都知道只有一个实例,将来再启动会复用,但是如果使用Intent.FLAG_ACTIVITY_CLEAR_TASK来启动,仍然会重建,并非完全遵守singleInstance的说明,还有不同Flag在叠加使用时候也会有不同的表现,单一而论Activity启动模式其实是很难的。本文也仅仅是涉及部分启动模式及Flag,更多组合跟场景要自己看源码或者实验来解决了。


1.面试连环炮之说说 Android 的四种启动模式



  • standard 这是 Activity 的默认启动模式,每次激活 Activity 的时候都会创建一个新的 Activity 实例,并放入任务栈中。 使用场景:基本绝大多数地方都可以用。




  • singleTop 这可能也是非常常用的 launchMode 了。如果在任务的栈顶正好存有该 Activity 的实例,则会通过调用 onNewIntent() 方法进行重用,否则就会同 standard 模式一样,创建新的实例并放入栈顶。即便栈中已经存在了该 Activity 的实例,也会创建新的实例,即:A -> B ->A,此时栈内为 A -> B -> A,但 A -> B ->B ,此时栈内为 A -> B。一句话概述就是:当且仅当启动的 Activity 和上一个 Activity 一致的时候才会通过调用 onNewIntent() 方法重用 Activity使用场景:资讯阅读类 APP 的内容界面。




  • singleTask 这个 launchMode专门用于解决上面 singleTop 的另外一种情况,只要栈中已经存在了该 Activity 的实例,就会直接调用 onNewIntent() 方法来实现重用实例。重用时,直接让该 Activity 的实例回到栈顶,并且移除之前它上面的所有 Activity 实例。如果栈中不存在这样的实例,则和 standard 模式相同。即: A ->B -> C -> D -> B,此时栈内变成了 A -> B。而 A -> B -> C,栈内还是 A -> B -> C。 使用场景:浏览器的主页面,或者大部分 APP 的主页面。




  • singleInstance 在一个新栈中创建该 Activity 的实例,并让多个应用共享该栈中的该 Activity 实例。一旦该模式的 Activity实例已经存在于某个栈中,任何应用再激活该 Activity 时都会重用该栈中的实例,是的,依然是调用 onNewIntent() 方法。其效果相当于多个应用共享一个应用,不管是谁激活,该 Activity 都会进入同一个应用中。但值得引起注意的是:singleInstance 不要用于中间页面,如果用户中间页面,跳转会出现很难受的问题。 这个在实际开发中我暂未遇到过,不过 Android 系统的来电页面,多次来电均是使用的同一个 Activity



四种模式的背书式理解记忆讲完了,你认为这样就结束了吗?


对,我也一度是这样认为的。


2.面试连环炮之说说 Intent标签起什么作用呢? 简单说一说


我们除了需要知道在 AndroidManifest.xml 里面设置 android:launchMode 属性,我们还需要了解下面这几个Intent标签的用法。


在 Android 中,我们除了在清单文件 AndroidManifest.xml 中配置 launchMode,当然可以用 Intent 标签说事儿。启动 Activity ,我们需要传递一个 Intent,完全可以通过设置 Intent.setFlags(int flags) 来设置启动的 Activity 的启动模式。


需要注意的是:通过代码来设置 Activity 的启动模式的方式,优先级比清单文件设置更高。





  • FLAG_ACTIVITY_NEW_TASK 这个标识会使新启动的 Activity 独立创建一个 Task




  • FLAG_ACTIVITY_CLEAR_TOP 这个标识会使新启动的 Activity 检查是否存在于 Task 中,如果存在则清除其之上的 Activity,使它获得焦点,并不重新实例化一个 Activity,一般结合 FLAG_ACTIVITY_NEW_TASK 一起使用。




  • FLAG_ACTIVITY_SINGLE_TOP 等同于在 launcherMode 属性设置为 singleTop






3.Android 的启动原理,他的流程是什么样的


总的流程图:



1.2.1.进程A与AMS的交互过程


此处以跨进程启动Activity分析一下源码流程:


①A调用startActivity时,需要与AMS交互,此时需要需要获取到AMS的代理对象Binder也就是上图的AMP,


通过ActivityManagerNative.getDefault()获得,并调用AMP的startActivity方法,然后会通过mRemote.transact方法进行Binder通信,在AMS的onTransact方法里面会获取到请求的Activity参数信息:


mRemote.transact(START_ACTIVITY_TRANSACTION,data,reply,0);
@Override
public boolean onTransact(int code, Parcel data, Parcel reply, int flags){
switch(code){
case START_ACTIVITY_TRANSACTION:{ startActivity(app,callingPackage,intent,...)
}
}
}

②AMS里面的startActivity方法最主要会去调用startSpecificActivityLocked函数,在此函数里面会去判断目标进程是否已经存在,并且目标向AMS注册过它自己的ApplicationThread也就是上图ATP代理对象,如果这两个条件都满足会去调用realStartActivityLocked方法,这个方法我们后面再看。如果上述条件不满足时,会去调用mService.startProcessLocked(r.processName,...)方法启动进程。





  • startProcessLocked方法首先调用Process.start("android.app.ActivityThread",)方法会向Zygote发送一个启动进程的请求,并告知Zygote进程启动之后,加载ActivityThread这个类的入口main函数,启动完成后返回进程的pid,并向AMS的Handler发送一个延迟消息,为的是要求目标进程启动后,10秒钟内需要向AMS报告,不然的话AMS就会清除目标进程的相关信息。




  • Process.start方法会去调用startViaZygote(processClass,)函数,这个函数主要做了两件事,一件就是打开通往ZygoteSocket,第二件事就是通过Socket发送启动进程参数。




  • Zygote端主要逻辑是在runOnce函数,函数内调用Zygote.forkAndSpecialize(...)创建子进程,创建完成之后就分别在父进程和子进程里面做各自的事情.





父进程通过hanleParentProc(pid)把子进程的pid通过Socket发送给AMS


子进程调用handleChildProc函数,做一些通用的初始化,比如启用Binder机制;执行应用程序的入口函数,也就是ActivityThreadMain函数.





  • ActivityThreadmain函数,里面会创建一个ActivityThread对象,并调用thread.attach(false),为的是向AMS报到,上面第一条里面有提到。




  • attach方法里面,其实是一个跨进程的调用,首先通过




IActivityManager mgr = ActivityManagerNative.getDefault();

获取到AMS的Binder代理对象,然后调用


IActivityManager mgr = ActivityManagerNative.getDefault();


mAppThread是应用端的一个Binder对象ApplicationThread,也就是最上面一张图的ATP,这样AMS端就可以调用应用端了。




  • attachApplication方法里面,最主要有两个方法,一个是通过传入的ApplicationThread对象,调用bindApplication初始化Application对象,另一个就是通过


mStactSupervisor.attachApplicationLoacked(app);


初始化挂起的Activity对象。




  • attachApplicationLoacked函数里,会调用


ActivityRecord hr = stack.topRunningActivityLocked(null);


其中要明白AMS里面有两个栈,一个是Launch桌面栈,一个就是非桌面栈mFocusedStack,此处的stack就是mFocusedStack,它会将栈顶的ActivityRecord返回出来,我们的目标Activity早就放置在了栈顶,只是一直没有初始化。然后调用方法,来启动Activity



如果我们不是启动另外一个进程,而是同一进程,那么这第二大部分就不会存在了,而是直接调用realStartActivityLocked方法。


realStartActivityLocked(hr,app,true,true);



写到这里是不是有很多码牛的小伙伴们已经坚持不下去了。还剩最后几个步骤



realStartActivityLocked函数会调用app.thread.scheduleLaunchActivity(new Intent(r.intent),...);也就是通过之前注册的Binder对象ATP,调用scheduleLaunchActivity函数,在scheduleLaunchActivity函数里面:


ActivityClientRecord r = new ActivityClientRecord();
...
sendMessage(H.LAUNCH_ACTIVITY,r);


封装了一个ActivityClientRecord消息,然后丢到主线程的Handler(mH)里。



②在主线程里面


final ActivityClientRecord r = (ActivityClientRecord)msg.obj ;
r.packageInfo = getPackageInfoNoCheck(...);
handleLaunchActivity(r,null);


getPackageInfoNoCheck 函数主要是用来生成一个LoadedApk对象,它用来保存我们的apk信息,因为后面我们需要一个ClassLoader去加载Apk里面的Activity类,所以这里提前准备好。



handleLaunchActivity里面分为两个部分,一个是performLaunchActivity函数,一个是handleResumeActivity函数。


performLaunchActivity


Activity activity = mInstrumentation.newActivity(...);

//返回之前创建好的
Application app = r.packageInfo.makeApplication(false,mInstrumentation);

//生成ContextImpl
Context appContext = createBaseContextForActivity(r,activity);

//给activity绑定上下文和一些初始化的工作,如createPhoneWindow
activity.attach(appContext,...);

mInstrumentation.callActivityOnCreate(activity,r.state); //生命周期的OnCreate

activity.performStart(); //生命周期的OnStart

return activity

handleResumeActivity


-> r.activity.performResume()     
-> mInstrumentation.callActivityOnResume(this);
-> activity.onResume()

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

提升编程思想,这才是少走十年弯路的学习方式

练习算法和数据结构 熟悉算法和数据结构可以帮助你更好地理解和解决问题,优化你的代码并提高你的代码质量 推荐学习流程 了解算法和数据结构的基础概念和术语。在学习任何新的主题之前,首先需要了解其基本概念和术语。对于算法和数据结构,您需要了解什么是算法,什么是...
继续阅读 »

练习算法和数据结构



熟悉算法和数据结构可以帮助你更好地理解和解决问题,优化你的代码并提高你的代码质量



推荐学习流程



  1. 了解算法和数据结构的基础概念和术语。在学习任何新的主题之前,首先需要了解其基本概念和术语。对于算法和数据结构,您需要了解什么是算法,什么是数据结构,它们有什么不同,以及它们的主要特征和优缺点。

  2. 了解常见的数据结构和算法。学习数据结构和算法的最好方法之一是了解它们的常见类型和应用。您可以使用在线资源和参考书籍来了解这些内容。

  3. 练习编写和实现算法和数据结构。通过编写和实现算法和数据结构来练习可以帮助您更好地理解它们的工作原理和应用。您可以使用在线代码编辑器和调试工具来练习这些技能。

  4. 解决实际问题和挑战。尝试解决实际问题和挑战可以帮助您将所学的算法和数据结构应用到实践中,并帮助您进一步掌握它们。


纸上得来终觉浅,我们学习之后肯定要进行使用,现在算法学习的网站已经完全为我们考虑了,关于上述的很多步骤天然支持。


那我们在解决算法和数据结构问题时,可以遵循以下步骤:



  1. 理解问题。阅读问题并确保您完全理解其要求和限制。这可以帮助您确定应该使用的数据结构和算法类型。

  2. 设计解决方案。根据您对问题的理解,设计一个解决方案。这可以包括确定应该使用的数据结构和算法、解决问题的步骤以及如何处理可能出现的异常情况。

  3. 实现方案。使用您选择的编程语言和编辑器实现您的解决方案。确保您按照您设计的步骤执行。

  4. 测试和调试。测试您的实现,并在需要时进行调试。这可以帮助您确定是否存在错误并确保您的代码在各种情况下都能正常工作。


当今时代,忙碌是我们整个社会的代名词,也是我们程序员的天生标签,真的,我从大专毕业到现在,每天早上上班每天下班都在不断的学习,但是结果都是在工作中有机会实战的,一清二楚,不能使用的,真的转眼就忘。
所以我们在学习的过程中要善于总结,总结可以缩小我们记忆搜索的范围,即使忘记了,我们也可以通过最小的代价找回来。


学会总结,学习算法是可以通过以下步骤总结



  1. 算法和数据结构的定义和区别。了解算法和数据结构的定义和区别可以帮助您更好地理解它们的工作原理和应用。

  2. 常见的数据结构和算法。了解常见的数据结构和算法可以帮助您更好地应用它们解决实际问题。

  3. 复杂度分析。了解算法和数据结构的时间和空间复杂度可以帮助您确定其效率和应用场景。

  4. 优化算法和数据结构。了解如何优化算法和数据结构可以帮助您更好地解决实际问题,并提高效率。


俗话说,专业的都是无味的,此时,我们可以转换思想去记忆


总有一种方法让你记住,因为谁都有故事



  1. 创造有趣的比喻或类比。将算法或数据结构与某些日常生活中的东西联系起来,可以帮助您更容易地记住它们。例如,您可以将树形数据结构比喻为现实世界中的树木。

  2. 使用记忆术。使用记忆术,例如联想和图像化,可以帮助您更好地记住算法和数据结构。例如,您可以将联想关键字与算法和数据结构名称相匹配。

  3. 创建抽象图形和示例。通过使用抽象图形和示例,可以更好地理解和记住算法和数据结构的工作原理和应用。

  4. 练习并实践。最好的方式是通过不断练习和实践来记住算法和数据结构。通过实际运用它们来解决实际问题,您将更好地理解它们的工作原理和应用,并在大脑中更深刻地记住它们。


学习算法和数据结构需要耐心、练习和实践,使用记忆术、抽象图形和示例来帮助记住算法和数据结构。


来,看个例子


以经常使用的快速排序为例


学习过程



了解快速排序算法的工作原理并熟悉其实现方法是学习快速排序算法的第一步。


快速排序算法是一种分治算法,其基本思想是将一个大问题分解成多个子问题,然后递归地解决这些子问题,最后将它们组合成一个完整的解决方案。


具体来说,快速排序算法将待排序数组分成两部分,其中一部分小于某个基准值,另一部分大于等于基准值。然后,对两部分分别递归地进行快速排序,最终得到一个有序数组。



以下是快速排序算法的核心实现代码:


function quickSort(arr, left, right) {
if (left < right) {
const pivotIndex = partition(arr, left, right);
quickSort(arr, left, pivotIndex - 1);
quickSort(arr, pivotIndex + 1, right);
}
}

function partition(arr, left, right) {
const pivot = arr[right];
let i = left - 1;

for (let j = left; j < right; j++) {
if (arr[j] < pivot) {
i++;
[arr[i], arr[j]] = [arr[j], arr[i]];
}
}

[arr[i + 1], arr[right]] = [arr[right], arr[i + 1]];
return i + 1;
}

解决过程



为了更好地理解快速排序算法的实现和工作原理,可以按照以下步骤解决一个简单的示例问题:



问题:给定一个未排序的整数数组 [8, 4, 2, 6, 7, 1, 3, 5],按升序排列。


解决步骤:


Step 1:选择一个基准元素(pivot)。为了简单起见,我们选择数组的最后一个元素 5 作为基准。


Step 2:将数组中小于基准的元素移到基准左侧,大于等于基准的元素移到基准右侧。这可以通过从数组的左侧开始扫描并与基准比较,如果小于基准,则将元素交换到基准左侧。


[4, 2, 3, 5, 7, 1, 8, 6]
^ ^
i j

在这个例子中,我们选择数组的第一个元素作为左指针 i,并从第二个元素开始扫描数组。当扫描到元素 4 时,发现其小于基准 5,因此将其交换到 i 处,并将 i 加 1。扫描到元素 2 时,同样将其交换到 i处。继续扫描,直到 j 扫描到基准 5 为止。


Step 3:将基准元素放回正确的位置。由于基准元素是最后一个元素,因此可以将 i+1 位置的元素与基准元素交换。


[4, 2, 3, 5, 7, 1, 6, 8]
^ ^
i+1 right

现在,数组被分成了两个部分:左侧部分 [4, 2, 3, 1] 小于基准值 5,右侧部分 [7, 6, 8] 大于等于基准值 5。


Step 4:对左右两部分分别进行快速排序。这是一个递归过程,直到左右部分的长度都为 1 时停止。


继续对左侧部分进行快速排序,以数组 [4, 2, 3, 1] 为例。


Step 1:选择基准元素。选择数组的最后一个元素 1 作为基准。


Step 2:将小于基准的元素移到基准左侧。在这个例子中,可以看到数组已经有序,因此不需要进行任何交换操作。


Step 3:将基准元素放回正确的位置。将 i+1 位置的元素 1 与基准元素 1 交换。注意,这里 i+1 等于左侧数组的左端点,因为左侧数组中只有一个元素。


Step 4:对左右两部分分别进行快速排序。由于左侧数组只有一个元素,无需进行任何操作。


继续对右侧部分进行快速排序,以数组 [7, 6, 8] 为例。


Step 1:选择基准元素。选择数组的最后一个元素 8 作为基准。


Step 2:将小于基准的元素移到基准左侧。在这个例子中,可以看到只有一个元素小于基准,因此将其与 i 处的元素交换。


[7, 6, 8]
^
i,j

Step 3:将基准元素放回正确的位置。将 i+1 位置的元素 7 与基准元素 8 交换。


[7, 8, 6]
^
i+1,right

Step 4:对左右两部分分别进行快速排序。继续对左侧部分 [7] 进行快速排序,无需进行任何操作。对右侧部分 [6] 进行快速排序,同样无需进行任何操作。


综上所述,对于数组 [8, 4, 2, 6, 7, 1, 3, 5],通过快速排序算法的处理,最终得到有序数组 [1, 2, 3, 4, 5, 6, 7, 8]。


核心总结:



快速排序算法是一种高效的排序算法,它的核心在于基准元素的选择和快速分割数组。通过不断选择基准元素,将数组划分成两个部分,并分别对这两个部分进行快速排序,最终将整个数组排序完成。



快速排序算法的时间复杂度为 O(n log n),空间复杂度为 O(log n)。它是一种原地排序算法,不需要额外的空间。


记忆方法:


快速排序算法的核心是基准元素的选择和数组的分割。可以用以下方法记忆:



  1. 选择基准元素:通常选择数组的最后一个元素作为基准,也可以选择其他位置的元素。

  2. 分割数组:将数组分割成小于基准的部分和大于等于基准的部分。可以使用双指针法来实现。

  3. 递归排序:对小于基准的部分和大于等于基准的部分分别进行快速排序,直到数组长度为 1 时停止递归。


记忆口诀:



  1. 基准元素找末尾,

  2. 双指针分割快速跑,

  3. 递归小大排,

  4. 最后数组有序到。


这个口诀可以快速记忆快速排序算法的核心思想和实现过程。


Android开发怎么融入其中


安卓开发中需要使用算法和数据结构时,可以将以上的学习模板应用到具体的问题中。



  1. 首先,可以选择适合问题的算法和数据结构。例如,如果你需要对一个数组进行排序,你可以使用快速排序算法。如果你需要快速查找元素,你可以使用二分查找算法。

  2. 其次,你可以使用学习模板中的学习过程和解决过程来帮助自己理解算法的实现过程和核心思想。通过学习模板,你可以更深入地了解算法和数据结构的实现原理,并将它们应用到实际的开发中。

  3. 最后,你可以使用学习模板中提供的核心总结和记忆方法来帮助自己记忆算法和数据结构的核心思想和实现过程。这可以帮助你更快速地掌握算法和数据结构,并在实际开发中应用它们。


总结


学而不思则亡 思而不学则殆


使用学习模板可以帮助你更有效地学习算法和数据结构,并将它们应用到实际的安卓开发中。这可以提升你的技能水平,并使你在开发中更加高效和优秀。


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

我: 向我推销这支笔 ,来看ChatGPT如何回答...

OpenAI: GPT系列模型:OpenAI的GPT系列模型是非常出名的语言模型,例如GPT-2、GPT-3等。这些模型可以生成人类般的语言,用于文本生成、对话系统等领域。 DALL-E:这是一个能够将自然语言描述转换为图像的系统。它可以生成与文本描述相对...
继续阅读 »

OpenAI:




  1. GPT系列模型:OpenAI的GPT系列模型是非常出名的语言模型,例如GPT-2、GPT-3等。这些模型可以生成人类般的语言,用于文本生成、对话系统等领域。

  2. DALL-E:这是一个能够将自然语言描述转换为图像的系统。它可以生成与文本描述相对应的图像,从简单的物体到复杂的场景都可以。

  3. Codex:这是一个基于人工智能的代码生成器,可以为程序员自动生成代码。它是由OpenAI与GitHub合作开发的。

  4. Gym:Gym是OpenAI的一个强化学习平台,为开发人员提供了一个可以测试和比较强化学习算法的标准化环境。

  5. RoboSumo:这是一个机器人足球比赛仿真平台,用于测试和开发机器人控制算法

  6. MuseNet:MuseNet是OpenAI的一个音乐生成系统,可以自动生成各种类型的音乐,从古典音乐到流行音乐等。

  7. GPT-Neo:这是OpenAI的一个基于GPT-3的开源模型,它具有类似GPT-3的功能,但是开源,可以由任何人使用。

  8. CLIP:这是OpenAI的一个视觉和语言模型,可以将视觉和语言信息结合起来,从而实现更复杂的任务,如图像分类、视觉问答等。



除此之外,OpenAI还开发了许多其他的人工智能应用和工具,如人工智能道德和安全方面的研究、自动语音识别、自动驾驶系统等。






今天有的伙伴问我:“为什么单单就中国不开放开来用,为什么我要用chatgpt还要去注册个外国的手机号才能搞这玩意,要不然我就只能用免费的公用的,动不动就“sorry,当前访问人数太多,请稍后再试”,绝了,为什么要排除我们国家。”



先来看看很多伙伴用的免费的公用的chatgpt地址,确实可以用,但是现在chatgpt很火,很多人很好奇,应该是同一时间段很多人都在访问,都在调用这个chatgpt的api,所以经常会出现这种情况。


image.png


为什么不是所有国家和地区都支持OpenAI?


OpenAI 是一家致力于帮助人类推进人工智能研究的科研机构,它在全球范围内提供免费的语言处理技术和其他人工智能工具。


然而,由于技术和法律限制,并非所有国家和地区都能够使用OpenAI的服务。


因此,OpenAI的服务并不能覆盖所有国家和地区。


官网地址: openai.com/api/


注册或者登陆地址为:openai.com/api/


注册 一道坎


我们注册完,写好一切,到某一步,就需要填写手机号,一个账号对应一个手机号,所以就会用到这个countries的接口:


countries: api.openai.com/dashboard/p…


(直接在浏览器打开就可以看到列出来的全部国家信息),看到有中国地区的,但是呢,当你点击发送验证码的时候,后台又会去调用一个接口去verify验证一下这个国家能不能用,到目前(2023-2-17)为止,中国地区是不能用的:


image.png


它这里整个官网啊、后台管理系统啊,用的是react框架搭建的,采用的是前后端分离的模式。


我们国内的过不去没法注册,没办法用啊,免费公众的用一直报错。解决办法就是[买一个外国的手机号]或者租一个,这里面呢也有免费号码可以提供,但就是最近chatgpt太火了免费也轮不到。现在很多可支持支付宝或者微信。主要是你懂得。一般价格在几块钱4块钱、5块钱这样子。也可以去搜索引擎看看有没有免费的OpenAI账号可以借来看看,玩一玩。


截屏2023-02-17下午5.34.47.png


进来玩一玩


overview 总览


截屏2023-02-17下午5.32.36.png


这就是一些基本的。 左边叫你看一下它的文档,学习并快速创建一个简单的应用;右边呢,就是叫你去看它现在现成的例子,就是叫你去玩的看的。


先别玩,先再看一下它的介绍还有什么:


image.png


chatgpt (你问它答)


chatgpt的网址在这里


一进来,它就给我一个提示,说2-10号更新的公告, chatgpt plus 可以 有beta版可用,不过你要通过申请排队,表单申请内容有一系列,其中有问你用这个chatgpt用来干嘛的,理由是什么,会用在哪里,将来有兴趣把它用在哪里。


没事, chatgpt 可用就行。我们来看看chatgpt。


image.png


没错,公用的不用登录账号的网址,跟这个一模一样。只不过偶尔那里会堵一点。


最近chatgpt 可是 火到 冲天,我们国内,或者国外都在讨论这个东西。还有一些人因为chatgpt什么的被骗,案例蛮多的最近,收费还不低,大家一定要擦亮你闪亮的小眼睛,分辨清楚,别轻易交钱就是了。


来问问chatgpt了,看看它到底牛在哪。


第一问


问它帮我写其他语言的代码


请帮我把以下java代码 翻译成 javascript 代码, 代码如下:

public class mmm {
public static void man(String[] args) {
String s1 = "hello";
String s2 = "HELLO".toLowerCase();

System.out.println(s1);
System.out.println(s2);

if (s1.equals(s2)) {
System.out.println("s1 === s2");
} else {
System.out.println("s1 !== s2");
}

}
}

下面是它回答我的:


let s1 = "hello";
let s2 = "HELLO".toLowerCase();

console.log(s1);
console.log(s2);

if (s1 === s2) {
console.log("s1 == s2");
} else {
console.log("s1 != s2");
}


请注意,JavaScript与Java有所不同,因此您需要确保语法和操作符正确。在这个例子中,我们将`System.out.println()`替换为了`console.log()`,将`String.equals()`替换为了`===`。此外,我们需要用`let`而不是`String`来声明变量。

image.png


它不仅能帮你转成JavaScript的写法,还给你解释了为什么这样做。


1、System.out.println()替换为了console.log()


2、String.equals()替换为了===


3、用let而不是String来声明变量


chatgpt次数是免费的,随你问。但如果问关于专业的或者代码的相关问题,学会提问的技巧哦,就跟搜索引擎一样就行,多加关键字关键词,github上有关于如何提问的文章,写的很好,放在这里了。《提问的智慧》 中文版的在这里 《提问的智慧 How to ask questions the smart way》


第二问


为什么不能除以零


这是它的回答:


image.png


第三问


给我一些关于年龄超过35程序员的职业规划的具体建议


这个问题对于我们这个行业来说,一直在问,今天我问问这个chatgpt它有什么好的想法


image.png


第四问


向我推销这支笔


image.png


image.png


image.png


外国人对于chatgpt的一些相关问答记录在这里了


http://www.emergentmind.com/


可以去看看翻一翻。


text completion 文字补充


借一步说话哈,它这个注册了这个东西,有用的并不单单是进来玩一下案例,还有一个api key。就是用这个api key这个钥匙去调用他们这个公司提供的接口,就这样这么简单,这个key可自行创可以删。


像它说的这个completion model,就是可以用来文本补充提示的这个功能的可用的模型有哪些。


通过GET https://api.openai.com/v1/models这个接口去调用查看可用模型列表。


返回来的数据结构就是这个:


image.png


回到正题,说到这个文字补充。


比如说你敲了文本“正如笛卡尔说的,我思故”,它就会帮你补充"我在"


"正如笛卡尔说的我思故我在"


点击确定按钮 发了一个请求,看一下


image.png


请求 api.openai.com/v1/moderati…


request 请求参数:


{
input: '正如笛卡尔说的我思故", // 这就是敲的文本
model: 'text-moderation-playground' // 这就是哪个模型类型
}


response 响应回来的:


image.png


返回来一些类别关于我们打的这段字的类别。如果我们敲的一些文本触发到这些类别,就会给予警告。


image.png


历史问过的会保存在你浏览器本地的localstorage里面:


image.png


AI画图


AI画图功能


image.png


AI画图的名字叫做: DALL.E


AI图像生成:


提供了三种与图像交互的方法:


1、基于文本提示从头开始创建图像


2、基于新文本提示创建对现有图像的编辑


3、创建现有图像的变体


这个DALL.E AI画图功能呢,还处在测试阶段,默认速率是限制每分钟 50张图,多的没有了。


如果你想要每分钟出超过50张,加钱。


image.png


那我现在,是在第一个月,有50个免费的使用机会,用完以后我就要买了。


废话不多说,来看看,有多神乎。


这是它这个功能的官网宣传图


image.png


image.png


image.png


image.png


AI图片生成模块 接口文档


生成的图像大小可以是 256 X 256 的,512 X 512 的, 1024 X 1024 的。尺寸越小生成速度越快。


用nodejs请求这么写


const res = await openai.createImage({
promt: '一只白色暹罗猫',
n: 1, // 这里 n 的取值范围是 1到10
size: '1024 X 1024',

// 可加可不加
response_format: 'URL' 或者 'Base64', // URL 会在一个小时后过期
})

image_url = res.data.data[0].url

文档它也说了,你写这个promt描述越详细,结果你得到的图像会越接近你想要得到的效果。


image.png


它这个后台本身就有个现成的案例给我们玩,只不过要钱, 基本的就是这样。


image.png


就是你写描述,它给你图。满不满意,看你描述得如何。


我描述了一下: A Mexican woman wearing a pearl necklace


一个戴着珍珠项链的墨西哥女人


image.png


很写实,确实一个戴着珍珠项链的墨西哥女人,那我写女人,不写美女,效果很写实,但不够惊艳,浪费了我一次机会。


突发奇想我去谷歌搜一下同样的描述,出来了一个这样的。


image.png


它这里做了一些可供我们操作的按钮:


截屏2023-02-18下午2.52.57.png


1、可以打开这张图的详情页,单独看某一张图


2、可以对某一张图进行修改(就是调整、抠图啊、裁剪啊,大小啊之类的)


3、进行改造,就是这张图你不满意,我再进行改造一下


4、下载单独这一张图


5、为这一张图生成报告


请求的接口是这个 https://labs.openai.com/api/labs/tasks/task-xxx


image.png


返回来的就是 一个 data 数组, 然后react就把这个数组循环渲染出各个图片链接,图片链接应该是放在某个cdn桶里。


然后这里呢请求的图片列表的同时,也同时扣了一次机会。


可以对比一下,这也就是他们成立一个公司开发OpenAI这一系列产品的可怕之处,确实是有料。


一个戴着珍珠项链的墨西哥女人 改成 戴着珍珠项链的18岁墨西哥女孩 An 18-year-old Mexican girl with a pearl necklace


image.png


还是搜一下'日本美女'吧


image.png


第一位日本美女出场


image.png


脸、眼睛、手、怪不得人家画画的说,画人物手比较难画,可能是吧我不是美术生,就记得一句话“画人难画手”✋。


其他的我就不一一举例了。


它这东西怎么想的, 啧啧...


最后我放一张我自己认为的日本美女。想了一下,自己去搜吧,放这里不太合适。




以上着重看了 你问我答 (chatgpt), 文本补充 (Text completion)、你说我出画 (Image generation)这三点。


image.png


还剩下 代码补充(生成代码、编辑代码、代码解释); 嵌入 (搜索、分类和比较文本); 模型训练 (训练模型) 这三块。


总结


我让chatgpt这个AI来帮我写结尾吧。


image.png


看了整篇文字数,废话很多,洋洋洒洒3千多。这个新技术确实像chatgpt它自己总结的一样,势不可挡,不容小觑。


潜力


1、自然语言处理

2、计算机视觉

3、自动化决策


应用在


1、医疗保健

2、交通运输

3、金融服务

4、能源


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

今年很火的 AI 绘画怎么玩

1.前言 2022年绝对可以说是AIGC元年,从google搜索的趋势来看,在2022年AI绘画及AI生成艺术的搜索量激增。 AI绘画在这一年的爆发一个很重要的原因就是 Stable Diffusion 的开源,这也来不开这几年 Diffusion Mode...
继续阅读 »

1.前言


2022年绝对可以说是AIGC元年,从google搜索的趋势来看,在2022年AI绘画及AI生成艺术的搜索量激增。


640.jpg


AI绘画在这一年的爆发一个很重要的原因就是 Stable Diffusion 的开源,这也来不开这几年 Diffusion Model 扩散模型在这几年里的迅猛发展,结合了 OPENAI 已经发展得很成熟的文本语言模型 GPT-3,从文本到图片的生成过程变得更加容易。


2.GAN(生成对抗网络)的瓶颈


从14年诞生,到18年的StyleGAN,GAN在图片生成领域获得了长足的发展。就好像自然界的捕食者与被捕食者相互竞争共同进化一样,GAN的原理简单来说就是使用两个神经网络:一个作为生成器、一个作为判别器,生成器生成不同的图像让判别器去判断结果是否合格,二者互相对抗以此训练模型。


640.png


GAN(生成对抗网络)经过不断发展其有了不错的效果,但有些始终难以克服的问题:生成结果多样性缺乏、模式坍缩(生成器在找到最佳模式后就不再进步了)、训练难度高。这些困难导致 AI 生成艺术一直难以做出实用的产品。


2.Diffusion Model(扩散模型)的突破


在 GAN 多年的瓶颈期后,科学家想出了非常神奇的 Diffusion Model(扩散模型)的办法去训练模型:把原图用马尔科夫链将噪点不断地添加到其中,最终成为一个随机噪声图像,然后让训练神经网络把此过程逆转过来,从随机噪声图像逐渐还原成原图,这样神经网络就有了可以说是从无到有生成图片的能力。而文本生成图片就是把描述文本处理后当做噪声不断添加到原图中,这样就可以让神经网络从文本生成图片。


6401.png


Diffusion Model(扩散模型)让训练模型变得更加简单,只需大量的图片就行了,其生成图像的质量也能达到很高的水平,并且生成结果能有很大的多样性,这也是新一代 AI 能有难以让人相信的「想象力」的原因。


当然技术也是一直在突破的,英伟达在1月底推出的StyleGAN的升级版StyleGAN-T就有了十分惊艳的进步,在同等算力下相比于Stable Diffusion生成一张图片需要3秒,StyleGAN-T仅需0.1秒。并且在低分辨率图像StyleGAN-T要比Diffusion Model要好,但在高分辨率图像的生成上,还是Diffusion Model的天下。由于StyleGAN-T并没有像Stable Diffusion那样获得广泛的应用,本文还是以介绍Stable DIffusion为主。


3.Stable Diffusion


在今年早些时间,AI作画圈经历了 Disco Diffusion、DALL-E2、Midjouney 群雄混战的时代,直到 Stable Diffusion 开源后,才进入一段时间的尘埃落定,作为最强的 AI 作画模型,Stable Diffusion 引起了 AI 社群的狂欢,基本上每天都有新的模型、新的开源库诞生。尤其是在Auto1111的WebUI版本推出后,无论是部署在云端还是本地,使用Stable Diffusion都变成一个非常简单的事情,并且随着社区的不断开发,很多优秀的项目,比如Dreambooth、deforum都作为 Stable Diffusion WEBUI版的一个插件加入进来,使得像微调模型、生成动画等功能都能一站式完成。


6402.png


4.AI绘画玩法及能力介绍


下面介绍下目前使用 Stable Diffusion 可以有哪些玩法以及能力


11.jpg


12.jpg


14.jpg


15.jpg


5.目前主要应用情况介绍


19.jpg


31529880d6c24b11bc54dde744214084.jpeg


6.自己搭一个Stable Diffusion WEBUI服务


6.1 云端版本


这里使用AutoDL提供的云端算力来搭建,也可以使用其他平台比如 Google Colab或者百度飞桨等。


1.首先在AutoDL上注册账号并且租一台 A5000/RTX3090 显卡的云主机。http://www.autodl.com/market/list


2.以此主机创建镜像,镜像可在 http://www.codewithgpu.com 上选择已经打包好的算法镜像。这里以 http://www.codewithgpu.com/i/AUTOMATIC… 这个镜像为例,选择后创建。


6403.png


3.创建后开机并启动JupterLab,


6404.png


运行下面指令启动服务即可。如果遇到系统盘空间不足的情况,也可以将stable-diffusion-webui/文件夹移入数据盘 autodl-tmp再启动。如果遇到启动失败,可以根据你机器的位置配置一下学术资源加速。


cd stable-diffusion-webui/
rm -rf outputs && ln -s /root/autodl-tmp outputs
python launch.py --disable-safe-unpickle --port=6006 --deepdanbooru

6.2 本地版本


如果你有一块显卡还不错的电脑,那可以部署在本地,这里介绍下Windows版本的搭建:


1.首先需要安装Python 3.10.6,并且添加环境变量到Path中


2.安装git


3.Clone Stable Diffusion WEBUI 的工程代码到本地


4.将模型文件放置于 models/Stable-Diffusion目录下,相关模型可以去huggingface.co/ 下载


5.运行 webui-user.bat,通过本机电脑ip及7860端口访问服务。


7.总结


本文介绍了AI绘画的一些相关的信息,感兴趣的朋友也可以自己把服务部署起来,自己试着学习用DreamBooth或者最新的Lora微调一下大模型。相信在2023年,随着 AIGC 热度的不断提高,我们的工作和生活都会因为 AI 带来巨大的改变。前段时间 ChatGPT的推出给我们带来了巨大的震撼,就好像刚进入互联网时期搜索信息的能力一样,今后学会使用 AI 来辅助我们的工作也将是一个非常重要的能力。


8.参考资料




  1. 从起因到争议,在 AI 生成艺术元年聊聊 AI
    sspai.com/post/76277




  2. 神经网络学习笔记6——生成式AI绘画背后的的GAN与Diffusion初解
    blog.csdn.net/qq_45848817…




  3. How diffusion models work: the math from scratch

    theaisummer.com/diffusion-m…




  4. GAN 结构概览

    developers.google.com/machine-lea…




  5. The absolute beginners guide to Midjourney – a magical introduction to AI art
    http://www.entrogames.com/2022/08/abs… magical-introduction-to-ai-art/




  6. The viral AI avatar app Lensa undressed me—without my consent
    http://www.technologyreview.com/2022/12/12/… lensa-undressed-me-without-my-consent/




  7. instruct-pix2pix
    huggingface.co/timbrooks/i…




文/misotofu


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

一键接入 ChatGPT,让你的QQ群变得热闹起来

项目效果 ChatGPT 的出现对于人们的生活和工作都有着重要的影响,作为一个强大的自然语言处理模型,可以理解和生成自然语言,所以 ChatGPT 非常适合作为聊天机器人,回答你的任何问题 在日常生活中想要更加方便的使用 ChatGPT,当然是将其设置成微信...
继续阅读 »

项目效果



  • ChatGPT 的出现对于人们的生活和工作都有着重要的影响,作为一个强大的自然语言处理模型,可以理解和生成自然语言,所以 ChatGPT 非常适合作为聊天机器人,回答你的任何问题

  • 在日常生活中想要更加方便的使用 ChatGPT,当然是将其设置成微信聊天机器人和QQ聊天机器人,随时随地解答你的问题,为你解决疑惑

  • 当然,不推荐您将其设置成微信聊天机器人,因为微信的检测机制,很容易导致你这个号废掉,本人已经亲自踩过坑,但是如果您有很多小号,请随意;因此这里只教大家QQ聊天机器人的搭建方式



🎈 安装环境



  • 支持在 Liunxwindow 上部署,也支持一键启动命令

  • 该项目 chatgpt-mirai-qq-botgithub 上已经进行开源,任何人都可以进行下载

  • 因为只做快速聊天体验,所以本人只部署 window 版本,想要 Liunx 版本的,可自行查看项目中的文档

  • 下载最新版稳定的 window 源码包:github.com/lss233/chat…

  • 本地解压后,双击其中的 初始化.cmd 即可进行环境初始化,一路回车,等待运行成功即可



🎈 配置文件



  • 在初始化的最后,将会打开配置文件 chatgpt/config.cfg,在这个文件中进行配置

  • 配置完成后,保存关闭,初始化环境即是成功

  • 如果暂时不知道怎么配置,也可以保存关闭,后期手动打开 chatgpt/config.cfg 配置即可

  • 关于配置文件中需要配置解释的如下

  • mirai.qq: 这个 qq 必须配置,一般是你的小号,用来作为聊天机器人的 qq

  • api_key: 这个 window 版本默认即可,无需修改,如果是 liunx 则需要修改成 123456789

  • http_url: 这个 window 版本默认即可,无需修改,如果是 liunx 则需要修改成 http://mirai:8080,具体细节可以去项目中的 issues 中查看

  • ws_url:http_url 一样

  • openai:关于 openai 中的配置是最重要的,你会看到可以配置多个账号,这是为了防止被 openai 限制,所以如果你有多个 openai 的账号,可以减轻请求压力,不过一般只填其中一个就行了,因此这里我们只需要完善第一个账号信息即可

  • mode: 可以选择 browser 也可以选择 browserlessbrowser 模式是通过浏览器自动化登录openai,可以看到登录过程,browserless 是走第三方代理,无需浏览器登录,这里我选择的是browserless

  • email/password: 不用不必多说

  • session_token/access_token/api_key: 如果你填了email/password,这个就不用管,不同的登录方式罢了

  • proxy: 这个是字面意思,如果你在国内,这个就必须要填,而且地址最好是漂亮国所在区域,这个参数一般只要修改端口即可,具体看你自己 proxy 的端口

  • system.accept_group_invite: 这个是对QQ的限制,是否接受用户拉你机器人到群里,这个我一般填false,被别人用多了,也增加风险

  • system.accept_friend_request: 这个是对QQ的限制,是否接受用户加你好友,一般人喜欢私聊一些问题,这个我选择true

  • presets: 关于预设,无需修改,默认正常就是使用 chatgpt 进行回复,喜欢不同风格的回复,自己聊天的时候加载就行,这个功能还是很好玩的,不同的预设回复你的时候,语气都不一样。


# 这里是 ChatGPT for QQ 的所有配置文件
# 请注意:以 "#" 开头的文本均为注释
# 不会被程序读取
# 如果你想要使用某个设置,请确保前面没有 "#" 号

[mirai]
qq = 请修改为你机器人的QQ号

manager_qq = 请修改为机器人管理员的QQ号

# 以下设置如果不了解,可以不用理会
api_key = "1234567890"
http_url = "http://localhost:8080"
ws_url = "http://localhost:8080"

[openai]
# OpenAI 相关设置

# 第 1 个账号信息
[[openai.accounts]]
# 模式选择, browser - 浏览器登录, browserless = 无浏览器登录
# 如果你使用浏览器登录卡在 Found session token,就使用无浏览器登录
mode = 'browserless'

# 你的 OpenAI 邮箱
email = "xxxx"
# 你的 OpenAI 密码
password = "xxx"

# 对于通过 Google 登录或者微软登录的同学,可以使用 session_token 登录
# 此时的 password 可以直接删除 (email 必填)
# session_token 获取方法:https://github.com/lss233/chatgpt-mirai-qq-bot/issues/96
# 提示:如果使用此模式,请删除下方 session_token 前面的 "#" 号,并给上方的 password 前面加上 "#"
# session_token = "一串 ey 开头的东西"

# 你的 OpenAI access_token,登录后访问`https://chat.openai.com/api/auth/session`获取
# 提示:如果使用此模式,请删除下方 access_token 前面的 "#" 号,并给上方的 email、password 前面加上 "#"
# access_token = "一串 ey 开头的东西"

# 如果你想使用的是 API 版的 ChatGPT,请使用
# api_key = "sk-xxxx"

# 如果你在国内,需要配置代理
# 提示:如果使用此功能,请删除下方 proxy 前面的 "#" 号
# proxy="http://127.0.0.1:1080"

# 是否为 ChatGPT Plus 用户(是的话设置为 true)
paid = false

# 是否开启标题自动重命名
# 若为空或保持注释即不开启
# 支持的变量: {session_id} - 此对话对应的上下文 ID,若产生在好友中,则为好友 QQ 号,若产生在群聊中,则为群号
# 具体见 README 中的介绍
# title_pattern="qq-{session_id}"

# 是否自动删除旧的对话,开启后用户发送重置对话时会自动删除以前的会话内容
# auto_remove_old_conversations = true

# # 第 2 个账号信息,access_token 登录示例
# [[openai.accounts]]
# mode = 'browserless'
# access_token = "xxxxx"
# # proxy="http://127.0.0.1:1080"
# paid = false

# # 第 3 个账号信息, api_key 登录示例
# [[openai.accounts]]
# api_key = "sk-xxxx"

# 如果你想增加更多账号,复制的文本继续添加即可

# [system]
# 是否自动同意进群邀请
accept_group_invite = false
# 是否自动同意好友请求
accept_friend_request = false

[presets]
# 切换预设的命令: 加载预设 猫娘
command = "加载预设 (\w+)"
loaded_successful = "预设加载成功!"
scan_dir = "./presets/"

[presets.keywords]
# 预设关键词 <-> 实际文件
"正常" = "presets/default.txt"
"猫娘" = "presets/catgirl.txt"
# "default" = "presets/default.txt" # 默认预设,开启后所有的会话都会先读这个预设,谨慎使用!

🎈 启动Mirai



  • 在初始化完环境后,会多出两个可执行文件,先点击 Mirai.cmd

  • 这个脚本主要用来登录你的 QQ 机器人账号的,把你的上面配置里面填写的 QQ 设置成机器人

  • 登录机器人命令:login <机器人QQ账号> <机器人QQ密码> <登录协议>

  • 回车即可登录成功,也就是说QQ机器人登录成功了,当然这部分也有可能是最难的,欢迎私信



🎈 启动ChatGPT



  • 最后点击启动 ChatGPT.cmd ,这个可执行文件,就是用来登录 openai

  • 登录成功后,说明 QQ 机器人已经连接上了 ChatGPT,此时即可跟机器人畅所欲言的聊天了



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

你代码的异味是故意的还是不小心?是故意的!

一、代码竟会有“气味” 食物在腐烂之际,会散发出异味,提醒人食物已经坏掉了,需要处理。同样,如果代码中某处出现了问题,也会有一些症状。这些症状,被称之为“代码异味”(Code smell,也译作“代码味道”)。与食物腐败发出的味道不同的是,代码异味并非真正的气...
继续阅读 »


一、代码竟会有“气味”


食物在腐烂之际,会散发出异味,提醒人食物已经坏掉了,需要处理。同样,如果代码中某处出现了问题,也会有一些症状。这些症状,被称之为“代码异味”(Code smell,也译作“代码味道”)。与食物腐败发出的味道不同的是,代码异味并非真正的气味,而是一种“暗示”,暗示我们代码可能有问题,提示程序员需要对项目设计进行更进一步的查看。


代码异味一词最初是由Kent Beck在帮助Martin Fowler在编写《重构:改善既有代码的设计》一书时创造的。Martin Fowler对代码异味的定义是:代码异味是一种表象,它通常对应于系统中更深层次的问题。


代码异味的产生原因跟厨师的“清洗过程中故意保留”不一样,它更多地并非刻意为之,创造者也未必“品尝”过自己所写的代码,它更多地是由于设计缺陷或不良编码习惯而导致的不良代码症状。


这种异味也并非来自一种有据可查的标准,更多的是来自程序员的直觉。尤其是经验丰富和知识渊博的程序员,他们无需思考,只要通过查看代码或一段设计就可以立马对这个代码质量产生这种“感觉”,能对代码设计的优劣有一个大致的判断。这有点类似我们英语学到一定程度后,即便不能完全看懂文章,但凭借语感也能选出正确答案。


二、 代码异味的影响


对于代码异味的出现我们其实无需过度紧张,因为在整个程序中代码异味是无处不在的。


一般情况下,有“异味”的代码也依旧能运行得很好。只是倘若重视不够,没有适当地维护或改进代码,代码质量就会下降,系统也会开始变得难以维护和扩展,同时也会增加技术债务。这就像做出有异味的九转大肠的的小胖厨师,在前期准备中对评委的建议置若罔闻,一意孤行,做出来的菜连自己都难以下咽。


所以团队应尽可能地做有质量的代码,减少甚至避免这些问题,产生高效益的成果。


三、 如何辨别代码异味



代码是否存在代码异味,通常是靠程序员的主观判断,但由于语言、开发者、开发理论的不同,对代码异味的判断也会存在差异。


所以要想更精准地识别代码异味,获得更高的代码质量,程序员需要大量的实践和经验。不过,前辈们总结的经验也可以让我们少走一些弯路。Martin Fowler在《重构:改善既有代码的设计》一书中,列举了最常见的24种代码异味,可以帮助我们轻松识别,便于处理和改善它们:


1) 过大的类(Large Class)


一个类包含许多字段、方法或者代码行,并逐渐变得臃肿。


2) 数据泥团(Data Clumps)


代码的不同部分包含了相同的变量组,且这些数据总是绑在一起出现。


3) 过长参数列表(Long Parameter List)


指一个方法的参数超过了三个或四个。出现这种情况一般是将几种类型的算法合并到一个方法之后。


4) 基本类型偏执(Primitive Obsession)


创建一个原始字段比创建一个全新的类要容易得多,所以对于具有意义的业务概念如钱、坐标、范围等,很多程序员不愿意进行建模,而是使用基本数据类型进行表示,进而导致代码内聚性差、可读性差。


5) 神秘命名(Mysterious Name)


在编程中,命名是一件非常恼人的事情。一些可能只有自己看懂的命名,无疑加大了代码可读性的难度,有时甚至自己也会忘记这些命名的含义。


6) 重复代码(Duplicated Code)


这几乎是最常见的异味。当多个程序员同时处理同一程序的不同部分时,通常会发生这种情况。


7) 过长的函数(Long Function)


根据Martin Fowler的经验,通常活得最长、最好的程序,其中的函数都比较短。函数越长,就越难理解。


8) 全局数据(Global Data)


这是一个非常可怕且刺鼻的异味代码。因为从代码库的任何一个角落都可以修改它,而且没有任何机制可以探测出到底是哪段代码做出了修改。全局数据造成一次又一次的诡异Bug,让我们很难找出出错的代码。


9) 可变数据(Mutable Data)


如果可变数据的变量的作用域越大, 越容易出现问题。变量是可以更改的,但我们可能不知道是哪里改变了它。


10) 发散式变化(Divergent Change)


是指一个类受到多种变化的影响。


11) 霰弹式修改(Shotgun Surgery)


是指一种变化引发多个类相应修改。


12) 依恋情结(Feature Envy)


一个类使用另一个类的内部字段和方法的数据多于它自己的数据。


13) 重复的switch(Repeated Switch)


在不同的地方反复使用switch逻辑。这带来的问题就是当我们想要增加一个选择分支时,就必须找到所有的switch,并逐一更新。


14) 循环语句(Loops)


在编程语言中,循环一直是程序设计的核心要素。在《重构》中,Martin Fowler认为它是一种代码异味,因为他们觉得如今的循环已经有点过时了。他们提出“以管道取代循环”,这样可以帮助我们更快看清被处理的元素以及处理它们的动作。


15) 冗赘的元素(Lazy Element)


这是几乎无用的组件。我们在设计代码时有时为了未来的功能设计出“预备”代码,但实际上从未实现;又或者这个类本来有用但随着重构,越来越小,最后只剩下一个函数。无论哪种,它们都是冗赘无用的。


16) 推测的通用性(Speculative Generality)


是指为了“以防万一”,支持预期的未来功能,但这些功能并未被实现,这些类、方法、字段或参数也从未被使用,结果导致代码变得难以理解和支持。


17) 临时字段(Temporary Field)


创建临时字段以用于需要大量输入的算法。但这些字段仅在算法中使用,其余时间不使用。


18) 过长的消息链(Message Chains)


当客户端请求另一个对象,该对象又请求另一个对象,依此类推时,就会出现过长的消息链。这些链意味着客户端依赖于类结构的导航。一旦发生更改,客户端也要跟着修改。


19) 中间人(Middle Man)


指一个类只执行一个动作,但将工作委托给另一个类,这种委托属于过度委托。该类也可能只是一个空壳,只负责委托且只有一件事。


20) 内幕交易(Insider Trading)


指模块之间大量地交换数据,增加模块之间的耦合。


21) 异曲同工的类(Alternative Classes with Different Interface)


是指两个类执行了相同的功能但具有不同的方法名称。


22) 纯数据类(Data Class)


指包含字段和访问它们的粗略方法(getter 和 setter)的类。这些只是其他类使用的数据容器。这些类不包含任何附加功能,并且不能独立操作它们拥有的数据。


23) 被拒绝的遗赠(Refused Bequest)


指如果子类复用了超类的行为,但又不愿意支持超类的接口的情况。


24) 注释(Comments)


程序员将其作为一种“除臭剂”使用情况下的行为。比如:一段代码有着长长的注释,但这段长注释的存在是因为代码很糟糕。


四、 如何对代码“除臭”



1)重构


上述代码异味没有优先级一说,所以对于程序员而言,只能依靠直觉和经验去决定是否需要重构。


重构,一言以蔽之,就是在不改变外部行为的前提下,有条不紊地改善代码。是实现敏捷性的最重要的技术因素之一。是程序员根据已识别出的气味然后将代码分成更小的部分的过程,再决定要么删除它们,要么用更好的代码替换它们,如此循环重复这个过程,直到异味消失,这样可能会提高代码质量并让代码变得更具简单性、灵活性和可理解性。


2)使用代码检测工具



识别和消除代码异味是一个令人厌烦且不确定的过程,而且也不可能手动查找到和删除掉所有异味,尤其是面对一个有着上千行异味的代码的时候。所以使用一些代码检测工具可以辅助我们进行快速大量地审查,帮助我们节约时间来做更为重要的工作,比如能专注于代码高层面的设计原则问题。


好了,关于代码异味的知识,算是讲了个清楚,那么让我们相约下一次代码评审吧!



作者:禅道程序猿
来源:juejin.cn/post/7207617774633123896
收起阅读 »

ChatGPT3微调-评论文本情感分析

前言 如果阅读过openai的文档,便能看到对于模型提供了fine-turning功能,即微调。GPT-3已经在互联网中进行了大量文本的预训练,当我们给出少量示例的提示时,它通常可以直观地了解正在尝试执行的任务并生成一个合理的完成。这通常被称为“小样本学习”。...
继续阅读 »

前言


如果阅读过openai的文档,便能看到对于模型提供了fine-turning功能,即微调。GPT-3已经在互联网中进行了大量文本的预训练,当我们给出少量示例的提示时,它通常可以直观地了解正在尝试执行的任务并生成一个合理的完成。这通常被称为“小样本学习”。但我们需要的是一些特定的需求,比如GPT之前未预训练过的数据或是一些私有数据,便可以用微调通过训练来改进小样本学习。


那么微调都可以解决什么问题呢?结合官网的指南,常见的场景有:



  • 文本生成:可以通过提供相关数据集和指导性的文本,生成更加准确和有针对性的文本

  • 文本分类:将一段文本分成多个类别,例如电子邮件分类

  • 情感分析:分析一段文本的情感倾向,是否积极或消极


本文将对情感分析进行一次尝试。


准备数据


先从网上获取了一份关于酒店评论的数据,总共就两列。


第一列是评论内容,第二列1代表积极, 0 代表消极。


image.png


有需要的可以从这里下载,总共是1w条
评论文本情感分析


不过目前的数据是不能直接使用的,我们需要转换成GPT能接受的格式


{"prompt": "", "completion": ""}
{"prompt": "", "completion": ""}
...

数据预处理


openai很贴心的准备一个工具来验证、格式化数据。


安装CLI


pip install --upgrade openai


验证、格式化


openai tools fine_tunes.prepare_data -f


image.png


执行命令后我们看到他返回的提示中告诉了数据一共有300条,并猜测我们是要进行分类模型,同时建议我们用ada模型,拆分出训练集和测试集,加入分隔符(加入分隔符可以帮助模型更好地理解输入的数据),分别会在接下来让我们选择


为所有提示添加后缀分隔符 `->`
- [Recommended] Add a suffix separator ` ->` to all prompts [Y/n]: Y
在完成的开头添加空格字符
- [Recommended] Add a whitespace character to the beginning of the completion [Y/n]: Y
是否要将其拆分为训练和验证集
- [Recommended] Would you like to split into training and validation set? [Y/n]: Y

无特殊情况全部选Y即可。


image.png


之后会生成两个jsonl文件,同时返回一段参考命令、训练预计的时间。


训练模型


选择模型


首先,我们需要对模型进行一个选择,目前只支持davincicuriebabbageada


模型名称描述训练/1K tokens使用/1K tokens
Davinci最强大的GPT-3模型,可以完成其他模型可以完成的任何任务,通常具有更高的质量$0.0300 $0.1200
Curie非常有能力,但速度更快,成本更低,比Davinci更适合$0.0030$0.0120
Babbage适用于简单任务,非常快速,成本更低$0.0006$0.0024
Ada适用于非常简单的任务,通常是GPT-3系列中速度最快,成本最低的模型$0.0004$0.0016

模型的训练和使用都是需要费用的。出于各种原因我们直接选择Ada。


开始训练


在此之前,我们先将key添加到环境变量中


export OPENAI_API_KEY=""


然后再来看一下之前openai给我们参考的代码


openai api fine_tunes.create 
-t ".\train_data_prepared_train.jsonl"
-v ".\train_data_prepared_valid.jsonl"
--compute_classification_metrics
--classification_positive_class " 1"

-t、-v分别是训练集和测试集


--compute_classification_metrics可以计算模型在分类任务中的性能指标,在验证集上计算模型的准确率(accuracy)、精确率(precision)、召回率(recall)和F1分数。这有助于评估模型的性能,并调整微调过程中的超参数和训练策略。


--classification_positive_class是指分类的积极类别或正例


这里还需要一个 -m,来设置选择的模型。我也是手快直接回车了,本来以为会报错,可它正常运行了,但是默认的模型可能不是我们期望的ada,所以我们需要取消这次训练。


3VGP%(3UDXQ@4`7`}0`IG%V.gif


openai api fine_tunes.cancel -i


不过我也是用list查了一下,发现默认的模型是curie


openai api fine_tunes.list


image.png


接下来我们加上模型等待训练完成即可。
如果过程中不小心关掉窗口或者中断了可以用以下命令恢复。


openai api fine_tunes.follow -i


结束训练


耗时25分钟,花费了0.06刀(比预计的少很多)。


image.png


最后我们看一下分析结果

openai api fine_tunes.results -i


image.png


详细的解析大家可以阅读官方文档,这里我们主要看一下准确度

image.png


使用模型


模型的性能指标给出了0.85的准确率,这里用Playground调用测试一下。


除此之外还可以使用CLI命令


openai api completions.create -m -p


或者使用API


const openai = new OpenAIApi(configuration);
const response = await openai.createCompletion({
model: "训练完后模型的id",
prompt: "Say this is a test",
});

输入的prompt末尾需要加上之前CLI自动给我们补齐的分隔符。


从大数据集中随机拿了几个例子,结果是对的,但是输出有问题
image.png


image.png


image.png


D9}6@O_VYQ@W5R)BI)J%Q_W.gif


应该是completion结尾没有分隔符的原因,明天再试试,顺便扩大一下样本。

梅开二度


第二次训练在completion的末尾全都加上了" ###"作为分隔符。


在playgroud、API、CLI中记得设置Stop


image.png


image.png


image.png


R`F1(}96)`OO(YWJD9`{U]D.jpg


作者:Defineee
来源:juejin.cn/post/7208108117837217848
收起阅读 »

Android将so库封装到jar包中并加载其中的so库

说明 因为一些原因,我们提供给客户的sdk,只能是jar包形式的,一些情况下,sdk里面有native库的时候,就不太方便操作了,此篇文章主要解决如何把so库放入jar包里面,如何打包成jar,以及如何加载。 1.如何把so库放入jar包 so库放入jar参考...
继续阅读 »

说明


因为一些原因,我们提供给客户的sdk,只能是jar包形式的,一些情况下,sdk里面有native库的时候,就不太方便操作了,此篇文章主要解决如何把so库放入jar包里面,如何打包成jar,以及如何加载。


1.如何把so库放入jar包


so库放入jar参考此文章ANDROID将SO库封装到JAR包中并加载其中的SO库
放置路径
将so库改成.jet后缀,放置和加载so库的SoLoader类同一个目录下面。


2.如何使用groovy打包jar


打包jar
先把需要打包的class放置到同一个文件夹下面,然后打包即可,利用groovy的copy task完成这项工作非常简单。


3.如何加载jar包里面的so


3.1.首先判断当前jar里面是否存在so

InputStream inputStream = SoLoader.class.getResourceAsStream("/com/dianping/logan/arm64-v8a/liblogan.jet");

如果inputStream不为空就表示存在。


3.2.拷贝

判断是否已经把so库拷贝到手机里面了,如果没有拷贝过就进行拷贝,这个代码逻辑很简单。


public class SoLoader {
private static final String TAG = "SoLoader";

/**
* so库释放位置
*/

public static String getPath() {
String path = GlobalCtx.getApp().getFilesDir().getAbsolutePath();
//String path = GlobalCtx.getApp().getExternalFilesDir(null).getAbsolutePath();
return path;
}

public static String get64SoFilePath() {
String path = SoLoader.getPath();
String v8a = path + File.separator + "jniLibs" + File.separator +
"arm64-v8a" + File.separator + "liblogan.so";
return v8a;
}

public static String get32SoFilePath() {
String path = SoLoader.getPath();
String v7a = path + File.separator + "jniLibs" + File.separator +
"armeabi-v7a" + File.separator + "liblogan.so";
return v7a;
}

/**
* 支持两种模式,如果InputStream inputStream = SoLoader.class.getResourceAsStream("/com/dianping/logan/arm64-v8a/liblogan.jet");
* 返回了空,表示可能此库是aar接入的,普通加载so库就行,不为空,需要拷贝so库,动态加载
*/

public static boolean jarMode() {
boolean jarMode = false;
InputStream inputStream = SoLoader.class.getResourceAsStream("/com/dianping/logan/arm64-v8a/liblogan.jet");
if (inputStream != null) {
jarMode = true;
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return jarMode;
}

/**
* 是否已经拷贝过so了
*/

public static boolean alreadyCopySo() {
String v8a = SoLoader.get64SoFilePath();
File file = new File(v8a);
if (file.exists()) {
String v7a = SoLoader.get32SoFilePath();
file = new File(v7a);
return file.exists();
}
return false;
}

/**
* 拷贝logan的so库
*/

public static boolean copyLoganJni() {
boolean load;
File dir = new File(getPath(), "jniLibs");
if (!dir.exists()) {
load = dir.mkdirs();
if (!load) {
return false;
}
}
File subdir = new File(dir, "arm64-v8a");
if (!subdir.exists()) {
load = subdir.mkdirs();
if (!load) {
return false;
}
}
File dest = new File(subdir, "liblogan.so");
//load = copySo("/lib/arm64-v8a/liblogan.so", dest);
load = copySo("/com/dianping/logan/arm64-v8a/liblogan.jet", dest);
if (load) {
subdir = new File(dir, "armeabi-v7a");
if (!subdir.exists()) {
load = subdir.mkdirs();
if (!load) {
return false;
}
}
dest = new File(subdir, "liblogan.so");
//load = copySo("/lib/armeabi-v7a/liblogan.so", dest);
load = copySo("/com/dianping/logan/armeabi-v7a/liblogan.jet", dest);
}
return load;
}

public static boolean copySo(String name, File dest) {
InputStream inputStream = SoLoader.class.getResourceAsStream(name);
if (inputStream == null) {
Log.e(TAG, "inputStream == null");
return false;
}
boolean result = false;
FileOutputStream outputStream = null;
try {
outputStream = new FileOutputStream(dest);
int i;
byte[] buf = new byte[1024 * 4];
while ((i = inputStream.read(buf)) != -1) {
outputStream.write(buf, 0, i);
}
result = true;
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return result;
}

}

3.3.加载

首先判断当前应用是32位还是64位Process.is64Bit();。然后加载对应的32或者64位的so。


static {
try {
if (SoLoader.jarMode()) {
if (SoLoader.alreadyCopySo()) {
sIsCloganOk = loadLocalSo();
} else {
boolean copyLoganJni = SoLoader.copyLoganJni();
if (copyLoganJni) {
sIsCloganOk = loadLocalSo();
}
}
} else {
System.loadLibrary(LIBRARY_NAME);
sIsCloganOk = true;
}
} catch (Throwable e) {
e.printStackTrace();
sIsCloganOk = false;
}
}

static boolean loadLocalSo() {
boolean bit = Process.is64Bit();
if (bit) {
String v8a = SoLoader.get64SoFilePath();
try {
System.load(v8a);
return true;
} catch (Throwable e) {
e.printStackTrace();
return false;
}
} else {
String v7a = SoLoader.get32SoFilePath();
try {
System.load(v7a);
return true;
} catch (Throwable e) {
e.printStackTrace();
return false;
}
}
}

作者:CCtomorrow
来源:juejin.cn/post/7206627150621851707
收起阅读 »

Android 完整的apk打包流程

在Android Studio中,我们需要打一个apk包,可以在Gradle task 任务中选一个 assembleDebug/assembleRelease 任务, 控制台上就可以看到所有的构建相关task: 可以看到,这么多个task任务,执行是有...
继续阅读 »

在Android Studio中,我们需要打一个apk包,可以在Gradle task 任务中选一个
assembleDebug/assembleRelease 任务,


企业微信截图_fa2194a8-735e-4720-91be-81fd2524d20f.png


控制台上就可以看到所有的构建相关task:


截屏2023-03-05 20.48.57.png
截屏2023-03-05 20.49.28.png
可以看到,这么多个task任务,执行是有先后顺序的,其实主要就是以下步骤:


//aidl 转换aidl文件为java文件
> Task :app:compileDebugAidl

//生成BuildConfig文件
> Task :app:generateDebugBuildConfig

//获取gradle中配置的资源文件
> Task :app:generateDebugResValues

// merge资源文件
> Task :app:mergeDebugResources

// merge assets文件
> Task :app:mergeDebugAssets
> Task :app:compressDebugAssets

// merge所有的manifest文件
> Task :app:processDebugManifest

//AAPT 生成R文件
> Task :app:processDebugResources

//编译kotlin文件
> Task :app:compileDebugKotlin

//javac 编译java文件
> Task :app:compileDebugJavaWithJavac

//转换class文件为dex文件
> Task :app:dexBuilderDebug

//打包成apk并签名
> Task :app:packageDebug

依靠这些关键步骤最后就能打包出一个apk。


首先看


第一步:aidl(编译aidl文件)


将项目中的aidl文件编译为java文件,AIDL用于进程间通信


第二步:生成BuildConfig文件


在项目中配置了
buildConfigField等信息,会在BuildConfig class类里以静态属性的方式展示:


截屏2023-03-05 21.09.18.png


第三步:合并Resources、assets、manifest、so等资源文件


在我们的项目中会依赖不同的库、组件,也会有多渠道的需求,所以merge这一步操作就是将不同地方的资源文件进行整合。
多个manifest文件也需要整理成一个完整的文件,所以如果有属性冲突这一步就会报错。资源文件也会整理分类到不同的分辨率目录中。


资源处理用的工具是aapt/aapt2


注意AGP3.0.0之后默认通过AAPT2来编译资源,AAPT2支持了增量更新,大大提升了效率。


AAPT 工具负责编译项目中的这些资源文件,所有资源文件会被编译处理,XML 文件(drawable 图片除外)会被编译成二进制文件,所以解压 apk 之后无法直接打开 XML 文件。但是 assets 和 raw 目录下的资源并不会被编译,会被原封不动的打包到 apk 压缩包中。
资源文件编译之后的产物包括两部分:resources.arsc 文件和一个 R.java。前者保存的是一个资源索引表,后者定义了各个资源 ID 常量。这两者结合就可以在代码中找到对应的资源引用。比如如下的 R.java 文件:


截屏2023-03-05 21.19.59.png
实际上被打包到 apk 中的还有一些其他资源,比如 AndroidManifest.xml 清单文件和三方库中使用的动态库 .so 文件。


第四步:编译java文件(用到的工具 javac )


1、java文件包含之前提到的AIDL 生成的java文件


2、java代码部份:通过Java Compiler 编译项目中所有的Java代码,包括R.java.aidl文件生成的.java文件、Java源文件,生成.class文件。在对应的build目录下可以找到相关的代码


3、kotlin代码部份:通过Kotlin Compiler编译项目中的所有Kotlin代码,生成.class文件


注:注解处理器(APT,KAPT)生成代码也是在这个阶段生成的。当注解的生命周期被设置为CLASS的时候,就代表该注解会在编译class文件的时候生效,并且生成java源文件和Class字节码文件。

第五步: Class文件打包成DEX(dx/r8/d8等工具编译class文件)


image.png



  • 在原来 dx是最早的转换工具,用于转换class文件为dex文件。

  • Android Studio 3.1之后,引入了D8编译器和 R8 工具。

  • Android Studio 3.4之后,默认开启 R8
    具体的区别可以点击看看


注意:JVM 和 Dalvik(ART) 的区别:JVM执行的是.class文件、Dalvik和ART执行的.dex文件。具体的区别可以点击看看


而在编译class文件过程也常用于编译插桩,比如ASM,通过直接操作字节码文件完成代码修改或生成。


第六步:apkbuilder/zipflinger(生成APK包)


这一步就是生成APK文件,将manifest文件、resources文件、dex文件、assets文件等等打包成一个压缩包,也就是apk文件。
在老版本使用的工具是apkbuilder,新版本用的是 zipflinger
而在AGP3.6.0之后,使用zipflinger作为默认打包工具来构建APK,以提高构建速度。


第七步: zipalign(对齐处理)


对齐是Android apk 很重要的优化,它会使 APK 中的所有未压缩数据(例如图片或原始文件)在 4 字节边界上对齐。这使得CPU读写就会更高效。


也就是使用工具 zipalign 对 apk 中的未压缩资源(图片、视频等)进行对齐操作,让资源按照 4 字节的边界进行对齐。这种思想同 Java 对象内存布局中的对齐空间非常类似,主要是为了加快资源的访问速度。如果每个资源的开始位置都是上一个资源之后的 4n 字节,那么访问下一个资源就不用遍历,直接跳到 4n 字节处判断是不是一个新的资源即可。


第八步: apk 签名


没有签名的apk 无法安装,也无法发布到应用市场。


大家比较熟知的签名工具是JDK提供的jarsigner,而apksignerGoogle专门为Android提供的签名和签证工具。


其区别就在于jarsigner只能进行v1签名,而apksigner可以进行v2v3v4签名。



  • v1签名


v1签名方式主要是利用META-INFO文件夹中的三个文件。


首先,将apk中除了META-INFO文件夹中的所有文件进行进行摘要写到 META-INFO/MANIFEST.MF;然后计算MANIFEST.MF文件的摘要写到CERT.SF;最后计算CERT.SF的摘要,使用私钥计算签名,将签名和开发者证书写到CERT.RSA。


所以META-INFO文件夹中这三个文件就能保证apk不会被修改。



  • v2签名


Android7.0之后,推出了v2签名,为了解决v1签名速度慢以及签名不完整的问题。


apk本质上是一个压缩包,而压缩包文件格式一般分为三块:


文件数据区,中央目录结果,中央目录结束节。


而v2要做的就是,在文件中插入一个APK签名分块,位于中央目录部分之前,如下图:


图片


这样处理之后,文件就完成无法修改了,这也是为什么 zipalign(对齐处理) 要在签名之前完成。



  • v3签名


Android 9 推出了v3签名方案,和v2签名方式基本相同,不同的是在v3签名分块中添加了有关受支持的sdk版本和新旧签名信息,可以用作签名替换升级。



  • v4签名


Android 11 推出了v4签名方案。


最后,apk得以完成打包


PMS 在安装过程中会检查 apk 中的签名证书的合法性,具体安装apk内容稍后介绍。


apk内容包含如下:


截屏2023-03-05 22.01.14.png


总体的打包流程图如下:


截屏2023-03-05 22.02.33.png


,,


作者:大强Dev
来源:juejin.cn/post/7206998548343668796
收起阅读 »

字节跳动音视频面试一面挂,转拿腾讯音视频 offer

一、面试官: 视频为什么需要压缩 心理分析:视频压缩在音视频领域是一个恒久不变的话题,有压缩也就意味有解压操作,我们把压 缩称为编码 解压成为解码。它们是成对出现的,做音视频最难的就在音视频编解码。如何提高音 视频播放效率,在不牺牲视频质量下 做高度压缩就显...
继续阅读 »

一、面试官: 视频为什么需要压缩



心理分析:视频压缩在音视频领域是一个恒久不变的话题,有压缩也就意味有解压操作,我们把压
缩称为编码 解压成为解码。它们是成对出现的,做音视频最难的就在音视频编解码。如何提高音
视频播放效率,在不牺牲视频质量下 做高度压缩就显得格外重要了。面试官想问的问题并不是压
缩了什么,而是编码中对视频帧做了什么



求职者:需要求职者对视频编码有所了解,接下来我们从帧内压缩,与帧间压缩讲起



  • 未经压缩的数字视频的数据量巨大 下图一分钟的视频量 差不多需要68G

  • 存储困难:一张32G的U盘只能存储几秒钟的未压缩数字视频。

  • 传输困难 : 1兆的带宽传输一秒的视频需要大约10分钟。



二、面试官: 封装格式是什么



心理分析:很多人对音视频的概念停留在 苍老师的小电影上,只能理解他是一个视频文件。面试官考
的对视频文件下的封装格式,封装格式里面的内容有没有了解



求职者:首先需要从封装格式概念讲起,慢慢深入到封装格式基础下,然后散发解封装与封装过程


(1)封装格式(也叫容器)就是将已经编码压缩好的视频轨和音频轨按照一定的格式放到一个文件中,也就
是说仅仅是一个外壳,可以把它当成一个放视频轨和音频轨的文件夹也可以。
(2)通俗点说视频轨相当于饭,而音频轨相当于菜,封装格式就是一个碗,或者一个锅,用来盛放饭菜的容
器。
(3)封装格式和专利是有关系的,关系到推出封装格式的公司的盈利。
(4)有了封装格式,才能把字幕,配音,音频和视频组合起来。
(5)常见的AVI、RMVB、MKV、ASF、WMV、MP4、3GP、FLV等文件都指的是一种封装格式。


举例MKV格式的封装



三、面试官: 一个视频文件是否可以完成倒放(或者你们的倒放如何实现)



心理分析:面试官考的是 是否有经历过音视频剪辑相关的经验,需要从求职者中得到的答案,不是
“能”或者“不能” 而是分析为什么不能倒放,



不能倒放的本质原因,对I B P有有没有了解



求职者:倒放在视频剪辑中 是必备功能,按常理来看,倒放肯定是能够实现的,但是问题就出现在
这里,求职者如果对视频编码原理不理解的话,对视频倒放肯定打不上来的,求职者需要首先答对
“一个视频不能实现倒放,两个文件可以" 这个入手, 再从编解码入手 讲解为什么不能实现倒放



答案


第一种方式:



  1. 从第一个gop然后顺序解码

  2. 将一个解码的gop的yuv写入文件中

  3. 将第二个gop顺序解码yuv写入文件

  4. 第三个gop序列,以此类推....


然后倒序读入内存中,进行编码即可.



缺点:如果文件过大,不能使用此方法,因为yuv文件较大,一分钟yuv就有1-2G左右,有可能撑爆sdcard.



第二种方式



  1. 全部遍历视频一遍,获取一共有多少gop序列

  2. 跳到(seek)到最后一个gop的I帧,然后把这个gop解码的yuv存放在sdcard

  3. 再逆序读出这个解码的gop的yuv,进行编码,这样最后一个gop就变成了第一帧的gop;

  4. 接下来seek到倒数第一个gop的I帧,依次类推,把每个gop解码、然后编码


其实在音视频岗位面试中,问到得远远不止上面的相关问题,上述知识举例,还有更多内容可以面试题可以参考:



1.什么是I帧 P帧 B帧?
2.简述H264视频编码流程?
3.视频能倒放吗,倒放如何实现?
4.硬编码与软编码有什么区别?
5.你对sps 和pps的理解?
6.如何从一段残缺H264数据 解析出画面?
7.讲讲MediaCodec硬编码底层解码机制?
8.音频播放过快,视频慢,如何选择丢帧
9.码率和分辨率都会影响视频的清晰度
10.生产者和消费者的关系
11.sps和pps的区别
12.……



对一些没有学习过、了解过音视频这块知识点的朋友,仿佛是在看天书一般,在这里请大家不要着急,在这为大家准备了《Android 音视频开发入门到精通》的学习笔记:https://qr18.cn/Ei3VPD,帮助大家快速提升。


作者:冬日毛毛雨
来源:juejin.cn/post/7208092574162157626
收起阅读 »

如何避免使用过多的 if else?

web
一、引言 相信大家听说过回调地狱——回调函数层层嵌套,极大降低代码可读性。其实,if-else层层嵌套,如下图所示,也会形成类似回调地狱的情况。 当业务比较复杂,判断条件比较多,项目进度比较赶时,特别容易使用过多if-else。其弊端挺多的,如代码可读性差、...
继续阅读 »

一、引言


相信大家听说过回调地狱——回调函数层层嵌套,极大降低代码可读性。其实,if-else层层嵌套,如下图所示,也会形成类似回调地狱的情况。


image.png


当业务比较复杂,判断条件比较多,项目进度比较赶时,特别容易使用过多if-else。其弊端挺多的,如代码可读性差、代码混乱、复杂度高、影响开发效率、维护成本高等。


因此,我们在日常编码时,有必要采取一些措施避免这些问题。本文的初衷不是建议大家完全不用if-else,而是希望我们能够在学会更多解决方案后更优雅地编码。


R-C.gif




二、8种if-else的优化/替代方案


1. 使用排非策略:!、!!


逻辑非(logic NOT),是逻辑运算中的一种,就是指本来值的反值。


当你想这么写时……


1、判断是否为空
if(value === null || value === NaN || value === 0 || value === ''|| value === undefined )
{
……
}

2、判断是否数组是否含有符合某条件的元素
const name = arr.find(item => item.status === 'error')?.name;
if(name !== undefined && name !== ''){
……
}

不妨尝试这么写:


1、判断是否为空
if(!value){……}

2、判断是否数组是否含有符合某条件的元素
if(!!arr.find(item => item.status === 'error')?.name){……}



2. 使用条件(三元)运算符: c ? t : f


三元运算符:
condition ? exprIfTrue : exprIfFalse;
如果条件为真值,则执行冒号(:)前的表达式;若条件为假值,则执行最后的表达式。


当你想这么写时……


let beverage = '';
if(age > 20){
beverage = 'beer';
} else {
beverage = 'juice';
}

不妨尝试这么写:


const beverage = age > 20 ? 'beer' : 'juice';

tips: 建议只用一层三元运算符,多层嵌套可读性差。




3. 使用短路运算符:&&||



  • && 为取假运算,从左到右依次判断,如果遇到一个假值,就返回假值,以后不再执行,否则返回最后一个真值;

  • || 为取真运算,从左到右依次判断,如果遇到一个真值,就返回真值,以后不再执行,否则返回最后一个假值。


当你想这么写时……


    if (isOnline){
makeReservation(user);
}

不妨尝试这么写:


 isOnline && makeReservation(user);



4. 使用 switch 语句


当你想这么写时……


    let result;
if (type === 'add'){
result = a + b;
} else if(type === 'subtract'){
result = a - b;
} else if(type === 'multiply'){
result = a * b;
} else if(type === 'divide'){
result = a / b;
} else {
console.log('Calculation is not recognized');
}

不妨尝试这么写:


let result;
switch (type) {
case 'add':
result = a + b;
break;
case 'subtract':
result = a - b;
break;
case 'multiply':
result = a * b;
break;
case 'divide':
result = a / b;
break;
default:
console.log('Calculation is not recognized');
}

个人认为,对于这类比较简单的判断,用switch语句虽然不会减少代码量,但是会更清晰喔。




5. 定义相关函数拆分逻辑,简化代码


当你想这么写时……


function itemDropped(item, location) {
if (!item) {
return false;
} else if (outOfBounds(location) {
var error = outOfBounds;
server.notify(item, error);
items.resetAll();
return false;
} else {
animateCanvas();
server.notify(item, location);
return true;
}
}

不妨尝试这么写:


// 定义dropOut和dropIn, 拆分逻辑并提高代码可读性
function itemDropped(item, location) {
const dropOut = function () {
server.notify(item, outOfBounds);
items.resetAll();
return false;
};

const dropIn = function () {
animateCanvas();
server.notify(item, location);
return true;
};

return !!item && (outOfBounds(location) ? dropOut() : dropIn());
}

细心的朋友会发现,在这个例子中,同时使用了前文提及的优化方案。这说明我们在编码时可以根据实际情况混合使用多种解决方案。




6. 将函数定义为对象,通过穷举查找对应的处理方法




  • 定义普通对象


    对于方案3的例子,不妨尝试这么写:




function calculate(action, num1, num2) {
 const actions = {
   add: (a, b) => a + b,
   subtract: (a, b) => a - b,
   multiply: (a, b) => a * b,
   divide: (a, b) => a / b,
};

 return actions[action]?.(num1, num2) ?? "Calculation is not recognized";
}



  • 定义 Map 对象


    普通对象的键需要是字符串,而 Map 对象的键可以是一个对象、数组或者更多类型,更加灵活。




let statusMap = new Map([
[
{ role: "打工人", status: "1" },
() => { /*一些操作*/},
],
[
{ role: "打工人", status: "2" },
() => { /*一些操作*/},
],
[
{ role: "老板娘", status: "1" },
() => { /*一些操作*/},
],
]);

let getStatus = function (role, status) {
statusMap.forEach((value, key) => {
if (JSON.stringify(key) === JSON.stringify({ role, status })) {
value();
}
});
};

getStatus("打工人", "1"); // 一些操作

tips: JSON.stringify()可用于深比较/深拷贝。




7. 使用责任链模式


责任链模式:将整个处理的逻辑改写成一条责任传递链,请求在这条链上传递,直到有一个对象处理这个请求。


例如 JS 中的事件冒泡


简单来说,事件冒泡就是在一个对象上绑定事件,如果定义了事件的处理程序,就会调用处理程序。相反没有定义的话,这个事件会向对象的父级传播,直到事件被执行,最后到达最外层,document对象上。


image.png


这意味着,在这种模式下,总会有程序处理该事件。

再举个🌰,当你想这么写时……


function demo (a, b, c) {
if (f(a, b, c)) {
if (g(a, b, c)) {
// ...
}
// ...
else if (h(a, b, c)) {
// ...
}
// ...
} else if (j(a, b, c)) {
// ...
} else if (k(a, b, c)) {
// ...
}
}

不妨参考这种写法:


const rules = [
{
match: function (a, b, c) { /* ... */ },
action: function (a, b, c) { /* ... */ }
},
{
match: function (a, b, c) { /* ... */ },
action: function (a, b, c) { /* ... */ }
},
{
match: function (a, b, c) { /* ... */ },
action: function (a, b, c) { /* ... */ }
}
// ...
]

// 每个职责一旦匹配,原函数就会直接返回。
function demo (a, b, c) {
for (let i = 0; i < rules.length; i++) {
if (rules[i].match(a, b, c)) {
return rules[i].action(a, b, c)
}
}
}


引申话题——如何降低if else代码的复杂度?


相关文章阅读: 如何无痛降低 if else 面条代码复杂度 建议多读几次!!!





8. 策略模式+工厂方法


因为此法比较复杂,此文暂时不做详细介绍。


详细可参考文章优化方案 8 if-else 代码优化的八种方案


三、小结


本文粗略介绍了8种优化/替代if-else的方法,希望能给你日常编码带来一些启示😄。


正如开头所说,我们的目的不是消灭代码中的if-else,而是让我们在学会更多解决方案的基础上,根据实际情况选择更优的编码方式。因此,当你发现自己的代码里面存在特别多的if-else或当你想用if-else时,不妨停下来思考一下——如何能写得更优雅、更方便日后维护呢


image.png


四、参考与感谢



  1. 优化 JS 中过多的使用 IF 语句

  2. 短路运算符(逻辑与&& 和 逻辑或||)

  3. 如何对多个 if-else 判断进行优化

  4. if-else 代码优化的八种方案

  5. 如何替换项目中的if-else和switch

  6. 如何无痛降低 if else 面条代码复杂度


作者:蓝瑟
来源:juejin.cn/post/7206529406613094460
收起阅读 »

听说你还不会使用Koa?

web
简介 Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。 通过利用 async 函数,Koa 帮你丢弃回调函数,并有力地增强错误处理。 Koa 并没...
继续阅读 »

简介


Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。 通过利用 async 函数,Koa 帮你丢弃回调函数,并有力地增强错误处理。 Koa 并没有捆绑任何中间件, 而是提供了一套优雅的方法,帮助您快速而愉快地编写服务端应用程序。


简单来说,Koa也是一个web框架,但是比Express更轻量,并且有更好的异步机制。


本文适合有Koa基础,急需需要搭建项目的同学食用,如果对Koa完全不了解的建议先去看看Koa官方文档。


在讲Koa的使用之前,我们先来介绍一下非常出名的洋葱模型,这对后面代码的理解有很好的帮助


洋葱模型


前面我们在介绍Express的时候就说过了洋葱模型,如下图所示,Koa中的中间件执行机制也类似一个洋葱模型,只不过和Express还是有些许区别。


image.png


我们来看看Koa中的中间件是怎么样执行的。与Express不同的是在Koa中,next是支持异步的。


// 全局中间件
app.use(async (ctx, next) => {
console.log(
"start In comes a " + ctx.request.method + " to " + ctx.request.url
);
next();
console.log(
"end In comes a " + ctx.request.method + " to " + ctx.request.url
);
});

// 单独中间件
router.get(
"/select",
async (ctx, next) => {
console.log("我是单独中间件 start");
const result = await Promise.resolve(123);
console.log(result);
next();
console.log("我是单独中间件 end");
},
async (ctx) => {
console.log("send result start");
ctx.body = "get method!";
console.log("send result end");
}
);

看看上面的输出结果,可以看到它的执行顺序和Express是一样的。


image.png


前面说了,在Koanext是支持异步的。也就是可以await,我们添加await来测试下


// 全局中间件
app.use(async (ctx, next) => {
console.log(
"start In comes a " + ctx.request.method + " to " + ctx.request.url
);
await next();
console.log(
"end In comes a " + ctx.request.method + " to " + ctx.request.url
);
});

// 单独中间件
router.get(
"/select",
async (ctx, next) => {
console.log("我是单独中间件 start");
const result = await Promise.resolve(123);
console.log(result);
await next();
console.log("我是单独中间件 end");
},
async (ctx) => {
console.log("send result start");
ctx.body = "get method!";
console.log("send result end");
}
);

看看运行结果


image.png


可以看到,在Koa中,await会阻塞所有后续代码的执行,完全保证了按洋葱模型执行代码。以next为分水岭,先从前往后执行next前半部分代码,然后从后往前执行next下半部分代码。


Express中,next方法是不支持异步await的,这个是KoaExpress洋葱模型里面最大的一个区别。


创建应用


首先我们需要安装koa


npm i koa

然后引入使用


const Koa = require("koa");
const app = new Koa();

app.listen(3000, () => {
console.log("serve running on 3000");
});

这个和Express还是很相似的。


路由


Koa的路由和Express还是有差别的。Koaapp是不支持直接路由的,需要借助第三方插件koa-router


我们先来安装


npm i @koa/router

然后就可以引入使用了


// routes/user.js
const Router = require("@koa/router");
const router = new Router({ prefix: "/user" }); // 路由前缀

router.get("/select", (ctx) => {
ctx.body = "get";
});

router.post("/add", (ctx) => {
ctx.body = "post";
});

router.delete("/delete", (ctx) => {
ctx.body = "delete";
});

router.put("/update", (ctx) => {
ctx.body = "put";
});

// 所有请求都支持
router.all("/userall", (ctx) => {
ctx.body = "所有请求都可以?" + ctx.method;
});

// 重定向
router.get("/testredirect", (ctx) => {
ctx.redirect("/user/select");
});

module.exports = router;

然后在入口文件,引入路由并注册就可以使用了


const Koa = require("koa");
const app = new Koa();
const userRouter = require("./routes/user");

app.use(userRouter.routes()).use(userRouter.allowedMethods());

这样我们就可以通过localhost:3000/user/xxx来调用接口了。


自动注册路由


同样的,如果模块很多的话,我们还可以优化,通过fs模块读取文件,自动完成路由的注册。


// routes/index.js
const fs = require("fs");

// 批量注册路由
module.exports = (app) => {
fs.readdirSync(__dirname).forEach((file) => {
if (file === "index.js") {
return;
}
const route = require(`./${file}`);
app.use(route.routes()).use(route.allowedMethods());
});
};

在入口文件,我们可以通过该方法批量注册路由了


const registerRoute = require("./routes/index");
registerRoute(app);

这样我们就可以通过localhost:3000/模块路由前缀/xxx来调用接口了。


路由说完了,我们再来看看怎么获取参数


参数获取


参数的获取分为query、param、body三种形式


query参数


对于query参数,通过req.query获取


router.get("/", (ctx) => {
const query = ctx.query;
// const query = ctx.request.query; // 上面是简写形式
ctx.body = query;
});

参数能正常获取


image.png


我们再来看看路径参数


路径参数


对于路径参数,通过:变量定义,然后通过request.params获取。


router.get("/user2/:name/:age", (ctx) => {
// 路径参数获取
const params = ctx.params;
// const params = ctx.request.params; // 上面是简写形式
ctx.body = params
});

参数能正常获取


image.png


body参数


对于body参数,也就是请求体里面的参数,就需要借助koa-body插件。但是在新版的Express中已经自身支持了。


首先安装koa-body插件


npm i koa-body

然后在入口文件使用


const { koaBody } = require("koa-body");

app.use(koaBody());

然后通过ctx.request.body就可以获取到参数啦。


router.post("/", (ctx) => {
const body = ctx.request.body;
ctx.body = body;
});

设置完后,我们来测试下,参数正常获取。


image.png


文件上传


说完参数的获取,我们再来看看怎么处理文件上传。


koa中,对于文件上传也是借助koa-body插件,只需要在里面配置上传文件的参数即可。相较Express要简单很多。


app.use(
koaBody({
// 处理文件上传
multipart: true,
formidable: {
// 使用oss上传就注释 上传到本地就打开。路径必须事先存在
uploadDir: path.join(__dirname, "./uploads"),
keepExtensions: true,
},
})
);

配置好后,我们来测试一下


Express不同的是,不管是单文件还是多文件,都是通过ctx.request.files获取文件。


单文件上传


router.post("/file", (ctx) => {
const files = ctx.request.files;
ctx.body = files;
});

我们可以看到,它返回的是一个对象,并且在没填写表单字段的时候,它的key是空的。


image.png


我们再来看看有表单字段的


router.post("/file2", (ctx) => {
const files = ctx.request.files;
ctx.body = files;
});

可以看到,它返回的对象key就是我们的表单字段名。


image.png


我们再来看看多文件上传的情况


多文件上传


我们先来看看多文件不带表单字段的情况


router.post("/files", (ctx) => {
const files = ctx.request.files;
ctx.body = files;
});

可以看到,它返回的还是一个对象,只不过属性值是数组。


image.png


我们来看看带表单字段的情况,对于带表单字段的多文件上传,它返回的对象里面的key值就不是空值,并且如果是多个文件,它是以数组形式返回。


image.png


image.png


静态目录


文件上传我们介绍完毕了,如果我们想访问我们上传的图片该怎么办呢?能直接访问吗


对于文件,我们需要开启静态目录才能通过链接访问到我们目录里面的内容。与Express不同,koa需要借助koa-static插件才能开启静态目录。


下面的配置就是将我们系统的uploads目录设置为静态目录,这样我们通过域名就能直接访问该目录下的内容了。


const koaStatic = require("koa-static");

app.use(koaStatic(path.join(__dirname, "uploads")));

可以看到,图片能正确访问。


image.png


细心的同学可能发现了它是直接在域名后面访问,并没有像Express一样有个static前缀。那怎么实现这种自定义前缀的效果呢?


自定义静态资源目录前缀


Koa中,需要借助koa-mount插件


我们先来安装一下


npm i koa-mount

然后和koa-static搭配使用


app.use(mount("/static", koaStatic(path.join(__dirname, "uploads"))));

然后我们就可以带上/static前缀访问静态资源了。


image.png


错误处理


koa也可以通过中间件来捕获错误,但是需要注意,这个中间件需要写在前面


app.use(async (ctx, next) => {
try {
await next();
} catch (error) {
// 响应用户
ctx.status = error.status || 500;
ctx.body = error.message || "服务端错误";
// ctx.app.emit("error", error); // 触发应用层级error事件
}
});

我们来测试一下


// 模拟错误
router.get("/error", function (ctx, next) {
// 同步错误可以直接捕获
throw new Error("同步错误");
});

可以看到,错误被中间件捕获并正常返回了。


image.png


我们再来看看异步错误


router.get("/error2", async function (ctx, next) {
// 新建异步错误
await Promise.reject(new Error("异步错误"));
});

也能被正常捕获。


image.png


可以看到,相较于Express的错误处理,Koa变得更简单了,不管是同步错误还是异步错误都能正常捕获。


日志


对于线上项目用来说,日志是非常重要的一环。log4js是使用得比较多的一个日志组件,经常跟Express一起配合使用。本文简单讲解下在Express怎么使用log4js


我们首先来安装该插件,笔者这里安装的版本是6.8.0


npm install log4js

然后我们创建一个utils文件夹下创建log.js,用来创建一个logger


// utils/log.js

const log4js = require("log4js");
const logger = log4js.getLogger();

logger.level = "debug"; // 需要打印的日志等级

module.exports = logger;

在需要的地方引入logger就可以了,我们来测试下


app.get("/logtest", (req, res) => {
logger.debug("Some debug messages");
logger.info("Some info messages");
logger.warn("Some warn messages");
logger.error("Some error messages");
res.send("test log");
});

可以看到,日志都打印出来了


image.png


日志等级


我们再来改变下输出日志的等级


logger.level = "warn"; // 需要打印的日志等级

再来测试下,发现只输出了warnerror等级的日志,debuginfo等级的过滤掉了。


image.png


日志输出到文件


日志如果想输出到文件,我们还可以配置log4js


const log4js = require("log4js");

log4js.configure({
appenders: { test: { type: "file", filename: "applog.log" } },
categories: { default: { appenders: ["test"], level: "warn" } },
});

const logger = log4js.getLogger();

module.exports = logger;

我们再来测试下,发现它自动创建了applog.log文件,并将日志写入到了里面。


image.png


连接数据库


数据库目前主要有关系型数据库、非关系型数据库、缓存数据库,这三种数据库我们各举一个例子。


连接mongodb


为了方便操作mongodb,我们使用mongoose插件


首先我们来安装


npm  i mongoose

安装完后我们先创建db文件夹,然后创建mongodb.js,在这里来连接我们的mongodb数据库


// db/mongodb.js

const mongoose = require("mongoose");

module.exports = () => {
// 数据库连接
return new Promise((resolve, reject) => {
mongoose
.connect("mongodb://localhost/ExpressApi", {
// useNewUrlParser: true,
// useUnifiedTopology: true,
// useFindAndModify: false,
})
.then(() => {
console.log("mongodb数据库连接成功");
resolve();
})
.catch((e) => {
console.log(e);
console.log("mongodb数据库连接失败");
reject();
});
});
};

然后在我们的入口文件引用使用


// index.js

// 连接mongodb
const runmongodb = require("./db/mongodb.js");
runmongodb();

保存,我们运行一下,可以看到mongodb连接成功。


image.png


我们查看mongodb面板,可以看到KoaApi数据库也创建成功了


image.png


数据库连接成功了,下面我们正式来创建接口。


我们以mvc模式,创建model、controller、route三个文件夹分别来管理模型、控制器、路由。


项目总体目录如下


model // 模型
controller // 控制器
route // 路由
db // 数据库连接
index.js // 入口文件

创建接口总共分为四步



  1. 创建模型

  2. 创建控制器

  3. 创建路由

  4. 使用路由


我们先来创建一个user model


// model/user.js
const mongoose = require("mongoose");
// 建立用户表
const UserSchema = new mongoose.Schema(
{
username: {
type: String,
unique: true,
},
password: {
type: String,
select: false,
},
},
{ timestamps: true }
);

// 建立用户数据库模型
module.exports = mongoose.model("User", UserSchema);

然后创建user控制器,定义一个保存和一个查询方法。


// controller/userController.js
const User = require("../model/user");

class UserController {
async create(ctx) {
const { username, password } = ctx.request.body;
const repeatedUser = await User.findOne({ username, password });
if (repeatedUser) {
ctx.status = 409;
ctx.body = {
message: "用户已存在",
};
} else {
const user = await new User({ username, password }).save();
ctx.body = user;
}
}

async query(ctx) {
const users = await User.find();
ctx.body = users;
}
}

module.exports = new UserController();

然后我们在路由里面定义好查询和创建接口


// route/user.js

const Router = require("@koa/router");
const router = new Router({ prefix: "/user" });
const { create, query } = require("../controller/userController");

router.post("/create", create);
router.get("/query", query);

module.exports = router;

最后我们在入口文件使用该路由,前面我们说啦,路由少可以一个一个引入使用,对于路由多的话还是推荐使用自动注入的方式。


为了方便理解,这里我们还是使用引入的方式


// index.js

const userRouter = require("./routes/user");
app.use(userRouter.routes()).use(userRouter.allowedMethods())

好啦,通过这四步,我们的接口就定义好啦,我们来测试一下


先来看看新增,接口正常返回


image.png


我们来看看数据库,发现user表添加了一条新记录。


image.png


我们再来看看查询接口,数据也能正常返回。


image.png


至此,我们的mongodb接口就创建并测试成功啦。


连接mysql


为了简化我们的操作,这里我们借助了ORM框架sequelize


我们先来安装这两个库


npm i mysql2 sequelize

然后在db目录下创建mysql.js用来连接mysql


const Sequelize = require("sequelize");

const sequelize = new Sequelize("KoaApi", "root", "123456", {
host: "localhost",
dialect: "mysql",
});

// 测试数据库链接
sequelize
.authenticate()
.then(() => {
console.log("数据库连接成功");
})
.catch((err) => {
// 数据库连接失败时打印输出
console.error(err);
throw err;
});

module.exports = sequelize;

这里要注意,需要先把数据库koaapi提前创建好。它不会自动创建。


跟前面一样,创建接口总共分为四步



  1. 创建模型

  2. 创建控制器

  3. 创建路由

  4. 使用路由


首先我们创建model,这里我们创建user2.js


// model/user2.js

const Sequelize = require("sequelize");
const sequelize = require("../db/mysql");

const User2 = sequelize.define("user", {
username: {
type: Sequelize.STRING,
},
password: {
type: Sequelize.STRING,
},
});

//同步数据库:没有表就新建,有就不变
User2.sync();

module.exports = User2;

然后创建控制器,定义一个保存和一个查询方法。


// controller/user2Controller.js

const User2 = require("../model/user2.js");

class user2Controller {
async create(ctx) {
const { username, password } = ctx.request.body;

try {
const user = await User2.create({ username, password });
ctx.body = user;
} catch (error) {
ctx.status = 500;
ctx.body = { code: 0, message: "保存失败" };
}
}

async query(ctx) {
const users = await User2.findAll();
ctx.body = users;
}
}

module.exports = new user2Controller();

然后定义两个路由


const router = new Router({ prefix: "/user2" });
const { query, create } = require("../controller/user2Controller");

// 获取用户
router.get("/query", query);
// 添加用户
router.post("/create", create);

module.exports = router;

最后在入口文件使用该路由


// index.js

const user2Router = require("./routes/user2");
app.use(user2Router.routes()).use(user2Router.allowedMethods())

好啦,通过这四步,我们的接口就定义好啦,我们来测试一下


先来看看新增,接口正常返回


image.png


我们来看看数据库,发现users表添加了一条新记录。


image.png


我们再来看看查询接口,数据也能正常返回。


image.png


至此,我们的mysql接口就创建并测试成功啦。


我们再来看看缓存数据库redis


连接redis


这里我们也需要借助node-redis插件


我们先来安装


npm i redis

然后在db目录下创建redis.js用来连接redis


// db/redis.js

const { createClient } = require("redis");

const client = createClient();

// 开启连接
client.connect();

// 连接成功事件
client.on("connect", () => console.log("Redis Client Connect Success"));
// 错误事件
client.on("error", (err) => console.log("Redis Client Error", err));

module.exports = client;

然后我们创建一个简单的路由来测试一下


// route/dbtest

const Router = require("@koa/router");
const router = new Router({ prefix: "/dbtest" });
const client = require("../db/redis");

router.get("/redis", async (ctx) => {
await client.set("name", "randy");
const name = await client.get("name");
ctx.body = { name };
});

module.exports = router;

然后把该路由在入口文件注册使用


// index.js

const dbtestRouter = require("./routes/dbtest");
app.use(dbtestRouter.routes()).use(dbtestRouter.allowedMethods())

最后我们来测试下接口,可以看到接口正常返回


image.png


我们再来查看一下我们的redis数据库,发现数据保存成功。


image.png


当然,这里只是一个简单的入门,redis的操作还有很多,大家可以看官方文档,这里笔者就不再详细说啦。


token验证


对于token的认证,我们这里使用目前比较流行的方案 jsonwebtoken


生成token


我们首先安装jsonwebtoken


npm i jsonwebtoken

安装完后,我们来实现一个登录接口,在接口里生成token并返回给前端。


注意这里因为是演示,所以将密钥写死,真实项目最好从环境变量里面动态获取。


// route/user.js
const jwt = require("jsonwebtoken");

// ...
async login(ctx) {
const { username, password } = ctx.request.body;
const user = await User.findOne({ username, password });
if (user) {
const token = jwt.sign(
{ id: user.id, username: user.username },
"miyao",
{ expiresIn: 60 }
);

ctx.body = {
token,
};
} else {
ctx.status = 401;
ctx.body = {
message: "账号或密码错误",
};
}
}

// ...

这里生成token的接口我们就定义好了,我们来测试一下。


首先输入错误的账号,看到它提示账号密码错误了


image.png


然后我们输入正确的账号密码试一下,可以看到,token被正常返回出来了。


image.png


到这里我们通过jsonwebtoken生成token就没问题了。接下来就是怎么验证token了。


token解密


在说token验证前,我们先来说个token解密,一般来说token是不需要解密的。但是如果非要看看里面是什么东西也是有办法解密的,那就得用到jwt-decode插件了。


该插件不验证密钥,任何格式良好的JWT都可以被解码。


我们来测试一下,


首先安装该插件


npm i jwt-decode

然后在登录接口里面使用jwt-decode解析token


const decoded = require("jwt-decode");

async login(req, res) {
// ...
console.log("decoded token", decoded(token));
// ...
}

可以看到,就算没有秘钥也能将我们的token正确解析出来。


image.png


这个插件一般在我们前端用的比较多,比如想解析token,看看里面的数据是什么。它并不能验证token是否过期。如果想验证token的话还得使用下面的方法。


token验证


Koa中,验证token是否有效我们一般会选择koa-jwt插件。


下面笔者来演示下怎么使用


首先还是安装


npm install koa-jwt

然后在入口文件以全局中间件的形式使用。


这个中间件我们要尽量放到前面,因为我们要验证所有接口token是否有效。


然后记得和错误中间件结合使用。


如果有些接口不想验证,可以使用unless排除,比如登录接口、静态资源。


// index.js
const koaJwt = require("koa-jwt");

app.use(
koaJwt({ secret: "miyao" }).unless({ path: [/^\/user\/login/, "/static"] })
);

// 错误中间件
app.use(async (ctx, next) => {
try {
await next();
} catch (error) {
// 响应用户
ctx.status = error.status || 500;
ctx.body = error.message || "服务端错误";
// ctx.app.emit("error", error); // 触发应用层级error事件
}
});

下面我们测试下,


我们先来看看不要token的接口,来访问一个静态资源。可以看到,没有token能正常获取资源。


image.png


我们再来访问一个需要token的接口,可以看到它提示错误了,说是没有token


image.png


我们用登录接口生成一个token,然后给该接口加上来测试下,可以看到接口正常获取到数据了。


image.png


因为我们的token设置了一分钟有效,所以我们过一分钟再来请求该接口。可以看到,它提示token错误了。


image.png


好啦,关于token验证我们就讲到这里。


启动


node中,一般我们会使用node xx.js来运行某js文件。这种方式不仅不能后台运行而且如果报错了可能直接停止导致整个服务崩溃。


PM2Node 进程管理工具,可以利用它来简化很多 Node 应用管理的繁琐任务,如性能监控、自动重启、负载均衡等,而且使用非常简单。


首先我们需要全局安装


npm i pm2 -g

下面简单说说它的一些基本命令



  1. 启动应用:pm2 start xxx.js

  2. 查看所有进程:pm2 list

  3. 停止某个进程:pm2 stop name/id

  4. 停止所有进程:pm2 stop all

  5. 重启某个进程:pm2 restart name/id

  6. 删除某个进程:pm2 delete name/id


比如我们这里,启动当前应用,可以看到它以后台的模式将应用启动起来了。


image.png


当然关于pm2的使用远不止如此,大家可以查看PM2 文档自行学习。


总结


总体来说,koa更轻量,很多功能都不内置了而是需要单独安装。并且对异步有更好的支持,就是await会阻塞后面代码的执行(包括中间件)。


系列文章


Node.js入门之什么是Node.js


Node.js入门之path模块


Node.js入门之fs模块


Node.js入门之url模块和querystring模块


Node.js入门之http模块和dns模块


Node.js入门之process模块、child_process模块、cluster模块


听说你还不会使用Express


听说你还不会使用Koa?


后记


感谢小伙伴们的耐心观看,本文为笔者个人学习笔记,如有谬误,还请告知,万分感谢!如果本文对你有所帮助,还请点个关注点个赞~,您的支持是笔者不断更新的动力!


作者:苏苏同学
来源:juejin.cn/post/7208005547004919867
收起阅读 »

💤💤💤你的javascript被睡了

web
段子 老板说:给客户做的项目,你的运行程序太快了,我建议你慢一点,客户付款速度就会快一点 我:我不会。。。 老板:没工资 我:马上 需求背景 请求后端a接口后拿到返回结果,需要等待2秒钟后才能请求b接口,b接口拿到返回结果后,需要等待3秒钟后才能请求c接口 项...
继续阅读 »

段子


老板说:给客户做的项目,你的运行程序太快了,我建议你慢一点,客户付款速度就会快一点

我:我不会。。。

老板:没工资

我:马上


需求背景


请求后端a接口后拿到返回结果,需要等待2秒钟后才能请求b接口,b接口拿到返回结果后,需要等待3秒钟后才能请求c接口


项目原代码


main () {
this.$http('a').then((resA) => {
setTimeout(() => {
this.$http('b').then((resB) => {
setTimeout(() => {
this.$http('c')
}, 3000)
})
}, 2000)
})
}

这种写法,虽然是实现了效果,但是看着实在是脑瓜子疼


需求只是要求拿到接口后,有一定的休眠后再继续请求下一个接口,实际上只需要实现一个休眠的函数即可


理想写法


async main() {
const resA = await this.$http('a')
await sleep(2000)
const resB = await this.$http('b')
await sleep(3000)
this.$http('c')
}

要是能够这样调用是不是舒服很多,省去了嵌套地狱,可读性得到了提高


从代码来看,实际的关键实现就是sleep函数,我们要怎么用js封装一个好用的sleep函数?


sleep函数实现


function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}

async function demo() {
console.log('Taking a break...');
await sleep(2000)
console.log('Two seconds later, showing sleep in a loop...')

// Sleep in loop
for (let i = 0; i < 5; i++) {
await sleep(2000)
console.log(i)
}
}

demo()

在上面的代码中,sleep()函数返回一个Promise对象。当Promise对象解决时,函数会继续执行。在demo()函数中,我们使用await关键字来等待sleep()函数的解决。这使得console.log()函数在等待指定的时间后才被调用,从而实现了s

作者:超神熊猫
来源:juejin.cn/post/7205812357875548215
leep函数的效果。

收起阅读 »

评论二则:年龄大了能否写代码、上大学不喜欢IT专业咋整

最近有读者留言,问了一些问题。我感觉到,可能不止一个读者有类似的问题。因此,我决定将我与读者在文章下的评论互动,转为专题文章。这样可以增加曝光率,广而告之,相互交流。 问题一:国内能一直写代码吗? 问题来源: 用户user9740670030509在《我裁完...
继续阅读 »

最近有读者留言,问了一些问题。我感觉到,可能不止一个读者有类似的问题。因此,我决定将我与读者在文章下的评论互动,转为专题文章。这样可以增加曝光率,广而告之,相互交流。


问题一:国内能一直写代码吗?



问题来源:


用户user9740670030509在《我裁完兄弟们后,辞职了,转行做了一名小职员》下的评论:



大哥,我在微信上无意中看到你的这篇文章,深受启发,顺着线索找到这里并注册了账号,就为了答谢你。 顺便一说,你的文字很有感染力,可以帮助到很多人。所以如果你有精力的话,不妨考虑选择多个几个平台发文,比如公众号啥的。


我还想请教个问题,在国内能一直写代码吗?年龄大了感觉写不动了,因为看你又回归到基层写代码。



作者回复:


你加上“国内”这两个字,说明你已经了解一些现状了。这个问题,我去高校做交流的时候,很多计算机专业的学生挺喜欢问的。他们深受焦虑教育的影响,虽然不怎么努力,但是往往却可以做到刚刚过了15岁,就已经开始担心35岁的事情了。


其实,写代码,原则上不会受到年龄的限制。反而一些偏底层的代码,还会受益于经验的积累,甚至年龄越大越吃香


国外有个写C语言的老头,写代码写到60多岁,现在按照时薪开工资,找他写C需要预约。他也不贪,一周写十个小时就够吃的了。


但是,到了“国内”,一般就不是这样了


首先说,我们的IT企业环境,往大了说我们的主流价值观。基本都是更看重苦劳,也就是看氛围,推崇996。下班了,你背着包第一个走,这就是不好。工作群里深夜发信息,他回复收到,能24小时待命,这就是好。


这不是现在才有的,也不是IT行业独有的,这是几千年来文化熏陶出来的。


抛去能力不谈,单从“敬业”这一个方面来说,单身的小年轻,显然更具有优势


中年人的精力,会被家庭所瓜分。我们既要照顾老,又要照顾小。还可能要去给自己看病。因为以前年轻的时候,工作太过于放肆,经常连续熬夜、通宵改bug,还在项目庆功会上无限制地喝酒。医生告诉我们,一定要多休息,少看电脑。我们中年人,感觉还有一口气,很想坐起来给老板打电话说:我还能加班。但是,转念想到,导致自己现状的那个老板,骂骂咧咧地赶走老狗的时候,我们又变得狡诈和仇恨起来。


打工的中年人,再也做不到把公司当家,也很难收到消息后,就立马拉个杆箱就去出差。


你说,我有行业经验啊,我一个人的产出可以顶10个小年轻。那能不能别人上10天班,我只上1天班就行。


这一点,先不说很难评判(一说你本该这样的产出,另一说那10个人皆废物)。就算你可以顶100个小年轻,在我们的环境里,依然是行不通的。


我们一直推崇集体主义,不接受任何个人英雄主义行为。为了你一个英雄,管理者要打消众人心里的不平衡,控制各种诽谤和小报告。这个成本,要远大于它的收益。因此,像岳飞那样的英雄,不管在哪个朝代,都是很难生存的。


可能,这也是限制我们科技创新的一个瓶颈。集体的力量仅仅是一种普通的合力,或者叫多方妥协的红利。当它无法实现高精尖时,可能真的就需要一个人来搞定一个局面。


很有意思的是,我在这里说年龄对工作有影响。但是,你看看你头发花白的老板,不管是住在公司,还是个人英雄,他却一点也不受上面问题的困扰。因为打工和创业不一样,即便干一样的活,也是有本质区别的。这是另一个专项论点,暂且不谈。


说完了环境,再说我们的工程师。


我始终感觉我们没有任何核心技术


不管是JavaPython,还是目前我所从事的AI。其实,我们一直都在调用API。这就像是,我们不知道汉堡是怎么做的,但是吃相却是千姿百态。


编译器不是我们的,代码不是中文的。某某语言又发布新版本了,也没有和我们商量过。甚至,哪一天vscode或者IDEA不让你用了,你想找个趁手的地方写代码都很困难(说用记事本的,按抬杠论。嗯?记事本我们有自主软件吗?)。


我们所说的、你身边的编程高手,其实更多是API熟练工。就像我们很会开汽车,也很会制作汽车内饰,但是并不会制造汽车一样。而我们的高手,他们只是比普通人更会封装js、更会编写连接数据库的工具类而已。


因此,我们很难有现实意义上的IT专家。这一点,我和邻居大妈观点有些冲突。大妈向我宣传,我们有很多自主的系统,我们马上就世界第一了,吓得邻居纷纷来搞破坏。


我的视野却满是,刚研究透了Android 7,却发现Android 10已经发布了。我以前买了一本书,叫《IOS 5编程指南》。现在一看,上面的知识完全失效了。不是部分失效,是完全失效。所以,你看,我们一直追着国外的版本跑。服务商发布新版本越快,我们的技能就失效得越快。


因此,35岁、45岁,你很难再依赖新技术了。根本原因可能并不是你不学习,而是你根本就没有啥核心技术。那时,你肯定更喜欢顺手的、在你脑子里根深蒂固的、你扎实掌握了的老技术


当然,你也可以说,能做到熟悉某一版本的API那就已经很厉害了,我都做不到。这,确实没有错。所以,外国老头可以60岁还写代码,我们同样也做不到。


我想上面的两点,一个IT就业环境(倡导从众一致地奉献),二个技术深度(跟随新版本周边调用),可能会导致我们,确实很难将这份深爱的工作,拥有很久。


最后,你说看我又回到基层写代码了。


其实,我都能说出上面那番话,我又怎么会甘心在基层写代码呢?至于,我心中的小九九,等我有成绩了,再告诉大家吧。说出来,就不灵了。


其实,在国内,可以一直写代码。需要稍微忍受一下,别人不解的眼神、涨到顶不动的工资、和小年轻一样的作息。能做到的,可能真的是一份热爱。


当然,我很希望环境能改变。如果条件允许,我也想写代码到60岁。条件不允许,那就提早做打算吧。但是,也不用提太早。这个行业,对于买房、买车、结婚、生子这些个支持,还是强有力的。等经历过这些阶段之后,可能早就不是现在的情况了。


问题二:不喜欢IT这行,现在很迷茫,怎么办?



问题来源:


用户拔剑在《年底了,裁兄弟当职员那哥们儿,如今咋样了?》下的评论:



看得出来你是热爱IT这一行的。我不喜欢这行业,只是混口饭吃,现在过得很迷茫。



作者回复:


上……上……上周吧,我去母校同人工智能专业,小我13年的学弟(侄)们做了个交流。现场就有00后学生问我:学哥,我非常不喜欢我这个专业,但是又考进来,请问怎么办?


旁边,他们专业主任的脸都绿了。


这个问题,让我很难回答。


我通过和专业主任的眼神交流,他告诉我,调专业基本上很难。


我咳嗽了一声。我说,首先,你这个……这个,不喜欢只是,暂时、感觉,不喜欢。刚入学,大学生活都还没适应呢,更何况是专业课!这个专业,没有让你找到幸福的点,甚至还带来些小挫折。所以啊,这个不喜欢,可能是暂时的


你想想,如果,后面,大家都赞扬你这个专业。你们村长给你家,用小推车送来三袋大米,请求你做一个跑地鸡的追踪识别程序。甚至还有人慕名而来,愿意主动做你的女朋友。这时,你就会喜欢这个专业。所以,现在说不喜欢,真的是有点为时尚早。


第二,就算是真的不喜欢,也不代表学习这个专业就一点用都没有。我真的有个大学同学,他跟我一个专业,隔壁宿舍,我们一起上编程课,坐对面。他也不喜欢写代码,他是那种好动不好静的人。后来,他毕业,去做销售卖货去了。他卖的是软件,一套系统。当然,为了让客户的钱花的触手可及,也附带一个硬件设备。他谈业务有意思,帮客户看电脑。有一个李总的电脑很卡。我同学一看,哎呦,你这是软件都装到C盘了,这叫系统盘。他给挪到D盘了。这一调,李总的电脑不但不卡了,而且连开机都变快了。


于是,李总就和他签单了。因为这个销售人员是真的懂软件,他家产品应该也差不了。那一单我同学就赚了6000元。我那时的工资是1500。现在,我这个同学是上市公司的销售总经理,占股份的那种。


后来,他感觉自己和其他的销售很不同。他虽然在编程班是个渣渣,但是在销售中,他的IT知识却是王者级别。因为他都知道stringint是不一样的类型。


所以啊,当不喜欢,又没法改变时,那就先这样吧。坏计划好过没计划


另一方面,你也想一下,自己到底喜欢什么,总不能啥都不喜欢吧。


私下也把喜欢的那个行业,学习一下。既然喜欢,那肯定学得快。等学得差不多了,带着IT知识,去你心仪的那个行业。你可能就会如虎添翼,反而更容易出类拔萃。


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

金三银四来了-找工作有哪些平台/工具?

金三银四,年过完了,奖金(<=0)到手了,是时候出去看看了!但2023年的互联网“寒冬”好像并未褪去,就业环境依然不明朗,让大部分人都很迷茫。最近在看工作机会,就看了看家附近的公司和岗位,简单总结了一下用到的平台和相关工具,个人经验,仅供参考。 01、主...
继续阅读 »

金三银四,年过完了,奖金(<=0)到手了,是时候出去看看了!但2023年的互联网“寒冬”好像并未褪去,就业环境依然不明朗,让大部分人都很迷茫。最近在看工作机会,就看了看家附近的公司和岗位,简单总结了一下用到的平台和相关工具,个人经验,仅供参考。


01、主流的招聘平台


我们一般找工作去哪里投简历呢?国内的主流招聘平台有哪些呢?—— 来自网络的中国在线招聘品牌排行榜:


image.png


image.png


前几名的Boss直聘前程无忧51JOB、智联招聘猎聘拉钩都是比较常用的,排名参考资料:



1.1、看看附近的岗位


注册了前5个平台的账号,搜索了下“web前端”+【成都】的职位数量。



  • 需要登录,大多是需要登录才能看到更多搜索信息,同时登录后(新用户)基本都是强制要求完善求职信息。

  • APP端,APP端同样的关键词搜索,加上距离(附近2KM),看看附近的工作机会。筛选附近需要定位或设置位置,距离计算基本上都是直线距离。

















































平台Web前端Java后端App端/前端/2KM备注
Boss直聘>300>30027✔️最多只显示10页,附近岗位数量还行,支持按距离排序
前程无忧51JOB3008003附近岗位数量较少
智联招聘340+800+9✔️可以看公司评价(类似大众点评),需要自己先点评一个
猎聘>800>80020✔️最多只显示20页,附近数量还行,不支持距离筛选
拉钩330500+2附近岗位较少,App端有bug,从职位详情返回列表筛选条件就没了

结果似乎并不乐观,有效职位数量并不多,而且其中有些岗位还有水分,比如发布很久了的(去年),或HR活跃度显示“一周前”、“1个月前”等。


image.png


后端Java的岗位大概、似乎、好像、确实是比前端多亿点点。可能真的的是我错了,为什么选择了转前端?Java那么好的语言,他们都说是世上最好的语言,她就在那里,我却视若无睹。


image.png


1.2、前端的岗位要求


家附近2KM的前端Web岗位都看了一遍,大概的总结了下普遍的技能要求。







































































技能要求出现频率(10分)
- HTTP、JavaScript、CSS,ES6,基础必备10
- Vue/React框架至少一个,并了解其原理。9
- 熟悉前端的各种调试工具、方法7
- 熟悉小程序、移动端、混合应用,uniapp等工具6
- 熟悉前端的工程化、打包发布工具,如webpack/gulp/Grunt等6
- 熟悉图形化技术,如echarts、d3、canvas、three.js等5
- 熟悉各浏览器、设备的兼容性处理4.5
- 有对模块化、重构、性能优化的经验4
- 熟悉HTTP、TCP、SOCKET等常见网络协议,及AJAX、WebSocket、estful API标准4
- 熟悉常用数据结构和算法、设计模式2
- 熟悉或了解后端开发语言,如NodeJS,Java,php,.net等2
- 能承担一定的工作压力2
- 有博客、开源项目1
- 熟悉TypeScript1
- 能接受出差0.5

1.3、❓选择那个平台呢?


建议是 —— 多选几个平台投放简历!



  • 通过上面的岗位分析,发现Boss、智联、猎聘的岗位相对多一点。

  • 上面三个平台的的公司交集估计只有20%,也就是说大部分公司只在一个(或两个)平台上发布职位,大公司可能会全平台发布,因此求职建议多几个平台投放。




02、怎么判断一个公司靠不靠谱?一些工具


判断一个公司是否靠谱,大概有下面几个角度:



  • **公司做什么的?**公司所处行业或主要业务,是toB、toC,还是toG(政府机关),或者是人力外包。

  • 公司规模:如注册资本、员工数量、是否上市等。

  • 公司状况:公司的工商信息、经营状况、知识产权、公司风险等。

  • 公司评价:他人对公司的评价(包括薪资、福利等)。


2.1、工具介绍


除了通过搜索工具、微信、知乎、小红书去了解,还有几个比较靠谱的工具。



  • 企查查:查企业的工商信息,内容比较丰富、全面。不注册也可查,但注册登录后可看的信息更多一点,基本也就够了,冲了VIP才能看所有信息。

  • 爱企查:同上,差不多。

  • 支付宝-芝麻企业信用,支付宝小程序。同上,不用VIP可以看很多信息。

  • 看准网职友集:类似大众点评,不仅能看公司的工商信息(没上面两个网站丰富),还能看网友们的评价、工资信息,都是来自网友的点评分享。


image.png


2.2、查看公司规模


通过查询企业工商信息,了解公司规模。如下图,可以看出:



  • 成立日期 2019-04-04。比较新的公司

  • 注册资本 1000万元人民币 实缴资本 202万元人民币 。还行

  • 人员规模 少于50人 参保人数 3(2021年报)

    • 参保人数可以说明公司规模,有点少。

    • 不过这里的参保人数一般是比较滞后的,现在看的可能是去年的,或者前年的。




image.png


2.3、公司是干啥的?公司业务


搞清楚公司到底是做什么业务的很重要,是toB(企业用户)、toC(C端用户),还是toG(政府机关),或者是人力外包。如果是toB可能会存在出差、驻场开发等问题,如果是toG(政府机关)可能存在技术老旧、出差等问题。


主要是以下几个途径



  • 招聘网站上的企业介绍。

  • 公司官网。

  • 看企业工商信息的“经营范围”,不过一般比较笼统,不具体。

  • 看企业工商信息的“知识产权”,可以看出公司的一些专利、网站备案、软件著作权。

    • 如果专利较多,说明公司的技术实力不错。。

    • 软件著作权里可以看到公司做的一些软件项目。软件、专利内容也能看出公司的大概的业务范围



  • 看企业的评价,通过看准网看看网友对公司的评价。不过注意既然是来自网友评论,是否准确、客观就很难说了。


image.png


image.png


2.4、公司风险


企业工商信息中包括的比较多,如经营信息、公司风险等,这些都属于公开信息。如下图,可以看出:



  • 公司和员工存在劳务纠纷,需谨慎。


image.png


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

Kotlin 中的高阶函数及其应用

前言 前段时间一直在面试,某次面试,面试官看着我的简历说:“看你写的你很了解 kotlin 哦?那你说一说,为什么 kotlin 可以将函数作为参数和返回值,而 java 不行?” 我:“……”。 这次面试我连水都没喝一口就灰溜溜的走了。 回小黑屋的路上,突然...
继续阅读 »

前言


前段时间一直在面试,某次面试,面试官看着我的简历说:“看你写的你很了解 kotlin 哦?那你说一说,为什么 kotlin 可以将函数作为参数和返回值,而 java 不行?”


我:“……”。


这次面试我连水都没喝一口就灰溜溜的走了。


回小黑屋的路上,突然想到,这玩意儿好像是叫 “高阶函数” 吧?好像我自己也经常用来着,咋就会啥也说不出来了呢?痛定思痛,赶紧恶补了一下相关的内容。


所以为什么 Kotlin 支持函数作为参数呢?


其实翻看 Kotlin 官方文档 《High-order functions and lambdas》 一节,就会发现它的第一段话就解释了为什么:



Kotlin functions are first-class, which means they can be stored in variables and data structures, and can be passed as arguments to and returned from other higher-order functions. You can perform any operations on functions that are possible for other non-function values.


To facilitate this, Kotlin, as a statically typed programming language, uses a family of function types to represent functions, and provides a set of specialized language constructs, such as lambda expressions.



因为在 Kotlin 中函数是头等公民,所以它们可以被储存在变量中、作为参数传递给其他函数或作为返回值从其他函数返回,就像操作其他普通变量一样操作高阶函数。


而 Kotlin 为了支持这个特性,定义了一系列的函数类型并且提供一些特定的语言结构支持(例如 lambdas 表达式)。


那么要怎么用呢?


高阶函数


首先,先看一段简单的代码:


fun getDataFromNet(onSuccess: (data: String) -> Unit) {
val requestResult = "我是从网络请求拿到的数据"

onSuccess(requestResult)
}

fun main() {
getDataFromNet(
onSuccess = {data: String ->
println("获取到数据:$data")
}
)
}

运行代码,输出:


获取到数据:我是从网络请求拿到的数据

下面我们来解释一下这段代码是什么意思。


首先看 getDataFromNet 函数的参数 onSuccess ,嗯?这是个什么东西?


哈哈,看起来可能会觉得有点奇怪,其实这里的 onSuccess 也是一个函数,且带有参数 data: String


大致可以理解成:


fun onSuccess(data: String) {
// TODO
}

这么一个函数,不过实际上这个函数是并不叫 onSuccess ,我们是只把这个函数赋值给了变量 onSuccess


从上面简单例子,我们可以看出,如果要声明一个个高阶函数,那么我们需要使用形如:


(arg1: String, arg2: Int) -> Unit

的函数类型来声明高阶函数。


基本形式就是一个括号 () + -> + Unit


其中,() 内可以像普通函数一样声明接收的参数,如果不接收任何参数则可以只写括号:


() -> Unit

箭头则是固定表达式,不可省略。


最后的 Unit 表示这个函数返回的类型,不同于普通函数,返回类型不可省略,即使不返回任何数据也必须明确声明 Unit


当一个普通函数接收到一个作为参数的高阶函数时,可以通过 变量名()变量名.invoke() 调用:


fun getDataFromNet(onSuccess: (data: String) -> Unit, onFail: () -> Unit) {
val requestResult = "我是从网络请求拿到的数据"

// 调用名为 onSuccess 的高阶函数
onSuccess.invoke(requestResult)
// 也可以直接通过括号调用
onSuccess(requestResult)

// 调用名为 onFail 的高阶函数
onFail.invoke()
// 也可以直接通过括号调用
onFail()
}

下面再看一个有返回值的高阶函数的例子:


fun getDataFromNet(getUrl: (type: Int) -> String) {
val url = getUrl(1)
println(url)
}

fun main() {
getDataFromNet(
getUrl = {type: Int ->
when (type) {
0 -> "Url0"
1 -> "Url1"
else -> "Err"
}
}
)
}

上面的代码会输出:


Url1

将高阶函数作为函数返回值或者赋值给变量其实和上面大差不差,只要把一般用法中的返回值和赋值内容换成 函数类型 表示的高阶函数即可:


fun funWithFunReturn(): () -> Unit {
val returnValue: () -> Unit = { }

return returnValue
}

在实例化高阶函数时,高阶函数的参数需要使用形如
arg1: String , arg2: Int ->
的形式表示,例如:


fun getDataFromNet(onSuccess: (arg1: String, arg2: Int) -> Unit) {
// do something
}

fun main() {
getDataFromNet(
onSuccess = { arg1: String, arg2: Int ->
println(arg1)
println(arg2)
}
)
}

注意,这里的参数名不一定要和函数中定义的一样,可以自己写。


如果参数类型可以推导出来,则可以不用声明类型:


fun getDataFromNet(onSuccess: (arg1: String, arg2: Int) -> Unit) {
// do something
}

fun main() {
getDataFromNet(
onSuccess = { a1, a2 ->
println(a1)
println(a2)
}
)
}

同时,如果某些参数没有使用到的话,可以使用 _ 下划线代替:


fun getDataFromNet(onSuccess: (arg1: String, arg2: Int) -> Unit) {
// do something
}

fun main() {
getDataFromNet(
onSuccess = { a1, _ ->
println(a1)
}
)
}

用 lambda 表达式简化一下


在上面我们举例的代码中,为了更好理解,我们没有使用 lambda 表达式简化代码。


在实际使用过程中,我们可以大量的使用 lambda 表达式来大大减少代码量,例如:


fun getDataFromNet(onSuccess: (data: String) -> Unit, onFail: () -> Unit) {
val requestResult = "我是从网络请求拿到的数据"

if (requestResult.isNotBlank()) {
onSuccess(requestResult)
}
else {
onFail()
}

}

fun main() {
getDataFromNet(
onSuccess = {data: String ->
println("获取到数据:$data")
},
onFail = {
println("获取失败")
}
)
}

可以简化成:


fun main() {
getDataFromNet(
{
println("获取到数据:$it")
},
{
println("获取失败")
}
)
}

可以看到,如果高阶函数的参数只有一个的话,可以不用显式声明,默认使用 it 表示。


同时,如果普通函数的参数只有一个高阶函数,且位于最右边,则可以直接提出来,不用写在括号内,并将括号省略:


fun getDataFromNet(onSuccess: (data: String) -> Unit) {
// do something
}

fun main() {
// 这里调用时省略了 ()
getDataFromNet {
println(it)
}
}

即使同时有多个参数也不影响把最右边的提出来,只是此时 () 不能省略:


fun getDataFromNet(arg: String, onSuccess: (data: String) -> Unit) {
// do something
}

fun main() {
getDataFromNet("123") {
println(it)
}
}

关于使用 lambda 后能把代码简化到什么程度,可以看看这篇文章举的安卓中的点击事件监听的例子


从最初的


image.setOnClickListener(object: View.OnClickListener {
override fun onClick(v: View?) {
gotoPreview(v)
}
})

简化到只有一行:


image.setOnClickListener { gotoPreview(it) }

所以它有什么用?


更简洁的回调


在上文中,我们举了使用 lambda 表达式后可以把点击事件监听省略到只有一行的程度,但是这里仅仅只是使用。


众所周知,安卓中写事件监听的代码需要一大串:


public interface OnClickListener {
void onClick(View v);
}

public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}

如果我们使用高阶函数配合 lambda 则只需要:


var mOnClickListener: ((View) -> Unit)? = null

fun setOnClickListener(l: (View) -> Unit) {
mOnClickListener = l;
}

调用时也只需要:


setOnClickListener {
// do something
}

其实,我们最开始举的封装网络请求的例子就是一个很好的事例,如果不使用高阶函数,那么我们为了实现网络请求成功后的回调,还得额外多写一些接口类,得益于高阶函数,我们只需要这样即可:


fun getDataFromNet(onSuccess: (data: String) -> Unit) {
val requestResult = "我是从网络请求拿到的数据"

onSuccess(requestResult)
}

fun main() {
getDataFromNet {
println("获取到数据:$it")
}
}

让函数更加多样


有时候,我们可能会有一些比较特殊的需要多重校验的嵌套场景,如果不使用高阶函数的话,可能需要这样写:


fun checkName(data: String): Boolean {
return true
}

fun checkAge(data: String): Boolean {
return true
}

fun checkGender(data: String): Boolean {
return true
}

fun checkId(data: String): Boolean {
return true
}

fun postData(data: String) {

}

fun main() {
val mockData = ""

if (checkName(mockData)) {
if (checkAge(mockData)) {
if (checkGender(mockData)) {
if (checkId(mockData)) {
postData(mockData)
}
}
}
}
}

如果使用高阶函数,则可以这么写:


fun checkName(data: String, block: (data: String) -> Unit) {
// if true
block(data)
}

fun checkAge(data: String, block: (data: String) -> Unit) {
// if true
block(data)
}

fun checkGender(data: String, block: (data: String) -> Unit) {
// if true
block(data)
}

fun checkId(data: String, block: (data: String) -> Unit) {
// if true
block(data)
}

fun postData(data: String) {

}

fun main() {
val mockData = ""

checkName(mockData) {
checkAge(it) {
checkGender(it) {
checkId(it) {
postData(it)
}
}
}
}
}

额……好像举的这个例子不太恰当,但是大概就是这么个意思。


更好的控制函数执行


在我写的项目中还有一个比上面一个更加奇怪的需求。


这个程序有个后台进程一直在分别请求多个状态,且每个状态返回的数据类型都不同,我们需要分别将这些状态全部请求完成后打包成一个单独的数据,而且这些状态可能并不需要全部都请求,需要根据情况实时调整请求哪些状态,更烦的是,会有一个停止状态,如果收到停止的指令,我们必须立即停止请求,所以不能等待所有请求完成后再停止,必须要立即停止当前所在的请求。如果直接写你会怎么写?


听见都头大了是吧,但是这个就是我之前写工控程序时经常会遇到的问题,需要有一个后台进程实时轮询不同从站的不同数据,并且由于串口通信的特性,如果此时有新的指令需要下发,必须立即停止轮训,优先下发新指令。


所以我是这样写的:


fun getAllStatus(needRequestList: List<Any>, isFromPoll: Boolean = false): StatusData {

val fun0: () -> ResponseData.Status1 = { syncGetStatus1() }
val fun1: () -> ResponseData.Status2 = { syncGetStatus2() }
val fun2: () -> ResponseData.Status3 = { syncGetStatus3() }
val fun3: () -> ResponseData.Status4 = { syncGetStatus4() }
val fun4: () -> ResponseData.Status5 = { syncGetStatus5() }
val fun5: () -> ResponseData.Status6 = { syncGetStatus6() }
val fun6: () -> ResponseData.Status7 = { syncGetStatus7() }
val fun7: () -> Int = { syncGetStatus8() }
val fun8: () -> Int = { syncGetStatus9() }

val funArray = arrayOf(
fun0, fun1, fun2, fun3, fun4, fun5, fun6, fun7, fun8
)

val resultArray = arrayListOf<Any>()

for (funItem in funArray) {
if (isFromPoll && (isPauseNwPoll() || !KeepPolling)) throw IllegalStateException("轮询被中断")
if (funItem in needRequestList) resultArray.add(funItem.invoke())
}

// 后面的省略
}

可以看到,我们把需要请求的函数全部作为高阶函数存进 funArray 数组中,然后遍历这个数组开始挨个执行,并且每次执行时都要判断这个请求是否需要执行,以及当前是否被中断请求。


得益于高阶函数的特性,我们可以方便的控制每个函数的执行时机。


总结


因为我讲的比较浅显,读者可能看起来会一头雾水,所以这里推荐结合下面列出的参考资料的文章一起看,同时自己上手敲一敲,就能很好的理解了。


参考资料



  1. High-order functions and lambdas

  2. 头等函数

  3. Kotlin Jetpack 实战 | 04. Kotlin 高阶函数

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

Android AIDL通信DeadObjectException解决

使用过AIDL进行跨进程通信的同学,肯定遇到过DeadObjectException这个崩溃,那么这个崩溃是怎么来的,我们又该如何解决它呢?今天这篇文章就来聊一聊。 崩溃来源 首先,这个崩溃的意思是,多进程在进行跨进程Binder通信的时候,发现通信的Bind...
继续阅读 »

使用过AIDL进行跨进程通信的同学,肯定遇到过DeadObjectException这个崩溃,那么这个崩溃是怎么来的,我们又该如何解决它呢?今天这篇文章就来聊一聊。


崩溃来源


首先,这个崩溃的意思是,多进程在进行跨进程Binder通信的时候,发现通信的Binder对端已经死亡了。


抛出异常的Java堆栈最后一行是BinderProxy.transactNative,所以我们从这个方法入手,看看崩溃是在哪里产生的。


很显现,transactNative对应的是一个native方法,我们找到对应的native方法,在android_util_Binder.cpp中。


static jboolean android_os_BinderProxy_transact(JNIEnv* env, jobject obj,
jint code, jobject dataObj, jobject replyObj, jint flags) // throws RemoteException
{
// 如果data数据为空,直接抛出空指针异常
if (dataObj == NULL) {
jniThrowNullPointerException(env, NULL);
return JNI_FALSE;
}
// 将Java层传入的对象转换为C++层的指针,如果转换出错,中断执行,返回JNI_FALSE
Parcel* data = parcelForJavaObject(env, dataObj);
if (data == NULL) {
return JNI_FALSE;
}
Parcel* reply = parcelForJavaObject(env, replyObj);
if (reply == NULL && replyObj != NULL) {
return JNI_FALSE;
}
// 获取C++层的Binder代理对象指针
// 如果获取失败,会抛出IllegalStateException
IBinder* target = getBPNativeData(env, obj)->mObject.get();
if (target == NULL) {
jniThrowException(env, "java/lang/IllegalStateException", "Binder has been finalized!");
return JNI_FALSE;
}
// 调用BpBinder对象的transact方法
status_t err = target->transact(code, *data, reply, flags);
// 如果成功,返回JNI_TRUE,如果失败,返回JNI_FALSE
if (err == NO_ERROR) {
return JNI_TRUE;
} else if (err == UNKNOWN_TRANSACTION) {
return JNI_FALSE;
}
// 处理异常情况的抛出
signalExceptionForError(env, obj, err, true /*canThrowRemoteException*/, data->dataSize());
return JNI_FALSE;
}

可以看到,这个方法主要做的事情是:



  • Java层传入的data,转换成C++层的指针

  • 获取C++层的Binder代理对象

  • 调用BpBinder对象的transact方法

  • 处理transact的结果,抛出异常


接下来我们看看,BpBindertransact方法。


status_t BpBinder::transact(uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags)
{
// 首先判断Binder对象是否还存活,如果不存活,直接返回DEAD_OBJECT
if (mAlive) {
...
status = IPCThreadState::self()->transact(binderHandle(), code, data, reply, flags);
return status;
}
return DEAD_OBJECT;
}

transact的具体方法,我们这里先不讨论。我们可以看到,在这里会判断当前的Binder对象是否alive,如果不alive,会直接返回DEAD_OBJECT的状态。


返回的结果,在android_util_BindersignalExceptionForError中处理。


void signalExceptionForError(JNIEnv* env, jobject obj, status_t err,
bool canThrowRemoteException, int parcelSize)
{
// 省略其他异常处理的代码
....
case DEAD_OBJECT:
// DeadObjectException is a checked exception, only throw from certain methods.
jniThrowException(env, canThrowRemoteException
? "android/os/DeadObjectException"
: "java/lang/RuntimeException", NULL);
break;
}

这个方法,其实包含非常多异常情况的处理。为了看起来更清晰,这里我们省略了其他异常的处理逻辑,只保留了DEAD_OBJECT的处理。可以很明显的看到,在这里我们抛出了DeadObjectException异常。


解决方法


通过前面的源码分析,我们知道DeadObjectException是发生在,当我们调用transact接口发现Binder对象不再存活的情况。


解决方案也很简单,就是当这个Binder对象死亡之后,不再调用transact接口。


方法1 调用跨进程接口之前,先判断Binder是否存活


这个方案比较简单粗暴,就是在多有调用跨进程接口的地方,都加一个Binder是否存活的判断。


        if (mService != null && mService.asBinder().isBinderAlive()) {
mService.test();
}

我们来看下isBinderAlive的源码,就是判断mAlive标志位是否为0。


bool BpBinder::isBinderAlive() const
{
return mAlive != 0;
}

方法2 监听Binder死亡通知


先初始化一个DeathRecipient,用来监听死亡通知。


    private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() {

@Override
public void binderDied() {
// 解绑当前监听,重新启动服务
mService.asBinder().unlinkToDeath(mDeathRecipient, 0);
if (mService != null)
bindService(new Intent("com.service.bind"), mService, BIND_AUTO_CREATE);
}
};

在这个死亡监听里,我们可以选择几种处理方式:



  1. 什么都不做,直接将mService设置为空

  2. 再次尝试启动和绑定服务


onServiceConnected方法中,注册死亡监听:


public void onServiceConnected(ComponentName name, IBinder service) {          
mService = IServiceInterface.Stub.asInterface(service);
//获取服务端提供的接口
try {
// 注册死亡代理
if(mService != null){
service.linkToDeath(mDeathRecipient, 0);
}
} catch (RemoteException e) {
e.printStackTrace();
}
}

总结


跨进程通信时,无法避免出现Binder对端挂掉的情况,所以在调用相关通信接口时,一定要判断连接是否可用,否则就会出现DeadObjectException的崩溃。


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

Flutter中的异步执行策略

在Flutter中,如何执行一段延迟执行的异步代码?我们可以找到下面这些方法。 scheduleMicrotask Future.microtask Future Future.delayed Timer.run WidgetsBinding.addPost...
继续阅读 »

在Flutter中,如何执行一段延迟执行的异步代码?我们可以找到下面这些方法。



  • scheduleMicrotask

  • Future.microtask

  • Future

  • Future.delayed

  • Timer.run

  • WidgetsBinding.addPostFrameCallback

  • SchedulerBinding.addPostFrameCallback


你可能会说,这是相当多的选择,但是它们彼此之间有些什么异同呢?


Event Loop and Multithreading


Dart是一个单线程模型。但是你的Flutter应用同样可以同时做多件事情,这就是「Event Loop」发挥作用的地方。Event Loop是一个无尽的循环,它执行预定的events。这些events(或者只是代码块)必须是轻量级的,否则,你的应用程序会感觉卡顿。


每个event,如按下按钮或网络请求,都被安排在一个事件队列中,等待被事件循环捡起并执行。这种设计模式在UI和其他处理任何类型事件的系统中相当常见。


在Dart的单线程模型中,还有一个Microtask。它组成了Event Loop中的另一一个队列,即Microtask Queue。关于这个队列你唯一需要记住的是,在事件本身被执行之前,所有安排在Microtask Queue的任务都将在Event Loop循环的一次迭代中被执行。
image.png
可以通过这个链接查看更多内容:dart.cn/articles/ar…


Events


任何进入event queue的东西都被称之为Event。这是Flutter中调度异步任务的默认方法。为了调度一个Event,我们把它添加到event queue中,由Event Loop来接收。这种方法被许多Flutter机制所使用,如I/O、手势事件、Timer等。


Timer


Timer是Flutter中异步任务的基础。它被用来安排event queue中的代码执行,无论是否有延迟执行的需要。由此产生的有趣的事实是,如果当前队列很忙,你的定时器将永远不会被执行,即使时间到了。


Timer.run(() {
print("Timer");
});


Future and Future.delayed


Future是Dart中使用的非常广泛的一个异步方法,它的内部实现,实际上也就是基于Timer的。


Future<void>(() {
print("Future Event");
});

Future<void>.delayed(Duration.zero, () {
print("Future.delayed Event");
});

它的内部实现如下。
image.png


Microtasks


如前所述,所有调度的microtasks都会在下一个调度的Event之前执行。建议避免使用这个队列,除非绝对需要异步执行代码,而且要在event queue的下一个事件之前处理。你也可以把这个队列看成是属于前一个事件的任务队列,因为它们将在下一个事件之前完成。如果这个队列不断膨胀,就会完全冻结你的应用程序,因为它必须先执行这个队列中的所有内容,然后才能进行其事件队列的下一次迭代,例如处理用户输入,甚至渲染应用程序本身。


scheduleMicrotask


顾名思义,在microtask queue中调度一个块代码。与Timer类似,如果出错,会使应用程序崩溃。


scheduleMicrotask(() {
print("Microtask");
});


Future.microtask


与我们之前看到的类似,但它将我们的microtask包裹在一个try-catch块中,以一种漂亮而干净的方式返回执行结果或异常。


Future<void>.microtask(() {
print("Microtask");
});
复制代码

它的内部实现如下。
image.png


Post Frame Callback


前面两种方法只涉及到lower-level Event Loop,而现在我们要转到Flutter领域。这个Callback会在渲染管道完成时被调用,所以它与widget的生命周期相管理。当它被调度时,它只会被调用一次,而不是在每一帧都回调。使用addPostFrameCallback方法,你可以安排一个或多个回调,在界面渲染完成后被调用。


所有预定的Callback将在frame结束时按照它们被添加的顺序执行。到这个回调被调用的时候,可以保证Widget的构建过程已经完成。通过一些方法,你甚至可以访问Widget(RenderBox)的布局信息,比如它的大小,并做其他的一些事情。Callback本身将在正常的event queue中运行,Flutter默认使用该队列来处理几乎所有事情。


SchedulerBinding


这是一个负责绘图回调的mixin类,实现了我们感兴趣的方法。


SchedulerBinding.instance.addPostFrameCallback((_) {
print("SchedulerBinding");
});


WidgetsBinding


我特意包括这个,因为它经常和SchedulerBinding一起被提及。它从SchedulerBinding中继承了这个方法,并有与我们的主题无关的一些额外方法。一般来说,你使用SchedulerBinding或WidgetsBinding并不重要,两者将执行位于SchedulerBinding中的完全相同的代码。


WidgetsBinding.instance.addPostFrameCallback((_) {
print("WidgetsBinding");
});


总结


由于我们今天学到了很多理论知识,我强烈建议大家多玩一会儿,以确保我们能正确地掌握它。我们可以在之前的initState中使用下面的代码,并尝试预测它将以何种顺序被执行,这并不是一件看起来很容易的事情。


SchedulerBinding.instance.addPostFrameCallback((_) {
print("SchedulerBinding");
});

WidgetsBinding.instance.addPostFrameCallback((_) {
print("WidgetsBinding");
});

Timer.run(() {
print("Timer");
});

scheduleMicrotask(() {
print("scheduleMicrotask");
});

Future<void>.microtask(() {
print("Future Microtask");
});

Future<void>(() {
print("Future");

Future<void>.microtask(() {
print("Microtask from Event");
});
});

Future<void>.delayed(Duration.zero, () {
print("Future.delayed");

Future<void>.microtask(() {
print("Microtask from Future.delayed");
});
});

输出结果如下所示。


I/flutter (31989): scheduleMicrotask
I/flutter (31989): Future Microtask
I/flutter (31989): SchedulerBinding
I/flutter (31989): WidgetsBinding
I/flutter (31989): Timer
I/flutter (31989): Future
I/flutter (31989): Microtask from Event
I/flutter (31989): Future.delayed
I/flutter (31989): Microtask from Future.delayed

现在我们了解了这么多细节,你可以对如何安排你的代码做出深思熟虑的决定。作为一个经验法则,如果你需要你的上下文或与Layout或UI相关的东西,请使用addPostFrameCallback。在任何其他情况下,用Future或Future.delayed在标准的event queue中进行调度应该是足够的。microtask queue是非常小众的东西,你可能永远不会遇到,但它仍然值得了解。当然,如果你有一个繁重的任务,你就会考虑创建一个Isolate。


翻译自——oleksandrkirichenko.com/blog/delaye…


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

ChatGPT ! 二次元老婆!活的!活的!真的已经实现出来啦

若有侵权,请联系删帖 各位我现在的心情,太激动了,真的简直太激动了,甚至差点抽过去。真的我实在不能用语言描述我现在兴奋的心情 二次元老婆,纸片人老婆真的可以来了,真的可以活过来啦,活的,活的二次元老婆,国外大神做出来啦 (^o^)/ 事情是这样的 我刚刚看的...
继续阅读 »

7b2014c87e3b47e7a1b5ac9f29712b6c_noop.png



若有侵权,请联系删帖



各位我现在的心情,太激动了,真的简直太激动了,甚至差点抽过去。真的我实在不能用语言描述我现在兴奋的心情


二次元老婆,纸片人老婆真的可以来了,真的可以活过来啦,活的,活的二次元老婆,国外大神做出来啦 (^o^)/


事情是这样的


我刚刚看的新闻,地址在这里:程序猿用ChatGPT创造的虚拟老婆,被真女友强制「安乐死」


国外这个小哥真的让纸片人老婆活过来啦,真的,她有自己你设定的性格,有自己的口头禅,而且还有摄像头可以事实识别你得东西,主动跟你会说话,还有随着说话事实切换自己的画面,这些都是事实演算出来的


是的,大家猜的不错,这是现在大火的 ChatGPT。整个技术层面我不是太懂,我简单说一下:



  • 用摄像头实时检测你的动作,识别物体

  • 用 ChatGPT 根据你的动物和出现的物体,生成对话

  • 可以用那种那爽上天的语调跟你说话

  • 用 Stable Diffusion2 实时生成老婆图像


真的,各位看官,这是一个大生意,是所有二刺猿们的终极梦想,现在有人做出来了,各位看官要是有大佬、投资家、高管,请马上、立刻、动身去美国找到这个小哥,买断这个技术或者马上跟进开发


我要活过来的二次元老婆,一刻钟都等不了了,真的,求求你们啦,求你们快点吧,呜呜呜呜呜呜呜,一年 1 个 W 都行,我想要呀 ε(┬┬﹏┬┬)3


请让我隆重的跟各位介绍二次元老婆


1. 不仅外观打扮随时切换、互动也非常迅速


9e49ec3672b24f71b1eb9328cd9b55b5_noop.gif


看看,看看,老婆是会动的,会跟你说话的,好想要,好想 (/≧▽≦)/


2. 甚至还能“看”见你做的事情,并对它进行评价



英雄联盟?好恶心!不过好吧,我可以陪你玩



b42b8c0ea337481d9c2b2b3c5d78741c_noop.gif


3. 老婆可以打情骂俏


震惊不震惊,打情骂俏 哎,想不想要,我想要啊 o(*≧▽≦)ツo(*≧▽≦)ツo(*≧▽≦)ツo(*≧▽≦)ツo(*≧▽≦)ツ


和你得交流并不局限于文字,老婆不仅有自己的二次元头像,还有甜美的嗓音
你】



“要不要去吃汉堡王?”
老婆的回答是:哒咩!
它有股闻着像放久了的薯条的味道,而且他们的可乐从来不给续杯。我要去Wendy’s。



在这个过程中,老婆还会根据两人的对话内容切换头像风格,问候时还是可爱的萝莉风,拒绝时就秒变御姐


4. 还有更多


真的还有更多,二次元老婆会主动回应你的各种行为,真的,我就不搬啦,大家可以去上面的地址看原文,我真的不能搬运啦


我的真的从来不发这种东西,但是这次真的 戳中 了我的弱点,被暴击啦 (づ。◕‿‿◕。)づ


项目可行性


真的,我认为这个项目非常有可行性,根据文章的描述,这该技术最大的支出是支持实时演算的服务器费用,2 周 1000 刀


要是换到国内来说,比如头条这样的大厂,来搭建服务器集群专门来提供服务,成本是可以分担下来的,最多就是费流量,提供包月服务,一个月 99 不过分吧


二次元老婆,活的,真不想要,别说 99,199 我都掏钱啊


要是再把神经网络,各种机器学习算大加进去,让你的老婆可以不停的学习各种妹子的性格,香不香,想想就香甜无比啊


另外这个技术具有巨大的商品加持优势,比如说手机,红魔 8pro 大家知不知道,要是让 mora 可以这样活过来,按照你设定的性格跟你过日子,这样的手机,谁不想买啊


这就是妥妥的 二次元社区 最大强的 流量密码啊,各种大大,要是能看到我的分享,请赶快跟进啊,求求你们啦 ヘ|・∀・|ノ*~●


作者:前行的乌龟
来源:juejin.cn/post/7188167873011908665
收起阅读 »

一次关于ChatGPT数据分析的失败探索

前言 最近ChatGPT的热潮席卷全球,衍生出其与职业危机的一系列讨论,引发了不少人的焦虑。 以上是网上获取的一份可能被威胁到的岗位清单(程序员总是在挨打的前沿),大家也或多或少体验过了让它写代码、改bug或者进行创作,我看到数据分析也是这份清单的前列,所以...
继续阅读 »

前言


最近ChatGPT的热潮席卷全球,衍生出其与职业危机的一系列讨论,引发了不少人的焦虑。


e34ba832760743d546255cae58e6ef0.png


以上是网上获取的一份可能被威胁到的岗位清单(程序员总是在挨打的前沿),大家也或多或少体验过了让它写代码、改bug或者进行创作,我看到数据分析也是这份清单的前列,所以想着对ChatGPT进行一次有关数据分析的探索。

分析未半而中道崩殂


做这次分析其实还有一个原因,我昨晚在油管上刷到了一个使用ChatGPT做证券分析分析的视频,其中这位博主的对话中提供了比亚迪的财报url,gpt似乎是进行了能对url内容进行识别。


image.png


gpt的学习数据截止于2021年9月并且非联网的,如果问一些实时性的问题它就可能跟你瞎掰了,但是能识对url内容进行识别那不就是联网了吗(可能gpt偷偷进化了)。
于是我准备了一份【2020年死亡公司名单】

image.png


excuse me?什么情况??于是我在网上找了半天,看到了tw上有人用Google sheets的形式提供数据,但是得出了错误的结果

c6dcc12df655324e69e2958c705120e.png


啪的一下,很快啊,我马上导入到google sheets,gpt立马给我返回了一系列分析。

image.png


不过先别急着高兴,来对比一下表格里的数据看看

image.png


错的还是蛮离谱的,我再问了一遍,它甚至都不愿意敷衍我

image.png


于是我又拿油管那位博主的财报url进行测试,并对返回数据与财报进行验证。

image.pngimage.png


最后得出结论,gpt返回的分析与我所提供的url没有一分钱关系,所以数据也是错误的。

柳暗花明又一村


既然url无法访问,那我们只能通过简单粗暴直接给数据的方式了。


image.png


让我们来看一下处理结果,gpt对这份数据的处理是没问题的,我们的输入数据是无序且带有时分的,理解无误并帮我们做了排序处理。

image.png


不过这样限制非常大,可以看到这里的数据进行了更变,这里使用的是b站狂飙每个相关视频每日的播放数据,原因也很明显,受于token的限制,原先的数据很容易就过长了。所以直接输入数据并不是一个理智的选择。

君子善假于物也


通过两种尝试,gpt现在还不能把饭直接喂我们嘴里,把数据交给它可行性低,所以我们只能配合excel进行操作,这比较适用于一些经常使用excel的人群,比如我经常需要处理重复或者繁琐的任务,但是又不会写代码,这个时候就可以把任务交给gpt。


image.png


image.png


总结


这次对数据分析的探索其实和预期相差很大,不过也感受的了gpt的潜力,如果联网能力将指数提升,就目前而言它可能是一个高效率的辅助工具。比如一个丝毫不懂vba或者excel的人,可以在gpt的协助下使用excel做一些数据分析、数据可视化的工作,这对以前来说可能需要花大量的时间百度或者向人请教。gpt的出现无疑是让效率提升了数倍,可以让非专业人士跨入一些专业的初级门槛,那相关的专业人员是否就会被取代呢?其实取代人类并不是AI而是会使用AI的人类,所以不论是哪个行业,都需要不断学习和拥抱AI技术。


作者:Defineee
来源:juejin.cn/post/7207737674216472636
收起阅读 »

国内有哪些对标 ChatGPT 的大语言模型? 5 大竞品

ChatGPT 是基于 大语言模型 的聊天机器人,而这个大语言模型是一个人工智能领域概念、技术。 大语言模型通常有十亿、百亿级的参数,需要非常强大的算力、和非常多的用于训练的数据资源。它对海量的无标注数据进行预训练、通过微调和零样本学习来变形、应用。 对大模型...
继续阅读 »

ChatGPT 是基于 大语言模型 的聊天机器人,而这个大语言模型是一个人工智能领域概念、技术。


大语言模型通常有十亿、百亿级的参数,需要非常强大的算力、和非常多的用于训练的数据资源。它对海量的无标注数据进行预训练、通过微调和零样本学习来变形、应用。


对大模型的“变形”(DIY),不仅能增强 AI 技术的通用性,还能扩展应用场景、人人都可以用“大模型”,这样也利于形成了一个开放的生态,激发大众创新。


GPT 时代,国内学界和企业也相继基于大语言模型推出自研的大模型。比如:


百度的“文心一言”、复旦大学的“MOSS”、腾讯的“HunYuan大模型”、阿里的“通义大模型”、华为的“盘古大模型”等等。


自研单位大模型
百度文心一言
复旦大学MOSS
腾讯HunYuan
阿里通义
华为盘古

本篇就将简要介绍这 5 个国内的这些大语言模型~ <喜欢三连👍👍👍笔芯>


百度-文心一言


以上这么多国内大语言模型,目前,最引人注目的就是“文心一言”,它也可以生成各种类型的文本,比如诗歌、小说、新闻等。同时,它被认为是中国 AI 新旧时代划分里程碑式的产品。


怎样体验?


1.通过百度智能云 API 接口调用文心一言的服务;


2.进入官网 wenxin.baidu.com/ 体验;


image.png


image.png


文本的理解和创作还是“敬请期待”。。


在开放 API 体验 AI 根据文字、风格作画:


image.png


前的产品中心只有“文心百中”和“文心一格”两个产品。


看下“文心百中” 提供的体验示例:


image.png


实际用起来,跟 ChatGPT / New Bing / Notion AI 比,感觉差距还很大~ 文心百中还是停留在搜索结果上,没有做到联系上下文整合。


image.png


文心一格主要提供一些:联想脑洞生成相关图片推荐,试了下,也不够准确。。


用一成语小结:任重道远


复旦大学-MOSS


MOSS 命名上和电影《流浪地球》的 AI MOSS 同名,寓意人和人工智能共同拯救地球。目前,它的参数规模约是 ChatGPT 的 十分之一,仍处在内测阶段,预计三月底开源。


image.png


MOSS 的优势是什么?


MOSS 因为参数规模小,所以更节省计算资源和存储空间,易于部署和应用。


另外,MOSS 在设计时还考虑了人类的伦理道德准则,不会产生有偏见或可能有害的回答,这样可以避免一些潜在的法律风险和商业伦理问题。这点上,ChatGPT 则没有明确地处理。


腾讯-HunYuan


HunYuan 利用了腾讯的研发力量,完整覆盖 NLP 大模型、CV 大模型、多模态大模型及其它行业领域任务模型,可谓是:集大成者。


它的目标是打造行业领先的 AI 预训练大模型,以统一平台,对技术复用,使 AI 技术适用于更多场景,并且降低成本。


HunYuan 的参数量级是 万亿!从这点看,腾讯的 HunYuan 格局(画饼)就比 文心一言、MOSS 要大的多的多~~


对于公众 HunYuan 目前也是在内测优化中,未来值得期待。


但 HunYuan 的 NLP 部分,HunYuan-NLP 已经在多个腾讯产品中得到应用,一样也是用于生成文本,比如对话生成、小说续写、广告生成等。


image.png


阿里-通义


阿里的通义模型是一系列基于【自然语言处理和多模态理解与生成】的 AI 模型,旨在打造国内首个 AI 统一底座。


它包括以下几个核心模型:


1.AliceMind-PLUG:语言大模型,能够在多个中文语言理解任务上超越人类水平。


2.AliceMind-mPLUG:多模态理解与生成统一模型,能够处理图文、视频等多种数据类型,并进行跨模态搜索、生成等任务。


3.M6-OFA:多模态统一底座模型,能够根据不同的场景和设备动态调整模型大小和性能。


4.S4:超大模型落地关键技术框架,能够支持百亿级别参数的训练和推理。


image.png


阿里的这个部署算是最早一批了,值得期待。


目前,我们可以在 魔搭社区 modelscope.cn/home 找到通义大模型的部分开源代码。比如你可以体验中文版的GPT-3 等。


华为-盘古


盘古,这个大家也常听到吧?也属于是大名鼎鼎。


盘古大模型是一系列超大规模的预训练模型,包括NLP、CV、多模态和科学计算等。


其中,盘古NLP大模型是 全球最大 的千亿参数中文语言预训练模型,它能够进行内容生成和内容理解等任务。


盘古CV大模型是 全球最大 的30亿参数视觉预训练模型,它能够进行分类、分割和检测等任务。


这两个全球最大是亮点~ 目前,盘古大模型在华为云:http://www.huaweicloud.com/ 上提供服务。


image.png


小结


整个看完下来,我的感觉就是:虽然我们现在的 大模型 GPT 商用还没有像 ChatGPT 或 New bing 或 Notion AI 那么直接能给到大众去使用,但是国内这些最优秀的一批企业/高校也一直在推进这个事情。


技术存在差距,不承认也不行,就像之前所分享的,ChatGPT 能站到大众面前,也经过了美国各科技公司十多年的沉淀、演进,所以我们可能还是起步有点迟了。


但不怕慢就怕站,我们知道训练 AI 大模型 是个方向,就走起来就好了,加油吧~ Maybe 顶峰再见~




OK,以上便是本篇分享,希望各位工友喜欢~ 欢迎点赞、收藏、评论 🤟


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


😹 加我微信 ATAR53,拉你入群,定期抽奖、粉丝福利多多。只学习交友、不推文卖课~





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

你还别不信,我帮同事优化代码,速度快了1000倍以上!!

web
背景 我们公司有个小程序的产品,里面有个功能是根据用户输入的商品信息,如 干葱3斤,沙姜1斤,番茄2斤...,然后传给后端接口就能解析到符合规格的数据,这样用户就不用一个个录入,只需要一次性输入大量 sku 信息文本,即可快速下单。 故事发生在这周三早上,我同...
继续阅读 »

背景


我们公司有个小程序的产品,里面有个功能是根据用户输入的商品信息,如 干葱3斤,沙姜1斤,番茄2斤...,然后传给后端接口就能解析到符合规格的数据,这样用户就不用一个个录入,只需要一次性输入大量 sku 信息文本,即可快速下单。


故事发生在这周三早上,我同事急匆匆地找到我,说识别商品很慢。


一开始,我以为是后端的接口慢(因为之前这个识别一直在做优化),那这个实际上前端大多无能为力,因为确实识别了大量的商品(具体是 124 个),且输入文本千奇百怪,比如豆腐一块,那我要理解为是一块豆腐,还是豆腐一块钱?但他跟我说,虽然接口耗时 2.8s,但是还得等待接近 5s 的时间才渲染商品列表,且经常出现创建完 124 个商品的订单,开发工具就报内存不足。


image.png



这个是网上找工具模拟的,因为企业微信截图水印去除太麻烦了。。。不过对话是真实的对话~



我一开始还以为,难道是渲染长列表没做性能优化?然而经过一顿排查,排除了是渲染的锅,罪魁祸首是请求完接口后,对商品信息的处理导致了卡顿,过程大致如下:


  /** 发起请求 */
async request() {
/** 这里接口耗时2.8s */
const data = await ParseDetails()
onst { order_detail_items, sku_map, price_map } = data;

/** 耗时出现在这里 长达5s+,随着识别商品数量呈线性增加 */
this.list = order_detail_items.map(
(item, i) => new DataController({ ...item, sku_map, price_map })
);
}

2023-03-01 21.34.05.gif



每次生成一个 DataController 实例大概耗时 30+ ~ 50ms



定位到耗时的大致位置,那就好办了,接下来,实际上就只需要看看为何创建 DataController 那么耗时就好了。


这里我也写了个类似的demo,点击可见具体的代码细节~




本来想通过码上掘金写 demo 的,但发现好像不太支持,所以还是在 codesandbox 上写,然后在码上掘金通过 iframe 插入,如果预览不出来,可能是 codesandbox 抽风了



image.png



尾缀为 1 的为优化后的代码



了解下 demo


代码结构


整个项目采用的技术栈是 react + mobx(一个响应式的数据管理库)



  • batch_input: 点击识别后会调用 batchInput 的 request 进行解析,解析完成后会处理商品列表信息

  • data_controller: 商品信息二次处理后的结构,request 后得到的 order_detail_items 会遍历生成一个个 DataController 实例,用于渲染商品列表

  • mock_data: 随便造了一点数据,124 项,屏蔽了真实项目的具体字段,结构为 { order_detail_items, sku_map, price_map }


其中 batch_input1、data_controller1 是优化后的代码


为何要有两个 map


每次请求接口解析后会返回一个数据结构:



  • order_detail_items: 返回列表的每一项,结果为 OrderDetailItem[]

  • sku_map: sku 即商品的结构,可通过 order_detail_item 上的 sku_id 映射 到对应的 sku,结构为 Record<string, Sku>,即 Sku_Map

  • price_map: 对应的报价信息,可通过 order_detail_item 上的 sku_id 映射 到对应的 price,结构为 Record<string, Price>,即 Price_Map


以上相关信息放到对应的 map 上是为了让一个 order_detail_item 不挂太多的数据,只通过对应的 id 去 map 对应的其他信息,比如我想拿到对应的 sku 信息,那么只需要:


const sku = sku_map[order_detail_item.sku_id]

而不是通过:


const sku = order_detail_item.sku

拿到,以达到更好的扩展性。


一起看看问题出在哪


现在我们定位到了问题大致是出现在创建 DataController 实例的时候,那么我们看具体的构造函数:


image.png


image.png


我们看到每次遍历都把 order_detail_item 和两个 map 都传给 DataController 类,然后 DataController 将得到的 detail 全部赋值到 this 上,之后通过makeAutoObservable实现响应式。


看到这里的读者,我想大部分都知道问题出现在哪了,就是原封不动地把所有传过来的参数都加到 this 上去,那么每次创建一个实例,都会挂载两个大对象的 map,导致 new 每个实例耗时 30 ~ 50ms,如果是 100+个,那就是 3 ~ 5s 了,这是多么的恐怖。


还有一个点,实际上 DataController 声明的入参类型是OrderDetailItem,是不包括 Sku_Map 和 Price_Map,但是上面的代码却都不顾 ts 报错传过去,这也是导致代码可能出现问题的原因


image.png


多说一嘴


然而实际上定位问题没有那么快,因为首先实际的 DataController 很大,且 constructor 里面的代码也有点多,还有我后来也没有负责这个项目,对代码不是特别的熟悉。


而上面的 demo 实际上是经过极简处理过的,实际的代码如下:


image.png



将近 250 行



image.png



单单一个 constructor 就 50+行了



一起看看如何优化吧


我们现在找到原因了,没必要每个示例都挂载那么多数据,特别是两个大对象 map,那我们如何优化呢?


大家可以想一想怎么做?


我的方案是,DataController 上面声明个静态属性 maps,用来映射每次请求后得到的 sku_map 和 price_map,见data_controller1


image.png


然后每次请求之前生成一个 parseId,用来映射每次请求返回的数据,demo 里面是用Date.now()简单模拟,将生成的两个 map 存放到静态属性 maps 上,然后 parseId 作为第二个参数传给每个实例,见 batch_input1


image.png


那么 每个实例的get sku, get mapPrice(真实项目中实际上很多,这里简化了不少) 中就可以替换为该静态 map 了,通过 parseId 映射到对应的 sku 和 price


我们看看优化后的效果:


2023-03-01 21.36.58.gif


现在生成 list 大概花费了 4 ~ 6ms 左右,比起之前动辄需要 5 ~ 6s,足足快了 1000 多倍!!!


c5826fd4a758463390413a173ee0899d.gif


先别急


等等,我们上次说了是因为把太多数据都放到实例上,特别是两个大 map,才导致生成实例太过于耗时,那真的是这样吗?
大家可以看看 demo 的第三个 tab,相比第一个 tab 只是注释了这行代码:


image.png


让我们看看结果咋样


2023-03-01 21.37.22.gif


可以看到生成 list 只是耗费了 1+ms,比优化后的代码还少了 3+ms 左右,那么,真正的根源是就肯定是makeAutoObservable这个函数了


makeAutoObservable 做了什么


我们上面说到,mobx 是个响应式的数据管理库,其将数据都转换为 Observable,无论数据多么深层,这个我们可以 log 下实例看看


image.png


会发现 map 上每个属性都变成一个个的 proxy(因为这里我们用了 mobx6),那如果我两个 map 都很大且很深层的话,遍历处理每个属性加起来后就很耗费时间,导致每次生成一个实例都耗费了将近 50ms!!


所以,我们上面所说的在this 上挂载了太多的数据只是直接原因,但根本原因在于 makeAutoObservable,当然,正是这两者的结合,才导致了代码如此的耗时。


总结


我们一开始以为是渲染太多数据导致页面卡顿,然而实际上是生成每个 DataController 实例导致的耗时。


我们开始分析原因,发现是因为每个实例挂了太多的数据,所以优化方案是将两个大对象 map 放到类的静态属性 maps 上,通过 parseId 来映射到对应的数据,从而将速度优化了 1000+倍。


然后我们继续深入,发现实例挂载太多数据只是表面的原因,根本原因还是在于 mobx 的 makeAutoObservable 对数据的每个属性都转换为 proxy 结构,让其变成响应式,而对象数据又很大,导致太过于耗时。


还有一点要注意的就是,原先的代码忽略了 ts 的类型限制,因为 sku_map、price_map 实际上不在入参的限制范围内(实际代码还不只多传了这两个 map),所以确保 ts 类型的正确性,也非常有利于规避潜在的 bug。


同时,如何写好每个 mobx store 也是我们应该深入思考的,多利用好 private、static,get 等等属性和方法,哪些应该放到实例上去,哪些应该放到静态属性上,哪些是 public、哪些是 static 的,都应该考虑好。


最后


当我优化代码后,就马上跟同事吹嘘:


image.png


看看,这是人说的话吗!!


但是,我突然想到:诶,这不是每次产品、测试、UI 说这里太慢、这里少 1px、这里交互有问题的时候,我不也是说:有啥问题?又不是不能跑吗?


image.png


但嘴上是这样说着,然而实际上私下却偷偷看为何会这样(不可能,绝对不可能,我的代码天下无敌),正所谓,嘴上说着不要,心里却很诚实。


QQ20230225-205345-HD.gif


好了,今天的故事就分享到这里,各位看官大大觉得可以的话,还请给个赞,谢谢~


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

删库跑路后的现场还原

数据库是公司重要资产,在此类重要资产平台上,尤其是重要操作,应该保持敬畏心。 数据库被删了?可怎么证明是某某某删了数据库?或者根本都不知道谁删除了数据库,又没抓现行,该怎么办? 正文 第一步 证据先行,有录屏有真相 删库动作的录制回放 录制回放让团队能清楚...
继续阅读 »

数据库是公司重要资产,在此类重要资产平台上,尤其是重要操作,应该保持敬畏心。



数据库被删了?可怎么证明是某某某删了数据库?或者根本都不知道谁删除了数据库,又没抓现行,该怎么办?



正文


第一步 证据先行,有录屏有真相


删库动作的录制回放


录制回放让团队能清楚了解和学习用户路径和行为,其中对于关键页面诸如删除等高价值的动作,可以开启录制回放功能,比如下图,就是某一用户某一行为的屏幕录制情况。
tutieshi_640x360_15s.gif


删库成功的页面截图


针对录制回放的内容,可以看到用户点击删除按钮这一高风险行为。


image.png


第二步 录屏背后是详细的用户访问数据


rum中查看用户会话


在用户使用产品的那一刻,用户体验就开始了。用户体验数据洞见很多,加购物车、下单、视频播放等高价值按钮背后的性能等相关数据和业务息息相关:比如下图展示了成功删除数据库的提示弹窗。


image.png


发现用户登录并浏览数据库平台的详细信息


每一次用户会话中,记录着用户的来源、访问时长,以及用户行为,这里面就包含对页面的加载(切换)和按钮点击。下图便是一个用户登录数据库管理平台后,0-20分钟以内的用户旅程
image.png


发现用户点击删除库的按钮的详细信息


链接或者按钮背后隐藏着逻辑和用户动机,充分利用能转化良好化学反应。反之,在用户旅程中,也能看到用户点击删除数据库的按钮的行为,如下图所示:
image.png


点击按钮成功触发删除数据库的接口请求


为了明白请求或行为在系统中的'前世今生',链路追踪已经成了必备,在下图中,用户行为触发的请求的完整上下文就被“追踪”到了:
image.png


后台处理接口请求


在产品使用流畅度中,丝滑不一定是卖点,但“慢”肯定是用户卡点,通过全链路链路追踪综合分析,可以得到请求耗时占比,进一步定位卡在哪里(前端、后端、网络),详情见下图:
image.png


第三步 成功删库的链路详情


前后端加上数据库形成可视化闭环,构成的业务链路,能够高效定位业务情况,下图能完整看出一次删库的效率:
image.png


第四步 自动关联删库日志


全链路追踪能锦上添花的要数自动关联日志的功能了,下图能清晰看到链路所产生的日志:
image.png


以上我们便通过用户删库的录屏用户行为链路信息、操作日志等,还原了删库现场。当然,其中涉及了很多技术内容,下面整理了其中一些常见问题


相关技术点的FAQ :


1. 如何针对关键步骤开启录制回放功能


删除按钮 为例 ,用户点击删除按钮后 可以开启 录制回放功能


  function deleteDB(){
showConfirm(deleteDB).then((yes,no)=>{
if(yes)=>[ datafluxRum.startSessionReplayRecording();]
})

}

2. 录制回放是否涉及密码等用户私密信息


出于数据安全考虑,任何情况下,以下元素都会被屏蔽:



  • password、email 和 tel 类型的输入

  • 具有 autocomplete 属性的元素,例如信用卡号、到期日期和安全代码


3 . 如何将 用户行为后端 进行关联


前后端关联通过http请求头的traceID进行关联,开启rumapm简单设置即可实现关联。
rum中仅仅需要在启动时注明后端地址。以本文的后台管理系统为例,需要在启动rum时开启allowTracingOrigin这个字段,配置见下图


image.png


可以参照如下代码


 window.DATAFLUX_RUM &&
window.DATAFLUX_RUM.init({
applicationId: "node_mongo_admin_express",
datakitOrigin: "http://mongodb_admin:9529", // 协议(包括://),域名(或IP地址)[和端口号]
env: "production",
service:"node_mongo_admin_express",
version: "1.0.0",
trackInteractions: true,
allowedTracingOrigins: ["http://mongodb_admin:1234"], // 非必填,允许注入trace采集器所需header头部的所有请求列表。可以是请求的origin,也可以是是正则
sessionSampleRate: 100,
sessionReplaySampleRate: 100,
defaultPrivacyLevel: 'allow',
});
window.DATAFLUX_RUM && window.DATAFLUX_RUM.startSessionReplayRecording()

4. 如何自动将采集的日志链路信息进行关联


需要将traceID注入日志,进行切分,就可以实现链路日志的关联。本文仅用一行进行了关联,代码见下图。


image.png


5. 如何从后端下钻到数据库


仅需要接入追踪工具即可实现下图全链路追踪,本文后端使用node的express框架,链路追踪展示图如下:


image.png


其中服务调用拓扑关系如下,也就是web端访问后端(node技术栈)的,后端调用数据库(mongo


image.png


6. 后端支持java吗?


支持javapythongo以及.net等,接入的学习成本是有的,整体对于开发而言,接入配置问题不大。


7. 前端的技术架构或技术栈有兼容性吗?


目前不论是mpa还是spa,不论是ssr、还是csr,亦或是vuereactjQuery等,都支持,但针对不同架构,需要选择接入的场景。


8. 还支持哪些场景?


支持的场景很多,比如:



  • 线上告警的故障定位

  • 开发、测试环境的bug调试

  • 用户行为的追踪与回放

  • 性能瓶颈的查找与性能提升


9.有关请求耗时占比,能更详细的举个例子吗?


我们以后端为例,看到db_create这个接口:


image.png


这些数据是如何统计得出的呢?感兴趣的同学可以查看下图:
image.png


其中每个部分的计算原理如下:


Queueing(队列)耗时 = Duration - First Byte - Download  
First Byte(首包)耗时 = responseStart - domainLookupStart
Download(下载)耗时 = responseEnd - responseStart


更深入的技术内容,我将在今后的文章继续为大家整理。


综上所述


可观测性切入点很多,聪明的团队会观测;可观测性是研发质量的试金石,是企业城墙的基石,用好可观测性,能更多的了解系统,扩宽业务。



本文由观测云高级产品技术专家刘刚和交付工程师 苏桐桐共同撰写,其中所有截图及数据,均来自模拟数据,此外也欢迎一起探讨技术和业务。



参考词汇



  • adminMongo:mongo数据库管理平台

  • rum: 真实用户体验

  • apm: 应用性能管理

  • metrics:指标

  • logs:日志

  • trace:链路


作者:Yestodorrow
来源:juejin.cn/post/7207787191622893624
收起阅读 »

2023和自己聊聊

自我质疑,他人质疑前几天约了面试,被面试官问了一个问题,你做这么久的开发,有哪些技术沉淀呢,或者自己擅长哪些呢,我突然楞了一下,其实自己也想过这个问题,结论是啥也不是,很一般。给面试官说目前 vue 用的比较久,做过不少项目,对这个比较擅长一点吧,从开始做项目...
继续阅读 »

自我质疑,他人质疑

前几天约了面试,被面试官问了一个问题,你做这么久的开发,有哪些技术沉淀呢,或者自己擅长哪些呢,我突然楞了一下,其实自己也想过这个问题,结论是啥也不是,很一般。给面试官说目前 vue 用的比较久,做过不少项目,对这个比较擅长一点吧,从开始做项目都是自己摸索,从最开始的 vue2 到现在的 vue3 和 react 项目技术框架是我负责主导的,然后也会帮组员处理一些问题之类的。但是我从你的面试结果来看,多数情况下是了解或者知道某个知识点的简单使用,但细节的问题就看你支支吾吾的,应该是理解不到位吧。有什么比较好的项目,或者攻克了哪些技术难点可以做下分享吗。我沉思了一下,好像觉得没什么值得去展示的,总觉得自己的项目很平常,就算是平常遇到一些问题,很快就能解决,不是自己厉害,而是实在是项目简单而已。那你为啥觉得你能帮助别人解决问题,帮助其他组员成长呢,我陷入了无限的沉思...。

工作经历

  1. 第一家是一个外包公司,算是用了包装的简历蒙混进去的,结果是差一点给自己干离职,压力太大了,真的是s什么都不会,感觉实在是扛不下来了,于是在项目交付的前三天说自己家里有事,提了辞职。结果没辞成,老板说你忙完了再回来就行,你的工作我先让其他同事接替你。(当时也去了新的面试,但是结果可想而知)于是在请假这两周中迅速恶补,像是要奔赴战场,硬着头皮回去了,在那个接替我的同事的帮助下终于开心的(提心吊胆,每天想着二次辞职,又碍于没有脸面再提,咬咬牙终于坚持了下来,整理了八百字的小作文描述当时的过程,后来想想还是不写出来了吧)完成了第一个jsp版的项目。

  2. 后来公司接了一个新的项目,做一个后台管理系统,让我来做前端,说写好页面给到java那边,让他们来开发,还是用jsp那套。当时心想着是用 vue 脚手架搭建,来做前后端分离模式,但是我一点经验也没有,问了我那个同事,她也没这做过这种模式的,她坚持自己的意见是说用老一套,类似 jsp 那样。毕竟她比我有经验一些,那就听她的先做下试试,但心里还是想着用前后端分离来做,没人指导,只能自己去摸索,最后还是找我领导商量前后端分离模式开发。他之前做 java 的,对前端也不懂,问了我前后端分离的东西,我也是现学现卖,告诉他怎么好怎么好,但是我之前没用过,是有试错成本的,他问了我这些技术目前成熟吗,我说好多公司都开始用了,以后这个是主流。在我的忽悠下同意了这个方案。当然一切都没那么顺利,也是一步一个坑,一步步趟了过来。也感谢我这个领导,在五月份我准备辞职回去毕业答辩时帮我申请了两周的假,顺利毕业。在这个后台管理项目如期上线以后,我也终于松了一口气,没有辜负领导的信任。也感谢当时的自己坚持了自己的想法,虽然过程很难,但是也扛了下来。

  3. 慢慢的发现遇到了技术瓶颈,最开始的自己像一个海绵,进入公司后一直在吸水给自己充电,后来充电越来越慢,甚至出现了漏电的情况。于是准备跳槽,在这个外包公司离职后进入了外派的这家公司,等于从乙方进了甲方,等于好像并没有跳。日复一日的上班,加班,下班好像做了很多,但是又好像什么都没做,整天做一些表单,表格的增删改查,没什么长进,差不多一年。于是准备第二次跳槽。然后准备过完年开始第二次跳槽。就遇上了疫情,然后又呆了一段时间,准备再过了年跳槽,然后在已经开始谈 offer ,准备再多面几家时,上海又开始了疫情,直接封了三个月,那个 offer 也就不了了之了。去年年底约了些面试,都不太理想,多数都是外包,然后就到了现在。想想还是因为自己不够坚决吧。

精神内耗

一方面觉得自己不够优秀,想要去努力,另一个方面在学习时发现很多东西太难了,然后就放弃了。于是在一边想要躺平,一边想要好好学习的的状态下无限循环。然后开始了自我怀疑,自己适合做这方面的工作吗,自己做这方面有优势吗,自己有什么技术上的优点值得拿出来说说吗,好像都没有。一次次的面试,一次次的没了下文,然后都把原因归结于自己不够优秀。于是又进入了,那为啥不好好学,我试着去学了,但是学不进去,学不会的轮循怪圈。

反思与醒悟

2023年了,想着自己要去改变些什么,但是又不知如何去做,之前买了不少的书,但看的也就几本其他都在吃灰。看朋友圈有人在微信读书,于是也试着看一些书看解决一下心理浮躁的问题,不能这么浑浑噩噩下去,不然真就废了。工作,生活,情感压力感觉都快抑郁了。直到最近看了大佬分享的书,才开始有所醒悟,是自己太急于求成了。太想在刚投入一点精力就要看到成果了,平常是看了不少学习的资料,但也都是在自己舒适区内,一旦遇到难的就告诉自己肯定学不会,所以就放弃了,不会将难题碎片化,一次解决一个小问题,爬山也不都是一步一步走上去的嘛。学会去接受自己的平凡,但是不能以自己是个普通人为理由而不去努力。实践是验证真理的唯一标准,所以我们在学习时也更要去思考,去试着用自己的话看能不能书写出来,讲给别人听,看对方能听明白不。如果只是以为自己去学习了,就万事大吉了,但过段时间可能就会忘记了,这一点我最近特别有体会。就拿写的两篇 vue 的基础知识点来说,以为自己很容易就能写出来,但写的时候发现没那么容易的。有的地方可能还需要再查下资料才能搞明白,不过也加深了对这些东西的理解,如果在帮助自己的同时能帮助别人就更好了。

一起共勉

书上的几个观点觉得很有用,分享给大家,如果目前有小伙伴也有我上面的焦虑
1. 试着跟自己和解,停止精神内耗,接受自己的普通,但不能因此而止步不前,摆烂
2. 在自己跳一跳就能够得着的地方做拉伸,在舒适区和困难区要么无所事事,要么备受打击
3. 不要急于求成,罗马不是一天建成了,只管按照自己的节奏去努力,事实会告诉你答案
4. 输入的同时也要去输出,形成闭环,实践是验证真理的唯一标准,试着去做到知行合一

作者:南岸月明
来源:juejin.cn/post/7207841934278344762

收起阅读 »

前端按钮生成器!要的就是效率!

web
大家好,我是前端实验室的老鱼!一名资深的互联网玩家,专注分享大前端领域技术、面试宝典、学习资料等 | 副业赚钱~ 「前端实验室」 专注分享 Github、Gitee等开源社区优质的前端项目、前端技术、前端学习资源以及前端资讯等各种有趣实用的前端内容。 按钮是我...
继续阅读 »

大家好,我是前端实验室的老鱼!一名资深的互联网玩家,专注分享大前端领域技术、面试宝典、学习资料等 | 副业赚钱~


「前端实验室」 专注分享 Github、Gitee等开源社区优质的前端项目、前端技术、前端学习资源以及前端资讯等各种有趣实用的前端内容。


按钮是我们页面开发中必不可少的一部分。在平常开发中,我们常常一遍又一遍的重复写着各种各样的按钮样式。


这些简单,但机械重复的工作是否影响到你的工作效率了呢?


今天为大家推荐一个按钮生成的网站。100+款按钮样式和响应方式供你挑选!图片准备好了吗?一起来看下吧!


3D款


平面3D效果的按钮。图片点击按钮,自动将按钮样式复制到剪切板,直接粘贴即可使用。


.css-button-3d--sky {
  min-width130px;
  height40px;
  color#fff;
  padding5px 10px;
  font-weight: bold;
  cursor: pointer;
  transition: all 0.3s ease;
  position: relative;
  display: inline-block;
  outline: none;
  border-radius5px;
  border: none;
  background#3a86ff;
  box-shadow0 5px #4433ff;
}
.css-button-3d--sky:hover {
  box-shadow0 3px #4433ff;
  top1px;
}
.css-button-3d--sky:active {
  box-shadow0 0 #4433ff;
  top5px;
}

渐变款


渐变的按钮平常使用不多,但就怕产品经理提这样的需求。图片


阴影边框


按钮带点阴影边框,在大师兄的项目中算是基本需求了。因为生硬的边框总会缺乏点柔和的美感。图片拷贝个代码来看看。


.css-button-shadow-border--sky {
  min-width130px;
  height40px;
  color#fff;
  padding5px 10px;
  font-weight: bold;
  cursor: pointer;
  transition: all 0.3s ease;
  position: relative;
  display: inline-block;
  outline: none;
  border-radius5px;
  border: none;
  box-shadow:inset 2px 2px 2px 0px rgba(255,255,255,.5), 7px 7px 20px 0px rgba(0,0,0,.1), 4px 4px 5px 0px rgba(0,0,0,.1);
  background#4433ff;
}
.css-button-shadow-border--sky:hover {
  background-color#3a86ff;
}
.css-button-shadow-border--sky:active {
  top2px;
}

这篇幅,让我自己一行代码一行代码的敲,确实有点费时间。还是直接拷贝来得快。


各种hover状态


浮光掠影的效果图片镂空效果图片滑动效果图片增加其他显示图片


其他


按钮的样式和交互功能,对大家来说都是很简单的操作。但重复的编写这些代码会浪费些许时间。

本文分享了各种常用的各种按钮形式,对于有自定义按钮需求的小伙伴可以作参考。



网站地址

markodenic.com/tools/butto…



写在最后


我是前端实验室的老鱼!一名资深的互联网玩家,专注分享大前端领域技术、面试宝典、学习资料、副业等~


喜欢的朋友,点赞收藏支持一下,也欢迎交流~



作者:程序员老鱼
来源:juejin.cn/post/7202907707472609337
收起阅读 »

内卷年代,是该学学WebGL了

web
前言 大部分公司的都会有可视化的需求,但是用echarts,antv等图表库,虽然能快速产出成果,但是还是要知道他们底层其实用canvas或svg来做渲染,canvas浏览器原生支持,h5天然支持的接口,而svg相比矢量化,但是对大体量的点的处理没有canva...
继续阅读 »

前言


大部分公司的都会有可视化的需求,但是用echarts,antv等图表库,虽然能快速产出成果,但是还是要知道他们底层其实用canvas或svg来做渲染,canvas浏览器原生支持,h5天然支持的接口,而svg相比矢量化,但是对大体量的点的处理没有canvas好,但是可以操作dom等优势。canvas和svg我们一般只能做2d操作,当canvas.getContext('webgl')我们就能获取webgl的3d上下文,通过glsl语言操作gpu然后渲染了。理解webgl,可以明白h5的很多三维的api底层其实都是webgl实现,包括对canvas和svg也会有新的认知。


canvas和webgl的区别


canvas和webgl都可以做二维三维图形的绘制。底层都会有对应的接口获取。cancvas一般用于二维ctx.getContext("2d"),三维一般可以通过canvas.getContext('webgl')


窥探WebGL


理解建模


如果你有建模软件基础的话,相信3dmax、maya、su等软件你一定不会陌生,本质其实就是点、线、面来组成千变万化的事物。打个比方球体就是无数个点连成线然后每三根线形成面,当然有常见的四边形,其实也是两个三边形组成,为什么不用四边形,因为三边形更稳定、重心可计算、数据更容易测算。


所以核心也就是点、线、三角面


了解WebGL


WebGL可以简单理解为是openGL的拓展,让web端通过js可以有强大的图形处理能力。当然为了与显卡做交互你必须得会glsl语言。


GLSL


glsl着色器语言最重要的就是顶点着色器和片元着色器。简单理解为一个定位置一个添颜色。


简单绘制一个点


webgl会有大量的重复性前置工作,也就是创建着色器 -> 传入着色器源码 -> 编译着色器 -> 创建着色器程序 -> 绑定、连接、启用着色器 -> 可以绘制了!


一般而言我们是不会重复写这个东西,封装好了直接调用就行。


function initShader (gl, VERTEX_SHADER_SOURCE, FRAGMENT_SHADER_SOURCE) {
const vertexShader = gl.createShader(gl.VERTEX_SHADER);

const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);

gl.shaderSource(vertexShader, VERTEX_SHADER_SOURCE);
gl.shaderSource(fragmentShader, FRAGMENT_SHADER_SOURCE);

//编译着色器
gl.compileShader(vertexShader);
gl.compileShader(fragmentShader);

//创建程序对象
const program = gl.createProgram();

gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);

gl.linkProgram(program);
gl.useProgram(program);

return program;
}

<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="./initShader.js"></script>
</head>

<body>
<canvas id="canvas" width="300" height="400">
不支持canvas
</canvas>
</body>

<script>
const ctx = document.getElementById('canvas')
const gl = ctx.getContext('webgl')

//着色器: 通过程序用固定的渲染管线,来处理图像的渲染,着色器分为两种,顶点着色器:顶点理解为坐标,片元着色器:像素

//顶点着色器源码
const VERTEX_SHADER_SOURCE = `
void main() {
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
gl_PointSize = 10.0;
}
`
//片元着色器源码
const FRAGMENT_SHADER_SOURCE = `
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
`
//创建着色器
const program = initShader(gl, VERTEX_SHADER_SOURCE, FRAGMENT_SHADER_SOURCE)

//执行绘制
gl.drawArrays(gl.POINTS, 0, 1)
//gl.drawArrays(gl.LINES, 0, 1)
//gl.drawArrays(gl.TRIANGLES, 0, 1)

</script>

</html>

绘制效果如下:


image.png


相信看了上面有段代码会有疑惑


image.png


gl_position代表坐标,vec4就一个存放个4个float的浮点数的容量,定义坐标, 分别对应x、y、z、w,也就是三维坐标,但是w就等于比例缩放xyz而已,一般在开发中,我们的浏览器的坐标要跟这个做个转换对应上,gl_POintSize是点的大小,注意是浮点数


image.png


gl_flagColor渲染的像素是红色,是因为这类似于比例尺的关系需要做个转换, (R值/255,G值/255,B值/255,A值/1) -》(1.0, 0.0, 0.0, 1.0)


绘制动态点


<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="./initShader.js"></script>
</head>

<body>
<canvas id="canvas" width="300" height="400">
不支持canvas
</canvas>
</body>

<script>
const canvas = document.getElementById('canvas')
const gl = canvas.getContext('webgl')

const VERTEX_SHADER_SOURCE = `
precision mediump float;
attribute vec2 a_Position;
attribute vec2 a_Screen_Size;
void main(){
vec2 position = (a_Position / a_Screen_Size) * 2.0 - 1.0;
position = position * vec2(1.0, -1.0);
gl_Position = vec4(position, 0, 1);
gl_PointSize = 10.0;
}
`

const FRAGMENT_SHADER_SOURCE = `
precision mediump float;
uniform vec4 u_Color;
void main() {
vec4 color = u_Color / vec4(255, 255, 255, 1);
gl_FragColor = color;
}
`

//前置工作,着色器可以渲染了!
const program = initShader(gl, VERTEX_SHADER_SOURCE, FRAGMENT_SHADER_SOURCE)

//获取glsl的变量对应的属性做修改
var a_Position = gl.getAttribLocation(program, 'a_Position');
var a_Screen_Size = gl.getAttribLocation(program, 'a_Screen_Size');
var u_Color = gl.getUniformLocation(program, 'u_Color');
gl.vertexAttrib2f(a_Screen_Size, canvas.width, canvas.height); //给glsl的属性赋值两个浮点数

//给个默认背景颜色
gl.clearColor(0, 0, 0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);

//存储点击位置的数组。
var points = [];
canvas.addEventListener('click', e => {
var x = e.pageX;
var y = e.pageY;
var color = { r: Math.floor(Math.random() * 256), g: Math.floor(Math.random() * 256), b: Math.floor(Math.random() * 256), a: 1 };
points.push({ x: x, y: y, color: color })

gl.clearColor(0, 0, 0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);

for (let i = 0; i < points.length; i++) {
var color = points[i].color;
gl.uniform4f(u_Color, color.r, color.g, color.b, color.a);
gl.vertexAttrib2f(a_Position, points[i].x, points[i].y);
gl.drawArrays(gl.POINTS, 0, 1);
}
})
</script>

</html>

vec2 position = (a_Position / a_Screen_Size) * 2.0 - 1.0; 注意这里的坐标转换,从canvas转为ndc坐标,其实就是看范围就行,[0, 1] -> [0, 2] -> [-1, 1]。上面总体的流程总结下就是,定义着色器,定义glsl着色器源码 -> 通过api获取canvas的信息转换坐标系 -> 监听点击事件传递变量到glsl中 -》通过pointer缓存 -> drawArrays绘制。但是这种方法,很明显有大量的重复渲染,每次遍历都要把之前渲染的重复执行。


大致效果


动画.gif


总结


通过简单的webgl入门,已经有了初步的认知,大致的流程为:着色器初始化 -> 着色器程序对象 -> 控制变量 -> 绘制,为了更好的性能,后面会使用缓冲区来解决重复渲染的问题,包括我们的顶点不会一个一个设置,一般是会涉及到矩阵的转换,如平移、缩放、旋转、复合矩阵。


作者:谦宇
来源:juejin.cn/post/7207417288705458231
收起阅读 »

聊聊自己思想怎么改变的

框架之争 xx 框架牛逼,xx 框架写起来舒服,xx 框架做不了大业务,xx 框架坑多,xx框架抄袭 xx 框架。 读者应该看到过类似的言论,不知道读者怎么想。反正我是觉得很没意思……((哥我是SB) 管你什么蛇皮框架,不都得老老实实的给编译成 js 代码给解...
继续阅读 »

框架之争


xx 框架牛逼,xx 框架写起来舒服,xx 框架做不了大业务,xx 框架坑多,xx框架抄袭 xx 框架。


读者应该看到过类似的言论,不知道读者怎么想。反正我是觉得很没意思……((哥我是SB)


管你什么蛇皮框架,不都得老老实实的给编译成 js 代码给解析器(v8)执行?(wasm另说喽)


双向绑定,虚拟DOM 等等随便一搜答案一堆堆的,但是读者是否有自己去了解过内部的具体实现呢?是否又去尝试将这个思想运用到业务上呢?


有?那么你不用看我这篇流水账了。


没有?那么思考下你看这些做咩?


为了应付面试?然后看完就忘了对吧,下次面试继续看……你这只是为了应付面试官骗自己。


是时候做出改变了,朋友……


怎么改变


我不到哇,我真不知道……我只能把自己的突破经验告诉你。至于读者是否能够摆脱当前的困境,emmmm 随缘把……


1676353480450.png


首先我们先约定一件事情,读者要记住:你是开发者,不是前端开发者,后端开发者更不是客户端开发者。


想起来以前学习 node 的时候,非要找 node后端的代码来练手。node 什么不能写,为什么我非要写 node 后端,为什么要局限于 web 端?我可以用 nodecli 工具,也可以用来写桌面客户端 electron


所以不要陷入身份认知,记住你的身份你是开发者!


不要像我,我学会了 node,看到大家都在用 node 写后端,就认为我只能用 node 写后端!


思想不要局限于某一门语言,某个开发框架,格局打开!


编程思想


我的思想改变,是在去年的3月份,当时在看《代码整洁之道》一书。


当时看到代码规范一章节,正想着吐槽作者这都是老生常谈的问题了,怎么还提出来一章讲。


突然想起来,这本书第一版是 09 年出版的。而我是在 22 年看的这本书, 13 年了这本书中的内容居然还没过时!


1676370379886.png


当时我想明白了,我应该学习代码的思想,而不是框架,框架会层出不穷。


框架思想


angularjs(1.x版本)读者应该听说过把,最初发布时间2009年,14年前就有"双向绑定","template"组件……


2016年 angular2 发布,很多核心理念都来至 angularjs,还带来新的机制 NgModule


一个 NgModule 就是一个容器,用于存放一些内聚的代码块,这些代码块专注于某个应用领域、某个工作流或一组紧密相关的功能。


Angular 的前世今生


最恐怖的是,这个 NgModule 机制还可以用来做后端 Nestjs


不管是 14年前的双向绑定,组件化,还是16年的 NgModule 开发思想其实一直都没怎么改变。


今年流行 xx 框架,过两年则又会流行新的 xx 框架。


与其花大量的时间去研究框架 xx面试题,不如去阅读框架的源码,自己去读去理解框架,看看框架中有什么奇技淫巧,从而运用到自己代码上,给自己带来真正的效益。而不是死记硬背,背完应付面试。


tips: 写代码最难的不是业务,而是怎么设计架构。业务会不停的变化,架构需要应付不停变动的业务。


读源码


多读源码!多读源码!多读源码!


读什么源码都可以,读的时候可以带着疑问去读。


比如说我想知道 vite 是怎么处理 vuetemplate 是怎么解析的。


要么查资料,要么自己翻源码。


vite 需要这个 vite-plugin-vue 插件做 vue 支持,去翻这个源码。发现这个插件里头用了 vue/compiler-sfc 这个包的 compileTemplate 方法,最后找到 compiler-dom 包的 compile 方法(人麻了……)


1676439918093.png


克服了重重困难总算找到 vite处理 vuetemplate 的代码了,仿佛跟着唐长老去去了一波西经。


其实结果并不怎么重要,重要的是整个过程。




  1. 我先去找了 vite-plugin-vue。这是一个知识点 vite 插件




  2. vite-plugin-vue里使用了 vue/compiler-sfc,我又要去 vue 源码里找东西。第二个知识点 vue 源码。




  3. 找到了 compile 第三个知识点 怎么处理 template 的。




在这个过程中我列举了我找到的三个知识点,就算我不去对前两个知识点深入研究,但寻找结果的过程已经无形中都增加了我的知识储备。


那么我深入探究细节呢?


这个问题留给读者把……


推荐资料




  1. 代码整洁之道




  2. HTTP权威指南




  3. 设计模式:可复用面向对象软件的基础




  4. JavaScript设计模式与开发实践




  5. 架构整洁之道




有读者可能要吐槽了,你这怎么推荐的有 Java 代码的书……


还记得上文说的一句吗?


思想不要局限于某一门语言,某个开发框架,格局打开!


我们要学的是"思想"。


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

Disruptor 高性能队列原理浅析

介绍 Disruptor 是英国外汇交易公司 LMAX 开发的一个高性能队列,研发的初衷是解决内存队列的延迟问题(在性能测试中发现竟然与 I/O 操作处于同样的数量级)。基于Disruptor 开发的系统单线程能支撑每秒 600 万订单,2010 年在 QCo...
继续阅读 »

介绍


Disruptor 是英国外汇交易公司 LMAX 开发的一个高性能队列,研发的初衷是解决内存队列的延迟问题(在性能测试中发现竟然与 I/O 操作处于同样的数量级)。基于Disruptor 开发的系统单线程能支撑每秒 600 万订单,2010 年在 QCon 演讲后,获得了业界关注。2011 年,企业应用软件专家 Martin Fowler 专门撰写长文介绍。同年它还获得了 Oracle 官方的 Duke 大奖。


本文主要参考它 2011 年的论文 《LMAX Disruptor: High performance alternative to bounded queues for exchanging data between concurrent threads》还结合了美团技术团队对它分析的文章。论文中文翻译参考了肥兔子爱豆畜子翻译的中文版。


这里所说的队列是系统内部的内存队列,而不是Kafka这样的分布式队列。


许多应用程序依靠队列在处理阶段之间交换数据。我们的性能测试表明,当以这种方式使用队列时,其延迟成本与磁盘(基于RAID或SSD的磁盘系统)的IO操作成本处于同一数量级都很慢。如果在一个端到端的操作中有多个队列,这将使整个延迟增加数百微秒。


测试表明,使用 Disruptor 的三阶段流水线的平均延迟比基于队列的同等方法低 3 个数量级。此外,在相同的配置下,Disruptor 处理的吞吐量约为 8 倍。


并发问题


在本文以及在一般的计算机科学理论中,并发不仅意味着两个以上任务同时并行发生,而且意味着它们在访问资源时相互竞争。争用的资源可以是数据库、文件、socket,甚至是内存中的一个位置。


代码的并发执行涉及两件事:互斥和内存可见性。互斥是关于如何管理保证某些资源的独占式使用。内存可见性是关于控制内存更改何时对其他线程可见。如果你可以避免多线程竞争的去更新共享资源,那么就可以避免互斥。如果您的算法可以保证任何给定的资源只被一个线程修改,那么互斥是不必要的。读写操作要求所有更改对其他线程可见。但是,只有争用的写操作需要对更改进行互斥。


在任何并发环境中,最昂贵的操作是争用写访问。要让多个线程写入同一资源,需要复杂而昂贵的协调。通常,这是通过采用某种锁策略来实现的。


但是锁的开销是非常大的,在论文中设计了一个实验:



  • 这个测试程序调用了一个函数,该函数会对一个 64 位的计数器循环自增 5 亿次。

  • 机器环境:2.4G 6 核

  • 运算: 64 位的计数器累加 5 亿次



单线程情况下,不加锁的性能 > CAS 操作的性能 > 加锁的性能。


在多线程情况下,为了保证线程安全,必须使用 CAS 或锁,这种情况下,CAS 的性能超过锁的性能,前者大约是后者的 8 倍。


保证线程安全一般使用锁或者原子变量。


采取加锁的方式,默认线程会冲突,访问数据时,先加上锁再访问,访问之后再解锁。通过锁界定一个临界区,同时只有一个线程进入。


原子变量能够保证原子性的操作,意思是某个任务在执行过程中,要么全部成功,要么全部失败回滚,恢复到执行之前的初态,不存在初态和成功之间的中间状态。例如 CAS 操作,要么比较并交换成功,要么比较并交换失败。由 CPU 保证原子性。


通过原子变量可以实现线程安全。执行某个任务的时候,先假定不会有冲突,若不发生冲突,则直接执行成功;当发生冲突的时候,则执行失败,回滚再重新操作,直到不发生冲突。


CAS 操作是一种特殊的机器代码指令,它允许将内存中的字有条件地设置为原子操作。比如对于前面的“递增计数器实验”例子,每个线程都可以在一个循环中自旋,读取计数器,然后尝试以原子方式将其设置为新的递增值。



如图所示,Thread1 和 Thread2 都要把 Entry 加 1。若不加锁,也不使用 CAS,有可能 Thread1 取到了myValue=1,Thread2 也取到了 myValue=1,然后相加,Entry 中的 value 值为 2。这与预期不相符,我们预期的是 Entry 的值经过两次相加后等于3。


CAS 会先把 Entry 现在的 value 跟线程当初读出的值相比较,若相同,则赋值;若不相同,则赋值执行失败。一般会通过 while/for 循环来重新执行,直到赋值成功。CAS无需线程进行上下文切换到内核态去执行,在用户态执行了 CPU 的原语指令 cmpxchg,CAS 相当于在用户态代码里边插入了一个 cmpxchg 指令,这样 CPU 一直在用户态执行,执行到 cmpxchg 指令就开始执行内核态内存空间的操作系统的代码。执行指令要比上下文切换的开销要小,所以 CAS 要比重量级互斥锁性能要高。(用户态和内核态没有切换)


如果程序的关键部分比计数器的简单增量更复杂,则可能需要使用多个CAS操作的复杂状态机来编排争用。使用锁开发并发程序是困难的;而使用 CAS 操作和内存屏障开发无锁算法要更加复杂多倍,而且难于测试和证明正确性。


内存屏障和缓存问题


出于提升性能的原因,现代处理器执行指令、以及内存和执行单元之间数据的加载和存储都是不保证顺序的。不管实际的执行顺序如何,处理器只需保证与程序逻辑的顺序产生相同的结果即可。这在单线程的程序中不是一个问题。但是,当线程共享状态时,为了确保数据交换的成功与正确,在需要的时候、内存的改变能够以正确的顺序显式是非常重要的。处理器使用内存屏障来指示内存更新顺序很重要的代码部分。它们是在线程之间实现硬件排序和更改可见性的方法。


内存屏障(英语:Memory barrier),也称内存栅栏,内存栅障,屏障指令等,是一类同步屏障指令,它使得 CPU 或编译器在对内存进行操作的时候, 严格按照一定的顺序来执行, 也就是说在内存屏障之前的指令和之后的指令不会由于系统优化等原因而导致乱序。


大多数处理器提供了内存屏障指令:



  • 完全内存屏障(full memory barrier)保障了早于屏障的内存读写操作的结果提交到内存之后,再执行晚于屏障的读写操作。

  • 内存读屏障(read memory barrier)仅确保了内存读操作;

  • 内存写屏障(write memory barrier)仅保证了内存写操作。


现代的 CPU 现在比当前一代的内存系统快得多。为了弥合这一鸿沟,CPU 使用复杂的高速缓存系统,这些系统是有效的快速硬件哈希表,无需链接。这些缓存通过消息传递协议与其他处理器缓存系统保持一致。此外,处理器还具有“存储缓冲区”(store buffer/load buffer,比 L1 缓存更靠近 CPU,跟寄存器同一个级别,用来当作 CPU 与高速缓存之间的缓冲。毕竟高速缓存由于一致性的问题也会阻塞)来缓冲对这些缓存的写入,以及作为“失效队列”,以便缓存一致性协议能够在即将发生写入时快速确认失效消息,以提高效率。


这对数据意味着,任何值的最新版本在被写入后的任何阶段都可以位于寄存器、存储缓冲区、L1/L2/L3 缓存之一或主内存中。如果线程要共享此值,则需要以有序的方式使其可见,这是通过协调缓存一致性消息的交换来实现的。这些信息的及时产生可以通过内存屏障来控制。


L1、L2、L3 分别表示一级缓存、二级缓存、三级缓存,越靠近 CPU 的缓存,速度越快,容量也越小。所以 L1 缓存很小但很快,并且紧靠着在使用它的 CPU 内核;L2 大一些,也慢一些,并且仍然只能被一个单独的 CPU 核使用;L3 更大、更慢,并且被单个插槽上的所有 CPU 核共享;最后是主存,由全部插槽上的所有 CPU 核共享。



当 CPU 执行运算的时候,它先去 L1 查找所需的数据、再去 L2、然后是 L3,如果最后这些缓存中都没有,所需的数据就要去主内存拿。走得越远,运算耗费的时间就越长。所以如果你在做一些很频繁的事,你要尽量确保数据在 L1 缓存中。



另外,线程之间共享一份数据的时候,需要一个线程把数据写回内存,而另一个线程访问内存中相应的数据。



如果你用一种能被预测的方式访问内存的话,CPU 可以预测下个可能访问的值从内存先缓存到缓存中,来降低下次访问的延迟。但是如果是一些非顺序的、步长无法预测的结构,让 CPU 只能访问内存,性能上与访问缓存差很多。所以为了有效利用 CPU 高速缓存的特性,我们应当尽量使用顺序存储结构。


队列的问题


队列通常使用链表或数组作为元素的底层存储。如果允许内存中的队列是无界的,那么对于许多类的问题,它可以不受约束地增长,直到耗尽内存而达到灾难性的后果,当生产者超过消费者时就会发生这种情况。无界队列在可以在生产者可以保证不超过消费者的系统中使用,因为内存是一种宝贵的资源,但是如果这种假设不成立,而队列增长没有限制,那么总是有风险的。为了避免这种灾难性的结果,队列的大小通常要受到限制(有界)。要使队列保持有界,就需要对其底层选择数组结构或主动跟踪其大小。


队列的实现往往要在 head、tail 和 size 变量上有写争用。在使用时,由于消费者和生产者之间的速度差异,队列通常总是接近于满或接近于空。它们很少在生产和消费速率均衡的中间地带运作。这种总是满的或总是空的倾向会导致高级别的争用、和/或昂贵的缓存一致性。问题在于,即使 head 和 tail 使用不同的并发对象(如锁或CAS变量)来进行读写锁分离,它们通常也占用相同的 cacheline。


管理生产者申请队列的 head,消费者申请队列的 tail,以及中间节点的存储,这些问题使得并发实现的设计非常复杂,除了在队列上使用一个粗粒度的锁之外,还难以管理。对于 put 和 take 操作,使用整个队列上的粗粒度锁实现起来很简单,但对吞吐量来说是一个很大的瓶颈。如果并发关注点在队列的语义中被分离开来,那么对于除单个生产者-单个消费者之外的任何场景,实现都变得非常复杂。


而使用相同的 cacheline 会产生伪共享问题。比如 ArrayBlockingQueue 有三个成员变量:



  • takeIndex:需要被取走的元素下标;

  • putIndex:可被元素插入的位置的下标;

  • count:队列中元素的数量;


这三个变量很容易放到一个缓存行中,但是之间修改没有太多的关联。所以每次修改,都会使之前缓存的数据失效,从而不能完全达到共享的效果。



如上图所示,当生产者线程 put 一个元素到 ArrayBlockingQueue 时,putIndex 会修改,从而导致消费者线程的缓存中的缓存行无效,需要从主存中重新读取。一般的解决方案是,增大数组元素的间隔使得由不同线程存取的元素位于不同的缓存行上,以空间换时间。


Disruptor 解决思路


启动时,将预先分配环形缓冲区的所有内存。环形缓冲区可以存储指向 entry 的指针数组,也可以存储表示 entry 的结构数组。这些 entry 中的每一个通常不是传递的数据本身,类似对象池机制,而是它的容器。这种 entry 的预分配消除了支持垃圾回收的语言中的问题,因为 entry 将被重用,并在整个 Disruptor 实例存活期间都有效。这些 entry 的内存是同时分配的。


一般的数据结构是像下面这样的:



我们可以使用一个环状的数组结构改进成下面这样:



数组的连续多个元素会一并加载到 CPU Cache 里面来,所以访问遍历的速度会更快。而链表里面各个节点的数据,多半不会出现在相邻的内存空间,自然也就享受不到整个 Cache Line 加载后数据连续从高速缓存里面被访问到的优势。遍历访问时 CPU 层面的分支预测会很准确。这可以使得我们更有效地利用了 CPU 里面的多级流水线,我们的程序就会跑得更快。


在像 Java 这样的托管运行时环境中开发低延迟系统时,垃圾收集机制可能会带来问题。分配的内存越多,给垃圾收集器带来的负担就越大。当对象的寿命很短或实际上是常驻的时候,垃圾收集器工作得最好。在环形缓冲区中预先分配 entry 意味着它对于垃圾收集器来说是常驻内存的,垃圾回收的负担就很轻。同时,数组结构对处理器的缓存机制更加友好。数组长度 2^n,通过位运算,加快定位的速度。下标采取递增的形式。不用担心 index 溢出的问题。index 是 long 类型,即使 100 万 QPS 的处理速度,也需要 30 万年才能用完。


一般的 Cache Line 大小在 64 字节左右,然后 Disruptor 在非常重要的字段前后加了很多额外的无用字段。可以让这一个字段占满一整个缓存行,这样就可以避免未共享导致的误杀。


每个生产者或者消费者线程,会先申请可以操作的元素在数组中的位置,申请到之后,直接在该位置写入或者读取数据。


下面用非环形的结构模拟无锁读写。


一个生产者的流程



  1. 申请写入m个元素;

  2. 若是有m个元素可以入,则返回最大的序列号。这儿主要判断是否会覆盖未读的元素;

  3. 若是返回的正确,则生产者开始写入元素。



多个生产者流程


多个生产者的情况下,会遇到“如何防止多个线程重复写同一个元素”的问题。Disruptor 的解决方法是,每个线程获取不同的一段数组空间进行操作。这个通过 CAS 很容易达到。只需要在分配元素的时候,通过 CAS 判断一下这段空间是否已经分配出去即可。


但如何防止读取的时候,读到还未写的元素。Disruptor 在多个生产者的情况下,引入了一个与 Ring Buffer 大小相同的 buffer,Available Buffer。当某个位置写入成功的时候,便把 Availble Buffer 相应的位置置位,标记为写入成功。读取的时候,会遍历 Available Buffer,来判断元素是否已经就绪。


读数据流程


生产者多线程写入的情况会复杂很多:



  1. 申请读取到序号n;

  2. 若 writer cursor >= n,这时仍然无法确定连续可读的最大下标。从 reader cursor 开始读取 available Buffer,一直查到第一个不可用的元素,然后返回最大连续可读元素的位置;

  3. 消费者读取元素。


如下图所示,读线程读到下标为 2 的元素,三个线程 Writer1/Writer2/Writer3 正在向 RingBuffer 相应位置写数据,写线程被分配到的最大元素下标是 11。


读线程申请读取到下标从3到11的元素,判断 writer cursor>=11。然后开始读取 availableBuffer,从 3 开始,往后读取,发现下标为7的元素没有生产成功,于是WaitFor(11)返回6。


然后,消费者读取下标从 3 到 6 共计 4 个元素(多个生产者情况下,消费者消费过程示意图)。



写数据流程


多个生产者写入的时候:



  1. 申请写入 m 个元素;

  2. 若是有 m 个元素可以写入,则返回最大的序列号。每个生产者会被分配一段独享的空间;

  3. 生产者写入元素,写入元素的同时设置 available Buffer 里面相应的位置,以标记自己哪些位置是已经写入成功的。


如下图所示,Writer1 和 Writer2 两个线程写入数组,都申请可写的数组空间。Writer1 被分配了下标 3 到下表 5 的空间,Writer2 被分配了下标 6 到下标 9 的空间。


Writer1 写入下标 3 位置的元素,同时把 available Buffer 相应位置置位,标记已经写入成功,往后移一位,开始写下标 4 位置的元素。Writer2 同样的方式。最终都写入完成。



总结


整体上来看 Disruptor 在提高吞吐量、减少并发执行损耗上做出了很大贡献,通过贴合硬件机制的方式进行设计,消除写争用,最小化读争用,并确保代码与现代处理器使用的 Cache 特性良好配合。我们可以看下 Log4j 2 的性能数据,Log4j 2 的 Loggers all async 就是基于 Disruptor 的。



总结来说 Disruptor 是性能极高的无锁队列,提供了一种很好的利用硬件特性实现尽可能从缓存读取来加速访问的无锁方案。


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

开发的功能不都是经过上线测试,为什么上线后还会那么多 Bug ?

你是否也经过这样的灵魂拷问:「开发的功能不都是经过上线测试的吗?为什么上线后还会那么多 Bug ?」。 大家明明都很努力,为什么「输出」的结果没有更进一步?今天我们就水一水这个「狗血」话题,究竟是谁个锅? 本篇只是毫无意义的「故事」,内容纯属「虚构」,如有...
继续阅读 »

你是否也经过这样的灵魂拷问:「开发的功能不都是经过上线测试的吗?为什么上线后还会那么多 Bug ?」。


大家明明都很努力,为什么「输出」的结果没有更进一步?今天我们就水一水这个「狗血」话题,究竟是谁个锅?




本篇只是毫无意义的「故事」,内容纯属「虚构」,如有雷同,自己「反思」。



对于这个话题,我想用一个「虚构」的故事来介绍下,这个故事里我有一个「朋友」,他在某电商项目组,当时恰逢经历了公司内部的「双十一需求立项」:


立项时


老板:“这次的需求很简单,主要就是参考去年 TB 的预热活动来走,核心就是提高客单量和活跃,具体细节你们和产品沟通就行”


产品:“TB 去年的活动预热大家应该都了解吧,我们这次主要就是复刻一个类似的活动,时间一个月,具体有***,总的来说,双十一活动一定要准时上线,这次运营那边投入很多经费,需求方面我会把控好,核心围绕老板的思路,细节大家可以看文档,基本就是这些内容,一个月应该够的,大家没问题吧?”


开发:“没问题,保证完成任务”


3 天后:


老板:“我刚看了 JD 好像推出了一个不错的小游戏,我觉得这个可以导入到这边活动里,这样可以提高用户的活跃,用户活跃了,消费自然就起来了”


开会


产品:“鉴于老板的意见,我觉得 JD 的这个游戏活动效果可以提升我们的复购,所以我计划在原本需求基础上增加一个支线活动。


产品:“大家不要紧张,支线会和当前主线同步开发,支线活动比较灵活,不对账号级别做限制,另外「设计」那边看下入口放哪里比较合适。”


开发:“上线时间还是没变吗?”


产品:“双十一日期会变吗?老板说了大家抓紧点,功能不多,时间还是够的”


10 天后:


老板:“我刚和x总沟通了下,他觉得我们的活动少了互动,不利于留存,你看下怎么处理”


开会


产品:“经过这几天和老板的探讨,我们发现这次活动少了一些互动性,必须增加一些交互游戏,这样才能提升日活和用户体验。


产品:“这样吧,这部分功能也是比较迫切,我知道任务比较重,但是为了这次能取到较好成果,大家加把劲,接下来周末幸苦下大家,先不休假,后面调休补回来,具体需求大家可以看文档,有一些调整,不多,时间还是够的”


开发:“。。。。”


14 天后:


老板:“我看大家工作的热情还是可以的,刚运营提了可以增加一些视频支持,我觉得这是一个很好的意见”


开会


产品:“目前看起来我们的开发进度还比较乐观,运营的同学说了他们录制了一些活动视频,我看了还不错,就在主会场增加一些视频播放的功能吧,细节你们直接和设计讨论下”


产品:“这个应该不难吧,把分享和下载加上,视频播放这么基础的东西,应该不耽误事,我看网上开源服务就挺多的。”


开发:“。。。。”


20 天后:


老板:“我刚去开发那里看了下效果,感觉分会场的效果挺好的,做支线太可惜了,给他加回主流程里”


开会


产品:“老板那边觉得分会场的效果更好,让我们把分会场的效果做到主会场里的核心交互里,分会场部分的逻辑就不要了,入口也取消。


产品:“大家不要激动,都是现成的东西,改一改很快的,不过项目进度目前看来确实起有点拉垮,接下来大家晚上多幸苦点,我们晚上11点后再下班,我申请给大家报销打车的费用”


开发:“。。。。”


23 天后:


老板:“整体效果我觉得可以,就是好像有一些糙,你们再过一过,另外大家开个会统一下目标,看看能不能有新的补充”


产品:“可以的,过去我们也一直在开会对齐,基本每两天都会对齐一次”


开会


产品:“我和设计对了下,发现有一些细节可以优化,比如一些模块的颜色可以细调一下,还有这些按键的动画效果也加上,我知道工期紧张,所以我从「隔壁」项目组借了几个开发资源,未来一周他们可以帮助大家缓解下压力”


开发:“。。。。”


28 天后:


开会


产品:“好了,项目可以提测了,相信之前测试也陆续介入跟进了需求,应该问题不大,目前看起来「燃尽图」还可以,测试完了尽快上线,老板那边也在催了”


测试:“不行啊,今天走了用例,一堆问题,而且提测版本怎么和用例还有需求文档不一致,这个提测的版本开发是不是自己没做过测试啊,一大堆问题,根本测不下去,这样我们很难做”


产品:“这个我来沟通,开发接下来这几天大家就不要回家了,马上活动上线,一起攻克难关”


产品:“我也理解大家很努力了,但是这次提测的质量确实不行,你们还是要自己多把把关,不能什么问题都等到 QA 阶段才暴露,这样不利于项目进度,需求一直都很明确,大家把 Bug 尽可能修一修,有什么问题我们及时沟通,尽快解决,敏捷开发”


开发:“。。。。”


上线前一天晚上 10 点:


开会


测试:“不行,还有 20 几个问题没有确认,这种情况我不能签字,上线了有问题谁负责。”


产品:“一些问题其实并不影响正常使用,目前主流程应该没问题,让开发把 P0 的两个问题修了之后先上线,剩下的在运营过程中逐步更新就好了,有问题让运营先收集和安抚”


开发:“上线了脏数据不好弄,会有一些账号同步的问题,而且用户等级可能还有坑”


产品:“没什么不好弄的,到时候有问题的用户让运营做个标志,接下来小步快跑修复就好了,时间不等人,明天就是上线时间, 活动上不去对老板和运营都没办法交代”


项目上线:


老板:“运营和我反馈了好多问题,你们版本上线是怎么测试的,要反思一下xxxx”


开会


产品:“我说过用户要集齐碎片和好友砍价之后才能给优惠券,等级不够的不让他们砍价成功,为什么只完成砍价的新人拿到大额优惠券?”


产品:“什么?因为账号数据绑定有 Bug ,不同渠道用户合并账号就可以满足?为什么会有这个 Bug ,测试那边没覆盖到吗?”


测试:“我不知道啊,需求文档没有说还能账号合并,而且这个功能之前没说过要限制用户等级吧?”


产品:“我出的需求肯定有,文档里肯定有,另外开发你既然知道问题,为什么不提前沟通,现在用户都消费了,这个事故你和测试 55 责,后面复盘的时候要避免这样的问题再次发生”


开发:“。。。。。”


最后


所以大家觉得是谁应该背锅?是开发的能力不够,还是测试用例的覆盖缺失?说起来,其实在此之前,我在掘金也遇到了一个 “Bug” ,比如:



文章 Markdown 里图片链接的 content-type 字段如果不符合 image/*** 规格,那么发布出来的时候链接就不会被掘金转码,所以不会有图片水印,同时直接指向了我自己的图床地址。



那么你觉得这个是 Bug 吗?明明是用户不遵循规范。但是这样不就留下漏洞了吗?



如果在文章审核通过之后,我修改图床上的图片内容,这样不就可以绕过审核在掘金展示一些「违规」内容了吗?



所以有时候一些功能的初衷是好的,但是引发的问题却又很隐蔽,那么这能怪「测试用例覆盖不到位吗」?


那么,你觉得「经过测试,为什么上线后还会那么多 Bug 」更多可能是谁的问题?


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

天生我材必有用,千金散尽还复来

入行前 我从2013-2016年在我们本地的一所中等高中读书,说是中等是因为我们本地有三所高中,我们学校升学率排第二,第一遥遥不及。 高中三年,第一年我还在重点班,当过小组长,高一结束考试要分班,那次考砸了,分到了普通班,从此堕落,班里的学习氛围极其差,跟大多...
继续阅读 »

入行前


我从2013-2016年在我们本地的一所中等高中读书,说是中等是因为我们本地有三所高中,我们学校升学率排第二,第一遥遥不及。


高中三年,第一年我还在重点班,当过小组长,高一结束考试要分班,那次考砸了,分到了普通班,从此堕落,班里的学习氛围极其差,跟大多数人一样,上课睡觉、玩手机,也就是高二那年,父亲给我买了人生中第一部手机-荣耀3C。化学老师总说我脑子可以,硬是自己把自己耽误了,现在想想,真是有点对不起各科老师。


高考结束之后填报志愿,当时年轻,总想着出去闯荡。觉得高考没考好,学一个与高中知识无关的专业,从零开始,就选择了计算机专业。还记得当时第一志愿填写的北京一所大学,第二志愿在天津。结果分数下来之后,怀着忐忑的心情去查分数,我也知道会很低,但没想到这么低。录取通知书迟迟没有下来,有点心灰意冷。


母亲当时建议我在本地学厨,我没有同意。我在网上无意间了解到北大青鸟这个机构,想去学习计算机,但父亲觉得学费有点贵,也不一定靠谱,就让在北京的阿姨去了解,了解完了之后说是培训机构,不建议我去,后来有几天处于迷离状态。不知过了多久,迟来的录取通知书终于下来了,竟是被我第三志愿河南一所专科录取。


上了大学之后,一个寝室里面就我高考分数最低,后来才了解到,当时我所报的计算机应用技术专业缺人,才补录上我,真的是惊险。


在大一跨年夜,我也跟我现在的女朋友正式“官宣”,到如今2023年已经六个多年头了。大二那年,微信小程序的横空出世,小游戏的跳一跳爆火,那时我不断去找方法学习小程序,后来了解到得先学基础HTMLCSSJavaScript,室友给我推荐了菜鸟教程我要自学网来学习,面对如此枯燥无味的代码世界,最终还是放弃了。


大三那年11月份,我们要出来实习了,那时还在纠结去郑州还是去西安,因为西安离家近点,最终选择了西安,那时候还不知道什么BOSS直聘拉钩,使用58同城在西安找视频剪辑的实习岗,我自认为在学校pr学的还不错,最终被忽悠到一家培训机构,说的我是天旋地转,学习不收钱,每个月还给1000块钱的补贴,学成之后,每个月给他们还1200(大概是这个数字,时间太久忘记了),还18个月,然后搞了个助学贷款。


班上很多同学都是零基础,相比较他们,我还是科班出身,还稍微了解了一丢丢的HTMLCSS,跟他们在一起,让我产生了优越感,对这门技术更加感兴趣,每天的课下练习都认真完成,放假也在家写代码,写一些复杂的商城首页。就这样,学习了5个月左右,回学校要开始最后的论文答辩、毕业手续了。


我是一个比较怀旧的人,很多老物件都不舍得删掉,包括我刚入行时的学习代码。分享一下当时学习的内容


image.png


大致一看,东西也不算多,其实就是带入门。


这个时候回到学校,就可以很自信的去学习微信小程序开发了。2019年6月份,我正式毕业了,属于我无忧无虑的时期结束了。


入行后


毕业之后,我继续回到西安开始找工作,这时候的我已经开始有压力了,毕竟每个月要还当初的助学贷款。


第一份工作(2019.06.? - 2019.06.?)两周


后端是Python,我工资要了6k(毕竟当时被机构洗脑了,说毕业之后最低都8k,我还是有点自知之明),最后说是试用期80%,4800,转正之后6k交社保。可谁知当初实战项目做得少,仅仅两周就被劝退了,还给了2000的工资,说实话,我都感觉没有为公司创造出一点价值。


第二份工作(2019.07 - 2020.04)9个月


7月份入职了一家电商公司,工资最后谈到了4.5,试用期给3.5,转正4.5,工作两年才有社保。公司挺大,但开发就三位,一位前端,一位后端php,一位运维,运维是一名实习生,但是挺强的。就在这家公司,让我真正的做了html+css+jquery的实战项目,业余时间还挺多,就使用vue开发一款个人博客,技术栈用到了vue2.xvue-routerelement-ui,数据全部使用的写死的假数据,也就是这个阶段,我频繁的使用csdn记录学习笔记,后来开发完之后,发现这东西开发出来也没有什么实质性的作用,开始开发后端,最开始使用的是NodeJSexpress框架,简单的写了几个增删改查,就租了台服务器,把我的域名xueshuai.top解析上去(域名在2019年去学习的时候就租了一年,刚开始还解析在FTP服务器上,仅仅部署静态页面访问),服务器装的windows server,将NodeJS接口以CMD的方式运行在服务器上,但此时,我的网站还是不怎么用,后来觉得服务器上的NodeJS运行方式有点奇怪,始终要打开CMD终端,最后了解到PM2这个东西,然后就使用PM2部署NodeJS接口,此时我也就是会用,深入还是小白。


晚上下班回去之后,又去学习uniapp,据说这个东西很强大,一套代码可以编译到多个平台,何乐而不为呢?彼时的uniapp只能编译H5AndroidIos微信小程序百度小程序,现在稍微大点的公司都有自己的小程序。


到了2020年,新型冠状病毒爆发,公司迟迟不复岗,届时的我身上还背着贷款,过完年,大年初六早早来到西安等复工。这个时候,在出租屋的我,天天写代码学习,有晚甚至写到第二天上午8点,然后睡觉。这个阶段,我了解了vue的服务端渲染NuxtJS,在Github上拉下来一个NuxtJS项目学习,然后重构个人博客第二版,直接在NuxtJS项目内新建了一个server目录编写后端接口。


直到2020年4月份,实在是扛不住了,就回公司离职了,(大部分岗位复工了,我们三个开发没有),离职证明给我写到了年前放假那天,我问他他说最近这段时间没复岗不算上班。我也没跟他纠缠,就撤了。


第三份工作(2020.04 - 2022.02)1年10个月


2020年4月10日,我入职了第三家公司,在这家公司,我独立完成了5个项目,算是长进了不少。



  1. UniApp 1个

  2. JQuery + BootStrap 4个


维护了2个项目



  1. Uniapp 1个

  2. Vue 1个


还有一个用Wap2app将一个移动端JQuery项目打包成了App


还是有满满地收获,也就是从这家公司,我才真正的算是入行。
平时也不断地学习,学了以下技术栈



  1. NodeJS的Egg框架

  2. Nginx入门

  3. MySQL入门

  4. MongoDB入门

  5. Redis入门

  6. 原生微信小程序

  7. 深入学习了Vue

  8. vueCli

  9. vueRouter

  10. axios

  11. NuxtJS

  12. 各种功能函数的封装思想

  13. Vant

  14. uviewui

  15. elementui

  16. antdesign

  17. 数据库权限设计等等


实现了以下功能



  1. web端微信授权

  2. app端微信授权登录

  3. app端微信、支付宝支付

  4. H5、app实现微信分享

  5. 条形码、二维码的生成与识别

  6. ...


是一家自研产品的公司,项目结束,老板让我学习点Php,顺便可以解决后台问题,也就顺便学习了点php的thinkPhp框架。


这个时间段我也开启了我职业生涯的首次接单,主要接了一些纯前端项目,包含以下技术栈



  1. JQuery + HTML + CSS

  2. 原生微信小程序

  3. 帮群友解决问题


由于合作很成功,也与这家公司开启了长期的合作,时至今日(2023.02.21)也还是有合作。
说到这里,你可能觉得我的个人博客已经作废了,不,你错了,一次又一次地推翻重来,这次我又开始折腾了,Nuxt2 + ElementUI 做前台,Vue2 + AntDesign做后台,EggJS + MySQL做后端,这次权限管理也会了,可以开始搞了,同时这份工作也要结束了。临走时老板还给我说可以先找工作,找到之后再提离职,愿意让我无缝衔接。


可谁料想到,第二天去面试了两家,第三天面试结果就出来了,当天就离职了,第四天去体检,第五天直接入职,而且新公司与这家公司离得很近,连吃饭的地方都没变。


第四份工作(2022.02 - 至今)


这家公司的后端是Java,使用SpringBoot一套技术栈。工作内容前期写页面调接口,后期组件封装、公共方法封装、代码审查、处理问题,前面几家都是只有我一个前端,这家还有两位同伴,也开发了我人生中第一个可视化项目,后面可视化项目一个接一个。


当然,业余时间还是不能忘记学习,又学了以下技术栈



  1. NestJS

  2. React

  3. React的服务端渲染NextJS

  4. React + AntDesign 开发一些小玩意练手

  5. TypeScript

  6. Vue3

  7. Vite

  8. Pinia

  9. moment && dayjs

  10. 养成了阅读源码的习惯



深刻明白了技术和生活的关系,好的技术可以有更好的生活,没有好的技术也可以有更好的生活。


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

如何让 ChatGPT 读懂超长保险条款?

前言 在去年年底 ChatGPT 刚火的时候我就有一个想法,它能不能帮我读一下晦涩难懂的保险条款,告诉我它到底在讲什么?到底什么病能赔多少钱?甚至能告诉我里面是不是藏有一些坑? 但是当我把条款内容复制到 ChatGPT 时,我们会发现,它直接告诉你:“太长了,...
继续阅读 »


前言


在去年年底 ChatGPT 刚火的时候我就有一个想法,它能不能帮我读一下晦涩难懂的保险条款,告诉我它到底在讲什么?到底什么病能赔多少钱?甚至能告诉我里面是不是藏有一些坑?
但是当我把条款内容复制到 ChatGPT 时,我们会发现,它直接告诉你:“太长了,它受不了”。


image.png
当我们自己打开 openai 的文档platform.openai.com/docs/api-re…),我们才明白:哦,原来它接受的最大长度是 4096tokens。但这个 一个 token 到底是多长呢?暂时还不知道,反正就是有这么个上限。很显然,我们的保险条款远远的超过了它的上限,因为我才复制两三页的内容它就 Error 了。
image.png
但我们还是纳闷,不应该啊,ChatGPT 不应该很强吗?它的官方例子可是摆了几十个案例,看网上的各种文章,它似乎在文字与编码领域,远超绝大数人类,怎么会连个保险条款都无法承受。
image.png


我想从这个案例中看看有没有其他路子,可惜确实没有合适的案例能解决我这种超长文本的诉求。于是我停止了这个想法,并先回家过了个快乐的新年。


但在最近,在我的不屑但可能没啥意义的努力下,我几乎完成了这个想法。先放几个截图给大家看看。


问蚂蚁爆款「好医保长期医疗」几个问题的答案:
image.png
问市面上很火的「达尔文7号重疾」的问题及答案:
image.png
如果你仔细看,你会发现,它已经能非常准确的回答这几个很多保险小白常问的问题了。
那我到底是怎么实现的呢?这篇文章来一探究竟。


先纠正一下


在我开始正文之前,先让 ChatGPT 跟大家做个简单介绍。
image.png
所以本文标题其实不对,准确说应该是「如何让 openai 的 API 帮我读懂保险条款」。因为我其实是调用了 openai 提供的 API 能力来满足需求的。更准确来说是调用了其 GPT-3 的一些模型,而不是挂代理直接问 ChatGPT。但为了大部分读者容易理解,就先取一个不恰当的标题了。


后文中,我将会以 GPT 通用指代我所调用的 openai 的 API 服务。


核心解决方案


话说在新年回来后,ChatGPT 仍愈演愈烈,因此我又来了点儿兴趣,并尝试把 GPT 接入我一个年久失修的个人公众号。就在这个接入的过程中,为了解决接入遇到的不少问题,我看了不少文档。果然是开卷有益,实干兴邦啊。过程中我又接触学习了一些有用知识。其中最重要的是两个点知识:


其一是:GPT 的多轮对话是如何实现的? 其实很简单,就是把历史对话都存起来,然后按照时序重新拼接,再加上这次的问题,合并一起作为 prompt再传给 GPT 即可。


其二就是,如何让 GPT 理解超长文本知识并做问题回答? 我在逛 openai 官方文档的时候,发现了其实人家早早就想到了这个问题,并贴心的准备好了教程文档。这在我上一篇 ChaGPT 的文章中也已提到:
公众号如何接入 ChatGPT 及 一些感想




  1. How to build an AI that can answer questions about your website:platform.openai.com/docs/tutori…

  2. Question Answering using Embeddings:github.com/openai/open…



它的思路其实很好理解,详细来说,主要是分几步:




  1. 先将巨量的文档知识拆块,并使用 openai 提供的Embeddings能力将该部分内容向量化,并做映射存储。向量化的目的是为了做两部分文本的相似性匹配。关于它的文档在这:platform.openai.com/docs/guides…




  2. 当用户提问时,将用户的「提问文本」也做向量化。




  3. 遍历已拆块并向量化的文档内容,将之与向量化后的「提问文本」做内容相似性比较,找到最为相似的文档内容向量。




  4. 根据之前的映射关系,找到这段「向量」映射着的原始文档内容块。并把这个内容块作为上下文传给 GPT。




  5. GPT 会根据这段上下文回答用户的提问。




原来如此,那么我只要把保险条款分段向量化,再根据用户提问匹配到相应的那段内容再回答不就好了吗。简单,上手吧。


把大象放进冰箱需要几步?


这个问题似乎正如「把大象放入冰箱」。描述起来很简单,真正要做起来就举步维艰。


在我们面前最大的问题就是,到底怎么把这个文档做分割?


最简单的方案自然是,把保险条款按页码一页一页分块,如果一页内容也超了,那我们就半页半页分块。 但这忽略了一个最大的问题,就像大象的各个器官并非水平均分分布一样,知识内容并非是按页码分割的。一个知识可能第三页正好起了个标题,第四页才是详细的描述。而向量化匹配的时候,却可能只匹配到第三页的内容。比如这个「好医保长期医疗」的责任免除条款,就很容易丢失下半部分的免除责任,造成回答准确性降低。
image.png
除此外,这样的分割还容易让 GPT “学坏”。因为粗暴的按页分割,很容易把无关的知识传给 GPT,导致它可能会因为这些无关的信息返回错误的答案。比如如下关于用户信息告知的条款:
前一页内容如下:
image.png
后一页内容如下:
image.png
如果你询问的问题是:“如果投保时年龄填写错误,理赔时会怎么样”。
那很有可能你只会将第一页内容传给 GPT,它将会告诉你保司不承担任何责任,并不退回保险费。
image.png
而用我实现的服务所拼接的知识块,得到的版本答案如下:
image.png
显然这个问题得到了准确回答。


以上两个案例比较生动的说明了分割的重要性。


如何分割文档



懂得了很多道理,也依旧过不好这一生。 - ChatGPT也不知道是谁说的



如何分割文档?其实这个也很好想方案,只是比较难搞。
保险条款是有文章结构的,只要咱们可以按文章标题给文档做结构化就好了。 最终文档就会成为这样的一个文档树:


interface INode {
title: string;
content: string;
children: INode[]
}

type DocTree = INode[]

然后我们在深度遍历这个文档树,去识别每个节点所包含的所有内容的长度,达到一定阈值就剪下来作为一个「知识块」。这就像剪一个西兰花 🥦,按自己可以含进去的大小,一朵朵剪下来。


1.gif


通过这样的手段,我们就能在满足知识文本长度的限制下,切下最为连续完整的知识内容。 这其实很简单,但如果一定要装逼取个算法名的话,那我称之为:西兰花算法。


但在我们切割西兰花之前,还有一个棘手的问题,怎么把一个条款文档先变成一棵西兰花(一颗文档树)?


第 0 步:先明白tokens咋回事


因为后文很多内容都跟这个tokens相关,所以我必须得提前介绍一下。
有时间的同学可以直接看官网介绍文档:
没时间的同学可以继续听我简单总结一下:



  1. tokens 不是指 prompt 字符串的长度;

  2. token指的是一段话中可能被分出来的词汇。比如:i love you,就是三个token,分别为 「i」「love」「you」。

  3. 不同语言token计算不一样,比如中文的「我爱你」其实是算 5 个 token,因为它会先把内容转成 unicode。读过我公众号那篇文章的同学,你们就会知道,有些 emoji 的token长度会超出你的想象。image.png

  4. 你可以用这个网站在线体验你的文字的token长度:platform.openai.com/tokenizer

  5. node.js环境中,你可以用 gpt-3-encoder 这个 npm 包来计算 tokens的长度。


OK,掌握这些知识就足够理解我们后文的内容了。


第 1 步:标题的识别


我们可以先看看市面比较火爆的医疗与重疾险产品的条款。发现其实保险大部分条款是有一定格式标准的。几乎都是嵌套数字标题 + 内容。那是否可以依据一定的规则,识别出那部分是标题,然后根据标题做切割即可?比如说,根据 「数字 + ·? + 数字?」的正则做匹配。
虽然我正则写不来,但是 ChatGPT 写的来呀


image.png


const text = '1 React 1.1 react hooks 的使用技巧 1.2 react suspense 的作用 2 Vue 2.1 Vue compostion api 的使用技巧';
const regex = /(\d+\.?\d*)\s(\w+)/g;
const matches = text.matchAll(regex);

const result = [];

for (const match of matches) {
result.push(match[1] + ' ' + match[2]);
}

console.log(result);
// output
['1 React', '1.1 react', '1.2 react', '2 Vue', '2.1 Vue']

虽然它的回答不够完美,但是基本够我们继续下一步编码了。于是我尝试把 PDF 的全文内容复制出来,并做分割。然后我就会发现几个很麻烦的地方:



  1. 数字不是只在标题中出现,正文中也很容易出现各种数字。

  2. 有些注释内容,也有数字+内容的出现


image.png
所以我们复制出来的文本是这样的:


module.exports = `2.3 等待期
自本合同生效(或最后复效)之日起 90 日内,被保险人因意外伤害4以外的原因, 被保险人因意外伤害发生上述情形的,无等待
被确诊患有本合同约定的轻症疾病5、中症疾病6、重大疾病7的,我们不承担保险
责任,这 90 日的时间称为等待期。
期。
轻症疾病 中症疾病
重大疾病
本合同的保险责任分为基本部分和可选部分。
,本合 ,退还
等待期内,我们的具体做法见下表:
等待期内发生的情形
我们的做法
不承担本合同“2.4 保险责任”中约定的保险责任
同继续有效
不承担本合同“2.4 保险责任”中约定的保险责任
您已交的本合同保险费8(不计利息),
本合同终止
2.4 保险责任
1 保单生效对应日:本合同生效日每年(或半年、季、月)的对应日为保单年(或半年、季、月)生效对应日。若当月 无对应的同一日,则以该月最后一日为保单生效对应日。
2 保单年度:自本合同生效日或年生效对应日零时起至下一个年生效对应日零时止为一个保单年度。
3 保险费约定交纳日:分期交纳保险费的,首期保险费后的年交、半年交、季交或月交保险费约定交纳日分别为本合同
的保单年生效对应日、半年生效对应日、季生效对应日或月生效对应日。`

所以,如果只是粗暴的根据某种标题规则来做分割,那我们只会得到错乱的结果。


那我们人眼是如何从页面中知道它是标题的呢?我们自然是根据这个文案的位置、大小,综合了我们的历史经验来判断它是不是标题。也就是说,要想真正从一段文本中做很好的标题识别以及内容分割,必须要获取这段文本的其他元数据。


我的下意识,自然是希望还有 AI 的能力。我把 PDF 转图片,都传给某个 AI,它很聪明,帮我 OCR 识别文档并做好了充分的文档结构化。


但我在 openai 官网并没有找到这样的 api 能力提供。由于我的 AI 储备非常薄弱,我也很难在网上找到可以满足我诉求的开源工具。而且根据我很可能不成熟的感觉,我感觉现在训练出来的开源 AI 模型,顶多只是识别出文字以及文字所在的绝对位置,也很难帮我直接把文档给按照标题结构化了。真有这样的需求,可能需要我自己准备大量材料来训练。这似乎再一次难倒了我。


于是我又想到了 pdf.js这个工具。我们 C端 部分投保协议就是利用这个工具包,把 PDF 文档转成 DOM 渲染到页面上。虽然我之前并没有使用过,但我相信它肯定可以拿到 PDF 上很多元数据,否则不可能做到还原成 DOM 去渲染。我甚至想,它有没有可能直接帮我转成一颗 根据标题已经结构化好的 DOM 树。


在我使用pdf.js后,我发现,刚才稍微想的有点多了,但也足够用了。它能把 PDF 文档的文字块以及这个文字块的文字与大小信息 解构出来。比如这样:


[{
"str": "2.4",
"dir": "ltr",
"width": 13.2,
"height": 10.56,
"transform": [10.56, 0, 0, 10.56, 346.03, 285.05],
"fontName": "g_d0_f1",
"hasEOL": false,
"pageNum": 4
},
{
"str": " 保险责任",
"dir": "ltr",
"width": 42.24,
"height": 10.56,
"transform": [10.56, 0, 0, 10.56, 364.39, 285.05],
"fontName": "g_d0_f12",
"hasEOL": false,
"pageNum": 4
}]

其中的 widthheight决定了文字块的大小,transform决定了文字块在文档上的绝对位置信息。pdf.js也是根据这些信息,把 PDF 内容以绝对位置与大小一个个的转成 DOM 并绘制在网页上。它不理解前后语序与内容结果,它只是粗暴的拼装。


但这对我来说已经够用了,有了这些信息,我就能分析出哪些文字块是标题,哪些文字块是正文的正常数字,哪些内容块是底部的注释内容。比如说:



  1. 出现最多的字体大小,有理由相信这就是正文字体大小

  2. 持续出现的一个很靠左的 X 坐标,且该坐标内容基本是数字,有理由相信,这就是数字标题或数字注释所在的 X 坐标

  3. 虽然符合上述第二条规则,但却比正文字体小很多,有理由相信,这是注释前的数字


等等等等吧,除此外,我们还需要判断什么时候到注释内容,什么是页码内容。因为这些内容都要做一些特殊处理。另外就是不同文档可能有些特殊的边界情况要处理一下。


虽然说这依旧很人肉,不智能,但至少能把路走通了。至于有些不是以 x.x.x 这样的数字做标题的文档,比如:第一章、第一节什么的,还是能拓展的,但就先不考虑了。


第 2 步:过长内容摘要化


事情走到这一步,大问题就没有了。但实际应用的时候,我们还是会发现一个小问题,就是很多小节的内容其实比较长,我们能做相似性映射的知识块其实往往不仅一块。当我们拼接多块知识的时候,内容又超出了。而如果我们只拼接一块内容,知识又不够完整。这又让我们抓耳挠腮了。


我仔细看了看这些小节的内容,我觉得,其实这段文本,要是用文言文来说,可能还可以再短一点(汉语真是博大精深)。但是我觉得如果让 GPT 帮我把它转成文言文的话,用户提问的问题很可能就映射不到了。当然,我也真的试了一下,发现 text-davinci-003这个模型似乎在文言文领域也不太行,保险条款它很难转成文言文。


但我有了另外一个思路,就是保险条款其实废话还是有些多的,我可以让 GPT 帮我做一些摘要性的总结,且尽量不丢失最核心的有效知识。在我网上搜索这块相关的知识时,发现 NLP 领域有一种叫「命名实体识别baike.baidu.com/item/%E5%91…)」的技术,常用于搜索引擎、信息提取、问答系统中。不管三七二十一了,openai 这么强大,那我就这么让它帮我这么做吧。


async function getSummary({ content, tokenLength }) {
const promptContext = `'''{{content}}'''基于命名实体识别构建内容摘要:`;
const contentTokenLength = tokenLength || encode(content).length;
const promptContextTokenLength = encode(promptContext).length;

const completion = await openai.createCompletion({
model: 'text-davinci-003',
prompt: promptContext.replace('{{content}}', content),
// 1000 ~ 4096,最大也不能超过1000
max_tokens: Math.min(
4096 - contentTokenLength - promptContextTokenLength,
1000,
),
temperature: 0,
});

return strip(completion?.data?.choices?.[0].text, ['\n']);
}

实际测试下来,这样的方式相比直接总结摘要,从最终效果来看,返回的结果会稳定很多,且返回的知识不会只说到一半。具体原因也不懂,有资深的大佬可以帮忙指点一下。


经过这样摘要化以后,我们就能把一段较长的知识文本给有效缩短。当用户问起相关知识时,可以调用更多的知识块来回答用户。


第 3 步:超长内容极限压缩


事情走到这一步,你可能以为就真没啥问题了。但实际上我们又遇到了个小麻烦。就是有部分小节的内容依旧还是太长了。就像一颗基因变异的西兰花 🥦。


image.png
我已经剪到最小的分支了,但这个最小的分支依旧超过了max_tokens的限制。这又难倒我了,现在我该怎么分割它?这似乎回到了我最开始遇到的问题。
不过好在,这些变异的西兰花并没有动画灵能百分百中的那么夸张,大部分还只是 略超 max_tokens一些,几乎不会超过其两倍。而自己观察这些超出去的内容,往往是两种类型。



  1. 较长的表格,比如药品列表,如下图1。

  2. 一些责任或疾病的详细介绍,如下图2。


image.pngimage.png
我们发现这些小节的内容,其实并不适合分割。比如药品列表要是分割成两块接近max_tokens的知识内容,一次性问答只能获取其中一块知识。这就会导致回答错误。比如你问有多少种药品可以报销,它自然会算错。责任也是一样。


但这些小节有另外一个方向,就是压缩内容。里面有很多文字其实是相似的,比如一堆的社保目录内/外。比如责任内容中频繁出现的:恶性肿瘤``保险金``被保险人等等。我们只要做一本字典,把这些很长的重复性文字,用另外一种特殊的较短的字符指代。这段长文本就会瞬间被压缩到较短的文本,我们再连同字典一起发给 GPT,让它再翻译回来并做摘要化,于是就绕过了max_tokens的限制。


但问题又来了,说的容易,代码怎么知道哪些文字是一段词语?如果代码不知道哪些文字是一段词语,又怎么做字典映射。总不能自己先把所有可能的词汇都预先想好吧。虽然保险有一些专业术语可以提前预设,但总归有更多的未知的。


这就引出了 NLP 领域的另外一门技术,分词。很开心的是,在中文领域,且在 node.js 生态中,有一个比较好用的分词工具「结巴分词-github.com/yanyiwu/nod…」。 不出意外,这也是 ChatGPT 告诉我的。


image.png
运用这个结巴分词,我们就可以把一段内容分割成一个个词汇,同时也支持传入用户预设的词汇字典。这样我们就能知道哪些词汇在一段文本中被重复使用多次。对于这些词汇,我们再用一个最短的字符去映射它。


const nodejieba = require('nodejieba');

nodejieba.load({
userDict: './userdict.utf8',
});

const longText = '相学长白天吃饭,相学长中午也吃饭,相学长晚上还吃饭';

const words = nodejieba.cut(longText);

console.log(words);

// output
['相学长','白天','吃饭',',','相学长','中午','也','吃饭',',','相学长','晚上','还','吃饭'];

为了映射的字符尽量的短,我也是挠了一下脑袋,本来最简单就是一个特殊字符加上从1递增的数字就好了,比如这样:*${index}。但是这个方式经过我实测,压缩完的tokens效果还不够极致。考虑到我们都是基本是中文环境,我最终选择了 26个字母大小写 + 24个拉丁字母大小写作为索引:
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZαβγδεζηθικλμνξοπρστυφχψωΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩ



根据第 0 步的知识,我们知道,千万别用 emoji 去做字典索引。



这样我们就得到最多100个索引,当然如果内容中已有出现具体的字母,最好还是针对该段内容剔除该字母。经过实际测试,这样的压缩效果会比数字映射法稍微好一些。且经过实测,这样问 openai 依旧能得到正确答案。举个例子:
上文中的,相学长白天吃饭,相学长中午也吃饭,相学长晚上还吃饭
会被转化成,a白天b,a中午也b,a晚上还b|上文中,a:相学长,b:吃饭
我们把这句话拿去问 GPT:相学长每天都在做什么。它能给出正确的回答:相学长每天都在吃饭


image.png
除了字典法压缩外,其实还有一个也比较显著的手段。就是把全角字符全部转成半角字符。在我的实际测试中,一段 8247 个tokens长度的内容。换半角相比不换半角,能多压缩 580 个tokens,简直是效果惊人!


其实不仅仅超过max_tokens的文本需要压缩。我建议超过 3000 tokens的文本都得压缩一下。因为 openai 最大的 4096 个token限制。并非是限制 prompt。而是限制 prompt+ 它的答案。也就是说,当我们做摘要化的时候,如果我们提供的原始内容越长,它能返回的摘要就越短。这显然不符合我们的诉求。所以,虽然文章中这里写着是第三步,但实际操作时,压缩其实是第二步,压缩需要在摘要化之前。


也是因为max_tokens的计算涵盖了 GPT 的回答内容,所以当我们根据用户提问拼接知识块的时候,不能按照 max_tokens的限制去打满内容,尽量留出 几百到一千的 tokens给 GPT 做回答。


在我实操过程中呢,其实还存在一个文档的内容,怎么压缩也压缩不到预期的长度。我确实选择了逃避,因为这段内容是无数个疾病的详细介绍,我骗自己说这些详细介绍并没太大用。因此最终我做了一个特殊处理,如果是这个超长的疾病介绍,我就只保留了疾病标题,去掉了疾病的内容。


针对这种,再压缩也解决不了的问题,我目前确实还没找到非常好的解法。


最终经过我们对 PDF 文档的分割、压缩、小节内容摘要化、转成嵌套文档树,最终再上一个西兰花算法。我们就能完成对这个 PDF 文档的合理分割了。最终我们再把分割后的内容做向量化处理,就能实现一个比较好的基于超长保单文档的保险产品问答服务。


其实其他领域的文档也差不多,只要这个文档结构比较好切割。


代码已开源


相关代码开源,有兴趣的同学自己下载继续研究吧~ github.com/wuomzfx/pdf…
关于到底怎么做向量化、怎么做匹配,我在本文就不多说了,这个还是比较容易了。包括其他还有一些特殊的处理,比如怎么把注释内容拼接到正文里。这些都可以在源码中方便寻找到。其他可能还稍微需要一点工具知识的,就是 node 中如何做两个 embedding 向量的相似性匹配。用 @stblib/blas这个 npm 包就行。DEMO 示例:


const ddot = require('@stdlib/blas/base/ddot');

const x = new Float64Array(questionEmbedding);
const y = new Float64Array(knowledgeEmbedding);

const result = ddot(x.length, x, 1, y, 1),

如果还有哪里不明白的,欢迎评论区或者先尝试问下 ChatGPT~


最后一点小感悟


感觉人工智能的时代真的要到来了,连我这种 AI 小白,似乎都已经能完成一个可能真的能投入使用的服务。我再整个小程序,糊个页面,把一些异常容错机制再完善完善。再稍微整个爬虫,从保险行业协会网站帮用户快捷找到相关的保险条款。我几乎就能实现一个帮助用户回答保险产品的应用了。


亦或者,我可以自己预设一些问题。通过这些问题,我可以从保险条款中结构化出很多有效的信息,比如保额保费、责任细节、投保年限、续保年限等等。结构化之后,我又可以直接做不同产品的对比,根据用户的要求推荐比较合适的保险产品。这是一件挺有可能的事情,我尝试把之前的两个问答作为对比再次问 GPT 推荐哪款产品,它的回答比较中肯且有用。


image.png


总之,新的 AI 基础设施,已经能成为现在大部分工程师的有利工具。在某些垂直领域做一些深入研究,通过这些工具,AI 就能发挥出意想不到的作用,我们可以快速的产出各种有意思的产品。就好像 HTML5 跟 小程序 带来一系列有意思的 轻量APP 一样。相信,AI 浪潮在这两年就要席卷而来了~~


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

ChatGPT火了,我连夜详解AIGC原理,并实战生成动漫头像

一、AIGC:人工智能的新时代AIGC可能会是人工智能的下一个时代。尽管很多人还不知道AIGC是什么。当还有大批人宣扬所谓人工智能、元宇宙都是概念,并且捂紧了口袋里的两百块钱的时候,人工智能行业发生了几件小事。首先,由人工智能生成的一幅油画作品《太空歌剧院》,...
继续阅读 »

一、AIGC:人工智能的新时代

AIGC可能会是人工智能的下一个时代。尽管很多人还不知道AIGC是什么。

当还有大批人宣扬所谓人工智能、元宇宙都是概念,并且捂紧了口袋里的两百块钱的时候,人工智能行业发生了几件小事。

首先,由人工智能生成的一幅油画作品《太空歌剧院》,获得了艺术博览会的冠军。

有人感觉这有什么?各种比赛多了去了,不就是获个奖吗?

可是这次不一样,这是一幅油画作品。在此之前,好的油画只能由人工绘制。但是现在人工智能也可以绘制了,而且还拿了冠军。

很多人类艺术家仰天长叹:“祖师爷啊,我这代人,在目睹艺术死亡!

上一次艺术家们发出这样的感慨,还是1839年,那时照相机问世了。

随后,ChatGPT横空出世。它真正做到了和人类“对答如流”。

它也可以做数学题、创作诗歌、写小说,甚至也能写代码、改bug。

再说一个震惊的报道:由ChatGPT生成的论文,拿下了全班的最高分。导师找到学生,说他上交的论文,段落简洁、举例恰当、论据严谨,甚至引经据典,古今中外,无所不通,教授不敢相信。学生瑟瑟发抖,他说,这是AI生成的,我只是想应付一下作业

另外,美国89%的大学生都在用ChatGPT做作业。以色列总统在周三发表了一个演讲,内容也是由人工智能写的。

现在全球都在讨论,这类人工智能技术,看似是带来了巨大的商业价值,实则可能会给人类带来严重的打击。

这项技术就是AIGC(AI-Generated Content),翻译成中文就是:人工智能生成内容

二、AIGC实战:智能生成动漫头像

其实,利用人工智能生成内容资源,很早就有了。记得有一年的双十一购物节,上万商家的广告图就是人工智能生成的。只是现在的数据、算法、硬件,这三个条件跟上了,这才让它大放异彩,全民可用。

下面,我就以人工智能生成动漫头像为例,采用TensorFlow框架,从头到尾给大家讲一下AIGC的全过程。从原理到实现都很详细,自己搭建,不调API,最后还带项目源码的那种

2.1 自动生成的意义

那位问了,自动生成内容有什么好处?我的天啊,省事省力省钱呐!

下图是一个游戏中的海洋怪物。这便是人工智能生成的。

这个大型游戏叫《无人深空(No Man's Sky)》。号称有1840亿颗不同的星球,每个星球都有形态各异的怪物。这游戏玩着得多爽啊?简直就是视觉震撼呐。这些怪物要是人工来做,得招聘多少团队,得花费多少时间?

用人工智能生成的话,你可以像去网吧一样,跟老板说:嗨,多开几台机子

当然,下面我要做的,没有上面那样地绚丽,甚至很原始。

但是过程类似,原理一致。效果就是AI生成动漫头像:

2.2 自动生成的原理

AIGC的原理,用中国古话可以一语概括,那就是:读书破万卷,下笔如有神

以生成猫咪的照片来举例子,基本上AIGC的套路是下面这样的:

首先,程序会设计两个角色。一个叫生成器,一个叫鉴别器。

为了便于理解,我们称呼生成器为艺术家,称鉴别器为评论家。

艺术家负责生产内容,也就是画猫。不要觉得拥有艺术家头衔就很了不起,他可能和你一样,画不好。但是,就算乱画,也得画。于是,他就画啊画啊画。

评论家呢,相比艺术家就负责一些了。他首先调研了大量猫的照片。他知道了猫的特点,有俩眼睛,有斑纹,有胡须。这些特征,他门儿清。

下面有意思的就来了

艺术家这时还啥也不懂,随便画一笔,然后交给评论家,说画好了。评论家拿旁光一看,瞬间就给否了。还给出一些意见,比如连轮廓都没有。

艺术家一听,你要轮廓那我就画个轮廓。他加了个轮廓,又交了上去。评论家正眼一看,又给否了。不过,他还是给出一些意见,比如没有胡须。

就这样,这俩人经过成千上万次的友好磋商(评论家幸好是机器,不然心态崩了)。到后来,艺术家再拿来画作,评论家会看好久,甚至拿出之前的照片挨个对照。最后他甚至还想诈一下艺术家,说你这是假的,艺术家说这次是真的。这时,评论家说好吧,我确实找不出问题了,我看也是真的

至此,剧终。

搞一个造假的,再搞一个验假的。然后训练。随着训练加深,生成器在生成逼真图像方面逐渐变强,而鉴别器在辨别真伪上逐渐变强。当鉴别器无法区分真实图片和伪造图片时,训练过程达到平衡。

上面这一套操作叫“生成对抗网络(Generative Adversarial Networks)”,简称叫GAN。我感觉,这套流程有点损,叫“干”没毛病。

2.3 数据准备

鉴别器是需要学习资料学习的。因此,我准备了20000张这样的动漫头像。

这些数据来自公开数据集Anime-Face-Dataset。数据文件不大,274MB。你很容易就可以下载下来。这里面有60000多张图片。我用我的电脑训练了一下。200分钟过去了,一个epoch(把这些数据走一遍)都还没有结束。那……稍微有效果得半个月之后了。

乡亲们,我这里是AI小作坊,干不了大的。于是乎,我就取了20000张图片,并且将尺寸缩小到56×56像素,再并且将彩色改为黑白。这样一来,效率马上就提高了。2分钟就可以训练一圈。如此,我训练500圈也就是不到一天的时间。这是可以承受的。

上面处理图片的代码:

import cv2
# 存放源图片的文件夹
dir_path = "anime"
all_files=os.listdir(dir_path)
# 循环里面的每一个文件
for j,res_f_name in enumerate(all_files):
res_f_path = dir_path+"/"+res_f_name
# 读入单通道
img1 = cv2.imread(res_f_path, 0)
# 重新定义尺寸为56
img2=cv2.resize(img1,(56,56),interpolation=cv2.INTER_NEAREST)
# 转存到face文件夹下
cv2.imwrite("face/"+res_f_name, img2)
# 超过20000退出循环
if j > 20000: break

相信加上注释后,还是通俗易懂的。

文件准备好了。尽管维度降了,但看起来,这个辨识度还过得去。

下一步要转为TensorFlow格式化的数据集。

from PIL import Image
import pathlib
import numpy as np

# 将图片文件转为数组

dir_path = "face"
data_dir = pathlib.Path(dir_path)
imgs = list(data_dir.glob('*.jpg'))
img_arr = []
for img in imgs:
img = Image.open(str(img))
img_arr.append(np.array(img))
train_images = np.array(img_arr)
nums = train_images.shape[0]

train_images = train_images.reshape(nums, 56, 56, 1).astype('float32')
# 归一化
train_images = (train_images - 127.5) / 127.5
# 转为tensor格式
train_dataset = tf.data.Dataset.from_tensor_slices(train_images).shuffle(nums).batch(256)

我很想说一下数据形态的变化过程。因为这和后续的神经网络结构有关联。

首先,我们的图片是56×56像素,单通道。所以,图片的数据数组img_arr的形状是(20000, 56, 56)。也就是说有20000组56×56的数组。这里面的数是int型的,取值为0到255,表示从纯黑到纯白。

((20000, 56, 56),
array([[ 0, 0, 0, 0, 0, …… 0],
[ 18, 18, 126, 136, 175, …… 0],
[ 0, 0, 253, 253, 0, …… 0]], dtype=uint8))

然后用reshape做一个升维,并且用astype('float32')做一个浮点转化。

升维的目的,是把每一个像素点单独提出来。因为每一个像素点都需要作为学习和判断的依据。浮点转化则是为了提高精确度。

到这一步train_images的形状变为(20000, 56, 56, 1)

((20000, 56, 56, 1),
array([[ [0.], [0.], [0.], [0.], [0.], …… [0.]],
[ [18.], [18.], [126.], [136.], [175.], …… [0.]],
[ [0.], [0.], [253.], [253.], [0.], …… [0.]]], dtype=float32))

接着,进行一个神奇的操作。执行了(train_images-127.5)/127.5这一步。这一步是什么作用呢?我们知道,色值最大是255,那么他的一半就是127.5。可以看出来,上一步操作就是把数据的区间格式化到[-1,1]之间。

如果你足够敏感的话,或许已经猜到。这是要使用tanh,也就是双曲正切作为激活函数。

这个函数的输出范围也是在-1到1之间。也就是说,经过一系列计算,它最终会输出-1到1之间的数值。这个数值我们反向转化回去,也就是乘以127.5然后加上127.5,那就是AI生成像素的色值。

2.4 生成器

首先我们来建立一个生成器。用于生成动漫头像的图片。

def make_generator_model():
model = tf.keras.Sequential()
model.add(layers.Dense(7*7*256, use_bias=False, input_shape=(160,)))
model.add(layers.BatchNormalization())
model.add(layers.LeakyReLU())

model.add(layers.Reshape((7, 7, 256)))
assert model.output_shape == (None, 7, 7, 256)
model.add(layers.Conv2DTranspose(128, (5, 5), strides=(1, 1), padding='same', use_bias=False))
assert model.output_shape == (None, 7, 7, 128)
model.add(layers.BatchNormalization())
model.add(layers.LeakyReLU())
……
model.add(layers.Conv2DTranspose(1, (5, 5), strides=(2, 2), padding='same', use_bias=False, activation='tanh'))
assert model.output_shape == (None, 56, 56, 1)

return model

# 生成一个试试
generator = make_generator_model()
noise = tf.random.normal([1, 160])
generated_image = generator(noise, training=False)

因为我最终会放出全部源码,所以这个地方省略了几层相似的神经网络。

从结构上看,输入层是大小为160的一维噪点数据。然后通过Conv2DTranspose实现上采样,一层传递一层,生成变化的图像。最终到输出层,通过tanh激活函数,输出56×56组数据。这将会是我们要的像素点。

如果输出一下,生成器生成的图片。是下面这个样子。

这没错,一开始生成的图像,就是随机的像素噪点。它只有一个确定项,那就是56×56像素的尺寸。

这就可以了。它已经通过复杂的神经网络,生成图片了。这个生成器有脑细胞,但刚出生,啥也不懂。

这就像是艺术家第一步能绘制线条了。如果想要画好猫,那就得找评论家多去沟通。

2.5 鉴别器

我们来建立一个鉴别器。用于判断一张动漫头像是不是真的。

def make_discriminator_model():
model
= tf.keras.Sequential()
model.add(layers.Conv2D(64, (5, 5), strides=(2, 2), padding='same', input_shape=[56, 56, 1]))
model.add(layers.LeakyReLU())
model.add(layers.Dropout(0.3))

model.add(layers.Conv2D(128, (5, 5), strides=(2, 2), padding='same'))
model.add(layers.LeakyReLU())
model.add(layers.Dropout(0.3))

model.add(layers.Flatten())
model.add(layers.Dense(1))

return model

# 鉴别上一个生成的噪点图片generated_image试试
discriminator = make_discriminator_model()
decision = discriminator(generated_image)

我们来看一下这个模型。它的输入形状是(56, 56, 1)。也就是前期准备的数据集的形状。它的输出形状是(1),表示鉴别的结果。中间是两层卷积,用于把输入向输出聚拢。采用的是LeakyReLU激活函数。

我们把生成器生成的那个噪点图,鉴别一下,看看啥效果。

tf.Tensor([[0.00207942]], shape=(1, 1), dtype=float32)

看这个输出结果,数值极小,表示可能性极低。

我们只是建立了一个空的模型。并没有训练。它这时就判断出了不是动漫头像。倒不是因为它智能,而是它看啥都是假的。它现在也是个小白。

下面就该训练训练了。

2.6 训练数据

开练!GAN!

cross_entropy = tf.keras.losses.BinaryCrossentropy(from_logits=True)

def discriminator_loss(real_output, fake_output):
real_loss = cross_entropy(tf.ones_like(real_output), real_output)
fake_loss = cross_entropy(tf.zeros_like(fake_output), fake_output)
total_loss = real_loss + fake_loss
return total_loss

def generator_loss(fake_output):
return cross_entropy(tf.ones_like(fake_output), fake_output)

……
@tf.function
def train_step(images):

noise = tf.random.normal([BATCH_SIZE, noise_dim])

with tf.GradientTape() as gen_tape, tf.GradientTape() as disc_tape:
generated_images = generator(noise, training=True)

real_output = discriminator(images, training=True)
fake_output = discriminator(generated_images, training=True)

gen_loss = generator_loss(fake_output)
disc_loss = discriminator_loss(real_output, fake_output)

gradients_of_generator = gen_tape.gradient(gen_loss, generator.trainable_variables)
gradients_of_discriminator = disc_tape.gradient(disc_loss, discriminator.trainable_variables)

generator_optimizer.apply_gradients(zip(gradients_of_generator, generator.trainable_variables))
discriminator_optimizer.apply_gradients(zip(gradients_of_discriminator, discriminator.trainable_variables))

for epoch in range(500):
for image_batch in dataset:
train_step(image_batch)

同样,我还是只放出了部分关键代码。不然影响你的阅读。最后我会开源这个项目,不要着急。

我们来分析原理,一定要反复看,精彩和烧脑程度堪比《三国演义》。我连图片都不敢加,怕打断你的思绪。

首先看损失函数。

算法训练的一个途径,就是让损失函数的值越变越小。损失函数表示差距,预测的差距和实际差距缩小,表示预测变准。

先看一下生成器的损失函数。位置在代码中的generator_loss部分。它返回两个数据之间的差距。第一个数是造假的结果fake_output,这个结果是鉴别器给的。另一个数据是标准的成功结果。随着训练的进行,算法框架会让这个函数的值往小了变。那其实就是让生成器预测出来的数据,同鉴别器判断出来的结果,两者之间的差距变得越来越小。这一番操作,也就是让框架留意,如果整体趋势是生成器欺骗鉴别器的能力增强,那就加分。

再看鉴别器的损失函数。也就是代码中的discriminator_loss函数。它这里稍微复杂一些。我们看到它的值是real_lossfake_loss,是两项损失值的总和。real_lossreal_output和标准答案的差距。fake_lossfake_output和标准答案的差距。

那这两个值又是怎么来的呢?得去train_step函数里看。real_output是鉴别器对训练数据的判断。fake_loss是鉴别器对生成器造假结果的判断。看到这里,我感叹人工智能的心机之重。它什么都要。

随着大量学习资料的循环,它告诉人工智能框架,它要锻炼自己对现有学习材料鉴别的能力。如果自己猜对了学习资料,也就是那20000张动漫头像。请提醒我,我要调整自己的见识,修改内部参数。代码中定义的training=True,意思就是可随着训练自动调节参数。

同时,伴着它学习现有资料的过程中,它还要实践。它还要去判断生成器是不是造假了。它也告诉框架,我要以我现在学到的鉴别能力,去判断那小子造的图假不假。

因为人工智能要想办法让损失函数变小。因此得让fake_loss的值变小,才能保证discriminator_loss整体变小。于是,框架又去找生成器。告诉它,鉴别器又学习了一批新知识,现在人家识别造假的能力增强了。不过,我可以偷偷地告诉你,它学了这个还有那个。这么一来,生成器造假的本领,也增强了。

如此循环往复。框架相当于一个“挑唆者”。一边让鉴别器提高鉴别能力,一边也告诉生成器如何实现更高级的造假。最终,世间所有的知识,两方全部都学到了。鉴别器再也没有新的知识可以学习。生成器的造假,鉴别器全部认可,也不需要再有新的造假方案。所有防伪知识全透明

这时AIGC就成功了。

2.7 自动生成

我对20000张动漫图片训练了500轮。每一轮都打印一个九宫格的大头贴。最终我们可以看到这500轮的演变效果。这张图大约25秒,只播放一遍(如果放完了,拖出来再看),需要耐心看。

从动态图看,整体趋势是往画面更清晰的方向发展的。

动图比较快,我放上一张静态图。这完全是由人工智能生成的图片。

生成的代码很简单。

# 加载训练模型
if os.path.exists(checkpoint_dir+"/checkpoint"):
checkpoint.restore(tf.train.latest_checkpoint(checkpoint_dir))

# 生成噪点作为输入
test_input = tf.random.normal([1, 160])
# 交给生成器批量生成
predictions = generator(test_input, training=False)
# 取出一张结果
img_arr = predictions[0][:, :, 0]
# 将结果复原成图片像素色值数据
img_arr = img_arr* 127.5 + 127.5

这是20000张图,500轮训练的效果。如果是百万张图片,几千轮训练呢?完全仿真很简单。

项目开源地址:gitee.com/bigcool/gan…

三、我们对AIGC该有的态度

AIGC的火爆出圈,引起全球的强烈讨论。很多地方甚至打算立法,禁止学生使用它做作业。

虽然我说了这么多。可能直到现在,依然有人觉得这是噱头:我的工作这么高级,是有灵魂的工作,人工智能写文章能比我通顺?它还写代码?它懂逻辑吗?

国外有一个IT老哥叫David Gewirtz。他从1982年开始就写代码,干了40多年,也在苹果公司待过。他以为用ChatGPT写代码不会有啥惊喜。直到出现结果,却吓了他一大跳。

他的需求是给它老婆写一个网站的插件,用于挑选顾客,并滚动顾客的名字展示。这个需要几天完成的工作,ChatGPT很快就完成了。而且代码纯粹简洁,极其规范。它还告诉你该操作哪个文件,该如何部署

现阶段的人工智能,可能没有自己的思考,但是它有自己的计算。

你会写文章,因为你读过300多本书,并且记住了里面20%的内容。这些让你引以为傲。但是人工智能,它读过人类历史上出现过的所有文献,只要硬盘够,它全部都能记住。而且它还不停对这些内容做分析、加工、整理:这里和这里有关联,这里和那里都是在介绍橙子的营养成分。它通过计算,让一切知识发生互联互通。

当有人向人工智能表示人类的担忧时,人工智能也给出了自己的回答。

我比较赞同它的观点。

抱有其他观点的人,主要担心有了人工智能,人类就会变得不动脑子了。时间长就废了。

我觉得,这些都是工具。相机出来的时候,也是被画家抵制,因为成像太简单了。现在想想,太简单有问题吗?没有!同样的还有计算器之于算盘,打字之于手写。甚至TensorFlow 2.0出来时,也被1.0的用户抵制。他们说开发太简单了,这让开发者根本接触不到底层。殊不知,1.0出来的时候,那些写汇编语言的开发者也想,他们堕落了,居然不操作寄存器。

其实,我感觉这些担心是多余的。每个时代都有会属于自己时代的产物。就像现在我们不用毛笔写字了,但是我们的祖先也没有敲过键盘呀!可能下一个时代的人,连键盘也不敲了。


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

Android音频播放AudioTrick详解

Android 中常用的播放音频的接口有MediaPlayer、AudioTrack和SoundPool,音频的渲染最常用的是AudioTrack和OpenSL ES ,下面将介绍下AudioTrack相关知识,主要内容如下: AudioTrack介绍 Au...
继续阅读 »

Android 中常用的播放音频的接口有MediaPlayerAudioTrackSoundPool,音频的渲染最常用的是AudioTrackOpenSL ES ,下面将介绍下AudioTrack相关知识,主要内容如下:



  1. AudioTrack介绍

  2. AudioTrack的创建

  3. AudioTrack音频数据写入

  4. AudioTrack生命周期

  5. AudioTrack的使用


AudioTrack介绍


AudioTrack用来点播放原始 pcm格式的音频数据,AudioTrack有两种播放模式:



  • MODE_STATIC:这种模式会将音频数据一次写入音频缓冲区,适合处理内存少及尽可能小的延迟播放的短声音场景,如播放的游戏音效、铃声、系统提示音等,此时这种模式开销最小。

  • MODE_STREAM:这种模式会不断的写入音频数据,适用于需要不断接受音频数据的场景,这种模式主要是由于某些音频数据持续时间长、或者音频特性(高采样率、更高位深等)导致不能一次性写入内存而出现的,正常播放 PCM原始音频数据就选择这种模式。


MediaPlayer相比较,MediaPlayer可以播放不同类型、不同格式的声音文件,会在底层创建与之对应的音频解码器,而AudioTrack只接收PCM原始音频数据,MediaPlayer在底层还是会创建AudioTrack,把解码后的PCM数流传递给AudioTrackAudioTrack再传递给AudioFlinger进行混音,然后才传递给硬件播放。


AudioTrack的创建


AudioTrack的创建使用如下方式:


// Android5.0开始
AudioTrack(
attributes: AudioAttributes!,
format: AudioFormat!,
bufferSizeInBytes: Int,
mode: Int,
sessionId: Int)

上面构造方法对应的参数含义如下:



  • attributes:表示音频流信息的属性集合,自从 Android5.0 开始使用AudioAttributes来取代流类型的设置,可以比流类型设置传达更多信息,常用来设置音频的用途、音频的内容等。

  • format:表示AudioTrack 接受的音频格式,对于线性 PCM来说,反应每个样本大小(8、16、32位)及表现形式(整型、浮点型),音频格式定义在AudioFormat中,常见的音频数据格式中只有AudioFormat.ENCODING_PCM_16BIT可以保证在所有的设备上正常使用,像AudioFormat.ENCODING_PCM_8BIT不能保证在所有设备上正常使用。

  • bufferSizeInBytes:表示音频数据缓冲区的大小,单位事字节,其大小一般是音频帧大小的非零倍数,如果播放模式是MODE_STATIC,则缓冲区大小是本次播放的音频的大小,如果播放模式是MODE_STREAM,则缓冲区大小不能小于最小缓冲区大小,也就是不能小于getMinBufferSize返回的大小。

  • mode:表示播放模式,AudioTrack提供了MODE_STATICMODE_STREAM两种方式,MODE_STATIC会将音频资源一次性写入音频缓冲区,适用于铃声、系统提示音等延时小、音频资源内存占用少的场景,,MODE_STREAM则适用于需要不断通过write方法写入数据的场景,相较MODE_STATIC会有一定延时,但是可以持续不断的接收音频数据。

  • sessionId:音频会话 Id,这里使用AudioManager.AUDIO_SESSION_ID_GENERATE有底层音频框架自己生成sessionId


AudioTrack音频数据写入


无论是流模式(STREAM_MODE)还是静态缓冲模式(STATIC_MODE)模式,都需通过write方式写入音频数据来进行播放,主要的write方式如下:


// AudioTrack构造函数中指定的格式应为AudioFormat#ENCODING_PCM_8BIT
open fun write(audioData: ByteArray, offsetInBytes: Int, sizeInBytes: Int): Int
// AudioTrack构造函数中指定的格式应为AudioFormat#ENCODING_PCM_16BIT
open fun write(audioData: ShortArray, offsetInShorts: Int, sizeInShorts: Int): Int
// AudioTrack构造函数中指定的格式应为AudioFormat#ENCODING_PCM_FLOAT
open fun write(audioData: FloatArray, offsetInFloats: Int, sizeInFloats: Int, writeMode: Int): Int

写入音频数据的返回值大于等于 0,读取音频数据常见异常如下:



  1. ERROR_INVALID_OPERATION:表示AudioTrack 未初始化。

  2. ERROR_BAD_VALUE:表示参数无效。

  3. ERROR_DEAD_OBJECT:表示已经传输了一些音频数据的情况下不返回错误码,将在下次 write返回处返回错误码。


这个跟AudioRecord中的 read 函数有点类似,具体细节查看官方文档。


AudioTrack生命周期


AudioTrack的生命周期主要是STATE_UNINITIALIZEDSTATE_INITIALIZEDSTATE_NO_STATIC_DATA,其中STATE_INITIALIZED对应STREAM_MODESTATE_NO_STATIC_DATA对应STATIC_MODE,至于播放状态不怎么重要,如下图所示:



AudioTrack的使用


AudioTrack的使用主要就是从PCM文件中读取数据,然后将读取到的音频写入AudioTrack进行播放,其关键代码如下:


// 初始化AudioTrack
private fun initAudioTrack() {
bufferSize = AudioTrack
.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, AUDIO_FORMAT)
attributes = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA) // 设置音频的用途
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) // 设置音频的内容类型
.build()
audioFormat = AudioFormat.Builder()
.setSampleRate(SAMPLE_RATE)
.setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
.build()
audioTrack = AudioTrack(
attributes, audioFormat, bufferSize,
AudioTrack.MODE_STREAM, AudioManager.AUDIO_SESSION_ID_GENERATE
)
}
// AudioTrack写入音频数据
private fun writeAudioData(){
scope.launch(Dispatchers.IO){
val pcmFile = File(pcmFilePath)
val ins = FileInputStream(pcmFile)
val bytes = ByteArray(bufferSize)
var len: Int
while (ins.read(bytes).also { len = it } > 0){
audioTrack.write(bytes, 0, len)
}
audioTrack.stop()
}
}
// 开始播放
private fun start(){
audioTrack.play()
writeAudioData()
}

AudioTrack的使用基本如上,AudioTrack播放音频的相关代码可以在回复关键字【audiotrack】关键字获取,案例中用到的本地PCM文件可以回复关键字【pcm】获取。


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

Android 完整的apk打包流程

在Android Studio中,我们需要打一个apk包,可以在Gradle task 任务中选一个 assembleDebug/assembleRelease 任务, 控制台上就可以看到所有的构建相关task: 可以看到,这么多个task任务,执行是有...
继续阅读 »

在Android Studio中,我们需要打一个apk包,可以在Gradle task 任务中选一个
assembleDebug/assembleRelease 任务,


企业微信截图_fa2194a8-735e-4720-91be-81fd2524d20f.png


控制台上就可以看到所有的构建相关task:


截屏2023-03-05 20.48.57.png
截屏2023-03-05 20.49.28.png
可以看到,这么多个task任务,执行是有先后顺序的,其实主要就是以下步骤:


//aidl 转换aidl文件为java文件
> Task :app:compileDebugAidl

//生成BuildConfig文件
> Task :app:generateDebugBuildConfig

//获取gradle中配置的资源文件
> Task :app:generateDebugResValues

// merge资源文件
> Task :app:mergeDebugResources

// merge assets文件
> Task :app:mergeDebugAssets
> Task :app:compressDebugAssets

// merge所有的manifest文件
> Task :app:processDebugManifest

//AAPT 生成R文件
> Task :app:processDebugResources

//编译kotlin文件
> Task :app:compileDebugKotlin

//javac 编译java文件
> Task :app:compileDebugJavaWithJavac

//转换class文件为dex文件
> Task :app:dexBuilderDebug

//打包成apk并签名
> Task :app:packageDebug

依靠这些关键步骤最后就能打包出一个apk。


首先看


第一步:aidl(编译aidl文件)


将项目中的aidl文件编译为java文件,AIDL用于进程间通信


第二步:生成BuildConfig文件


在项目中配置了
buildConfigField等信息,会在BuildConfig class类里以静态属性的方式展示:


截屏2023-03-05 21.09.18.png


第三步:合并Resources、assets、manifest、so等资源文件


在我们的项目中会依赖不同的库、组件,也会有多渠道的需求,所以merge这一步操作就是将不同地方的资源文件进行整合。
多个manifest文件也需要整理成一个完整的文件,所以如果有属性冲突这一步就会报错。资源文件也会整理分类到不同的分辨率目录中。


资源处理用的工具是aapt/aapt2


注意AGP3.0.0之后默认通过AAPT2来编译资源,AAPT2支持了增量更新,大大提升了效率。


AAPT 工具负责编译项目中的这些资源文件,所有资源文件会被编译处理,XML 文件(drawable 图片除外)会被编译成二进制文件,所以解压 apk 之后无法直接打开 XML 文件。但是 assets 和 raw 目录下的资源并不会被编译,会被原封不动的打包到 apk 压缩包中。
资源文件编译之后的产物包括两部分:resources.arsc 文件和一个 R.java。前者保存的是一个资源索引表,后者定义了各个资源 ID 常量。这两者结合就可以在代码中找到对应的资源引用。比如如下的 R.java 文件:


截屏2023-03-05 21.19.59.png
实际上被打包到 apk 中的还有一些其他资源,比如 AndroidManifest.xml 清单文件和三方库中使用的动态库 .so 文件。


第四步:编译java文件(用到的工具 javac )


1、java文件包含之前提到的AIDL 生成的java文件


2、java代码部份:通过Java Compiler 编译项目中所有的Java代码,包括R.java.aidl文件生成的.java文件、Java源文件,生成.class文件。在对应的build目录下可以找到相关的代码


3、kotlin代码部份:通过Kotlin Compiler编译项目中的所有Kotlin代码,生成.class文件


注:注解处理器(APT,KAPT)生成代码也是在这个阶段生成的。当注解的生命周期被设置为CLASS的时候,就代表该注解会在编译class文件的时候生效,并且生成java源文件和Class字节码文件。

第五步: Class文件打包成DEX(dx/r8/d8等工具编译class文件)


image.png



  • 在原来 dx是最早的转换工具,用于转换class文件为dex文件。

  • Android Studio 3.1之后,引入了D8编译器和 R8 工具。

  • Android Studio 3.4之后,默认开启 R8
    具体的区别可以点击看看


注意:JVM 和 Dalvik(ART) 的区别:JVM执行的是.class文件、Dalvik和ART执行的.dex文件。具体的区别可以点击看看


而在编译class文件过程也常用于编译插桩,比如ASM,通过直接操作字节码文件完成代码修改或生成。


第六步:apkbuilder/zipflinger(生成APK包)


这一步就是生成APK文件,将manifest文件、resources文件、dex文件、assets文件等等打包成一个压缩包,也就是apk文件。
在老版本使用的工具是apkbuilder,新版本用的是 zipflinger
而在AGP3.6.0之后,使用zipflinger作为默认打包工具来构建APK,以提高构建速度。


第七步: zipalign(对齐处理)


对齐是Android apk 很重要的优化,它会使 APK 中的所有未压缩数据(例如图片或原始文件)在 4 字节边界上对齐。这使得CPU读写就会更高效。


也就是使用工具 zipalign 对 apk 中的未压缩资源(图片、视频等)进行对齐操作,让资源按照 4 字节的边界进行对齐。这种思想同 Java 对象内存布局中的对齐空间非常类似,主要是为了加快资源的访问速度。如果每个资源的开始位置都是上一个资源之后的 4n 字节,那么访问下一个资源就不用遍历,直接跳到 4n 字节处判断是不是一个新的资源即可。


第八步: apk 签名


没有签名的apk 无法安装,也无法发布到应用市场。


大家比较熟知的签名工具是JDK提供的jarsigner,而apksignerGoogle专门为Android提供的签名和签证工具。


其区别就在于jarsigner只能进行v1签名,而apksigner可以进行v2v3v4签名。



  • v1签名


v1签名方式主要是利用META-INFO文件夹中的三个文件。


首先,将apk中除了META-INFO文件夹中的所有文件进行进行摘要写到 META-INFO/MANIFEST.MF;然后计算MANIFEST.MF文件的摘要写到CERT.SF;最后计算CERT.SF的摘要,使用私钥计算签名,将签名和开发者证书写到CERT.RSA。


所以META-INFO文件夹中这三个文件就能保证apk不会被修改。



  • v2签名


Android7.0之后,推出了v2签名,为了解决v1签名速度慢以及签名不完整的问题。


apk本质上是一个压缩包,而压缩包文件格式一般分为三块:


文件数据区,中央目录结果,中央目录结束节。


而v2要做的就是,在文件中插入一个APK签名分块,位于中央目录部分之前,如下图:


图片


这样处理之后,文件就完成无法修改了,这也是为什么 zipalign(对齐处理) 要在签名之前完成。



  • v3签名


Android 9 推出了v3签名方案,和v2签名方式基本相同,不同的是在v3签名分块中添加了有关受支持的sdk版本和新旧签名信息,可以用作签名替换升级。



  • v4签名


Android 11 推出了v4签名方案。


最后,apk得以完成打包


PMS 在安装过程中会检查 apk 中的签名证书的合法性,具体安装apk内容稍后介绍。


apk内容包含如下:


截屏2023-03-05 22.01.14.png


总体的打包流程图如下:


截屏2023-03-05 22.02.33.png


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

彻底销毁!这地10亿条个人涉疫数据被清理

过去三年,健康码作为精准防疫的重要数字工具,在大众的工作和生活中渗透率极高。有数据表明,此前我国有近9亿人申领了“健康码”,使用次数超600亿次。不过随着疫情防控政策放开,健康码消失、行程卡下线,每天几乎都要扫码的“特殊时期”已然过去,后续如何处理在此期间积累...
继续阅读 »

过去三年,健康码作为精准防疫的重要数字工具,在大众的工作和生活中渗透率极高。有数据表明,此前我国有近9亿人申领了“健康码”,使用次数超600亿次。

不过随着疫情防控政策放开,健康码消失、行程卡下线,每天几乎都要扫码的“特殊时期”已然过去,后续如何处理在此期间积累的个人涉疫数据,逐渐成为关注焦点。

近期,素有“江南胜地”之称的江苏无锡,举办了一场“特别”的仪式,为这一问题提供了可行答案。

1、销毁10亿个人涉疫数据无锡开全国地级市先河

不久前江苏无锡市政府发文称,3月2日当地举行了涉疫个人数据销毁仪式,首批销毁10亿余条此前出于疫情防控及服务目的存储在城市大数据中心的个人信息数据。第三方审计机构和公证处也参与了当天的活动,确保数据彻底销毁、无法还原。

无锡此次销毁涉疫公民个人数据系全国地级市中首例。

据悉,无锡市相关负责人表示,此次销毁涉疫个人数据,主要出于四个方面的考量:

第一,体现了依法执政理念,依法依规删除目的已经实现的数据;

第二,保护了公民隐私,防止数字时代公民个人信息被盗用或滥用;

第三,防止了数据泄露,通过数据彻底销毁减少数据泄露的可能性;

第四,节约了存储空间,进一步提高存储效率。

以健康码为例,其采集的个人敏感数据包括,个人强关联信息(身-份-证-号-码、手机号码等)、个人健康信息(绿码)、详细的个人行程信息(场所码)、个人健康证明(核酸信息)以及疫苗接种信息等。

从网络安全角度看,健康码得以发挥作用,本质上是民众让渡了个人隐私权来支持全国的疫情防控。

因此,去年年底新冠防疫“新十条”出台,健康码基本失去作用后,就有业界人士呼吁:类似核酸信息、场所码等数据应该销毁、封存或脱敏。

一方面,大部分信息的采集是为了流调,有一定的时效性。过了时间节点,就没有留存的必要;另一方面,各地健康码管理平台会从社区、卫生机构、通信、交通运输等部门间接获取个人信息,这些信息一旦使用不当,就可能造成个人信息的泄露。

事实上,自去年12月以来,已有多个涉及个人涉疫数据的平台公开表示,将对相关数据进行销毁。

2022年12月13日,中国信息通信研究院发布通告称,根据国务院联防联控机制综合组有关要求,当日零时起,“通信行程卡”服务正式下线。中国信通院已按照有关法律法规规定,同步删除了行程卡相关所有数据,切实保障个人信息安全。

今年2月14日,广东省健康码“粤康码”发布服务公告称,按照国家新冠病毒感染防控政策措施优化调整要求,抗原自测、老幼助查、健康申报、电子证照、防疫工作台等服务于2月16日11时起停止服务。公告明确,停止相关服务后,“粤康码”将按照有关法律法规规定,彻底删除、销毁服务相关所有数据,切实保障个人信息安全。


来源:微博@羊城晚报

当然,像无锡这样举办数据销毁仪式的还是全国首例,以公开透明的方式来处理如此体量的个人数据,不仅更加干脆利落令人信服,也展现了政府捍卫个人隐私信息的态度与魄力。

2、“善后”争议,健康码功成身退还是转型他用

不过,由于健康码以各地自建为主,缺乏统一标准,因此在健康码下线后,相关数据到底如何“善后”,目前并没有定论。

除了像无锡、广东这样坚持销毁个人涉疫数据信息的做法之外,还有一部分城市则更倾向于借由健康码的庞大用户基础,与其他政务大数据融合,打造更多便民便企的应用服务。

那么这些数据到底是要功成身退还是转型他途?对此,业内专家也提出了截然不同的观点。

有人认为,在保护隐私的前提下充分利用也许更好。

“涉疫相关数据对各级政府来说是一笔宝贵财富,里面不但有成功知识和经验,也有失误教训,涉及精准管控、生活物资保障、民众出行和应急处置等方方面面。”大数据协同安全技术国家工程研究中心副主任钟力表示,数据销毁在数字经济时代可能并不是最佳的选择。数据作为生产要素,应该充分激活其价值,且现在信息技术为涉疫数据的存储、安全保护和分析利用提供了强大的技术基础。

也有人坚持,涉疫数据无论从法律还是伦理上都只能用于防疫目的,在疫情常态化后健康码收集的个人信息和数据就应该全部删除。

“不论数据存放在什么地方,总是离不开云端,而且存储就会产生成本。健康码数据作为特殊时期的一个‘应急产品’,即便是匿名化处理后,仍可能被部分机构用于其他目的或牟利。这种(情况)从法理和伦理层面都无法得到辩护。”华中科技大学生命伦理学研究中心执行主任雷瑞鹏如是说。

还有人则表示,健康码包含多领域敏感个人信息,必须用户自主单独同意后才能变更使用目的。

中国电子技术标准化研究院网安中心测评实验室副主任何延哲就持有上述立场。在他看来,如果不能达成用户自主单独同意这一先决条件,那么相关信息就必须删除或作匿名化处理。同时他还提到,健康码作为一个运行成熟的系统,下线不代表底层代码被删除。“我是赞成把数据删除,把代码保留。这样不仅保留了积攒的抗疫经验,也为以后的公共卫生应急系统提供一个更好的基础。”

3、代表建言献策,保护涉疫数据安全仍是当务之急

当下全国各地关于健康码个人数据是否留存依旧各执己见,采取实际行动进行涉疫数据销毁的地区也不算多,国家层面对此也并未出台统一规定。但无论结果如何,当务之急是要切实解决数据安全和隐私保护问题,防止数据窃取、泄露、滥用和不合规等安全事件。

关于这一点早有前车之鉴。比如,由于赋码不透明导致去年年中个别地区使用健康码红码限制民众正常出行,再比如,关于“疫情、红码”等相关钓鱼攻击和电信诈骗行为层出不穷,可以预见,在疫情防控放开的当下,这类行为还将长期存在,而健康码等信息的泄露无疑会让不法分子的行为变得可信度更高。

如何保障涉疫数据安全?大数据协同安全技术国家工程研究中心副主任钟力认为,应该从以下三个方面进行努力。

一是存储涉疫数据的大数据平台的安全,应支持数据全生命周期安全保护;二是做好涉疫数据相关数据处理活动的合规监管与安全监测,防止数据泄露、滥用和被攻击窃取等安全事件;三是安全管控涉疫数据的开发利用和流通共享,对原始数据需严格保护、严控使用,并采用隐私计算技术来支撑,更多的是提供去标识化、匿名化和静态脱敏的涉疫数据。

今年两会期间,关于健康码数据的处理问题,也有代表委员建言献策。

全国人大代表、德力西集团董事局主席胡成中提出,建议在国务院应急管理制度中增补健康码相关内容,明确其启用、停用的条件和权限,明确赋码规则,限时梳理并删除全国各级健康码数据。

全国政协委员、奇安信集团董事长齐向东也表示,健康码的数据需要删除或者做匿名化处理,即使要使用也必须先获得用户自主同意;有关运营部门要及时公开数据的流向、删除或者匿名化的有关情况。

总而言之,三年多来,公众将大量个人信息提交给健康码等系统,既是法律的要求,也是在主动履行协助防疫的社会责任。对此,有关部门如何处理这些数据的“善后”问题,不仅关乎个人隐私安全是否得到尊重和切实保护,也一定程度上影响着有关部门在民众心中的公信力。

参考链接:

http://www.inewsweek.cn/society/2023-03-06/17764.shtml

baijiahao.baidu.com/s?id=1759356399553406615

hackernews.cc/archives/43399

作者:千山
来源:mp.weixin.qq.com/s/qYbKIVCQxS1OMe-o3HXr-Q

收起阅读 »

一张架构图让我认识到前端的渺小

web
前言 大家好这里是阳九,一个文科转码的野路子码农,热衷于研究和手写前端工具. 今天我们不聊前端,咱们来聊聊后端,聊聊架构 目的是正视自己的地位和价值,在寒冬中保持清醒 借用若川大佬的一句话: 所知甚少,唯善学 先别问我到底是前端程序员还是后端程序员,我自己也...
继续阅读 »


前言


大家好这里是阳九,一个文科转码的野路子码农,热衷于研究和手写前端工具.


今天我们不聊前端,咱们来聊聊后端,聊聊架构


目的是正视自己的地位和价值,在寒冬中保持清醒


借用若川大佬的一句话: 所知甚少,唯善学




先别问我到底是前端程序员还是后端程序员,我自己也不知道。当然自己也很想在某个领域精进,但是业务嘛,咱们就是一块砖,哪里需要哪里搬,硬着头皮上呗


最近是在维护公司的业务引擎, 对其进行优化并添加功能,技术栈的话大体来讲, 前端是React+Node BFF,后端是GO (Gin框架+原生)


随着看后端代码的时间越来越长,作为一个切图仔,我越来越觉得恐怖。那是一种,看到了过于庞大的未知世界,并深深觉得自己的认知太少的恐怖。


因为这个项目是定制项目,通俗点来讲就是"改装车",不从头造车但是要改装,这里改改哪里改改,一旦改动的点多了,就需要你把整个项目的逻辑全部理顺。


于是乎我在这两个月里,里里外外看了几万行代码。也是硬着头皮把整个架构梳理了一遍。


先在这里放上一张整理出来的架构图


(当然这里根据原系统魔改了的很多地方,并进行了简化,并修改了名称,防止泄密,模块的大小差不多是以核心逻辑代码量来算的,前端的核心代码量有多少咱们前端er应该都懂)


XXX系统总架构图.jpg


本文目的


想通过一次后端架构分析, 让我们前端人意识到自己的不足与眼界的狭窄,我们前端er需要对一个完整的大型项目有一个整体的认知,意识到自己的不足,才能在这条路上更好的走下去。
不要满足于html拼拼页面,写写样式,做做一些简单的工作。


技术栈介绍


这里先简单介绍一下技术栈, 否则无法理解



  • 前端 React webpack antd redux ... 前端er都懂,以下省略

  • Koa框架 一个node后端框架

  • Gin框架 一个GO后端框架

  • Docker 容器引擎

  • K8S Docker集群管理

  • RabbitMQ 实现AMQP消息队列协议的代理软件,也就是消息队列,用于通信

  • GFS 分布式文件共享系统,用于大量数据访问

  • MongoDB 快读读取用数据库

  • Elastic Search 分布式数据库,进行大批量存储查询

  • SQL 传统关系型数据库

  • MobileSuit 后端框架工厂框架,用于创建独立Gin服务

  • 扩容服务 GO原生实现

  • 引擎 GO原生实现

  • 守护进程 GO原生实现


关于前端


看到左上角我特意标出来的那一小块红色的UI了吗?我们称之为 前端


数据库


mongo DB : 用于小体积数据的快速读取,用作数据中间传输(原生json,使用方便)


Elastic Search : 分布式数据库, 用于大体积,大批量数据存储和快速检索(动辄十几亿条数据,一条json数据几千个字段)


SQL: 用于存储不变的数据,比如国家信息,公司信息等等,重在稳定。


容器化部署


简单介绍一下什么是容器,当然我这里默认大家都懂。 容器提供了程序所需要运行的环境, 并将其独立隔离出来,


Docker:  是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的镜像中


k8s: 是 Google 开源的一个容器集群管理系统


架构中除了守护进程和引擎扩容服务,所有引擎,前后端服务,数据库服务都使用Docker进行容器化,并使用K8S进行统一管理。


image.png


引擎



  • 引擎扩容服务:可以在判断需要并能够提供多个引擎的时候,开启更多的引擎进行处理。

  • 树状引擎结构: 一个任务可能需要多个引擎协同参与, 而且下级引擎依赖上级引擎的结果

  • 上级引擎的结果需要经过调度服务,通过MQ进行消息传递,调度后再传递给下级引擎

  • 最终结果写入数据库


image.png


调度层




  • 任务调度器:提供任务优先级调度,任务状态管理,任务拆分,任务下放等功能




  • 结果处理器; 提供各引擎对应的结果处理解析功能,有大量的数据库查询,结果计算,字符串解析逻辑,体积非常庞大.




  • 当然优先级调度器和引擎结果处理服务是单独运行在Docker中,需要使用MQ和GFS,数据库进行数据交换




image.png


数据聚合层


也就是node写的BFF数据聚合层,将gin框架(gopher层)获取来的数据进行聚合,格式化成前端所需的数据结构,并提供前端接口。这里就不赘述了


gopher服务层


提供主体服务, 数据库IO等等,体量最大。提供了各种处理模块,接口等。


架构也是一个简单的类node三层架构,


(Router - controller - Service)
带上validator层和数据库操作层(与node中的Model层区别不大)


image.png


守护进程


原生GO写的守护进程,一个部署时直接运行在机器某端口的进程, 主要功能有3个


创建 - 监视 - 代理



  • 它将整个系统包裹起来, 用于监视各个容器的运行情况,

  • 提供了一个用于自动注册Gin框架路由的上级自研框架MobileSuit,系统的每个服务都会通过MS框架进行启动,生成一个Gin路由实例。 并挂载到总路由树上。


image.png



  • 守护进程包裹了所有的服务, 系统各个服务发出的请求都会首先被代理到守护进程上,守护进程将其统一拦截下来, 方便之后的统一请求代理处理。


image.png


对前端人和自己的话


不知道小伙伴们看到我整理出来的架构有什么看法?有没有认识到前端的渺小.
我们一定要正视自己的地位,在寒冬中保持清醒


再聊聊,为什么很多小伙伴会觉得 前端已死?


我想说的是,对比起后端,前端人在几年内吃了太多的互联网红利。这个行业可能需要自我净化,提升整体素质。


我见过包装3年实际一年,连vscode调试都不会的前端人拿很高的月薪。


也见过对算法,原理,底层,优化,都很熟悉的3-4年后端的人拿的不比我这个小外包高多少。


我们前端人一定要明白,普通前端的可替代性实在太强太强。


要么我们深入业务,要么我们深入原理。


对于真正学习计算机科学的人来说,什么webpack代码构建,babel编译,什么react链表结构,vue模板编译,这些看上去比较底层的东西,在他们眼里可能只是基本功,可能只是常识。


如果不深入原理,那么最终真的只能“前端已死”。



  • 想想在刚入行的时候,读了一下某开源工具的源码,我的反应是


“哇这架构好神奇,居然将三层类层层嵌套” “哇一个参数居然能通过观察者模式传三层”



  • 想想在刚入行的时候,看了一下react渲染的原理


"哇他们真聪明,居然能想到将大任务分成多个5ms小任务,运行在浏览器每一帧上,阻止卡顿"


跟一个后端/硬件的朋友讨论,他跟我说"这不是常识吗?调操作系统底层,5ms任务给你掐了"


现在看来,这不过是基础罢了。但很多前端er,连这些都搞不明白,就像原来的我一样。


毕竟,即便是深入了前端的原理,可能也只是到达了软件开发的基本水平吧。


还是借用那句话吧。
所知甚少,唯善学。


前端不会死的,它只是停止了狂奔。


作者:不月阳九
来源:juejin.cn/post/7207617774634451000
收起阅读 »