注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

leetcode-最接近的三数之和

往常周末都是睡懒觉,今天早起去体检了。每年都是到了体检的时候,才会想起来身体才是革命的本钱吧。还好都不是什么大问题,最大的问题就是自己没有坚持锻炼。 先立个Flag,每周至少有5天,专门锻炼30分钟以上吧。先把标准定的低一点,能做到最重要,不然都是5分钟热情,...
继续阅读 »

往常周末都是睡懒觉,今天早起去体检了。每年都是到了体检的时候,才会想起来身体才是革命的本钱吧。还好都不是什么大问题,最大的问题就是自己没有坚持锻炼。

先立个Flag,每周至少有5天,专门锻炼30分钟以上吧。先把标准定的低一点,能做到最重要,不然都是5分钟热情,过了几天这个目标就抛在脑后了吧。

当然,说起坚持,一个比较好的方法是定期review,如果有1天没做到,也不要觉得反正已经没做到,破罐子破摔,后面根本就不再做的。每天坚持是每天新的挑战,能够比之前的自己坚持做更久,就是自己的突破。

每天做1题算法题,也是上个月立下的Flag,虽然已经倒了,还是希望后面能多坚持,毕竟进一寸就有进一寸的欢喜。今天继续刷leetcode第16题,跟昨天的题目非常类似,昨天要求3个数之和=0,今天要求是跟target偏离最小。


题目



给定一个包括 n 个整数的数组 nums 和 一个目标值 target。找出 nums 中的三个整数,使得它们的和与 target 最接近。返回这三个数的和。假定每组输入只存在唯一答案。




示例

输入: nums = [-1,2,1,-4], target = 1

输出: 2

解释: 与 target 最接近的和是 2 (-1 + 2 + 1 = 2) 。



思路


跟昨天的题目是非常类似的,首先肯定还是排序。
排序后,假定一个最小值a,然后从a右边的数里面找出2个b和c,使得abs(a+b+c-target)的值最小。因为a右边的数组也是有序的,这时候找b和c其实也不需要2层for循环来遍历,可以使用双指针,分别指向剩余数组的最小和最大,如果a+b+c-target小于0,就让最小值往右边走一个,如果a+b+c-target大于0,就让最大值往左边走一个。


Java版本代码


class Solution {
public int threeSumClosest(int[] nums, int target) {
int len = nums.length;
int ans = 3001;
Arrays.sort(nums);
for (int i = 0; i < len -2; i++) {
if (i > 0 && nums[i] == nums[i-1]) {
continue;
}
int start = i + 1;
int end = len - 1;
while (start < end) {
int sum = nums[i] + nums[start] + nums[end];
if (sum == target) {
ans = sum;
return ans;
}
if (Math.abs(sum - target) < Math.abs(ans - target)) {
ans = sum;
}
if (sum > target) {
end--;
} else if (sum < target) {
start++;
}
}
}
return ans;
}
}

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

Android正确的保活方案,不要掉进保活需求死循环陷进

在开始前,还是给大家简单介绍一下,以前出现过的一些黑科技: 大概在6年前Github中出现过一个叫MarsDaemon,这个库通过双进程守护的方式实现保活,一时间风头无两。好景不长,进入 Android 8.0时代之后,这个库就废掉了。 最近2年Github上...
继续阅读 »

在开始前,还是给大家简单介绍一下,以前出现过的一些黑科技:


大概在6年前Github中出现过一个叫MarsDaemon,这个库通过双进程守护的方式实现保活,一时间风头无两。好景不长,进入 Android 8.0时代之后,这个库就废掉了。


最近2年Github上面出来一个Leoric 感兴趣的可以去看一下源码,谁敢用在生产环境呢,也就自己玩玩的才会用吧(不能因为保活而导致手机卡巴斯基),我没有试过这个,我想说的是:黑科技能黑的了一时,能黑的了一世吗?


没有规矩,不成方圆,要提升产品的存活率,最终还是要落到产品本身上面来,尊重用户,提升用户体验才是正道。


以前我也是深受保活需求的压迫,最近发现QQ群里有人又提到了如何保活,那么我们就来说一说,如何来正确保活App?




Android 8.0之后: 加强了应用后台限制,当时测试过一组数据:



应用处于前台,启动一个前台Service,里面使用JobScheduler启动定时任务(30秒触发一次),
此时手机锁屏,前10分钟内,定时任务都是正常执行;

大概在12分钟左右,发现应用进程就被kill掉了,解锁屏幕,app也不在前台了;



各大国产手机厂商底层都经过自己魔改,自家都有自己的一套自启动管理,小米手机更乱(当时有个神隐模式的概念,那也是杀后台高手),只能说当时Android手机各种性能方面都不足,各家都会有自己的一套省电模式,以此来达到省电和提高手机性能,Android 系统变得越来越完善,但是厂商定制的自启动、省电模式还在,所以我们要做保活。


1.Android 8.0之前-常用的保活方案



1.开启一个前台Service

2.Android 6.0+ 忽略电池优化开关(稍后会有代码)

3.无障碍服务(只针对有用这个功能的app,如支付宝语音增强提醒用了它)





2.Android 8.0之后-常用的保活方案



1.开启一个前台Service(可以加上,单独启用的话无法满足保活需求)

2.Android 6.0+ 忽略电池优化开关(稍后会有代码)

3.无障碍服务(只针对有用这个功能的app,如支付宝语音增强提醒用了它)

4.应用自启动权限(最简单的方案是针对不同系统提供教程图片-让用户自己去打开)

5.多任务列表窗口加锁(提供GIF教程图片-让用户自己去打开)

6.多任务列表窗口隐藏App(仅针对有这方面需求的App)

7.应用后台高耗电(仅针对Vivo手机)



3.保活方案实现步骤


(1). 前台Service


//前台服务
class ForegroundCoreService : Service() {
override fun onBind(intent: Intent?): IBinder? = null
private var mForegroundNF:ForegroundNF by lazy {
ForegroundNF(this)
}
override fun onCreate() {
super.onCreate()
mForegroundNF.startForegroundNotification()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if(null == intent){
//服务被系统kill掉之后重启进来的
return START_NOT_STICKY
}
mForegroundNF.startForegroundNotification()
return super.onStartCommand(intent, flags, startId)
}
override fun onDestroy() {
mForegroundNF.stopForegroundNotification()
super.onDestroy()
}
}

//初始化前台通知,停止前台通知
class ForegroundNF(private val service: ForegroundCoreService) : ContextWrapper(service) {
companion object {
private const val START_ID = 101
private const val CHANNEL_ID = "app_foreground_service"
private const val CHANNEL_NAME = "前台保活服务"
}
private var mNotificationManager: NotificationManager? = null

private var mCompatBuilder:NotificationCompat.Builder?=null

private val compatBuilder: NotificationCompat.Builder?
get() {
if (mCompatBuilder == null) {
val notificationIntent = Intent(this, MainActivity::class.java)
notificationIntent.action = Intent.ACTION_MAIN
notificationIntent.addCategory(Intent.CATEGORY_LAUNCHER)
notificationIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
//动作意图
val pendingIntent = PendingIntent.getActivity(
this, (Math.random() * 10 + 10).toInt(),
notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT
)
val notificationBuilder: NotificationCompat.Builder = NotificationCompat.Builder(this,CHANNEL_ID)
//标题
notificationBuilder.setContentTitle(getString(R.string.notification_content))
//通知内容
notificationBuilder.setContentText(getString(R.string.notification_sub_content))
//状态栏显示的小图标
notificationBuilder.setSmallIcon(R.mipmap.ic_coolback_launcher)
//通知内容打开的意图
notificationBuilder.setContentIntent(pendingIntent)
mCompatBuilder = notificationBuilder
}
return mCompatBuilder
}

init {
createNotificationChannel()
}

//创建通知渠道
private fun createNotificationChannel() {
mNotificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
//针对8.0+系统
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_ID,
CHANNEL_NAME,
NotificationManager.IMPORTANCE_LOW
)
channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
channel.setShowBadge(false)
mNotificationManager?.createNotificationChannel(channel)
}
}

//开启前台通知
fun startForegroundNotification() {
service.startForeground(START_ID, compatBuilder?.build())
}

//停止前台服务并清除通知
fun stopForegroundNotification() {
mNotificationManager?.cancelAll()
service.stopForeground(true)
}
}

(2).忽略电池优化(Android 6.0+)


1.我们需要在AndroidManifest.xml中声明一下权限


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

2.通过Intent来请求忽略电池优化的权限(需要引导用户点击)


//在Activity的onCreate中注册ActivityResult,一定要在onCreate中注册
//监听onActivityForResult回调
mIgnoreBatteryResultContract = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { activityResult ->
//查询是否开启成功
if(queryBatteryOptimizeStatus()){
//忽略电池优化开启成功
}else{
//开启失败
}
}

通过Intent打开忽略电池优化弹框:


val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS)
intent.data = Uri.parse("package:$packageName")
//启动忽略电池优化,会弹出一个系统的弹框,我们在上面的
launchActivityResult(intent)

查询是否成功开启忽略电池优化开关:


fun Context.queryBatteryOptimizeStatus():Boolean{
val powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager?
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
powerManager?.isIgnoringBatteryOptimizations(packageName)?:false
} else {
true
}
}

(3).无障碍服务


看官方文档:创建自己的无障碍服务

它也是一个Service,它的优先级比较高,提供界面增强功能,初衷是帮助视觉障碍的用户或者是可能暂时无法与设备进行全面互动的用户完成操作。

可以做很多事情,使用了此Service,在6.0+不需要申请悬浮窗权限,直接使用WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY 挺方便的

(仅针对有需要此服务的app,可以开启增强后台保活)


(4).自启动权限(即:白名单管理列表页面)


是系统给用户自己去打开“自启动权限”开关的入口,我们需要针对不同的手机厂商和系统版本,弹出提示引导用户是否前去打开“自启动权限”

有的手机厂商叫:白名单管理,有的叫:自启动权限,两个是一个概念;

点击查看跳转到『手机自启动设置页面』完整代码


(需要注意:如果是代码控制跳转,无法保证永远可以调整,系统升级可能就给你屏蔽了,
最简单的方法是:显示一个如何找到自启动页面的引导图,下面以华为手机为例:)



华为手机-自启动管理

(5).多任务列表窗口加锁


可以针对不同手机厂商,显示引导用户,开启App窗口加锁之后,点击清理加速不会导致应用被kill



华为手机窗口加锁-教程图

(6).多任务列表窗口隐藏App窗口


刚刚上面多任务窗口加锁完,再提示用户去App里面把隐藏App窗口开关打开,这样用户就不会多任务列表里面把App窗口给手抖划掉


多任务窗口中『隐藏App窗口』,可以用如下代码控制:

(这个也只是针对有这方面需求App提供的一种增强方案罢了:因为隐藏了窗口,用户就不会去想他,不会去手痒去划掉它)


//在多任务列表页面隐藏App窗口
fun hideAppWindow(context: Context,isHide:Boolean){
try {
val activityManager: ActivityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
//控制App的窗口是否在多任务列表显示
activityManager.appTasks[0].setExcludeFromRecents(isHide)
}catch (e:Exception){
.....
}
}

(7).应用后台高耗电(Vivo手机独有)


开启的入口:“设置”>“电池”>“后台高耗电”>“找到xxxApp打开开关”



vivo允许后台高耗电



最后还是奉劝那些,仍然执着于找寻黑科技的开发者,醒醒吧,太阳晒屁股了。


如果说你的App用户群体不是普通用户,是专门给一些玩机大神们用的,都可以root手机的话,那么直接 move 到系统目录 priv/system/app 即可, 即使被用户强杀也会自动重新拉起。


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

Android 优雅处理重复点击(建议收藏)

一般手机上的 Android App,主要的交互方式是点击。用户在点击后,App 可能做出在页面内更新 UI、新开一个页面或者发起网络请求等操作。Android 系统本身没有对重复点击做处理,如果用户在短时间内多次点击,则可能出现新开多个页面或者重复发起网络请...
继续阅读 »

一般手机上的 Android App,主要的交互方式是点击。用户在点击后,App 可能做出在页面内更新 UI、新开一个页面或者发起网络请求等操作。Android 系统本身没有对重复点击做处理,如果用户在短时间内多次点击,则可能出现新开多个页面或者重复发起网络请求等问题。因此,需要对重复点击有影响的地方,增加处理重复点击的代码。


之前的处理方式


之前在项目中使用的是 RxJava 的方案,利用第三方库 RxBinding 实现了防止重复点击:


fun View.onSingleClick(interval: Long = 1000L, listener: (View) -> Unit) {
    RxView.clicks(this)
        .throttleFirst(interval, TimeUnit.MILLISECONDS)
        .subscribe({
            listener.invoke(this)
        }, {
            LogUtil.printStackTrace(it)
        })
}

但是这样有一个问题,比如使用两个手指同时点击两个不同的按钮,按钮的功能都是新开页面,那么有可能会新开两个页面。因为 Rxjava 这种方式是针对单个控件实现防止重复点击,不是多个控件。


现在的处理方式


现在使用的是时间判断,在时间范围内只响应一次点击,通过将上次单击时间保存到 Activity Window 中的 decorView 里,实现一个 Activity 中所有的 View 共用一个上次单击时间。


fun View.onSingleClick(
    interval: Int = SingleClickUtil.singleClickInterval,
    isShareSingleClick: Boolean = true,
    listener: (View) -> Unit
) {
    setOnClickListener {
        val target = if (isShareSingleClick) getActivity(this)?.window?.decorView ?: this else this
        val millis = target.getTag(R.id.single_click_tag_last_single_click_millis) as? Long ?: 0
        if (SystemClock.uptimeMillis() - millis >= interval) {
            target.setTag(
                R.id.single_click_tag_last_single_click_millis, SystemClock.uptimeMillis()
            )
            listener.invoke(this)
        }
    }
}

private fun getActivity(view: View): Activity? {
    var context = view.context
    while (context is ContextWrapper) {
        if (context is Activity) {
            return context
        }
        context = context.baseContext
    }
    return null
}

参数 isShareSingleClick 的默认值为 true,表示该控件和同一个 Activity 中其他控件共用一个上次单击时间,也可以手动改成 false,表示该控件自己独享一个上次单击时间。


mBinding.btn1.onSingleClick {
    // 处理单次点击
}

mBinding.btn2.onSingleClick(interval = 2000, isShareSingleClick = false) {
    // 处理单次点击
}

其他场景处理重复点击


间接设置点击


除了直接在 View 上设置的点击监听外,其他间接设置点击的地方也存在需要处理重复点击的场景,比如说富文本和列表。


为此将判断是否触发单次点击的代码抽离出来,单独作为一个方法:


fun View.onSingleClick(
    interval: Int = SingleClickUtil.singleClickInterval,
    isShareSingleClick: Boolean = true,
    listener: (View) -> Unit
) {
    setOnClickListener { determineTriggerSingleClick(interval, isShareSingleClick, listener) }
}

fun View.determineTriggerSingleClick(
    interval: Int = SingleClickUtil.singleClickInterval,
    isShareSingleClick: Boolean = true,
    listener: (View) -> Unit
) {
    ...
}

直接在点击监听回调中调用 determineTriggerSingleClick 判断是否触发单次点击。下面拿富文本和列表举例。


富文本


继承 ClickableSpan,在 onClick 回调中判断是否触发单次点击:


inline fun SpannableStringBuilder.onSingleClick(
    listener: (View) -> Unit,
    isShareSingleClick: Boolean = true,
    ...
): SpannableStringBuilder = inSpans(
    object : ClickableSpan() {
        override fun onClick(widget: View) {
            widget.determineTriggerSingleClick(interval, isShareSingleClick, listener)
        }
        ...
    },
    builderAction = builderAction
)

这样会有一个问题, onClick 回调中的 widget,就是设置富文本的控件,也就是说如果富文本存在多个单次点击的地方, 就算 isShareSingleClick 值为 false,这些单次点击还是会共用设置富文本控件的上次单击时间。


因此,这里需要特殊处理,在 isShareSingleClick 为 false 的时候,创建一个假的 View 来触发单击事件,这样富文本中多个单次点击 isShareSingleClick 为 false 的地方都有一个自己的假的 View 来独享上次单击时间。


class SingleClickableSpan(
    ...
) : ClickableSpan() {

    private var mFakeView: View? = null

    override fun onClick(widget: View) {
        if (isShareSingleClick) {
            widget
        } else {
            if (mFakeView == null) {
                mFakeView = View(widget.context)
            }
            mFakeView!!
        }.determineTriggerSingleClick(interval, isShareSingleClick, listener)
    }
    ...
}

在设置富文本的地方,使用设置 onSingleClick 实现单次点击:


mBinding.tvText.movementMethod = LinkMovementMethod.getInstance()
mBinding.tvText.highlightColor = Color.TRANSPARENT
mBinding.tvText.text = buildSpannedString {
    append("normalText")
    onSingleClick({
        // 处理单次点击
    }) {
        color(Color.GREEN) { append("clickText") }
    }
}

列表


列表使用 RecyclerView 控件,适配器使用第三方库 BaseRecyclerViewAdapterHelper。


Item 点击:


adapter.setOnItemClickListener { _, view, _ ->
    view.determineTriggerSingleClick {
        // 处理单次点击
    }
}

Item Child 点击:


adapter.addChildClickViewIds(R.id.btn1, R.id.btn2)
adapter.setOnItemChildClickListener { _, view, _ ->
    when (view.id) {
        R.id.btn1 -> {
            // 处理普通点击
        }
        R.id.btn2 -> view.determineTriggerSingleClick {
            // 处理单次点击
        }
    }
}

数据绑定


使用 DataBinding 的时候,有时会在布局文件中直接设置点击事件,于是在 View.onSingleClick 上增加 @BindingAdapte 注解,实现在布局文件中设置单次点击事件,并对代码做出调整,这个时候需要将项目中 listener: (View) -> Unit 替换成 listener: View.OnClickListener。


@BindingAdapter(
    *["singleClickInterval", "isShareSingleClick", "onSingleClick"],
    requireAll = false
)
fun View.onSingleClick(
    interval: Int? = SingleClickUtil.singleClickInterval,
    isShareSingleClick: Boolean? = true,
    listener: View.OnClickListener? = null
) {
    if (listener == null) {
        return
    }

    setOnClickListener {
        determineTriggerSingleClick(
            interval ?: SingleClickUtil.singleClickInterval, isShareSingleClick ?: true, listener
        )
    }
}

在布局文件中设置单次点击:


<androidx.appcompat.widget.AppCompatButton
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@string/btn"
    app:isShareSingleClick="@{false}"
    app:onSingleClick="@{()->viewModel.handleClick()}"
    app:singleClickInterval="@{2000}" />

在代码中处理单次点击:


class YourViewModel : ViewModel() {

    fun handleClick() {
        // 处理单次点击
    }
}

总结


对于直接在 View 上设置点击的地方,如果需要处理重复点击使用 onSingleClick,不需要处理重复点击则使用原来的 setOnClickListener。


对于间接设置点击的地方,如果需要处理重复点击,则使用 determineTriggerSingleClick 判断是否触发单次点击。


项目地址


https://github.com/TaylorKunZhang/single-click


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

【Flutter App】GetX框架的实践

正在做的这款App是一个打卡软件,旨在让用户能够更好地坚持自己所设置的目标,坚持自己的初心。由于项目还只是在前期阶段,目前根据需要建立了以下结构: 参考了部分官方插件以及结合官方getX文档中建议的目录:暂时没有对state分离出来一层的想法。 以下...
继续阅读 »

正在做的这款App是一个打卡软件,旨在让用户能够更好地坚持自己所设置的目标,坚持自己的初心。

由于项目还只是在前期阶段,目前根据需要建立了以下结构: image.png

参考了部分官方插件以及结合官方getX文档中建议的目录:

image.png

暂时没有对state分离出来一层的想法。 以下是各层详细内容:

image.png

image.png

在使用GetX的时候,往往每次都是用需要手动实例化一个控制器final controller = Get.put(CounterController());,如果每个界面都要实例化一次,有些许麻烦。使用Binding 能解决上述问题,可以在项目初始化时把所有需要进行状态管理的控制器进行统一初始化,直接使用Get.find()找到对应的GetxController使用。

  • 可以将路由、状态管理器和依赖管理器完全集成
  • 这里介绍三种使用方式,推荐第一种使用getx的命名路由的方式
  • 不使用binding,不会对功能有任何的影响。
  • 第一种:使用命名路由进行Binding绑定
/// 入口类
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
/// 这里使用 GetMaterialApp
/// 初始化路由
return GetMaterialApp(
initialRoute: RouteConfig.onePage,
getPages: RouteConfig.getPages,
);
}
}

/// 路由配置
class RouteConfig {
static const String onePage = "/onePage";
static const String twoPage = "/twoPage";

static final List<GetPage> getPages = [
GetPage(
name: onePage,
page: () => const OnePage(),
binding: OnePageBinding(),
),
// GetPage(
// name: twoPage,
// page: () => TwoPage(),
// binding: TwoPageBinding(),
// ),
];
}

/// binding层
class OnePageBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut(() => CounterController());
}
}

/// 逻辑层
class CounterController extends GetxController{
var count = 0;
/// 自增方法
void increase(){
count++;
update();
}
}
  • 第二种:使用initialBinding初始化所有的Binding
/// 入口类
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return GetMaterialApp(
/// 初始化所有的Binding
initialBinding: AllControllerBinding(),
home: const OnePage(),
);
}
}

/// 所有的Binding层
class AllControllerBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut(() => CounterController());
///Get.lazyPut(() => OneController());
///Get.lazyPut(() => TwoController());
}
}


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

收起阅读 »

美团跨端一体化富文本管理技术实践

为了减少产品和前端开发人员之间的矛盾,不断降本提效,美团医药技术部构建了跨端一体化富文本管理平台Page-佩奇。本文系统介绍了该平台的定位、设计思路、实现原理以及取得的成效。希望这些实战经验与总结,能给大家带来一些启发或帮助。一、引言在互联网圈,开发和产品经理...
继续阅读 »



为了减少产品和前端开发人员之间的矛盾,不断降本提效,美团医药技术部构建了跨端一体化富文本管理平台Page-佩奇。本文系统介绍了该平台的定位、设计思路、实现原理以及取得的成效。希望这些实战经验与总结,能给大家带来一些启发或帮助。

一、引言

在互联网圈,开发和产品经理之间相爱相杀的故事,相信大家都有所耳闻。归根结底,往往都是从简单的改需求开始,然后你来我往、互不相让,接着吵架斗嘴,最后导致矛盾不断升级,甚至带来比较严重的后果。

pic_4bf1ae9b.png

图1

在这种背景下,如果把一些功能相对简单的、需求变动比较频繁的页面,直接交给产品或者运营自己去通过平台实现,是不是就可以从一定程度上减少产品和开发人员之间的矛盾呢?

二、背景

当然上述的情况,美团也不例外。近些年,美团到家事业群(包括美团外卖、美团配送、闪购、医药、团好货等)的各个业务稳步发展,业务前端对接的运营团队有近几十个,每个运营团队又有不同的运营规则,这些规则还存在一些细微的样式差别,同时规则内容还会随着运营季节、节日、地理位置等进行变化和更新。这些需求具体来说有以下几个特点:

  1. 需求量大:业务稳步发展,业务需求不断叠加,甚至部分业务呈指数级增长,且业务方向涉及到一些业务规则、消息通知、协议文档、规则介绍等需求。

  2. 变更频繁:面对市场监管和法务的要求,以及新业务调整等因素的影响,会涉及到需求的频繁变更,像一些业务FAQ、产品介绍、协议文档、业务规则、系统更新日志等页面,需要做到快速响应和及时上线。

  3. 复杂度低:这些页面没有复杂的交互逻辑,如果能把这些简单的页面交给运营/产品去实现,开发人员就能有更多的时间去进行复杂功能的研发。

  4. 时效性高:临时性业务需求较多,且生命周期较短,具有定期下线和周期性上线等特点。

基于以上特点,为了提高研发效率,美团医药技术部开始构建了一个跨端一体化富文本管理平台,希望提供解决这一大类问题的产研方案。不过,部门最初的目标是开发一套提效工具,解决大量诸如帮助文档、协议页、消息通知、规则说明等静态页面的生产与发布问题,让产品和运营同学能够以所见即所得的方式自主完成静态页面制作与发布,进而缩短沟通成本和研发成本。

但是,随着越来越多业务部门开始咨询并使用这个平台,我们后续不断完善并扩充了很多的功能。经过多次版本的设计和迭代开发后,将该平台命名为Page-佩奇,并且注册成为美团内部的公共服务,开始为美团内部更多同学提供更好的使用体验。

本文将系统地介绍Page-佩奇平台的定位、设计思路、实现原理及取得成效。我们也希望这些实战经验与总结,能给更多同学带来一些启发和思考。

三、跨端一体化富文本管理解决方案

3.1 平台定位

我们希望将Page-佩奇打造成一款为产品、运营、开发等用户提供快速一站式发布网页的产研工作台,这是对该平台的一个定位。

  • 对产品运营而言,他们能够可视化地去创建或修改一些活动说明、协议类、消息类的文章,无需开发排期,省去向开发二次传递消息等繁琐的流程,也无需等待漫长的发布时间,从而达到灵活快速地进行可视化页面的发布与管理。

  • 对开发同学而言,他们能够在线编写代码,并实现秒级的发布上线,并且支持ES 6、JavaScript 、Less、CSS语法,我们还提供了基础的工具、图表库等,能够生成丰富多样的页面。帮助开发同学快速实现数据图表展示,设计特定样式,完成各种交互逻辑等需求。

  • 对项目管理方而言,他们能够清晰地看到整个需求流转状态和开发日志信息,为运营管理提供强大的“抓手”。

一般来讲,传统开发流程是这样的:首先产品提出需求,然后召集研发评审,最后研发同学开发并且部署上线;当需求上线之后,如果有问题需要反馈,产品再找研发同学进行沟通并修复,这种开发流程也是目前互联网公司比较常见的开发流程。

pic_b81cd143.png

图2 传统开发流程图

而美团Page-佩奇平台的开发流程是:首先产品同学提出需求,然后自己在Page平台进行编辑和发布上线,当需求上线之后有问题需要反馈,直接就能触达到产品同学,他们通常可自行进行修复。如果需求需要定制化,或者需要做一些复杂的逻辑处理,那么再让研发人员配合在平台上进行开发并发布上线。

pic_b3e5d331.png

图3 Page-佩奇平台开发流程图

简单来说,对那些功能相对简单、需求变动比较频繁的页面,如果用传统的开发流程将会增加产研沟通和研发排期成本,因此传统方案主要适用于功能复杂型的需求。而Page-佩奇平台开发流程,并不适合功能复杂型的需求,特别适用于功能相对简单、需求变动比较频繁的页面需求。

综上所述,可以看出这两种开发流程其实起到了一个互补的作用,如果一起使用,既可以减少工作量,又可以达到降本提效的目的。

3.2 设计思路

我们最初设计Page-佩奇平台的初心其实很简单,为了给产品和运营提供一个通过富文本编辑器快速制作并发布网页的工具。但是,在使用的过程中,很多缺陷也就慢慢地开始暴露,大致有下面这些问题:

  1. 简单的富文本编辑器满足不了想要的页面效果,怎么办?

  2. 如果能导入想要的模板,是否会更友好?

  3. 怎么查看这个页面的访问数据?如何能监控这个页面的性能问题?

  4. 发布的页面是否有存在安全风险?

于是,我们针对这些问题进行了一些思考和调研:

  • 当富文本编辑器满足不了想要实现的效果的时候,可以引入了WebIDE编辑器,可以让研发同学再二次编辑进行实现。

  • 一个系统想要让用户用得高效便捷,那么就要完善它的周边生态。就需要配备完善的模板素材和物料供用户灵活选择。

  • 如果用户想要了解页面的运行情况,那么页面运行的性能数据、访问的数据也是必不可少的。

  • 如果发布的内容存在不当言论,就会造成不可控的法律风险,所以内容风险审核也是必不可少的。

实现一个功能很容易,但是想要实现一个相对完善的功能,就必须好好下功夫,多思考和多调研。于是,围绕着这些问题,我们不断挖掘和延伸出了一系列功能:

  1. 富文本编辑:强大而简单的可视化编辑器,让一切操作变得简单、直观。产品同学可以通过编辑器自主创建、编辑网页,即使无程序开发经验也可以通过富文本编辑器随意操作,实现自己想要的效果,最终可以实现一键快速发布上线。

  2. WebIDE:定制化需求,比如,与客户端和后端进行一些通信和请求需求,以及针对产品创建的HTML进行二次加工需求,均可以基于WebIDE通过JavaScript代码实现。具备专业开发经验的同学也可以选择通过前端框架jQuery、Vue,Echarts或者工具库Lodash、Axios实现在线编辑代码。

  3. 页面管理:灵活方便地管理页面。大家可以对有权限的文档进行查看、编辑、授权、下线、版本对比、操作日志、回滚等操作,且提供便捷的文档搜索功能。

  4. 模板市场:丰富多样的网页模板,简易而又具备个性。模板市场提供丰富的页面模板,大家可选择使用自己的模板快速创建网页,且发布的每个页面又可以作为自己的模板,再基于这个模板,可随时添加个性化的操作。

  5. 物料平台:提供基础Utils、Echart、Vue、jQuery等物料,方便开发基于产品的页面进行代码的二次开发。

  6. 多平台跨端接入:高效快捷地接入业务系统。通过通信SDK,其他系统可以快速接入Page-佩奇平台。同时支持以HTTP、Thrift方式的开放API供大家选择,支持客户端、后端调用开放API。

  7. 内容风险审核:严谨高效的审核机制。接入美团内部的风险审核公共服务,针对发布的风险内容将快速审核,防止误操作造成不可控的法律风险。

  8. 数据大盘:提供页面的数据监测,帮助大家时刻掌握流量动向。接入美团内部一站式数据分析平台,帮助大家安全、快速、高效地掌握页面的各种监测数据。

  9. 权限管理:创建的每个页面都有相对独立的权限,只有经过授权的人才能查看和操作该页面。

  10. 业务监控:提供页面级别JavaScript错误和资源加载成功率等数据,方便开发排查和解决线上问题。

功能流程图如下所示:

pic_9d007bab.png

图4 Page-佩奇平台功能流程图

3.3 实现原理

3.3.1 基础服务

Page-佩奇平台的基础服务有四个部分,包括物料服务、编译服务、产品赋能、扩展服务。

pic_f0bf9912.png

图5 整体架构图

3.3.2 核心架构

pic_bbb3bb9a.png

图6 核心架构图

Page-佩奇平台核心架构主要包含页面基础配置层、页面组装层以及页面生成层。我们通过Vuex全局状态对数据进行维护。

  • 页面基础配置层主要提供生成页面的各种能力,包括富文本的各种操作能力、编辑源码(HTML、CSS、JavaScript)的能力、自定义域名配置、适配的容器(PC/H5)、发布环境等。

  • 页面组装层则会基于基础配置层所提供的的能力,实现页面的自由编辑,承载大量的交互逻辑,用户的所有操作都在这一层进行。

    • 业务PV和UV埋点,错误统计,访问成功率上报。

    • 自动适配PC和移动端样式。

    • 内网页面显示外网不可访问标签。

  • 页面生成层则需要根据组装后的配置进行解析和预处理、编译等操作,最终生成HTML、CSS、JavaScript渲染到网页当中。

3.3.3 关键流程

pic_b5adef78.png

图7 关键流程图

如上图7所示,平台的核心流程主要包含页面创建之后的页面预览、编译服务、生成页面。

  • 页面预览:创建、编辑之后的页面,将会根据内容进行页面重组,对样式和JavaScript进行预编译之后,对文本+JavaScript+CSS进行组装,生成HTML代码块,然后将代码块转换成Blob URL,最终以iframe的方式预览页面。

  • 编译服务:文件树状结构和代码发送请求到后端接口,基于Webpack将Less编译成CSS,ES 6语法编译成ES 5。通用物料使用CDN进行引入,不再进行二次编译。

  • 生成页面:当创建、编辑之后的页面进行发布时,服务端将会进行代码质量检测、内容安全审查、代码质量检测、单元测试、上传对象存储平台、同步CDN检测,最终生成页面链接进行访问。

3.3.4 多平台接入

Page-佩奇平台也可以作为一个完善的富文本编辑器供业务系统使用,支持内嵌到其他系统内。作为消息发布等功能承载,减少重复的开发工作,同时我们配备完善的SDK供大家选择使用。通过Page-SDK可以直接触发Page平台发布、管理等操作,具体的流程如下图所示:

pic_e3c8e777.png

图8 Page-SDK流程图

3.3.5 Open API

在使用Page-佩奇平台的时候,美团内部一些业务方提出想要通过Page-佩奇平台进行页面的发布,同时想要拿到发布的内容做一些自定义的处理。于是,我们提供了Open API开放能力,支持以HTTP和Thrift两种方式进行调用。下面主要讲一下Thrift API实现的思路,首先我们先了解下Thrift整体流程:

pic_a6286e54.png

图9 Thrift整体流程图

Thrift的主要使用过程如下:

  1. 服务端预先编写接口定义语言 IDL(Interface Definition Language)文件定义接口。

  2. 使用Thrift提供的编译器,基于IDL编译出服务语言对应的接口文件。

  3. 被调用服务完成服务注册,调用发起服务完成服务发现。

  4. 采用统一传输协议进行服务调用与数据传输。

下面具体讲讲,Node语言是如何实现和其他服务语言实现调用的。由于我们的服务使用的Node语言,因此我们的Node服务就充当了服务端的角色,而其他语言(Java等)调用就充当了客户端的角色。

pic_ca36c11d.png

图10 Thrift使用详细流程图

  • 生成文件:由服务端定义IDL接口描述文件,然后基于IDL文件转换为对应语言的代码文件,由于我们用的是Node语言,所以转换成JavaScript文件。

  • 服务端启动服务:引入生成的JavaScript文件,解析接口、处理接口、启动并监听服务。

  • 服务注册:通过服务器内置的“服务治理代理”,将服务注册到美团内部的服务注册路由中心(也就是命名服务),让服务可被调用方发现。

  • 数据传输:被调用时,根据“服务治理服务”协议序列化和反序列化,与其他服务进行数据传输。

目前,美团内部已经有相对成熟的NPM包服务,已经帮我们实现了服务注册、数据传输、服务发现和获取流程。客户端如果想调用我们所提供的的Open API开放能力,首先申请AppKey,然后选择使用Thrift方式或者HTTP的方式,按照所要求的参数进行请求调用即可。

3.4 方案实践

3.4.1 H5协议

能力:富文本编辑。

描述:提供富文本可视化编辑,产品和运营无需前端就可以发布和二次编辑页面。

场景:文本协议,消息通知,产品FAQ。

具体案例:

pic_f883a4d1.png

图11 H5静态文本协议案例

3.4.2 业务自定义渲染

能力:开放API(Thirft + HTTP)。

描述:提供开放API,支持业务自定义和样式渲染到业务系统,同时解决了iframe体验问题。

场景:客户端、后端、小程序的同学,可根据API渲染文案,实现动态化管理富文本信息。

具体案例:

小程序使用组件、Vue使用v-html指令实现动态化渲染商品选择说明。

{
   "code": 0,
   "data": {
     "tag": "苹果,标准",
     "title": "如何挑选苹果",
     "html": "<h1>如何挑选苹果</h1>><p>以下标准可供消费者参考</p><ul><li>酸甜</li><li>硬度</li></ul>",
     "css": "",
     "js": "",
     "file": {}
  },
   "msg": "success"
}

3.4.3 投放需求

能力:WebIDE代码编辑。

描述:开发基于WebIDE代码开发工作,基于渠道和环境修改下载链接,能够做到分钟级支撑。

场景:根据产品创建静态页面进行逻辑和样式开发。

具体案例:

var ua = window.navigator.userAgent
   var URL_MAP = {
       ios: 'https://apps.apple.com/cn/app/xxx',
       android: 'xxx.apk',
       ios_dpmerchant: 'itms-apps://itunes.apple.com/cn/app/xxx'
  }
   
   if (ua.match(/android/i)) location.href = URL_MAP.android
   if (ua.match(/(ipad|iphone|ipod).*os\s([\d_]+)/i)) {
       if (/xx\/com\.xxx\.xx\.mobile/.test(ua)) {
           location.href = URL_MAP.ios_dpmerchant
      } else {
           location.href = URL_MAP.ios
      }
  }

3.4.4 客户端通信中间页

能力:WebIDE代码编辑 + 物料平台。

描述:通过物料平台,引入公司客户端桥SDK,可以快速完成客户端通信需求。方便前端调试客户端基础桥功能。

场景:客户端跳转,通信中间页。

具体案例:

// 业务伪代码
   XXX.ready(() => {
       XXX.sendMessage({
          sign: true,
           params: {
               id: window.URL
          }
      }, () => {
           console.error('通信成功')
      }, () => {
           console.error('通信失败')
      })
  })

3.4.5 业务系统内嵌Page

能力:提供胶水层Page-SDK,连接业务系统和Page。

描述:业务系统与Page-佩奇平台可进行通信,业务系统可调用Page发布、预览、编辑等功能,Page可返回业务系统页面链接、内容、权限等信息。减少重复前后端工作,提升研发效率。

场景:前端富文本信息渲染,后端富文本信息管理后台。

具体案例:

pic_ec5b5f49.png

图12 业务系统内嵌Page案例

3.5 业务成绩

截止目前数据统计,Page-佩奇平台生成网页5000多个,编辑页面次数16000多次,累计页面访问PV超过8260万。现在,美团已经有十多个部门和三十多条业务线接入并使用了Page-佩奇平台。

pic_becbff8a.png

图13 Page-佩奇平台每日生成页面统计

四、总结与展望

富文本编辑器和WebIDE不仅是复杂的系统,而且还是比较热门的研究方向。特别是在和美团的基建结合之后,能够解决团队内部很多效率和质量问题。这套系统还提供了语法智能提示、Diff对比、前置检测、命令行调试等功能,不仅要关注业务发布出去页面的稳定性和质量,更要有内置的一系列研发插件,主动帮助研发提高代码质量,降低不必要的错误。

经过长期的技术和业务演进,Page-佩奇平台已经能够有效地帮助研发人员大幅提升开发效率,具备初级的Design To Code能力,但是仍有许多业务场景值得去我们探索。我们也期待优秀的你参与进来,一起共同建设。

  • WebIDE融合:完善基础设施建设和功能需求,更好地支持Vue、React、ES 6、TS、Less语法,预览模式采用浏览器编译,能有效地提高预览的速度,发布使用后端编译的模式。

  • 研发流程链路:针对代码进行有效评估,包括ESlint、代码重复率、智能提示是否可以三方库替代。出具开发代码质量、业务上线的质量报告。

  • 综合研发平台:减少团队同学了解整体基建的时间成本,内置了监控、性能、任务管理等功能,提升业务开发效率。建设自动化日报、周报系统,降低非开发工作量占比。

  • 物料开放能力:接入公共组件平台,沉淀更多的物料,快速满足产品更多样化的需求。

五、作者简介

高瞻、宇立、肖莹、浩畅,来自美团医药终端团队。王咏、陈文,来自美团闪购终端团队。
来源:https://blog.csdn.net/MeituanTech/article/details/121551030

收起阅读 »

Gradle 与 AGP 构建 API: 配置您的构建文件

欢迎阅读全新的 MAD Skills 系列 之 Gradle 及 Android Gradle plugin API 的第一篇文章。我们将在本文中了解 Android 构建系统的工作方式以及 Gradle 的基础知识。 我们将会从 Gradle 的构建阶段开始...
继续阅读 »

欢迎阅读全新的 MAD Skills 系列GradleAndroid Gradle plugin API 的第一篇文章。我们将在本文中了解 Android 构建系统的工作方式以及 Gradle 的基础知识。


我们将会从 Gradle 的构建阶段开始,讨论如何使用 AGP (Android Gradle Plugin) 的配置选项自定义您的构建,并讨论如何使您的构建保持高效。如果您更喜欢通过视频了解此内容,请在 此处 查看。


通过了解构建阶段的工作原理及配置 Android Gradle plugin 的配置方法,可以帮您基于项目的需求自定义构建。让我们回到 Android Studio,一起看看构建系统是如何工作的吧。


Gradle 简介


Gradle 是一个通用的自动化构建工具。当然,您可以使用 Gradle 来构建 Android 项目,但实际上您可以使用 Gradle 来构建任何类型的软件。


Gradle 支持单一或多项目构建。如果要将项目配置为使用 Gradle,您需要在项目文件夹中添加 build.gradle 文件。


在多项目层级结构中,根项目中会包含一个 settings.gradle 文件,其中列出了构建中包含的其他项目。Android 使用多项目构建来帮您模块化应用。


△ Android 项目结构与 build.gradle 及 settings.gradle 文件


△ Android 项目结构与 build.gradle 及 settings.gradle 文件


由于插件的存在,Gradle 可以处理不同类型的项目,比如 Android 或 Java。这些插件会包含预定义的功能,用于配置和构建特定类型的项目。


例如,为了构建 Android 项目,您需要使用 Android Gradle 插件配置您的 Gradle 构建文件。无论当前的 Android 项目是应用还是依赖库,Android Gradle 插件都知道如何对其进行构建和打包。


Task (任务)


Gradle 的构建流程围绕名为 Task (任务) 的工作单元展开。您可以通过终端查看 Task 列表,或通过启用 Android Studio Gradle 面板中的 Task 列表来查看任务。


△ Gradle Task 列表


△ Gradle Task 列表


这些 Task 可以接收输入、执行某些操作,并根据执行的操作产生输出。


Android Gradle Plugin 定义了自己的 Task,并且知道构建 Android 项目时,需要以何种顺序执行这些 Task。


Gradle 构建文件由许多不同的部分组成。Gradle 的配置语法被称为 Gradle DSL,其为开发者定义了配置插件的方式。Gradle 会解析 build.gradle 文件中的 android DSL 块并创建 AGP DSL 对象,例如 ApplicationExtensionBuildType


典型的 Android 项目会包含一个顶层 Gradle 构建文件。Android 项目中的每个模块又分别有一个 Gradle 构建文件。在示例项目中,我仅有一个应用模块。


在模块层的 build.gradle 文件中,我需要声明和应用构建项目所需的插件。为了让 Gradle 知道我正在构建 Android 项目,我需要应用 com.android.applicationcom.android.library 插件。这两个插件分别定义了如何配置和构建 Android 应用和依赖库。在本例中,我要构建的是 Android 应用项目,所以我需要应用 com.android.application 插件。由于我需要使用 Kotlin,所以在示例中也应用了 kotlin.android 插件。


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

Android Gradle Plugin 提供了它自己的 DSL,您可以用它配置 AGP,并使该配置在构建时应用于 Task。


想要配置 Android Gradle Plugin,您需要使用 android 块。在该代码块中,您可以为不同的构建类型 (如 debug 或 release) 定义 SDK 版本、工具版本、应用详情及其它一些配置。如需了解更多有关 gradle 如何使用这些信息来创建变体,以及您可以使用哪些其他选项,请参阅 构建文档:


android {
compileSdk 31

defaultConfig {
applicationId "com.example.myapp"
minSdk 21
targetSdk 31
versionCode 1
versionName "1.0"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}

在下一部分中,您可以定义依赖。Gradle 的依赖管理支持兼容 MavenIvy 的仓库,以及来自文件系统的本地二进制文件。


dependencies {

implementation 'androidx.core:core-ktx:1.7.0'
implementation 'com.google.android.material:material:1.4.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.0'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'

}

构建阶段


Gradle 分三个阶段评估和运行构建,分别是 Initialization (初始化)、Configuration (配置) 和 Execution (执行),更多请参阅 Gradle 文档


在 Initialization (初始化) 阶段,Gradle 会决定构建中包含哪些项目,并会为每个项目创建 Project实例。为了决定构建中会包含哪些项目,Gradle 首先会寻找 settings.gradle 来决定此次为单项目构建还是多项目构建。


在 Configuration (配置) 阶段,Gradle 会评估构建项目中包含的所有构建脚本,随后应用插件、使用 DSL 配置构建,并在最后注册 Task,同时惰性注册它们的输入。


需要注意的是,无论您请求执行哪个 Task,配置阶段都会执行。为了保持您的构建简洁高效,请避免在配置阶段执行任何耗时操作。


最后,在 Execution (执行) 阶段,Gradle 会执行构建所需的 Task 集合。


下篇文章中,在编写我们自己的插件时,我们将深入剖析这些阶段。


Gradle DSL 支持使用 Groovy 与 Kotlin 脚本编写构建文件。到目前为止,我都在使用 Groovy DSL 脚本来配置此工程的构建。您可以在下面看到分别由 Kotlin 和 Groovy 编写的相同构建文件。注意 Kotlin 脚本文件名后缀为 ".kts"。


△ Kotlin 与 Groovy 脚本对比


△ Kotlin 与 Groovy 脚本对比


从 Groovy 迁移到 Kotlin 或其他配置脚本的方法,不会改变您执行 Task 的方式。


总结


以上便是本文的全部内容。Gradle 与 Android Gradle Plugin 有许多可以让您自定义构建的功能。在本文中,您已经了解了 Gradle Task、构建阶段、配置 AGP 以及使用 DSL 配置构建的基础知识。



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

换一个方式组织你的Axios代码?

自从Jquery被mvvm平替了之后,$.ajax 也被 axios 平替了,在使用这个方式之前,我想大部分的人也想到了去封装一个请求,然后每一次调用去做Get 、Post的请求服务,也有一些人习惯在vue里直接编写 this.$axios.get() ,萝卜...
继续阅读 »

自从Jquery被mvvm平替了之后,$.ajax 也被 axios 平替了,在使用这个方式之前,我想大部分的人也想到了去封装一个请求,然后每一次调用去做Get 、Post的请求服务,也有一些人习惯在vue里直接编写 this.$axios.get() ,萝卜青菜各有所爱,没有优劣之分。



灵感来源


d4axios的灵感来源于open-figen,现在功能还没有那么丰富,但是足以应付多大多数的场景,比如上传、下载、get、post等等请求,配合上ts,可以解决数据类型前后台一致性,数据类型的转换,保证了请求的便利性。让代码专注于数据处理,而非复制粘贴模版代码



别忘了 在 [ts | js]config.json 文件里开启对装饰器的支持。"experimentalDecorators":true



在项目中使用 d4axios



d4axios (Decorators for Axios) 是一款基于axios请求方式的装饰器方法组,可以快速地对服务进行管理和控制,增强前端下服务请求方式,有效降低服务的认知难度,并且在基于ts的情况下可以保证服务的前后台的数据接口一致性



npm i d4axios

yarn add d4axios

一、 引入配置信息


在这里提供了几种配置方式,可以采用原始的axios的配置方法,也可以采用 d4axios 提供的方法


// 在 vue3下我们建议使用 `createService` 
// 其他情况下使用 `serviceConfig`
import { createApp } from 'vue'
import {createService,serviceConfig} from 'd4axios'


createApp(App).use(createService({ ... /* 一些配置信息 */}))


1.1 提供的axios配置项


createServiceserviceConfig 使用的配置项是一样的,并且完全兼容axios的配置。在现有的项目中改造的话,可以使用:


// 可以直接使用由d4axios提供的服务
createService()

// 可直接传入axios的相关配置,由d4axios自动基于配置相关构建
createService({axios:{ baseURL:"domain.origin" }})

// 可直接传入已经配置好的 `axios` 实例对象
const axios = Axios.create({ /* 你的配置*/ });


createService({axios})

1.2 提供基于请求和相应数据的配置


createService({
beforeRequest(requestData){
// form对象会被转为JSON对象的浅拷贝,但是会在该方法执行完后重新转为form对象
// 你可在请求前追加一些补充的请求参数
// 比如对请求体进行签名等等
return requestData
},
beforeResponse(responseData){
// 默认情况下会返回 axios的response.data值,而不会返回response的完整对象
// 可以修改返回的响应结果
return responseData
}
})

1.3 提供快速的axios interceptors 配置


createService({
interceptors:{
request:{
use(AxiosRequestConfig){},
reject(){}
},
response{
use(AxiosResponse){},
reject(){}
}
}
})

配置完成后,会返回一个axios实例对象,可以继续对axios对象做更多的操作,可以绑定到vue.prototype.$axios下使用


Vue.prototype.$axios = serviceConfig({... /*一些配置*/})

二、创建请求服务


为了更好的组织业务逻辑,d4axios提供了一系列的组织方法,供挑选使用


import {Service,Prefix,Get,Post,Download,Upload,Param,After,Header} from 'd4axios'

@Service("UserService") // 需要提供一个服务名称,该名称将在使用服务的时候被用到
@Prefix("/user") // 可以给当前的服务添加一个请求前缀
export class UserService {

@Get("/:id") // 等同于 /user/:id
@After((respData)=>{
//在输出给最终结果前,可以对结果做一些简单处理
return respData
})
async getUserById(id:string){
// 异步请求需要增加 `async` 属性以便语法识别
// 支持restful的方式
}


@Post("/regist")
@Header({'plantform':'android'}) // 请求前追加一些header参数
async registUser(form:UserForm){
// 可以在请求的时候做一些参数修改
form.nickName = createNickName(form.nickName);

// return的值是最终请求的参数
return {
...form,
plant:"IOS"
};
}

@Download("/user/card") // 支持文件下载
async downloadCard(@Param("id") id:stirng){
// 当我们的参数较少并且不是一个key-value形式的值时
// 可以使用@Param辅助,确定传参名称
}

@Upload("/user/card") // 支持文件上传
async uploadCard(file:File){
return {file}
}

// 可以定义同步函数,直接做服务计算
someSyncFunc(){
return 1+1
}

// 我们还可以直接定义非请求函数
async someFunc(){
// 所有的当前函数都是可以直接调用的
return await this.getUserById(this.someSyncFunc());
}

}


三、使用服务



使用服务分为几种方式,第一种是在一个服务中调用另一个服务。第二种是在react或者vue中调用服务,对于这两种有不同的方法,也可以用相同的方法。



3.1 在 vue或者react中使用useService 导入服务


// 在 vue 或者 react中,可以直接使用 useService 导入一个服务对象
import {useService} from 'd4axios'
import SomeService from './some.service'

const someService = useService(SomeService)

复制代码

3.2 在一个服务中Use调用另一个服务


import {Use} from 'd4axios'
import SomeService from './some.service'
// 也可以直接像上面一样的导入进来是用
const someService = useService(SomeService)

@Service("MyService")
export class MyService {
@Use(SomeService) // use 导入服务
// 默认的属性名为小写驼峰
// 用 S<T> 包裹服务名称,这样可以得到相应的async方法的响应类型
someService !: S<SomeService>

async someMethod(){
// 就可以使用了,
await this.someService.something();
}
}

四、响应重写


默认情况下,d4axios支持async响应类型值,该值为


 export interface ResponseDataType<T> { }

在项目根路径下定义 d4axios.d.ts文件
然后文件内定义,通过重写该类型,可以得到响应的 response type类型,比如



export interface ResponseDataType<T> {
data : T;
msg:string ;
code:string ;
}

后即可以得到相关内容的提示信息


dataType.png


五、其他一些基于 Decorators 的操作


5.1 在使用装饰器的class上都可以使用 Use 导入服务 比如:


import {Component,Vue,} from 'vue-class-decorator'
import SomeService from './some.service'

@VueServiceBind(MyService,OtherService) // 只能在vue的这种形式下使用,可以绑定多个值
@Component
export default class MyVueComp extends Vue {

@Use(SomeService) // use 导入服务
// 默认的属性名为小写驼峰
// 用 S<T> 包裹服务名称,这样可以得到相应的async方法的响应类型
someService !: S<SomeService>

myService !: S<MyService>

otherService !: S<OtherService>
}

5.2 在一般的vue的服务下可以使用这种 mapService 形式


// 传统的模式下

import { mapService } from 'd4axios';
import MyService from './MyService.service'

export default {
computed:{
...mapService(MyService,OtherService)
},
created(){
this.myService.getName(10086);
}

作者:非思不可
链接:https://juejin.cn/post/7041930275458285582

收起阅读 »

如何在浏览器 console 控制台中播放视频?

如何在浏览器 console 控制台中播放视频? 要实现这个目标,主要涉及到这几个点: 如何获取和解析视频流? 如何在 console 里播放动态内容? 如何在 console 里播放彩色内容? 如何连接视频流和 console? 事实上最后的代码极其简单...
继续阅读 »

如何在浏览器 console 控制台中播放视频?


要实现这个目标,主要涉及到这几个点:



  1. 如何获取和解析视频流?

  2. 如何在 console 里播放动态内容?

  3. 如何在 console 里播放彩色内容?

  4. 如何连接视频流和 console?


事实上最后的代码极其简单,我们就一步一步简单讲一下


效果



测试地址:yu-tou.github.io/colors-web/…


如何获取和解析视频流?


这里我们用电脑摄像头捕获视频流,然后获取视频流每一帧的图像数据,作为下一步的输入。


// 捕捉电脑摄像头的视频流
const mediaStream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true,
});
// 创建一个 video 标签
const video = document.createElement("video");
document.body.appendChild(video);

video.onloadedmetadata = function (e) {
video.play(); // 等摄像头数据加载完成后,开始播放
};
// video 标签播放视频流
video.srcObject = mediaStream;

如何获取每一帧图像的数据?创建一个 canvas 画布,可以将 video 当前的内容绘制到画布上,然后通过 canvas 的方法即可拿到图像的像素数据。


const ctx = canvas.getContext("2d");
canvas.width = width;
canvas.height = height;

ctx.drawImage(video, 0, 0, width, height);
const imageData = ctx.getImageData(0, 0, width, height);
const data = imageData.data;
// imageData 的结构是平铺的,需要自己去学习下

如何在 console 里播放动态内容?


视频每帧的图像内容我们已经可以拿到了,继续下一步,如果需要在 console 中完成播放视频,首先需要能够一帧一帧绘制内容,但是这个好像是不太现实的,console.log 只能输出文本。


回想远古时代,在终端里大家怎么播放视频的?没错,用字符画一帧一帧绘制,连起来不就是动态的视频了。


当然 chrome dev tool 里如果每一帧绘制后都调用 console.clear() 清空重绘,体验不是很好,闪烁会很严重,所以我们采用持续输出的方式绘制,当你停留在 console 的最后的时候,看起来也算是动态内容了。


如何在 console 里播放彩色内容?


console.log 支持部分 css 特性,可以为输出的字符串指定简单的样式,最基本的支持背景色、字体颜色、下划线等,甚至支持 background-image、padding 等特性,利用这些特性,甚至可以插入图片,但是这些特性在不同浏览器的 console 中或多或少有些兼容问题,不过要实现字体着色,或者输出色块(用背景色),问题不大。


我们在此使用 colors-web 来更方便地输出彩色内容到控制台。


这是一个非常方便的库,可以使用链式调用在控制台快速输出彩色内容,并且支持诸多特性,无需自己去了解,直接使用对应的方法即可。


如:


import { logger, colors } from "colors-web";
logger(
colors().red().fontsize(48).fontfamily("SignPainter").log("hello"),
colors().white.redBg("hello").linethrough(),
"world",
colors().white.padding(2, 5).underline().lightgrey("芋头")
);

相信我不解释,大家也基本理解这些用法,非常简单和自由,而且支持 typescript。


我们这里,用 colors-web 输出色块:


for (let i = 0; i < height; i++) {
for (let j = 0; j < width; j++) {
if (i * width + j < data.length) {
const color = `rgb(${data[(i * width + j) * 4 + 0]},${data[(i * width + j) * 4 + 1]},${
data[(i * width + j) * 4 + 2]
})`;
colors()
.bg(color)
.color(color)
.fontfamily(/Chrome/.test(navigator.userAgent) ? "Courier" : "")
.log("╳");
}
}
}

最终逻辑


最终我将每一帧所有的像素值都转换成一个 colors 的实例,记录到数组之后,最终统一调用 logger 即可完成一帧的渲染。


const frameColors = [];
for (let i = 0; i < height; i++) {
for (let j = 0; j < width; j++) {
if (i * width + j < data.length) {
const color = `rgb(${data[(i * width + j) * 4 + 0]},${data[(i * width + j) * 4 + 1]},${
data[(i * width + j) * 4 + 2]
})`;
frameColors.push(
colors()
.bg(color)
.color(color)
.fontfamily(/Chrome/.test(navigator.userAgent) ? "Courier" : "")
.log("╳")
);
}
}
}
// 绘制,colors() 只是在准备数据结构,logger 才是真正的输出
logger(...frameColors);

大公告成啦!


作者:芋头君
链接:https://juejin.cn/post/7013620775143866376

收起阅读 »

某科技公司前端三面面经

okay, it's me again. 哈哈哈我怎么也没想到,我又会经历多一次三面,这次可以说是被狠狠的按在地上摩擦了,没办法,只能奉行一贯的“技术不够,吹牛来凑”原则 btw 应该看多点别人的面经,而不是自己写面经,当然自己写也可以当作一个很好的复盘 这次...
继续阅读 »

okay, it's me again.


哈哈哈我怎么也没想到,我又会经历多一次三面,这次可以说是被狠狠的按在地上摩擦了,没办法,只能奉行一贯的“技术不够,吹牛来凑”原则


btw 应该看多点别人的面经,而不是自己写面经,当然自己写也可以当作一个很好的复盘


这次是某家准备上市的公司,公司的技术部门也是挺强大的,所以也才会有三面吧可能


其实复盘过程中记的也不太清楚,只能说想起来一点写一点,这里建议将整个面试过程录音,以便做一次彻底的复盘,当然最好还是取得面试官同意才这么做


面试流程:boss直聘聊 -> 发邮件邀约面试 -> 一面技术面 -> 二面技术面 -> 三面HR面 -> 电话沟通薪资和入职事宜 -> offer


一面技术面




  1. 自我介绍




  2. 一个业务场景,PC端用vue做后台管理系统的时候,一般路由是动态生成的,前端的文件与路由是一一对应的,假如不小心删了一个文件,这个时候就会跳404页面,会有不好的用户体验,怎么做才能比较好的防止跳去404页面?




  3. 有一个页面,一个绝对够长的背景图,我们知道不给盒子设定高度的情况下默认是100%的高度,盒子高度会被内容所撑开。那么怎么做到第一屏完全显示背景图,第二屏也能继续显示呢?


    好,来看我的第一个错误回答🤣


    <style>
    * {
    margin: 0;
    padding: 0;
    }
    .container {
    width: 100%;
    height: 100vh;
    background-image: url('./assets/images/long.jpeg');
    }
    </style>

    <body>
    <div class="container">
    <p>1</p>
    这里复制出足够多的<p>1</p>就好,我就不贴出来重复代码占据太大篇幅了
    </div>
    </body>


    这是第一屏的效果,嗯很好完全没有问题! 但是当我们鼠标来到第二屏就哦豁了🙈




WechatIMG83.jpeg


然后我的第二个回答是:将图片绝对定位,这样图片就能适应不管多少屏了,但是图片绝对定位的话,没有内容撑开,那么第一屏根本都不会出现背景,所以这样也是不行的😅


答案:将 height: 100vh; 换成 min-height: 100vh;就可以了😂




  1. 我们都知道在谷歌浏览器里面字体的最低像素是 12px ,就算设置font-size: 8px;也会变成 12px ,我现在有一个需求需要 8px 的字体,怎么才能突破 12px 的限制?


    基本原理是使用css3的 transform: scale(); 属性


    需求是 8px 的字体,那我们就 font-size: 16px; transform: scale(0.5); 即可




  2. 讲一下 ES6 的新特性




  3. 说一些你经常用到的数组的方法




  4. 前端性能优化


    传送门:聊一聊前端性能优化




  5. 原型链


    传送门:继承与原型链


    传送门:JavaScript原型系列(三)Function、Object、null等等的关系和鸡蛋问题




  6. 假设在一个盒子里,里面所有小盒子的宽高都是相等的(PS技术不好,画的不相等),大盒子刚好放得下7个小盒子,使用css实现下面的布局




WechatIMG84.png




  1. 讲一下微信登录流程




  2. 怎么给每个请求加上 Authorization token ? (考察封装请求,axios 拦截器)




  3. 讲一下 vue 的双向数据绑定原理




  4. 移动端防止重复点击,防抖节流




  5. 怎么触发BFC,有什么应用场景?




  6. Promise有哪几种状态?




  7. 如果现在有一个任务,让你来做主力开发,架构已经搭好了,UI设计图也已经出完了,那你第一步会做什么?




  8. 后台管理系统怎么做权限分配?




  9. 怎么判断一个对象是否为空对象?




  10. 数字1-50的累加,不用 for 循环,用递归写


    因为我很抗拒当场写代码,然后满脑子都是1-50的累加为什么不用 for 循环,用 for 循环不是更快吗?为什么要用递归?但是面试官都把纸笔递过来了,没办法也是只能硬着头皮上了,但是这也是很简单的一道题,下面贴出当时手写的代码(是错的)


        // 这是错的这是错的这是错的
    function add(n) {
    let sum = 0;

    if (n > 0) {
    sum += add(n - 1);
    } else {
    return sum;
    }
    }

    // 这是根据上面改进之后的写法
    function add(n, sum) {
    if (n > 0) {
    return add(n - 1, (sum += n));
    } else {
    return sum;
    }
    }

    // 当然还有一种更为优雅与简便的写法
    function add(n) {
    return n === 0 ? 0 : n + add(n - 1);
    }

    // 想一行代码搞定的话就是
    const add = (n) => (n === 0 ? 0 : n + add(n - 1));



  11. 怎么解决 vuex 里的数据在页面刷新后丢失的问题?




  12. 说一下 vue 组件通信有几种方式(老生常谈的问题)




  13. 说一下 vue 和微信小程序各自的生命周期




  14. 看一下这个 ts 问题


        let num: string = '1';
    转一下数据类型转成 number



  15. 说一下 ts 总共有多少种数据类型




二面技术面




  1. 封装一个级联组件,讲一下思路




  2. 封装 v-model




  3. POST请求的 Content-Type 有多少种?




  4. css flex: 1; 是哪几个属性的组合写法




  5. vue provide/inject 的数据不会及时回流到父组件的问题(我记得没错的话好像是这么问的)




  6. 不用Promise的情况下,怎么实现一个Promise.all()方法




  7. [1, 2, 3].map((item, index) => parseInt(item, index))的结果


    这里考察了两点,1是parseInt()方法的第二个参数有什么作用,2是进制转换的相关知识




  8. cookie,sessionStorage,localStorage 3者之间有什么区别?




  9. http://www.xxx.com (a网站) 和 http://www.api.xxx.com (b网站) 两个网站,在b网站里登录授权拿到了 cookie ,怎么在a网站里拿到这个 cookie ?




  10. 说一下 forEach, map, for...in, for...of 的区别




  11. git fetch和git pull的区别(最后一道题)


    git pull:相当于是从远程获取最新版本并 merge 到本地


    git fetch:相当于是从远程获取最新版本到本地,不会自动 merge


    区别就是会不会自动 merge




三面HR面


这里就不展开了,HR面差不多都是那些东西


以上


其实一面二面还有很多问题都没有写出来,但是碍于当时也没有录音,只记得这么多


严格来讲,这并不太算是一篇面经,在上面很多都只是抛出了问题,因为技术的原因并没有做出相应的解答,还是有些遗憾的



作者:Lieo
链接:https://juejin.cn/post/7021394272519716872

收起阅读 »

亲身经历,大龄程序员找工作,为什么这么难!

背景 临近年底,公司还在招人,可筛选的人才真是越来越少,这可能是因为大家都在等年终奖吧。于是在简历筛选时,将学历和年龄都适当的放松了。正因为如此,面试了不少大龄的程序员。 网络上一直有讨论大龄程序员找工作困境的话题,对于我个人来说,是将信将疑的,但作为程序员对...
继续阅读 »

背景


临近年底,公司还在招人,可筛选的人才真是越来越少,这可能是因为大家都在等年终奖吧。于是在简历筛选时,将学历和年龄都适当的放松了。正因为如此,面试了不少大龄的程序员。


网络上一直有讨论大龄程序员找工作困境的话题,对于我个人来说,是将信将疑的,但作为程序员对自己职业生涯和未来的危机感还是有的。同时,作为技术部门领导,我是不介意年龄比我大,能力比我强的人加入的,只要能把事做好,这都不是事。


随着互联网的发展,大量程序员必然增多,都找不到工作是不可能的。而且中国的未来必然也会像发达国家一样,几十岁甚至一辈子都在写代码,也不是有可能的。


那么,我们担忧的是什么呢?又是什么影响了找工作呢?本文就通过自己亲身面试的几个典型案例来说说,可能样本有些小,仅供参考,不喜勿喷。


大厂与小厂招人的区别


前两天在朋友圈发了一条招人的感慨,关于大厂招人和小公司招人的区别。


大厂:有影响力,有钱,能够吸引了大量的应聘者。因此,也就有了筛选的资格,比如必须985名校毕业,必须35岁以下,不能5年3跳,必须这个……不能那个……当员工不合适时,绩效分给的低点或直接赔钱让其出局。


小公司:没有品牌,资金有限,每一分钱都要精打细算。招聘的人选有限,在这有限的选择范围内,还要考虑成本、能不能用、能不能留住等问题。能力太强,给不起钱,留不住;能力太弱,只会让项目越来越糟糕;所以,最好的选择只能是稍微高于现有团队能力,又不至于轻易跳槽的人。


有了上面的基本前提,再来看看大厂与小厂对待大龄程序员的差别。


对于35岁以后的程序员,有的大厂已经直接卡死,也就别死磕了。另外一些大厂还是开放的,但肯定是有一定的要求的,比如职位必须达到什么等级,能力必须达到什么要求。换句话说,如果你是牛人,其实35岁并不是什么问题,如果不是,那么这个选项几乎不存在。


所以,大厂的选择基本上等于没什么选择。再来看看小公司,小公司追求的核心是性价比,或者直白点说就是能干事且节省成本。另外就是能不能一职多能,能不能带新人,能不能加班……


个人看来,相对于大厂的要求,小公司的要求稍微努力一下还是可以满足的。对于加班这一项,不是所有的公司都有加班文化,也不是所有的公司常年需要加班。


招聘案例


挑选面试中几个比较典型的案例来聊聊,看看对你有什么启发。


案例一


84年的应聘者,自己在简历上填写的是应聘“中高级Java开发”。面试中,各项技能都平平,属于有功能开发经验,但没有深钻技术,没有考虑更好解决方案的状态。明确加班不能超过9点。也有写博客和公众号。9月份离职,目前暂未未找到工作。


就这位应聘者而言:第一,能接受员工比自己年龄大的领导不多,因为担心管不住;第二,技能没有亮点,就像他自己定位的那样,十多年工作经验,只是中高级开发;第三,加班这一项卡的太死,哪家公司上个线不到10点以后了,有突发需求不加个班?


案例二


86年的应聘者,别家公司裁员,推荐过来的简历。十来年工作经验,一直负责一个简单彩会不会是敏感词票业务的开发,中间有几年还没有项目履历。简历上写的功能还是:用户管理、权限管理、XX接口对接。推荐他的老板,给他的定位是中级开发。


这位应聘者,真的是将一年的代码写了十年。上家老板裁员选择了他,定位也只是中级,然后帮忙推荐了他到其他公司。这背后的故事,你仔细品,再仔细品。


案例三


87年的应聘者,学历一般,这两年的工作履历有点糟糕,跳槽的时机选择的也不好。长期从事支付行业,十来年的工作履历中,有七八年在做支付及相关的行业,其中在一家支付公司工作了四年。面试中,特意问了行业中的一些解决方案、技术实现,都没问题。基础知识稍微有点弱,但影响不大。面试过后,发了Offer,其中我还在老板面前帮忙争取了一把。


这位应聘者,虽然在学历,近两年的经历,基础知识上都略有不足。但他的行业经验丰富,给人一种踏实做事的感觉。整体能力恰好符合上面提到的小公司选择标准:比现有团队人能力强,有行业经验,薪资适中,稳定性较好。他的长板完全弥补了短板。


案例四


91年的应聘者,一家小有名气二线互联网公司出来。最近半年未工作,给出的原因是:家中有事,处理了半年,现在决定找工作了。聊半年前做过的项目,基本上记不起逻辑了;聊技术知识,也只能说个大概,但能感觉还是做过一些功能的,但没有深入思考过或没有做过复杂逻辑。


这位应聘者,不确定已经面试多久了,但应该不那么容易找工作。第一,半年未工作,即使有原因,也让人多少有些顾虑;第二,面试前完全没做功课,这不是能力问题,而是态度的问题了。


上面的案例,有成功的也有失败的。总结一下失败的原因,基本上有几点:能力与年龄不匹配、不能加班、家庭影响、没有特长……当然,你如果能看到其他的失败原因,那就更好了。


小结


上面只是最近一段时间面试的几个典型案例,至于你能从中获得什么,能不能提前行动,做好准备。那就是大家自己的事了。当然,还是那句话,样本有些小,但也能说明一些问题。仅个人观点,不抬杠,不喜勿喷。


我也曾为职场的未来担忧,也曾为年龄担忧,但始终未放弃的就是持续学习和思考。多位朋友都曾说过:无论你是否当上领导,是否还在写代码,技术能力都不能丢,你必须是团队中技术最牛的那一个。我一直在努力做到,你呢


作者:程序新视界
链接:https://juejin.cn/post/7043589223345029133

收起阅读 »

当 Adapter 遇上 Kotlin DSL,无比简单的调用方式

早在去年的时候我就提到过使用工厂的方式获取 Adapter 而不是为每个 Adapter 定义一个类文件。这样的好处是,对于不是那么复杂的 Adapter 可以节省大量的代码,提升开发效率和解放双手,同时更好的支持多类型布局效果。 1、Kotlin DSL 和...
继续阅读 »

早在去年的时候我就提到过使用工厂的方式获取 Adapter 而不是为每个 Adapter 定义一个类文件。这样的好处是,对于不是那么复杂的 Adapter 可以节省大量的代码,提升开发效率和解放双手,同时更好的支持多类型布局效果。


1、Kotlin DSL 和 Adapter 工厂方法


可以把 Kotlin DSL 当作构建者使用。这里有一篇不错的文章,想了解的可以阅读下,



http://www.ximedes.com/2020-04-21/…



Kotlin DSL 是拓展函数的延申,比如我们常用的 with 等函数就是函数的拓展,


public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return receiver.block()
}

这里是泛型 T 的拓展。这里的 T 可以类比到 Java 构建者模式中的 Builder,通过方法接收外部参数之后调用 build() 方法创建一个最终的对象即可。


对于 Adapter 工厂方法,之前我是通过如下方式使用的,


fun <T> getAdapter(
@LayoutRes itemLayout:Int,
converter: (helper: BaseViewHolder, item: T) -> Unit,
data: List<T>
): Adapter<T> = Adapter(itemLayout, converter, data)

class Adapter<T>(
@LayoutRes private val layout: Int,
private val converter: (helper: BaseViewHolder, item: T) -> Unit,
val list: List<T>
): BaseQuickAdapter<T, BaseViewHolder>(layout, list) {
override fun convert(helper: BaseViewHolder, item: T) {
converter(helper, item)
}
}

也就是每次想要得到 Adapter 的时候只要调用 getAdapter() 方法即可。这种封装方式比较简陋,支持的功能有限。后来慢慢采用了 Kotlin DSL 之后,我封装了 Kotlin DSL 风格的工厂方法。采用 Kotlin DSL 风格之后更加优雅和方便快捷,同时更好的支持多类型布局效果。


2、使用


2.1 引入依赖


首先,该项目依赖于 BRVAH,所以,你需要引入该库之后才可以使用。BRVAH 可以说是目前开源的最好用的 Adapter,我们没必要再另起炉灶自己再造轮子。这个框架设计最好地方在于通过 SpareArray 收集了 ViewHolder 控件,从而避免了自定义 ViewHolder,这是我们框架设计的基础思想。


该项目已经上传到了 MavenCentral,你需要先在项目中引入该仓库,


allprojects {
repositories {
mavenCentral()
}
}

然后在项目中添加如下依赖,


implementation "com.github.Shouheng88:xadapter:${latest_version}"

2.2 使用 Adapter 工厂方法


使用 xAdapter 之后,当你需要定义一个 Adapter 的时候,你无需单独创建一个类文件,只需要通过 createAdapter() 方法获取一个 Adapter,


adapter = createAdapter {
withType(Item::class.java, R.layout.item_eyepetizer_home) {
// Bind data with viewholder.
onBind { helper, item ->
helper.setText(R.id.tv_title, item.data.title)
helper.setText(R.id.tv_sub_title, item.data.author?.name + " | " + item.data.category)
helper.loadCover(requireContext(), R.id.iv_cover, item.data.cover?.homepage, R.drawable.recommend_summary_card_bg_unlike)
helper.loadRoundImage(requireContext(), R.id.iv_author, item.data.author?.icon, R.mipmap.eyepetizer, 20f.dp2px())
}
// Item level click and long click events.
onItemClick { _, _, position ->
adapter?.getItem(position)?.let {
toast("Clicked item: " + it.data.title)
}
}
}
}

在这种新的调用方式中,你需要通过 withType() 方法指定数据类型及其对应的布局文件,然后在 onBind() 方法中即可实现数据到 ViewHolder 的绑定操作。这里的 onBind() 方法的使用与 BRVAH 中的 convert() 方法使用一致,可以通过阅读该库了解如何使用。总之,xAapter 在 BRVAH 的基础上做了二次封装,可以说,比简单更简单。


xAdapter 支持为每个 ViewHolder 绑定点击和长按事件,同时也支持为 ViewHolder 上的某个单独的 View 添加点击和长按事件。使用方式如上所示,只需要添加 onItemClick() 方法并实现自己的逻辑即可。其他的点击事件可以参考项目的示例代码。


效果,





2.3 使用多类型 Adapter


多类型 Adapter 的使用方式非常简单,类似于上面的调用方式,只需要在 createAdapter() 内再添加一个 withType() 方法即可。下面是一个写起来可能相当复杂的 Adapter,但是采用了 xAdpater 的调用方式之后,一切变得非常简单,


private fun createAdapter() {
adapter = createAdapter {
withType(MultiTypeDataGridStyle::class.java, R.layout.item_list) {
onBind { helper, item ->
val rv = helper.getView<RecyclerView>(R.id.rv)
rv.layoutManager = GridLayoutManager(context, 3)
val adapter = createSubAdapter(R.layout.item_home_page_data_module_1, 1)
rv.adapter = adapter
adapter.setNewData(item.items)
}
}
withType(MultiTypeDataListStyle1::class.java, R.layout.item_home_page_data_module_2) {
onBind { helper, item ->
converter.invoke(helper, item)
}
onItemClick { _, _, position ->
(adapter?.getItem(position) as? MultiTypeDataListStyle1)?.let {
toast("Clicked style[2] item: " + it.item.data.title)
}
}
}
withType(MultiTypeDataListStyle2::class.java, R.layout.item_list) {
onBind { helper, item ->
val rv = helper.getView<RecyclerView>(R.id.rv)
rv.layoutManager = LinearLayoutManager(context, RecyclerView.HORIZONTAL, false)
val adapter = createSubAdapter(R.layout.item_home_page_data_module_4, 3)
rv.adapter = adapter
adapter.setNewData(item.items)
}
}
withType(MultiTypeDataListStyle3::class.java, R.layout.item_home_page_data_module_3) {
onBind { helper, item ->
converter.invoke(helper, item)
}
onItemClick { _, _, position ->
(adapter?.getItem(position) as? MultiTypeDataListStyle3)?.let {
toast("Clicked style[4] item: " + it.item.data.title)
}
}
}
}
}

xAdapter 对多类型布局方式的支持是在 BRVAH 之上进行的改造,在这种封装方式中,数据类无需实现任何类和接口。Adpater 内部通过 Class 区分各个 ViewHolder.


效果,





总结


相对于为各种类型的数据定义 Adapter 的使用方式,以上封装方式的优势是:



  1. 借助 BRVAH 的优势,封装了大量的方法,进一步简化了 Adapter 的使用;

  2. 通过工厂和 DSL 封装,简化了调用 Adapter 的方式,你无需为数据类型定义 Adapter 文件,减少了项目中需要维护的代码和类文件数量;

  3. 通过以上封装,使用 Adapter 更加简洁,节省了大量的代码,提升开发效率和解放双手;

  4. 自由地在单一类型布局和多类型布局之间进行切换,但是少了没必要的工厂方法。


当有更加简洁的使用方式的时候,继续采用复杂的调用方式无异于抱残守缺,对于程序员而言,做这种重复而没有太大价值的工作,付出再多的汗水都不值得同情。以上是部分功能和代码的展示,可以通过阅读源码了解更多。后续我参考其他优秀的库的设计思想,支持更多 Adapter 特性的封装来实现快速调用。


项目已开源,感兴趣的可以直接阅读项目源码,源码地址:github.com/Shouheng88/…


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

我用Flutter写了一个上班摸鱼应用

网上最近看到了个摸鱼应用,还挺好玩的。我打算自己用flutter写了一个之前我有用flutter制作过mobile应用,但是没有在desktop尝试过;毕竟2.0大更新,我这里就在这试手一下,并说说flutter的体验.当前flutter环境 2.8增加flu...
继续阅读 »

网上最近看到了个摸鱼应用,还挺好玩的。

moyu.jpeg

我打算自己用flutter写了一个

之前我有用flutter制作过mobile应用,但是没有在desktop尝试过;毕竟2.0大更新,我这里就在这试手一下,并说说flutter的体验.

当前flutter环境 2.8

截屏2021-12-18 上午9.59.29.png

增加flutter desktop支持 (默认项目之存在ios,android项目包)

flutter config --enable-<platform>-desktop

我这里是mac,因此platform=macos,详细看flutter官网

代码十分简单,UI部分就不讲了

在摸鱼界面,我是用了 Bloc 做倒计时计算逻辑,默认摸鱼时长15分钟

 MoYuBloc() : super(MoyuInit()) {
on(_handleMoyuStart);
on(_handleUpdateProgress);
on(_handleMoyuEnd);
}

摸鱼开始事件处理

// handle moyu start action
FutureOr<void> _handleMoyuStart(
MoyuStarted event, Emitter<MoyuState> emit) async {
if (_timer != null && _timer!.isActive) {
_timer?.cancel();
}

final totalTime = event.time;
int progressTime = state is MoyuIng ? (state as MoyuIng).progressTime : 0;

_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
add(MoyuProgressUpdated(totalTime, ++progressTime));

if (progressTime >= totalTime) {
timer.cancel();
add(MoyuEnded());
}
});
emit(MoyuIng(progress: 0, progressTime: 0));
}

摸鱼进度更新

// handle clock update
FutureOr<void> _handleUpdateProgress(
MoyuProgressUpdated event, Emitter<MoyuState> emit) async {
final totalTime = event.totalTime;
final progressTime = event.progressTime;
emit(
MoyuIng(progress: progressTime / totalTime, progressTime: progressTime),
);
}

摸鱼结束,释放结束事件

// handle clock end
FutureOr<void> _handleMoyuEnd(
MoyuEnded event, Emitter<MoyuState> emit) async {
emit(MoyuFinish());
}
总结3个event (摸鱼开始,进程更新,摸鱼结束)

abstract class MoyuEvent {}

class MoyuStarted extends MoyuEvent {
final int time;
final System os;

MoyuStarted({required this.time, required this.os});
}

class MoyuProgressUpdated extends MoyuEvent {
final int totalTime;
final int progressTime;

MoyuProgressUpdated(this.totalTime, this.progressTime);
}

class MoyuEnded extends MoyuEvent {
MoyuEnded();
}

其中3个state (摸鱼初始,正在摸鱼,摸鱼结束)

abstract class MoyuState {}

class MoyuInit extends MoyuState {}

class MoyuIng extends MoyuState {
final double progress;
final int progressTime;

MoyuIng({required this.progress, required this.progressTime});
}

class MoyuFinish extends MoyuState {}

启动摸鱼使用, 记录总时长和消耗时间,计算进度百分比,更新UI进度条

下面是界面更新逻辑

BlocConsumer<MoYuBloc, MoyuState>(
builder: (context, state) {
if (state is MoyuIng) {
final progress = state.progress;

return _moyuIngView(progress);
} else if (state is MoyuFinish) {
return _replayView();
}
return const SizedBox();
},
listener: (context, state) {},
listenWhen: (pre, cur) => pre != cur,
),

很简单 最重要的是进度状态,其次结束后是否重新摸鱼按钮

构建运行flutter应用

flutter run -d macos 

最后结果展示

windows_update.png

mac_update.png

总结下flutter desktop使用

  1. 简单上手,按着官网走基本没问题,基本上没踩上什么雷,可能项目比较简单
  2. 构建流程简单,hot reload强大
  3. 性能强大,启动速度很快,并且界面无顿挫感

比较遗憾的事desktop电脑构建系统独立,mac环境下无法构建windows应用,有点小遗憾.

项目完全开源 可以前往GitHub查看 不要忘点个star😊

github.com/lau1944/moy…


作者:1vau
链接:https://juejin.cn/post/7042864240817864740
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

丢掉丑陋的 toast,会动的 toast 更有趣!

前言 我们通常会用 toast(也叫吐司)来显示提示信息,例如网络请求错误,校验错误等等。大多数 App的 toast 都很简单,简单的半透明黑底加上白色文字草草了事,比如下面这种. 说实话,这种toast 的体验很糟糕。假设是新手用户,他们并不知道 toa...
继续阅读 »

前言


我们通常会用 toast(也叫吐司)来显示提示信息,例如网络请求错误,校验错误等等。大多数 App的 toast 都很简单,简单的半透明黑底加上白色文字草草了事,比如下面这种.
image.png
说实话,这种toast 的体验很糟糕。假设是新手用户,他们并不知道 toast 从哪里出来,等出现错误的时候,闪现出来的时候,可能还没抓住内容的重点就消失了(尤其是想截屏抓错误的时候,更抓狂)。这是因为一个是这种 toast 一般比较小,而是动效非常简单,用来提醒其实并不是特别好。怎么破?本篇来给大家介绍一个非常有趣的 toast 组件 —— motion_toast


motion_toast 介绍


从名字就知道,motion_toast 是支持动效的,除此之外,它的颜值还很高,下面是它的一个示例动图,仔细看那个小闹钟图标,是在跳动的哦。这种提醒效果比起常用的 toast 来说醒目多了,也更有趣味性。
center_motion_toast_2.gif
下面我们看看 motion_toast 的特性:



  • 可以通过动画图标实现动效;

  • 内置了成功、警告、错误、提醒和删除类型;

  • 支持自定义;

  • 支持不同的主题色;

  • 支持 null safety;

  • 心跳动画效果;

  • 完全自定义的文本内容;

  • 内置动画效果;

  • 支持自定义布局(LTR 和 RTL);

  • 自定义持续时长;

  • 自定义展现位置(居中,底部或顶部);

  • 支持长文本显示;

  • 自定义背景样式;

  • 自定义消失形式。


可以看到,除了能够开箱即用之外,我们还可以通过自定义来丰富 toast 的样式,使之更有趣。


示例


介绍完了,我们来一些典型的示例吧,首先在 pubspec.yaml 中添加依赖motion_toast: ^2.0.0(最低Dart版本需要2.12)。


最简单用法


只需要一行代码搞定!其他参数在 success 的命名构造方法中默认了,因此使用非常简单。


MotionToast.success(description: '操作成功!').show(context);

其他内置的提醒


内置的提醒也支持我们修改默认参数进行样式调整,如标题、位置、宽度、显示位置、动画曲线等等。


// 错误提示
MotionToast.error(
description: '发生错误!',
width: 300,
position: MOTION_TOAST_POSITION.center,
).show(context);

//删除提示
MotionToast.delete(
description: '已成功删除',
position: MOTION_TOAST_POSITION.bottom,
animationType: ANIMATION.fromLeft,
animationCurve: Curves.bounceIn,
).show(context);

// 信息提醒(带标题)
MotionToast.info(
description: '这是一条提醒,可能会有很多行。toast 会自动调整高度显示',
title: '提醒',
titleStyle: TextStyle(fontWeight: FontWeight.bold),
position: MOTION_TOAST_POSITION.bottom,
animationType: ANIMATION.fromBottom,
animationCurve: Curves.linear,
dismissable: true,
).show(context);

不过需要注意的是,一个是 dismissable 参数只对显示位置在底部的有用,当在底部且dismissabletrue 时,点击空白处可以让 toast 提前消失。另外就是显示位置 positionanimationType 是存在某些互斥关系的。从源码可以看到底部显示的时候,animationType不能是 fromTop,顶部显示的时候 animationType 不能是 fromBottom


void _assertValidValues() {
assert(
(position == MOTION_TOAST_POSITION.bottom &&
animationType != ANIMATION.fromTop) ||
(position == MOTION_TOAST_POSITION.top &&
animationType != ANIMATION.fromBottom) ||
(position == MOTION_TOAST_POSITION.center),
);
}

自定义 toast


自定义其实就是使用 MotionToast 构建一个实例,其中,descriptioniconprimaryColor参数是必传的。自定义的参数很多,使用的时候建议看一下源码注释。


MotionToast(
description: '这是自定义 toast',
icon: Icons.flag,
primaryColor: Colors.blue,
secondaryColor: Colors.green[300],
descriptionStyle: TextStyle(
color: Colors.white,
),
position: MOTION_TOAST_POSITION.center,
animationType: ANIMATION.fromRight,
animationCurve: Curves.easeIn,
).show(context);

下面对自定义的一些参数做一下解释:



  • icon:图标,IconData 类,可以使用系统字体图标;

  • primaryColor:主颜色,也就是大的背景底色;

  • secondaryColor:辅助色,也就是图标和旁边的竖条的颜色;

  • descriptionStyle:toast 文字的字体样式;

  • title:标题文字;

  • titleStyle:标题文字样式;

  • toastDuration:显示时长;

  • backgroundType:背景类型,枚举值,共三个可选值,transparentsolidlighter,默认是 lighterlighter其实就是加了一层白色底色,然后再将原先的背景色(主色调)加上一定的透明度叠加到上面,所以看起来会泛白。

  • onClose:关闭时回调,可以用于出现多个错误时依次展示,或者是关闭后触发某些动作,如返回上一页。


总结


看完之后,是不是觉得以前的 toast 太丑了?用 motion_toast来一个更有趣的吧。另外,整个 motion_toast 的源码并不多,有兴趣的可以读读源码,了解一下toast 的实现也是不错的。


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

Android 常见内存泄漏总结、避免踩坑、提供解决方案

对常见的内存泄漏进行一波总结,希望可以帮到大家。静态实例持有非静态内部类描述🤦♀️非静态内部类会持有外部类的实例,所以如果非静态内部类的实例是静态的话,那么它的生命周期就是整个APP的生命周期,而它则会一直持有外部类的引用,阻止外部类实例被系统回收。举个例子🌰...
继续阅读 »



对常见的内存泄漏进行一波总结,希望可以帮到大家。

静态实例持有非静态内部类

描述🤦♀️

非静态内部类会持有外部类的实例,所以如果非静态内部类的实例是静态的话,那么它的生命周期就是整个APP的生命周期,而它则会一直持有外部类的引用,阻止外部类实例被系统回收。

举个例子🌰:

public class TestActivity extends AppCompatActivity {

   static InnerClass innerClass;

   @Override
   protected void onCreate(@Nullable Bundle savedInstanceState) {

       super.onCreate(savedInstanceState);
       setContentView(R.layout.item_layout);
       findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {

           @Override
           public void onClick(View v) {
               innerClass = new InnerClass();
          }
      });
  }

   class InnerClass{
 
    }
}

此时点击button时,会创建InnerClass实例并且赋值给innerClass。因为innerClassstatic修饰,所以InnerClass实例的生命周期会和应用程序一样长,但是它会持有TestActivity的实例,所以就会导致如果系统需要回收不了TestActivity的实例。造成内存泄漏😮。

解决办法🙆♀️

  • 将非静态内部类替换成静态内部类,因为静态内部类不会持有外部类的引用

  • 一定要用非静态内部类的话,要保证内部类的生命周期短于外部类

耗时任务相关的匿名内部类/非静态内部类

描述🤦♀️

这个和上一个类似,非静态内部类持有外部类的实例大家都知道了,这里不在叙述了;匿名内部类也会持有外部类的实例,而且匿名内部类会结合线程使用得多,这里就拉出来讲一下。

同理因为匿名内部类会持有外部类的实例,比如线程的Runable如果在里面做了耗时任务,在外部类对象需要回收的时候,但是线程任务没有执行完,那么就会因为匿名内部类持有外部类的引用,进而阻止系统回收外部类对象了。

简单举个例子

public class TestActivity extends AppCompatActivity {

   @Override
   protected void onCreate(@Nullable Bundle savedInstanceState) {

       super.onCreate(savedInstanceState);
       setContentView(R.layout.item_layout);
       findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {

           @Override
           public void onClick(View v) {
               ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.MICROSECONDS, new SynchronousQueue(), new ThreadFactory() {
                   @Override
                   public Thread newThread(Runnable r) {
                       return new Thread(new ThreadGroup("test-thread"),r);
                  }
              });
               threadPoolExecutor.submit(new Runnable() {
                   @Override
                   public void run() {
                       while (true){
                           Log.d("TAG", "run: test");
                      }
                  }
              });
          }
      });
  }
}

点击button执行线程任务,提交了一个runable进去,因为里面死循环永远不会结束。所以匿名内部类会一直持有TestActivitty对象。不会被系统回收掉😮。因为匿名内部类这玩意进场使用,所以还是需要注意的!!!🤦♀️

解决方案🙆♀️

  • 将匿名内部类/非静态内部类替换成静态内部类,因为静态内部类不会持有外部类的引用

  • 一定要用匿名内部类/非静态内部类的话,要保证内部类的生命周期短于外部类

Handle内存泄漏

描述🤦♀️

这个就有点老生常谈了😂,但还是的说一下。

Handler发送的Message会存储在MessageQueue里面,但是他们不一定马上就被处理了。

另外我们知道Message的Target会持有记录当前的Handler对象,用于进行消息分发。所以如果Message不被及时处理,那么Handler就无法被回收。

那么如果此时Handler是非静态的,则Handler也会导致引用它的Activity不能被回收😮。

举个例子🌰

public class TestActivity extends AppCompatActivity {

   @Override
   protected void onCreate(@Nullable Bundle savedInstanceState) {

       super.onCreate(savedInstanceState);
       setContentView(R.layout.item_layout);
       Handler mHandler = new Handler(){

           @Override
           public void handleMessage(Message msg) {
               super.handleMessage(msg);
          }
      };
       findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {

           @Override
           public void onClick(View v) {
   
               mHandler.sendMessageDelayed(new Message(),100000);
               finish();
          }
      });
  }
}

当点击button时,会finish当前activity。但是因为消息没有被及时处理,间接引用了Handler对象,Handler又是匿名内部类实例,持有了activity对象。所以导致内存泄漏🤦♀️。

解决方案🙆♀️

  • 使用静态Handler内部类,handler的持有者用弱引用。

  • 在onDestroy中将未执行的消息和Callbacks清除。

    if (mHandler != null) {
        mHandler.removeCallbacksAndMessages(null);
    }

Context被长期持有

描述🤦♀️

这个也很简单,比如你把Activity的Context传给了一个长期存在的对象,那其实activity的context就是它自身,那么因为被持有就回收不了。造成内存泄漏

解决方案🙆♀️

  • 对于不是必须使用Activity的Context的情况(Dialog的Context必须使用Activity的Context),可以考虑使用Application来代替Activity的Context,因为一般使用Context无非时获取一些资源而已。

  • 一定要传入Activity的Context的话,一定要注意生命周期,不可以被长期引用。

View被静态修饰

描述🤦♀️

这个。。。。我估计没有多少人这么使用吧。如果View被静态修饰的话,因为View会持有Context,所以就会导致当前Activity不会被回收。🤦♀️

举个例子🌰

public class TestActivity extends AppCompatActivity {

   static View button;

   @Override
   protected void onCreate(@Nullable Bundle savedInstanceState) {

       super.onCreate(savedInstanceState);
       setContentView(R.layout.item_layout);
       button = findViewById(R.id.button);
  }
}

解决方案🙆♀️

  • 在onDestory中将view置null。

大对象/监听器释放

描述🤦♀️

大对象比如Bitmap

Bitmap对象一般比较大,而且好多操作都需要变换产生新的对象。所以需要注意一定要尽快释放临时的Bitmap对象用于节省内存。尽量避免被静态修饰或者其他长生命周期引用。

监听器的释放

很多服务需要register和unregister监听器,需要在合适的时候及时的unregister这些监听器否则容易产生内存泄漏。

解决方案🙆♀️

  • 大对象及时释放

  • 监听器在合适的时候进行释放

资源对象注意关闭

描述🤦♀️

资源对象比如File、Cursor等,如果不进行正常关闭,会造成内存泄漏。

解决方案🙆♀️

  • 通常使用异常代码块捕获,在finally语句中进行关闭,防止出现异常资源没有被正常释放问题。

集合对象

描述🤦♀️

注意一些生命周期很长的集合,比如被static修饰的集合,它的生命周期会时APP的生命周期,那么它里面维持的对象,如果在没用之后要即使清理掉,否则就会造成内存泄漏。

举个例子🌰

public class TestActivity extends AppCompatActivity {

   static List<View> vies;

   @Override
   protected void onCreate(@Nullable Bundle savedInstanceState) {

       super.onCreate(savedInstanceState);
       setContentView(R.layout.item_layout);
       vies.add(findViewById(R.id.button));
  }
}

view被加入集合,因为集合生命周期为APP的生命周期,所以View、Activity也回收不了,内存泄漏。

解决方案🙆♀️

  • 这个完全需要自己注意,对于长生命周期集合内部对象的管理。

————————————————
作者:pumpkin的玄学
来源:https://blog.csdn.net/weixin_44235109/article/details/122029725

收起阅读 »

JavaScript函数封装随机颜色验证码

数字或者字母或者数字字母混合的n位验证码带随机的颜色。下面是完整的代码,需要的自取哈!function verify(a = 6,b = "num"){ //定义三个随机验证码验证码库 var num ="0123456789" var str ="ab...
继续阅读 »

数字或者字母或者数字字母混合的n位验证码带随机的颜色。下面是完整的代码,需要的自取哈!

function verify(a = 6,b = "num"){
//定义三个随机验证码验证码库
var num ="0123456789"
var str ="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNIPQRSTUVWXYZ"
var mixin = num +str;
 
//定义一个空字符串用来存放验证码
var verify=""
if(a == undefined || b == undefined){
  //验证输入是否合法 不通过就抛出一个异常
  throw new Error("参数异常");
}else{
    if(a ==""||b==""){
      //判断用户是否没有输入
      throw new Error("参数非法.");
    }else{
      //检测输入的类型来判断是否进入
      var typea = typeof(a);
      var typeb = typeof(b);
      if(typea =="number" && typeb =="string"){
          if(b == "num"){
                 
              //定义一个循环来接收验证码   纯数字验证码
              for(var i=0;i<a;i++){
                    //定义一个变量来存储颜色的随机值
                    var r1 = Math.random()*255;
                    var g1 = Math.random()*255;
                    var b1 = Math.random()*255;

                    //确定随机索引
                    var index = Math.floor(Math.random()*(num.length-1))
                    //确定随机的验证码
                    var char = num[index];
                    //给随机的验证码加颜色
                    verify += `<span style ='color:rgb(${r1},${g1},${b1})'>${char}</span>`
                }
                //返回到数组本身
              return verify;
          }else if(b =="str"){
                for(var i=0;i<a;i++){
                  //纯字母的验证码
                  var r1 = Math.random()*255;
                  var g1 = Math.random()*255;
                  var b1 = Math.random()*255;

                  var index = Math.floor(Math.random()*(str.length-1));
                  var char = str[index];

                  verify += `<span style ='color:rgb(${r1},${g1},${b1})'>${char}</span>`
                }
                return verify;  
          }else if(b == "mixin"){
                // 混合型的验证码
              for(var i=0;i<a;i++){
                  var r1 = Math.random()*255;
                  var g1 = Math.random()*255;
                  var b1 = Math.random()*255;

                  var index = Math.floor(Math.random()*(mixin.length-1));
                  var char = mixin[index];

                  verify += `<span style ='color:rgb(${r1},${g1},${b1})'>${char}</span>`
              }
              return verify;
          }else{
              //验证没通过抛出一个异常
              throw new Error("输入类型非法.")
          }
       
      }else{
          //验证没通过抛出一个异常
          throw new Error("输入类型非法.")
      }
    }
}
}

下面我们来调用函数试试看

  //第一个值为用户输入的长度,第二个为类型! 
var arr = verify(8,"mixin");
    document.write(arr)

上面就是结果啦!

这个记录下来为了方便以后使用的方便,也希望大佬们多多交流,多多留言,指出我的不足的之处啦!

有需要的小伙伴可以研究研究啦!!
————————————————
作者:土豆切成丝
来源:https://blog.csdn.net/A20201130/article/details/122030872

收起阅读 »

localhost、127.0.0.1和0.0.0.0和本机IP的区别

localhostlocalhost其实是域名,一般windows系统默认将localhost指向127.0.0.1,但是localhost并不等于127.0.0.1,localhost指向的IP地址是可以配置的 127.0.0.1首先我们要先知道一...
继续阅读 »
localhost
localhost其实是域名,一般windows系统默认将localhost指向127.0.0.1,但是localhost并不等于127.0.0.1,localhost指向的IP地址是可以配置的
 
127.0.0.1
首先我们要先知道一个概念,凡是以127开头的IP地址,都是回环地址(Loop back address),其所在的回环接口一般被理解为虚拟网卡,并不是真正的路由器接口。
所谓的回环地址,通俗的讲,就是我们在主机上发送给127开头的IP地址的数据包会被发送的主机自己接收,根本传不出去,外部设备也无法通过回环地址访问到本机。
 
小说明:正常的数据包会从IP层进入链路层,然后发送到网络上;而给回环地址发送数据包,数据包会直接被发送主机的IP层获取,后面就没有链路层他们啥事了。
而127.0.0.1作为{127}集合中的一员,当然也是个回环地址。只不过127.0.0.1经常被默认配置为localhost的IP地址。
一般会通过ping 127.0.0.1来测试某台机器上的网络设备是否工作正常。
 
0.0.0.0
首先,0.0.0.0是不能被ping通的。在服务器中,0.0.0.0并不是一个真实的的IP地址,它表示本机中所有的IPV4地址。监听0.0.0.0的端口,就是监听本机中所有IP的端口。
 
本机IP
本机IP通常仅指在同一个局域网内,能同时被外部设备访问和本机访问的那些IP地址(可能不止一个)。像127.0.0.1这种一般是不被当作本机IP的。本机IP是与具体的网络接口绑定的,比如以太网卡、无线网卡或者PPP/PPPoE拨号网络的虚拟网卡,想要正常工作都要绑定一个地址,否则其他设备就不知道如何访问它。
 
 

localhost
首先 localhost 是一个域名,在过去它指向 127.0.0.1 这个IP地址。在操作系统支持 ipv6 后,它同时还指向ipv6 的地址 [::1] 
在 Windows 中,这个域名是预定义的,从 hosts 文件中可以看出:
# localhost name resolution is handled within DNS itself.
# 127.0.0.1 localhost
# ::1 localhost
 
而在 Linux 中,其定义位于 /etc/hosts 中:
127.0.0.1 localhost
 
注意这个值是可修改的,比如把它改成
192.068.206.1 localhost
 
然后再去 ping localhost,提示就变成了
PING localhost (192.168.206.1) 56(84) bytes of data.
 
127.0.0.1
127.0.0.1 这个地址通常分配给 loopback 接口。loopback 是一个特殊的网络接口(可理解成虚拟网卡),用于本机中各个应用之间的网络交互。只要操作系统的网络组件是正常的,loopback 就能工作。Windows 中看不到这个接口,Linux中这个接口叫 lo:
#ifconfig
eth0 Link encap:Ethernet hwaddr 00:00:00:00:00:00
  inet addr :192.168.0.1 Bcase:192.168.0.255 Mask:255.255.255.0
  ......
lo     Link encap:Local Loopback
  inetaddr: 127.0.0.1 Mask: 255.0.0.0
       ......
 
可以看出 lo 接口的地址是 127.0.0.1。事实上整个 127.* 网段都算能够使用,比如你 ping 127.0.0.2 也是通的。 
但是使用127.0.0.1作为loopback接口的默认地址只是一个惯例,比如下面这样:
#ifconfig lo 192.168.128.1
#ping localhost  #糟糕,ping不通了
#ping 192.128.128.1 # 可以通
#ifconfig lo
lo  Link encap:Local Loopback
  inetaddr: 192.168.128.1 Mask: 255.255.255.0
     ......
 
如果随便改这些配置,可能导致很多只认 127.0.0.1 的软件挂掉。
 
本机IP
确切地说,“本机地址”并不是一个规范的名词。通常情况下,指的是“本机物理网卡所绑定的网络协议地址”。由于目前常用网络协议只剩下了IPV4,IPX/Apple Tak消失了,IPV6还没普及,所以通常仅指IP地址甚至ipv4地址。一般情况下,并不会把 127.0.0.1当作本机地址——因为没必要特别说明,大家都知道。 
本机地址是与具体的网络接口绑定的。比如以太网卡、无线网卡或者PPP/PPPoE拨号网络的虚拟网卡,想要正常工作都要绑定一个地址,否则其他设备就不知道如何访问它。
● localhost 是个域名,不是地址,它可以被配置为任意的 IP 地址,不过通常情况下都指向 127.0.0.1(ipv4)和 ::1 
● 整个127.* 网段通常被用作 loopback 网络接口的默认地址,按惯例通常设置为 127.0.0.1。这个地址在其他计算机上不能访问,就算你想访问,访问的也是自己,因为每台带有TCP/IP协议栈的设备基本上都有 localhost/127.0.0.1。 
● 本机地址通常指的是绑定在物理或虚拟网络接口上的IP地址,可供其他设备访问到。 
● 最后,从开发度来看 
○ localhost是个域名,性质跟 “www.baidu.com” 差不多。不能直接绑定套接字,必须先gethostbyname转成IP才能绑定。 
○ 127.0.0.1 是绑定在 loopback 接口上的地址,如果服务端套接字绑定在它上面,你的客户端程序就只能在本机访问

原文链接:https://www.cnblogs.com/absoluteli/p/13958072.html
收起阅读 »

Android 使用 Span 打造丰富多彩的文本

1.引言 在开发过程中经常需要使用文本,有时候需要对一段文字中的部分文字进行特殊的处理,如改变其中部分文字的大小、颜色、加下划线等,这个时候使用Span就能方便地解决这些问题。本文将主要介绍SpannableStringBuilder和各种Span的使用。 2...
继续阅读 »

1.引言


在开发过程中经常需要使用文本,有时候需要对一段文字中的部分文字进行特殊的处理,如改变其中部分文字的大小、颜色、加下划线等,这个时候使用Span就能方便地解决这些问题。本文将主要介绍SpannableStringBuilder和各种Span的使用。


2.SpannableStringBuilder的基本用法


新建一个SpannableStringBuilder对象的操作如下:


SpannableStringBuilder builder = new SpannableStringBuilder("Hello World!");

SpannableStringBuilder的setSpan()方法如下:


//what:各种文本Span,如BackgroundColorSpan、ForegroundColorSpan等
//start:应用Span的文本的开始位置索引
//end:应用Span的文本的结束位置索引
//flags:标志
public void setSpan(Object what, int start, int end, int flags) {
setSpan(true, what, start, end, flags, true/*enforceParagraph*/);
}

3.使用Span给文本添加效果


3.1 AbsoluteSizeSpan

此Span用来改变文本的绝对大小,示例如下:


 SpannableStringBuilder builder = new SpannableStringBuilder("Hello World!");
builder.setSpan(new AbsoluteSizeSpan(60),3,9, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
tv_content.setText(builder);

3.2 BackgroundColorSpan

此Span用来改变文本的背景颜色大小,示例如下:


SpannableStringBuilder builder = new SpannableStringBuilder("Hello World!");
builder.setSpan(new BackgroundColorSpan(Color.GREEN),3,9, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
tv_content.setText(builder);

3.3 ClickableSpan

此Span用来给文本添加点击效果,示例如下:


SpannableStringBuilder builder = new SpannableStringBuilder("Hello World!");
builder.setSpan(new ClickableSpan() {
@Override
public void onClick(@NonNull View widget) {
Toast.makeText(MainActivity.this,"ClickableSpan",Toast.LENGTH_SHORT).show();
}
}, 3, 9, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
tv_content.setText(builder);
tv_content.setMovementMethod(LinkMovementMethod.getInstance());
tv_content.setHighlightColor(Color.TRANSPARENT);

3.4 DrawableMarginSpan

此Span用来给段落添加drawable和padding,这个padding指的是drawable和文本之间的距离,默认值是0,Span要从文本的起始位置设置,否则Span将不会渲染或者错误地渲染,示例如下:


SpannableStringBuilder builder = new SpannableStringBuilder("Hello World!");
Drawable drawable = AppCompatResources.getDrawable(MainActivity.this,R.drawable.ic_launcher);
builder.setSpan(new DrawableMarginSpan(drawable,30), 0, builder.length(), Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
tv_content.setText(builder);

3.5 DynamicDrawableSpan

此Span使用drawable替换文本内容,示例如下:


SpannableStringBuilder builder = new SpannableStringBuilder("Hello World!");
builder.setSpan(new DynamicDrawableSpan() {
@Override
public Drawable getDrawable() {
Drawable drawable =
AppCompatResources.getDrawable(MainActivity.this,R.drawable.ic_launcher);
drawable.setBounds(0,0,drawable.getIntrinsicWidth(),drawable.getIntrinsicHeight());
return drawable;
}
}, 3, 9, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
tv_content.setText(builder);

3.6 ForegroundColorSpan

此Span可以用来改变文本的颜色,示例如下:


SpannableStringBuilder builder = new SpannableStringBuilder("Hello World!");
builder.setSpan(new ForegroundColorSpan(Color.GREEN), 3, 9, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
tv_content.setText(builder);

3.7 IconMarginSpan

此Span可以在文本开始的地方添加位图,而且可以在位图和文本之间设置padding,padding的默认值是0px,示例如下:


SpannableStringBuilder builder = new SpannableStringBuilder("Hello World!");
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher);
builder.setSpan(new IconMarginSpan(bitmap,30), 0, builder.length(), Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
tv_content.setText(builder);

3.8 ImageSpan

此Span可以使用Drawable替换文本,创建ImageSpan的构造方法有很多,示例如下:


SpannableStringBuilder builder = new SpannableStringBuilder("Hello World!");
builder.setSpan(new ImageSpan(MainActivity.this,R.drawable.ic_launcher), 3, 9, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
tv_content.setText(builder);

3.9 MaskFilterSpan

此Span可以给文本设置MaskFilter,例如给文本设置模糊效果,示例如下:


SpannableStringBuilder builder = new SpannableStringBuilder("Hello World!");
MaskFilter maskFilter = new BlurMaskFilter(10f, BlurMaskFilter.Blur.NORMAL);
builder.setSpan(new MaskFilterSpan(maskFilter), 3, 9, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
tv_content.setText(builder);

3.10 QuoteSpan

此Span可以在文本开始的地方添加一个垂直的线条,示例如下:


SpannableStringBuilder builder = new SpannableStringBuilder("Hello World!");
builder.setSpan(new QuoteSpan(Color.GREEN), 0, builder.length(), Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
tv_content.setText(builder);

3.11 RelativeSizeSpan

此Span可以按一定的比例缩放文本的大小,示例如下:


SpannableStringBuilder builder = new SpannableStringBuilder("Hello World!");
builder.setSpan(new RelativeSizeSpan(2.0f), 3, 9, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
tv_content.setText(builder);

3.12 ScaleXSpan

此Span以一定的系数在水平方向缩放文本的大小,示例如下:


SpannableStringBuilder builder = new SpannableStringBuilder("Hello World!");
builder.setSpan(new ScaleXSpan(2.5f), 3, 9, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
tv_content.setText(builder);

3.13 StrikethroughSpan

此Span可以在文本上添加下划线,示例如下:


SpannableStringBuilder builder = new SpannableStringBuilder("Hello World!");
builder.setSpan(new StrikethroughSpan(), 3, 9, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
tv_content.setText(builder);

3.14 StyleSpan

此Span可以设置文本的样式,可用的样式有Typeface.NORMAL、Typeface.BOLD、Typeface.ITALIC、Typeface.BOLD_ITALIC,示例如下:


SpannableStringBuilder builder = new SpannableStringBuilder("Hello World!");
builder.setSpan(new StyleSpan(Typeface.BOLD), 3, 9, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
tv_content.setText(builder);

3.15 SubscriptSpan

此Span可以将文本的基线移动到更低的地方,示例如下:


SpannableStringBuilder builder = new SpannableStringBuilder("Hello World!");
builder.setSpan(new SubscriptSpan(), 3, 9, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
tv_content.setText(builder);

3.16 SuperscriptSpan

此Span可以将文本的基线移动到更高的地方,示例如下:


SpannableStringBuilder builder = new SpannableStringBuilder("Hello World!");
builder.setSpan(new SuperscriptSpan(), 3, 9, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
tv_content.setText(builder);

3.17 UnderlineSpan()

此Span可以在文本下面添加下划线,示例如下:


SpannableStringBuilder builder = new SpannableStringBuilder("Hello World!");
builder.setSpan(new UnderlineSpan(), 3, 9, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
tv_content.setText(builder);

4.多个Span组合使用


Span不但可以单独使用,还可以组合在一起使用,以下示例演示了如何同时加粗文字,改变文字的颜色和添加下滑线:


SpannableStringBuilder builder = new SpannableStringBuilder("Hello World!");
builder.setSpan(new UnderlineSpan(), 3, 9, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
builder.setSpan(new ForegroundColorSpan(Color.GREEN), 3, 9, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
builder.setSpan(new StyleSpan(Typeface.BOLD), 3, 9, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
tv_content.setText(builder);

5.总结


Span的功能相当丰富,如改变文本颜色、大小、添加点击效果、加下划线等功能,本文介绍了经常用到的各种Span,Span支持单独使用和组合使用,使用它能够对文本进行各种灵活的操作,去实现个性化的需求。


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

看完这篇Broadcast你还不会,来找我

看完这篇文章,你将明白以下内容: broadcast是什么,使用场景 Android中广播的分类 广播的注册方式 本地广播优点、原理 广播的安全 1.1 什么是 BroadcastReceiver 是四大组件之一, 主要用于接收 app 发送的广播 内部...
继续阅读 »

看完这篇文章,你将明白以下内容:



  • broadcast是什么,使用场景

  • Android中广播的分类

  • 广播的注册方式

  • 本地广播优点、原理

  • 广播的安全


1.1 什么是 BroadcastReceiver



  • 是四大组件之一, 主要用于接收 app 发送的广播

  • 内部通信实现机制:通过 android 系统的 Binder 机制.


1.2 广播分类


1.2.1 无序广播



  • 也叫标准广播,是一种完全异步


执行的广播。



  • 在广播发出之后,所有广播接收器几乎都会在同一时刻接收到这条广播消息,它们之间没有任何先后顺序,广播的效率较高。

  • 优点: 完全异步, 逻辑上可被任何接受者收到广播,效率高

  • 缺点: 接受者不能将处理结果交给下一个接受者, 且无法终止广播.


1.2.2 有序广播




  • 是一种同步执行的广播。




  • 在广播发出之后,同一时刻只有一个广播接收器能够收到这条广播消息,当其逻辑执行完后该广播接收器才会继续传递。




  • 调用 SendOrderedBroadcast() 方法来发送广播,同时也可调用 abortBroadcast() 方法拦截该广播。可通过 <intent-filter> 标签中设置 android:property 属性来设置优先级,未设置时按照注册的顺序接收广播。




  • 有序广播接受器间可以互传数据。




  • 当广播接收器收到广播后,当前广播也可以使用 setResultData 方法将数据传给下一个接收器。




  • 使用 getStringExtra 函数获取广播的原始数据,通过 getResultData 方法取得上个广播接收器自己添加的数据,并可用 abortBroadcast 方法丢弃该广播,使该广播不再被别的接收器接收到。




  • 总结





  1. 按被接收者的优先级循序传播 A > B > C ,

  2. 每个都有权终止广播, 下一个就得不到

  3. 每一个都可进行修改操作, 下一个就得到上一个修改后的结果.


1.2.3 最终广播者



  • Context.sendOrderedBroadcast ( intent , receiverPermission , resultReceiver , scheduler , initialCode , initialData , initialExtras ) 时我们可以指定 resultReceiver 为最终广播接收者.

  • 如果比他优先级高的接受者不终止广播, 那么他的 onReceive 会执行两次

  • 第一次是正常的接收

  • 第二次是最终的接收

  • 如果优先级高的那个终止广播, 那么他还是会收到一次最终的广播


1.2.4 常见的广播接收者运用场景



  • 开机启动, sd 卡挂载, 低电量, 外拨电话, 锁屏等

  • 比如根据产品经理要求, 设计播放音乐时, 锁屏是否决定暂停音乐.


1.3 BroadcastReceiver 的种类


1.3.1 广播作为 Android 组件间的通信方式,如下使用场景:



对前一部分 “ 请描述一下 BroadcastReceiver ” 进行展开补充




  • APP 内部的消息通信。

  • 不同 APP 之间的消息通信。

  • Android 系统在特定情况下与 APP 之间的消息通信。

  • 广播使用了观察者模式,基于消息的发布 / 订阅事件模型。广播将广播的发送者和接受者极大程度上解耦,使得系统能够方便集成,更易扩展。

  • BroadcastReceiver 本质是一个全局监听器,用于监听系统全局的广播消息,方便实现系统中不同组件间的通信。

  • 自定义广播接收器需要继承基类 BroadcastReceiver ,并实现抽象方法 onReceive ( context, intent ) 。默认情况下,广播接收器也是运行在主线程,因此 onReceiver() 中不能执行太耗时的操作( 不超过 10s ),否则将会产生 ANR 问题。onReceiver() 方法中涉及与其他组件之间的交互时,可以使用发送 Notification 、启动 Service 等方式,最好不要启动 Activity


1.3.2 系统广播



  • Android 系统内置了多个系统广播,只要涉及手机的基本操作,基本上都会发出相应的系统广播,如开机启动、网络状态改变、拍照、屏幕关闭与开启、电量不足等。在系统内部当特定时间发生时,系统广播由系统自动发出。

  • 常见系统广播 Intent 中的 Action 为如下值:



  1. 短信提醒:android.provider.Telephony.SMS_RECEIVED

  2. 电量过低:ACTION_BATIERY_LOW

  3. 电量发生改变:ACTION_BATTERY_CHANGED

  4. 连接电源:ACTION_POWER_CO             



  • Android 7.0 开始,系统不会再发送广播 ACTION_NEW_PICTUREACTION_NEW_VIDEO ,对于广播 CONNECTIVITY_ACTION 必须在代码中使用 registerReceiver 方法注册接收器,在 AndroidManifest 文件中声明接收器不起作用。

  • Android 8.0 开始,对于大多数隐式广播,不能在 AndroidManifest 文件中声明接收器。


1.3.3 局部广播



  • 局部广播的发送者和接受者都同属于一个 APP

  • 相比于全局广播具有以下优点:



  1. 其他的 APP 不会受到局部广播,不用担心数据泄露的问题。

  2. 其他 APP 不可能向当前的 APP 发送局部广播,不用担心有安全漏洞被其他 APP 利用。

  3. 局部广播比通过系统传递的全局广播的传递效率更高。



  • Android v4 包中提供了 LocalBroadcastManager 类,用于统一处理 APP 局部广播,使用方式与全局广播几乎相同,只是调用注册 / 取消注册广播接收器和发送广播偶读方法时,需要通过 LocalBroadcastManager 类的 getInstance() 方法获取的实例调用。


1.4 BroadcastReceiver 注册方式


1.4.1 静态注册


AndroidManifest.xml 文件中配置。


<receiver android:name=".MyReceiver" android:exported="true">
<intent-filter>
<!-- 指定该 BroadcastReceiver 所响应的 Intent 的 Action -->
<action android:name="android.intent.action.INPUT_METHOD_CHANGED"
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>


  • 两个重要属性需要关注:



  1. android: exported 其作用是设置此 BroadcastReceiver 能否接受其他 APP 发出的广播 ,当设为 false 时,只能接受同一应用的的组件或具有相同 user ID 的应用发送的消息。这个属性的默认值是由 BroadcastReceiver 中有无 Intent-filter 决定的,如果有 Intent-filter ,默认值为 true ,否则为 false

  2. android: permission 如果设置此属性,具有相应权限的广播发送方发送的广播才能被此 BroadcastReceiver 所接受;如果没有设置,这个值赋予整个应用所申请的权限。


1.4.2 动态注册



  • 调用 ContextregisterReceiver ( BroadcastReceiver receiver , IntentFilter filter ) 方法指定。


1.5 在 Mainfest 和代码如何注册和使用 BroadcastReceiver ? ( 一个 action 是重点 )


1.5.1 使用文件注册 ( 静态广播 )




  • 只要 app 还在运行,那么会一直收到广播消息




  • 演示:





  1. 一个 app 里: 自定义一个类继承 BroadcastReceiver 然后要求重写 onReveiver 方法


public class MyBroadCastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Log.d("MyBroadCastReceiver", "收到信息,内容是 :&emsp;" + intent.getStringExtra("info") + "");
}
}


  1. 清单文件注册,并设置 Action , 就那么简单完成接收准备工作


<receiver android:name=".MyBroadCastReceiver">
<intent-filter>
<action android:name="myBroadcast.action.call"/>
</intent-filter>
</receiver>

1.5.2 代码注册 ( 动态广播 )




  • 当注册的 Activity 或者 Service 销毁了那么就会接收不到广播.




  • 演示:





  1. 在和广播接受者相同的 app 里的 MainActivity 添加一个注册按钮 , 用来注册广播接收者

  2. 设置意图过滤,添加 Action


//onCreate创建广播接收者对象
mReceiver = new MyBroadCastReceiver();

//注册按钮
public void click(View view) {
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction("myBroadcast.action.call");
registerReceiver(mReceiver, intentFilter);
}


  1. 销毁的时候取消注册


@Override
protected void onDestroy() {
unregisterReceiver(mReceiver);
super.onDestroy();
}

1.5.3 在另一个 app , 定义一个按钮, 设置意图, 意图添加消息内容, 意图设置 action( ... ) 要匹配 , 然后发送广播即可.


public void click(View view) {
Intent intent = new Intent();
intent.putExtra("info", "消息内容");
intent.setAction("myBroadcast.action.call");
sendBroadcast(intent);
}


  • 运行两个 app 之后:



  1. 静态注册的方法: 另一 app 直接发广播就收到了

  2. 动态注册的方法: 自己的 app 先代码注册,然后另一个 app 直接发广播即可.


1.6 BroadcastReceiver 的实现原理是什么?




  • Android 中的广播使用了设计模式中的观察者模式:基于消息的发布 / 订阅事件模型。




  • 模型中主要有 3 个角色:





  1. 消息订阅者( 广播接收者 )

  2. 消息发布者( 广播发布者 )

  3. 消息中心( AMS,即 Activity Manager Service


1.6.1 原理:



  • 广播接收者通过 Binder 机制在 AMSActivity Manager Service ) 注册;

  • 广播发送者通过 Binder 机制向 AMS 发送广播;

  • AMS 根据广播发送者要求,在已注册列表中,寻找合适的 BroadcastReceiver ( 寻找依据:IntentFilter / Permission );

  • AMS 将广播发送到 BroadcastReceiver 相应的消息循环队列中;

  • 广播接收者通过消息循环拿到此广播,并回调 onReceive() 方法。

  • 需要注意的是:广播的发送和接受是异步的,发送者不会关心有无接收者或者何时收到。


1.7 本地广播



  • 本地广播机制使得发出的广播只能够在应用程序的内部进行传递,并且广播接收器也只能接受来自本应用程序发出的广播,则安全性得到了提高。

  • 本地广播主要是使用了一个 LocalBroadcastManager 来对广播进行管理,并提供了发送广播和注册广播接收器的方法。

  • 开发者只要实现自己的 BroadcastReceiver 子类,并重写 onReceive ( Context context, Intetn intent ) 方法即可。

  • 当其他组件通过 sendBroadcast()sendStickyBroadcast()sendOrderBroadcast() 方法发送广播消息时,如果该 BroadcastReceiver 也对该消息“感兴趣”,BroadcastReceiveronReceive ( Context context, Intetn intent ) 方法将会被触发。

  • 使用步骤:



  1. 调用 LocalBroadcastManager.getInstance() 获得实例

  2. 调用 registerReceiver() 方法注册广播

  3. 调用 sendBroadcast() 方法发送广播

  4. 调用 unregisterReceiver() 方法取消注册


1.7.1 注意事项:



  1. 本地广播无法通过静态注册方式来接受,相比起系统全局广播更加高效。

  2. 在广播中启动 Activity 时,需要为 Intent 加入 FLAG_ACTIVITY_NEW_TASK 标记,否则会报错,因为需要一个栈来存放新打开的 Activity

  3. 广播中弹出 Alertdialog 时,需要设置对话框的类型为 TYPE_SYSTEM_ALERT ,否则无法弹出。

  4. 不要在 onReceiver() 方法中添加过多的逻辑或者进行任何的耗时操作,因为在广播接收器中是不允许开启线程的,当 onReceiver() 方法运行了较长时间而没有结束时,程序就会报错。


1.8 Sticky Broadcast 粘性广播



  • 如果发送者发送了某个广播,而接收者在这个广播发送后才注册自己的 Receiver ,这时接收者便无法接收到刚才的广播

  • 为此 Android 引入了 StickyBroadcast ,在广播发送结束后会保存刚刚发送的广播( Intent ),这样当接收者注册完 Receiver 后就可以继续使用刚才的广播。

  • 如果在接收者注册完成前发送了多条相同 Action 的粘性广播,注册完成后只会收到一条该 Action 的广播,并且消息内容是最后一次广播内容。

  • 系统网络状态的改变发送的广播就是粘性广播。



  1. 粘性广播通过 ContextsendStickyBroadcast ( Intent ) 接口发送,需要添加权限

  2. uses-permission android:name=”android.permission.BROADCAST_STICKY”

  3. 也可以通过 ContextremoveStickyBroadcast ( Intent intent ) 接口移除缓存的粘性广播


1.9 LocalBroadcastManager 详解


1.9.1 特点:



  1. 使用它发送的广播将只在自身APP内传播,因此你不必担心泄漏隐私数据;

  2. 其他 APP 无法对你的 APP 发送该广播,因为你的APP根本就不可能接收到非自身应用发送的该广播,因此你不必担心有安全漏洞可以利用;

  3. 比系统的全局广播更加高效。


1.9.2 源码分析 :



  1. LocalBroadcastManager 内部协作主要是靠这两个 Map 集合:MReceiversMActions ,当然还有一个 List 集合 MPendingBroadcasts ,这个主要就是存储待接收的广播对象。

  2. LocalBroadcastManager 高效的原因主要是因为它内部是通过 Handler 实现的,它的 sendBroadcast() 方法含义并非和我们平时所用的一样,它的 sendBroadcast() 方法其实是通过 handler 发送一个 Message 实现的;

  3. 既然它内部是通过 Handler 来实现广播的发送的,那么相比于系统广播通过 Binder 实现那肯定是更高效了,同时使用 Handler 来实现,别的应用无法向我们的应用发送该广播,而我们应用内发送的广播也不会离开我们的应用;


1.9.3 BroadcastReceiver 安全问题



  • BroadcastReceiver 设计的初衷是从全局考虑可以方便应用程序和系统、应用程序之间、应用程序内的通信,所以对单个应用程序而言BroadcastReceiver 是存在安全性问题的 ( 恶意程序脚本不断的去发送你所接收的广播 ) 。为了解决这个问题 LocalBroadcastManager 就应运而生了。

  • LocalBroadcastManagerAndroid Support 包提供了一个工具,用于在同一个应用内的不同组件间发送 BroadcastLocalBroadcastManager 也称为局部通知管理器,这种通知的好处是安全性高,效率也高,适合局部通信,可以用来代替 Handler 更新 UI


1.9.4 广播的安全性



  • Android 系统中的广播可以跨进程直接通信,会产生以下两个问题:



  1. 其他 APP 可以接收到当前 APP 发送的广播,导致数据外泄。

  2. 其他 APP 可以向当前 APP 放广播消息,导致 APP 被非法控制。



  • 发送广播



  1. 发送广播时,增加相应的 permission ,用于权限验证。

  2. Android 4.0 及以上系统中发送广播时,可以使用 setPackage() 方法设置接受广播的包名。

  3. 使用局部广播。



  • 接受广播



  1. 注册广播接收器时,增加相应的 permission ,用于权限验证。

  2. 注册广播接收器时,设置 android:exported 的值为false。



  • 使用局部广播



  1. 发送广播时,如果增加了 permission

  2. 那接受广播的 APP 必须申请相应权限,这样才能收到对应的广播,反之亦然。


1.9.5 使用 BroadcastReceiver 的好处



  1. 因广播数据在本应用范围内传播,你不用担心隐私数据泄露的问题。

  2. 不用担心别的应用伪造广播,造成安全隐患。

  3. 相比在系统内发送全局广播,它更高效。


1.10 如何让自己的广播只让指定的 app 接收?



  • 在发送广播的 app 端,自定义定义权限, 那么想要接收的另外 app 端必须声明权限才能收到.



  1. 权限, 保护层级是普通正常.

  2. 用户权限


<permission android:name="broad.ok.receiver" android:protectionLevel="normal"/>
<uses-permission android:name="broad.ok.receiver" />


  1. 发送广播的时候加上权限字符串


public void click(View view) {
Intent intent = new Intent();
intent.putExtra("info", "消息内容");
intent.setAction("myBroadcast.action.call");
sendBroadcast(intent, "broad.ok.receiver");
//sendOrderedBroadcast(intent,"broad.ok.receiver");
}


  1. 其他app接收者想好获取广播,必须声明在清单文件权限


<uses-permission android:name="broad.ok.receiver"/>

1.11 广播的优先级对无序广播生效吗?



  • 优先级对无序也生效.


1.12 动态注册的广播优先级谁高?



  • 谁先注册,谁就高


1.13 如何判断当前的 BrodcastReceiver 接收到的是有序还是无序的广播?



  • onReceiver 方法里,直接调用判断方法得返回值


public void onReceive(Context context, Intent intent) {
Log.d("MyBroadCastReceiver", "收到信息,内容是 :&emsp;" + intent.getStringExtra("info") + "");
boolean isOrderBroadcast = isOrderedBroadcast();
}

1.14 BroadcastReceiver 不能执行耗时操作



  • 一方面



  1. BroadcastReceiver 一般处于主线程。

  2. 耗时操作会导致 ANR



  • 另一方面



  1. BroadcastReceiver 启动时间较短。

  2. 如果一个进程里面只存在一个 BroadcastReceiver 组件。并且在其中开启子线程执行耗时任务。

  3. 系统会认为该进程是优先级最低的空进程。很容易将其杀死。


总结






  1. 本文对 BroadcastReceiverContentProvider 的 知识进行了详尽的总结,如果有可以补充的知识点,欢迎大家在评论区指出。




  2. 希望大家通过本次阅读都能有所收获。


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

让 Flutter 在鸿蒙系统上跑起来

前言 鸿蒙系统 (HarmonyOS)是华为推出的一款面向未来、面向全场景的分布式操作系统。在传统单设备系统能力的基础上,鸿蒙提出了基于同一套系统能力、适配多种终端形态的分布式理念。自 2020 年 9 月 HarmonyOS 2.0 发布以来,华为加快了鸿蒙...
继续阅读 »

前言


鸿蒙系统 (HarmonyOS)是华为推出的一款面向未来、面向全场景的分布式操作系统。在传统单设备系统能力的基础上,鸿蒙提出了基于同一套系统能力、适配多种终端形态的分布式理念。自 2020 年 9 月 HarmonyOS 2.0 发布以来,华为加快了鸿蒙系统大规模落地的步伐,预计 2021 年底,鸿蒙系统会覆盖包括手机、平板、智能穿戴、智慧屏、车机在内数亿台终端设备。对移动应用而言,新的系统理念、新的交互形式,也意味着新的机遇。如果能够利用好鸿蒙的开发生态及其特性能力,可以让应用覆盖更多的交互场景和设备类型,从而带来新的增长点。


与面临的机遇相比,适配鸿蒙系统带来的挑战同样巨大。当前手机端,尽管鸿蒙系统仍然支持安卓 APK 安装及运行,但长期来看,华为势必会抛弃 AOSP,逐步发展出自己的生态,这意味着现有安卓应用在鸿蒙设备上将会逐渐变成“二等公民”。然而,如果在 iOS 及 Android 之外再重新开发和维护一套鸿蒙应用,在如今业界越来越注重开发迭代效率的环境下,所带来的开发成本也是难以估量的。因此,通过打造一套合适的跨端框架,以相对低的成本移植应用到鸿蒙平台,并利用好该系统的特性能力,就成为了一个非常重要的选项。


在现有的众多跨端框架当中,Flutter 以其自渲染能力带来的多端高度一致性,在新系统的适配上有着突出的优势。虽然Flutter 官方并没有适配鸿蒙的计划,但经过一段时间的探索和实践,美团外卖 MTFlutter 团队成功实现了 Flutter 对于鸿蒙系统的原生支持。


这里也要提前说明一下,因为鸿蒙系统目前还处于Beta版本,所以这套适配方案还没有在实际业务中上线,属于技术层面比较前期的探索。接下来本文会通过原理和部分实现细节的介绍,分享我们在移植和开发过程中的一些经验。希望能对大家有所启发或者帮助。


背景知识和基础概念介绍


在适配开始之前,我们要明确好先做哪些事情。先来回顾一下 Flutter 的三层结构:



在 Flutter 的架构设计中,最上层为框架层,使用 Dart 语言开发,面向 Flutter 业务的开发者;中间层为引擎层,使用 C/C++ 开发,实现了 Flutter 的渲染管线和 Dart 运行时等基础能力;最下层为嵌入层,负责与平台相关的能力实现。显然我们要做的是将嵌入层移植到鸿蒙上,确切地说,我们要通过鸿蒙原生提供的平台能力,重新实现一遍 Flutter 嵌入层


对于 Flutter 嵌入层的适配,Flutter 官方有一份不算详细的指南,实际操作起来成本很高。由于鸿蒙的业务开发语言仍然可用 Java,在很多基础概念上与 Android 也有相似之处(如下表所示),我们可以从 Android 的实现入手,完成对鸿蒙的移植。



Flutter 在鸿蒙上的适配


如前文所述,要完成 Flutter 在新系统上的移植,我们需要完整实现 Flutter 嵌入层要求的所有子模块,而从能力支持角度,渲染交互以及其他必要的原生平台能力是保证 Flutter 应用能够运行起来的最基本的要素,需要优先支持。接下来会依次进行介绍。


1. 渲染流程打通


我们再来回顾一下 Flutter 的图像渲染流程。如图所示,设备发起垂直同步(VSync)信号之后,先经过 UI 线程的渲染管线(Animate/Build/Layout/Paint),再经过 Raster 线程的组合和栅格化,最终通过 OpenGL 或 Vulkan 将图像上屏。这个流程的大部分工作都由框架层和引擎层完成,对于鸿蒙的适配,我们主要关注的是与设备自身能力相关的问题,即:


(1) 如何监听设备的 VSync 信号并通知 Flutter 引擎?
(2) OpenGL/Vulkan 用于上屏的窗口对象从何而来?



VSync 信号的监听及传递


在 Flutter 引擎的 Android 实现中,设备的 VSync 信号通过 Choreographer 触发,它产生及消费流程如下图所示:


Flutter VSync


Flutter 框架注册 VSync 回调之后,通过 C++ 侧的 VsyncWaiter 类等待 VSync 信号,后者通过 JNI 等一系列调用,最终 Java 侧的 VsyncWaiter 类调用 Android SDK 的 Choreographer.postFrameCallback 方法,再通过 JNI 一层层传回 Flutter 引擎消费掉此回调。Java 侧的 VsyncWaiter 核心代码如下:


@Override
public void asyncWaitForVsync(long cookie) {
Choreographer.getInstance()
.postFrameCallback(
new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
float fps = windowManager.getDefaultDisplay().getRefreshRate();
long refreshPeriodNanos = (long) (1000000000.0 / fps);
FlutterJNI.nativeOnVsync(
frameTimeNanos, frameTimeNanos + refreshPeriodNanos, cookie);
}
});
}

在整个流程中,除了来自 Android SDK 的 Choreographer 以外,大多数逻辑几乎都由 C++ 和 Java 的基础 SDK 实现,可以直接在鸿蒙上复用,问题是鸿蒙目前的 API 文档中尚没有开放类似 Choreographer 的能力。所以现阶段我们可以借用鸿蒙提供的类似 iOS Grand Central Dispatch 的线程 API,模拟出 VSync 的信号触发与回调:


@Override
public void asyncWaitForVsync(long cookie) {
// 模拟每秒 60 帧的屏幕刷新间隔:向主线程发送一个异步任务, 16ms 后调用
applicationContext.getUITaskDispatcher().delayDispatch(() -> {
float fps = 60; // 设备刷新帧率,HarmonyOS 未暴露获取帧率 API,先写死 60 帧
long refreshPeriodNanos = (long) (1000000000.0 / fps);
long frameTimeNanos = System.nanoTime();
FlutterJNI.nativeOnVsync(frameTimeNanos, frameTimeNanos + refreshPeriodNanos, cookie);
}, 16);
};

渲染窗口的构建及传递


在这一部分,我们需要在鸿蒙系统上构建平台容器,为 Flutter 引擎的图形渲染提供用于上屏的窗口对象。同样,我们参考 Flutter for Android 的实现,看一下 Android 系统是怎么做的:



Flutter 在 Android 上支持 Vulkan 和 OpenGL 两种渲染引擎,篇幅原因我们只关注 OpenGL。抛开复杂的注册及调用细节,本质上整个流程主要做了三件事:



  1. 创建了一个视图对象,提供可用于直接绘制的 Surface,将它通过 JNI 传递给原生侧;

  2. 在原生侧获取 Surface 关联的本地窗口对象,并交给 Flutter 的平台容器;

  3. 将本地窗口对象转换为 OpenGL ES 可识别的绘图表面(EGLSurface),用于 Flutter 引擎的渲染上屏。


接下来我们用鸿蒙提供的平台能力实现这三点。


a. 可用于直接绘制的视图对象


鸿蒙系统的 UI 框架提供了很多常用视图组件(Component),比如按钮、文字、图片、列表等,但我们需要抛开这些上层组件,获得直接绘制的能力。借助官方 媒体播放器开发指导 文档,可以发现鸿蒙提供了 SurfaceProvider 类,它管理的 Surface 对象可以用于视频解码后的展示。而 Flutter 渲染与视频上屏从原理上是类似的,因此我们可以借用 SurfaceProvider 实现 Surface 的管理和创建:


// 创建一个用于管理 Surface 的容器组件
SurfaceProvider surfaceProvider = new SurfaceProvider(context);
// 注册视图创建回调
surfaceProvider.getSurfaceOps().get().addCallback(surfaceCallback);

// ... 在 surfaceCallback 中
@Override
public void surfaceCreated(SurfaceOps surfaceOps) {
Surface surface = surfaceOps.getSurface();
// ...将 surface 通过 JNI 交给 Native 侧
FlutterJNI.onSurfaceCreated(surface);
}

b. 与 Surface 关联的本地窗口对象


鸿蒙目前开放的 Native API 并不多,在官方文档中我们可以比较容易地找到 Native_layer API。根据文档的说明,Native API 中的 NativeLayer 对象刚好对应了 Java 侧的 Surface 类,借助 GetNativeLayer 方法,我们实现了两者之间的转化:


// platform_view_android_jni_impl.cc
static void SurfaceCreated(JNIEnv* env, jobject jcaller, jlong shell_holder, jobject jsurface) {
fml::jni::ScopedJavaLocalFrame scoped_local_reference_frame(env);
// 通过鸿蒙 Native API 获取本地窗口对象 NativeLayer
auto window = fml::MakeRefCounted(
GetNativeLayer(env, jsurface));
ANDROID_SHELL_HOLDER->GetPlatformView()->NotifyCreated(std::move(window));
}

c. 与本地窗口对象关联的 EGLSurface


在 Android 的 AOSP 实现中,EGLSurface 可通过 EGL 库的 eglCreateWindowSurface 方法从本地窗口对象 ANativeWindow 创建而来。对于鸿蒙而言,虽然我们没有从公开文档找到类似的说明,但是 鸿蒙标准库 默认支持了 OpenGL ES,而且鸿蒙 SDK 中也附带了 EGL 相关的库及头文件,我们有理由相信在鸿蒙系统上,EGLSurface 也可以通过此方法从前一步生成的 NativeLayer 转化而来,在之后的验证中我们也确认了这一点:


// window->handle() 即为之前得到的 NativeLayer
EGLSurface surface = eglCreateWindowSurface(
display, config_, reinterpret_cast(window->handle()),
attribs);
//...交给 Flutter 渲染管线

2. 交互能力实现


交互能力是支撑 Flutter 应用能够正常运行的另一个基本要求。在 Flutter 中,交互包含了各种触摸事件、鼠标事件、键盘录入事件的传递及消费。以触摸事件为例,Flutter 事件传递的整个流程如下图所示:


Flutter 事件分发


iOS/Android 的原生容器通过触摸事件的回调 API 接收到事件之后,会将其打包传递至引擎层,后者将事件传发给 Flutter 框架层,并完成事件的消费、分发和逻辑处理。同样,整个流程的大部分工作已经由 Flutter 统一,我们要做的仅仅是在原生容器上监听用户的输入,并封装成指定格式交给引擎层而已。


在鸿蒙系统上,我们可以借助平台提供的 多模输入 API,实现多种类型事件的监听:


flutterComponent.setTouchEventListener(touchEventListener); // 触摸及鼠标事件
flutterComponent.setKeyEventListener(keyEventListener); // 键盘录入事件
flutterComponent.setSpeechEventListener(speechEventListener); // 语音录入事件

对于事件的封装处理,可以复用 Android 已有逻辑,只需要关注鸿蒙与 Android 在事件处理上的对应关系即可,比如触摸事件的部分对应关系:



3. 其他必要的平台能力


为了保证 Flutter 应用能够正常运行,除了最基本的渲染和交互外,我们的嵌入层还要提供资源管理、事件循环、生命周期同步等平台能力。对于这些能力 Flutter 大多都在嵌入层的公共部分有抽象类声明,只需要使用鸿蒙 API 重新实现一遍即可。


比如资源管理,引擎提供了 AssetResolver 声明,我们可以使用鸿蒙 Rawfile API 来实现:


class HAPAssetMapping : public fml::Mapping {
public:
HAPAssetMapping(RawFile* asset) : asset_(asset) {}
~HAPAssetMapping() override { CloseRawFile(asset_); }

size_t GetSize() const override { return GetRawFileSize(asset_); }

const uint8_t* GetMapping() const override {
return reinterpret_cast(GetRawFileBuffer(asset_));
}

private:
RawFile* const asset_;

FML_DISALLOW_COPY_AND_ASSIGN(HAPAssetMapping);
};

对于事件循环,引擎提供了 MessageLoopImpl 抽象类,我们可以使用鸿蒙 Native_EventHandler API 实现:


// runner_ 为鸿蒙 EventRunnerNativeImplement 的实例
void MessageLoopHarmony::Run() {
FML_DCHECK(runner_ == GetEventRunnerNativeObjForThread());
int result = ::EventRunnerRun(runner_);
FML_DCHECK(result == 0);
}

void MessageLoopHarmony::Terminate() {
int result = ::EventRunnerStop(runner_);
FML_DCHECK(result == 0);
}

对于生命周期的同步,鸿蒙的 Page Ability 提供了完整的生命周期回调(如下图所示),我们只需要在对应的时机将状态上报给引擎即可。


Page Ability Lifecycle


当以上这些能力都准备好之后,我们就可以成功把 Flutter 应用跑起来了。以下是通过 DevEco Studio 运行官方 flutter gallery 应用的截图,截图中 Flutter 引擎已经使用鸿蒙系统的平台能力进行了重写:


DevEco Running Flutte


借由鸿蒙的多设备支持能力,此应用甚至可在 TV、车机、手表、平板等设备上运行:


Flutter Multiple Devices


总结和展望


通过上述的构建和适配工作,我们以极小的开发成本实现了 Flutter 在鸿蒙系统上的移植,基于 Flutter 开发的上层业务几乎不做任何修改就可以在鸿蒙系统上原生运行,为迎接鸿蒙系统后续的大规模推广也提前做好了技术储备。


当然,故事到这里并没有结束。在最基本的运行和交互能力之上,我们更需要关注 Flutter 与鸿蒙自身生态的结合:如何优雅地适配鸿蒙的分布式技术?如何用 Flutter 实现设备之间的快速连接、资源共享?现有的众多 Flutter 插件如何应用到鸿蒙系统上?未来 MTFlutter 团队将在这些方面做更深入的探索,因为解决好这些问题,才是真正能让应用覆盖用户生活的全场景的关键。


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

Flutter PlatformView 在闲鱼直播业务中的实践

背景 闲鱼近期实现了端上直播间的 Flutter 技术重构,验证和拓展了 Flutter 在音视频领域的业务边界。因为直播丰富的玩法和可变的交互,通常我们会在直播间页面覆盖一层互动层,用于处理和展示业务互动行为。这一互动层,通常是由 H5/Weex 等技术来实...
继续阅读 »

背景


闲鱼近期实现了端上直播间的 Flutter 技术重构,验证和拓展了 Flutter 在音视频领域的业务边界。因为直播丰富的玩法和可变的交互,通常我们会在直播间页面覆盖一层互动层,用于处理和展示业务互动行为。这一互动层,通常是由 H5/Weex 等技术来实现的,以满足动态性和业务投放的需求。因为其背后有着一整套配套的解决方案和能力,显然在 Flutter 场景下,复用或移植成熟的 Native 能力是比较好的解决方案,PlatformView 是最适合用于实现该组件的技术,这也是我们采用的方案。


什么是 PlatformView?


PlatformView 技术是 Flutter 提供的一种能够将 Native 组件嵌入到 Flutter 页面中的能力,有了这种能力,一些 Native 上非常成熟的功能组件,例如地图、广告页面、WebView 就可以很方便地和 Flutter 结合,在 Flutter 页面上展示。


实现技术上,iOS 中,PlatformView 的 Native View 会被加入到 Flutter 的 UI 视图层级中,这种方式称之为 Hybrid Composition;而 Android 支持使用 VirtualDisplay 和 Hybrid Composition 两种模式,前者将 Native View 绘制到内存中,然后和 Flutter Widget 一起渲染到 Flutter 页面中,后者和 iOS 上类似。闲鱼目前在 Android 中使用的是 VirtualDisplay 这种模式。


直播间互动层组件


互动层是一个覆盖整个直播间的组件,隶属于某一特定的直播间,可以跟随该直播间上下翻页滑动,一般情况下处于直播间 View 层级的最上层,这样才能做到可以在直播间任意位置布局任意的元素并展示。它是一个背景透明的组件,当用户点击互动层上的元素时,由互动层来进行响应交互,而当用户点击透明区域时,事件会穿透互动层,由下面层级上最合适的组件来进行响应,不影响正常的直播间功能。这就要求我们对该组件的事件分发进行一些处理。这里主要的处理方案是,获取用户的点击位置坐标点,判断组件该像素点的透明度,根据一定的阈值来区分究竟是该由谁来响应。以 iOS 为例,我们知道 iOS 的事件响应分为两个阶段:第一阶段用于寻找最佳响应者,即在 View Tree 上不断调用 hitTest 和 pointInside 方法进行检测,第二阶段才是真正响应事件。所以对于 iOS 实现来说 ,我们只要干预第一阶段,重写该互动层 View 的 pointInside 方法增加上我们的透明度判断逻辑,就可以实现。Android 也是进行类似的处理。


[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wV7LXOVn-1636119253919)(https://upload-images.jianshu.io/upload_images/27208383-1a3401d11fb3db0a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]


PlatformView 互动层的事件分发问题


虽然通过 PlatformView 可以很方便地嵌入互动层 Native View,但在这个场景下事件处理遇到了一些麻烦。通常情况下,如果嵌入的 PlatformView 是一个正常响应事件的组件,那一般不会有问题,但由上面的分析可知,我们需要对事件进行特殊处理,因为互动层 PlatformView 处于整个直播间的最上层,所以正常情况下,所有它下面的 Flutter 组件都无法响应事件。我们可以将 PlatformView 的 hitTestBehavior 设置为 PlatformViewHitTestBehavior.transparent,这样可以将事件穿透到下面其他的 Flutter 组件,但是互动层本身就无法响应事件了。


PlatformView 有两个身份,一个是作为 Native View,另外一个是作为 Flutter 组件树上的一个节点(Widget),因此它上面的事件由两部分共同配合处理。


所以我们需要先探究一下 Flutter 框架是如何处理 PlatformView 在 Native 和 Flutter 两侧的事件分发和控制的,下面以 iOS 为例来进行具体分析。


PlatformView 事件响应分析


在这里插入图片描述



由 UI 层级图和下面的源码可以看出,PlatformView 是 FlutterView 的 subView。源码中的 embeded_view 是我们真正提供给 Flutter 的 Native View,它会传递给 FlutterTouchInterceptingView 的构造方法,成为其 subView,FlutterTouchInterceptingView 又是 ChildClippingView 的 subView。



FlutterTouchInterceptingView


这几个 view 中,FlutterTouchInterceptingView 是 Flutter 用来控制和处理 PlatformView 事件的关键。Flutter 需要有这个一个View 来拦截事件,因为事件毕竟是从 Native 传递给 Flutter 的,而 PlatformView 本身也是个 Native View,如果不对作用在 PlatformView 上的事件进行拦截的话,PlatformView 自身就会消化掉事件,而 Flutter 侧则感知不到也没法控制了。



FlutterTouchInterceptingView 的 frame 和 embededView 保持一致,并作为其 superView,根据 iOS 的事件传递规则,FlutterTouchInterceptingView 会先接收到事件。由注释可知,FlutterTouchInterceptingView 实现了两个能力:一是它会延迟或者阻止事件到达 embededView;二是它会将所有的事件直接转发给 FlutterView。而这两点,分别是由 DelayingGestureRecognizer 和 ForwardingGestureRecognizer 这两个手势来完成的。
在这里插入图片描述


DelayingGestureRecognizer


DelayingGestureRecognizer 需要延迟其他事件响应,并且将除了 ForwardingGestureRecognizer 之外的其他手势都失效。



DelayingGestureRecognizer 是添加在 embededView 的 superView(FlutterTouchInterceptingView) 上的手势(Gesture),之所以能够起作用拦截 embededView 的手势和事件,原因在于 iOS 的手势识别优先级高于普通的事件响应,且响应链一旦确定,每次事件响应,整条响应链上手势的 delegate 方法都会被调用,用于确定最终可以识别的手势。如下图,如果触摸了 View4,确定 View 4 为最佳响应者,则从 View4 到 rootView 上的所有手势(gesture2、gesture3、gesture4、gesture5)的 delegate 方法都会被调用。
在这里插入图片描述


ForwardingGestureRecognizer


ForwardingGestureRecognizer 的实现就很简单了,它重写了事件的相关处理方法,将事件直接转发。


在这里插入图片描述


因为 PlatformView 也作为 Flutter Widget Tree 的一个节点,事件转发到 Flutter 之后,遵循 Flutter 的事件处理机制,和其他手势一起在竞技场(Arena)中角逐是否能够响应。最终如果竞争成功,事件该由 PlatformView 来响应,则 FlutterTouchInterceptingView 的 releaseGesture 方法会被调用,DelayingGestureRecognizer 手势会被置成 Failed 状态,其他事件就可以开始响应。相应地如果竞争失败,那么 FlutterTouchInterceptingView 的 blockGesture 方法会被调用,DelayingGestureRecognizer 手势会被置成 Ended 状态,这表明事件被它响应并且完成了,其他手势或者 View 响应者就不会再响应该事件了。
在这里插入图片描述
在这里插入图片描述


解决事件分发问题


由上面 iOS 上的的事件原理可知,Flutter 主要是通过 DelayingGestureRecoginzer 和 ForwardingGestureRecoginizer 这两个手势来干预和控制 PlatformView 上 Native 的事件分发行为。所以可以想到,如果没有这两个事件,事件的响应又会和我们熟悉的 Native 流程一致。


所以想要自定义 PlatformView 事件分发,在 iOS 上我们可以这么做:


1.根据需要设置 PlatformView Widget 的 hitTestBehavior 参数;


2.重写 PlatformView 的 pointInside 方法,在里面增加控制 Flutter 这两个手势的逻辑。因为 hitTest 寻找最佳响应者的过程一定在响应链响应之前,所以此处对 Flutter 手势的处理,不会影响事件转发给 Flutter 后的处理逻辑。
在这里插入图片描述


具体到直播互动场景来说,为了能让事件在大多数情况下能够被互动层下面的组件响应,在 Flutter 侧 PlatformView Widget 的 hitTestBehavior 需要设置为 PlatformViewHitTestBehavior.transparent,目的是为了让 PlatformView Widget 之下的 Flutter Widget 可以响应事件。重写 PlatformView 的 pointInside,如果透明度判断认为该由互动层来响应,则禁用 Flutter 的这两个手势;如果不该由互动层响应(即该由其他 Flutter 组件来响应),则恢复这两个手势响应,不影响正常的逻辑。相关实现代码如下:


[图片上传失败...(image-1c9fb8-1636119207800)]


因为 DelayingGestureRecoginzer 和 ForwardingGestureRecoginizer 这两个手势是定义在 FlutterTouchInterceptingView 中,而 FlutterTouchInterceptingView 是我们互动层 Native View 的 superView,所以代码中的 self.fluttterForwardGestureRecognizer 和 self.flutterDelayingGestureRecognizer 可以通过反射、循环遍历 superView 的手势列表来获取到。


关于 Android 上的处理


因为 Android 上 PlatfromView 采用了 VirtualDisplay 方案实现的,所以 FlutterView 和 PlatfromView 并不是真正处于同一个 ViewTree 中,因而在这个问题的处理上面和 iOS 略有不同,但原理相通。这里简单的说一下Android 上面的情况。


PlatformView 原本的设计中是由 FlutterView 接收 Android 的原生 TouchEvent,然后转化为 Flutter 中用于事件处理的 Event,再分发给 Flutter中 的 Widget。当 PlatformView 的 Widget(AndroidView)接收到事件后,会再次将事件转化为 Android 的 TouchEvent,然后转给 Native 的 PlatformView 实现 View。如图:


[图片上传失败...(image-7e84ed-1636119207800)]


我们可以在 FlutterView 外面再套一层用于事件处理的 View(EventInterceptView),该 View 会优先接收到事件,然后根据实际需要,决定事件转发给 FlutterView 还是 PlatformView。如图:
在这里插入图片描述


通过上述方案,我们在 iOS 和 Android 两端都可以做到在 PlatformView 互动层中实现透明度判断逻辑,并交给正确的响应者(Flutter 组件或者 Native View)来进行事件响应。使用 PlatformView 也让我们最大限度地复用了直播互动层背后对应的一整套解决方案,业务能力和使用体验都达到了和 Native 原生实现一样的效果。


写在最后


PlatformView 提供了在 Flutter 页面中嵌入 Native View 的能力,方便了很多业务场景和功能的实现,但 PlatformView 技术也还存在一些问题,例如使用了 PlatformView 在某些场景下可能会导致图片、视频的纹理错乱等。闲鱼在直播业务中第一次在生产环境中使用了 PlatformView 技术,也解决了一些已知问题,但仍有很多问题是我们还没有发现和解决的,后续我们也会继续在这方面进行研究,探索使用 PlatformView 的最佳实践!


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

IdleHandler你会用吗?记一次IdleHandler使用误区,导致ANR

1. 示例 问题抛出,当引入线上ANR抓取工具后,发现了不少IdleHandler带来的问题。堆栈具体见下图 思考为什么idleHandler会带来这样的问题呢,或许你会觉得是单个消息执行时间过长导致的。那么请看示例,项目本身代码较为复杂,简化代码如下: ...
继续阅读 »

1. 示例


问题抛出,当引入线上ANR抓取工具后,发现了不少IdleHandler带来的问题。堆栈具体见下图


image.png


思考为什么idleHandler会带来这样的问题呢,或许你会觉得是单个消息执行时间过长导致的。那么请看示例,项目本身代码较为复杂,简化代码如下



  • 工具类


    /**
* 添加任务到IdleHandler
*
* @param runnable runnable
*/
public static void run(Runnable runnable) {
IUiRunnable uiRunnable = new IUiRunnable(runnable);
Looper.getMainLooper().getQueue().addIdleHandler(uiRunnable);
}


  • 使用工具类


public class MainActivity extends AppCompatActivity {

public static final String TAG = "idleHandler";

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}

@Override
protected void onResume() {
super.onResume();
Log.e(TAG, "onResume: start");
//1 //关键代码处 延迟 3s执行的delay msg
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
Log.e(TAG, "delay: msg");
startService(new Intent(MainActivity.this, MyService.class));
}
}, 3000);
//关键代码处 添加到IdleHandler里的三个任务
UIManager.run(() -> test(1));
UIManager.run(() -> test(2));
UIManager.run(() -> test(3));
}
//延迟任务
private void test(final int i) {

try {
Log.e(TAG, "queueIdle:test start " + i);
Thread.sleep(3000);
Log.e(TAG, "queueIdle:test end " + i);

} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

让我们来猜猜下面代码的输出顺序。再回看一下代码,肯定以为是下面这样对吧?


修复后.png


如果你觉得上面的输出没问题,那就更需要继续读下去了


这里把真实log截出来:


WX20211128-185004@2x.png


what??delay3000ms为什么失效了。而是Idle消息先执行了。


为什么呢?不慌遇到这种问题我们肯定要通过IdleHandler机制的源码,来找答案了。


2. 源码分析


//MessageQueue.java
Message next() {
// Return here if the message loop has already quit and been disposed.
// This can happen if the application tries to restart a looper after quit
// which is not supported.
final long ptr = mPtr;
if (ptr == 0) {
return null;
}

int pendingIdleHandlerCount = -1; // -1 only during first iteration
int nextPollTimeoutMillis = 0;
for (;;) {
if (nextPollTimeoutMillis != 0) {
Binder.flushPendingCommands();
}

nativePollOnce(ptr, nextPollTimeoutMillis);

...
// If first time idle, then get the number of idlers to run.
// Idle handles only run if the queue is empty or if the first message
// in the queue (possibly a barrier) is due to be handled in the future.
//1 执行时机
if (pendingIdleHandlerCount < 0
&& (mMessages == null || now < mMessages.when)) {
pendingIdleHandlerCount = mIdleHandlers.size();
}
if (pendingIdleHandlerCount <= 0) {
// No idle handlers to run. Loop and wait some more.
mBlocked = true;
continue;
}

if (mPendingIdleHandlers == null) {
mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
}
//2 copy副本
mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
}

// Run the idle handlers.
// We only ever reach this code block during the first iteration.
//3 逐个执行
for (int i = 0; i < pendingIdleHandlerCount; i++) {
final IdleHandler idler = mPendingIdleHandlers[i];
mPendingIdleHandlers[i] = null; // release the reference to the handler

boolean keep = false;
try {
keep = idler.queueIdle();
} catch (Throwable t) {
Log.wtf(TAG, "IdleHandler threw exception", t);
}
//4 是否移除当前idleHandler
if (!keep) {
synchronized (this) {
mIdleHandlers.remove(idler);
}
}
}

// Reset the idle handler count to 0 so we do not run them again.
pendingIdleHandlerCount = 0;

// While calling an idle handler, a new message could have been delivered
// so go back and look again for a pending message without waiting.
nextPollTimeoutMillis = 0;
}
}

//5 外部调用添加idleHandler
public void addIdleHandler(@NonNull IdleHandler handler) {
if (handler == null) {
throw new NullPointerException("Can't add a null IdleHandler");
}
synchronized (this) {
mIdleHandlers.add(handler);
}
}

鉴于next()方法比较长且相关介绍也比较多,这里不细说。




  • 注释1处,这里是IdleHandler执行时机,即主线程无消息或者未到执行时机(空闲时间)。




  • 注释2处,可以看到mPendingIdleHandlers相当于copy了mIdleHandlers中的内容,由注释5处可以看到我们调用addIdleHandler()添加的任务都存进了mIdleHandlers。




  • 注释3处,这里循环意味着要一次性处理之前添加的全部IdleHandler任务,如果我们短时内调用了多次addIdleHandler(),意味着这些idle msg将被拿出来逐个执行;而且要全部处理掉,才可以继续执行原消息队列的消息,如果idle msg很耗时,便会出现前面的主线程的postDelay任务执行时间就非常不可靠了;同理如果期间有触摸事件发生,那极有可能会因为得不到及时处理而导致ANR发生。赶紧检查下自己的项目中是否有此类问题。




  • 注释4处,控制本次的IdleHandler是会被再次调用还是单次调用呢,是由queueIdle()方法的返回值决定,这点是我们该利用起来的。




上面的工具方法初衷是好的,提供接口暴露给各业务侧在某个需要的时刻将非紧急任务延迟加载,进而减少卡顿,但用起来却没有那么丝滑,甚至导致ANR,希望大家理解后能够避开这个坑。


3.安全用法


IdleHandler再介绍



  • 这里再赘述下IdleHandler作用,我们都知道在做启动性能优化的时候,要尽可能多地减少启动阶段主线程任务。对一些启动阶段非必须且一定要在主线程里完成的任务,我们可以在应用启动完成之后再去加载。正是考虑到这些google官方提供了IdleHandler机制来告诉我们线程空闲时机。


利用IdleHandler设计的工具类


直接使用IdleHandler,肯定不太符合博主这种“大项目”的编码风格,简单封装是必要,方便我们做些功能定制,下面给大家说下项目中的工具类。



  • 正确的方法 将启动过程中非紧急的主线程任务全部放进uiTasks里,然后逐个执行,切记单个消息耗时不要太长。


private static List<Runnable> uiTasks = new ArrayList<>();

public static UIPoolManager addTask(Runnable runnable) {
tasks.add(runnable);
return this;
}

public static UIManager runUiTasks() {
NullHelper.requireNonNull(uiTasks);
IUiTask iUiTask = new IUiTask() {
@Override
public boolean queueIdle() {
if (!uiTasks.isEmpty()) {
Runnable task = uiTasks.get(0);
task.run();
uiTasks.remove(task);
}
//逐次取一个任务执行 避免占用主线程过久
return !uiTasks.isEmpty();
}
};

Looper.myQueue().addIdleHandler(iUiTask);
return this;
}

替换成上面的方法再试一遍。


@Override
protected void onResume() {
super.onResume();
Log.e(TAG, "onResume: start");
//1
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
Log.e(TAG, "delay: msg");
startService(new Intent(MainActivity.this, MyService.class));
}
}, 3000);
UIManager.addTask(() -> test(1));
UIManager.addTask(() -> test(2));
UIManager.addTask(() -> test(3));
UIManager.runUiTasks();
}

得到最初希望的时序结果


修复后.png


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

iOS整体框架介绍

iOS
这是我参与11月更文挑战的第18天,活动详情查看:2021最后一次更文挑战iOS整体框架通常我们称iOS的框架为cocoa框架. 话不多说,官方的整体框架图如下:简单解释一下:Cocoa (Application) Layer(触摸层)Media Layer ...
继续阅读 »

这是我参与11月更文挑战的第18天,活动详情查看:2021最后一次更文挑战

iOS整体框架

通常我们称iOS的框架为cocoa框架. 话不多说,官方的整体框架图如下:

image.png

简单解释一下:

  • Cocoa (Application) Layer(触摸层)
  • Media Layer (媒体层)
  • Core Services Layer(核心服务层)
  • Core OS Layer (核心系统操作层)
  • The Kernel and Device Drivers layer(内核和驱动层)

注:Cocoa (Application) Layer(触摸层)其实包含cocoa Touch layer(触摸层) 和Application Layer (应用层).应用层原本在触摸层上面,因为应用层是开发者自己实现,所以和触摸层合在一起.

其实每一层都包含多个子框架, 如下图:

image.png

简单解释下(瞄一眼就得了):

  • Cocoa Touch Layer:触摸层提供应用基础的关键技术支持和应用的外观。如NotificationCenter的本地通知和远程推送服务,iAd广告框架,GameKit游戏工具框架,消息UI框架,图片UI框架,地图框架,连接手表框架,UIKit框架、自动适配等等

  • Media Layer:媒体层提供应用中视听方面的技术,如图形图像相关的CoreGraphics,CoreImage,GLKit,OpenGL ES,CoreText,ImageIO等等。声音技术相关的CoreAudio,OpenAL,AVFoundation,视频相关的CoreMedia,Media Player框架,音视频传输的AirPlay框架等等

  • Core Services Layer:系统服务层提供给应用所需要的基础的系统服务。如Accounts账户框架,广告框架,数据存储框架,网络连接框架,地理位置框架,运动框架等等。这些服务中的最核心的是CoreFoundationFoundation框架,定义了所有应用使用的数据类型。CoreFoundation是基于C的一组接口,Foundation是对CoreFoundation的OC封装

  • Core OS Layer:系统核心层包含大多数低级别接近硬件的功能,它所包含的框架常常被其它框架所使用。Accelerate框架包含数字信号,线性代数,图像处理的接口。针对所有的iOS设备硬件之间的差异做优化,保证写一次代码在所有iOS设备上高效运行。CoreBluetooth框架利用蓝牙和外设交互,包括扫描连接蓝牙设备,保存连接状态,断开连接,获取外设的数据或者给外设传输数据等等。Security框架提供管理证书,公钥和私钥信任策略,keychain,hash认证数字签名等等与安全相关的解决方案。

想看更详细的可以移步:iOS总体框架介绍和详尽说明

我们只需要知道其中重要的框架就是UIKit和Function框架.下面说说这两个框架.

Function框架

Foundation框架为所有应用程序提供基本的系统服务。应用程序以及 UIKit和其他框架,都是建立在 Foundation 框架的基础结构之上。 Foundation框架提供许多基本的对象类和数据类型,使其成为应用程序开发的基础。

话不多说,我们先来看看Foundation框架,三个图,包括了Foundation所以的类,图中灰色的是iOS不支持的,灰色部分是OS X系统的。

image.png

image.png

image.png

这里只需要知道绝大部分Function框架的类都继承NSObject, 小部分继承NSProxy

对于Foundation框架中的一些基本类的使用方法详情参见:iOS开发系列—Objective-C之Foundation框架

UIKit框架

UIKit框架提供一系列的Class(类)来建立和管理iOS应用程序的用户界面( UI )接口、应用程序对象、事件控制、绘图模型、窗口、视图和用于控制触摸屏等的接口。

UIKit框架的类继承体系图如下图所示:

image.png

在图中可以看出,responder 类是图中最大分支的根类,UIResponder为处理响应事件和响应链定义了界面和默认行为。当用户用手指滚动列表或者在虚拟键盘上输入时,UIKit就生成事件传送给UIResponder响应链,直到链中有对象处理这个事件。相应的核心对象,比如:UIApplication ,UIWindowUIView都直接或间接的从UIResponder继承 。

这里需要知道一点:UIKit框架所有的类都继承NSObject

UIKit框架的各个类的简单介绍戳后面的链接:UIKit框架各个类的简要说明 

收起阅读 »

阿里二面:什么是mmap?

iOS
平时在面试中你肯定会经常碰见的问题就是:RocketMQ为什么快?Kafka为什么快?什么是mmap?这一类的问题都逃不过的一个点就是零拷贝,虽然还有一些其他的原因,但是今天我们的话题主要就是零拷贝。传统IO在开始谈零拷贝之前,首先要对传统的IO方式有一个概念...
继续阅读 »

平时在面试中你肯定会经常碰见的问题就是:RocketMQ为什么快?Kafka为什么快?什么是mmap?

这一类的问题都逃不过的一个点就是零拷贝,虽然还有一些其他的原因,但是今天我们的话题主要就是零拷贝。

传统IO

在开始谈零拷贝之前,首先要对传统的IO方式有一个概念。

基于传统的IO方式,底层实际上通过调用read()write()来实现。

通过read()把数据从硬盘读取到内核缓冲区,再复制到用户缓冲区;然后再通过write()写入到socket缓冲区,最后写入网卡设备。

整个过程发生了4次用户态和内核态的上下文切换4次拷贝,具体流程如下:

作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS开发公众号:编程大鑫,不管你是小白还是大牛都欢迎入驻 ,让我们一起进步,共同发展!

  1. 用户进程通过read()方法向操作系统发起调用,此时上下文从用户态转向内核态
  2. DMA控制器把数据从硬盘中拷贝到读缓冲区
  3. CPU把读缓冲区数据拷贝到应用缓冲区,上下文从内核态转为用户态,read()返回
  4. 用户进程通过write()方法发起调用,上下文从用户态转为内核态
  5. CPU将应用缓冲区中数据拷贝到socket缓冲区
  6. DMA控制器把数据从socket缓冲区拷贝到网卡,上下文从内核态切换回用户态,write()返回

那么,这里指的用户态内核态指的是什么?上下文切换又是什么?

简单来说,用户空间指的就是用户进程的运行空间,内核空间就是内核的运行空间。

如果进程运行在内核空间就是内核态,运行在用户空间就是用户态。

为了安全起见,他们之间是互相隔离的,而在用户态和内核态之间的上下文切换也是比较耗时的。

从上面我们可以看到,一次简单的IO过程产生了4次上下文切换,这个无疑在高并发场景下会对性能产生较大的影响。

那么什么又是DMA拷贝呢?

因为对于一个IO操作而言,都是通过CPU发出对应的指令来完成,但是相比CPU来说,IO的速度太慢了,CPU有大量的时间处于等待IO的状态。

因此就产生了DMA(Direct Memory Access)直接内存访问技术,本质上来说他就是一块主板上独立的芯片,通过它来进行内存和IO设备的数据传输,从而减少CPU的等待时间。

但是无论谁来拷贝,频繁的拷贝耗时也是对性能的影响。

零拷贝

零拷贝技术是指计算机执行操作时,CPU不需要先将数据从某处内存复制到另一个特定区域,这种技术通常用于通过网络传输文件时节省CPU周期和内存带宽。

那么对于零拷贝而言,并非真的是完全没有数据拷贝的过程,只不过是减少用户态和内核态的切换次数以及CPU拷贝的次数。

这里,仅仅有针对性的来谈谈几种常见的零拷贝技术。

mmap+write

mmap+write简单来说就是使用mmap替换了read+write中的read操作,减少了一次CPU的拷贝。

mmap主要实现方式是将读缓冲区的地址和用户缓冲区的地址进行映射,内核缓冲区和应用缓冲区共享,从而减少了从读缓冲区到用户缓冲区的一次CPU拷贝。

整个过程发生了4次用户态和内核态的上下文切换3次拷贝,具体流程如下:

  1. 用户进程通过mmap()方法向操作系统发起调用,上下文从用户态转向内核态
  2. DMA控制器把数据从硬盘中拷贝到读缓冲区
  3. 上下文从内核态转为用户态,mmap调用返回
  4. 用户进程通过write()方法发起调用,上下文从用户态转为内核态
  5. CPU将读缓冲区中数据拷贝到socket缓冲区
  6. DMA控制器把数据从socket缓冲区拷贝到网卡,上下文从内核态切换回用户态,write()返回

mmap的方式节省了一次CPU拷贝,同时由于用户进程中的内存是虚拟的,只是映射到内核的读缓冲区,所以可以节省一半的内存空间,比较适合大文件的传输。

sendfile

相比mmap来说,sendfile同样减少了一次CPU拷贝,而且还减少了2次上下文切换。

sendfile是Linux2.1内核版本后引入的一个系统调用函数,通过使用sendfile数据可以直接在内核空间进行传输,因此避免了用户空间和内核空间的拷贝,同时由于使用sendfile替代了read+write从而节省了一次系统调用,也就是2次上下文切换。

整个过程发生了2次用户态和内核态的上下文切换3次拷贝,具体流程如下:

  1. 用户进程通过sendfile()方法向操作系统发起调用,上下文从用户态转向内核态
  2. DMA控制器把数据从硬盘中拷贝到读缓冲区
  3. CPU将读缓冲区中数据拷贝到socket缓冲区
  4. DMA控制器把数据从socket缓冲区拷贝到网卡,上下文从内核态切换回用户态,sendfile调用返回

sendfile方法IO数据对用户空间完全不可见,所以只能适用于完全不需要用户空间处理的情况,比如静态文件服务器。

sendfile+DMA Scatter/Gather

Linux2.4内核版本之后对sendfile做了进一步优化,通过引入新的硬件支持,这个方式叫做DMA Scatter/Gather 分散/收集功能。

它将读缓冲区中的数据描述信息--内存地址和偏移量记录到socket缓冲区,由 DMA 根据这些将数据从读缓冲区拷贝到网卡,相比之前版本减少了一次CPU拷贝的过程

整个过程发生了2次用户态和内核态的上下文切换2次拷贝,其中更重要的是完全没有CPU拷贝,具体流程如下:

  1. 用户进程通过sendfile()方法向操作系统发起调用,上下文从用户态转向内核态
  2. DMA控制器利用scatter把数据从硬盘中拷贝到读缓冲区离散存储
  3. CPU把读缓冲区中的文件描述符和数据长度发送到socket缓冲区
  4. DMA控制器根据文件描述符和数据长度,使用scatter/gather把数据从内核缓冲区拷贝到网卡
  5. sendfile()调用返回,上下文从内核态切换回用户态

DMA gathersendfile一样数据对用户空间不可见,而且需要硬件支持,同时输入文件描述符只能是文件,但是过程中完全没有CPU拷贝过程,极大提升了性能。

应用场景

对于文章开头说的两个场景:RocketMQ和Kafka都使用到了零拷贝的技术。

对于MQ而言,无非就是生产者发送数据到MQ然后持久化到磁盘,之后消费者从MQ读取数据。

对于RocketMQ来说这两个步骤使用的是mmap+write,而Kafka则是使用mmap+write持久化数据,发送数据使用sendfile

总结

由于CPU和IO速度的差异问题,产生了DMA技术,通过DMA搬运来减少CPU的等待时间。

传统的IOread+write方式会产生2次DMA拷贝+2次CPU拷贝,同时有4次上下文切换。

而通过mmap+write方式则产生2次DMA拷贝+1次CPU拷贝,4次上下文切换,通过内存映射减少了一次CPU拷贝,可以减少内存使用,适合大文件的传输。

sendfile方式是新增的一个系统调用函数,产生2次DMA拷贝+1次CPU拷贝,但是只有2次上下文切换。因为只有一次调用,减少了上下文的切换,但是用户空间对IO数据不可见,适用于静态文件服务器。

sendfile+DMA gather方式产生2次DMA拷贝,没有CPU拷贝,而且也只有2次上下文切换。虽然极大地提升了性能,但是需要依赖新的硬件设备支持。

收起阅读 »

Swift接入例子-适合多人协作

iOS
在「 Swift接入例子 」中介绍了Swift项目如何接入SOT。但是要求SDK解压到特定目录中,编译配置的路径也是绝对路径,不适合多人协作合开。文本介绍适合多人开发协作的接入方法。 还是以开源的「 SwiftMessages 」Demo为例,该工程全部用Sw...
继续阅读 »

「 Swift接入例子 」中介绍了Swift项目如何接入SOT。但是要求SDK解压到特定目录中,编译配置的路径也是绝对路径,不适合多人协作合开。文本介绍适合多人开发协作的接入方法。


还是以开源的「 SwiftMessages 」Demo为例,该工程全部用Swift语言开发。这里把修改好的版本上传到了git上,分支为 「 sotcollaboration 」SotDebug接入免费版,SotRelease接入网站版,读者只需要进行下面的 Step1.配置编译环境 就可以直接用该分支测试。


现在开始从头讲解,git clone原本的工程后(我的路径为/Applications/SwiftMessages),命令行cd /Applications/SwiftMessages进入根目录,使用版本切换命令:git checkout 1e49de7b3780b699(因本文档制作于21年10月21号,以当日版本为准)。首先进入Demo目录,打开Demo.xcodeproj工程,scheme默认就已经选中了Demo:...


我使用的是Xcode12.4,可以直接编译成功,启动APP能看到画面(模拟器):


......


点击最上面的MESSAGE VIEW控件,会弹出一个错误提示窗口,今天我们就来用SOT热更的方式修改错误提示的文案:...


Step1: 配置编译环境


「 下载SOT的SDK 」,解压到项目目录下 /Applications/SwiftMessages/Demo/sotsdk...


在terminal运行命令:sh /Applications/SwiftMessages/Demo/sotsdk/compile-script/install.sh安装SOT编译工具链,需要输入密码。


用文本编辑器打开 /Applications/SwiftMessages/Demo/sotsdk/project-script/sotconfig.sh,修改EnableSot=1:...新版SDK已经不会再使用sotconfig.sh里的sdkdir,sotbuilder和objbuilder路径了,所以不用修改这些配置了,删掉也可以。


Step2: 增加Configuration


增加两个Configuration,只有切换到这两个Configuration才使用SOT编译模式,平时还是用原来的Configuration做开发,步骤如下:



  1. 选中Demo Project,然后选择Info面板,点击Configurations的下面加号,复制Debug的编译配置,并且命名为SotDebug,用来接入免费版的SOT。再选择复制Release编译配置,命名为SotRelease,用来配置网站版的SOT,注意名字都不要留有空格:...加完就是:...

  2. SwiftMessages也加上这两个Configuration:...


注意:读者应用到自己项目中时,需要把所有的工程都加上这两个Configuration,否则编译会报找不到文件等等的错误。所以加完这两个Configuration的之后,就马上切换到它们去Build和Run一下,看是否有编译错误,如果没有再进行下面的操作,如果有,请检查是否漏了一些工程没有添加上。


Step3: 修改编译选项


添加热更需要的编译选项,添加SOT虚拟机静态库等,步骤如下:




  1. 选中Demo工程,然后选择Demo这个Target,再选择Build Settings:...




  2. Other Linker FlagsSotDebug中添加-sotmodule $(PRODUCT_NAME) sotsdk/libs/libsot_free.a -sotsaved $(SRCROOT)/sotsaved/$(CONFIGURATION)/$(CURRENT_ARCH) -sotconfig $(SRCROOT)/sotsdk/project-script/sotconfig.sh


    SotRelease中添加-sotmodule $(PRODUCT_NAME) sotsdk/libs/libsot_web.a -sotsaved $(SRCROOT)/sotsaved/$(CONFIGURATION)/$(CURRENT_ARCH) -sotconfig $(SRCROOT)/sotsdk/project-script/sotconfig.sh


    每个选项的意义如下:



    • -sotmodule是module的名字,可以直接用$(PRODUCT_NAME),也可以自定义名字,名字不要有空格

    • -sotsaved是编译中间产物保存的目录,补丁自动化生成需要对比前后编译的产物来生成补丁

    • -sotconfig指定了项目sotconfig.sh的路径,该脚本控制sot编译器的工作

    • sotsdk/libs/libsot_free.a是SOT虚拟机静态库的路径,链接的是免费版的虚拟机

    • sotsdk/libs/libsot_web.a是SOT虚拟机静态库的路径,链接的是网站版的虚拟机




  3. Other C Flags以及Other Swift Flags的SotDebug和SotRelease下添加-sotmodule $(PRODUCT_NAME) -sotconfig $(SRCROOT)/sotsdk/project-script/sotconfig.sh,意义跟上一步是一样的,需要保持一致。经过上面两步,相关的编译配置结果如下图:...




  4. Preprocessor Macros添加USE_SOT=1,后面用来控制是否编译调用SDK的代码...




  5. 因为SOT SDK库文件编译时不带Bitcode,所以也需要把Enable Bitcode设为No...




  6. 为了模拟器架构时不编译arm64,给SotRelease增加如下配置...或者把Build Active Architecture Only设为Yes




Step4: 增加拷贝补丁脚本


SDK里提供了一个便利脚本,路径在sdk目录的project-script/sot_package.sh,它会把生成的补丁拷贝到Bundle文件夹下,在每次项目编译成功时调用该脚本,添加步骤如下:


...


脚本内容为:



if [[ "$CONFIGURATION" == "SotDebug" || "$CONFIGURATION" == "SotRelease" ]];then
sh "$SOURCE_ROOT/sotsdk/project-script/sot_package.sh" "$SOURCE_ROOT/sotsdk/project-script/sotconfig.sh" "$SOURCE_ROOT/sotsaved/$CONFIGURATION" Demo
fi

复制代码

...


Based on dependency analysis的勾去掉。




Step5: 链接C++库


SOT需要压缩库和c++标准库的支持,还是在这个页面下,打开Link Binary With Libraries页...


点击加号,分别加入这两,libz.tbdlibc++.tbd...




Step6: 调用SDK API


需要用Swift代码调用OC代码,已经提供了一个样例代码在SDK的swift-call-objc目录中,先把callsot.h和callsot.m拷贝到Demo目录下...


再添加到Demo工程中。点击Xcode软件的File按钮,找到Demo目录下的callsot.h和callsot.m,接着点击Add Files to "Demo",如下图所示:...


点击Add按钮,添加后会弹出询问:是否创建桥接文件。点击按钮Create Bridging Header...


然后可以看到项目中多了3个文件,分别是callsot.h,callsot.m和Demo-Bridging-Header.h:...


打开Demo-Bridging-Header.h,加入一行代码#import "callsot.h"...


打开callsot.m,修改代码为


#import <Foundation/Foundation.h>
#import "callsot.h"
#import "../sotsdk/libs/SotWebService.h"
@implementation CallSot:NSObject
-(void) InitSot
{
#ifdef USE_SOT
#ifdef DEBUG
[SotWebService ApplyBundleShip];
#else

[SotWebService Sync:@"1234567" is_dev:false cb:^(SotDownloadScriptStatus status)
{
if(status == SotScriptStatusSuccess)
{
NSLog(@"SotScriptStatusSuccess");
}
else
{
NSLog(@"SotScriptStatusFailure");
}
}];

#endif
#endif
}
@end
复制代码

注意SotWebService.h的头文件路径不再依赖于绝对路径,并且代码里用了#ifdef USE_SOT宏来隔开API调用代码,不影响正常编译:...


打开AppDelegate.swift,加入两行代码let sot = CallSot()sot.initSot()...


注意:读者在应用到自己项目中时,以上这些配置的路径不要生搬硬套。例如找不到SotWebService.h文件,找不到sotconfig.sh文件等等,读者自己要清楚SDK的目录与自己工程目录的相对关系,灵活调整这些配置的路径。


测试热更-免费版


按上面配置完之后,先测试免费版热更功能


Step1: 热更注入



  1. Build Configuration切换到SotDebug...

  2. 确保sotconfig.sh的配置是,EnableSot=1以及GenerateSotShip=0,先Clean Build Folder一下,然后再Build:...


然后看编译日志的输出,Link日志可以看到run sot link等输出,会告诉你每个文件里哪些函数可以被热更等信息:......


项目编译成功了,该APP可以正常启动。同时它具备了热更能力,可以加载补丁改变程序的代码逻辑,下面介绍如何生成补丁来测试它。




Step2: 生成补丁


上一步进行了热更注入的编译,当时的代码保存到了Demo/sotsaved这个文件夹下,用来和新代码比较生成补丁。生成补丁步骤如下:



  1. 首先启动SOT生成补丁模式,修改sotconfig.shEnableSot=1GenerateSotShip=1

  2. ...

  3. 接下来直接在Xcode里修改源代码,把ViewController.swift文件的”Something is horribly wrong!“改成了”SOT is great“,修改前:...修改后:...

  4. Swift项目生成补丁,每次都需要先Clean项目,再Build项目。然后查看编译日志输出,可以看到生成了补丁并且被脚本拷贝到了Bundle目录下,可以展开Link Demo(x86_64)的编译日志:...点击展开后,可看到生成补丁的Link日志,日志里显示了函数demoBasics被修改了:...

  5. 生成出来的补丁原始文件保存到了Demo/sotsaved/SotDebug/x86_64/ship/ship.sot,还记得之前加了一个script到Build Phase中吗?它会每次编译结束时,会把这个补丁拷贝到了Bundle目录中,并且添加CPU架构到文件名中。可以在Bundle中看到这个补丁,至此完毕。




Step3: 加载补丁


启动APP,API会判断Bundle内是否有补丁,有则加载,加载成功的日志大概如下,提示有一个模块加载了热更补丁:...之后点击最上面的MESSAGE VIEW控件,发现弹出的文案变成了SOT is great:...


如果去Xcode断点调试demoBasics,会发现无法断住了,因为实际执行补丁代码的是SOT虚拟机。


顺便提一嘴,GenerateSotShip=1时,编译APP用的是保存在sotsaved目录下的代码,所以无论怎么修改Xcode里的代码,如果没有把补丁拷贝到Bundle目录里,那么APP都是最后一次GenerateSotShip=0热更注入时的样子。


如果怀疑,可以把拷贝补丁的Script脚本从Build Phases删除,可以发现GenerateSotShip=1怎么改代码都不会生效了。


注意:如果读者接入自己的一个很简单项目进行测试,例如设置某个控件的颜色,热更前是红色,修改后是绿色,发现无法生效。那是因为这样的项目太过于简单,寥寥几行代码。热更前没有访问过绿色的这个全局变量,在热更时也无法访问到了,SOT只能利用原有的能力,无法无中生有。所以不要这样测试,更具体的原因在「 热更能力-语言特性 」说明。通常完整的项目代码比较多,所以就不会有这样的缺陷。


接入网站版


按上面的教程,已经对APP实现了免费版和网站版的接入。它俩区别只是链接的库不一样,具体就是Other Linker Flags根据Configuration区别配置,SotRelease下接入了网站版。但除了APP接入了网站版SDK,还需要用配合网站来管理补丁的发布。


Step1: 注册网站



  1. 第一步当然是注册网站,成为会员。点击跳转注册页面,免费注册,注册需要验证邮箱,然后登录。

  2. 从导航栏进入我的APP:...

  3. 点击创建APP,弹出弹窗填写APP的名字:...

  4. 进入APP页面,点击右上角的创建新版本按钮,会弹出弹窗,需要选择网站版,SDK版本选择1.0,目前只有1.0版本,然后输入版本号,版本号可以是随意字符串,方便区分就行。...

  5. 创建版本成功后,点击版本,进入版本页面,左上角是唯一标识该版本的VersionKey,后面API接口需要这个Key。...


Step2: 修改VersionKey


打开callsot.m,修改网站版的Sync接口,第一个参数填入你在网站创建的版本的VersionKey。...至此,网站版热更就算接入完成了。


Step3: 测试网站热更




  • 网站版生成补丁的步骤免费版是一样的,需要经历热更注入->出包->修改代码->生成补丁,这里不再赘述。


    唯一不同的是,生成出来的补丁要上传到网站上,然后才能通过网络同步到手机上实现热更。通过之前的免费版教程,知道生成的补丁会被拷贝到Bundle目录下,所以去Bundle目录里就能找它,在Xcode导航栏里右键选择Products下的Demo.app,选择Show in Finder:...




  • 右键Demo文件,选择Show Package Contents:...




  • 找到目录下的sotship_arm64.sot,这里用手机测试,cpu是arm64类型,补丁名字带有cpu后缀,这就是补丁了:...




  • 回到网站的版本页面,点击右侧上传补丁按钮:...




  • 弹出页面里,真机的架构一般选择arm64,除非是老的armv7的机器,并把补丁文件拖到框里,点击上传:...




  • 上传成功并且补丁文件无异常(补丁最大支持5MB),则会添加成功,补丁默认是停用状态,需要点击编辑来启用它:...




  • 这里选择全量启用,点击下面的提交按钮,然后补丁就会成功启用了:...




  • 上一步更新了补丁状态,通常很快生效,但CDN有时也需要1到2分钟才能生效。之后手机打开APP,如果成功下载补丁和加载的话,就能看到下面的日志:...这里输出的md5也跟网站上的补丁md5是一致的。




  • 打开APP后,点击最上面的MESSAGE VIEW控件,发现弹出的文案变成了SOT is great:...




注意:使用网站版,需要考虑到网络传输延迟的问题,只有看到了下载补丁和成功加载补丁的日志之后,调用的函数才会使用热修后的函数。例如有的开发问我,首屏代码怎么无法热更生效?那是因为首屏代码调用的时机太早了,SOT去网站上拿补丁,是异步的,不会一直卡住等着,而且在异步线程中等待结果。在补丁没传输回来之前,首屏的代码都已经调用结束了,这种情况下当然调用的还是老的代码了。而免费版没有这个问题,因为免费版是同步加载补丁的,直接去Bundle里加载,不是异步的。


构建热更注入版本和构建补丁必须是同一台机器,同一个Xcode版本。例如上架前APP用Xcode12进行了热更注入,而之后用Xcode13来构建补丁,那么将得到无效甚至错误的补丁。请使用同一个版本Xcode。


Step4: 几点提示



  • 网站版跟免费版主要接入流程差不多,可以用免费版测试,功能通过测试之后再接入网站版。

  • 网站版需要有网络的情况下才能生效,如果手机没有网,即使之前已经下载过了补丁,也无法加载。

  • 网站版费用很低,日活10万的APP,一个月几百块就够了。

  • 网站版补丁和配置都放在CDN上,支持高并发。




非主Target接入热更


上面的教程都是针对主Target,也就是Demo。这个工程还有一个名为SwiftMessages的Framework,也可以热更,下面介绍如何配置。


可以看到SwiftMessages的Mach-O Type是Dynamic Library,通过下图方式查看得到:...


这种类型的话,配置相对麻烦些。还有一种是Static Library,配置起来会简单得多。但本例改成Static Library启动会崩溃,所以按Dynamic Library的方式来介绍。


Step1: 修改编译选项



  1. 选中SwiftMessages.xcodeproject工程,然后选择SwiftMessages这个Target,再选择Build Settings:...

  2. Other Linker FlagsSotDebug中添加-sotmodule $(PRODUCT_NAME) $(SRCROOT)/Demo/sotsdk/libs/libsot_free.a -sotsaved $(SRCROOT)/Demo/sotsaved/$(CONFIGURATION)/$(CURRENT_ARCH) -sotconfig $(SRCROOT)/Demo/sotsdk/project-script/sotconfig.sh

  3. Other Linker FlagsSotRelease中添加-sotmodule $(PRODUCT_NAME) $(SRCROOT)/Demo/sotsdk/libs/libsot_web.a -sotsaved $(SRCROOT)/Demo/sotsaved/$(CONFIGURATION)/$(CURRENT_ARCH) -sotconfig $(SRCROOT)/Demo/sotsdk/project-script/sotconfig.sh

  4. Other C Flags以及Other Swift FlagsSotDebugSotRelease中,添加-sotmodule $(PRODUCT_NAME) -sotconfig $(SRCROOT)/Demo/sotsdk/project-script/sotconfig.sh,意义跟上一步是一样的,需要保持一致。经过上面两步,相关的编译配置结果如下图:...这一步跟Demo的配置差不多,区别在于有些路径写法不一样,以达到复用Demo配置的目的,读者可以仔细比较一下。

  5. Preprocessor Macros添加USE_SOT=1,后面用来控制是否编译调用SDK的代码...

  6. 需要把Target的Enable Bitcode设为No...

  7. 为了模拟器架构时不编译arm64,给SotRelease增加如下配置...


Step2: 链接C++库


点击Build Phases页面,打开Link Binary With Libraries页,点击加号,分别加入这两,libz.tbdlibc++.tbd...


Step3: 调用SDK API


因为SwiftMessages是动态库,所以需要在它的编译文件中调用SDK的热更初始化接口。跟Demo一样,添加OC文件。先从Demo文件夹中复制callsot.h和callsot.m文件到SwiftMessages文件夹中...


选中SwiftMessages工程,点击Xcode软件的File按钮,接着点击Add Files to "SwiftMessages.xcodeproject",如下图所示:...


选择到SwiftMessages目录,同时选中callsot.h和callsot.m两个文件,勾选下面的Copy items if needed,勾选Add to targets:中的SwiftMessages target,如下图所示:...


点击Add按钮,然后可以看到项目中多了2个文件,分别是callsot.h,callsot.m,修改CallSot类名为CallSotMessage:...


去到右边面板,把文件属性改成public:...


打开callsot.m,做相应路径和类名的修改:...


打开Demo-Bridging-Header.h,加入一行代码#import "SwiftMessages/callsot.h"...


打开AppDelegate.swift,加入两行代码let sot1 = CallSotMessage()sot1.initSot()...


因为这时候有两个Target都可以生成补丁,Demo和SwiftMessages,需要修改拷贝补丁的脚本,加入SwiftMessages:...


Step4: 测试热更




  1. 测试热更的流程跟之前是一模一样的,只是输出的日志可能会有所区别,我们过一遍。EnableSot=1和GenerateSotShip=0热更注入,先Clean后Build,如果去看编译日志的Link SwiftMessages,也可以看到热更注入的信息。




  2. 然后修改MessageView.swift的代码,错误提示的文案会加上“SOT is great”:...




  3. GenerateSotShip=1开启生成补丁模式,Clean后Build,查看Link SwiftMessages日志,有提示该函数被热更:...




  4. 接下来可以看到补丁拷贝脚本日志输出的信息,这里它检测到有两个Target都生成了补丁文件,会把它们两个合成一个,拷贝到Bundle目录下:...




  5. 启动APP,会看到两条加载补丁的日志,因为我们Demo Target和SwiftMessages Target都调用了API接口:...




  6. 点击MESSAGE VIEW控件,可以看到错误提示文案后面多了“SOT is great”,热更成功:


    ...网站版的测试跟以前也是一样的,这里不再重复了。




Step5: 几点提示



  1. Dynamic Library的热更编译改法其实跟主Target,也就是Mach-O Type为Executable的改法是一样,只是这里复用了主Target的一些配置,例如sotsaved目录和sotconfig.sh的路径。增加再多的Target也可以按同样的改法修改它们。

  2. 补丁拷贝脚本只需要主Target有就行了,把要热更的sotmodule对应的名字加上即可,条件就是sotsaved目录必须是同一个。

  3. 如果需要接入网站版,那么每个需要热更的Target都需要调用API跟网站同步,它们的消耗是独立计费的。


Static Library的改法


上面说到Dynamic Library的改法步骤比较多,而且有诸多缺点,如果能把Framework的Mach-O Type改成Static Library是最好的,会少很多步骤和配置。由于本例无法修改,这里简单说一下步骤:



  1. Other Libraian Flags添加-sotmodule $(PRODUCT_NAME) -sotsaved $(SRCROOT)/Demo/sotsaved/$(CONFIGURATION)/$(CURRENT_ARCH) -sotconfig $(SRCROOT)/Demo/sotsdk/project-script/sotconfig.sh ,注意是Other Libraian Flags而不是Other Linker Flags了。还有这里比Dynamic Library加的配置少一个,即没有链接SDK的.a库文件了。

  2. Other C Flags以及Other Swift Flags添加-sotmodule $(PRODUCT_NAME) -sotconfig $(SRCROOT)/Demo/sotsdk/project-script/sotconfig.sh,这步跟之前是一模一样的。

  3. 需要把Target的Enable Bitcode设为No

  4. 修改拷贝补丁的脚本,加入该Target的名字,例如本例加入SwiftMessages,跟之前也是一模一样的:...


然后就配置完了,如果是使用网站版,同步一次消耗,就能实现所有Target的热更,修改简单,对包体影响最小。




总结


本文完整介绍Swift项目如何接入免费版和网站版。


本文的方式是把SDK拷贝到了工程文件夹里,让它可以跟随项目一起进行版本管理,路径也配置成了相对路径,更加灵活。


通过新增Configuration的方式,也做到了不影响原来的开发,Debug和Release相当于没有接入SOT,适合大多数开发平时使用。只需上线前改成SotRelease出包,就能让APP就得到热更能力。


作者:忒修斯科技
链接:https://juejin.cn/post/7033403091550470180
收起阅读 »

Java内存区域异常

一、内存区域划分Java程序执行时在逻辑上按照功能划分为不同的区域,其中包括方法区、堆区、Java虚拟机栈、本地方法栈、程序计数器、执行引擎、本地库接口、本地方法库几个部分,各个区域宏观结构如下,各部分详细功能及作用暂不展开论述。 JVM内存自动管理是Java...
继续阅读 »



一、内存区域划分

Java程序执行时在逻辑上按照功能划分为不同的区域,其中包括方法区、堆区、Java虚拟机栈、本地方法栈、程序计数器、执行引擎、本地库接口、本地方法库几个部分,各个区域宏观结构如下,各部分详细功能及作用暂不展开论述。

JVM内存自动管理是Java语言的一大优良特性,也是Java语言在软件市场长青的一大优势,有了JVM的自动内存管理,程序员不再为令人抓狂的内存泄漏担忧,但深入理解Java虚拟机内存区域依然至关重要,JVM内存自动管理并非永远万无一失,不当的使用也会造成OOM等内存异常,本文尝试列举Java内存溢出相关的常见异常并提供使用建议。

二、栈溢出

  • 方法调用深度过大

我们知道Java虚拟机栈及本地方法栈中存放的是方法调用所需的栈帧,栈帧是一种包括局部变量表、操作数栈、动态链接和方法返回地址的数据结构,每一次方法的调用和返回对应着压栈及出栈操作。Java虚拟机栈及本地方法栈由每个Java执行线程独享,当方法嵌套过深直至超过栈的最大空间时,当再次执行压栈操作,将抛出StackOverflowError异常。为演示栈溢出,假设存在如下代码:

/**
* 最大栈深度
*/
public class MaxInvokeDeep {
  private static int count = 0;

  /**
  * 无终止条件的递归调用,每次调用将进行压栈操作,超过栈最大空间后,将抛出stackOverFlow异常
  */
  public static void increment() {
      count++;
      increment();
  }

  public static void main(String[] args) {
      try {
          increment();
      } catch (Throwable e) {
          System.out.println("invokeDeep:" + count);
          e.printStackTrace();
      }
  }
}

对于示例中的increment()方法,每次方法调用将深度+1,通过不断的栈帧压栈操作,当栈中空间无法再进行扩展时,程序将抛出StackOverflowError异常。Java虚拟机栈的空间大小可以通过JVM参数调整,我们先设置栈空间大小为512K(-Xss512k),程序执行结果如下:(方法调用5355次后触发栈溢出) 我们再将栈空间缩小至256k(-Xss256k),程序执行结果如下:(方法调用2079次时将触发栈溢出): 由此可见,Java虚拟机栈的空间是有限的,当我们进行程序设计时,应尽量避免使用递归调用。生产环境抛出StackOverflowError异常时,可以排查系统中是否存在调用深度过大的方法。方法的不断嵌套调用,不但会占用更多的内存空间,也会影响程序的执行效率。当我们进行程序设计时需要充分考虑并设置合理的栈空间大小,一般情况下虚拟机默认配置即满足大部分应用场景。

  • 局部变量表过大

局部变量表用于存放Java方法中的局部变量,我们需要合理的设置局部变量,避免过多的冗余变量产生,否则可能会导致栈溢出,沿用刚刚的实例代码,我们定义一个创建大量局部变量的重载方法,则在栈空间不变的情况下(-Xss512k),创建大量局部变量的方法将降低栈调用深度,更容易触发StackOverflowError异常,通过分别执行increment及其重载方法,其中代码及执行结果如下:

package org.learn.jvm.basic;

/**
* 最大栈深度
*/
public class MaxInvokeDeep {
  private static int count = 0;

  /**
    * 无终止条件的递归调用,每次调用将进行压栈操作,超过栈最大空间后,将抛出stackOverFlow异常
    */
  public static void increment() {
      count++;
      increment();
  }

  /**
    * 创建大量局部变量,导致局部变量表过大,影响栈调用深度
    *
    * @param a
    * @param b
    * @param c
    */
  public static void increment(long a, long b, long c) {
      long d = 1, e = 2, f = 3, g = 4, h = 5, i = 6,
              j = 7, k = 8, l = 9, m = 10;
      count++;
      increment(a, b, c);
  }

  public static void main(String[] args) {
      try {
          //increment();
          increment(1, 2, 3);
      } catch (Throwable e) {
          System.out.println("invokeDeep:" + count);
          e.printStackTrace();
      }
  }
}

执行increment()时,最大栈深度为5355(栈空间大小-Xss512k)

执行increment(1,2,3)时,最大栈深度为1487(栈空间大小-Xss512k)

通过对比我们知道,创建大量的局部变量将使得局部变量表膨胀从而引发StackOverflowError异常,程序设计时应当尽量避免同一个方法中包含大量局部变量,实在无法避免可以考虑方法的重构及拆分。

三、OOM

对于Java程序员而言,OOM算是比较常见的生产环境异常,OOM往往容易引发线上事故,因此有必要梳理常见可能导致OOM的场景,并尽量规避保障服务的稳定性。

  • 创建大量的类

方法区主要的职责是用于存放类型相关信息,如类名、接口名、父类、访问修饰符、字段描述、方法描述等,Java8以后方法区从永久区移到了Metaspace,若系统中创建过多的类时,可能会引发OOM。Java开发中经常会利用字节码增强技术,通过创建代理类从而实现系统功能,以我们熟知的Spring框架为例,经常通过Cglib运行时创建代理类实现类的动态性,通过设置虚拟机参数,-XX:MaxMetaspaceSize=10M,则示例程序运行一段时间时间后,将触发Full GC并最终抛出OOM异常,因此日常开发过程中要特别留意,系统中创建的类的数量是否合理,以免产生OOM。实例代码如下:

/**
* 大量创建类导致方法区OOM
*/
public class JavaMethodAreaOOM {
  /**
    * VM Args: -XX:MaxMetaspaceSize=10M(设置Metaspace空间为10MB)
    *
    * @param args
    */
  public static void main(String[] args) {
      while (true) {
          Enhancer enhancer = new Enhancer();
          enhancer.setSuperclass(OOMObject.class);
          enhancer.setUseCache(false);
          enhancer.setCallback(new MethodInterceptor() {
              @Override
              public Object intercept(Object obj, Method method,
                                      Object[] args, MethodProxy proxy) throws Throwable {
                  return proxy.invokeSuper(obj, args);
              }
          });
          Object object = enhancer.create();
          System.out.println("hashcode:" + object.hashCode());
      }
  }

  /**
    * 对象
    */
  static class OOMObject {

  }
}

设置虚拟机参数:-Xms1M -Xmx1M -verbose:gc -XX:+PrintGCDetails 限定元数据区为10M,则虚拟机Full GC之后,将引发OOM异常,程序执行结果如下:

  • 创建大对象

Java堆内存空间非常宝贵,若系统中存在数组、复杂嵌套对象等大对象,将会引发FullGc并最终引发OOM异常,因此程序设计时需要合理设置类结构,避免产生过大的对象,示例代码如下:

/**
* 堆内存溢出
*/
public class HeapOOM {
  /**
    *
    */
  static class InnerClass {
      private int value;
      private byte[] bytes = new byte[1024];
  }

  /**
    * VM Args: -Xms1M -Xmx1M -verbose:gc -XX:+PrintGCDetails
    *
    * @param args
    */
  public static void main(String[] args) {
      List<InnerClass> innerClassList = new ArrayList<>();
      while (true) {
          innerClassList.add(new InnerClass());
      }
  }
}

设置虚拟机参数:-Xms1M -Xmx1M -verbose:gc -XX:+PrintGCDetails 限定最大堆内存空间为10M,则虚拟机Full GC之后,将引发OOM异常,程序执行结果如下。

  • 常量池溢出

Java8开始,常量池从永久区移至堆区,当系统中存在大量常量并超过常量池最大容量时,将引发OOM异常。String类的intern()方法内部逻辑是:若常量池中存在字符串常量,则直接返回字符串引用,否则创建常量将其加入常量池中并返回常量池引用。通过每次创建不同的字符串,常量池将因为无法容纳新创建的字符串常量而最终引发OOM,示例代码如下:

/**
* VM Args: -Xms1M -Xmx1M -verbose:gc -XX:+PrintGCDetails
*
* @param args
*/
public static void main(String[] args) {
  Set<String> set = new HashSet<>();
  int i = 0;
  while (true) {
      set.add(String.valueOf(i++).intern());
  }
}

设置虚拟机参数:-Xms1M -Xmx1M -verbose:gc -XX:+PrintGCDetails 限定最大堆内存空间为10M,则虚拟机Full GC之后,将引发OOM异常,程序执行结果如下。

  • 直接内存溢出

直接内存是因NIO技术而引入的独立于堆区的一块内存空间,对于读写频繁的场景,通过直接操作直接内存可以获得更高的执行效率,但其内存空间受制于操作系统本地内存大小,超过最大限制后,也将抛出OOM 异常,以下代码通过UnSafe类,直接操作内存,程序执行一段时间后,将引发OOM异常。

public class DirectMemoryOOM {
  private static final int _1MB = 1024 * 1024;

  /**
    * VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M
    * @param args
    * @throws IllegalAccessException
    */
  public static void main(String[] args) throws IllegalAccessException {
      Field field = Unsafe.class.getDeclaredFields()[0];
      field.setAccessible(true);
      Unsafe unsafe = (Unsafe) field.get(null);
      while (true) {
          unsafe.allocateMemory(_1MB*1024);
      }
  }
}

设置虚拟机参数:-Xmx20M -XX:MaxDirectMemorySize=10M 限定最大直接内存空间为10M,程序最终执行结果如下:

四、总结

本文通过实际的案例列举了常见的OOM异常,由此我们可以得知,程序设计过程中应当合理的设置栈区、堆区、直接内存的大小,从实际情况出发,合理设计数据结构,从而避免引发OOM故障,此外通过分析引发OOM的原因也有利于我们针对深入理解JVM并对现有系统进行系统调优。

作者:洞幺幺洞
来源:https://juejin.cn/post/7043442191619850277

收起阅读 »

如何用JavaScript实现双向映射?

本文翻译自 《How to create a Bidirectional Map in JavaScript》双向映射是指在键值对中建立双向一一对应关系的一种模式。它既可以通过键名(key)去获取值(value),也可以通过值去获取键名。让我们看下如何在Jav...
继续阅读 »



本文翻译自 《How to create a Bidirectional Map in JavaScript》

双向映射是指在键值对中建立双向一一对应关系的一种模式。它既可以通过键名(key)去获取值(value),也可以通过值去获取键名。让我们看下如何在JavaScript中实现一个双向映射,以及 TypeScript 中的应用。

双向映射背后的计算机科学与数学

首先看一下双向映射的基本定义:

在计算机科学中,双向映射是由一一对应的键值对组成的数据结构,因此在每个方向都可以建立二元关系:每个值也可以对应唯一的键。

百科指路双向映射

计算机科学中的双向映射,源于数学上的双射函数。双射函数是指两个集合中的每个元素,都可以在另一个集合中找到与之匹配的另一个元素,反之也可以通过后者找到匹配的前者,因此也被叫做可逆函数。

百科指路: 双射函数

扩展:

  • 单射(injection):每一个x都有唯一的y与之对应;

  • 满射(surjection):每一个y都必有至少一个x与之对应;

  • 双射(又叫一一对应,bijection):每一个x都有y与之对应,每一个y都有x与之对应。

根据上面的说明,一个简单的双射函数就像这样:

f(1) = 'D';
f(C) = 3;

另外,双射函数需要两个集合的长度相等,否则会失败。

初始化双向映射

我们可以在JavaScript 中创建一个类来初始化键值对:

const bimap = new BidirectionalMap({
 a: 'A',
 b: 'B',
 c: 'C',
})

在类里面,我们将会创建两个列表,一个用来处理正向映射,存放初始化对象的副本;另一个用来处理逆向映射,存放的内容是「键」「值」翻转后的初始化对象。

class BidirectionalMap {
 fwdMap = {}
 revMap = {}

 constructor(map) {
     this.fwdMap = { ...map }
     this.revMap = Object.keys(map).reduce(
        (acc, cur) => ({
             ...acc,
            [map[cur]]: cur,
        }),
        {}
    )
}
}

注意,由于初始对象本身的性质,你不能用数字当 key,但可以作为值来使用。

const bimap = new BidirectionalMap({
 a: 42,
 b: 'B',
 c: 'C',
})

如果不满足于此,也有更强大健壮的实现方式,按照 JavaScript 映射数据类型 中允许使用数字、函数甚至NaN来作为 key 的规范来实现,当然这会更加复杂。

通过双向映射获取元素

现在,我们有了一个包含两个对象的数据结构,它们互为键值对的镜像。我们现在需要一个方法来取出元素,让我们来实现一个 get() 函数:

 get( key ) {
   return this.fwdMap[key] || this.revMap[key]
}

这个方法非常简单: 如果正向映射里存在就返回,否则返回逆向映射,都没有就返回 undefined

试一下获取元素:

console.log(bimap.get('a')) // displays A
console.log(bimap.get('A')  // displays a

给双向映射添加元素

目前映射还无法添加元素,我们创建一个添加方法:

add(pair) {
   this.fwdMap[pair[0]] = pair[1]
   this.revMap[pair[1]] = pair[0]
}

add 函数接收一个双元素数组(在TypeScript 中叫做元组),按不同键值顺序加入到相应对象中。

现在我们可以添加和读取映射中的元素了:

bimap.add(['d', 'D'])
console.log( bimap.get('D') ) // displays d

在TypeScript中安全使用双向映射

为了确保数据类型安全,我们可以在 TypeScript 中进行改写,对输入类型进行检查,例如初始化的映射必须为一个通用对象,添加的元素必须为一个 元组

class BidirectionalMap {
 fwdMap = {}
 revMap = {}

 constructor(map: { [key: string]: string }) {
     this.fwdMap = { ...map }
     this.revMap = Object.keys(map).reduce(
        (acc, cur) => ({
             ...acc,
            [map[cur]]: cur,
        }),
        {}
    )
}

 get(key: string): string | undefined {
     return this.fwdMap[key] || this.revMap[key]
}

 add(pair: [string, string]) {
   this.fwdMap[pair[0]] = pair[1]
   this.revMap[pair[1]] = pair[0]
}
}

这样我们的映射就更加安全和完美了。在这里,我们的 key 和 value 都必须使用字符串。


翻译:sherryhe
来源:https://juejin.cn/post/6976797991277428750

收起阅读 »

消息队列的使用场景是什么样的?

本文从异步、解耦、削峰填谷等核心应用场景,以及消息中间件常用协议、推拉模式对比来解答此问题。什么是消息中间件作为一种典型的消息代理组件(Message Broker),是企业级应用系统中常用的消息中间件,主要应用于分布式系统或组件之间的消息通讯,提供具有可靠、...
继续阅读 »



本文从异步、解耦、削峰填谷等核心应用场景,以及消息中间件常用协议、推拉模式对比来解答此问题。

什么是消息中间件

作为一种典型的消息代理组件(Message Broker),是企业级应用系统中常用的消息中间件,主要应用于分布式系统或组件之间的消息通讯,提供具有可靠、异步和事务等特性的消息通信服务。应用消息代理组件可以降低系统间耦合度,提高系统的吞吐量、可扩展性和高可用性。

分布式消息服务主要涉及五个核心角色,消息发布者(Publisher)、可靠消息组件(MsgBroker)、消息订阅者(Subscriber)、消息类型(Message Type)和订阅关系(Binding),具体描述如下:

  1. 消息发布者,指发送消息的应用系统,一个应用系统可以发送一种或者多种消息类型,发布者发送消息到可靠消息组件 (MsgBroker)。

  2. 可靠消息组件,即 MsgBroker,负责接收发布者发送的消息,根据消息类型和订阅关系将消息分发投递到一个或多个消息订阅者。整个过程涉及消息类型校验、消息持久化存储、订阅关系匹配、消息投递和消息恢复等核心功能。

  3. 消息订阅者,指订阅消息的应用系统,一个应用系统可以订阅一种或者多种消息类型,消息订阅者收到的消息来自可靠消息组件 (MsgBroker)。

  4. 消息类型:一种消息类型由 TOPIC 和 EVENTCODE 唯一标识。

  5. 订阅关系,用来描述一种消息类型被订阅者订阅,订阅关系也被称为 Binding。

核心功能特色

可为不同应用系统间提供可靠的消息通信,降低系统间耦合度并提高整体架构的可扩展性和可用性。

可为不同应用系统间提供异步消息通信,提高系统吞吐量和性能。

发布者系统、消息代理组件以及订阅者系统均支持集群水平扩展,可依据业务消息量动态部署计算节点。

支持事务型消息,保证消息与本地数据库事务的一致性。

远程调用RPC和消息MQ区别

谈到消息队列,有必要看下RPC和MQ的本质区别,从两者的定义和定位来看,RPC(Remote Procedure Call)远程过程调用,主要解决远程通信间的问题,不需要了解底层网络的通信机制;消息队列(MQ)是一种能实现生产者到消费者单向通信的通信模型。核心区别在于RPC是双向直接网络通讯,MQ是单向引入中间载体的网络通讯。单纯去看队列,队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。在队列前面增加限定词“消息”,意味着通过消息驱动来进行整体的架构实现。RPC和MQ本质上是网络通讯的两种不同的实现机制,RPC同步等待结果对比于MQ在异步、解耦、削峰填谷等上的特征显著差异主要有以下几点差异:

  1. 在架构上,RPC和MQ的差异点是,Message有一个中间结点Message Queue,可以把消息存储起来。

  2. 同步调用:对于要立即等待返回处理结果的场景,RPC是首选。

  3. MQ的使用,一方面是基于性能的考虑,比如服务端不能快速的响应客户端(或客户端也不要求实时响应),需要在队列里缓存;另外一方面,它更侧重数据的传输,因此方式更加多样化,除了点对点外,还有订阅发布等功能。

  4. 随着业务增长,有的处理端调用下游服务太多或者处理量会成为瓶颈,会进行同步调用改造为异步调用,这个时候可以考虑使用MQ。

核心应用场景

针对MQ的核心场景,我们从异步、解耦、削峰填谷等特性进行分析,区别于传统的RPC调用。尤其在引入中间节点的情况下,通过空间(拥有存储能力)换时间(RPC同步等待响应)的思想,增加更多的可能性和能力。

异步通信

针对不需要立即处理消息,尤其那种非常耗时的操作,通过消息队列提供了异步处理机制,通过额外的消费线程接管这部分进行异步操作处理。

解耦

在应用和应用之间,提供了异构系统之间的消息通讯的机制,通过消息中间件解决多个系统或异构系统之间除了RPC之外另一种单向通讯的机制。

扩展性

因为消息队列解耦了主流程的处理过程,只要另外增加处理过程即可,不需要改变代码、不需要调整参数,便于分布式扩容。

分布式事务一致性

在2个应用系统之间的数据状态同步,需要考虑数据状态的最终一致性的场景下,利用消息队列所提供的事务消息来实现系统间的数据状态一致性。

削峰填谷

在访问量剧增的情况下,应用仍然需要继续发挥作用,但是这样的突发流量无法提前预知;如果为了能处理这类瞬间峰值访问提前准备应用资源无疑是比较大的浪费。使用消息队列在突发事件下的防脉冲能力提供了一种保障,能够接管前台的大脉冲请求,然后异步慢速消费。

可恢复性

系统的一部分组件失效时,不会影响到整个系统。消息队列降低了应用间的耦合度,所以即使一个处理消息的应用挂掉,加入队列中的消息仍然可以在系统恢复后被处理。

顺序保证

在大多使用场景下,数据处理的顺序都很重要。大部分消息队列本来就是排序的,并且能保证数据会按照特定的顺序来进行处理。

大量堆积

通过消息堆积能力处理数据迁移场景,针对旧数据进行全量迁移的同时开启增量消息堆积,待全量迁移完毕,再开启增量,保证数据最终一致性且不丢失。

数据流处理

分布式系统产生的海量数据流,如:业务日志、监控数据、用户行为等,针对这些数据流进行实时或批量采集汇总,然后导入到大数据实时计算引擎,通过消息队列解决异构系统的数据对接能力。

业界消息中间件对比

详细的对比可以参考:blog.csdn.net/wangzhipeng…

消息中间件常用协议

AMQP协议

AMQP即Advanced Message Queuing Protocol,提供统一消息服务的高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同开发语言等条件的限制。

优点:可靠、通用

MQTT协议

MQTT(Message Queuing Telemetry Transport,消息队列遥测传输)是IBM开发的一个即时通讯协议,成为物联网的重要组成部分。该协议支持所有平台,几乎可以把所有联网物品和外部连接起来,被用来当做传感器和致动器(比如通过Twitter让房屋联网)的通信协议。

优点:格式简洁、占用带宽小、移动端通信、PUSH、嵌入式系统

STOMP协议

STOMP(Streaming Text Orientated Message Protocol)是流文本定向消息协议,是一种为MOM(Message Oriented Middleware,面向消息的中间件)设计的简单文本协议。STOMP提供一个可互操作的连接格式,允许客户端与任意STOMP消息代理(Broker)进行交互。

优点:命令模式(非topic/queue模式)

XMPP协议

XMPP(可扩展消息处理现场协议,Extensible Messaging and Presence Protocol)是基于可扩展标记语言(XML)的协议,多用于即时消息(IM)以及在线现场探测。适用于服务器之间的准即时操作。核心是基于XML流传输,这个协议可能最终允许因特网用户向因特网上的其他任何人发送即时消息,即使其操作系统和浏览器不同。

优点:通用公开、兼容性强、可扩展、安全性高,但XML编码格式占用带宽大

基于TCP/IP自定义的协议

有些特殊框架(如:redis、kafka、rocketMQ等)根据自身需要未严格遵循MQ规范,而是基于TCP/IP自行封装了一套二进制编解码协议,通过网络socket接口进行传输,实现了MQ的标准规范相关功能。

消息中间件推和拉模式对比

Push推模式:服务端除了负责消息存储、处理请求,还需要保存推送状态、保存订阅关系、消费者负载均衡;推模式的实时性更好;如果push能力大于消费能力,可能导致消费者崩溃或大量消息丢失

Push模式的主要优点是:

  1. 对用户要求低,方便用户获取需要的信息

  2. 及时性好,服务器端即时地向客户端推送更行的动态信息

Push模式的主要缺点是:

  1. 推送的信息可能并不能满足客户端的个性化需求

  2. Push消息大于消费者消费速率额,需要有协调QoS机制做到消费端反馈

Pull拉模式:客户端除了消费消息,还要保存消息偏移量offset,以及异常情况下的消息暂存和recover;不能及时获取消息,数据量大时容易引起broker消息堆积。

Pull拉模式的主要优点是:

  1. 针对性强,能满足客户端的个性化需求

  2. 客户端按需获取,服务器端只是被动接收查询,对客户端的查询请求做出响应

Pull拉模式主要的缺点是:

  1. 实时较差,针对于服务器端实时更新的信息,客户端难以获取实时信息

  2. 对于客户端用户的要求较高,需要维护位点

相关资料

建议学习以下的技术文档,了解更多详细的技术细节和实现原理,加深对消息中间件的理解和应用,同时可以下载开源的源代码,本地调试相应的代码,加深对技术原理的理解和概念的掌握,以及在实际生产中更多的掌握不同的消息队列应用的场景下,高效和正确地使用消息中间件。

RocketMQ资料: github.com/apache/rock…

Kafka资料: kafka.apache.org/documentati…

阿里云RocketMQ文档: help.aliyun.com/document_de…


作者:阿里巴巴淘系技术
来源:https://juejin.cn/post/7025955365812437028

收起阅读 »

业务实战中经典算法的应用

有网友提问:各种机器学习算法的应用场景分别是什么(比如朴素贝叶斯、决策树、K 近邻、SVM、逻辑回归最大熵模型)?这些在一般工作中分别用到的频率多大?一般用途是什么?需要注意什么?根据问题,核心关键词是基础算法和应用场景,比较担忧的点是这些基础算法能否学有所用...
继续阅读 »



有网友提问:各种机器学习算法的应用场景分别是什么(比如朴素贝叶斯、决策树、K 近邻、SVM、逻辑回归最大熵模型)?

这些在一般工作中分别用到的频率多大?一般用途是什么?需要注意什么?

根据问题,核心关键词是基础算法和应用场景,比较担忧的点是这些基础算法能否学有所用?毕竟,上述算法是大家一上来就接触的,书架上可能还放着几本充满情怀的《数据挖掘导论》《模式分类》等经典书籍,但又对在深度学习时代基础算法是否有立足之地抱有担忧。

网上已经有很多内容解答了经典算法的基本思路和理论上的应用场景,这些场景更像是模型的适用范围,这与工业界算法实际落地中的场景其实有很大区别。

从工业界的角度看,业务价值才是衡量算法优劣的金钥匙,而业务场景往往包含业务目标、约束条件和实现成本。如果我们只看目标,那么前沿的算法往往占据主导,可如果我们需兼顾算法运行复杂度、快速迭代试错、各种强加的业务限制等,经典算法往往更好用,因而就占据了一席之地。

针对这个问题,淘系技术算法工程师感知同学写出本文详细解答。

在实际业务价值中,算法模型的影响面大致是10%

在工业界,算法从想法到落地,你不是一个人在战斗。我以推荐算法为例,假设我们现在接到的任务是支持某频道页feeds流的推荐。我们首先应该意识到对业务来说,模型的影响面大致是10%,其他几个重要影响因子是产品设计(40%)、数据(30%)、领域知识的表示和建模(20%)。

这意味着,你把普通的LR模型升级成深度模型,即使提升20%,可能对业务的贡献大致只有2%。当然,2%也不少,只是这个折扣打的让人脑壳疼。

当然,上面的比例划分并不是一成不变的,在阿里推荐算法元年也就是2015年起,个性化推荐往往对标运营规则,你要是不提升个20%都不好意思跟人打招呼。那么一个算法工程师的日常除了接需求,就是做优化:业务给我输入日志、特征和优化目标,剩下的事情就交给我吭哧吭哧。

可随着一年年的水涨船高,大家所用模型自然也越来越复杂,从LR->FTRL->WDL->DeepFM->MMOE,大家也都沿着前辈们躺过的路径一步步走下去。这时候你要是问谁还在用LR或是普通的决策树,那确实会得到一个尴尬的笑容。

但渐渐的,大家也意识到了,模型优化终究是一个边际收益递减的事情。当我们把业务方屏蔽在外面而只在一个密闭空间中优化,天花板就已经注定。于是渐渐的,淘系的推荐慢慢进入第二阶段,算法和业务共建阶段。业务需求和算法优化虽然还是分开走,但已经开始有融合的地方。

集团对算法工程师的要求也在改变:一个高大上的深度模型,如果不能说清楚业务价值,或带来特别明显提升,那么只能认为是自嗨式的闭门造车。这时,一个优秀的算法工程师,需要熟悉业务,通过和业务反复交流中,能够弄清楚业务痛点。

注意,业务方甚至可能当局者迷,会提出既要又要还要的需求给你,而你需要真正聚焦到那个最值得做的问题上。然后,才是对问题的算法描述。做到这一步你会发现,并不是你来定哪个模型牛逼,而是跟着问题走来选择模型。这个模型的第一版极大可能是一个经典算法,因为,你要尽快跑通链路,快速验证你的这个idea是有效的。后面的模型迭代提升,只是时间问题。

经典算法在淘系的应用场景示例:TF-IDF、K近邻、朴素贝叶斯、逻辑回归等

因而现阶段,在淘系的大多数场景中,并不是算法来驱动业务,而是配合业务一起来完成增长。一个只懂技术的算法工程师,最多只能拿到那10%的满分。为了让大家有体感,这里再举几个小例子:

比如业务问题是对用户进行人群打标,人群包括钓鱼控、豆蔻少女、耳机发烧友、男神style等。在实操中我们不仅需考虑用户年龄、性别、购买力等属性,还需考虑用户在淘系的长期行为,从而得到一个多分类任务。如果模型所用的的特征是按月访问频次,那么豆蔻少女很可能网罗非常多的用户,因为女装是淘系行为频次最多的类目。

比如,某用户对耳机发烧友和豆蔻少女一个月内都有4次访问,假设耳机发烧友人均访问次数是3.2次,而豆蔻少女是4.8次,那么可知该用户对耳机发烧友的偏好分应更高。因此,模型特征不仅应该使用用户对该人群的绝对行为频次,还需参照大盘的水位给出相对行为频次。

这时,入选吴军老师《数学之美》的TF-IDF算法就派上用场了。通过引入TF-IDF构建特征,可以显著提高人群标签的模型效果,而TF-IDF则是非常基础的文本分类算法。

在淘系推荐场景,提升feeds流的点击率或转化率往往是一个常见场景。可业务总会给你惊喜:比如商品的库存只有一件(阿里拍卖),比如推荐的商品大多是新品(天猫新品),比如通过小样来吸引用户复购正品,这些用户大多是第一次来(天猫U先),或者是在提升效率的同时还需兼顾类目丰富度(很多场景)。

在上述不同业务约束背景,才是我们真实面对的应用场景。面对这种情况首先是定方向,比如在阿里拍卖中的问题可描述为“如何在浅库存约束下进行个性化推荐”。假如你判断这是一个流量调控问题,就需要列出优化目标和约束条件,并调研如何用拉格朗日乘子法求解。重要的是,最终的结果还需要和个性化推荐系统结合。详见:阿里拍卖全链路导购策略首次揭秘

面对上述应用场景,你需明白你的战略目标是证明浅库存约束下的推荐是一个流量调控问题,并可以快速验证拿到效果。采用一个成熟经典的方法先快速落地实验,后续再逐步迭代是明智的选择。

又比如,K近邻算法似乎能简单到能用一张图或一句话来描述。可它在解决正负样本不均衡问题中就能派上用场。上采样是说通过将少数类(往往是正样本)的数据复制多份,但上采样后存在重复数据集可能会导致过拟合。过拟合的一种原因是在局部范围正负样本比例存在差异,如下图所示:

我们只需对C类局部样本进行上采样,这其中就运用到了K近邻算法。本例中的经典算法虽然只是链路的一部分,甚至只是配角儿,但离了它不行。

朴素贝叶斯虽然简单了点,但贝叶斯理论往后的发展,包括贝叶斯网络、因果图就不那么simple了。比如不论是金融业务中的LR评分卡模型,或是推荐算法精排中的深度模型,交叉特征往往由人工经验配置,这甚至是算法最不自动的一个环节。

使用贝叶斯网络中的structure learning,并结合业务输入的行业知识,构建出贝叶斯概率图,并从中发现相关特征做交叉,会比人工配置带来一定提升,也具有更好的可解释性。这就是把业务领域知识和算法模型结合的一个很好的例子。可如果贝叶斯理论不扎实,很难走到这一步。

不论深度学习怎么火,LR都是大多场景的一个backup。比如在双十一大促投放场景中,如果在0:00~0:30这样的峰值期,所有场景都走深度模型,机器资源肯定不足,这时候就需要做好一个使用LR或FTRL的降级预案。

如何成为业界优秀的算法工程师?

在淘系,算法并不是孤立的存在,算法工程师也不只是一个闭关的剑客。怎么切中业务痛点,快速验证你的idea,怎么进行合适的算法选型,这要求你有很好的算法基本功,以及较广的算法视野。一个模型无论搞的多复杂最终的回答都是业务价值,一个有良好基本功、具备快速学习能力,且善于发掘业务价值的算法工程师,将有很大成长空间。

好吧,假设上面就是我们的职业目标,可怎么实现呢。元(ye)芳(jie),你怎么看?其实只有一个句话,理论和实践相结合

理论

对算法基本思路的了解,查查知乎你可能只需要20分钟,可你忘记它可能也只需要2周。这里的理论是你从内而外对这个算法的感悟,比如说到决策树,你脑海里好像就能模拟出它的信息增益计算、特征选择、节点分裂的过程,知道它的优和劣。最终,都是为了在实践中能快速识别出它就是最适合解决当前问题的模型。

基本功弄扎实是一个慢活儿,方法上可以是看经典原著、学习视频分享或是用高级语言实现。重要的是心态,能平心静气,不设预期,保持热情;另外,如果你有个可以分享的兴趣小组,那么恭喜你。因为学习书籍或看paper的过程其实挺枯燥的,但如果有分享的动力你可以走的更远。

实践

都知道实践出真知,可实践往往也很残酷。因为需求和约束多如牛毛,问题需要你来发现,留给你的时间窗口又很短暂。也就是,算法是一个偏确定性、有适用边界、标准化的事情;而业务则是发散的、多目标的、经验驱动的事情。

你首先是需要有双发现的眼睛,找到那个最值得发力的点,这个需要数据分析配合业务经验,剥丝抽茧留下最主要的优化目标、约束条件,尽量的简化问题;其次是有一张能说会道的嘴,要不然业务怎么愿意给你这个时间窗口让你尝试呢;最后是,在赌上你的人设之后,需要尽快在这个窗口期做出效果,在这个压力之下,考验你算法基本功的时候就到了。

结语

最后,让大家猜个谜语:它是一个贪心的家伙,计算复杂度不大,可以动态的做特征选择,不论特征是离散还是连续;它还可以先剪枝或后剪枝避免过拟合;它融合了信息熵的计算,也可以不讲道理的引入随机因子;它可以孤立的存在,也完全可以做集成学习,还可以配合LR一起来解决特征组合的问题;对小样本、正负样本不均、在线数据流的学习也可以支持;所得到则是有很强可解释性的规则。它,就是决策树。

每一个基础算法,都好比是颗带有智慧的种子,这不是普通的锦上添花,而是石破天惊的原创思维。有时不妨傻傻的回到过去的年代,伴着大师原创的步伐走一走,这种原创的智慧对未来长期的算法之路,大有裨益。神经网络从种下到开花,将近50年;贝叶斯也正在开花结果的路上。下一个,将会是谁呢?


作者:阿里巴巴淘系技术
来源:https://juejin.cn/post/7025964095530598436

收起阅读 »

android 水波纹控件,仿京东语音评价动画

先上效果gradle 引用implementation 'com.maxcion:multwaveviewlib:1.0.0'https://github.com/Likeyong/MultWaveView第一种样式存在三层水波纹,所以要生成3个Wave对象,...
继续阅读 »



先上效果

gradle 引用

implementation 'com.maxcion:multwaveviewlib:1.0.0'

项目地址

https://github.com/Likeyong/MultWaveView

  • 第一种效果是多重水波纹的效果, 三层水波纹,每层水播放可以设置不同的颜色、波高与波宽

  • 第二种效果是 单层水波纹,并且支持从底部慢慢上涨的效果。 这里也可以设置为多层水波纹上涨效果,颜色、波高、波宽以及背景图都是可以定制

  • 第三种效果就是模仿京东语音评价动画,最底部的seekbar 是用来模拟语音输出的声音大小,声音越大波高越大,并且每条水波纹的粗细以及速度都不同

第一种和第二种效果的背景图是这样的, 如果设置了背景图,那么整个控件的大小就是背景图大小与xml中设置的大小无关

List<Wave> waves = new ArrayList<>();
       Wave wave1 = new Wave(0, 150, 1, Color.parseColor("#00FFFF"), 3);
       Wave wave2 = new Wave(1f / 8, 150, 1, Color.parseColor("#6600FFFF"), 3);
       Wave wave3 = new Wave(1f / 4, 150, 1, Color.parseColor("#4400FFFF"), 3);
       waves.add(wave1);
       waves.add(wave2);
       waves.add(wave3);

       waveView.start(WaveArg.build()
              .setWaveList(waves)
              .setAutoRise(false)
              .setIsStroke(false)
              .setTransformBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.batman_logo))

第一种样式存在三层水波纹,所以要生成3个Wave对象,参数说明如下

设置完水波纹数据后,就可以开始动画了

waveView.start(WaveArg.build()
              .setWaveList(waves)//设置水波纹数据
              .setAutoRise(false)//设置水波纹是否自动上升
              .setIsStroke(false)//设置水波纹是实心的还是线条模式
              .setTransformBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.batman_logo))
               //设置背景图

      );

第二种样式:只有一层水波纹但是需要自动上涨,所以只用生成一条wave对象

List<Wave> waves = new ArrayList<>();
       Wave wave1 = new Wave(0, 150, 1, Color.parseColor("#00FFFF"), 3);
       waves.add(wave1);

waveView2.start(WaveArg.build()
              .setWaveList(waves)
              .setAutoRise(true)//设置自动上涨
              .setIsStroke(false)
              .setTransformBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.batman_logo))

      );

第三种样式,仅线条模式,并且三条水波纹粗细不一样,速度不一样,水波纹的粗细和速度都是通过Wave的构造函数设置的

List<Wave> waves = new ArrayList<>();
       Wave wave1 = new Wave(0, 150, 1, Color.parseColor("#000000"), 3);
       Wave wave2 = new Wave(0, 150, 3, Color.parseColor("#000000"), 3);
       Wave wave3 = new Wave(0, 150, 2, Color.parseColor("#000000"), 3);
       waves.add(wave1);
       waves.add(wave2);
       waves.add(wave3);

waveView3.start(WaveArg.build()
              .setWaveList(waves)
              .setAutoRise(false)
              .setIsStroke(true)

      );

这样写我们的三条粗细、速度都不同的水波纹动画就出来了,接下来就是根据声音大小来调整波高,通过 waveView.setWaveHeightMultiple(mult), 因为语音一直输入,声音大小也是一直变化的,科大讯飞的语音转写SDK中就有声音大小的回调,然后将回调出来的声音大小通过setWaveHeightMultiple,就可以实现波高动态改变了

作者:maxcion
来源:https://www.jianshu.com/p/9a770b0e68ff

收起阅读 »

Vue图片懒加载

1、问题在vue项目中,如果图片是从服务器端加载到页面上,图片较大的时候,就会存在一部分一部分加载的情况,会显示非常卡顿,影响体验。2、实现(1)、图片懒加载首先将图片的src链接设为一张我们已经准备好的图片(比如类似加载中的图片),并将其真正的图片地址存储在...
继续阅读 »

1、问题

在vue项目中,如果图片是从服务器端加载到页面上,图片较大的时候,就会存在一部分一部分加载的情况,会显示非常卡顿,影响体验。

2、实现

(1)、图片懒加载

首先将图片的src链接设为一张我们已经准备好的图片(比如类似加载中的图片),并将其真正的图片地址存储在img标签的自定义属性中。当js监听到该图片元素进入可视窗口时,即将自定义属性中的地址存储到src属性中,达到懒加载的效果。这样就可以缓解服务器压力,并且提高用户体验。

(2)、安装vue-lazyload
npm i vue-lazyload -S
(3)、在main.js中引入
import VueLazyload from "vue-lazyload";
Vue.use(VueLazyload,{
  preLoad: 1.3,
  loading: require('../src/assets/loading.gif'),
  attempt: 1
})

其中../src/assets/loading.gif是我本地的正在加载图片gif路径。

3、查看效果

在LazyLoad.vue中引入一张网络图片,在浏览器中限制网速,模拟图片加载缓慢的情况。

LazyLoad.vue

<template>
<div>
<img v-lazy=url1 alt="">
</div>
</template>
<script>
export default {
data (){
return{
url1: 'https://w.wallhaven.cc/full/pk/wallhaven-pkgkkp.png'
}
}
}
</script>

效果:

图片加载中:

图片加载完成:


常用参数:


作者:小绵杨Yancy
来源:https://blog.csdn.net/ZHANGYANG_1109/article/details/121868420

收起阅读 »

大白话讲解JavaScript 执行机制,一看就懂

JavaScript的运行机制所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。 为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是...
继续阅读 »



JavaScript的运行机制

1.JavaScript为什么是单线程?
JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊。
JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。
为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。

2、执行机制相关知识点

  • 同步任务

  • 异步任务

举个栗子
有一天,张三要去做饭 这时候他要做两件事 分别是蒸米饭 和 炒菜 ,现在有两种方式去完成这个任务

A. 先去蒸米饭 然后等蒸米饭好了 再去抄菜 ---同步任务
B. 先去蒸米饭 然后等蒸米饭的过程中 再去抄菜 ---异步任务

同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;
异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

当我们打开网站时,网页的渲染过程就是一大堆同步任务,比如页面骨架和页面元素的渲染。而像加载图片音乐之类占用资源大耗时久的任务,就是异步任务。

具体来说,异步执行的运行机制如下:(同步执行也是如此,因为它可以被视为没有异步任务的异步执行。)
(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
(4)主线程不断重复上面的第三步。

总结: 同步任务在主线程执行,形成一个执行栈,执行栈中的所有同步任务执行完毕,就会去读取任务队列,就是对应的异步任务。

JavaScript的宏任务与微任务
除了广义上的定义,我们可以将任务进行更精细的定义,分为宏任务微任务

宏任务(macro-task):包括整体代码script脚本的执行,setTimeout,setInterval,ajax,dom操作,还有如 I/O 操作、UI 渲染等。

微任务(micro-task):Promise回调 node 中的 process.nextTick 、对 Dom 变化监听的 MutationObserver。

主线程都从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为 Event Loop(事件循环)

我们解释一下这张图:

1、同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入Event Table并注册函数。
2、当指定的事情完成时,Event Table会将这个函数移入Event Queue。
3、主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。
上述过程会不断重复,也就是常说的Event Loop(事件循环)。(Event Loop是javascript的执行机制)

优先级
setTimeout()、setInterval()
setTimeout() 和 setInterval() 产生的任务是 异步任务,也属于 宏任务。
setTimeout() 接受两个参数,第一个是回调函数,第二个是推迟执行的毫秒数。
如果将第二个参数设置为0或者不设置,意思 并不是立即执行,而是指定某个任务在主线程最早可得的空闲时间执行,也就是说,尽可能早得执行。它在"任务队列"的尾部添加一个事件,因此要等到同步任务和"任务队列"现有的事件都处理完,才会得到执行。(画重点)
所以说,setTimeout() 和 setInterval() 第二个参数设置的时间并不是绝对的,它需要根据当前代码最终执行的时间来确定的

Promise
Promise 相对来说就比较特殊了,在 new Promise() 中传入的回调函数是会 立即执行 的,但是它的 then() 方法是在 执行栈之后,任务队列之前 执行的,它属于 微任务。

process.nextTick
process.nextTick 是 Node.js 提供的一个与"任务队列"有关的方法,它产生的任务是放在 执行栈的尾部,并不属于 宏任务 和 微任务,因此它的任务 总是发生在所有异步任务之前。

setImmediate
setImmediate 是 Node.js 提供的另一个与"任务队列"有关的方法,它产生的任务追加到"任务队列"的尾部,它和 setTimeout(fn, 0) 很像,但优先级都是 setTimeout 优先于 setImmediate。
有时候,setTimeout 的执行顺序会在 setImmediate 的前面,有时候会在 setImmediate 的后面,这并不是 node.js 的 bug,这是因为虽然 setTimeout 第二个参数设置为0或者不设置,但是 setTimeout 源码中,会指定一个具体的毫秒数(node为1ms,浏览器为4ms),而由于当前代码执行时间受到执行环境的影响,执行时间有所起伏,如果当前执行的代码小于这个指定的值时,setTimeout 还没到推迟执行的时间,自然就先执行 setImmediate 了,如果当前执行的代码超过这个指定的值时,setTimeout 就会先于 setImmediate 执行。

通过上面的介绍,我们就可以得出一个代码执行的优先级:
同步代码(宏任务) > process.nextTick > Promise(微任务)> setTimeout(fn)、setInterval(fn)(宏任务)> setImmediate(宏任务)> setTimeout(fn, time)、setInterval(fn, time),其中time>0

面试回答
面试中该如何回答呢? 下面是我个人推荐的回答:
首先js 是单线程运行的,在代码执行的时候,通过将不同函数的执行上下文压入执行栈中来保证代码的有序执行。
在执行同步代码的时候,如果遇到了异步事件,js 引擎并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务
当同步事件执行完毕后,再将异步事件对应的回调加入到与当前执行栈中不同的另一个任务队列中等待执行。
任务队列可以分为宏任务对列和微任务对列,当当前执行栈中的事件执行完毕后,js 引擎首先会判断微任务对列中是否有任务可以执行,如果有就将微任务队首的事件压入栈中执行。
当微任务对列中的任务都执行完成后再去判断宏任务对列中的任务。

面试遇到的问题总结
1、同步和异步的区别是什么?分别举一个同步和异步的例子
同步会阻塞代码执行,而异步不会。alert是同步,setTimeout是异步

2、为何需要异步呢?
如果第一个示例中间步骤是一个 ajax 请求,现在网络比较慢,请求需要5秒钟。如果是同步,这5秒钟页面就卡死在这里啥也干不了了。

最后,前端 JS 脚本用到异步的场景主要有两个:

  • 定时 setTimeout setInverval

  • 网络请求,如 ajax 加载

  • 事件绑定

3、写出下图执行顺序

执行顺序是2431
在 new Promise() 中传入的回调函数是 立即执行 的,但是它的 then() 方法是在 执行栈之后,任务队列之前 执行的,
.then是回调函数,链式回调,会被放在挂起,等待执行栈的内容执行完后(输出4)再回调(输出3),最后执行异步的1

作者:我写的代码绝对没有问题
来源:https://www.jianshu.com/p/22641c97e351

收起阅读 »

9个问题带你一起了解什么是元宇宙,如何在现实生活中实现

2021 最火的新概念,莫过于元宇宙。2021 年 10 月 29 日,Facebook 宣布改名 Meta;2021 年 11 月 1 日,“元宇宙第一股” Roblox 经过短暂调整,宣布重新上线。元宇宙概念的热度可见一斑,国内外都在研究,我们程序员能否借...
继续阅读 »

2021 最火的新概念,莫过于元宇宙。2021 年 10 月 29 日,Facebook 宣布改名 Meta;2021 年 11 月 1 日,“元宇宙第一股” Roblox 经过短暂调整,宣布重新上线。元宇宙概念的热度可见一斑,国内外都在研究,我们程序员能否借用元宇宙概念进行技术延申,今天这篇文章或许会给到你一些启发。


一、什么是元宇宙?


1)元宇宙概念的提出


元宇宙在很长一段时间内仅存在于文学与影视作品中。元宇宙(Metaverse)由Meta和Verse两个词根组成,Meta表示“超越”“元”,verse表示“宇宙Universe”。Metaverse一词最早来自1992年的科幻小说《雪崩》。小说描绘人们在虚拟现实世界中通过控制自己的数字化身相互竞争以提升社会地位。在其后的接近30年间,元宇宙的概念在《黑客帝国》《头号玩家》《西部世界》等影视作品,《模拟人生》等游戏中有所呈现。在这一阶段,元宇宙的概念比较模糊,更多地被理解为平行的虚拟世界。


image.png


2)元宇宙八大要素


根据元宇宙概念上市公司Roblox的定义,元宇宙应具备身份、朋友、沉浸感、低延迟、多元化、随地、经济系统、文明等八大要素。元宇宙的表现形式大多以游戏为起点,并逐渐整合互联网、数字化娱乐、社交网络等功能,长期来看甚至可以整合社会经济与商业活动。


image.png


3)元宇宙第一股Roblox上市


2021年3月10日,Roblox采取直接挂牌模式(DPO)在纽约证券交易所上市。Roblox认为元宇宙用于描述虚拟宇宙中持久的、共享的、三维虚拟空间的概念。


4)元宇宙总结了前期科技的发展方向


在迸发元宇宙概念前,5G基础设施、用于智能终端的显示屏、AI芯片等技术不断发展演进,同时工业互联网、产业互联网、数字孪生、VR游戏等概念均不断成熟。而在元宇宙概念诞生后,可以很好地总结这一时期大部分技术的发展方向。。


image.png


二、元宇宙的架构?


51aspx认为,元宇宙的载体与内容这两个概念十分宽泛,主要分三部分:



  • 元宇宙的底层由基础设施与终端硬件设备组成:包括但不限于人机交互、3D引擎、GIS、设计工具、游戏渲染、画面渲染、隐私计算、AI、操作系统、工业互联网、内容分发、应用商店以及智能合约;

  • 在此基础上,元宇宙还需要大量的软件与技术协同:包括但不限于:基础设施端的5G、6G、云计算、区块链节点、边缘计算节点、DPU;用户端的路由器、传感器、芯片、VR头显、显示器、脑机接口;

  • 基于此,元宇宙可以衍生出相应的应用,并基于元宇宙各类应用发展出潜在的内容载体。


image.png


三、元宇宙的发展模式?


1)元宇宙的发展是循序渐进的过程,技术端、内容端、载体端都在不断演变



  • 技术端,区块链技术在不断演进;

  • 内容端,元宇宙概念的游戏不断增加,生态不断加强,用户数也随之增长。以Roblox、Sandbox为代表的UGC元宇宙概念游戏得益于玩家的参与而不断丰富自己游戏的内容;

  • 载体端,通信技术、虚拟现实、芯片等底层技术也在不断演进。


四、元宇宙的渗透路径?


这个问题也可以理解成我们距离元宇宙的距离,东方证券张颖表示,元宇宙的内容短期将集中于游戏端与艺术端(NFT艺术藏品),长期来看,元宇宙的渗透路径预计将为“游戏/艺术-工作-生活”。


1)游戏端:以Roblox为代表


Roblox平台主要由三个产品构成:



  • 客户端:允许用户探索3D数字世界的应用程序。(面向用户);

  • 工作室:允许开发人员和创作者构建、发布和操作Roblox客户端访问的3D体验和其他内容的工具群。(面向开发者)

  • Roblox云:为共同体验平台提供动力的服务和基础设施。


Roblox的主要营收来源为用户的游戏内支出。玩家需要充值换取游戏中的代币Robux获取Roblox的各种功能,这也是Roblox的营收来源。


并且,Roblox的激励机制十分明确,除掉25%支付给APPStore的营收以及用于平台各种费用的营收(约26%),剩下约49%的营收基本由公司和开发者平分。



其实,游戏UGC平台的概念可以追溯至魔兽争霸3:魔兽争霸3WE地图编辑器支持开发者创造出许多RTS(即时战略游戏)、MOBA(多人在线战术竞技)的游戏地图和游戏类型。但魔兽3对于开发者的奖励机制缺失,导致整体商用化程度不高。



2)艺术端:NFT构建元宇宙经济基础


非同质化代币(NFT)具有不可互换性、独特性、不可分性、低兼容性以及物品属性。并且产品流通渠道单一,市场透明度、价格发现能力均有较高提升空间。


目前,多家互联网大厂正试水NFT领域:2021年6月,阿里巴巴发售支付宝付款码皮肤NFT,2021年8月,腾讯围绕NFT进行一系列战略布局。



3)工作端:Facebook与英伟达的布局


Infinite office是Facebook元宇宙战略中重要环节。2020年9月,Facebook宣布推出VR虚拟办公应用InfiniteOffice,支持用户们创建虚拟办公空间,提高工作效率。


英伟达推出了NVIDIA Omniverse,一个专为虚拟协作和物理属性准确的实时模拟打造的开放式平台。并且已经开始投入使用,宝马公司正在内部推进NVIDIAOmniverse平台,以协调全球31座工厂的生产。而根据英伟达官网披露的信息,NVIDIA Omniverse将宝马的生产规划效率提高30%。


4)生活端:面向体验场景


东方证券认为,元宇宙的未来在于探索其应用场景的共性。这些应用场景均需考量用户的体验,元宇宙未来的商业模式与智能手机类似,即通过体验感增加用户的使用时间,进而提高用户粘性。这些时间(体验)成为元宇宙中各项服务的基础。



五、元宇宙时代有哪些确定性趋势?


元宇宙主要的载体(基础设施)主要包括如下几部分


1)网络(通信)


5G作为具有高速率、低时延和大连接特点的新一代宽带移动通信技术,是实现人机物互联的网络基础设施。


2)芯片(算力)


元宇宙的内容、网络、区块链、图形显示等功能均需要更为强大的算力。


云端算力方面,DPU芯片(数据处理芯片)通过分流、加速和隔离各种高级网络、存储和安全服务,为云、数据中心或边缘等环境中的各种工作负载提供安全的加速基础设施。


终端算力方面,异构芯片可以让SoC中的CPU、GPU、FPGA、DPU、ASIC等芯片协同工作,不断提升算力以提升用户体验。



3)云与边缘计算


云计算与边缘计算为用户提供所需的计算资源,降低用户触达元宇宙的门槛。


4)AI


AI在元宇宙中应用渗透较广泛。AI可以帮助创建元宇宙资产、艺术品和其他内容(AIGC),并可以改进我们用来构建所有这些内容的软件和流程。


六、元宇宙在产业端如何发挥价值?


全球范围内许多互联网企业、工业软件企业就工业元宇宙的相关技术已有长期的布局,其中包括数字孪生、工业互联网、仿真测试、数字化工厂、CAD、CAE、EDA等工业软件。


1)英伟达:Omniverse平台


Omniverse的架构包括Connect、Nucleus、Kit、Simulation、RTXrenderer等五个部分,他们与第三方数字内容创建工具(DCC)以及基于Omniverse的微服务构成了Omniverse的生态。



黄仁勋在参加Computex2021线上会议表示未来虚拟世界与现实世界将产生交叉融合,元宇宙与NFT将在其中扮演重要角色。其中Omniverse平台主要面向建筑、工程和施工;制造业;媒体和娱乐以及超级计算场景。



根据英伟达官网对于Omniverse平台的介绍,通过Omniverse平台,用户可以完成实时虚拟协作、模拟现实的设计、模拟环境以及搭建未来工厂等操作。


2)微软:数字孪生探索


2021年9月,微软CEO Satya Nadella在Inspire2021演讲中提出全新“企业元宇宙”概念。微软的元宇宙计划中期望元宇宙可以打破现在的通信和业务流程之间的障碍,把他们融合在一起,让工业场景更为便捷。在宣布“企业元宇宙”概念之前,微软就已通过Azure数字孪生及AI等技术建立了工业元宇宙的底座。



Azure数字孪生是一个物联网(IoT)平台,可用于创建真实物品、地点、业务流程和人员的数字表示形式。通用电气航空的数字集团使用Azure数字孪生构建了一个实时和自动演变的模型,因此客户将随时可以访问其飞机的更新、准确和可用的数据模型。并且,通过内置数字可追溯性,可以实时记录每架飞机上每一个实物资产和部件。



3)能科股份:布局仿真与测试


能科股份主要业务包括智能制造、智能电气两个板块,其中公司智能制造业务基于数字孪生理念,整合业内先进工业软件和数字化IOT设备,虚拟世界内定义生产力中台并为客户开发个性化的工业微应用,物理世界内建立数字化、智能化的生产线和测试台,满足制造业企业产品全生命周期的数据与业务协同需求,帮助企业实现其自主创新、运营成本、生产效率、不良品率和客户满意度等业务目标。



4)阿里云:数字工厂“新基建”


根据德国工程师协会的定义,数字工厂(DF)是由数字化模型、方法和工具构成的综合网络,包含仿真和3D虚拟现实可视化,通过连续的没有中断的数据管理集成在一起。


阿里云工业互联网平台助力制造企业数字化转型,打造工厂内、供应链、产业平台全面协同的新基建,将工厂的设备、产线、产品、供应链、客户紧密地连接协同起来,为企业提供可靠的基础平台和上层丰富的工业应用,结合全面的产业支撑,助力企业完成数字化转型。



七、元宇宙是否需要去中心化?


1)个人信息可携带权


在个人信息可携带权的时代,用户成为关键参与者,由用户主动发起个人信息数据传输并自行上传,从而实践个人数据可携带权,去中心化成为必不可少的条件。


2)去中心化不等于没有中心、没有监管


在去中心化概念下,仍有较为高级的节点参与治理或运营,这与分布式架构完全舍弃中心的概念不同。在去中心化概念下,有效的监管和治理仍可存在。



3)去中心化如何践行?参考DAO


去中心化自治组织(DecentralizedAutonomousOrganization,DAO)是基于区块链核心思想理念,由达成同一个共识的群体自发产生的共创、共建、共治、共享的协同行为衍生出来的一种组织形态,是区块链解决信任问题后的附属产物。


DAO将组织的管理和运营规则以智能合约的形式编码在区块链上,从而在没有集中控制或第三方干预的情况下自主运行。DAO具有充分开放、自主交互、去中心化控制、复杂多样以及涌现等特点,可成为应对不确定、多样、复杂环境的有效组织。


八、元宇宙是否需要NFT?


1)什么是NFT?区块链的主流资产之一。


NFT代表不可替代的代币,是可以用来表示独特物品所有权的代币。NFT让艺术品、收藏品甚至房地产等事物标记化。他们一次只能拥有一个正式所有者,并且他们受到以太坊等区块链的保护,没有人可以修改所有权记录或复制/粘贴新的NFT。


NFT具有不可互换性、独特性、不可分性、低兼容性以及物品属性,可应用于流动性挖矿、艺术品交易、游戏/VR以及链下资产NFT化等场景,大幅提升数据流转效率。



2)NFT应用:一种潜在的元宇宙经济模式


NFT由于自身的数字稀缺性被率先运用于收藏、艺术品以及游戏场景。51aspx认为,NFT在元宇宙中将扮演关键角色。


九、元宇宙时代,互联网形态是否会发生变化?


51aspx认为,基于元宇宙的发展,互联网的协议可能发生改变,互联网会针对于打造可信化的数字底座进行演进。而区块链技术也在攻克自身的缺陷:交易吞吐量低、与外界沟通困难等。


未来会发展成什么样还是未知的,但是目前的理念技术还是很值得我们去探讨。关于相应的源码可以点击查看51aspx.com


文章系转载:wallstreetcn.com/articles/36…


设计排版:51aspx.com


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

掀起的“元宇宙”热潮,能给我们带来什么?

近日超火的“元宇宙”,被说成是互联网的未来,它以各种姿势吸金,让创业族们看到闪着金光的未来。然而,关于元宇宙究竟是什么? 小编相信大多数人都有听过这一热词,但是都不清楚它究竟是什么? 那么,元宇宙究竟是什么呢?为什么互联网科技和数字科技等各大巨头纷纷入局元宇宙...
继续阅读 »

近日超火的“元宇宙”,被说成是互联网的未来,它以各种姿势吸金,让创业族们看到闪着金光的未来。然而,关于元宇宙究竟是什么? 小编相信大多数人都有听过这一热词,但是都不清楚它究竟是什么?


那么,元宇宙究竟是什么呢?为什么互联网科技和数字科技等各大巨头纷纷入局元宇宙?


AR.png 


准确来说,元宇宙到目前为止,尚无公认的定义 ,因为元宇宙不是一个新的概念,它算是一个原有的经典概念的重组词,是在扩展现实(XR)、区块链、云计算、数字孪生等新技术下的概念具化。同时也是集互联网、物联网、大数据、5G等为一体的运用,是虚拟世界与现实世界的结合,元宇宙的运用在未来会有很大的发展空间。


比特币.png


 


继续深入发掘,会了解到元宇宙一词诞生于1992年出版的科幻小说《雪崩》,小说里提到了“Met aver se(元宇宙)”和“Avat ar(化身)”两个概念。在小说描绘的虚拟世界里,人们拥有自己的虚拟替身,这个虚拟世界就叫元宇宙。当然,关于元宇宙的争论还在继续,而且我们能够从不同的角度分析能够得出差异性极大的结论报告,但是关于元宇宙的特征情况已获得了业界的认可。


在今年8月份以来,元宇宙概念火遍全球,甚至日本社交巨头GREE宣布将开展元宇宙业务、英伟达发布会上出场了十几秒的“数字替身”、微软在Inspire全球合作伙伴大会上宣布了企业元宇宙解决方案等


但实际上,不仅有各大科技巨头在争相布局元宇宙赛道,同时还有一些国家的政府相关部门也积极参与其中。5月18日,韩国科学技术和信息通信部发起成立了以现代、SK集团、LG集团等200多家韩国本土企业和组织的名叫“元宇宙联盟”的组织,其目的是想要打造国家级别以及增强线下现实平台,并在未来向社会提供公共虚拟服务;今年都的7月13日,日本发布了《关于虚拟空间行业未来可能性与课题的调查报告》,总结了日本虚拟空间行业相关问题,同时也期望能够占据主导地位在全球虚拟空间行业里;今年8月31日,韩国财政部发布了下一年的预算情况报告,计划斥资2000万美元用于元宇宙平台开发。


以上种种报告显示,元宇宙深受科技巨头、政府部门的青睐,那么究竟是什么原因元宇宙可以做到“人见人爱”的这种现象呢?


从企业的角度来看,目前还处于初级阶段的元宇宙,无论是基础的技术还是基本的应用场景,与未来的成熟形态相比较还是有一定的差距,但这也意味着元宇宙相关产业可拓展的空间巨大。 因此,如果想要守住市场,数字科技领域初创企业要获得超速的机会,就要学会提前布局,甚至还要加强马力。


从政府层面来看,元宇宙不仅代表着重要的新兴产业,同时也是需要被重视的社会会治理领域。 伴随着元宇宙这一新兴产业的不断发展,随之而来的将会是一系列的革命性发展和挑战。


元宇宙.png 


同时,元宇宙也算一个具有极致开放的、复杂且巨大的系统,它涵盖了全部的网络空间、多个基础的硬件设备和最为基本的现实条件,是由多类型建设者共同构建的超大型数字科学技术的应用生态。


因此,我们再来从技术的角度来看,技术局限性是元宇宙目前发展的最大瓶颈,XR、区块链、人工智能等相应底层技术距离元宇宙落地应用的需求仍有较大差距。需要做大量的基础研究才能支撑元宇宙产业的成熟。对此,我们不仅要谨防元宇宙成为炒作噱头,还要鼓励各大相关的企业更进一步加强技术创新能力提高产业技术的成熟度。


BI“元宇宙”也是人们热议的话题,大数据有望最先受益于元宇宙放量,就以国内著名BI厂商Smartbi为例,Smartbi持续完善数据产业链,以“Smartb一站式大数据分析平台”为核心引擎,打造大数据运营管理能力,拓展场景应用,加速企业数据化转型。


数据分析.png


数据可视化,简单来说就是将一种将相对复杂的表达形式、抽象的数据通过可视化转化为更容易理解的图形显示的一种形式。数据可视化可以更为生动形象地展现出数据所表达的内在价值,同时也十分方便企业利用数据智能更好地开展业务。 关于数据可视化,Smartbi支持完整的ECharts图库及多种图形,包括瀑布、热力图、树图等数l十种动态交互的图形,可视化功能十分地灵活,同时页面也展现的十分清晰明了,该功能也深受客户喜爱。


数据安全.png


 


数据安全是每个企业的重中之重,我们这里说的数据安全是狭义的,指的是采用现代信息存储手段对数据进行主动防护。 Smartbi在数据的收集、存储、使用、加工、传输、公开等各个环节,都提供了安全、可控的保护手段。其安全管理体系就是通过对多种权限进行控制,从而保障了数据资源的使用安全。另外还提供了定期备份、水印设置、分享设置等功能,大大降低了数据破坏、外泄的风险。


最后,我们可以肯定的是,在技术的不断提高进步和人类不断增多的生活需求的共同推动下,元宇宙场景的实现,元宇宙产业的成熟,只是一个时间问题。它对于我们之后的发展,无论是经济方面还是其他方面都是有着巨大的机遇和革命性作用


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

背包问题_概述(动态规划)

写在前 问题描述 注意:0-1 背包问题无法使用贪心算法来求解,也就是说不能按照先添加性价比最高的物品来达到最优,这是因为这种方式可能造成背包空间的浪费,从而无法达到最优。基本思路 第 i 件物品没添加到背包,最大价值:dp[i][j] = dp[i - 1]...
继续阅读 »




写在前

问题描述

有N件物品和一个最多能被重量为W 的背包。一个物品只有两个属性:重量和价值。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。

注意:0-1 背包问题无法使用贪心算法来求解,也就是说不能按照先添加性价比最高的物品来达到最优,这是因为这种方式可能造成背包空间的浪费,从而无法达到最优。

基本思路

这里有两个可变量体积和价值,我们定义dp[i][j]表示前i件物品体积不超过j能达到的最大价值,设第 i 件物品体积为 w,价值为 v,根据第 i 件物品是否添加到背包中,可以分两种情况讨论:

  • 第 i 件物品没添加到背包,最大价值:dp[i][j] = dp[i - 1][j]

  • 第 i 件物品添加到背包中:dp[i][j] = dp[i - 1][j - w] + v

第 i 件物品可添加也可以不添加,取决于哪种情况下最大价值更大。因此,0-1 背包的状态转移方程为: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - w] + v)

代码实现

// W 为背包总重量
// N 为物品数量
// weights 数组存储 N 个物品的重量
// values 数组存储 N 个物品的价值
public int knapsack(int W, int N, int[] weights, int[] values) {
   // dp[i][0]和dp[0][j]没有价值已经初始化0
   int[][] dp = new int[N + 1][W + 1];
   // 从dp[1][1]开始遍历填表
   for (int i = 1; i <= N; ++i) {
       // 第i件物品的重量和价值
       int w = weights[i - 1], v = values[i - 1];
       for (int j = 1; j <= W; ++j) {
           if (j < w) {
               // 超过当前状态能装下的重量j
               dp[i][j] = dp[i - 1][j];
          } else {
               dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weights[i]] + values[i]);
          }
      }
  }
   return dp[N][W];
}

dp[i][j]的值只与dp[i-1][0,...,j-1]有关,所以我们可以采用动态规划常用的方法(滚动数组)对空间进行优化(即去掉dp的第一维)。因此,0-1 背包的状态转移方程为: dp[j] = max(dp[j], dp[j - w] + v)

特别注意:为了防止上一层循环的dp[0,...,j-1]被覆盖,循环的时候 j 只能逆向遍历。优化空间复杂度:

ps:滚动数组:即让数组滚动起来,每次都使用固定的几个存储空间,来达到压缩,节省存储空间的作用。

public int knapsack(int W, int N, int[] weights, int[] values) {
   int[] dp = new int[W + 1];
   for (int i = 1; i <= N; i++) {
       int w = weights[i - 1], v = values[i - 1];
       for (int j = W; j >= 1; --j) {
           if (j >= w) {
               dp[j] = Math.max(dp[j], dp[j - w] + v);
          }
      }
  }
   return dp[W];
}

ps:01背包内循环理解:还原成二维的dp就很好理解,一维的dp是二维dp在空间上进行复用的结果。dp[i]=f(dp[i-num]),等式的右边其实是二维dp上一行的数据,应该是只读的,在被读取前不应该被修改。如果正序的话,靠后的元素在读取前右边的dp有可能被修改了,倒序可以避免读取前被修改的问题。

作者:_code_x
来源:https://www.jianshu.com/p/b789ec845641

收起阅读 »

Android端小到不行的分页加载库

RecyclerView几乎在每个app里面都有被使用,但凡使用了列表就会采用分页加载进行数据请求和加载。android 官方也推出了分页库,但是感觉只有kotlin一起使用才能体会到酸爽。Java 版本的也有很多很强大的第三方库,BaseRecyclerVi...
继续阅读 »

RecyclerView几乎在每个app里面都有被使用,但凡使用了列表就会采用分页加载进行数据请求和加载。android 官方也推出了分页库,但是感觉只有kotlin一起使用才能体会到酸爽。Java 版本的也有很多很强大的第三方库,BaseRecyclerViewAdapterHelper这个库是我用起来最顺手的分页库,里面也包含了各式各样强大的功能:分组、拖动排序、动画,因为功能强大,代码量也相对比较大。 但是很多时候我们想要的就是分页加载,所以参照BaseRecyclerViewAdapterHelper写下了这个分页加载库,只有分页功能。(可以说照搬,也可以说精简,但是其中也加入个人理解)。
这个库相对BaseRecyclerViewAdapterHelper只有两个优点:

  • 代码量小

  • BaseRecyclerViewAdapterHelper 在数据不满一屏时仍然显示加载更多以及页面初始化时都会显示loadmoewView(虽然提供了api进行隐藏,但是看了很长时间注释和文档都没了解该怎么使用),而这个库在初次加载和不满一屏数据时不会显示loadmoreView

gradle引用

implementation 'com.maxcion:pageloadadapter:1.0.0'

项目地址:https://github.com/Likeyong/PageLoadAdapter

如果看不到gif,就到掘金吧 https://juejin.cn/post/6944242618658193415

单列分页加载

混合布局分页加载

Recyclerview 多type分页加载

单列分页加载

//一定要在PageLoadRecyclerVewAdapter<String> 的泛型参数里面指定数据源item格式
public class SimpleAdapter extends PageLoadRecyclerVewAdapter<String> {
   public SimpleAdapter(List<String> dataList) {
       super(dataList);
  }

   //这里进行 数据绑定
   @Override
   protected void convert(BaseViewHolder holder, String item) {
       holder.setText(R.id.text, item);
  }

   //这里返回布局item id
   @Override
   protected int getItemLayoutId() {
       return R.layout.item_simple;
  }
}

第一步 adapter实现好了,现在需要打开adapter的分页加载功能

public class SingleColumnActivity extends BaseActivity<String> implements IOnLoadMoreListener {


   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_single_column);
       RecyclerView rv = findViewById(R.id.rv);
       //实例化adapter
       mAdapter = new SimpleAdapter(null);
       //给adapter 设置loadmoreview
       mAdapter.setLoadMoreView(new CommonLoadMoreView());
       //设置滑动到底部时进行更多加载的回调
       mAdapter.setOnLoadMoreListener(this);
       rv.setAdapter(mAdapter);
       rv.setLayoutManager(new LinearLayoutManager(this));
       request();
  }



   @Override
   public void onLoadMoreRequested() {

       request();
  }

   //这个函数不用管
   @Override
   protected List<String> convertRequestData(List<String> originData) {
       return originData;
  }


}

第二步,RecyclerView也打开了分页加载功能,第三部就是根据接口返回的数据判断到底是 加载失败了、加成成功了还是加载结束(没有更多数据需要加载)

protected void request() {
       NetWorkRequest.request(mAdapter.getDataSize() / PAGE_SIZE + 1, mFailCount, new NetWorkRequest.Callback() {
           @Override
           public void onSuccess(List<String> result) {
               List<T> finalResult = convertRequestData(result);
               if(result.size() >= PAGE_SIZE){// 接口返回了满满一页的数据,这里数据加载成功
                   if (mAdapter.getDataSize() == 0){
                       //当前列表里面没有数据,代表是初次请求,所以这里使用setNewData()

                       mAdapter.setNewData(finalResult);
                  }else {
                       //列表里面已经有数据了,这里使用addDataList(),将数据添加到列表后面
                       mAdapter.addDataList(finalResult);
                  }
                   //这里调用adapter。loadMoreComplete(true) 函数通知列表刷新footview, 这里参数一定要传true
                   mAdapter.loadMoreComplete(true);
              }else {
                   //如果接口返回的数据不足一页,也就代表没有足够的数据了,那么也就没有下一页数据,所以这里
                   //认定分页加载结束
                   //这里的参数也一定要传true
                   mAdapter.loadMoreEnd(true);
              }
          }

           @Override
           public void onFail() {
               mFailCount++;
               //请求失败 通知recyclerview 刷新footview 状态
               mAdapter.loadMoreFail(true);
          }
      });
  }

上面是我写的模拟接口请求,不用在意其他代码,只要关注onSuccess 和onFail 两个回调里面的逻辑。

混合布局的支持

在电商行业经常能看到商品列表中,同一个列表,有的商品占满整整一行,有的一行显示2-3个商品。这种实现方案就是通过GridLayoutManager 的SpanSizeLookup 来控制每个item占几列的。

RecyclerView rv = findViewById(R.id.rv);
       mAdapter = new SimpleAdapter(null);
       mAdapter.setLoadMoreView(new CommonLoadMoreView());
       mAdapter.setOnLoadMoreListener(this);
     //这里我们将列表设置最多两列
       GridLayoutManager layoutManager = new GridLayoutManager(this, 2);
       layoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
           @Override
           public int getSpanSize(int position) {
             //根据position 设置每个item应该占几列
             //如果当前的position是3的整数倍 我们就让他占满2列,其他的只占1列
               return position % 3 == 0 ? 2 : 1 ;
          }
      });
       rv.setLayoutManager(layoutManager);
       rv.setAdapter(mAdapter);

RecyclerView多Type支持

如果要使用多type, 在写Adapter的时候要继承PageLoadMultiRecyclerViewAdapter<T, BaseViewHolder>,其中T 是数据源item类型,这个类型必须实现 IMultiItem 接口,并在getItemType()函数中返回当前item对应的type

public class MultiPageLoadAdapter extends PageLoadMultiRecyclerViewAdapter<MultiData, BaseViewHolder> {
   public MultiPageLoadAdapter(List<MultiData> dataList) {
       super(dataList);
       //构造函数里面将 每种type 和 type 对应的布局进行绑定
       addItemLayout(MultiData.TYPE_TEXT, R.layout.item_simple);
       addItemLayout(MultiData.TYPE_IMAGE, R.layout.item_multi_image);
       addItemLayout(MultiData.TYPE_VIDEO, R.layout.item_multi_video);
  }

   @Override
   protected void convert(BaseViewHolder holder, MultiData item) {
       //在convert中针对不同的type 进行不同的bind逻辑
       switch (holder.getItemViewType()){
           case MultiData.TYPE_VIDEO:
               holder.setText(R.id.text, item.content);
               break;

           case MultiData.TYPE_IMAGE:
               holder.setText(R.id.text, item.content);
               break;

           case MultiData.TYPE_TEXT:
               holder.setText(R.id.text, item.content);
           default:
               break;
      }
  }
}

引入方式也和上面两种方式一样

RecyclerView recyclerView = findViewById(R.id.rv);
       mAdapter = new MultiPageLoadAdapter(null);
       mAdapter.setLoadMoreView(new CommonLoadMoreView());
       mAdapter.setOnLoadMoreListener(this);
       recyclerView.setLayoutManager(new LinearLayoutManager(this));
       recyclerView.setAdapter(mAdapter);

作者:maxcion
来源:https://www.jianshu.com/p/aa3054b4d03c

收起阅读 »

12个有用的JavaScript数组技巧

数组是Javascript最常见的概念之一,它为我们提供了处理数据的许多可能性,熟悉数组的一些常用操作是很有必要的。1、数组去重1、from()叠加new Set()方法字符串或数值型数组的去重可以直接使用from方法。var plants = ['Satur...
继续阅读 »

数组是Javascript最常见的概念之一,它为我们提供了处理数据的许多可能性,熟悉数组的一些常用操作是很有必要的。

1、数组去重

1、from()叠加new Set()方法

字符串或数值型数组的去重可以直接使用from方法。

var plants = ['Saturn', 'Earth', 'Uranus', 'Mercury', 'Venus', 'Earth', 'Mars', 'Jupiter'];
var uniquePlants = Array.from(new Set(plants));
console.log(uniquePlants); // [ 'Saturn', 'Earth', 'Uranus', 'Mercury', 'Venus', 'Mars', 'Jupiter' ]

2、spread操作符(…)

扩展运算符是ES6的一大创新,还有很多强大的功能。

var plants = ['Saturn', 'Earth', 'Uranus', 'Mercury', 'Venus', 'Earth', 'Mars', 'Jupiter'];
var uniquePlants = [...new Set(plants)];
console.log(uniquePlants); // [ 'Saturn', 'Earth', 'Uranus', 'Mercury', 'Venus', 'Mars', 'Jupiter' ]

2、替换数组中的特定值

splice() 方法向/从数组中添加/删除项目,然后返回被删除的项目。该方法会改变原始数组。特别需要注意插入值的位置!

// arrayObject.splice(index,howmany,item1,.....,itemX)

var plants = ['Saturn', 'Uranus', 'Mercury', 'Venus', 'Earth', 'Mars', 'Jupiter'];
var result = plants.splice(2, 1, 'www.shanzhonglei.com')
console.log(plants); // ['Saturn','Uranus','www.shanzhonglei.com','Mercury','Venus','Earth','Mars','Jupiter']
console.log(result); // ['Mercury']

3、没有map()的映射数组

我们先介绍一下map方法。map()方法返回一个新数组,数组中的元素为原始数组元素调用函数处理后的值,它会按照原始数组元素顺序依次处理元素。注意: map()不会改变原始数组,也不会对空数组进行检测。
下面我们来实现一个没有map的数组映射:

// array.map(function(currentValue,index,arr), thisValue)

var plants = [
  { name: "Saturn" },
  { name: "Uranus" },
  { name: "Mercury" },
  { name: "Venus" },
]
var plantsName = Array.from(plants, ({ name }) => name);
console.log(plantsName); // [ 'Saturn', 'Uranus', 'Mercury', 'Venus' ]

4、空数组

如果要清空一个数组,将数组的长度设置为0即可,额,这个有点简单。

var plants = ['Saturn', 'Earth', 'Uranus', 'Mercury', 'Venus', 'Earth', 'Mars', 'Jupiter'];
plants.length = 0;
console.log(plants); // []

5、将数组转换为对象

如果要将数组转换为对象,最快的方法莫过于spread运算符(…)。

var plants = ['Saturn', 'Earth', 'Uranus', 'Mercury', 'Venus', 'Earth', 'Mars', 'Jupiter'];
var plantsObj = {...plants }
console.log(plantsObj); // {'0': 'Saturn','1': 'Earth', '2': 'Uranus','3': 'Mercury','4': 'Venus','5': 'Earth','6': 'Mars','7': 'Jupiter'}

6、用数据填充数组

如果我们需要用一些数据来填充数组,或者需要一个具有相同值的数据,我们可以用fill()方法。

var plants = new Array(8).fill('8');
console.log(plants); // ['8', '8', '8','8', '8', '8','8', '8']

7、合并数组

当然你会想到concat()方法,但是哦,spread操作符(…)也很香的,这也是扩展运算符的另一个应用。

var plants1 = ['Saturn', 'Earth', 'Uranus', 'Mercury'];
var plants2 = ['Venus', 'Earth', 'Mars', 'Jupiter'];
console.log([...plants1, ...plants2]); // ['Saturn', 'Earth','Uranus', 'Mercury','Venus', 'Earth','Mars', 'Jupiter']

8、两个数组的交集

要求两个数组的交集,首先确保数组不重复,然后使用filter()方法和includes()方法。

var plants1 = ['Saturn', 'Earth', 'Uranus', 'Mercury', 'Venus', 'Earth', 'Mars', 'Jupiter'];
var plants2 = ['Saturn', 'Earth', 'Uranus'];
var alonePlants = [...new Set(plants1)].filter(item => plants2.includes(item));
console.log(alonePlants); // [ 'Saturn', 'Earth', 'Uranus' ]

9、删除数组中的假值

我们时常需要在处理数据的时候要去掉假值。在Javascript中,假值是false, 0, " ", null, NaN, undefined。

var plants = ['Saturn', 'Earth', null, undefined, false, "", NaN, 'Uranus', 'Mercury', 'Venus', 'Earth', 'Mars', 'Jupiter'];
var trueArr = plants.filter(Boolean);
console.log(trueArr); // ['Saturn', 'Earth','Uranus', 'Mercury','Venus', 'Earth','Mars', 'Jupiter']

10、获取数组中的随机值

我们可以根据数组长度获得一个随机索引号。

var plants = ['Saturn', 'Earth', 'Uranus', 'Mercury', 'Venus', 'Earth', 'Mars', 'Jupiter'];
console.log(plants[Math.floor(Math.random() * (plants.length + 1))])

11、lastIndexOf()方法

lastIndexOf()可以帮助我们查找元素最后一次出现的索引。

var plants = ['Saturn', 'Earth', 'Uranus', 'Mercury', 'Venus', 'Earth', 'Mars', 'Jupiter'];
console.log(plants.lastIndexOf('Earth')) // 5

12、将数组中的所有值相加

reduce()方法接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终计算为一个值。

// array.reduce(function(total, currentValue, currentIndex, arr), initialValue)

var nums = [1, 2, 3, 4, 5];
var sum = nums.reduce((x, y) => x + y);
console.log(sum); // 15

作者:前端技术驿站
来源:https://www.jianshu.com/p/651338c88bb4

收起阅读 »

字节面试被虐后,是时候搞懂 DNS 了

前几天面了字节 👦🏻:“浏览器从输入URL到显示页面发生了什么?” 👧🏻:%^&@#^&(这我怎么可能没有准备?从网络到渲染说了一通后) 👦🏻:“你刚刚提到了 DNS,那说说 DNS 的查询过程吧” 👧🏻:“DNS 查询是一个递归 + 迭代的...
继续阅读 »

前几天面了字节



👦🏻:“浏览器从输入URL到显示页面发生了什么?”


👧🏻:%^&@#^&(这我怎么可能没有准备?从网络到渲染说了一通后)


👦🏻:“你刚刚提到了 DNS,那说说 DNS 的查询过程吧”


👧🏻:“DNS 查询是一个递归 + 迭代的过程...”


👦🏻:“那具体的递归和迭代过程是怎样的呢?”


👧🏻:“...”



当时我脑子里有个大概的过程,但是细节就记不起来了,所以今天就来梳理一下 DNS 相关的内容,如有不妥之处,还望大家指出。


什么是 DNS


DNS 即域名系统,全称是 Domain Name System。当我们在浏览器输入一个 URL 地址时,浏览器要向这个 URL 的主机名对应的服务器发送请求,就得知道服务器的 IP,对于浏览器来说,DNS 的作用就是将主机名转换成 IP 地址。下面是摘自《计算机网络:自顶向下方法》的概念:



DNS 是:



  1. 一个由分层的 DNS 服务器实现的分布式数据库

  2. 一个使得主机能够查询分布式数据库的应用层协议



也就是,DNS 是一个应用层协议,我们发送一个请求,其中包含我们要查询的主机名,它就会给我们返回这个主机名对应的 IP;


其次,DNS 是一个分布式数据库,整个 DNS 系统由分散在世界各地的很多台 DNS 服务器组成,每台 DNS 服务器上都保存了一些数据,这些数据可以让我们最终查到主机名对应的 IP。


所以 DNS 的查询过程,说白了,就是去向这些 DNS 服务器询问,你知道这个主机名的 IP 是多少吗,不知道?那你知道去哪台 DNS 服务器上可以查到吗?直到查到我想要的 IP 为止。


分布式、层次数据库


什么是分布式?

这个世界上没有一台 DNS 服务器拥有因特网上所有主机的映射,每台 DNS 只负责部分映射。


什么是层次?

DNS 服务器有 3 种类型:根 DNS 服务器、顶级域(Top-Level Domain, TLD)DNS 服务器和权威 DNS 服务器。它们的层次结构如下图所示:



DNS 的层次结构.jpeg



图片来源:《计算机网络:自顶向下方法》



  • 根 DNS 服务器


首先我们要明确根域名是什么,比如 http://www.baidu.com,有些同学可能会误以为 com 就是根域名,其实 com 是顶级域名,http://www.baidu.com 的完整写法是 http://www.baidu.com.,最后的这个 . 就是根域名。


根 DNS 服务器的作用是什么呢?就是管理它的下一级,也就是顶级域 DNS 服务器。通过询问根 DNS 服务器,我们可以知道一个主机名对应的顶级域 DNS 服务器的 IP 是多少,从而继续向顶级域 DNS 服务器发起查询请求。



  • 顶级域 DNS 服务器


除了前面提到的 com 是顶级域名,常见的顶级域名还有 cnorgedu 等。顶级域 DNS 服务器,也就是 TLD,提供了它的下一级,也就是权威 DNS 服务器的 IP 地址。



  • 权威 DNS 服务器


权威 DNS 服务器可以返回主机 - IP 的最终映射。


关于这几个层次的服务器之间是怎么交互的,接下来我们会讲到 DNS 具体的查询过程,结合查询过程,大家就不难理解它们之间的关系了。


本地 DNS 服务器


之前对 DNS 有过了解的同学可能会发现,上一节的 DNS 层次结构,为什么没有提到本地 DNS 服务器?因为严格来说,本地 DNS 服务器并不属于 DNS 的层次结构,但它对 DNS 层次结构是至关重要的。那什么是本地 DNS 服务器呢?


每个 ISP 都有一台本地 DNS 服务器,比如一个居民区的 ISP、一个大学的 ISP、一个机构的 ISP,都有一台或多台本地 DNS 服务器。当主机发出 DNS 请求时,该请求被发往本地 DNS 服务器,本地 DNS 服务器起着代理的作用,并负责将该请求转发到 DNS 服务器层次结构中。


接下来就让我们通过一个简单的例子,看看 DNS 的查询过程是怎样的,看看客户端、本地 DNS 服务器、DNS 服务器层次结构之间是如何交互的。


递归查询、迭代查询


如下图,假设主机 m.n.com 想要获取主机 a.b.com 的 IP 地址,会经过以下几个步骤:



DNS.png





  1. 首先,主机 m.n.com 向它的本地 DNS 服务器发送一个 DNS 查询报文,其中包含期待被转换的主机名 a.b.com




  2. 本地 DNS 服务器将该报文转发到根 DNS 服务器;




  3. 该根 DNS 服务器注意到 com 前缀,便向本地 DNS 服务器返回 com 对应的顶级域 DNS 服务器(TLD)的 IP 地址列表。


    意思就是,我不知道 a.b.com 的 IP,不过这些 TLD 服务器可能知道,你去问他们吧;




  4. 本地 DNS 服务器则向其中一台 TLD 服务器发送查询报文;




  5. 该 TLD 服务器注意到 b.com 前缀,便向本地 DNS 服务器返回权威 DNS 服务器的 IP 地址。


    意思就是,我不知道 a.b.com 的 IP,不过这些权威服务器可能知道,你去问他们吧;




  6. 本地 DNS 服务器又向其中一台权威服务器发送查询报文;




  7. 终于,该权威服务器返回了 a.b.com 的 IP 地址;




  8. 本地 DNS 服务器将 a.b.com 跟 IP 地址的映射返回给主机 m.n.comm.n.com 就可以用该 IP 向 a.b.com 发送请求啦。





bqb4.jpeg



“你说了这么多,递归呢?迭代呢?”


这位同学不要捉急,其实递归和迭代已经包含在上述过程里了。


主机 m.n.com 向本地 DNS 服务器 dns.n.com 发出的查询就是递归查询,这个查询是主机 m.n.com 以自己的名义向本地 DNS 服务器请求想要的 IP 映射,并且本地 DNS 服务器直接返回映射结果给到主机。


而后继的三个查询是迭代查询,包括本地 DNS 服务器向根 DNS 服务器发送查询请求、本地 DNS 服务器向 TLD 服务器发送查询请求、本地 DNS 服务器向权威 DNS 服务器发送查询请求,所有的请求都是由本地 DNS 服务器发出,所有的响应都是直接返回给本地 DNS 服务器


那么问题来了,所有的 DNS 查询都必须遵循这种递归 + 迭代的模式吗?


当然不是。


从理论上讲,任何 DNS 查询既可以是递归的,也可以是迭代的。下图的所有查询就都是递归的,不包含迭代。



DNS2.png



看到这里,大家可能会有个疑问,TLD 一定知道权威 DNS 服务器的 IP 地址吗?


emmm...



bqb7.png



还真不一定,有时 TLD 只是知道中间的某个 DNS 服务器,再由这个中间 DNS 服务器去找到权威 DNS 服务器。这种时候,整个查询过程就需要更多的 DNS 报文。


DNS 缓存


为了让我们更快的拿到想要的 IP,DNS 广泛使用了缓存技术。DNS 缓存的原理非常简单,在一个 DNS 查询的过程中,当某一台 DNS 服务器接收到一个 DNS 应答(例如,包含某主机名到 IP 地址的映射)时,它就能够将映射缓存到本地,下次查询就可以直接用缓存里的内容。当然,缓存并不是永久的,每一条映射记录都有一个对应的生存时间,一旦过了生存时间,这条记录就应该从缓存移出。


事实上,有了缓存,大多数 DNS 查询都绕过了根 DNS 服务器,需要向根 DNS 服务器发起查询的请求很少。


面试感想


这次面试收获还蛮大的,有些东西以为自己懂了,以为自己能说清楚,但到了真的要说的时候,又没有办法完整地梳理出来,描述起来磕磕绊绊,在面试中会很减分。


所以不要偷懒,不要抱有侥幸心理,踏实学。共勉。



作者:我是陆小北
链接:https://juejin.cn/post/6990344840181940261

收起阅读 »

H5页面中调用微信和支付宝支付

最近在工作中,有个H5页面需要实现微信支付和支付宝支付的功能,现在已经完成,抽个时间写出来,分享给有需要的人。 第一步:先判断当前环境 判断用户所属环境,根据环境不同,执行不同的支付程序。 if (/MicroMessenger/.test(window.na...
继续阅读 »

最近在工作中,有个H5页面需要实现微信支付和支付宝支付的功能,现在已经完成,抽个时间写出来,分享给有需要的人。


第一步:先判断当前环境


判断用户所属环境,根据环境不同,执行不同的支付程序。


if (/MicroMessenger/.test(window.navigator.userAgent)) {
// alert('微信');
} else if (/AlipayClient/.test(window.navigator.userAgent)) {
//alert('支付宝');
} else {
//alert('其他浏览器');
}

第二步:如果是微信环境,需要先进行网页授权


网页授权的详细介绍可以查看微信相关文档。这里不做介绍。


第三步:


1、微信支付


微信支付有两种方法:
1:调用微信浏览器提供的内置接口WeixinJSBridge
2:引入微信jssdk,使用wx.chooseWXPay方法,需要先通过config接口注入权限验证配置。
我这里使用的是第一种,在从后台拿到签名、时间戳这些数据后,直接调用微信浏览器提供的内置接口WeixinJSBridge即可完成支付功能。


getRequestPayment(data) {
function onBridgeReady() {
WeixinJSBridge.invoke(
"getBrandWCPayRequest", {
"appId": data.appId, //公众号ID,由商户传入
"timeStamp": data.timeStamp, //时间戳,自1970年以来的秒数
"nonceStr": data.nonceStr, //随机串
"package": data.package,
"signType": data.signType, //微信签名方式:
"paySign": data.paySign //微信签名
},
function(res) {
alert(JSON.stringify(res));
// get_brand_wcpay_request
if (res.err_msg == "get_brand_wcpay_request:ok") {
// 使用以上方式判断前端返回,微信团队郑重提示:
//res.err_msg将在用户支付成功后返回ok,但并不保证它绝对可靠。
}
}
);
}
if (typeof WeixinJSBridge == "undefined") {
if (document.addEventListener) {
document.addEventListener(
"WeixinJSBridgeReady",
onBridgeReady,
false
);
} else if (document.attachEvent) {
document.attachEvent("WeixinJSBridgeReady", onBridgeReady);
document.attachEvent("onWeixinJSBridgeReady", onBridgeReady);
}
} else {
onBridgeReady();
}
},

2、支付宝支付


支付宝支付相对于微信来说,前端这块工作更简单 ,后台会返回给前端一个form表单,我们要做的就是把这个表单进行提交即可。相关代码如下:


this.$api.alipayPay(data).then((res) => {
// console.log('支付宝参数', res.data)
if (res.code == 200) {
var resData =res.data
const div = document.createElement('div')
div.id = 'alipay'
div.innerHTML = resData
document.body.appendChild(div)
document.querySelector('#alipay').children[0].submit() // 执行后会唤起支付宝
}

}).catch((err) => {
})

作者:故友
链接:https://juejin.cn/post/7034033584684204068

收起阅读 »

Android系统启动-Zygote进程

本篇文章基于Android6.0源码分析 相关源码文件: /system/core/rootdir/init.rc /system/core/rootdir/init.zygote64.rc /frameworks/base/cmds/app_proces...
继续阅读 »

本篇文章基于Android6.0源码分析



相关源码文件:


/system/core/rootdir/init.rc
/system/core/rootdir/init.zygote64.rc

/frameworks/base/cmds/app_process/App_main.cpp
/frameworks/base/core/jni/AndroidRuntime.cpp

/frameworks/base/core/java/com/android/internal/os/
- ZygoteInit.java
- Zygote.java
- ZygoteConnection.java

/frameworks/base/core/java/android/net/LocalServerSocket.java
/system/core/libutils/Threads.cpp

Zygote进程启动前的概述


通过init.rc的文件解析会启动zygote相关的服务从而启动zygote进程。通过import导入决定启动哪种类型的zygote服务脚本,这里分为32位和64位架构的zygote服务脚本


import /init.${ro.zygote}.rc

在/system/core/rootdir目录中有四个zygote相关的服务脚本


init.zygote32.rc // 支持32位的zygote
init.zygote32_64.rc // 即支持32位也支持64位的zygote,其中以32位为主,64位为辅
init.zygote64.rc // 支持64位的zygote
init.zygote64_32.rc // 即支持64位也支持32位的zygote,其中以64位为主,32位为辅


下面我们分析只分析64位的zygote服务脚本的Android初始化语言:


service zygote /system/bin/app_process64 -Xzygote /system/bin --zygote --start-system-server
class main
socket zygote stream 660 root system
onrestart write /sys/android_power/request_state wake
onrestart write /sys/power/state on
onrestart restart media
onrestart restart netd
writepid /dev/cpuset/foreground/tasks


zygote进程的执行程序为/system/bin/app_process64中,其中参数为:-Xzygote /system/bin --zygote --start-system-server,classname为main 。除了在Init进程解析时创建Zygote进程,在servicemanager、surfaceflinger、systemserver进程被杀时Zygote进程也会进行重启。


其中/system/bin/app_process64的映射的执行文件为:/frameworks/base/cmds/app_process/app_main.cpp


Zygote进程启动


image.png
如图1所示,zygote进程启动时会先启动app_main类的main()方法:


  // 参数argv为 :  -Xzygote /system/bin --zygote --start-system-server
int main(int argc, char* const argv[])
{
// 创建一个AppRuntime实例,AppRuntime 继承 AndoirdRuntime
AppRuntime runtime (argv[0], computeArgBlockSize(argc, argv));
//忽略第一个参数
argc--;
argv++;


// 解析参数并对变量赋值
bool zygote = false;
bool startSystemServer = false;
bool application = false;
String8 niceName;
String8 className;

++i; // Skip unused "parent dir" argument.
while (i < argc) {
const char * arg = argv [i++];
if (strcmp(arg, "--zygote") == 0) {
// 参数中有--zygote
zygote = true;
niceName = ZYGOTE_NICE_NAME;
} else if (strcmp(arg, "--start-system-server") == 0) {
// 参数中有--start-system-server
startSystemServer = true;
} else if (strcmp(arg, "--application") == 0) {
application = true;
} else if (strncmp(arg, "--nice-name=", 12) == 0) {
niceName.setTo(arg + 12);
} else if (strncmp(arg, "--", 2) != 0) {
className.setTo(arg);
break;
} else {
--i;
break;
}
}


if (zygote) {
//如果zygote为true,则调用AndroidRuntime的start方法,并传入了"com.android.internal.os.ZygoteInit"参数
runtime.start("com.android.internal.os.ZygoteInit", args, zygote);
} else if (className) {
runtime.start("com.android.internal.os.RuntimeInit", args, zygote);
} else {
fprintf(stderr, "Error: no class name or --zygote supplied.\n");
app_usage();
LOG_ALWAYS_FATAL("app_process: no class name or --zygote supplied.");
return 10;
}
}

在app_main的mian()方法中,主要是根据zygote的脚本的参数进行解析,在解析到有--zygote字符后,则确定执行AndroidRuntime.start方法,并且第一个参数传为com.android.internal.os.ZygoteInit


AndroidRuntime.start()


在此方法中,主要做了三件事:
· 创建虚拟机实例
· JNI方法的注册
· 调用参数的main()方法


  // 这里的className为:com.android.internal.os.ZygoteInit
void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote)
{

/* start the virtual machine */
JniInvocation jni_invocation;
jni_invocation.Init(NULL);
JNIEnv * env;
// 1. 创建虚拟机
if (startVm(& mJavaVM, &env, zygote) != 0) {
return;
}
onVmCreated(env);
// 2. JNI方法注册
if (startReg(env) < 0) {
ALOGE("Unable to register all android natives\n");
return;
}

// 解析classname参数
//将"com.android.internal.os.ZygoteInit"转换为"com/android/internal/os/ZygoteInit"
char * slashClassName = toSlashClassName(className);
jclass startClass = env->FindClass(slashClassName);

if (startClass == NULL) {
} else {
// 得到ZygoteInit的main方法
jmethodID startMeth = env->GetStaticMethodID(startClass, "main",
"([Ljava/lang/String;)V");
if (startMeth == NULL) {
} else { env ->
// 3. 执行ZygoteInit的main方法
CallStaticVoidMethod(startClass, startMeth, strArray);
}
}
free(slashClassName);

}

对start方法进行了一些删减后,主要是通过startVm 创建虚拟机,通过startReg(env)进行JNI方法注册,最后解析className参数,去执行ZygoteInit.main方法。下面将逐一分析这三种状态。


1. 创建虚拟机实例


startVm


int AndroidRuntime::startVm(JavaVM** pJavaVM, JNIEnv** pEnv, bool zygote)
{
// ...
// JNI检测功能
bool checkJni = false;
property_get("dalvik.vm.checkjni", propBuf, "");
if (strcmp(propBuf, "true") == 0) {
checkJni = true;
} else if (strcmp(propBuf, "false") != 0) {
/* property is neither true nor false; fall back on kernel parameter */
property_get("ro.kernel.android.checkjni", propBuf, "");
if (propBuf[0] == '1') {
checkJni = true;
}
}
ALOGD("CheckJNI is %s\n", checkJni ? "ON" : "OFF");
if (checkJni) {
addOption("-Xcheck:jni");
}

// /虚拟机产生的trace文件,主要用于分析系统问题,路径默认为/data/anr/traces.txt
parseRuntimeOption("dalvik.vm.stack-trace-file", stackTraceFileBuf, "-Xstacktracefile:");

//对于不同的软硬件环境,这些参数往往需要调整、优化,从而使系统达到最佳性能
parseRuntimeOption("dalvik.vm.heapstartsize", heapstartsizeOptsBuf, "-Xms", "4m");
parseRuntimeOption("dalvik.vm.heapsize", heapsizeOptsBuf, "-Xmx", "16m");

parseRuntimeOption(
"dalvik.vm.heapgrowthlimit",
heapgrowthlimitOptsBuf,
"-XX:HeapGrowthLimit="
);
parseRuntimeOption("dalvik.vm.heapminfree", heapminfreeOptsBuf, "-XX:HeapMinFree=");
parseRuntimeOption("dalvik.vm.heapmaxfree", heapmaxfreeOptsBuf, "-XX:HeapMaxFree=");
parseRuntimeOption(
"dalvik.vm.heaptargetutilization",
heaptargetutilizationOptsBuf,
"-XX:HeapTargetUtilization="
);
// ...
// 初始化虚拟机
if (JNI_CreateJavaVM(pJavaVM, pEnv, & initArgs) < 0) {
ALOGE("JNI_CreateJavaVM failed\n");
return -1;
}

return 0;
}


startVm方法里面有很多代码,但主要分为三步,第一步是检测,第二步是软硬件参数的设置,第三步是初始化虚拟机。


2. JNI方法的注册


startReg


  int AndroidRuntime::startReg(JNIEnv* env)
{
androidSetCreateThreadFunc((android_create_thread_fn) javaCreateThreadEtc);

ALOGV("--- registering native functions ---\n");
env->PushLocalFrame(200);
//进程JNI方法的注册
if (register_jni_procs(gRegJNI, NELEM(gRegJNI), env) < 0) { env ->
PopLocalFrame(NULL);
return -1;
}
env->PopLocalFrame(NULL);
return 0;
}

// 这里的array[]是gRegJNI,它是一个映射了很多方法的数组
static int register_jni_procs(const RegJNIRec array[], size_t count, JNIEnv* env)
{
// 执行很多映射的方法
for (size_t i = 0; i < count; i++) {
if (array[i].mProc(env) < 0) {
return -1;
}
}
return 0;
}

startReg方法是对JNI方法的注册,它通过一个有很多宏定义的数组,并执行数组定义的方法,进行对JNI和Java层方法一一映射。


3. 调用ZygoteInit.main方法


在AndroidRuntime.start()方法的最后,通过反射执行了其ZygoteInit.main()方法。


    if (startClass == NULL) {
} else {
// 得到ZygoteInit的main方法
jmethodID startMeth = env->GetStaticMethodID(startClass, "main",
"([Ljava/lang/String;)V");
if (startMeth == NULL) {
} else { env ->
// 3. 执行ZygoteInit的main方法
CallStaticVoidMethod(startClass, startMeth, strArray);
}
}

通过反射去执行ZygoteInit.main方法,也是第一次进入java语言的世界。所以AndroidRuntime的start方法做了三件事,一是初始化虚拟机,二是JNI方法的注册,三是通过反射执行ZygoteInit.main方法。


ZygoteInit.main


Zygote进程用于创建管理framework层的SystemServer进程,还用于创建App进程,就是应用App启动创建进程时,是由Zygote进程创建的,并且Zygote创建子进程将使用copy on write的技术,就是子进程直接继承父进程的现有的资源,在子进程对于共有的资源是读时共享,写时复制
ZygoteInit.main方法中主要做了四件事:
· 注册服务端的socket,用于接收创建子进程的信息
· 提前预加载类和资源,用于子进程共享
· 创建SystemServer进程,其管理着framework层
· 循环监听服务socket,创建子进程


  public static void main(String argv[])
{
try {
// 创建服务端Soctet,用于接收创建子进程信息
registerZygoteSocket(socketName);
// 提前预加载类和资源
preload();
// gc操作
gcAndFinalize();
// 创建SystemServer进程
if (startSystemServer) {
startSystemServer(abiList, socketName);
}
// 用服务socket监听创建进程信息,并创建子进程
runSelectLoop(abiList);

closeServerSocket();
} catch (MethodAndArgsCaller caller) {
caller.run();
} catch (RuntimeException ex) {
Log.e(TAG, "Zygote died with exception", ex);
closeServerSocket();
throw ex;
}
}


通过registerZygoteSocket方法去创建服务端的socket,preload()方法去提前加载类和资源,startSystemServer方法去创建SystemServer进程去管理framework层,runSelectLoop方法循环监听创建子进程。


1. 注册服务端Socket


registerZygoteSocket


  private static void registerZygoteSocket(String socketName)
{
if (sServerSocket == null) {
int fileDesc;
final String fullSocketName = ANDROID_SOCKET_PREFIX + socketName;
try {
String env = System . getenv (fullSocketName);
fileDesc = Integer.parseInt(env);
} catch (RuntimeException ex) {
throw new RuntimeException (fullSocketName + " unset or invalid", ex);
}

try {
// 创建服务端的socket
FileDescriptor fd = new FileDescriptor();
fd.setInt$(fileDesc);
sServerSocket = new LocalServerSocket (fd);
} catch (IOException ex) {
throw new RuntimeException (
"Error binding to local socket '" + fileDesc + "'", ex);
}
}
}

创建一个服务端的socket用于接口多个客户端的信息接收,在后面的runSelectLoop方法用于监听服务端的socket信息,以便创建子进程。


2. 预加载资源


preload


  static void preload()
{
preloadClasses(); //预加载位于/system/etc/preloaded-classes文件中的类
preloadResources(); //预加载资源,包含drawable和color资源
preloadOpenGL(); //预加载OpenGL
preloadSharedLibraries(); //预加载"android","compiler_rt","jnigraphics"这3个共享库
preloadTextResources(); //预加载 文本连接符资源
WebViewFactory.prepareWebViewInZygote(); //仅用于zygote进程,用于内存共享的进程
}

preloadClasses()方法通过Class.forName()反射的方法去加载类,preloadResources方法主要是加载位于com.android.internal.R.array.preloaded_drawables和com.android.internal.R.array.preloaded_color_state_lists的资源。
提前加载资源的好处是,在复制创建子进程时,提前加载好的资源可以给子进程直接使用,不用第二次创建,但不好的地方是每个创建的子进程都有拥有很多资源,而不管是否需要。


3. 启动SystemServer进程


startSystemServer


  private static boolean startSystemServer(String abiList, String socketName)
throws MethodAndArgsCaller, RuntimeException
{
// 通过数组保存创建systemserver进程的信息
String args [] = {
"--setuid=1000",
"--setgid=1000",
"--setgroups=1001,1002,1003,1004,1005,1006,1007,1008,1009,1010,1018,1021,1032,3001,3002,3003,3006,3007",
"--capabilities=" + capabilities + "," + capabilities,
"--nice-name=system_server",
"--runtime-args",
"com.android.server.SystemServer",
};
ZygoteConnection.Arguments parsedArgs = null;

int pid;

try {
parsedArgs = new ZygoteConnection . Arguments (args);
ZygoteConnection.applyDebuggerSystemProperty(parsedArgs);
ZygoteConnection.applyInvokeWithSystemProperty(parsedArgs);

// 创建systemserver进程
pid = Zygote.forkSystemServer(
parsedArgs.uid, parsedArgs.gid,
parsedArgs.gids,
parsedArgs.debugFlags,
null,
parsedArgs.permittedCapabilities,
parsedArgs.effectiveCapabilities
);
} catch (IllegalArgumentException ex) {
throw new RuntimeException (ex);
}

/* pid==0 则是子进程,就是systemserver */
if (pid == 0) {
if (hasSecondZygote(abiList)) {
waitForSecondaryZygote(socketName);
}

// 完成system_server进程剩余的工作
handleSystemServerProcess(parsedArgs);
}

return true;
}


通过Zygote.forkSystemServer去创建SystemServer进程,其进程是管理着framework的,我们将在下一篇分析SystemServer进程进程的启动。


4. 循环等待孵化进程


runSelectLoop


  private static void runSelectLoop(String abiList) throws MethodAndArgsCaller
{
// FileDescriptor数组
ArrayList<FileDescriptor> fds = new ArrayList<FileDescriptor>();
// ZygoteConnection数组
ArrayList<ZygoteConnection> peers = new ArrayList<ZygoteConnection>();
//sServerSocket是socket通信中的服务端,即zygote进程。保存到fds[0]
fds.add(sServerSocket.getFileDescriptor());
peers.add(null);

while (true) {
// StructPollfd数组,并将相应位置fds的值赋值
StructPollfd[] pollFds = new StructPollfd[fds.size()];
for (int i = 0; i < pollFds.length; ++i) {
pollFds[i] = new StructPollfd ();
pollFds[i].fd = fds.get(i);
pollFds[i].events = (short) POLLIN;
}
try {
//处理轮询状态,当pollFds有事件到来则往下执行,否则阻塞在这里
Os.poll(pollFds, -1);
} catch (ErrnoException ex) {
throw new RuntimeException ("poll failed", ex);
}
for (int i = pollFds.length - 1; i >= 0; --i) {

//采用I/O多路复用机制,当接收到客户端发出连接请求 或者数据处理请求到来,则往下执行;
// 否则进入continue,跳出本次循环。
if ((pollFds[i].revents & POLLIN) == 0) {
continue;
}
if (i == 0) {
ZygoteConnection newPeer = acceptCommandPeer (abiList);
peers.add(newPeer);
fds.add(newPeer.getFileDesciptor());
} else {
//i>0,则代表通过socket接收来自对端的数据,并执行相应操作
boolean done = peers.get (i).runOnce();
if (done) {
peers.remove(i);
fds.remove(i);
}
}
}
}
}

在runSelectLoop方法中有一个轮询的状态,如果有事件接收则会去执行runOnce()的方法操作:


  boolean runOnce() throws ZygoteInit.MethodAndArgsCaller
{

String args [];
Arguments parsedArgs = null;
FileDescriptor[] descriptors;

try {
//读取socket客户端发送过来的参数列表
args = readArgumentList();
descriptors = mSocket.getAncillaryFileDescriptors();
} catch (IOException ex) {
Log.w(TAG, "IOException on command socket " + ex.getMessage());
closeSocket();
return true;
}

if (args == null) {
// EOF reached.
closeSocket();
return true;
}


try {
//将binder客户端传递过来的参数,解析成Arguments对象格式
parsedArgs = new Arguments (args);
...
// fork创建一个新的进程
pid = Zygote.forkAndSpecialize(
parsedArgs.uid, parsedArgs.gid, parsedArgs.gids,
parsedArgs.debugFlags, rlimits, parsedArgs.mountExternal, parsedArgs.seInfo,
parsedArgs.niceName, fdsToClose, parsedArgs.instructionSet,
parsedArgs.appDataDir
);
} catch (ErrnoException ex) {
logAndPrintError(newStderr, "Exception creating pipe", ex);
} catch (IllegalArgumentException ex) {
logAndPrintError(newStderr, "Invalid zygote arguments", ex);
} catch (ZygoteSecurityException ex) {
logAndPrintError(
newStderr,
"Zygote security policy prevents request: ", ex
);
}

try {
if (pid == 0) {
// 处理子进程
IoUtils.closeQuietly(serverPipeFd);
serverPipeFd = null;
handleChildProc(parsedArgs, descriptors, childPipeFd, newStderr);

// should never get here, the child is expected to either
// throw ZygoteInit.MethodAndArgsCaller or exec().
return true;
} else {
// 父进程
IoUtils.closeQuietly(childPipeFd);
childPipeFd = null;
return handleParentProc(pid, descriptors, serverPipeFd, parsedArgs);
}
} finally {
IoUtils.closeQuietly(childPipeFd);
IoUtils.closeQuietly(serverPipeFd);
}
}


所以在runSelectLoop方法中,通过客户端的socket不断的和服务端的socket通信的监听,通过调用起runOnce方法去不断的创建新的进程。


总结


Zygote进程的启动过程主要有:



  1. 创建虚拟机和JNI方法的注册

  2. 注册服务Socket和提前加载系统类和资源

  3. 创建SystemServer进程

  4. 循环等待孵化进程

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

NDK系列:JNI基础

1 Java、JNI、C/C++中的数据类型之间的映射关系 JNI是接口,Java与C/C++交互会有一个数据类型的对应,而JNI为此提供了一套中间类型。 2 JNI动态注册与静态注册 2.1 静态注册 步骤: 编写Java类,比如StaticRegiste...
继续阅读 »

1 Java、JNI、C/C++中的数据类型之间的映射关系


JNI是接口,Java与C/C++交互会有一个数据类型的对应,而JNI为此提供了一套中间类型。


2 JNI动态注册与静态注册


2.1 静态注册


步骤:



  • 编写Java类,比如StaticRegister.java;


package register.staticRegister;

public class StaticRegister {
public static native String func();//注意native关键字
public static void main(String[] args) {
System.out.println(func());
}
}


  • 在.java源文件目录下,命令行输入“javac StaticRegister.java”生成StaticRegister.class文件;

  • 在StaticRegister.class所属包所在目录下,命令行执行“javah register.staticRegister.StaticRegister”(完整类名无后缀),在包所在目录生成register_staticRegister_StaticRegister.h头文件;


image.png


image.png



  • 如果是JDK 1.8或以上,以上步骤可简化为一步:在StaticRegister.java目录下,命令行执行 javac -h . StaticRegister.java,直接在当前目录下得到.class文件和.h文件;

  • 创建CLion项目并拷贝register_staticRegister_StaticRegister.h文件到项目目录;

  • 在CLion项目中添加jni.h头文件和jni_md.h头文件,这两个头文件是JDK自带的,在C:\Program Files\Java\jdk1.8.0_144\include目录下,将这两个头文件拷贝到CLion项目目录;

  • 在register_staticRegister_StaticRegister.h中修改#include 为#include "jni.h"

  • 我们其实可以看到,register_staticRegister_StaticRegister.h文件里面就是一个Java方面native方法的一个JNI声明,格式为JNIEXPORT 关键字一 jstring 返回值的JNI类型 JNICALL 关键字二 Java_register_staticRegister_StaticRegister_func Java_全类名_方法名
    (JNIEnv *, jclass);如下;


/* DO NOT EDIT THIS FILE - it is machine generated */
#include "jni.h"
/* Header for class register_staticRegister_StaticRegister */

#ifndef _Included_register_staticRegister_StaticRegister
#define _Included_register_staticRegister_StaticRegister
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: register_staticRegister_StaticRegister
* Method: func
* Signature: ()Ljava/lang/String;
*/

JNIEXPORT jstring JNICALL Java_register_staticRegister_StaticRegister_func
(JNIEnv *, jclass)
;

#ifdef __cplusplus
}
#endif
#endif


  • 编写头文件register_staticRegister_StaticRegister.h对应的register_staticRegister_StaticRegister.c源文件,拷贝并实现register_staticRegister_StaticRegister.h下的函数,如下:


#include "register_staticRegister_StaticRegister.h"

JNIEXPORT jstring JNICALL Java_register_staticRegister_StaticRegister_func
(JNIEnv *env, jclass jobj)
{
return (*env)->NewStringUTF(env,"Hi Java, this is JNI");
};


  • 编写CMakeLists.txt文件,这是库的配置文件。最重要的是最后两个add_library(),其余的都是自动生成的。add_library()声明库的名字、类型和包含的.c&.h文件。SHARED关键字表示创建的库是动态库.dll,STATIC关键字表示创建的库是静态库.a。*注意:库本身有动态库和静态库之分,Java native方法也有静态注册和动态注册之分,二者没有关系。*这里将动态库命名为StaticRegisterLib


cmake_minimum_required(VERSION 3.15)
project(JNI_C)

set(CMAKE_CXX_STANDARD 14)

add_library(JNI_C SHARED library.cpp library.h)
add_library(StaticRegisterLib SHARED register_staticRegister_StaticRegister.c register_staticRegister_StaticRegister.h)


  • 此时CLion项目结构如下图,Build Project生成动态链接库,得到libStaticRegisterLib.dll


image.png


image.png


image.png



  • 在最开始的Java源文件中,添加静态代码块,使用System.load()方法加载该动态链接库,如下:


package register.staticRegister;

public class StaticRegister {

static {
System.load("E:\\_Projects_\\JNI_Projects\\JNI_C\\cmake-build-debug\\libStaticRegisterLib.dll");
}

public static native String func();
public static void main(String[] args) {
System.out.println(func());
}
}


  • 在Java侧运行,得到如下效果,Java成功调用了dll中的方法,静态注册完毕。


image.png



  • 上述过程,我们在JNI中使用Java_PACKAGENAME_CLASSNAME_METHODNAME与Java侧的方法进行匹配,这种方式我们称之为静态注册


2.2 动态注册


步骤:



  • 编写Java类,比如DynamicRegister.java,如下;


package register.dynamicRegister;

public class DynamicRegister {
public static native String func1(String s);
public static native int func2(int[] a);
public static void main(String[] args) {
System.out.println(func1("Hi JNI"));
int[] a = {1,2,3};
System.out.println("该数组有"+func2(a)+"个元素");
}
}


  • 在CLion项目中添加jni.h头文件和jni_md.h头文件,这两个头文件是JDK自带的,在C:\Program Files\Java\jdk1.8.0_144\include目录下,将这两个头文件拷贝到CLion项目目录;

  • 新建CLion项目,新建C/C++源文件dynamicRegister.c。在该.c文件中,实现两个函数,这两个函数将是native方法在JNI的实现,如下:


#include "jni.h"

jstring f1(JNIEnv *env, jclass jobj){
return (*env)->NewStringUTF(env,"Hi Java");
}
//注意JNI侧数组形参的写法以及如何求数组长度
jint f2(JNIEnv *env, jclass jobj, jintArray arr){
int len = (*env)->GetArrayLength(env,arr);
return len;
}


  • 到目前,f1(),f2()与Java侧native方法func1(),func2()还没有任何关联,我们需要手动**管理关联**;

  • 首先,我们新建一个以JNINativeMethod结构体为元素的数组,如下:


static const JNINativeMethod mMethods[] = {
{"func1","(Ljava/lang/String;)Ljava/lang/String;",(jstring *)f1},
{"func2","([I)I",(jint *)f2},
};


  • 以上数组中每一个元素,都是JNI侧实现方法与Java侧native方法的关联,前两个是Java侧native方法的描述,最后一个是JNI侧函数实现的描述,格式为:


{"Java侧的native方法名","方法的签名",函数指针}


  • 我们需要实现jni.h中的JNI_OnLoad()方法,该方法的实现方法是一个模板,如下:


JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM *vm, void *reserved)
{
JNIEnv* env = NULL;
//获得 JNIEnv
int r = (*vm)->GetEnv(vm, (void**) &env, JNI_VERSION_1_4);
if( r != JNI_OK){
return -1;
}
jclass mainActivityCls =
(*env)—>FindClass(env,"register/dynamicRegister/DynamicRegister");
// 最后参数是需要注册的native方法的个数,如果小于0则注册失败。
r = (*env)->RegisterNatives(env, mainActivityCls, mMethods, 2);
if(r != JNI_OK ){
return -1;
}
return JNI_VERSION_1_4;
}


  • 注意!第一:以上FindClass(env,"register/dynamicRegister/DynamicRegister")中的字符串是Java侧DynamicRegister类的全类名,注意此处的写法"/";第二:RegisterNatives(env, mainActivityCls, mMethods, 2)中的最后一个参数是需要动态注册的方法个数,手动添加注册或者删除注册都需要对应变化,当然可以直接把mMethod[]的长度传进去,一劳永逸,如下:


int cnt = sizeof(mMethods)/ sizeof(mMethods[0]);
r = (*env)->RegisterNatives(env, mainActivityCls, mMethods, cnt);


  • 在最开始的Java源文件中,添加静态代码块,使用System.load()方法加载该动态链接库,如下:


package register.dynamicRegister;

public class DynamicRegister {
static {
System.load("E:\\_Projects_\\JNI_Projects\\JNI_C\\cmake-build-debug\\libDynamicRegisterLib.dll");
}
public static native String func1(String s);
public static native int func2(int[] a);
public static void main(String[] args) {
System.out.println(func1("Hi JNI"));
int[] a = {1,2,3};
System.out.println("该数组有"+func2(a)+"个元素");
}
}


  • Build Project生成动态链接库,得到libDynamicRegisterLib.dll

  • Java侧运行,效果如下:


image.png



  • 动态注册完毕。


3 system.load()与system.loadLibrary()


System.load()
System.load()参数必须为库文件的绝对路径,可以是任意路径,例如: System.load("C:\Documents and
Settings\TestJNI.dll"); //Windows
System.load("/usr/lib/TestJNI.so"); //Linux


System.loadLibrary()
System.loadLibrary()参数为库文件名,不包含库文件的扩展名
System.loadLibrary("TestJNI"); //加载Windows下的TestJNI.dll本地库
System.loadLibrary("TestJNI"); //加载Linux下的libTestJNI.so本地库


注意:TestJNI.dll 或 libTestJNI.so 必须是在JVM属性java.library.path所指向的路径中。
loadLibary需要[配置当前项目的java.library.path路径]


3 JNI上下文与Java签名


3.1 JNI上下文环境


3.1.1 JNIEnv


JNIEnv类型实际上代表了Java环境,通过JNIEnv*指针,JNI函数可以对Java侧的代码进行操作。例如,创建Java类的对象,调用Java对象的方法,获取对象中的属性等。JNIEnv的指针会被传入到JNI侧的native方法的实现函数中,来对Java端的代码进行操作。例如:


jstring f1(JNIEnv *env, jclass jobj){
return (*env)->NewStringUTF(env,"Hi Java");
}

3.1.2 区分jobject与jclass


在JNI侧声明Java native方法的实现的时候,会有两个默认形参(除开native方法自己的传入参数),分别是JNIEnv指针,另外一个是jobject/jclass,这两个的区别在于:



  • jobject:如果Java侧的native方法是非静态的,那么传给JNI的第二个参数是类的对象,所有类的对象在JNI侧都是jobject类型。

  • jclass:如果Java侧的native方法是静态的,那么传给JNI的第二个参数是类的运行时类,所有运行时类在JNI侧都是jclass类型。

显然,这段JNI代码是native方法在JNI侧的实现,其中先后



  1. 创建了一个jintArray;

  2. 调用了Java侧JNICallJavaMethod类的构造方法;

  3. JNICallJavaMethod类的非静态方法;

  4. JNICallJavaMethod类的静态方法;

  5. JNICallJavaMethodNative类的非静态方法。


我们分别分析,以下代码就是上述代码的分别分析。


调用构造函数


步骤:



  1. 加载类,被调用方法所在类的运行时类,即jclass

  2. 获取方法ID,即jmethodID

  3. 创建一个类的实例,即jobject

  4. 调用方法。


注意:



  1. MethodName形参直接传""即可;

  2. 构造函数在Java侧没有返回值,连void都不是;但是,在JNI侧的方法签名中,返回值是void。比如,一个类的默认构造函数的签名是**"()V"**;

  3. 凡是JNI方法的*GetXxx()*过程,都必须进行异常处理,即使用前判断是否为NULL。


代码:


//todo:调用另一个类的构造函数
jclass jclz1 = NULL;
jclz1 = (*env)->FindClass(env, "JNICallJava/JNICallJavaMethod");
if(jclz1 == NULL){
printf("JNI Side : jclz is NULL.");
return ji;
}
jmethodID jmethodId1 = NULL;
jmethodId1 = (*env)->GetMethodID(env, jclz1, "", "()V");
if(jmethodId1 == NULL){
printf("JNI Side : jmethodId1 is NULL.");
return ji;
}
jobject jobj1 = (*env)->NewObject(env, jclz1, jmethodId1);
(*env)->CallVoidMethod(env, jobj1, jmethodId1);

调用非静态方法


步骤:



  1. 加载类,被调用方法所在类的运行时类,即jclass

  2. 获取方法ID,即jmethodID

  3. 创建一个类的实例,即jobject

  4. 调用方法。


注意:



  1. 因为该非静态方法与上述构造方法同属一个类,所以此时可以省去加载运行时类的步骤一,直接用已经获取到的jclass;

  2. Java侧来说,一个类的对象可以调用多个方法;但是JNI侧的jobject是与jmethodID一一对应的。所以,即使JNI侧调用的不同方法属于同一个类,也需要创建不同的jobject,不能共用;从创建jobject的JNI函数可以看出来:


//jobject与jmethodID是一一对应的关系:
jobject jobj1 = (*env)->NewObject(env, jclz1, jmethodId1);

代码:


//todo:调用另一个类的非静态方法
jmethodID jmethodId2 = NULL;
jmethodId2 = (*env)->GetMethodID(env, jclz1, "func", "(I)I");
if(jmethodId2 == NULL){
return ji;
}
jobject jobj2 = (*env)->NewObject(env, jclz1, jmethodId2);
jint i1 = (*env)->CallIntMethod(env, jobj2, jmethodId2, 5);
printf("JNI Side : func returns %d.\n", i1);

调用静态方法


步骤:



  1. 加载类,被调用方法所在类的运行时类,即jclass

  2. 获取方法ID,即jmethodID

  3. 调用方法。


注意:



  1. 因为该非静态方法与上述构造方法同属一个类,所以此时可以省去加载运行时类的步骤一,直接用已经获取到的jclass;

  2. 因为静态方法的调用不需要对象实例,所以调用Java静态方法时,不需要jobject。


代码:


//todo:调用另一个类的静态方法
jmethodID jmethodId3 = NULL;
jmethodId3 = (*env)->GetStaticMethodID(env, jclz1, "staticFunc", "([I)I");
if(jmethodId3 == NULL){
printf("JNI Side : jmethodId3 is NULL.");
return ji;
}
jint i2 = (*env)->CallStaticIntMethod(env, jclz1, jmethodId3, jArr);
printf("JNI Side : staticFunc returns %d.\n", i2);

调用native方法所在类的方法


这里以非静态方法为例,因为Java侧方法与native方法在同一个类中,而JNI侧实现native方法时,会传入一个jclass/jobject,分别对应Java侧的native方法声明是static native/native。此时我们可以直接使用传入的jclass,或者利用**(*env)->GetObjectClass(env,jobj)**获取到运行时类。关键在于,调用哪个方法,首先需要加载该方法所在的类到JVM运行时环境中


代码:


//todo:调用与native方法同属一个类的方法
jclass jclz0 = NULL;
jclz0 = (*env)->GetObjectClass(env, jobj);
if(jclz0 == NULL){
printf("JNI Side : jclz0 is NULL.");
return ji;
}
jmethodID jmethodId4 = NULL;
jmethodId4 = (*env)->GetMethodID(env, jclz0,"func2","()Ljava/lang/String;");
if(jmethodId4 == NULL){
printf("JNI Side : jmethodId4 is NULL.");
return ji;
}
//todo:接收Java方法返回的字符串,并在JNI侧打印
jstring jstr = (jstring)(*env)->CallObjectMethod(env, jobj, jmethodId4);
char *ptr_jstr = (char *)(*env)->GetStringUTFChars(env,jstr,0);
printf("JNI Side : func2 returns %s\n",ptr_jstr);

JNI调用Java方法答疑


JNI侧如何创建整形数组

步骤:



  1. 声明数组名字与数组长度,即jArr、4

  2. 获取数组元素类型(jint型)的指针,通过调用(*env)->GetIntArrayElements(env, jArr, NULL)

  3. 利用指针,为元素赋值;

  4. 释放指针资源,数组得以保留。


代码:


//todo:JNI侧创建一个int array
jintArray jArr = (*env)->NewIntArray(env, 4);//步骤1
jint *arr = (*env)->GetIntArrayElements(env, jArr, NULL);//步骤2
arr[0] = 0;//步骤3
arr[1] = 10;
arr[2] = 20;
arr[3] = 30;
(*env)->ReleaseIntArrayElements(env,jArr,arr,0);//步骤4

Java侧方法返回String,JNI调用时如何打印返回值?

步骤:



  1. 定义jstring变量,并用(jstring)强转jobject;

  2. 定义字符型指针,并用 (char *)强转;

  3. 打印。


//todo:接收Java方法返回的字符串,并在JNI侧打印
jstring jstr = (jstring)(*env)->CallObjectMethod(env, jobj, jmethodId4);
char *ptr_jstr = (char *)(*env)->GetStringUTFChars(env,jstr,0);
printf("JNI Side : func2 returns %s\n",ptr_jstr);

JNI侧与Java侧的控制台打印顺序

结论是:


JNI侧的控制台打印一定出现在Java侧程序运行结束之后。


我们可以调试看现象:


image.png


两遍I am constructor?

:在调用构造方法和非静态方法的两个调用过程中,都需要通过(*env)->NewObject(env, jclz, jmethodId)创建与jmethodID一一对应的jobject,所以调用了两次构造函数。


两遍func is called?

:待解答!


能否脱离native方法的实现来调用Java侧方法?

:可以,JNI是Java跨平台的实现机制,是Java与原生代码交互的机制。上述的过程我们一般都是在JNI侧的native方法实现中进行的,因为native方法的JNI实现中就有JNIEnv*指针,是获取JNIEnv*最容易的方式,并非唯一方式。如何获取JNIEnv*?待解答!


4.3 JNI处理从Java传来的字符串


Java与C字符串的区别



  • Java内部使用的是utf-16 16bit 的编码方式;

  • JNI里面使用的utf-8 unicode编码方式,英文是1个字节,中文3个字节;

  • C/C++ 使用ASCII编码,中文的编码方式GB2312编码,中文2个字节。


image.png


实战代码


//Java:
package JNICallJava;

public class GetSetJavaString {
static {
System.load("E:\\_Projects_\\JNI_Projects\\JNI_C\\cmake-build-debug\\libGetSetJavaStringLib.dll");
}
public static native String func(String s);
public static void main(String[] args) {
String str = func("--Do you enjoy coding?");
System.out.println(str);
}
}

//C:
#include "stdio.h"
#include "jni.h"
JNIEXPORT jstring JNICALL Java_JNICallJava_GetSetJavaString_func
(JNIEnv *,jclass,jstring)
;//没有用专门的.h文件,此声明可写可不写。

JNIEXPORT jstring JNICALL Java_JNICallJava_GetSetJavaString_func
(JNIEnv *env,jclass jclz,jstring jstr)
{
const char *chr = NULL;//字符指针定义与初始化分开
jboolean iscopy;//判断jstring转成char指针是否成功
chr = (*env)->GetStringUTFChars(env,jstr,&iscopy);//&iscopy位置一般直接传入NULL就好
if(chr == NULL){
return NULL;//异常处理
}
char buf[128] = {0};//申请空间+初始化
sprintf(buf,"%s\n--Yes, I do.",chr);//字符串拼接
(*env)->ReleaseStringUTFChars(env,jstr,chr);//编程习惯,释放内存
return (*env)->NewStringUTF(env,buf);
}

//CMakeLists.txt
add_library(GetSetJavaStringLib SHARED GetSetJavaString.c)

运行结果


image.png


异常处理


上述代码实例中,GetStringUTFChars()方法将JNI的jstring变量转换成C语言能操作的char指针,这个过程可能失败,其实任何转换过程都可能失败,这些过程的目标变量的定义和初始化都需要分开进行,并通过判空进行异常处理


C语言字符串拼接


在C语言中,没有String,字符串都是字符指针。其拼接过程不像Java等语言那么简单,分为以下过程:



  1. malloc申请空间

  2. 初始化

  3. 拼接字符串

  4. 释放内存


灵活的静态注册



  • 此实战代码中,我们没有想一般的静态注册一样使用Java native产生的.h文件,而是直接在实现JNI方法之前写了一个JNI静态注册,这也是可行的,甚至这个提前的声明注册也是可以不写的。此时我们在CMakeLists.txt中的add_library()中值包含了该.c文件。核心在于add_library()中一定要包含native方法在JNI的实现函数,.h文件更多是Java命令生成的教你怎么写JNI实现的一个辅助,无关紧要。

  • JNI无视Java侧的访问控制权限,但会区别静态或非静态。


5 JNI引用


5.1 三种引用


只有当JNI返回值是jobject的引用,才是三种引用之一。


比如(*env)->GetMethodID()返回值就不是引用,是一个结构体。


局部引用



  • 绝大部分JNI方法返回的是局部引用;

  • 局部引用的作用域或者生命周期始于创建它的本地方法,终止于本地方法的返回;

  • 通常在局部引用不再使用时,可以显式使用**DeleteLocalRef()**方法来提前释放它所指向的对象,一边GC回收;

  • 局部引用时线程相关的,只能在创建他的线程里面使用,通过全局变量缓存并使用在其他线程是不合法的。


全局引用


调用NewGlobalRef()基于局部引用创建,会阻止GC回收所引用的对象。可以跨方法、跨线程使用。JVM不会自动释放,必须调用DeleteGlobalRef()手动释放。


弱全局引用


调用NewWeakGlobalRef()基于局部引用创建,不会阻止GC回收所引用的对象。可以跨方法、跨线程使用。JVM不会自动释放,必须调用DeleteWeakGlobalRef()手动释放。


5.2 野指针


上一次创建的东西在程序结束的被回收了,但是静态局部变量未释放,不为NULL。


作业1:写代码实现访问java 非静态和静态方法,返回值必须是object类型
作业2:写代码体会野指针异常


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

跟我学企业级flutter项目:用bloc手把手教你搭建用户认证系统

前言 跟我学flutter系列:跟我学flutter:我们来举个例子通俗易懂讲解dart 中的 mixin跟我学flutter:我们来举个例子通俗易懂讲解异步(一)ioslate跟我学flutter:我们来举个例子通俗易懂讲解异步(二)ioslate循环机制企...
继续阅读 »
前言

跟我学flutter系列:
跟我学flutter:我们来举个例子通俗易懂讲解dart 中的 mixin
跟我学flutter:我们来举个例子通俗易懂讲解异步(一)ioslate
跟我学flutter:我们来举个例子通俗易懂讲解异步(二)ioslate循环机制
企业级篇目:
跟我学企业级flutter项目:用bloc手把手教你搭建用户认证系统
跟我学企业级flutter项目:dio网络框架增加公共请求参数&header
跟我学企业级flutter项目:如何用dio封装一套企业级可扩展高效的网络层
跟我学企业级flutter项目:如何封装一套易用,可扩展的Hybrid混合开发webview
跟我学企业级flutter项目:手把手教你制作一款低耦合空页面widget

在以flutter为底的app项目中,用户登录,退出等认证必须做在flutter项目里,那么采用何种状态管理,来全局管理用户认证呢?
今天我就借助flutter_bloc这个库来搭建一套可以复用的成熟用户认证系统


搭建前夕准备


一、我们需要了解现有app有多少认证事件,那么常规来说,流程如下:


1、启动app,判断有无token,有token则跳转首页获取数据,无token则跳转需要授权页面如登录页 

2、登录页登录,登陆后保存token,跳转首页 

3、退出登录,删除token跳转需要授权页


那么总结起来就有三种事件

1、启动事件 

2、登录事件 

3、退出登录事件


二、那么有了认证事件,我们还需要有几个认证状态,有哪些状态呢,我来梳理一下:


1、在app启动后,需要初始化用户状态,那么用户当前是一个身份需要初始化的状态 

2、如果有token,或者用户登录后那么用户就是一个已认证的状态 

3、如果用户退出登录,那么用户当前是未认证的状态


三、咱们还需要做一个用户认证接口,接口主要是为了解耦,为了后期扩展能力、接口需要有哪些内容呢继续梳理一下:


1、是否有token,token是决定app是否认证的关键 

2、删除token,退出登录需要删除 

3、保存token,登录需要保存 

4、跳转授权页面 

5、跳转非授权页面


准备好如上工作,那么我们开始搭建用户认证系统吧


1、先编写认证事件:


part of 'authentication_bloc.dart';

//App认证事件,一般来说有三种,启动认证,登录认证,退出认证
abstract class AuthenticationEvent extends Equatable {
const AuthenticationEvent();

@override
List get props => [];
}
//App启动事件
class AppStart extends AuthenticationEvent{}
//App登录事件
class LogIn extends AuthenticationEvent{
final String token;

LogIn(this.token);

@override
List get props => [token];

@override
String toString() =>"LoggedIn { token: $token }";
}
//App退出事件
class LogOut extends AuthenticationEvent{}

2、编写认证状态


part of 'authentication_bloc.dart';

/// 认证状态 
abstract class AuthenticationState extends Equatable {
const AuthenticationState();
@override
List get props => [];
}

/// - uninitialized - 身份验证未初始化
class AuthenticationUninitialized extends AuthenticationState {}

/// - authenticated - 认证成功
class AuthenticationAuthenticated extends AuthenticationState {}

/// - unauthenticated - 未认证
class AuthenticationUnauthenticated extends AuthenticationState {}

3、编写外部接口



abstract class IUserAuthentication{

bool hasToken();

void saveToken(String token);

void deleteToken();

void authPage();

void unAuthPage();
}

4、有了如上的内容咱们就可以编写核心逻辑bloc了


import 'dart:async';

import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'i_user_authentication.dart';

part 'authentication_event.dart';
part 'authentication_state.dart';

class AuthenticationBloc extends Bloc {

final IUserAuthentication iUserAuthentication;

/// 初始化认证是未认证状态
AuthenticationBloc(this.iUserAuthentication) : super(AuthenticationUninitialized());

@override
Stream mapEventToState(
AuthenticationEvent event,
)
async*
{
if(event is AppStart){
// 判断是否有Token
if(iUserAuthentication.hasToken()){
yield AuthenticationAuthenticated();
} else {
yield AuthenticationUnauthenticated();
}
}else if(event is LogIn){
iUserAuthentication.saveToken(event.token);
yield AuthenticationAuthenticated();
}else if(event is LogOut){
iUserAuthentication.deleteToken();
yield AuthenticationUnauthenticated();
}
}
}

为了使用方便咱们需要做一个工具类来支撑外部使用



import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

import 'authentication_bloc.dart';

class Authentication{

static TransitionBuilder init({
TransitionBuilder? builder,
})
{
return (BuildContext context, Widget? child) {
var widget = BlocListener(
listener: (context, state) {
var bloc = BlocProvider.of(context);
if (state is AuthenticationAuthenticated) {
bloc.iUserAuthentication.authPage();
} else if (state is AuthenticationUnauthenticated) {
bloc.iUserAuthentication.unAuthPage();
}
},
child: child,
);
if (builder != null) {
return builder(context, widget );
} else {
return widget;
}
};
}
}

使用


在项目中如何使用呢?? 

1、接口事件类 

2、bloc初始化 

3、监听初始化


代码如下:
接口实现类


class Auth implements IUserAuthentication{
static final String userTokenN = 'userToken';

Auth(){
_userMMKV = MMKVStore.appMMKV(name: "123");
}
@override
void authPage() {
RouterName.navigateTo(LibRouteNavigatorObserver.instance.navigator!.context, RouterName.home,clearStack: true);

}
late MMKV _userMMKV;

@override
void deleteToken() {
_userMMKV.removeValue(userTokenN);
}

@override
bool hasToken() {
return _userMMKV.decodeString(userTokenN)!=null;
}

@override
void saveToken(String token) {
_userMMKV.encodeString(userTokenN, token);
}

@override
void unAuthPage() {
RouterName.navigateTo(LibRouteNavigatorObserver.instance.navigator!.context, RouterName.login,replace: true);
}

}

2、初始化


MultiBlocProvider(
providers: [
//AuthenticationBloc bloc初始化
BlocProvider(create: (_) => AuthenticationBloc(Auth())),
],
child: MaterialApp(
...
builder: Authentication.init() //监听初始化
),
);

3、事件调用


1、退出按钮调用,BlocProvider.of(context).add(LogOut()) 

2、登录页面调用,BlocProvider.of(context).add(LogIn("123")) 

3、SplashPage页面调用 BlocProvider.of(context).add(AppStart())


大功告成


如上搭建的一个用户认证系统,可以抽离项目做成package,再下次开发其他项目时候,就可以直接使用。方便快捷。

收起阅读 »

Kotlin 1.5 新特性 Inline classes,还不了解一下?

Kotlin 1.5 如约而来了。 如果你正在使用Android Studio 4.2.0 、IntelliJ IDEA 2020.3 或更高的版本,近期就会收到 Kotlin 1.5 的Plugin推送了。作为一个大版本,1.5带来了不少新特性,其中最主要的...
继续阅读 »

Kotlin 1.5 如约而来了。


如果你正在使用Android Studio 4.2.0 、IntelliJ IDEA 2020.3 或更高的版本,近期就会收到 Kotlin 1.5 的Plugin推送了。作为一个大版本,1.5带来了不少新特性,其中最主要的要数inline class了。


早在kotlin 1.3 就已经有了 inline class 的alpha版本。到 1.4.30 进入 beta,如今在 1.5.0 中 终于迎来了 Stable 版本。早期的实验版本的 inline 关键字 在 1.5 中被废弃,转而变为 value关键字


//before 1.5
inline class Password(private val s: String)

//after 1.5 (For JVM backends)
@JvmInline
value class Password(private val s: String)

个人很认同从 inline 变为 value 的命名变化,这使得其用途更为明确:



inline class 主要就是用途就是更好地 "包装" value



有时为了语义更有辨识度,我们会使用自定义class包装一些基本型的value,这虽然提高了代码可读性,但额外的包装会带来潜在的性能损失,基本型的value由于被在包装在其他class中,无法享受到jvm的优化(由堆上分配变为栈上分配)。 而 inline class 在最终生成的字节码中被替换成其 “包装”的 value, 进而提高运行时的性能。


// For JVM backends
@JvmInline
value class Password(private val s: String)

如上,inline class 构造参数中有且只能有一个成员变量,即最终被inline到字节码中的value。


val securePassword = Password("Don't try this in production")

如上,Password实例在字节码中被替换为String类型"Don't try this in production"


PS:如何安装 Kotlin 1.5



  1. 首先更新IDE的 Kotlin Plugin,如果没收到推送,可以手动方式升级:


Tools > Kotlin > Configure Kotlin Plugin Updates



  1. 配置languageVersion & apiVersion


compileKotlin {
kotlinOptions {
languageVersion = "1.5"
apiVersion = "1.5"
}
}



经 inline 处理后代码




inline classes 转化为字节码后究竟是怎样的呢?


fun check(password: Password) {
//...
}

fun main() {
val securePassword = Password("Don't try this in production")
check(securePassword)
}

对于Password这个inline class, 字节码反编译的产物如下:


   public static final void check_XYhEtbk/* $FF was: check-XYhEtbk*/(@NotNull String password) {
Intrinsics.checkNotNullParameter(password, "password");
}

public static final void main() {
String securePassword = Password.constructor-impl("Don't try this in production");
check-XYhEtbk(securePassword);
}

// $FF: synthetic method
public static void main(String[] var0) {
main();
}


  • securePassword 的类型由Password替换为String

  • check方法改名为check_XYhEtbk,签名类型也有 Password 替换 String


可见,无论是变量类型或是函数参数类型,所有的inline classes都被替换为其包装的类型。


名字被混淆处理(check_XYhEtbk)主要有两个目的



  1. 防止重载函数的参数经过 inline 后出现相同签名的情况

  2. 防止从Java侧调用到参数经过 inline 后的方法




Inline class 的成员




inline class 具备普通class的所有特性,例如拥有成员变量、方法、初始化块等


@JvmInline
value class Name(val s: String) {
init {
require(s.length > 0) { }
}

val length: Int
get() = s.length

fun greet() {
println("Hello, $s")
}
}

fun main() {
val name = Name("Kotlin")
name.greet() // `greet()`作为static方法被调用
println(name.length) // property getter 也是一个static方法
}

但是,inline class 的成员不能有自己的幕后属性,只能作为代理使用。 inline class的创建的对象在字节码中会被消除,所以这个实例无法拥有自己的状态以及行为,对inline class 实例的方法调用,在实际运行时会变为一格静态方法调用。




Inline class 的继承




interface Printable {
fun prettyPrint(): String
}

@JvmInline
value class Name(val s: String) : Printable {
override fun prettyPrint(): String = "Let's $s!"
}

fun main() {
val name = Name("Kotlin")
println(name.prettyPrint()) // prettyPrint()也是一个 static方法调用
}

inline class 可以实现任意inteface, 但不能继承自class。因为在运行时将无处安放其父类的属性或状态。如果你试图继承另一个Class,IDE会提示错误:Inline class cannot extend classes




自动拆装箱




inline class 在字节码中并非总被消除,有时也是需要存在的。例如当出现在泛型中、或者以 Nullable 类型出现时,此时它会根据情况自动与被包装类型进行转换,实现像Integerint那样的自动拆装箱


@JvmInline
value class WrappedInt(val value: Int)

fun take(w: WrappedInt?) {
if (w != null) println(w.value)
}

fun main() {
take(WrappedInt(5))
}

如上,take 接受一个 Nulable 的 WrappedInt 后进行 print 处理


public static final void take_G1XIRLQ(@Nullable WrappedInt w) {
if (Intrinsics.areEqual(w, (Object)null) ^ true) {
int var1 = w.unbox_impl();
System.out.println(var1);
}
}

public static final void main() {
take_G1XIRLQ(WrappedInt.box_impl(WrappedInt.constructor_impl(5)));
}

字节码中,take的参数并没有变为Int,而仍然是原始类型 WrappedInt。因此,在 take 的调用处,需要通过box_impl 做装箱处理, 而在take的实现中,通过 unbox_impl 拆箱后再进行print


同理,在泛型方法或者泛型容器中使用 inline class 时,需要通过装箱保证传入其原始类型:


genericFunc(color)         // boxed
val list = listOf(color) // boxed
val first = list.first() // unboxed back to primitive

反之,从容器获取 item 时,需要拆箱为被包装类型。



关于自动拆装箱在开发中无需太在意,只要知道有这个特性存在即可。





对比其他类型




与 type aliases 的区别 ?


inline class 与 type aliases 在概念上有点相似,都会在编译后被替换为被代理(包装)的类型, 区别在于



  • inline class 本身是实际存在的Class 只是在字节码中被消除了并被替换为被包装类型

  • type aliases仅仅是个别名,它的类型就是被代理类的类型。


typealias NameTypeAlias = String

@JvmInline
value class NameInlineClass(val s: String)

fun acceptString(s: String) {}
fun acceptNameTypeAlias(n: NameTypeAlias) {}
fun acceptNameInlineClass(p: NameInlineClass) {}

fun main() {
val nameAlias: NameTypeAlias = ""
val nameInlineClass: NameInlineClass = NameInlineClass("")
val string: String = ""

acceptString(nameAlias) // OK: NameTypeAlias等同String,可以传递
acceptString(nameInlineClass) // Not OK: NameInlineClass 与 String是两个类,不能等同

// 反之亦然:
acceptNameTypeAlias(string) // OK: 传入String也是可以的
acceptNameInlineClass(string) // Not OK: String不等同于NameInlineClass
}

与 data class 的区别 ?


inline class 与 data class 在概念上也很相似,都是对一些数据的包装,但是区别很明显



  • inline class 只能有一个成员属性,其主要目的是通过一个额外类型的包装让代码更易用

  • data clas 可以有多个成员属性,其主要目的是更高效地处理一组相关数据的集合




使用场景


上面说到, inline class 的目的是通过包装让代码更易用,这个易用性体现在诸多方面:


场景1:提高可读性


fun auth(userName: String, password: String) { println("authenticating $userName.") }

如上, auth的两个参数都是String,缺乏辨识度,即使像下面这样传错了也难以发觉


auth("12345", "user1") //Error

@JvmInline value class Password(val value: String)
@JvmInline value class UserName(val value: String)

fun auth(userName: UserName, password: Password) { println("authenticating $userName.")}

fun main() {
auth(UserName("user1"), Password("12345"))
//does not compile due to type mismatch
auth(Password("12345"), UserName("user1"))
}

使用 inline class 使的参数更具辨识度,避免发生错误


场景2:类型安全(缩小扩展函数作用域)


inline fun <reified T> String.asJson() = jacksonObjectMapper().readValue<T>(this)

String类型的扩展方法asJson可以转化为指定类型T


val jsonString = """{ "x":200, "y":300 }"""
val data: JsonData = jsonString.asJson()

由于扩展函数是top-level的,所有的String类型都可以访问,造成污染


"whatever".asJson<JsonData> //will fail

通过inline class可以将Receiver类型缩小为指定类型,避免污染


@JvmInline value class JsonString(val value: String)

inline fun <reified T> JsonString.asJson() = jacksonObjectMapper().readValue<T>(this.value)

如上,定义JsonString,并为之定义扩展方法。


场景3:携带额外信息


/**
* parses string number into BigDecimal with a scale of 2
*/
fun parseNumber(number: String): BigDecimal {
return number.toBigDecimal().setScale(2, RoundingMode.HALF_UP)
}

fun main() {
println(parseNumber("100.12212"))
}

如上,parseNumber的功能是将任意字符串解析成数字并保留小数点后两位。


如果我们希望通过一个类型将解析前后的值都保存下来然后分别打印,可能首先想到的使用Pair或者data class。但是当这两个值之间是有换算关系时,其实也可以用inline class实现。如下


@JvmInine value class ParsableNumber(val original: String) {
val parsed: BigDecimal
get() = original.toBigDecimal().setScale(2, RoundingMode.HALF_UP)
}

fun getParsableNumber(number: String): ParsableNumber {
return ParsableNumber(number)
}

fun main() {
val parsableNumber = getParsableNumber("100.12212")
println(parsableNumber.parsed)
println(parsableNumber.original)
}

ParsableNumber的包装类型是String,同时通过parsed携带了解析后的值。如前文提到的那样,字节码中,parsed getter 会以static方法的形式存在,因此虽然携带了更多信息,但实际上并不存在这样一个包装类实例:


@NotNull
public static final String getParsableNumber(@NotNull String number) {
Intrinsics.checkParameterIsNotNull(number, "number");
return ParsableNumber.constructor_impl(number);
}

public static final void main() {
String parsableNumber = getParsableNumber("100.12212");
BigDecimal var1 = ParsableNumber.getParsed_impl(parsableNumber);
System.out.println(var1);
System.out.println(parsableNumber);
}



最后




Inline class 是个好工具,在提高代码的可读性、易用性的同时,不会造成性能的损失。 早期由于一直处于试验状态没有被大家所熟知, 随着如今在 Kotlin 1.5 中的转正,相信未来一定会被在更广泛地使用、发掘更多应用场景。


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

Flutter 2021 中的按钮

在本文中,我们将介绍令人惊叹的 Flutter 按钮,它们可以帮助所有初学者或专业人士为现代应用程序设计漂亮的 UI。 首先让我告诉你关于 Flutter 中按钮的一件重要事情,在flutter最新版本中以下Buttons在fluter中被废弃了:废弃的推荐的...
继续阅读 »

在本文中,我们将介绍令人惊叹的 Flutter 按钮,它们可以帮助所有初学者或专业人士为现代应用程序设计漂亮的 UI。


首先让我告诉你关于 Flutter 中按钮的一件重要事情,在flutter最新版本中以下Buttons在fluter中被废弃了:

废弃的推荐的替代
RaisedButtonElevatedButton
OutlineButtonOutlinedButton
FlatButtonTextButton

那么让我们来探索一下 Flutter 中的按钮。



Elevated Button


StadiumBorder


image.png


ElevatedButton(
onPressed: (){},
child: Text('Button'),
style: ElevatedButton.styleFrom(
shadowColor: Colors.green,
shape: StadiumBorder(),
padding: EdgeInsets.symmetric(horizontal: 35,vertical: 20)),
)

image.png


RoundedRectangleBorder


ElevatedButton(
onPressed: (){},
child: Text('Button'),
style: ElevatedButton.styleFrom(
shadowColor: Colors.green,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),

CircleBorder


image.png


ElevatedButton(
onPressed: () {},
child: Text('Button'),
style: ElevatedButton.styleFrom(
shape: CircleBorder(),
padding: EdgeInsets.all(24),
),
)

BeveledRectangleBorder


image.png


ElevatedButton(
onPressed: () {},
child: Text('Button'),
style: ElevatedButton.styleFrom(
shape: BeveledRectangleBorder(
borderRadius: BorderRadius.circular(12)
),
),
)

Outlined Button


StadiumBorder
image.png


OutlinedButton(
onPressed: () {},
child: Text('Button'),
style: OutlinedButton.styleFrom(
shape: StadiumBorder(),
),
)

RoundedRectangleBorder


image.png


OutlinedButton(
onPressed: () {},
child: Text('Button'),
style: OutlinedButton.styleFrom(
shape: BeveledRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
)

CircleBorder


image.png


OutlinedButton(
onPressed: () {},
child: Text('Button'),
style: OutlinedButton.styleFrom(
shape: CircleBorder(),
padding: EdgeInsets.all(24),
),
)

BeveledRectangleBorder


image.png


OutlinedButton(
onPressed: () {},
child: Text('Button'),
style: OutlinedButton.styleFrom(
shape: BeveledRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
)

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

一个录音项目的开发总结(一)

iOS
最近,工作之余,自己做了一个项目,项目的一期主要功能是音频录制和播放,音频格式包含m4a、mp3、wav三种格式,录制过程中要支持变音,还要能获取到metering以绘制录音过程的声音强弱变化图,播放功能包括音频波形图的绘制以及音频播放。 在做之前,我对iOS...
继续阅读 »

最近,工作之余,自己做了一个项目,项目的一期主要功能是音频录制和播放,音频格式包含m4a、mp3、wav三种格式,录制过程中要支持变音,还要能获取到metering以绘制录音过程的声音强弱变化图,播放功能包括音频波形图的绘制以及音频播放。


在做之前,我对iOS中的录音方面的知识了解甚少, 以至于走了很多弯路,虽然浪费了很多时间,但是也从中学到了很多知识,最终完成了项目的编码。


以下,主要介绍录音方面开发总结,播放方面后续记录。


在iOS中如果想实现录音功能,那么有四种方式可以实现:



  1. AVAudioRecorder :这是最简单的录音方式,只需要配置好录音格式就能得到相应的文件,但是相应的,这种方式无法得到录音过程中的音频源数据,无法实现变音功能和录制mp3文件。

  2. AVAudioEngine:AVAudioEngine功能强大,能实现录音、播放、混响、变音等功能,当我发现我这个类时,我高兴坏了,看了很多文档,结果做demo时,发现这个类外强中干,譬如你不能改变AudioEngine默认的inputNode、outputNode、mainMixNode的数据format,即使你千辛万苦找方法成功改变了数据格式,输出的数据也会狠狠地扇你一巴掌,告诉你高兴太早了,而且inputNode、outputNode、mainMixNode不会自动转换数据格式......如果想录制m4a、mp3还是需要其他方式才能实现。

  3. AudioQueue & AudioFile:AudioQueue在录音过程中有个回调方法,抛出编码后的音频数据,如果想处理音频数据,譬如变音、转码,可以在这个回调中实现,而且能获取到metering数据; AudioFile用于存储回调方法中抛出的音频数据,这个音频数据要与AudioFile创建时配置的inFormat一致。如果想了解audioqueue,可以看看这个文档

  4. AudioUnit &  ExtAudioFile:AudioUnit在录音过程中也会有回调,AudioUnit只支持pcm录制,所以要辅以ExtAudioFile来实现其他音频格式的录制,ExtAudioFile是个宝藏类,这个类可以实现音频格式的自动转换。


如果只是单纯的录用,那么使用AVAudioRecorder就可以非常简单的实现了,如果想对音频数据做处理,那么对于录音过程中抛出的数据请务必是pcm,只要能拿到pcm源数据,任何能做到的音频处理只要你想,都能实现。


iOS中,系统支持的录音编码格式为:


官方参考文档点击查看



wav文件是无损编码格式pcm,m4a文件编码格式AAC,mp3文件iOS系统不支持录制,系统支持mp3文件播放,有解码器但是无相应编码器,如果想录制mp3 文件只能依托于第三方的编码库,我采用的是lame库。


变音


iOS系统中本身是支持变音功能的,AudioUnit中有一个属性kNewTimePitchParam_Pitch,可以改变声音的音色,但是这个属性是MixerUnit的属性,录音的OutputUnit不支持这个属性,要想将 混音MixerUnit和录音OutputUnit连接到一块,需要AUGraph类去连接,但是AUGraph已弃用。。。系统推荐去使用AVAudioEngine类,这个类也是个坑,我在做demo时无法实现m4a文件的录制,就放弃了。


所以变音我采用的也是三方类SoundTouch,SoundTouch支持pcm编码、音频数据是lettle-endian,所以录音过程中抛出的音频buffer的format必须是pcm的,不然无法实现边录音边变音。


这样看起来AudioUnit貌似更适合这个项目一些,但是AudioUnit无法获取到录音过程中的metering数据,真是个令人悲伤的事情,AudioUnit很强大,如果AUGraph不被弃用,可能我会用它来录音。


最终我的实现方案是:


1、wav文件录制:AudioQueue录音、 AudioFile存储文件,AudioQueue和AudioFile的dataFormat为:


AudioStreamBasicDescription mDataFormat;

mDataFormat.mSampleRate = 44100;
mDataFormat.mChannelsPerFrame = 1;
mDataFormat.mReserved = 0;
mDataFormat.mBitsPerChannel = 16;
mDataFormat.mFramesPerPacket = 1;
mDataFormat.mSampleRate = self.model.sampleRate;
mDataFormat.mBytesPerFrame = mDataFormat.mBytesPerPacket = 2;
mDataFormat.mFormatID = kAudioFormatLinearPCM;
mDataFormat.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked;


AudioFile的fileType为kAudioFileCAFType


2、mp3文件:AudioQueue录音, lame转码, FILE文件存储, AudioQueue的编码格式为:


AudioStreamBasicDescription mDataFormat;

mDataFormat.mSampleRate = 44100;
mDataFormat.mChannelsPerFrame = 1;
mDataFormat.mReserved = 0;
mDataFormat.mBitsPerChannel = 16;
mDataFormat.mFramesPerPacket = 1;
mDataFormat.mSampleRate = self.model.sampleRate;
mDataFormat.mBytesPerFrame = mDataFormat.mBytesPerPacket = 2;
mDataFormat.mFormatID = kAudioFormatLinearPCM;
mDataFormat.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger
| kLinearPCMFormatFlagIsPacked
|kAudioFormatFlagIsNonInterleaved


3、m4a文件:AudioQueue录音,  ExtAudioFile转码加存储,AudioQueue和ExtAudioFile的kExtAudioFileProperty_ClientDataFormat属性的编码格式为:


AudioStreamBasicDescription mDataFormat;

mDataFormat.mSampleRate = 44100;
mDataFormat.mChannelsPerFrame = 1;
mDataFormat.mReserved = 0;
mDataFormat.mBitsPerChannel = 16;
mDataFormat.mFramesPerPacket = 1;
mDataFormat.mSampleRate = self.model.sampleRate;
mDataFormat.mBytesPerFrame = mDataFormat.mBytesPerPacket = 2;
mDataFormat.mFormatID = kAudioFormatLinearPCM;
mDataFormat.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked;


ExtAudioFile文件的音频编码格式为:


AudioStreamBasicDescription outputFormat;outputFormat.mSampleRate = 44100;

outputFormat.mFormatID = kAudioFormatMPEG4AAC;outputFormat.mFormatFlags = 0;outputFormat.mBytesPerPacket = 0;outputFormat.mFramesPerPacket = 1024;

outputFormat.mBytesPerFrame = 0;outputFormat.mChannelsPerFrame = 1;outputFormat.mBitsPerChannel = 0;outputFormat.mReserved = 0;


关于四种录音方式的代码实现后续会更新,音频编辑功能,二期可能会上,到时也会研究、记录,希望到时自己不会太懒......


作者:阿喵同学
链接:https://juejin.cn/post/6936869349546426382

收起阅读 »

“杀死” App 上的疑难崩溃!

iOS
问题与背景在移动应用性能方面,崩溃带来的影响是最为严重的,程序崩溃可以打断用户正在进行的操作体验,造成关键业务中断、用户留存率下降、品牌口碑变差、生命周期价值下降等影响。很多公司将崩溃率作为优先级最高的技术指标,因此程序崩溃的监控与收集就成为了一项必不可少的工...
继续阅读 »

问题与背景

在移动应用性能方面,崩溃带来的影响是最为严重的,程序崩溃可以打断用户正在进行的操作体验,造成关键业务中断、用户留存率下降、品牌口碑变差、生命周期价值下降等影响。很多公司将崩溃率作为优先级最高的技术指标,因此程序崩溃的监控与收集就成为了一项必不可少的工作, 目前58同城App使用腾讯Bugly作为发布环境下App异常数据的收集工具。

我们的崩溃率一直在优化,每个版本都有专门负责监控线上崩溃以及解决问题的同学,经过我们不断的优化,目前 58同城iOS App的崩溃率维持在一个比较优秀的水准, Bugly上收集的崩溃大部分都是野指针崩溃和疑难崩溃。但是遗留的疑难崩溃优化手段比较有限,一个主要的原因是Bugly上的崩溃不能正常解析,定位不到真正原因。我们拿一个简单的例子来说明一下。

RN的HOOK函数问题

0 CoreFoundation  0x00000001804f504c 	0x000000018045c000 + 626764
1 Foundation 0x0000000181dae6cc 0x0000000181c7e000 + 1246924
2 UIKit 0x0000000198e5cf30 0x0000000198e57000 + 24368
3 AppName 0xe622388106d79fcc RCTFBQuickPerformanceLoggerConfigureHooks + 16244
4 CoreTelephony 0x0000000198e5e628 0x0000000198e57000 + 30248
5 CoreTelephony 0x0000000108f68fe4 0x0000000198e57000 + 78455260
6 CoreTelephony 0x00000001061ed870 0x0000000198e57000 + 30763624
7 CoreTelephony 0x0000000108f657ec 0x0000000198e57000 + 78440932
8 AppName 0x0000000108f67024 _ZN6tflite19AcquireFlexDelegateEv + 78447132
9 Foundation 0x0000000108f67024 _NSGetUsingKeyValueGetter + 88

在近几个版本中,我们发现Bugly上有大量的崩溃日志都会携带一个来自RN的函数调用栈: RCTFBQuickPerformanceLoggerConfigureHooks,这是一个RNHOOK函数。多条崩溃日志的堆栈都指向这个函数,且这个函数是一个空函数,没有任何实现,这让我们比较困扰。用过Bugly的同学都知道,Bugly每条崩溃日志都有个跟踪数据,记录着这个崩溃发生之前页面的跟踪日志,通过页面的跟踪日志我们发现这些崩溃中用户浏览的页面大多数都不涉及RN业务,与RN没有任何关系。而且每条崩溃的页面跟踪日志也不相同。既然程序崩溃之前浏览的业务不涉及RN但Bugly上的堆栈确指向RN,因此我们怀疑这种崩溃不是崩溃在RNHOOK函数上并且它们是不同错误导致的崩溃。带着这种疑问,我们开始验证这个猜想,来看一看我们的怀疑是否准确。

如何验证Bugly解析错误

因为Bugly无法拿到应用崩溃后所产生的ips文件,无法利用symbolicatecrash等工具符号化日志。因此我们采用atos命令来验证我们的怀疑是否正确。

1. atos验证

atos工具会输出崩溃的代码语句和它所在的文件以及行数,前置条件是需要拿到dSYM文件,确定手机架构是arm64还是armv7,还需要拿到atos需要的load-addressaddress,根据这些信息就能够找到问题所在。aots命令格式如下:

atos -o yourAppName.app.dSYM/Contents/Resources/DWARF/yourAppName -arch arm64/armv7 -l <load-address> <address>

怎么获取dSYM文件与架构这里就不做详细介绍了,我们来看一下怎么在Bugly的崩溃日志中拿到load-addressaddress

一般以app命名的地方就是崩溃的位置,例如:正常的一个崩溃日志格式为:

0x0000000103ef6970 0x0000000102728000 + 30252

其中0x0000000103ef6970为运行地址,就是atos需要的address0x0000000102728000为运行起始地址,就是atos需要的load address302522为偏移量,一般来说,偏移量 + 运行起始地址 = 运行地址。

介绍完atos需要的load address(运行起始地址)与address(运行地址)之后,再来看一下RCTFBQuickPerformanceLoggerConfigureHooks这个函数的崩溃,根据图中示例我们看到这个崩溃的运行地址为0xe622388106d79fcc,但是这个崩溃地址是错误的,一般地址小于0xFFFFFFFFFF,示例中明显大很多。因此我需要将高位地址清洗,清洗后此地址为0x106d79fcc。因此address0x106d79fcc

接下来我们打开Bugly其他信息一栏,看到App base addr(基地址):0x0000000102604000,这个就是atos需要需要的load address。 image20211116110000581.png 通过上述信息,我们以RCTFBQuickPerformanceLoggerConfigureHooks这个函数为例验证一下Bugly的解析结果是否正确

➜ atos -o AppName.app.dSYM/Contents/Resources/DWARF/AppName -arch arm64 -l 0x0000000102604000 0x0000000106d79fcc
-[NSMutableDictionary(YJKit) yjKit_setObject:forKey:] (in AppName) (YJKit.m:432)

结果发现atos符号化后的结果与Bugly给我们的结果确实不一致。再根据Bugly的页面跟踪数据我们确认atos符号化后的结果是正确的,这与我们的怀疑是一致的。

既然Bugly的堆栈错误的指向了这个RN的空函数,那么我们就来看一看源码中RCTFBQuickPerformanceLoggerConfigureHooks是怎样的存在。

**2. 源码中的RCTFBQuickPerformanceLoggerConfigureHooks**函数

RCTFBQuickPerformanceLoggerConfigureHooks函数在源码中的声明如下:

image-20211116110100992.png 源码中,这个函数没有任何实现,完全是一个空函数。将RCT__EXTERN 展开后为__attribute__((visibility("default"))),其作用为将RCTFBQuickPerformanceLoggerConfigureHooks向外界暴露,如果外界存在同名函数,那么RCTFBQuickPerformanceLoggerConfigureHooks会报符号冲突的错误。这里利用__attribute__((weak))RCTFBQuickPerformanceLoggerConfigureHooks声明为弱符号,当外界有同名函数时,SDK内部调用外届的函数,否则调用内部空函数,这个弱符号在RN里起到了HOOK的作用 ,接下来我们就详细的了解一下弱符号。

3. 弱符号__attribute__ ((weak))

在一个程序中,无论是变量名,还是函数名,在编译器的眼里,就是一个符号而已,符号可以分为强符号和弱符号。对于C/C++来说,编译器默认函数和初始化了的全局变量为强符号,未初始化的全局变量是弱符号,强符号和弱符号在程序编译连接过程中一般遵循下面三个规则:

  1. 不允许强符号被多次定义。如果有多个强符号,会报符号重定义错误

  2. 如果有一个强符号,其他定义都是弱符号,则选择强符号

  3. 如果一个符号在所有文件中都是弱符号,则选择其中一个占用空间最大的

强弱符号规则定义摘选自:强符号和弱符号,强引用和弱引用

duplicate symbol '_OBJC_CLASS_$_XXX'这个错误大家应该都比较熟悉,通过错误的描述我们很容易就可以知道这是因为在链接的时候有重复的符号。在编译时,编译器向汇编器输出每个全局符号,若两个或两个以上全局符号(函数或变量名)名字一样,且都是强符号就会出现符号重定义错误,如果有一个是弱符号(weak symbol),则不会出现问题。

一个程序内同时存在强符号与弱符号时,链接器会忽略弱符号,去使用普通的全局符号来解析所有对这些符号的引用,但当普通的全局符号不可用时,链接器会使用弱符号。当有函数或变量名可能被用户覆盖时,该函数或变量名可以声明为一个弱符号。可以通过__attribute__((weak))来定义弱符号。

4. 弱符号的使用

在开发中,假如我们不确定外部模块是否提供一个函数func,但是我们不得不用这个函数,即自己模块的代码必须用到func函数:

extern int func(void);
...int a = func;
...

我们不知道func函数是否被定义了,这会导致2个结果:

  1. 外部存在这个函数func,那么在我自己的模块使用这个函数func,正确。
  2. 外部如果不存在这个函数,那么我们使用func,程序直接崩溃。

所以这个时候,__attribute__((weak)) 派上了用场,在自己的模块中定义:

int __attribute__((weak)) func(......)
{
return 0;
}

将本模块的func转成弱符号类型,如果遇到强符号类型(即外部模块定义了func),那么我们在本模块执行的func将会是外部模块定义的func。如果外部模块没有定义,那么,将会调用这个弱符号。

我们发现Bugly对某些没有解析正确的崩溃,堆栈都会定位到项目中的弱符号上,同时我们还发现在58同城App中,Bugly不单单定位到RCTFBQuickPerformanceLoggerConfigureHooks这一个弱符号上,还有大量的崩溃定位到了其他的弱符号上。

上面我们通过atos还原了正确的日志,并定位到了是弱符号的问题,下面我们结合符号表来看一下日志符号化的原理。

如何处理bugly解析异常的数据

Crash 日志在被符号化之前是不可读的,所谓符号化就是把堆栈信息解释成源码里可读的函数名或方法名,也就是所谓的符号。只有符号化成功后,Crash 日志才能更好的帮助开发者定位问题。日志的解析需要用到dSYM文件,dSYM指的是 Debug Symbols, 也就是调试符号。

DWARF是一种被众多编译器和调试器使用的用于支持源码级别调试的调试文件格式,该格式是一个固定的数据格式,dSYM就是按照DWARF格式保存调试信息的文件,我们常常称为符号表文件。

日志的符号化有很多种方式,例如xcode分析、symbolicatecrashatosdwarfdump等,本质其实就是查找崩溃指令在符号表哪个函数的指令区间。今天我们主要讲一下Bugly解析不准的日志怎么在符号表里查找出正确的堆栈。

1. Bugly还原正确堆栈的原理

以弱符号RCTFBQuickPerformanceLoggerConfigureHooks函数为例,还原一下日志的解析原理。

0 CoreFoundation  0x00000001804f504c 	0x000000018045c000 + 626764
1 Foundation 0x0000000181dae6cc 0x0000000181c7e000 + 1246924
2 UIKit 0x0000000198e5cf30 0x0000000198e57000 + 24368
3 AppName 0xe622388106d79fcc RCTFBQuickPerformanceLoggerConfigureHooks + 16244
4 CoreTelephony 0x0000000198e5e628 0x0000000198e57000 + 30248
5 CoreTelephony 0x0000000108f68fe4 0x0000000198e57000 + 78455260
6 CoreTelephony 0x00000001061ed870 0x0000000198e57000 + 30763624
7 CoreTelephony 0x0000000108f657ec 0x0000000198e57000 + 78440932
8 AppName 0x0000000108f67024 _ZN6tflite19AcquireFlexDelegateEv + 78447132
9 Foundation 0x0000000108f67024 _NSGetUsingKeyValueGetter + 88

  • 上图中我们看到 RCTFBQuickPerformanceLoggerConfigureHooks 这行调用栈的虚拟内存地址存在异常,一般地址地址小于0xFFFFFFFFFF ,示例中明显大很多。我们将高位地址清洗后来保证堆栈正常。调整后,地址为 0x106d79fcc,但当然不是每个Bugly解析错误的日志虚拟内存地址都异常,如果是正常的,则不用改变
  • 查看其他信息,找到基地址App base addr,此处为 0x102604000。如果崩溃发生在其他动态库,那么查找下方对应动态库的地址。
  • 经过第一步和第二步,我们获取到了 0x106d79fcc 和 0x102604000
  • 指令偏移地址为:0x4775FCC = (步骤1)0x106d79fcc - (步骤2)0x102604000
  • 找到此次打包对应的Bugly符号表,并以文本的方式打开
  • 查找0x4775FCC在哪一行符号区间内
  • 最终查找到其在 0x4775fb4 ≤ 0x4775FCC < 0x4775fd0,即3997407行的符号,符号区间遵循前闭后开原则

image20211112210415260.png 通过以上步骤我们找到了RCTFBQuickPerformanceLoggerConfigureHooks函数的实际崩溃位置,并且与我们用atos工具验证后的结果一致,说明这个结果是正确的。

上面我们在符号表里查找到Bugly解析错误的日志的正确堆栈,那如果没有符号表怎么呢,这就涉及到了提取符号表。

2. 如何提取符号表

如果符号表丢失了,但是代码没有改动,那么可以尝试在相同的环境下重新编译和提取符号表,这个步骤有两个前提 1. 代码要与之前保持一致 2. 编译和链接环境都相同,防止由于Debug/Relase对最终包有影响。如果是Debug包,可以用过dsymutil xxx.app/xxx -o xxx.dSYM 来提取符号表

有了以上两个前提就可以通过dSYM文件来提取符号表了,目前我们实现了Bugly轻量符号表的提取,并且文件体积相对于Bugly符号表体积减少到60%。推动ICI(58项目管理平台)按照一定规则输出符号表,目前可以做到根据崩溃日志的UUID直接下载对应的符号表,日志解析和问题排查效率极大提高。

3. 无符号表符号化日志

如果既找不到符号表(dSYM文件或symbol文件),也无法恢复到原先的代码重新生成符号表,那么可以考虑借助无符号表符号化工具 WBBlades 来还原日志:github.com/wuba/WBBlad…

WBBlades是基于Mach-O文件解析的工具集,包括未使用代码检测(支持ObjCSwift)、应用程序大小分析、不需要dSYM文件的日志恢复。

由于方案自身的限制,目前还不能解析除了OC方法以外的崩溃日志,如:block的崩溃、自定义C函数的崩溃。后续需要考虑如何将block的崩溃日志进行符号化。

优化成果与收益

现在我们知道了当Bugly解析不准的时候,我们可以利用Bugly给我们提供的其他信息在符号表里找到正确的答案。通过以上研究,我们通过自研解析工具重新对Bugly的日志进行符号化,通过工具我们在集团内部解决了除RNHOOK函数问题以外还解决了多个遗留已久的历史版本崩溃问题,这里简单的介绍几个比较有代表性的。

1. 拿不到基地址的问题

通过RCTFBQuickPerformanceLoggerConfigureHooks函数的崩溃的介绍,我们可以在Bugly的其他信息里获取到日志的基地址,通过这个地址我们不论是用atos验证还是手动在符号表里查找都可以还原正确的堆栈,但是如果Bugly的其他信息里没有基地址怎么办,我们来看一下下面的这种崩溃日志。

0 CoreFoundation 0x00000001835891b8 0x0000000183459000 + 1245624
5 UIKit 0x000000018963a660 0x000000018942f000 + 2143840
6 AppName 0x00000001075c9904 str_to_integral_8ExpectedIT_NS_14Conversion + 1950052
7 AppName 0x000000010627f94c RCTFBQuickPerformanceLoggerConfigureHooks + 3098344
8 AppName 0x00000001062015a0 RCTFBQuickPerformanceLoggerConfigureHooks + 2581308
9 AppName 0x00000001061fe498 RCTFBQuickPerformanceLoggerConfigureHooks + 2568756
10 AppName 0x00000001061fed38 RCTFBQuickPerformanceLoggerConfigureHooks + 2570964
11 AppName 0x00000001061ed900 RCTFBQuickPerformanceLoggerConfigureHooks + 2500252
12 AppName 0x0000000105231bd8 _ZZGetAppIdTableEvE12arAppIdTable + 57325128
13 libdispatch.dylib 0x00000001824121fc 0x0000000182411000 + 4604
21 UIKit 0x00000001894a4534 UIApplicationMain + 208
22 AppName 0x00000001085b73e8 _ZN15CTXAppidConvert13GetAppIdTableEv + 8521236
23 libdyld.dylib 0x00000001824455b8 0x0000000182441000 + 17848

通过上面的堆栈信息我们看到崩溃的调用栈也停留在了弱符号RCTFBQuickPerformanceLoggerConfigureHooks上,但与我们上面举的例子的不同点是这个崩溃在Bugly上的其他信息一栏里是空的,也就是拿不到基地址,因此我们使用atos命令是不可行的,所以只能在符号表里查找,但是我们要首先要拿到基地址。下面我们来看一下遇到这种情况该怎样拿到基地址。

  1. 首先我们看到 22 AppName 0x00000001085b73e8 str_to_integral_8ExpectedIT_NS_14Conversion + 8521236 这一行信息,熟悉Bugly与crash 日志的同学一定知道, 这一行大概率是main函数,那么我们就在这里找到突破口。
  2. 我们看到main函数的调用栈符号是_ZN15CTXAppidConvert13GetAppIdTableEv,这个函数运行地址是0x00000001085b73e8
  3. 那这个函数的运行起始地址为 0x00000001085b73e8 - 0x8521236 = 0x107D96DD4
  4. 打开符号表,找到_ZN15CTXAppidConvert13GetAppIdTableEv这个符号的偏移地址为 0x7d86dd4
  5. App base addr(基地址): 0x107D96DD4 - 0x7d86dd4 = 0x100010000

这样我们就拿到了这个日志的基地址,然后利用上面的方式在符号表里找到正确的堆栈,因此也就能将这个日志正确的解析了。

2. 百度地图SDK的崩溃问题

除了RNHOOK函数问题,我们还发现有大量的崩溃日志调用栈都指向了百度地图SDK。 image20211116110252571.png 首先我们通过Bugly显示的堆栈信息以为是百度地图SDK的崩溃,这个崩溃在某几个版本中占58同城App总崩溃率的40%左右,是58App内崩溃率最高的一个模块,在更换了新的SDK后崩溃率也并没有下降,而这么高的崩溃率,我们在开发与测试中却从未遇到过。通过Bugly上的跟踪数据我们看到最后的页面记录停留在了金融业务内,而金融业务与百度地图没有任何关系。因此这个崩溃应该与上面描述的一样,解析错误。拿到基地址与运行地址,通过我们自研的工具拿到了正确的堆栈。结果为金融业务使用的一个人脸识别SDK的崩溃,文件名称与Bugly上的跟踪日志也相同。

3. 安居客IM登录问题

我们编写了脚本文件,利用脚本文件定位了一个安居客存在很久的问题。在Bugly上排名比较靠前,崩溃占比很高,Bugly上的堆栈显示异常,因此这个崩溃之前并没有定位到具体原因。脚本协助安居客定位是IMSDK的原因。 image20211116110315205.png

以上是几个比较具有代表性的Bugly解析错误的日志,我们通过研究分析将这些错误的堆栈还原正确并解决了问题。

目前我们支持按版本自动排查出Bugly上前200名崩溃中解析异常的日志,并且可以将异常日志自动符号化成正确的日志。整个过程在符号表已经提前下载并解析好的前提下,只有10秒左右,大大提升了我们日常研发以及解决问题的效率。通过对Bugly上的疑难崩溃的治理,目前为止我们修复了Bugly上70%左右的疑难崩溃,大大降低了58 App的崩溃率。 image20211125150625544.png 除了上述我们研究的Bugly解析异常的日志可以正确解析外,58同城还支持其他异常日志的解析。 例如App内存在段迁移发生崩溃后的日志,段迁移崩溃日志中的库名变成了异常字符、丢失了进程的起始地址,获取到错误的偏移地址,这种情况下我们可以进行自动修正并解析出正确的堆栈信息。

总结与展望

文本首先介绍了我们使用Bugly遇到的RNHOOK函数问题,通过这个问题我们提出Bugly可能解析存在错误的疑问,后续用atos命令以及符号表排查找到了正确的答案,过程中又发现了弱符号的问题。按照这个研究方向我们在集团内做了一系列工具并解决了多个版本的历史遗留问题,大大的降低了58同城iOS App的崩溃率,也提高了日常工作研发效率。

App的性能优化对用户的体验十分重要,而崩溃作为其中最重要的一个环节需要我们持续的钻研与探索。后续我们将持续优化App的性能给用户带来最好的体验。

首发自CSDN:“杀死” App 上的疑难崩溃!


作者:ZYJ
链接:https://juejin.cn/post/7037308047382806565

收起阅读 »

Flutter定制一个ScrollView嵌套webview滚动的效果

场景描述 业务需要在一个滚动布局中嵌入一个webview,但是在Android平台上有一个关于webview高度的bug: 当webview高度过大时会导致Crash甚至手机重启。所以我想到了这样一种布局:最外层是一个ScrollView,内部含有一个定高的可...
继续阅读 »

场景描述


业务需要在一个滚动布局中嵌入一个webview,但是在Android平台上有一个关于webview高度的bug: 当webview高度过大时会导致Crash甚至手机重启。所以我想到了这样一种布局:最外层是一个ScrollView,内部含有一个定高的可以滚动的webview。这里有两个问题:



  1. webview怎么滚动

  2. webview的滚动怎么和外部的ScrollView联动


解决方案


第一个问题可以通过设置gestureRecognizers解决:
gestureRecognizers: [Factory(() => EagerGestureRecognizer())].toSet(),


但是这种方法会导致webview在手势竞争中获胜,外部的ScrollView根本无法获得滚动事件,从而导致webview滚动完全独立于外部ScrollView的滚动,这也是这种布局很少出现的原因。


于是我想到了使用NestedScrollView的方案,但是很明显我需要重新定义,因为我最终想要的效果是这样子的:



OutScrollView 滑动或者Fling时InnerScrollView完全静止。


在滚动InnerScrollView时OutScrollView完全不会滑动,只有在InnerScrollView滑动到边界时才能滑动OutScrollView。如果InnerScrollView Fling, OutScrollView不会Fling,同样的在InnerScrollView边界Fling则会触发OutScrollView的Fling。


下面就是具体方案:
NestedScrollView介入滚动是靠自定义ScrollActivityDelegate开始的,scrollable.dart源码中展示了滚动手势的传递过程:


Scrollable->GestureRecorgnizer->Drag(ScrollDragController)->ScrollActivityDelegate


当用户手指拖动ScrollView时会调用:


ScrollDragController:
@override
void update(DragUpdateDetails details) {
//other codes
delegate.applyUserOffset(offset);
}

当拖动结束时调用:


@override
void end(DragEndDetails details) {
///other codes, goBallistic代表Fling
delegate.goBallistic(velocity);
}

所以自定义ScrollActivityDelegate就是Hook滚动的开始,在NestedScrollView中这个类是_NestedScrollCoordinator, 所以我的思路就是自己定义一个Delegate。下面是魔改的过程:


需要判断InnerScrollView是否在滚动


我强制InnerScrollView必须被我的自定义Widget包裹:


class _NestedInnerScrollChildState extends State<NestedInnerScrollChild> {
@override
Widget build(BuildContext context) {
return Listener(
child: NotificationListener<ScrollEndNotification>(
child: widget.child,
onNotification: (end) {
widget.coordinator._innerTouchingKey = null;
//继续向上冒泡
return false;
},
),
onPointerDown: _startScrollInner,
);
}

void _startScrollInner(_) {
widget.coordinator._innerTouchingKey = widget.scrollKey;
}
}

我使用了Listener onPointerDown 方法来判断用户触摸了inner view, 但是并没有使用onPointerUp或者onPointerCancel来判断滚动结束,原因就是Fling的存在,Fling效果下手指已经离开屏幕但是view可能还在滑动,因此使用ScrollEndNotification这个标记更靠谱。


OutScrollView滑动时完全禁止InnerScrollView的滑动



  1. applyUserOffset的hook


  @override
void applyUserOffset(double delta) {
if (!innerScroll) {
_outerPosition.applyFullDragUpdate(delta);
}
}


  1. Fling


首先会调用Coordinator的goBallistic方法,然后触发beginActivity方法,我们直接在beginActivity中拦截即可:


///_innerPositions并不是所有innerView的集合,这个后面会讲到
if (innerScroll) {
for (final _NestedScrollPosition position in _innerPositions) {
final ScrollActivity newInnerActivity = innerActivityGetter(position);
position.beginActivity(newInnerActivity);
scrolling = newInnerActivity.isScrolling;
}
}

InnerScrollView和OutScrollView嵌套滑动



  1. applyUserOffset


借鉴NestedScrollView即可


@override
void applyUserOffset(double delta) {
double remainDelta = innerPositionList.first.applyClampedDragUpdate(delta);
if (remainDelta != 0.0) {
_outerPosition.applyFullDragUpdate(remainDelta);
}
}


  1. Fling


innerView触发Fling手势的调用链:ScrollDragController会调用ScrollActivityDelegate的goBallistic方法->触发ScrollPosition的beginActivity方法并创建BallisticScrollActivity实例->BallisticScrollActivity实例结合Simulation不断计算滚动距离。


BallisticScrollActivity有个方法:


 /// Move the position to the given location.
///
/// If the new position was fully applied, returns true. If there was any
/// overflow, returns false.
///
/// The default implementation calls [ScrollActivityDelegate.setPixels]
/// and returns true if the overflow was zero.
@protected
bool applyMoveTo(double value) {
return delegate.setPixels(value) == 0.0;
}

当这个方法返回false时就会立刻停止滚动,正好NestedScrollView有创建自定义OutBallisticScrollActivity方法,所以我在applyMove那里判断如果是innerView 正在滚动就返回false


  @override
bool applyMoveTo(double value) {
if (coordinator.innerScroll) {
return false;
}
// other codes
}

当然,这里也可以加个优化:比如innerView如果在边界触发了Fling就可以放开。


支持多个inner scroll view


outview只能有一个,但是innerView理论上可以有多个,我这里贴下参考的文章链接[:]("Flutter 扩展NestedScrollView (二)列表滚动同步解决 - 掘金 (juejin.cn)")。核心就是在ScrollController attach detach时实现position和ScrollView的绑定。


实现webview的滚动


这里我也是借鉴的大神的思路[:](大道至简:Flutter嵌套滑动冲突解决之路 - V大师在一号线 (vimerzhao.top))


Flutter中所有的滚动View最终都是用Scrollable+Viewport来实现的,Scrollable负责获取滚动手势,距离计算等,而绘制则交给Viewport来实现。翻看viewport.dart相关源码,我贴下paint的方法:


@override
void paint(PaintingContext context, Offset offset) {
if (firstChild == null)
return;
if (hasVisualOverflow && clipBehavior != Clip.none) {
_clipRectLayer.layer = context.pushClipRect(
needsCompositing,
offset,
Offset.zero & size,
_paintContents,
clipBehavior: clipBehavior,
oldLayer: _clipRectLayer.layer,
);
} else {
_clipRectLayer.layer = null;
_paintContents(context, offset);
}
}

void _paintContents(PaintingContext context, Offset offset) {
for (final RenderSliver child in childrenInPaintOrder) {
if (child.geometry!.visible)
context.paintChild(child, offset + paintOffsetOf(child));
}
}

paintOffsetOf(child)就可以简化为滚动导致的绘制偏差。举个栗子:一个viewport高500,内容高度1000,默认绘制[0-500]的内容,当用户向上滑动了100,则绘制[100,600]的内容,这里的100就是paintOffset。


所以我最后创建了一个自定义Viewport,但是Flutter端绘制时paintOffset始终传0,我把真正的offset传递给webview,然后调用window.scrollTo(0,offset)即可实现webview内容的滑动了。简而言之,传统的ScrollView是内容不动,画布在动,而我的方案就是画布不动,但是内容在动。参考代码:[]("inner_scroll_webview.dart (github.com)")


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

iOS 实现类似探探、陌陌的卡片左滑右滑效果

iOS
本文章分析怎么实现这种卡片效果以及都有哪些功能,基于这些功能是否可以完善,让框架更加灵活,可拓展等。现已封装成通用框架。效果图如下: 代码地址 1、功能分析 不管是探探还是陌陌的点点匹配模块,都是对卡片的左滑右滑进行的操作,那么以陌陌的点点匹配模块分析,所涉...
继续阅读 »

本文章分析怎么实现这种卡片效果以及都有哪些功能,基于这些功能是否可以完善,让框架更加灵活,可拓展等。现已封装成通用框架。效果图如下:


效果图


代码地址


1、功能分析


不管是探探还是陌陌的点点匹配模块,都是对卡片的左滑右滑进行的操作,那么以陌陌的点点匹配模块分析,所涉及的功能有:



  • 卡片复用机制

  • 拖拽卡片时的动画

    • 卡片拖拽左上方、左下方、右上方、右下方都会有相应的旋转动画



  • 喜欢功能

    • 喜欢功能又分为拖拽卡片向右侧滑出、点击下方的喜欢按钮控制卡片滑出。



  • 不喜欢功能

    • 不喜欢功能也是和喜欢功能逻辑一样,只是方向不同。



  • 超级喜欢功能

    • 超级喜欢功能跟喜欢功能一样,只不过是针对的vip用户,普通用户需要充值vip才能使用,vip用户触发超级喜欢,会有一个炫丽的动画特效,并进行其它业务处理



  • 回退功能

    • 回退功能也是针对vip用户设计的,vip用户单次只能回退一张卡片。我们实现回退多张功能,让外界控制是否可以回退功能,更加灵活。



  • 预加载功能

    • 卡片可以无限的滑走,那么数据源获取就得支持加载更多数据。



  • 无数据的处理功能

    • 卡片数据操作完了,就得处理无数据的情况,显示占位图等等。



  • 触发卡片喜欢功能时,需要检测是否允许此次操作

    • 这个功能也是根据业务去做,探探里面,好像对喜欢的操作是有限制的,如果超出了这个限制,再次触发喜欢功能就会提示充值vip同时拖拽的卡片也会恢复原位。






2、功能实现


完成了上面的功能分析之后,接下来就可以一个一个的去实现了。




  • 卡片复用机制


    这里用了4张卡片,最上面的卡片划走之后,会被放在最下面一层,达到复用。




  • 拖拽卡片时的动画


    卡片拖拽左上方、左下方、右上方、右下方都会有相应的旋转动画。我们可以确定用户手势触发的点的位置,根据方位进行设置相关的旋转角度,左右是相反的。




  • 喜欢、不喜欢功能


    1、通过拖拽手势划走卡片
    给每个卡片添加一个拖拽手势,当拖拽卡片的时候,根据拖拽的距离和卡片原始的中心点X值进行判断卡片是向左还是向右,拖拽结束的时候,通过改变卡片的位置并加上动画,达到卡片划走的效果。


    2、通过按钮触发划走卡片
    按钮触发的时候,指定卡片的x位置。然后内部统一走手势结束的方法。




  • 超级喜欢功能
    超级喜欢功能,其实也是喜欢的一种,通过按钮触发喜欢操作,之后加上自己的炫丽动画以及业务逻辑.




  • 回退功能


    卡片回退的实现,将最底下的卡片放到最上面的卡片上面并加上入场动画,同时更新对应的索引数据。
    陌陌vip用户只支持回退一张,我们可以设计支持多张,有多少张不喜欢卡片,默认回退多少张,如果想要实现一张也可以,外界可以控制。相关的方法如下:

    这里回退操作的场景很多,比如:左滑10次,然后右滑4次,回退5次,在一次次的滑动,那么怎么保证卡片是按照正常的顺序显示的呢。这里用了2个数组处理的,第一个数组保存左滑的数据index,第二个数组保存回退的index。具体的思路看下截图:




  • 预加载功能
    每次划走一个卡片,都会代理回调对应的数据源Index供上层更新底部的卡片显示内容, 卡片划走的时候,也会做校验,看看当前的index相对于数据源总数是否小于一个值,这个值我们称为阀值。小于这个阀值会触发加载更多的代理回调。




  • 无数据的处理功能
    每次划走一个卡片,都会更新底部卡片显示内容,如果内部卡片的数据index超出了外界的数据源总数,则将卡片内容隐藏,也会做无数据的检测。




  • 触发卡片喜欢功能时,需要检测是否允许此次操作
    可以在拖拽手势结束的时候,通过代理去询问是否允许滑走,如果不允许则内部更改拖动的距离x值,走复位逻辑。






3、总结


卡片交互的细节很多,很多控制的地方也很多,封装的框架现已支持上面的所有功能, 使用的时候,可以自定义卡片cell实现自己的样式。提供的有示例demo. 欢迎预览。


作者:大大的太阳
链接:https://juejin.cn/post/7036769362652430372
收起阅读 »

现代配置指南——YAML 比 JSON 高级在哪?

一直以来,前端工程中的配置大多都是 .js 文件或者 .json 文件,最常见的比如:package.jsonbabel.config.jswebpack.config.js这些配置对前端非常友好,因为都是我们熟悉的 JS 对象结构。一般静态化的配置会选择 j...
继续阅读 »

一直以来,前端工程中的配置大多都是 .js 文件或者 .json 文件,最常见的比如:

  • package.json

  • babel.config.js

  • webpack.config.js

这些配置对前端非常友好,因为都是我们熟悉的 JS 对象结构。一般静态化的配置会选择 json 文件,而动态化的配置,涉及到引入其他模块,因此会选择 js 文件。

还有现在许多新工具同时支持多种配置,比如 Eslint,两种格式的配置任你选择:

  • .eslintrc.json

  • .eslintrc.js

后来不知道什么时候,突然出现了一种以 .yaml.yml 为后缀的配置文件。一开始以为是某个程序的专有配置,后来发现这个后缀的文件出现的频率越来越高,甚至 Eslint 也支持了第三种格式的配置 .eslintrc.yml

既然遇到了,那就探索它!

下面我们从 YAML 的出现背景使用场景具体用法高级操作四个方面,看一下这个流行的现代化配置的神秘之处。

出现背景

一个新工具的出现避免不了有两个原因:

  1. 旧工具在某些场景表现吃力,需要更优的替代方案

  2. 旧工具也没什么不好,只是新工具出现,比较而言显得它不太好

YAML 这种新工具就属于后者。其实在 yaml 出现之前 js+json 用的也不错,也没什么特别难以处理的问题;但是 yaml 出现以后,开始觉得它好乱呀什么东西,后来了解它后,越用越喜欢,一个字就是优雅。

很多文章说选择 yaml 是因为 json 的各种问题,json 不适合做配置文件,这我觉得有些言过其实了。我更愿意将 yaml 看做是 json 的升级,因为 yaml 在格式简化和体验上表现确实不错,这个得承认。

下面我们对比 YAML 和 JSON,从两方面分析:

精简了什么?

JSON 比较繁琐的地方是它严格的格式要求。比如这个对象:

{
 name: 'ruims'
}

在 JSON 中以下写法通通都是错的:

// key 没引号不行
{
 name: 'ruims'
}
// key 不是 "" 号不行
{
 'name': 'ruims'
}
// value 不是 "" 号不行
{
 "name": 'ruims'
}

字符串的值必须 k->v 都是 "" 才行:

// 只能这样
{
 "name": "ruims"
}

虽然是统一格式,但是使用上确实有不便利的地方。比如我在浏览器上测出了接口错误。然后把参数拷贝到 Postman 里调试,这时就我要手动给每个属性和值加 "" 号,非常繁琐。

YAML 则是另辟蹊径,直接把字符串符号干掉了。上面对象的同等 yaml 配置如下:

name: ruims

没错,就这么简单!

除了 "" 号,yaml 觉得 {}[] 这种符号也是多余的,不如一起干掉。

于是呢,以这个对象数组为例:

{
 "names": [{ "name": "ruims" }, { "name": "ruidoc" }]
}

转换成 yaml 是这样的:

names:
- name: ruims
- name: ruidoc

对比一下这个精简程度,有什么理由不爱它?

增加了什么?

说起增加的部分,最值得一提的,是 YAML 支持了 注释

用 JSON 写配置是不能有注释的,这就意味着我们的配置不会有备注,配置多了会非常凌乱,这是最不人性化的地方。

现在 yaml 支持了备注,以后配置可以是这样的:

# 应用名称
name: my_app
# 应用端口
port: 8080

把这种配置丢给新同事,还怕他看不懂配了啥吗?

除注释外,还支持配置复用的相关功能,这个后面说。

使用场景

我接触的第一个 yaml 配置是 Flutter 项目的包管理文件 pubspec.yaml,这个文件的作用和前端项目中的 package.json 一样,用于存放一些全局配置和应用依赖的包和版本。

看一下它的基本结构:

name: flutter_demo
description: A new Flutter project.

publish_to: 'none'
version: 1.0.0

dependencies:
cupertino_icons: ^1.0.2

dev_dependencies:
flutter_lints: ^1.0.0

你看这个结构和 package.json 是不是基本一致?dependencies 下列出应用依赖和版本,dev_dependencies 下的则是开发依赖。

后来在做 CI/CD 自动化部署的时候,我们用到了 GitHub Action。它需要多个 yaml 文件来定义不同的工作流,这个配置可比 flutter 复杂的多。

其实不光 GitHub Action,其他流行的类似的构建工具如 GitLab CI/CDcircleci,全部都是齐刷刷的 yaml 配置,因此如果你的项目要做 CI/CD 持续集成,不懂 yaml 语法肯定是不行的。

还有,接触过 Docker 的同学肯定知道 Docker Compose,它是 Docker 官方的单机编排工具,其配置文件 docker-compose.yml 也是妥妥的 yaml 格式。现在 Docker 正是如日中天的时候,使用 Docker 必然免不了编排,因此 yaml 语法早晚也要攻克。

上面说的这 3 个案例,几乎都是现代最新最流行的框架/工具。从它们身上可以看出来,yaml 必然是下一代配置文件的标准,并且是前端-后端-运维的通用标准。

说了这么多,你跃跃欲试了吗?下面我们详细介绍 yaml 语法。

YAML 语法

介绍 yaml 语法会对比 json 解释,以便我们快速理解。

先看一下 yaml 的几个特点:

  • 大小写敏感

  • 使用缩进表示层级关系

  • 缩进空格数不强制,但相同层级要对齐

  • # 表示注释

相比于 JSON 来说,最大的区别是用 缩进 来表示层级,这个和 Python 非常接近。还有强化的一点是支持了注释,JSON 默认是不支持的(虽然 TS 支持),这也对配置文件非常重要。

YAML 支持以下几种数据结构:

  • 对象:json 中的对象

  • 数组:json 中的数组

  • 纯量:json 中的简单类型(字符串,数值,布尔等)

对象

先看对象,上一个 json 例子:

{
 "id": 1,
 "name": "杨成功",
 "isman": true
}

转换成 yaml:

id: 1
name: 杨成功
isman: true

对象是最核心的结构,key 值的表示方法是 [key]:,注意这里冒号后面有个空格,一定不能少。value 的值就是一个纯量,且默认不需要引号。

数组

数组和对象的结构差不多,区别是在 key 前用一个 - 符号标识这个是数组项。注意这里也有一个空格,同样也不能少。

- hello
- world

转换成 JSON 格式如下:

["hello", "world"]

了解了基本的对象和数组,我们再来看一个复杂的结构。

众所周知,在实际项目配置中很少有简单的对象或数组,大多都是对象和数组相互嵌套而成。在 js 中我们称之为对象数组,而在 yaml 中我们叫 复合结构

比如这样一个稍复杂的 JSON:

{
 "name": "杨成功",
 "isman": true,
 "age": 25,
 "tag": ["阳光", "帅气"],
 "address": [
  { "c": "北京", "a": "海淀区" },
  { "c": "天津", "a": "滨海新区" }
]
}

转换成复合结构的 YAML:

name: 杨成功
isman: true
age: 25
tag:
- 阳光
- 帅气
address:
- c: 北京
  a: 海淀区
- c: 天津
  a: 滨海新区

若你想尝试更复杂结构的转换,可以在 这个 网页中在线实践。

纯量

纯量比较简单,对应的就是 js 的基本数据类型,支持如下:

  • 字符串

  • 布尔

  • 数值

  • null

  • 时间

比较特殊的两个,null 用 ~ 符号表示,时间大多用 2021-12-21 这种格式表示,如:

who: ~
date: 2019-09-10

转换成 JS 后:

{
 who: null,
 date: new Date('2019-09-10')
}

高级操作

在 yaml 实战过程中,遇到过一些特殊场景,可能需要一些特殊的处理。

字符串过长

在 shell 中我们常见到一些参数很多,然后特别长的命令,如果命令都写在一行的话可读性会非常差。

假设下面的是一条长命令:

$ docker run --name my-nginx -d nginx

在 linux 中可以这样处理:

$ docker run \
--name my-nginx \
-d nginx

就是在每行后加 \ 符号标识换行。然而在 YAML 中更简单,不需要加任何符号,直接换行即可:

cmd: docker run
--name my-nginx
-d nginx

YAML 默认会把换行符转换成空格,因此转换后 JSON 如下,正是我们需要的:

{ "cmd": "docker run --name my-nginx -d nginx" }

然而有时候,我们的需求是保留换行符,并不是把它转换成空格,又该怎么办呢?

这个也简单,只需要在首行加一个 | 符号:

cmd: |
docker run
--name my-nginx
-d nginx

转换成 JSON 变成了这样:

{ "cmd": "docker run\n--name my-nginx\n-d nginx" }

获取配置

获取配置是指,在 YAML 文件中定义的某个配置,如何在代码(JS)里获取?

比如前端在 package.json 里有一个 version 的配置项表示应用版本,我们要在代码中获取版本,可以这么写:

import pack from './package.json'
console.log(pack.version)

JSON 是可以直接导入的,YAML 可就不行了,那怎么办呢?我们分环境解析:

在浏览器中

浏览器中代码用 webapck 打包,因此加一个 loader 即可:

$ yarn add -D yaml-loader

然后配置 loader:

// webpack.config.js
module.exports = {
 module: {
   rules: [
    {
       test: /\.ya?ml$/,
       type: 'json', // Required by Webpack v4
       use: 'yaml-loader'
    }
  ]
}
}

在组件中使用:

import pack from './package.yaml'
console.log(pack.version)

在 Node.js 中

Node.js 环境下没有 Webpack,因此读取 yaml 配置的方法也不一样。

首先安装一个 js-yaml 模块:

$ yarn add js-yaml

然后通过模块提供的方法获取:

const yaml = require('js-yaml')
const fs = require('fs')

const doc = yaml.load(fs.readFileSync('./package.yaml', 'utf8'))
console.log(doc.version)

配置项复用

配置项复用的意思是,对于定义过的配置,在后面的配置直接引用,而不是再写一遍,从而达到复用的目的。

YAML 中将定义的复用项称为锚点,用& 标识;引用锚点则用 * 标识。

name: &name my_config
env: &env
version: 1.0

compose:
key1: *name
key2: *env

对应的 JSON 如下:

{
 "name": "my_config",
 "env": { "version": 1 },
 "compose": { "key1": "my_config", "key2": { "version": 1 } }
}

但是锚点有个弊端,就是不能作为 变量 在字符串中使用。比如:

name: &name my_config
compose:
key1: *name
key2: my name is *name

此时 key2 的值就是普通字符串 my name is *name,引用变得无效了。

其实在实际开发中,字符串中使用变量还是很常见的。比如在复杂的命令中多次使用某个路径,这个时候这个路径就应该是一个变量,在多个命令中复用。

GitHub Action 中有这样的支持,定义一个环境变量,然后在其他的地方复用:

env:
NAME: test
describe: This app is called ${NAME}

这种实现方式与 webpack 中使用环境变量类似,在构建的时候将变量替换成对应的字符串。

作者:杨成功
来源:https://segmentfault.com/a/1190000041108051

收起阅读 »

LOOK 直播活动地图生成器方案

在最近的活动开发中,笔者就刚好碰到了这个问题。这次活动开发需要完成一款大富翁游戏,而作为一款大富翁游戏,地图自然是必不可少的。在整个地图中,有很多的不同种类的方格,如果一个个手动去调整位置,工作量是很大的。那么有没有一种方案能够帮助我们快速确定方格的位置和种类...
继续阅读 »

对于前端而言,与视觉稿打交道是必不可少的,因为我们需要对照着视觉稿来确定元素的位置、大小等信息。如果是比较简单的页面,手动调整每个元素所带来的工作量尚且可以接受;然而当视觉稿中素材数量较大时,手动调整每个元素便不再是个可以接受的策略了。

在最近的活动开发中,笔者就刚好碰到了这个问题。这次活动开发需要完成一款大富翁游戏,而作为一款大富翁游戏,地图自然是必不可少的。在整个地图中,有很多的不同种类的方格,如果一个个手动去调整位置,工作量是很大的。那么有没有一种方案能够帮助我们快速确定方格的位置和种类呢?下面便是笔者所采用的方法。

方案简述

位点图

首先,我们需要视觉同学提供一张特殊的图片,称之为位点图。

这张图片要满足以下几个要求:

  1. 在每个方格左上角的位置,放置一个 1px 的像素点,不同类型的方格用不同颜色表示。

  2. 底色为纯色:便于区分背景和方格。

  3. 大小和地图背景图大小一致:便于从图中读出的坐标可以直接使用。

bitmap

上图为一个示例,在每个路径方格左上角的位置都有一个 1px 的像素点。为了看起来明显一点,这里用红色的圆点来表示。在实际情况中,不同的点由于方格种类不同,颜色也是不同的。

bitmap2

上图中用黑色边框标出了素材图的轮廓。可以看到,红色圆点和每个路径方格是一一对应的关系。

读取位点图

在上面的位点图中,所有方格的位置和种类信息都被标注了出来。我们接下来要做的,便是将这些信息读取出来,并生成一份 json 文件来供我们后续使用。

const JImp = require('jimp');
const nodepath = require('path');

function parseImg(filename) {
   JImp.read(filename, (err, image) => {
       const { width, height } = image.bitmap;

       const result = [];

       // 图片左上角像素点的颜色, 也就是背景图的颜色
       const mask = image.getPixelColor(0, 0);

       // 筛选出非 mask 位置点
       for (let y = 0; y < height; ++y) {
           for (let x = 0; x < width; ++x) {
               const color = image.getPixelColor(x, y);
               if (mask !== color) {
                   result.push({
                       // x y 坐标
                       x,
                       y,
                       // 方格种类
                       type: color.toString(16).slice(0, -2),
                  });
              }
          }
      }

       // 输出
       console.log(JSON.stringify({
           // 路径
           path: result,
      }));
  });
}

parseImg('bitmap.png');

在这里我们使用了 jimp 用于图像处理,通过它我们能够去扫描这张图片中每个像素点的颜色和位置。

至此我们得到了包含所有方格位置和种类信息的 json 文件:

{
   "path": [
      {
           "type": "",
           "x": 0,
           "y": 0,
      },
       // ...
  ],
}

其中,x y 为方格左上角的坐标;type 为方格种类,值为颜色值,代表不同种类的地图方格。

通路连通算法

对于我们的项目而言,只确定路径点是不够的,还需要将这些点连接成一个完整的通路。为此,我们需要找到一条由这些点构成的最短连接路径。

代码如下:

function takePath(point, points) {
   const candidate = (() => {
       // 按照距离从小到大排序
       const pp = [...points].filter((i) => i !== point);
       const [one, two] = pp.sort((a, b) => measureLen(point, a) - measureLen(point, b));

       if (!one) {
           return [];
      }

       // 如果两个距离 比较小,则穷举两个路线,选择最短连通图路径。
       if (two && measureLen(one, two) < 20000) {
           return [one, two];
      }
       return [one];
  })();

   let min = Infinity;
   let minPath = [];
   for (let i = 0; i < candidate.length; ++i) {
       // 递归找出最小路径
       const subpath = takePath(candidate[i], removeItem(points, candidate[i]));

       const path = [].concat(point, subpath);
       // 测量路径总长度
       const distance = measurePathDistance(path);

       if (distance < min) {
           min = distance;
           minPath = subpath;
      }
  }

   return [].concat(point, minPath);
}

到这里,我们已经完成了所有的准备工作,可以开始绘制地图了。在绘制地图时,我们只需要先读取 json 文件,再根据 json 文件内的坐标信息和种类信息来放置对应素材即可。

方案优化

上述方案能够解决我们的问题,但仍有一些不太方便的地方:

  1. 只有 1px 的像素点太小了,肉眼无法辨别。不管是视觉同学还是开发同学,如果点错了位置就很难排查。

  2. 位点图中包含的信息还是太少了,颜色仅仅对应种类,我们希望能够包含更多的信息,比如点之间的排列顺序、方格的大小等。

像素点合并

对于第一个问题,我们可以让视觉同学在画图的时候,将 1px 的像素点扩大成一个肉眼足够辨识的区域。需要注意两个区域之间不要有重叠。

bitmap3

这时候就要求我们对代码做一些调整。在之前的代码中,当我们扫描到某个颜色与背景色不同的点时,会直接记录其坐标和颜色信息;现在当我们扫描到某个颜色与背景色不同的点时,还需要进行一次区域合并,将所有相邻且相同颜色的点都纳入进来。

区域合并的思路借鉴了下图像处理的区域生长算法。区域生长算法的思路是以一个像素点为起点,将该点周围符合条件的点纳入进来,之后再以新纳入的点为起点,向新起点相邻的点扩张,直到所有符合条件条件的点都被纳入进来。这样就完成了一次区域合并。不断重复该过程,直到整个图像中所有的点都被扫描完毕。

我们的思路和区域生长算法非常类似:

  1. 依次扫描图像中的像素点,当扫描到颜色与背景色不同的点时,记录下该点的坐标和颜色。

    步骤1.png

  2. 之后扫描与该点相邻的 8 个点,将这些点打上”已扫描“的标记。筛选出其中颜色与背景色不同且尚未被扫描过的点,放入待扫描的队列中。

    步骤2.png

  3. 从待扫描队列中取出下一个需要扫描的点,重复步骤 1 和步骤 2。

  4. 直到待扫描的队列为空时,我们就扫描完了一整个有颜色的区域。区域合并完毕。

    步骤3.png

const JImp = require('jimp');

let image = null;
let maskColor = null;

// 判断两个颜色是否为相同颜色 -> 为了处理图像颜色有误差的情况, 不采用相等来判断
const isDifferentColor = (color1, color2) => Math.abs(color1 - color2) > 0xf000ff;

// 判断是(x,y)是否超出边界
const isWithinImage = ({ x, y }) => x >= 0 && x < image.width && y >= 0 && y < image.height;

// 选择数量最多的颜色
const selectMostColor = (dotColors) => { /* ... */ };

// 选取左上角的坐标
const selectTopLeftDot = (reginDots) => { /* ... */ };

// 区域合并
const reginMerge = ({ x, y }) => {
   const color = image.getPixelColor(x, y);
   // 扫描过的点
   const reginDots = [{ x, y, color }];
   // 所有扫描过的点的颜色 -> 扫描完成后, 选择最多的色值作为这一区域的颜色
   const dotColors = {};
   dotColors[color] = 1;

   for (let i = 0; i < reginDots.length; i++) {
       const { x, y, color } = reginDots[i];

       // 朝临近的八个个方向生长
       const seeds = (() => {
           const candinates = [/* 左、右、上、下、左上、左下、右上、右下 */];

           return candinates
               // 去除超出边界的点
              .filter(isWithinImage)
               // 获取每个点的颜色
              .map(({ x, y }) => ({ x, y, color: image.getPixelColor(x, y) }))
               // 去除和背景色颜色相近的点
              .filter((item) => isDifferentColor(item.color, maskColor));
      })();

       for (const seed of seeds) {
           const { x: seedX, y: seedY, color: seedColor } = seed;

           // 将这些点添加到 reginDots, 作为下次扫描的边界
           reginDots.push(seed);

           // 将该点设置为背景色, 避免重复扫描
           image.setPixelColor(maskColor, seedX, seedY);

           // 该点颜色为没有扫描到的新颜色, 将颜色增加到 dotColors 中
           if (dotColors[seedColor]) {
               dotColors[seedColor] += 1;
          } else {
               // 颜色为旧颜色, 增加颜色的 count 值
               dotColors[seedColor] = 1;
          }
      }
  }

   // 扫描完成后, 选择数量最多的色值作为区域的颜色
   const targetColor = selectMostColor(dotColors);

   // 选择最左上角的坐标作为当前区域的坐标
   const topLeftDot = selectTopLeftDot(reginDots);

   return {
       ...topLeftDot,
       color: targetColor,
  };
};

const parseBitmap = (filename) => {
   JImp.read(filename, (err, img) => {
       const result = [];
       const { width, height } = image.bitmap;
       // 背景颜色
       maskColor = image.getPixelColor(0, 0);
       image = img;

       for (let y = 0; y < height; ++y) {
           for (let x = 0; x < width; ++x) {
               const color = image.getPixelColor(x, y);

               // 颜色不相近
               if (isDifferentColor(color, maskColor)) {
                   // 开启种子生长程序, 依次扫描所有临近的色块
                   result.push(reginMerge({ x, y }));
              }
          }
      }
  });
};

颜色包含额外信息

在之前的方案中,我们都是使用颜色值来表示种类,但实际上颜色值所能包含的信息还有很多。

一个颜色值可以用 rgba 来表示,因此我们可以让 r、g、b、a 分别代表不同的信息,如 r 代表种类、g 代表宽度、b 代表高度、a 代表顺序。虽然 rgba 每个的数量都有限(r、g、b 的范围为 0-255,a 的范围为 0-99),但基本足够我们使用了。

rgba.png

当然,你甚至可以再进一步,让每个数字都表示一种信息,不过这样每种信息的范围就比较小,只有 0-9。

总结

对于素材量较少的场景,前端可以直接从视觉稿中确认素材信息;当素材量很多时,直接从视觉稿中确认素材信息的工作量就变得非常大,因此我们使用了位点图来辅助我们获取素材信息。

无标题-2021-09-28-1450.png

地图就是这样一种典型的场景,在上面的例子中,我们已经通过从位点图中读出的信息成功绘制了地图。我们的步骤如下:

  1. 视觉同学提供位点图,作为承载信息的载体,它需要满足以下三个要求:

    1. 大小和地图背景图大小一致:便于我们从图中读出的坐标可以直接使用。

    2. 底色为纯色:便于区分背景和方格。

    3. 在每个方格左上角的位置,放置一个方格,不同颜色的方格表示不同类型。

  2. 通过 jimp 扫描图片上每个像素点的颜色,从而生成一份包含各个方格位置和种类的 json。

  3. 绘制地图时,先读取 json 文件,再根据 json 文件内的坐标信息和种类信息来放置素材。

上述方案并非完美无缺的,在这里我们主要对于位点图进行了改进,改进方案分为两方面:

  1. 由于 1px 的像素点对肉眼来说过小,视觉同学画图以及我们调试的时候,都十分不方便。因此我们将像素点扩大为一个区域,在扫描时,对相邻的相同颜色的像素点进行合并。

  2. 让颜色的 rgba 分别对应一种信息,扩充位点图中的颜色值能够给我们提供的信息。

我们在这里只着重讲解了获取地图信息的部分,至于如何绘制地图则不在本篇的叙述范围之内。在我的项目中使用了 pixi.js 作为引擎来渲染,完整项目可以参考这里,在此不做赘述。

FAQ

  • 在位点图上,直接使用颜色块的大小作为路径方格的宽高可以不?

    当然可以。但这种情况是有局限性的,当我们的素材很多且彼此重叠的时候,如果依然用方块大小作为宽高,那么在位点图上的方块就会彼此重叠,影响我们读取位置信息。

  • 如何处理有损图的情况?

    有损图中,图形边缘处的颜色和中心的颜色会略微有所差异。因此需要增加一个判断函数,只有扫描到的点的颜色与背景色的差值大于某个数字后,才认为是不同颜色的点,并开始区域合并。同时要注意在位点图中方块的颜色尽量选取与背景色色值相差较大的颜色。

    这个判断函数,就是我们上面代码中的 isDifferentColor 函数。

    const isDifferentColor = (color1, color2) => Math.abs(color1 - color2) > 0xf000ff;
  • 判断两个颜色不相等的 0xf000ff 是怎么来的?

    随便定的。这个和图片里包含颜色有关系,如果你的背景色和图片上点的颜色非常相近的话,这个值就需要小一点;如果背景色和图上点的颜色相差比较大,这个值就可以大一点。

参考资料

作者:李一笑
来源:https://segmentfault.com/a/1190000041115022

收起阅读 »

一个Vue3可使用的JSON转excel组件

JSON to Excel for VUE3在浏览器中将JSON格式数据以excel文件的形式下载。该组件是基于this thread 提出的解决方案。支持Vue3.2.25及以上版本使用重要提示! Microsoft Excel中的额外提示此组件中实现的方法...
继续阅读 »



JSON to Excel for VUE3

在浏览器中将JSON格式数据以excel文件的形式下载。该组件是基于this thread 提出的解决方案。支持Vue3.2.25及以上版本使用

重要提示! Microsoft Excel中的额外提示

此组件中实现的方法使用HTML表绘制。在xls文件中,Microsoft Excel不再将HTML识别为本机内容,因此在打开文件之前会显示警告消息。excel的内容已经完美呈现,但是提示信息无法避免,请不要在意!

Getting started

安装依赖:

npm install vue3-json-excel

在vue3的应用入口处有两种注册组件的方式:

import Vue from "vue"
import {vue3JsonExcel} from "vue3-json-excel"

Vue.component("vue3JsonExcel", vue3JsonExcel)

或者

import Vue from "vue"
import vue3JsonExcel from "vue3-json-excel"

Vue.use(vue3JsonExcel)

在template文件中直接使用即可

<vue3-json-excel :json-data="json_data">
Download Data
</vue3-json-excel>

Props List

NameTypeDescriptionDefaultremark
json-dataArray即将导出的数据

fieldsObject要导出的JSON对象内的字段。如果未提供任何属性,将导出JSON中的所有属性。

export-fields (exportFields)Object用于修复使用变量字段的其他组件的问题,如vee-validate。exportFields的工作原理与fields完全相同

typestringMime 类型 [xls, csv]xls1.0.x版本暂时只支持xls,csv会在下个版本迭代
namestringFile 导出的文件名jsonData.xls
headerstring/Array数据的标题。可以是字符串(一个标题)或字符串数组(多个标题)。

title(deprecated)string/Array与header相同,title是出于追溯兼容性目的而维护的,但由于与HTML5 title属性冲突,不建议使用它。

License

MIT

Status

该项目处于早期开发阶段。欢迎参与共建。
有好的产品建议可以联系我!!!!

npm地址

vue3-json-excel

作者:小章鱼
来源:https://segmentfault.com/a/1190000041117522

收起阅读 »