注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

面试必备:Android 常见内存泄漏问题盘点

1. 前言当我们开发安卓应用时,性能优化是非常重要的一个方面。一方面,优化可以提高应用的响应速度、降低卡顿率,从而提升用户体验;另一方面,优化也可以减少应用的资源占用,提高应用的稳定性和安全性,降低应用被杀死的概率,从而提高用户的满意度和留存率。但是,对于许多...
继续阅读 »

1. 前言

当我们开发安卓应用时,性能优化是非常重要的一个方面。一方面,优化可以提高应用的响应速度、降低卡顿率,从而提升用户体验;另一方面,优化也可以减少应用的资源占用,提高应用的稳定性和安全性,降低应用被杀死的概率,从而提高用户的满意度和留存率。

但是,对于许多开发者来说,安卓性能优化往往是一个比较棘手的问题。因为性能优化包罗万象,涉及的知识面也比较多,而内存泄露是最常见的一类性能问题,也是各类面试题中的常客,因此了解内存泄漏是每个安卓开发者应该具备的进阶技能。

本文就带大家盘点常见的内存泄漏问题。

2. 内存泄漏的本质

内存泄漏的本质就是对象引用未释放,当对象被创建时,如果没有被正确释放,那么这些对象就会一直占用内存,直到应用程序退出。例如,当一个Activity被销毁时,如果它还持有其他对象的引用,那么这些对象就无法被垃圾回收器回收,从而导致内存泄漏

当存在内存泄漏时,我们需要通过GCRoot来识别内存泄漏的对象和引用。

GCRoot是垃圾回收机制中的根节点,根节点包括虚拟机栈、本地方法栈、方法区中的类静态属性引用、活动线程等,这些对象被垃圾回收机制视为“活着的对象”,不会被回收。

当垃圾回收机制执行时,它会从GCRoot出发,遍历所有的对象引用,并标记所有活着的对象,未被标记的对象即为垃圾对象,将会被回收。

当存在内存泄漏时,垃圾回收机制无法回收一些已经不再使用的对象,这些对象仍然被引用,形成了一些GCRoot到内存泄漏对象的引用链,这些对象将无法被回收,导致内存泄漏。

通过查找内存泄漏对象和GCRoot之间的引用链,可以定位到内存泄漏的根源,进而解决内存泄漏问题,LeakCancry就是通过这个机制实现的。

一些常见的GCRoot包括:

  • 虚拟机栈(Local Variable)中引用的对象。
  • 方法区中静态属性(Static Variable)引用的对象。
  • JNI 引用的对象。
  • Java 线程(Thread)引用的对象。
  • Java 中的 synchronized 锁持有的对象。

什么情况会造成对象引用未释放呢?简单举几个例子:

  • 匿名内部类造成的内存泄漏:匿名内部类通常会持有外部类的引用,如果外部类的生命周期比匿名内部类长,(更正一下,这里用生命周期不太恰当,当外部类被销毁时,内部类并不会自动销毁,因为内部类并不是外部类的成员变量,它们只是在外部类的作用域内创建的对象,所以内部类的销毁时机和外部类的销毁时机是不同的,所以会不会取决与对应对象是否存在被持有的引用)那么就会导致外部类无法被回收,从而导致内存泄漏。

  • 静态变量持有Activity或Context的引用:如果一个静态变量持有Activity或Context的引用,那么这些Activity或Context就无法被垃圾回收器回收,从而导致内存泄漏。

  • 未关闭的Cursor、Stream或者Bitmap对象:如果程序在使用Cursor、Stream或者Bitmap对象时没有正确关闭这些对象,那么这些对象就会一直占用内存,从而导致内存泄漏。

  • 资源未释放:如果程序在使用系统资源时没有正确释放这些资源,例如未关闭数据库连接、未释放音频资源等,那么这些资源就会一直占用内存,从而导致内存泄漏。

接下来我们通过代码示例看一下各种常见内存泄露以及如何避免相关问题的最佳实践

3. 静态引用导致的内存泄漏

当一个对象被一个静态变量持有时,即使这个对象已经不再使用,也不会被垃圾回收器回收,这就会导致内存泄漏

public class MySingleton {
    private static MySingleton instance;
    private Context context;

    private MySingleton(Context context) {
        this.context = context;
    }

    public static MySingleton getInstance(Context context) {
        if (instance == null) {
            instance = new MySingleton(context);
        }
        return instance;
    }
}

上面的代码中,MySingleton持有了一个Context对象的引用,而MySingleton是一个静态变量,导致即使这个对象已经不再使用,也不会被垃圾回收器回收。

最佳实践:如果需要使用静态变量,请注意在不需要时将其设置为null,以便及时释放内存。

4. 匿名内部类导致的内存泄漏

匿名内部类会隐式地持有外部类的引用,如果这个匿名内部类被持有了,就会导致外部类无法被垃圾回收。

public class MyActivity extends Activity {
    private Button button;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        button = new Button(this);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // do something
            }
        });
        setContentView(button);
    }
}

匿名内部类OnClickListener持有了外部类MyActivity的引用,如果MyActivity被销毁之前,button没有被清除,就会导致MyActivity无法被垃圾回收。(此处可以将Button 看作是自己定义的一个对象,一般解法是将button对象置为空)

最佳实践:在Activity销毁时,应该将所有持有Activity引用的对象设置为null。

5. Handler引起的内存泄漏

Handler是在Android应用程序中常用的一种线程通信机制,如果Handler被错误地使用,就会导致内存泄漏。

public class MyActivity extends Activity {
    private static final int MSG_WHAT = 1;
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MSG_WHAT:
                    // do something
                    break;
                default:
                    super.handleMessage(msg);
            }
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mHandler.sendEmptyMessageDelayed(MSG_WHAT, 1000 * 60 * 5);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        // 在Activity销毁时,应该将Handler的消息队列清空,以避免内存泄漏。
        mHandler.removeCallbacksAndMessages(null);
        }
}

Handler持有了Activity的引用,如果Activity被销毁之前,Handler的消息队列中还有未处理的消息,就会导致Activity无法被垃圾回收。

最佳实践:在Activity销毁时,应该将Handler的消息队列清空,以避免内存泄漏。

6. Bitmap对象导致的内存泄漏

当一个Bitmap对象被创建时,它会占用大量内存,如果不及时释放,就会导致内存泄漏。

public class MyActivity extends Activity {
    private Bitmap mBitmap;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 加载一张大图
        mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.big_image);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        // 释放Bitmap对象
        mBitmap.recycle();
        mBitmap = null;
    }
}

当Activity被销毁时,Bitmap对象mBitmap应该被及时释放,否则就会导致内存泄漏。

最佳实践:当使用大量Bitmap对象时,应该及时回收不再使用的对象,避免内存泄漏。另外,可以考虑使用图片加载库来管理Bitmap对象,例如Glide、Picasso等。

7. 资源未关闭导致的内存泄漏

当使用一些系统资源时,例如文件、数据库等,如果不及时关闭,就可能导致内存泄漏。例如:

public void readFile(String filePath) throws IOException {
    FileInputStream fis = null;
    try {
        fis = new FileInputStream(filePath);
        // 读取文件...
    } finally {
        if (fis != null) {
            try {
                fis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

上面的代码中,如果在读取文件之后没有及时关闭FileInputStream对象,就可能导致内存泄漏。

最佳实践:在使用一些系统资源时,例如文件、数据库等,要及时关闭相关对象,避免内存泄漏。

避免内存泄漏需要在编写代码时时刻注意,及时清理不再使用的对象,确保内存资源得到及时释放。 ,同时,可以使用一些工具来检测内存泄漏问题,例如Android Profiler、LeakCanary等。

8. WebView 内存泄漏

当使用WebView时,如果不及时释放,就可能导致内存泄漏

public class MyActivity extends Activity {
    private WebView mWebView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mWebView = findViewById(R.id.webview);
        mWebView.loadUrl("https://www.example.com");
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        // 释放WebView对象
        if (mWebView != null) {
            mWebView.stopLoading();
            mWebView.clearHistory();
            mWebView.clearCache(true);
            mWebView.loadUrl("about:blank");
            mWebView.onPause();
            mWebView.removeAllViews();
            mWebView.destroy();
            mWebView = null;
        }
    }
}

上面的代码中,当Activity销毁时,WebView对象应该被及时释放,否则就可能导致内存泄漏。

最佳实践:在使用WebView时,要及时释放WebView对象,可以在Activity销毁时调用WebView的destroy方法,同时也要清除WebView的历史记录、缓存等内容,以确保释放所有资源。

9. 监测工具

  1. 内存监视工具:Android Studio提供了内存监视工具,可以在开发过程中实时监视应用程序的内存使用情况,帮助开发者及时发现内存泄漏问题。
  2. DDMS:Android SDK中的DDMS工具可以监视Android设备或模拟器的进程和线程,包括内存使用情况、堆栈跟踪等信息,可以用来诊断内存泄漏问题。
  3. MAT:MAT(Memory Analyzer Tool)是一款基于Eclipse的内存分析工具,可以分析应用程序的堆内存使用情况,识别和定位内存泄漏问题。
  4. 腾讯的Matrix,也是非常好的一个开源项目,推荐大家使用

10. 总结

内存泄漏是指程序中的某些对象或资源没有被妥善地释放,从而导致内存占用不断增加,最终可能导致应用程序崩溃或系统运行缓慢等问题。

常见的内存泄漏问题和对应的最佳实践整理如下

问题最佳实践
长时间持有Activity或Fragment对象导致的内存泄漏及时释放Activity或Fragment对象
匿名内部类和非静态内部类导致的内存泄漏避免匿名内部类和非静态内部类
WebView持有Activity对象导致的内存泄漏在使用WebView时,及时调用destroy方法
单例模式持有资源对象导致的内存泄漏在单例模式中避免长时间持有资源对象
资源未关闭导致的内存泄漏及时关闭资源对象
静态变量持有Context对象导致的内存泄漏避免静态变量持有Context对象
Handler持有外部类引用导致的内存泄漏避免Handler持有外部类引用
Bitmap占用大量内存导致的内存泄漏在使用Bitmap时,及时释放内存
单例持有大量数据导致的内存泄漏避免单例持有大量数据
收起阅读 »

Compose 实战经验分享:开发要点&常见错误&面试题

1. 前言从 Compose 还在 alpha 到现在,用 Compose 完整的从零到一写了三个应用:Twidere X Android、Mask-Android,还有一个暂未公开的项目。https://github.com/TwidereProject/T...
继续阅读 »

1. 前言

从 Compose 还在 alpha 到现在,用 Compose 完整的从零到一写了三个应用:Twidere X Android、Mask-Android,还有一个暂未公开的项目。

这三个应用每一个都有不一样的收获,现在将开发过程中的经验集中做一波总结:

2. 要点总结

直接说几个总结出来的要点吧:

  1. Compose UI 最核心的一个思想就是:状态向下,事件向上,Compose UI 组件的状态都应该来自其参数而不是自身,不要在 Compose UI 组件中做任何计算,有非常多的性能问题其实是来自对于这一条核心思想的不理解。
  2. 如果一个组件不得不内部持有一些状态,切记将这些状态所有的变量都用上 remember,因为 Compose 函数是会被非常频繁的执行,不用 remember 的话会导致频繁的赋值和初始化,甚至进行一些计算操作。
  3. Compose UI 组件的参数最好是不可变(immutable)的,否则最好的情况是遇到和预期表现不符,最差的情况就是影响到性能了。
  4. 每个 Compose UI 组件最好都有 Modifier,这样 Compose UI 组件就可以很方便的在不同地方复用。
  5. 为了可维护性,请尽量拆分基础 Compose UI 组件和业务 Compose UI 组件,基础 Compose UI 组件尽量拆分的细一些,业务 Compose UI 组件看情况,最好也要拆分的细一些,你不会想去维护一个上千行的 Compose UI 组件的,同时细分也会提高一定的复用率。

下面总结了一些常见的不正确的用法,其中大部分会导致性能问题,有很多人会说 Compose 性能差,但其实更多的是本身的用法有误。

3. 滥用 remember { mutableStateOf() }

Compose UI 最核心的一个思想就是:状态向下,事件向上。这句话举个例子可能会更好理解。 一般初学者在看完教程之后马上就会写下这样的代码:

@Composable
fun Counter() {
    var count by remember { mutableStateOf(0) }
    Button(
        onClick = {
            count++
        }
    ) {
        Text("count $count")
    }
}

然后当业务逻辑复杂之后,他的代码可能会像这样:

@Composable
fun Counter() {
    var count by remember { mutableStateOf(0) }
    var text by remember { mutableStateOf("") }

    Column {
        Button(
            onClick = {
                count++
            }
        ) {
            Text("count $count")
        }
        TextField(
            value = text,
            onValueChange = {
                text = it
            }
        )
        OtherCounter()
    }
}

@Composable
fun OtherCounter() {
    var text by remember { mutableStateOf("Hello world!") }
    Column {
        Text(text)
        TextField(
            value = text,
            onValueChange = {
                text = it
            }
        )
    }
}

抛开代码的业务逻辑不谈,这里的 Composable 函数是带状态的,这会带来不必要的 recomposition,从而导致写出来的 Compose UI 出现性能问题,按照核心思想状态向下,事件向上,上面的代码应该这样写:

@Composable
fun CounterRoute(
    viewModel: CounterViewModel = viewModel<CounterViewModel>()

) {
    val state by viewModel.state.collectAsState()
    Counter(
        state = state,
        onIncrement = {
            viewModel.onIncrement()
        },
        onTextChange = {
            viewModel.onTextChange(it)
        },
        onOtherTextChange = {
            viewModel.onOtherTextChange(it)
        },
    )
}

@Composable
fun Counter(
    state: CounterState,
    onIncrement: () -> Unit,
    onTextChange: (String) -> Unit,
    onOtherTextChange: (String) -> Unit,
)
 {
    Column {
        Button(
            onClick = {
                onIncrement.invoke()
            }
        ) {
            Text("count ${state.count}")
        }
        TextField(
            value = state.text,
            onValueChange = {
                onTextChange.invoke(it)
            }
        )
        OtherCounter(
            text = state.otherText,
            onTextChange = onOtherTextChange,
        )
    }
}

@Composable
fun OtherCounter(
    text: String,
    onTextChange: (String) -> Unit,
)
 {
    Column {
        Text(text)
        TextField(
            value = text,
            onValueChange = {
                onTextChange.invoke(it)
            }
        )
    }
}

这样的写法吧所有状态都放到顶层,同时事件也交由顶层处理,这样的 Compose UI 组件是没有任何状态的,这样的的 Compose UI 组件会有非常好的性能。

4. 忘记 remember

刚刚说完滥用,现在说忘记。当一个组件不得不内部持有状态的时候,这个时候切记:一定要吧所有的变量都用上 remember。

常见的有这样的错误:

@Composable
fun SomeList() {
    val list = listOf("a""b""c")
    LazyColumn {
        items(list) {
            Text(it)
        }
    }
}

这里的 list 完全没有被 remember,而 Compose 函数会非常频繁的执行,这就导致每次执行到 val list = listOf("a", "b", "c") 的时候都会有一次生成赋值甚至计算的操作,这样的写法是非常影响性能的,正确的写法应该是这样:

@Composable
fun SomeList() {
    val list = remember { listOf("a""b""c") }
    LazyColumn {
        items(list) {
            Text(it)
        }
    }
}

当然最好是把 list 移到参数上:

@Composable
fun SomeList(
    list: List<String>,
)
 {
    LazyColumn {
        items(list) {
            Text(it)
        }
    }
}

5. 参数是可变的

还是接着上一个例子,光是 list 移动到参数还是不够的,因为你可以在 Composable 函数外边更改这个列表,比如执行 list.add("") 的操作,Compose 编译器会认为这个 Composable 函数仍然是带状态的,所以还不是最优化的状态。最好是使用 kotlinx.collections.immutable 里面的 ImmutableList:

@Composable
fun SomeList(
    list: ImmutableList<String>,
)
 {
    LazyColumn {
        items(list) {
            Text(it)
        }
    }
}

除了基础类型之外,其他参数中的自定义 class 最好是标记上 @Immutable,这样 Compose 编译器会优化这个 Composable 函数。当然不要定义一个 data class 然后里面一个 var a: String 然后问为什么 a.a = "b" 没有效果,建议传给 Composable 函数的 data class 全是 val。

6. 没开启 R8

R8 对于 Compose 的提升是非常巨大的,如果是简单 UI 的话没有 R8 可能还可以用,复杂 UI 下非常推荐开启 R8,代码优化之后的性能的 Debug 的性能差距极大。

7. 最后:面试题推荐

其实理解了 Compose UI 的核心思想之后,写出来的 Compose 程序应该不会有什么性能问题,而且在这个核心思想下写出来的 Compose UI 逻辑非常的清晰,因为整个 UI 是无状态的,你只需要关系在什么状态下这个 UI 显示的是什么样的,心智负担非常小。

最后推荐一些 Compose 相关的面试题,大家可以做一个自我测试,如果你能回答的七七八八,那么恭喜你,可能已经击败 95% 的同行了。

  1. Jetpack Compose有了解吗?和传统Android UI有什么不同?
  2. DisposableEffect、SideEffect、LaunchedEffect之间的区别?
  3. pointer事件在各个Composable function之间是如何处理的?
  4. 自定义Layout?
  5. CompositionLocal起什么作用?staticCompositionLocalOf和compositionLocalOf有什么区别?
  6. Composable function的状态是如何持久化的?
  7. LazyColumn是如何做Composable function缓存的?
  8. 如何解决LazyColumn和其他Composable function的滑动冲突?
  9. @Composable的作用是什么?
  10. Jetpack Compose是用什么渲染的?执行流程是怎么样的?与flutter/react那样做diff有什么区别/优劣?
  11. Jetpack Compose多线程执行是如何实现的?
  12. 什么是有状态的 Composable 函数?什么是无状态的 Composable 函数?
  13. Compose 的状态提升如何理解?有什么好处?
  14. 如何理解 MVI 架构?和 MVVM、MVP、MVC 有什么不同的?
  15. 在 Android 上,当一个 Flow 被 collectAsState,应用转入后台时,如果这个 Flow 再进行更新,对应的 State 会不会更新?对应的 Composable 函数会不会更新?
收起阅读 »

认识Base64,看这篇足够了

web
Base64的简介 Base64是常见的用于传输8Bit字节码的编码方式之一,基于64个可打印字符来标识二进制数据点方法。 使用Base64的编码不可读,需要解码。 Base64实现方式 Base64编码要求把3个8位字节(3*8=24)转化为4个6位...
继续阅读 »

Base64的简介


Base64是常见的用于传输8Bit字节码的编码方式之一,基于64个可打印字符来标识二进制数据点方法。


使用Base64的编码不可读,需要解码。


Base64实现方式


Base64编码要求把3个8位字节(3*8=24)转化为4个6位的字节(4*6=24),之后在6位的前面补两个0,形成8位一个字节的形式。如果剩下的字符不足3个字节,则用0填充,输出字符使用=,因此编码后输出的文本末尾可能会出现1个或2个=


Base64就是包括小写字母a-z,大写字母A-Z,数字0-9,符号+/组成的64个字符的字符集,另外包括填充字符=。任何符号都可以转换成这个字符集中的字符,这个转化过程就叫做Base64编码。


为了保证输出的编码位可读字符,Base64指定了一个编码表,以便进行统一转换,编码表的大小为2^6=64,即Base64名称的由来。




























































































































































































索引 对应字符 索引 对应字符 索引 对应字符 索引 对应字符
0 A 17 R 34 i 51 z
1 B 18 S 35 j 52 0
2 C 19 T 36 k 53 1
3 D 20 U 37 l 54 2
4 E 21 V 38 m 55 3
5 F 22 W 39 n 56 4
6 G 23 X 40 o 57 5
7 H 24 Y 41 p 58 6
8 I 25 Z 42 q 59 7
9 J 26 a 43 r 60 8
10 K 27 b 44 s 61 9
11 L 28 c 45 t 62 +
12 M 29 d 46 u 63 /
13 N 30 e 47 v
14 O 31 f 48 w
15 P 32 g 49 x
16 Q 33 h 50 y


十进制转二进制


十进制数转换为二进制数时,由于整数和小数的转换方法不同,所以先将十进制数的整数部分和小数部分分别转换后,再加以合并。


十进制整数转换为二进制整数采用 "除 2 取余,逆序排列" 法。


具体做法是:用 2 整除十进制整数,可以得到一个商和余数;再用 2 去除商,又会得到一个商和余数,如此进行,直到商为小于 1 时为止,然后把先得到的余数作为二进制数的低位有效位,后得到的余数作为二进制数的高位有效位,依次排列起来。


示例:求十进制34对应的二进制数





Base64编码示例


示例1:对字符串 Son 进行 Base64编码

























































ASCII字符 S(大写) o n
十进制 83 111 110
二进制 01010011 01101111 01101110
每6个bit为一组 010100 110110 111101 101110
高位补0 00010100 00110110 00111101 00101110
对应的Base64索引 20 54 61 46
对应的Base64字符 U 2 9 u


Son通过Base64编码转换成了U29u,3个ASCII字符刚好转换成对应的Base64字符。


示例2:对字符串 S 进行 Base64编码

























































ASCII字符 S(大写)
十进制 83
二进制 01010011
每6个bit为一组 010100 110000 000000 000000
高位补0 00010100 00110000 00000000 00000000
对应的Base64索引 20 48
对应的Base64字符 U w = =


字符串S对应1个字节,一共8位,按6位为一组分为4组,每组不足6位的补0,然后在每组的高位补0,找到Base64编码进行转换。


Base64的优缺点


优点:





  • 将二进制数据(如图片)转为可打印字符,在HTTP协议下传输



  • 对数据进行简单加密,肉眼安全



  • 如果是在html或css中处理图片,可以减少http请求


缺点:





  • 内容编码后体积变大,至少1/3



  • 编码和解码需要额外工作量


Base64编码和解码





  • btoaatob方法:js中有两个函数被分别用来处理编码和解码Base64字符串



    • btoa():该函数基于二进制数据字符串创建一个Base64编码的字符串



    • atob():该函数用于解码使用Base64编码的字符串




btoa()示例:


let btoaStr = window.btoa(123456)
console.log(btoaStr) // MTIzNDU2

atob()示例:


let atobStr = window.atob(btoaStr)
console.log(atobStr) // 123456

这两个函数容易混淆,可以这样记下,比如btoa是以b字母开头,编码也是以b字母开头。即btoa编码,升序的atob解码。



Data URI Scheme


Data URI scheme的目的是将一些小的数据,直接嵌入到网页中,从而不用再从外部文件载入,减少请求资源的连接数,缺点是不会被浏览器缓存起来。


Data URI scheme支持的Base64编码的类型比如:





  • data:image/png;base64,base64编码的png图片数据



  • data:image/gif;base64,base64编码的gif图片数据



  • data:text/javascript;base64,base64编码的javascript代码


Base64的应用


使用Base64编码资源文件


比如:在html文档中渲染一张Base64的图片


<img :src="data:image/png;base64,IVBORsssssfjsfsfs==" alt="">img>


注意:如果图片较大,在Base64编码后字符串会非常大,影响页面加载进度,这种情况不适合Base64编码。



在HTTP中传递较长的标识信息


比如使用Base64将一个较长的标识符(128bit的UUID)编码为一个字符串,用作表单和httpGET请求中的参数。


标准的Base64编码后不适合直接放在请求的URL中传输,因为URL编码器会把Base64中的/+变成乱码,即%xxx的形式。我们可以将base64编码改进一下用于URL中,比如Base64中的/+转换成_-等字符,避免URL编码器的转换。


canvas生成图片


canvas提供了toDataURL()toBlob()方法。


HTMLCanvasElement.toDataURL() 方法返回一个包含图片展示的data URI。可以使用 type 参数其类型,默认为PNG格式。图片的分辨率为 96dpi。


canvas.toDataURL(type, encoderOptions);

使用示例:设置jpeg图片的质量,图片将以Base64存储


let quality = canvas.toDataURL("image/jpeg"1.0);
// data:image/jpeg;base64,/9j/4AAQSkZJRgABAQ...9oADAMBAAIRAxEAPwD/AD/6AP/Z"

HTMLCanvasElement.toBlob() 方法创造Blob对象,用以展示 canvas 上的图片;这个图片文件可以被缓存或保存到本地(由用户代理自行决定)。


HTMLCanvasElement.toBlob(callback, type?, quality?)

使用示例:


canvas.toBlob(function(blob){...}, "image/jpeg"0.95); // JPEG at 95% quality

读取文件


FileReader.readAsDataURL()方法会读取指定的BlobFile对象。读取操作完成的时候,readyState会变成已完成DONE,并触发 loadEnd事件,同时result属性将包含一个data:URL 格式的字符串(Base64 编码)以表示所读取文件的内容。


读取文件示例:


代码演示可以点击这里查看哦


<input type="file" onchange="previewFile()" />
<br />
<br />
<img src="" alt="导入图片进行预览" />
<script>
  function previewFile({
    var preview = document.querySelector("img");
    var file = document.querySelector("input[type=file]").files[0];
    var reader = new FileReader();

    reader.addEventListener("load",
      function ({
        preview.src = reader.result;
        console.log(reader.result); // Base64编码的图片格式
      },
      false,
    );

    if (file) {
      reader.readAsDataURL(file);
    }
  }
script>

简单"加密"某些数据


Base64常用作一个简单的“加密”来保护某些数据,比如个人密码我们如果不使用其他加密算法的话,可以使用Base64来简单处理。


Base64是加密算法吗


Base64不是加密算法,只是一种编码方式,可以用Base64来简单的“加密”来保护某些数据。


结语


❤️ 🧡 💛大家喜欢我写的文章的话,欢迎大家点点关注、点赞、收藏和转载!!


作者:前端开心果
来源:mdnice.com/writing/3c8306915f484882a5b720bfdedc6e02
收起阅读 »

Flutter路由跳转参数处理小技巧

需求 我们在开发应用中,经常会出现一个界面跳转到另外一个界面并带有参数传递,在Android中大家都知道使用Intent传递参数,在第二个Activity中onCreate中可以获取到这个参数。 实现 那么在Flutter中,我们经常会使用路由跳转到另外...
继续阅读 »

需求


我们在开发应用中,经常会出现一个界面跳转到另外一个界面并带有参数传递,在Android中大家都知道使用Intent传递参数,在第二个Activity中onCreate中可以获取到这个参数。


实现


那么在Flutter中,我们经常会使用路由跳转到另外一个界面,那么如果这个时候需要传参。 代码如下:


/// 路由跳转并带参数
 Navigator.pushNamed(
            context,
            RouteConst.routeNext,
            arguments: (TestArguments("一笑轮回""江苏省徐州市")),
);       
     
     
/// 测试数据模型
class TestArguments {
  String? name;
  String? address;
  TestArguments(this.name, this.address);
}

没错,直接赋值arguments字段就可以了,那么我们如何获取呢?


在第二个页面中


class TwoPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // 从路由设置中获取传递的参数
    var arguments = ModalRoute.of(context)?.settings.arguments;
    // 其他部分的代码...
  }
}


我们需要通过 ModalRoute.of(context)?.settings.arguments获取数据,那么我们直接在 initState方法中直接通过 ModalRoute.of(context)?.settings.arguments获取,会报错


这里出错原因,可以通过错误并查看源码可知,这里部讲述。


我们有的时候需要在initState方法中获取数据并处理一些事情,我们应该怎么做呢?


下面提供一个小技巧。





  • 路由定义


class RouteConst {
  static const routeNext = "/route_next";
}


class RoutePathConst {
  static var routePaths = <String, Widget Function(BuildContext context)>{
    RouteConst.routeNext: (context) => ArgumentsNextPage(),
  };
}




  • 跳转代码


 Navigator.pushNamed(
            context,
            RouteConst.routeNext,
            arguments: (TestArguments("一笑轮回""江苏省徐州市")),
          );

/// 测试数据模型
class TestArguments {
  String? name;
  String? address;

  TestArguments(this.name, this.address);
}




  • 定义ArgumentsMixin


/// Arguments参数数据
mixin ArgumentsMixin {
  late final Object? arguments;
}

/// 路由拼接的参数数据
mixin RouteQueryMixin {
  final Map<String, String> routeParams = HashMap();
}




  • 重写onGenerateRoute



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

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      ...
      onGenerateRoute: (settings) {
        var uri = Uri.parse(settings.name ?? "");
        var route = uri.path;
        var params = uri.queryParameters;
        if (!RoutePathConst.routePaths.containsKey(route)) {
          return null;
        }
        return MaterialPageRoute(
          builder: (context) {
            var widgetBuilder = RoutePathConst.routePaths[route];
            var widget = widgetBuilder!(context);
            if (widget is RouteQueryMixin) {
              (widget as RouteQueryMixin).routeParams.addAll(params);
            }
            if (widget is ArgumentsMixin) {
              (widget as ArgumentsMixin).arguments = settings.arguments;
            }
            return widget;
          },
          settings: settings,
        );
      },
    );
  }
}





  • 创建ArgumentsNextPage



///第二页
class ArgumentsNextPage extends StatefulWidget
    with ArgumentsMixin, RouteQueryMixin {
  ArgumentsNextPage({super.key});

  @override
  State<ArgumentsNextPage> createState() => _ArgumentsNextPageState();
}

class _ArgumentsNextPageState extends State<ArgumentsNextPage> {
  /// 传参数据文本
  String get result {
    // Arguments传参数据
    TestArguments? arguments;
    if (widget.arguments != null && widget.arguments is TestArguments) {
      arguments = widget.arguments as TestArguments;
    }

    // 路由拼接的数据
    var params = widget.routeParams;

    // 拼接结果数据
    return "arguments:name=${arguments?.name ?? ""} address=${arguments?.address ?? ""} \nrouteParams=$params";
  }

  @override
  void initState() {
    super.initState();
    print("result=$result}");
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: XYAppBar(
        title: "第二页",
        onBack: () {
          Navigator.pop(context);
        },
      ),
      body: Center(
        child: Text(result),
      ),
    );
  }
}


这样就OK了,好像没讲啥,直接看代码吧。


详细代码见:github.com/yixiaolunhui/flutter_xy


作者:移动小样
来源:mdnice.com/writing/3d43c6e3544b45c59773b133a135fb01
收起阅读 »

hive宽表窄表互转

hive宽表窄表互转 背景 在工作中经常会遇到高表转宽表,宽表转窄表的场景,在此做一些梳理。 宽表转窄表 传统思路 使用sql代码作分析的时候,几次遇到需要将长格式数据转换成宽格式数据,一般使用left join或者case when实现,代码...
继续阅读 »

hive宽表窄表互转


背景


在工作中经常会遇到高表转宽表,宽表转窄表的场景,在此做一些梳理。


宽表转窄表


传统思路



使用sql代码作分析的时候,几次遇到需要将长格式数据转换成宽格式数据,一般使用left join或者case when实现,代码看起来冗长,探索一下,可以使用更简单的方式实现长格式数据转换成宽格式数据。



select year,
max(case when month=1 then money else 0 endas M1,
max(case when month=2 then money else 0 endas M2,
max(case when month=3 then money else 0 endas M3,
max(case when month=4 then money else 0 endas M4 
from sale group by year;

需求描述


某电商数据库中存在一张客户信息表user_info,记录着客户属性数据和消费数据,需要将左边长格式数据转化成右边宽格式数据。 需求实现



涉及函数: str_to_map, concat_ws, collect_set, sort_array




实现思路: 步骤一:将客户信息转化成map格式的数据。 collect_set形成的集合是无序的,若想得到有序集合,可以使用sort_array对集合元素进行排序。 步骤二:将map格式数据中的key与value提取出来,key就是每一列变量名,value就是变量值



select 
    user_no,
    message1['name'name,
    message1['sex'] sex,
    message1['age'] age,
    message1['education'] education,
    message1['regtime'] regtime,
    message1['first_buytime'] first_buytime
from 
  (select
      user_no,
      str_to_map(concat_ws(',',sort_array(collect_set(concat_ws(':', message, detail))))) message1
      from user_info
      group by user_no
      order by user_no
   ) a


窄表转宽表


长宽格式数据之间相互转换使用到的函数,可以叫做表格生成函数


需求描述


某电商数据库中存在表user_info1,以宽格式数据记录着客户属性数据和消费数据,需要将左边user_info1宽格式数据转化成右边长格式数据。 需求实现



步骤一:将宽格式客户信息转化成map格式的数据。 步骤二:使用explode函数将 map格式数据中的元素拆分成多行显示



select user_no, explode(message1)
    from 
    (select user_no, 
        map('name',name'sex',sex, 'age',age, 'education',education, 'regtime',regtime, 'first_buytime',first_buytime) message1
        from user_info1
    ) a

总结



不管是将长格式数据转换成宽格式数据还是将宽格式数据转换成长格式数据,都是先将数据转换成map格式数据。长格式数据转换成宽格式数据:先将长格式数据转换成map格式数据,然后使用列名['key']得到每一个key的value;宽格式数据转换成长格式数据:先将宽格式数据转换成map格式数据,然后使用explode函数将 map格式数据中的元素拆分成多行显示。顺便说一句,R语言中也是通过类似的方法实现长宽格式之间相互转换的。



作者:大数据启示录
来源:mdnice.com/writing/cfacb28094f643d5970e425fc6130980
收起阅读 »

8月来临,再不给自己定年度目标,年终总结又没得写了!

年度目标有多重要? 试想一下,一只无头苍蝇,即便饿了,也只会漫无目的地飞来飞去,最终还是难逃被饿死的命运。 人也是如此,如果连以年度为时间粒度设置的目标都没有,每天只是浑浑噩噩地混混日子,那么他大概率这辈子也会白白浪费掉。 毕竟习惯具有惯性,除非一个人能...
继续阅读 »

年度目标有多重要?


试想一下,一只无头苍蝇,即便饿了,也只会漫无目的地飞来飞去,最终还是难逃被饿死的命运。


人也是如此,如果连以年度为时间粒度设置的目标都没有,每天只是浑浑噩噩地混混日子,那么他大概率这辈子也会白白浪费掉。


毕竟习惯具有惯性,除非一个人能时不时跳出原有舒适圈。



图片来自网络

图片来自网络


事实上,哪怕KPI没有完成,至少知道自己是有方向的,今年完不成,还有明年,明年完不成,就换个目标。


如果你还想死磕,在时间、经济和健康状况都不错的前提下,你可以这么做。


如果超过两年,这个目标还没完成,你就应该考虑一下这个目标是不是设定得有问题了。


这些问题可能来自以下2个方面:


1.违背当前自然规律


比如你要在2年内找到外星人的踪迹;在两年内证明鬼魂的存在;在两年内让已故的猫咪复活……


2.没有违背当前自然规律,但不切实际


比如纯路人的你要在2年内嫁给某知名男星;没有任何创业经验、对公司理财一无所知的情况下想做一家2年内赶超某宝的电商平台;每天24小时不睡觉才能完成的目标……



图片来自网络

图片来自网络


那么,什么样的目标更合理、在两年内是有可能完成的呢?


答案很多。但在目标合理的前提下,一定要对目标进行量化。


比如:


体重超标的你,计划在两年内减掉多少斤?


从事IT行业的你计划在两年内发布多少篇技术博客?这些技术博客有没有点赞要求?有没有技术难度要求?如果有,具体是多少?


你有没有存钱目标?如果有,计划在两年内拥有多少存款?如果主职收入能达到存款要求,是否需要降低物欲来节流?


如果主职收入达不到存款要求,你有没有能赚取额外收入的副业渠道?如果有,能否满足存款要求?如果不能满足,那么有没有办法拓展客户、加强营销以提高副业收入?


如果没有办法,那有没有可能在一年内摸索出收入更高的副业渠道?(剩下一年来搞副业赚钱)


上面问题的解决方案还可以是有计划、有目的地提升自己的职业技能,跳槽换一份待遇更高的工作。


……


8月来临,还没有定下年度目标的你,现在是否该定几个年度目标了?


现在定目标,明年年终总结时,你就有可以量化的成果了。


想想就很激动。


还等什么呢,赶紧行动起来!在你的备忘录里、日记本里把它们写出来。


作者:美人薇格
来源:mdnice.com/writing/d96e3e9c630a43c7bab96a298661cb54
收起阅读 »

为什么你不应该使用div作为可点击元素

web
按钮是为任何网络应用程序提供交互性的最常见方式。但我们经常倾向于使用其他HTML元素,如divspan等作为clickable元素。但通过这样做,我们错过了许多内置浏览器的功能。 我们缺少什么? 无障碍问题(空格键或回车键无法触发按钮点击) 元素将无法通过按...
继续阅读 »

按钮是为任何网络应用程序提供交互性的最常见方式。但我们经常倾向于使用其他HTML元素,如divspan等作为clickable元素。

但通过这样做,我们错过了许多内置浏览器的功能。


我们缺少什么?



  1. 无障碍问题(空格键或回车键无法触发按钮点击)

  2. 元素将无法通过按Tab键来聚焦


1062174618-64c4718ba0919.webp


权宜之计


我们需要在每次创建可点击的 div 按钮时,以编程方式添加所有这些功能


image.png


更好的解决方案


始终优先使用 button 作为可点击元素,以获取浏览器的所有内置功能,如果你没有使用它,始终将上述列出的可访问性功能添加到你的div中。


虽然,直接使用按钮并不直观。我们必须添加并修改一些默认的CSS和浏览器自带的行为。


使用按钮的注意事项


1. 它自带默认样式


我们可以通过将每个属性值设置为 unset 来取消设置现有的CSS。


我们可以添加 all:unset 一次性移除所有默认样式。


在HTML中,我们有三种类型的按钮。 submit, reset and button.
默认的按钮类型是 submit.


无论何时使用按钮,如果它不在表单内,请始终添加 type='button' ,因为 submit 和 reset 与表格有关。


2.请不要在按钮标签内部放置 divs


我们仍然需要添加 cursor:pointer 以便将光标更改为手形。


image.png


作者:王大冶
来源:juejin.cn/post/7261985825089110076
收起阅读 »

从9G到0.3G,腾讯会议对他们的git库做了什么?

web
导读 过去三年在线会议需求井喷,腾讯会议用户量骤增到3亿。快速迭代的背后,腾讯会议团队发现:业务保留了长达5年的历史数据,大量未进行 lfs 转换,新 clone 仓库本地空间占17.7G+。本地磁盘面临严重告急,强烈影响团队 clone 效率。当务之急是将仓...
继续阅读 »

导读


过去三年在线会议需求井喷,腾讯会议用户量骤增到3亿。快速迭代的背后,腾讯会议团队发现:业务保留了长达5年的历史数据,大量未进行 lfs 转换,新 clone 仓库本地空间占17.7G+。本地磁盘面临严重告急,强烈影响团队 clone 效率。当务之急是将仓库进行瘦身。本栏目特邀腾讯会议的智子研发团队成员李双君,回顾腾讯会议客户端的瘦身历程和经验,欢迎阅读。


目录


1 瘦身成效


2 瘦身前事项


3 瘦身整体方案


4 瘦身具体命令执行


5 新代码库验证


6 解决其它设备本地老分支 push 问题


7 其他平台适配


8 最后的验证


9 瘦身完毕后的知会


10 兜底回滚方案


11 踩坑记录及应对


12 写在最后


*作者所在的腾讯会议智子研发团队是腾讯会议的终端团队,负责腾讯会议 Win、Mac、Linux、Android、iOS、小程序、Web 等全栈开发,致力于打造一流的端产品体验。


01、瘦身成效


腾讯会议瘦身完毕后整体收益:




  • Git 仓库大小,9G 到350M。




  • 新 clone 仓库占用空间,从17.7G 到12.2G。




  • 平常拉代码速度(北京地区测试):macbook m1 pro:提升45%;devcloud win:提升56%。




  • 包构建流水线全量拉代码耗时,从16分钟减少到5分钟以内。





02、瘦身前事项


2.1 环境准备


使用有线网,看看能否通过其他办法给机器的上传和下载速度提速?不建议在家中开代理来瘦身,因为家里网速一般都没有公司快;如果在家操作,提前配置好远程桌面,远程公司电脑来瘦身。


使用性能较好的机器,硬盘空间至少要有 xxxG 剩余 (可以提前演练,看看究竟要多大磁盘空间?会议最起码得要求有600G 空余)。会议本次瘦身使用的设备是 MAC Book M1 Pro(16寸)笔记本电脑。


2.2 周知


工作开发群或者邮件等通知瘦身时间和注意事项:


瘦身时间: 选一个大家基本上都不会提交代码的时间,比如十一国庆或者春节;会议选的是春节期间。


注意事项: (开发重点关注)


瘦身期间会锁库,必须提前推送代码到远端,否则需要手动同步; 锁库期间无法进行 MR,且已创建 MR 会失效; 因删除历史记录,会导致本地仓库与远端冲突,请恢复后重新 clone 代码; 需要查询或处理更老的代码,需要去备份仓库查看。

2.3 代码库锁定


禁止代码库写操作,一般公司的代码管理平台可以提供这个功能,Git 项目的 owner 有权限。


2.4 第三方 Git 平台禁用


如果 Git 项目被第三方 Git 平台使用了,要保证瘦身前仓库的同步任务禁用。


比如,会议使用了 Ugit(UGit 是腾讯内部的一款自研 Git 客户端,主要是为腾讯内部研发环境特点而定制),就要如下禁用项目同步:



03、瘦身整体方案


原仓库继续保留作为备份仓库,另外新建仓库,新仓库沿用原仓库的项目名称、版本库路径和 id,并同步原项目数据。


之所以这么做,是为了保证其他平台无缝对接新的 Git 仓库,不用再更换 Git 地址,另外有些通过 api 调用的系统和工具也不受到影响。


瘦身内容:




  • 历史记录删除,只保留最近半年的历史记录。




  • 将历史大文件以及未加入 lfs 的大文件进行 lfs 处理。




04、瘦身具体命令执行


4.1 clone 项目,拉取所有文件版本到本地


git clone example.com/test.git


为了后面的比对验证,可以拷贝一份 test 文件夹放到和 test 同级目录下面的新建的 copyForCompare 文件夹中。


ulimit -n 9999999 # 解决可能出现的报错too many open files的问题
ulimit -n # 查看改成9999999了没

遍历拉取所有分支的 lfs 最新文件,并追踪远端分支到本地


以下这段 shell 脚本可以直接拷贝到终端运行,也可以创建一个.sh 文件放到根目录执行


cur_index=1
j=1
git branch -r | grep -v '->' |
while read remote
do
echo ”deal $cur_index th branch“
cur_index=$[cur_index+1]
git branch --track "${remote#origin/}" "$remote"
echo "begin to lfs fetch branch $remote"
git lfs fetch origin $remote
if [ $? -eq 0 ]; then
echo "fetch branch $remote success"
else
echo "fetch branch $remote failed"
lfs_fetch_fail_array[$j]=$remote
j=$[j+1]
fi
done
if [ ${#lfs_fetch_fail_array[*]} -gt 0 ]; then
echo "git lfs fetch error branches are: ${lfs_fetch_fail_array[*]}"
else
echo "fetch all branches success. done."
fi

获取所有分支的文件和 lfs 文件版本


git fetch --all
git lfs fetch --all

4.2 使用 git filter-branch 截断历史记录


这次瘦身只保留最近半年的历史记录,2022.6.1之前的提交记录都删除,所以截断的 commit 节点按如下所述来找:


提前用 sourceTree(或者别的 Git 界面工具)找出来需要截断的那个 commit,以主干 master 为例,找到 master 分支上提交的并且只有一个父的提交节点(如果提交节点有多个父,那么所有父节点都要处理),该节点必须是所有分支的父节点,否则需要考虑其他分支特殊处理的情况,该情况后面的【特殊分支处理】会有说明。



可以看到选中的截断 commit id 是 ff75cc5cdbf0423a24b4f5438e52683210813ba0



  • 根据上面的 commit id,带入下面的命令,找出其父


git cat-file -p ff75cc5cdbf0423a24b4f5438e52683210813ba0



可以看到只有一个父,其父是7ffe6782272879056ca9618f1d85a5f9716f8e90 ,所以该提交 id 就是要置为空的。如果有多个父都需要处理。



  • 执行命令


注意:对于普通提交节点,下面命令的 parent 值是"-p parentId";对于合并提交节点,下面命令的 parent 值是"-p parentId1 -p parentId2 -p parentId3 ..."


git filter-branch --force --parent-filter '
read parent
if [ "$parent" = "-p 7ffe6782272879056ca9618f1d85a5f9716f8e90" ]
then
echo
else
echo "$parent"
fi' --tag-name-filter cat -- --all


  • 重点验证:上述命令执行完毕后,一定要用如下命令检查是否修改成功


注意:因为执行完了命令已经修改了历史记录,此时 Git log 命令执行会慢点,大概5分钟可以出结果,另外可以用这个在线时间戳转换工具来转换时间戳。


工具链接:http://www.beijing-time.org/shijianchuo…


如果执行成功会把之前的文件版本取最新的 add 到这个截断的提交节点里面,如下图:


git log --all-match --author="xxxx" --grep="auto update .code.yml by robot" --name-status --before="1654043400" --after="1654012800" --all


4.3 使用 git-filter-repo 清理截断日期前的所有历史记录,并将截断节点的提交信息修改


注意此步骤要谨慎处理,因为这步会真正地删除提交记录。


提前安装好 git-filter-repo,执行下面的 python 代码。


import os
try:
import git_filter_repo as fr
except ImportError:
raise SystemExit("Error: Couldn't find git_filter_repo.py. Did you forget to make a symlink to git-filter-repo named git_filter_repo.py or did you forget to put the latter in your PYTHONPATH?")

k_work_dir = "/Volumes/SolidCompany/S_Shoushen/test"
# 2022.6.1 00:00:00
k_clean_history_deadline = b"1654012800"
# 2022.6.1 07:05:07
k_clean_deadline_commit_date = b"1654038307"
k_clean_deadline_commit_author_name = b"xxxxx"
k_new_root_commit_message = "仓库瘦身历史记录裁剪,截断提交记录后新根结点新增历史文件;如果想查看更多历史记录,请去备份仓库:https://example.com/test_backup.git"

def commitCallBackFun(commit, metadata):
[time_stamp, timezone] = commit.committer_date.split()
if time_stamp == k_clean_deadline_commit_date and commit.author_name == k_clean_deadline_commit_author_name:
commit.message = k_new_root_commit_message.encode("utf-8")
if time_stamp >= k_clean_history_deadline:
return
commit.file_changes = []

def main():
os.chdir(k_work_dir)
print("git work dir is", os.getcwd())
args = fr.FilteringOptions.parse_args(['--force', '--debug'])
filter = fr.RepoFilter(args, commit_callback = commitCallBackFun)
filter.run()

if __name__ == '__main__':
main()

验证下截断提交结点的提交信息更改成功了没?


git log --all-match --author="xxx" --grep="仓库瘦身历史记录裁剪" --name-status --before="1654043400" --after="1654012800"

如下就对了:



以上执行完后做个简单验证:


用 BeyondCompare 工具跟刚开始备份的 copyForCompare 目录下的 test 仓库对比,看看有没有增删改文件,期望应该没有任何变化才对。



  • 特殊分支处理


说明:以上历史记录裁剪并删除历史提交记录执行完后,对于基于截断提交节点前的提交节点创建出来的分支或者其子分支会出现文件被删除或者整个分支被删除的情况。



所以要提前弄清楚有没有在截断节点之前早就创建出来一直在用的分支, 如果有就得特殊处理上面的2和3步骤了:


第2步中截断历史记录的时候,要类似分析 master 分支那样分析其它需要保留的特殊分支,找出各自的截断节点的父提交 id;然后执行的 shell 脚本里面条件判断改成判断所有的父提交 id;类似这样:


git filter-branch --force --parent-filter '
read parent
if [ "$parent" = "-p 85f5ee6314f4f46cc47eb02c6af93bd3020a1053 -p cd207e9b3372f68a6d1ffe06fcf189d952e3bf9f" ] || [ "$parent" = "-p 7ffe6782272879056ca9618f1d85a5f9716f8e90" ]
then
echo
else
echo "$parent"
fi' --tag-name-filter cat -- --all

第3步中删除截断节点前提交记录的 python 脚本里面,按照分支名字和自己分支的截断日期来做比对逻辑进行删除提交记录的操作。类似如下:


#!/usr/bin/env python3
import os
try:
import git_filter_repo as fr
except ImportError:
raise SystemExit("Error: Couldn't find git_filter_repo.py. Did you forget to make a symlink to git-filter-repo named git_filter_repo.py or did you forget to put the latter in your PYTHONPATH?")

k_work_dir = "/Users/jevon/Disk/work/appShoushen/shoushen/test"

# 2022.6.1 07:05:07
k_master_cut_date = b"1654038307"

# 2022.3.25 19:32:00
k_private_new_saas_sdk_master_cut_date = b"1648207920"

k_new_root_commit_message = "仓库瘦身历史记录裁剪,截断提交记录后新根结点新增历史文件;如果想查看更多历史记录,请去备份仓库:https://example.com/test_backup.git"

def commitCallBackFun(commit, metadata):
[time_stamp, timezone] = commit.committer_date.split()
# 每个特殊分支的截断提交点的提交信息修改
if (time_stamp == k_master_cut_date and commit.author_name == b"xxx_author1") or \
(time_stamp == k_private_new_saas_sdk_master_cut_date and commit.author_name == b"xxx_author2"):
commit.message = k_new_root_commit_message.encode("utf-8")

# 每个特殊分支的截断提交点前的提交记录,需要根据各自截止日期来做比对删除日期前的历史记录
strBranch = commit.branch.decode("utf-8")
if strBranch.endswith("refs/heads/master"):
if time_stamp < k_master_cut_date:
commit.file_changes = []
elif strBranch.endswith("refs/heads/private/feature/3.12.1/new-saas-sdk-master"):
if time_stamp < k_private_new_saas_sdk_master_cut_date:
commit.file_changes = []
def main():
os.chdir(k_work_dir)
print("git work dir is", os.getcwd())
args = fr.FilteringOptions.parse_args(['--force', '--debug'])
filter = fr.RepoFilter(args, commit_callback = commitCallBackFun)
filter.run()

if __name__ == '__main__':
main()

以上[特殊分支处理]没有实验过,但是个解决思路,具体实践结果待补充,也欢迎实验过的同学交流。


4.4 进行 lfs 转换


rm -Rf .git/refs/original
rm -Rf .git/logs
git branch | wc -l # 看一下本地分支总数
# 拷贝原来的仓库到新目录下面
git clone file:///Users/jevon/Disk/work/appShoushen/shoushen/test /Users/jevon/Disk/work/appShoushen/shoushen/test_new
cd test_new
git branch -r | grep -v '->' |
while read remote
do
git branch --track "${remote#origin/}" "$remote"
done
git fetch --all git branch | wc -l # 看一下本地分支总数,和拷贝之前是否一样
# 分析仓库中占用空间较大的文件类型(演练的时候可以提前分析出来,节省瘦身时间)
git lfs migrate info --top=100 --everything

命令结果如下,是按照文件所有的历史版本累加统计的,只有未加入 lfs 的才会统计。



git rev-list --objects --all | git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' | sed -n 's/^blob //p' | sort --numeric-sort --key=2 | cut -c 1-12,41- | $(command -v gnumfmt || echo numfmt) --field=2 --to=iec-i --suffix=B --padding=7 --round=nearest | grep MiB

该命令执行结果如下,是把所有大于 1Mb 的文件版本都列出来了,不进行累加,从小到大排序,已经加入 lfs 的不会统计。



# lfs转换
# --include=之后填入根据实际分析的大文件列表
git lfs migrate import --everything --include="*.jar,tool/ATG/index.js,xxx"
# 上面lfs转换执行完后,看一下根目录的.gitattribute文件里面是不是加入了新的lfs文件了

4.5 新建新仓库,推送所有历史记录修改


新创建目标仓库 test_backup.git ,然后运行下面代码:


git remote remove origin
git remote add origin https://example.com/test_backup.git
git remote set-url origin https://example.com/test_backup.git
git remote -v # 确保设置成功新仓库地址

此时可以用下面的命令看看还有没有大文件了(可选)。


git rev-list --objects --all | git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' | sed -n 's/^blob //p' | sort --numeric-sort --key=2 | cut -c 1-12,41- | $(command -v gnumfmt || echo numfmt) --field=2 --to=iec-i --suffix=B --padding=7 --round=nearest | grep MiB

用以下命令看看还有没有未转换的大的 lfs 文件了(可选)。


git lfs migrate info --top=100 --everything
# 推送历史记录修改到目标新仓库:
git push origin --no-verify --all
git push origin --no-verify --tags

4.6 回到原来的 test 目录,推送 lfs 文件


cd ../test
git config lfs. https://example.com/test_backup.git /info/lfs.access basic
git lfs env # 看一下改成了basic了吗
# 设置成远端目标仓库test_backup.git
git remote remove origin
git remote add origin https://example.com/test_backup.git
git remote set-url origin https://example.com/test_backup.git
git remote -v # 确保设置成功新仓库地址


将 upload_objects.sh 拷贝到 test 目录,然后执行 sh ./upload_objects.sh


upload_objects.sh 内容如下:


#!/bin/bash

set -e

count=$(find .git/lfs/objects -type f | wc -l)
echo "-- total objects count is $count"

index=0
concurrency=25

find .git/lfs/objects -type f | awk -F '/' '{print $NF}' | while read -r obj; do
echo "-- $(date) -- uploading ($index/$count) $obj"
git lfs push origin --object-id "$obj" &
index=$((index+1))
if [[ $index%$concurrency -eq 0 ]]; then
echo -e "\r\n-- $(date) -- waiting --------------------------\r\n"
wait
fi
done

注意脚本里面的并发值不能设置太高,不然会报错./upload_objects.sh: line 12: echo: write error: Interrupted system call,测试发现设置25是比较合适的。



确保像上图这样,最后一个也上传成功。


05、新代码库验证


git clone https://example.com/test_backup.git

使用 git lfs pull 先拉取主干分支所有的历史文件进行测试,保留瘦身的本地仓库;  后续如果发现有其他分支的 lfs 文件没有上传成功,再单独上传即可。


上传命令:


git lfs push --object-id origin "$objId"

对比新旧代码库主干最新代码是否一致,可使用 beyond compare 工具进行验证。四端编译不同代表性的分支运行验证。


06、解决其它设备本地老分支 push 问题


在公司的代码管理平台上设置瘦身后的 test_backup 仓库单文件大小上限为1.5M。


一般公司自己的代码管理平台都会提供设置单个 git 文件上传大小上限的功能,得管理员才有权限设置;腾讯的代码管理平台是像下图这样设置的:



解释:之后的步骤将会把新老仓库互换,新旧仓库互换后,其它机器本地的老仓库分支还是有 push 上去的风险,这样就会把瘦身前的历史记录又推送到瘦身后的 Git 仓库,造成瘦身白费。


07、其他平台适配


7.1 代码管理平台


找代码管理平台协助完成下面的操作:(需要提前预约沟通好)


会议用的代码管理平台是工蜂:


项目名称、版本库路径互换:test_backup 重命名为 test,test 重命名为 test_backup。 将两个项目项目 id 进行调换:新项目沿用旧项目的项目 id,以此保证通过 api 调用的系统和工具不受到影响。 项目数据同步:同步项目成员和权限相关的数据、保护分支规则组到新仓库。

自己工蜂适配(可以提前进行)。对照老工蜂的所有配置,在新工蜂上手动同步修改。


7.2 第三方 Git 工具


如果使用了第三方 Git 工具平台做过瘦身仓库与其他项目仓库的同步,需要处理下(会议使用了 UGit 第三方工具):


通知 UGit 相关负责人把旧的工作区移除一下,重新 clone test 仓库。 把 Ugit 里面 test 仓库的同步任务恢复(如有需要)。

7.3 出包流水线构建平台


因为执行完瘦身后,Git 的 commit id 都变了,历史记录也变了,而 coding 的构建机如果不清理缓存删掉老仓库的话,会导致构建机本地仓库历史与远端冲突,执行 Git 命令会异常,所以 coding 必须要清理掉老仓库,重新 clone。


08、最后的验证


代码管理平台以及出包构建平台都处理完成后,进行最后的验证。


本地验证:


本地是否能正常 clone 代码。 本地对比新旧仓库主干最新代码是否一致。 本地随机抽取分支对比新旧仓库文件个数以及最新代码是否一致。 本地编译验证,程序启动主流程验证。

出包构建平台验证:


主干分支、发布分支、个人需求分支、个人分支等的构建验证。

代码管理平台验证:


代码库基础、高级配置是否正确 保护分支规则配置是否正确,是否有效 项目成员是否和原仓库一致 MR 是否可正常发起、合并,能否正常调起检测流水线

代码库写权限恢复:


保证瘦身后的 Git 仓库恢复写权限;备份仓库禁用写权限。

09、瘦身完毕后的知会


知会参考模板:


xxx 仓库瘦身完成了!接下来需要开发重点关注:本地旧仓库已经失效,必须删掉重新 clone 代码【最最重要】未提前push到远端的本地新增代码需要手动同步旧的未完成的MR已经失效,需要关闭后重新发起需要查询或处理更老的代码,需要去备份仓库查看(xxxx/xxxx.git)开发过程中有任何疑问,欢迎请随时联系 xxx

10、兜底回滚方案


因为使用了备份仓库,所以不会修改原始仓库,但只有代码管理平台(工蜂)在第七步的时候修改了原始仓库,对于这个工蜂的协助修改,需要提前确认好工蜂那边做好了回滚的方案。


11、踩坑记录及应对


11.1 上传 lfs 的时候报错 User is null or anonymous user



LFS: Git:User is null or anonymous user.


解决:git config lfs.example.com/test_backup… basic


输入 git lfs env 看一下输出结果改成了 basic 了吗?



11.2 git push 的时候报错



把远程链接改成 https 的:


git remote set-url origin https://example.com/test_backup.git
git remote -v

如果~/.gitconfig 中有如下的内容要先注释掉。


url.git@example.com:.insteadof=http://example.com/ url.git@example.com:.insteadof=https://example.com/

最后再 push 即可。


如果上述还不行,那么在命令行中执行:


git config --global https.postbuffer 1572864000git config --global https.lowSpeedLimit 0git config --global https.lowSpeedTime 999999

如仍然无法解决,可能是用户的客户端默认有设默认值限制 git 传输包的大小,可执行指令:


git config --global core.packedGitLimit 2048m
git config --global core.packedGitWindowSize 2048m

11.3 window 如何在 git batch 里面运行 git-filter-repo?


安装 python:打开 cmd 窗口,运行 python -m pip install git-filter-repo,安装 git-filter-repo;


用 everything 查找 git-filter-repo.exe;


cmd 窗口,运行 git --exec-path,得到路径类似:C:\Program Files\Git\mingw64\libexec\git-core;


把上面找到的 git-filter-repo.exe 拷贝到 git-core 文件夹里面;


此时在 git batch 窗口中,输入命令 git filter-repo(注意输入的git后面没有-),会提示 No arguments specified.证明 ok 了。


11.4 如果想让 git-filter-repo 作为一个 python 库来使用,实现更复杂的功能该怎么办?


比如,不想这么用了 git-filter-repo --force --commit-callback "xxxx python code...",因为这么用只能写回调的 python 代码,太弱了。


解决:python3 -m pip install --user git-filter-repo,不行就 python3 -m pip install git-filter-repo,安装这个 git-filter-repo包,然后就可以在 python 代码中作为库使用:import git_filter_repo as fr。


11.5 瘦身后发现 coding 的 win 构建机器在 clone 代码时出问题,怎么办?


卡在 git lfs pull:



卡在 git checkout --force xxxxx 提交 id:



卡在 checking out files:



调查发现,是 lfs 进程卡住,不知道什么样的场景触发的,官方有个类似 issue,以上问题均是因为 git 或者 git lfs 版本过低导致的,升级到高版本即可解决。


据当时出错 case 总结得出结论,以下 git 和 git lfs 的版本号可以保证稳定运行不出问题,如果版本号低于以下所示,最好升级。



11.6 执行 git lfs fetch 的时候报错 too many open files 的问题


解决办法:ulimit -n 9999999


12、写在最后


仓库瘦身是个细致耗时的工作,需要谨慎认真地完成。最后腾讯会议客户端仓库的大小也从 9G 瘦身到 350M ,实现的效果还是不错的。


本次我们分享了仓库瘦身的全历程,把执行命令也公示给各位读者。希望可以帮助到为类似困境而头疼的开发者们。这篇文章对您有帮助的话,欢迎转发分享。


-End-


原创作者|李双君


技术责编|陈从贵、郭浩伟



作者:腾讯云开发者
来源:juejin.cn/post/7261814990843265061
收起阅读 »

浏览器渲染15M文本导致崩溃怎么办

web
最近,我刚刚完成了一个阅读器的txt文件阅读功能,但在处理大文件时,遇到了文本内容过多导致浏览器崩溃的问题。 一般情况下,没有任何样式渲染时不会出现什么问题,15MB的文件大约会有3秒的空白时间。 <div id="content"></di...
继续阅读 »

最近,我刚刚完成了一个阅读器的txt文件阅读功能,但在处理大文件时,遇到了文本内容过多导致浏览器崩溃的问题。


一般情况下,没有任何样式渲染时不会出现什么问题,15MB的文件大约会有3秒的空白时间。


<div id="content"></div>

fetch('./dp.txt').then(resp => resp.text()).then(text => {
document.getElementById('content').innerText = text
})

尽管目前还没有严重的问题,但随着文件继续增大,肯定会超过浏览器内存限制而导致崩溃。


在开发阅读器的过程中,我添加了下面的样式,结果导致浏览器直接崩溃:


* {
margin: 0;
padding: 0;
}

html,
body {
width: 100%;
height: 100%;
overflow: hidden;
}

body {
column-fill: auto;
column-width: 375px;
overflow-x: auto;
}

预期结果应该是像下面这样分段显示:



然而,实际出现了下面的问题:


unnamed.png


因此,文件内容太多会导致浏览器崩溃。即使进行普通的渲染,我们也要考虑这个问题。


如何解决


解决这个问题的方法有点经验的前端开发工程师应该都知道可以使用虚拟滚动,重点是怎么对文本分段分段,最容易的可能就是按照一定数量字符划分,但是这个导致文本衔接不整齐出现文本跳动。如图,橙色和蓝色表示两端文本的衔接,虚拟滚动必然会提前移除橙色那块内容,那么就会导致蓝色文本位置发生改变。



要解决这个问题,我们需要想办法用某个元素替代原来的位置。当前页橙色部分删除并计算位置,问题会变得复杂并且误差比较大,因此这一部分直接保留,把这部分前面的内容移除,然后用相同长度的元素占据,接下来重点就是怎么获取到橙色部分与前面内容的分界点。


获取分界点可以使用document.createRange()document.createRange()是 JavaScript 中用于创建Range对象的方法。Range对象表示一个包含节点与文本节点之间一定范围的文档片段。这个范围可以横跨单个节点、部分节点或者多个节点。


// 创建 Range 对象
const range = document.createRange();

range.setStart(textNode, 0); // 第一个参数可以是文本节点,第二个参数表示偏移量
range.setEnd(textNode, 1);
const rect = range.getBoundingClientRect(); // 获取第一个字符的位置信息

利用Range对象的特性,我们可以从橙色部分的最后一个字符开始计算,直到找到分界点的位置。


阅读器如果仅仅只是从左往右阅读,按照上面的方法已经可以实现,但是阅读器可能会出现页面直接跳转,跳转之后的文本起点你并不知道,并且页面总页码你也无法知道。因此从一开始就要知道每一页的分界点,也就是需要做预渲染。以下是一个简单的示例:


let text = '...'
const step = 300
let end = Math.min(step, value.length) // 获取结束点

while (text.length > 0) {
node.innerText = value.substring(0, end) // 取部分插入节点
const range = document.createRange()
range.selectNodeContents(node)
const rect = range.getBoundingClientRect() // 获取当前内容的位置信息

if (rect.height > boxHeight) {
// 判断当前内容高度是否会超出显示区域的高度
// 如果超出,从 end 最后一个字符开始计算,直到不超出范围
while (bottom > boxHeight) {
// node.childNodes[0] 表示文本节点
range.setStart(node.childNodes[0], end - 1)
range.setEnd(node.childNodes[0], end)
bottom = range.getBoundingClientRect().bottom
end--
}
} else {
// 如果没有超出,end 继续增加
// ...
}
}

上面只是简单的实现原理,可以达到精确区分每一页的字符,但是计算量有点太大,15MB文本大约500多万字,循环次数估计也在几十万到上百万。在本人的电脑上测试大约需要20秒,每个人设备的性能不同,所需时间也会有所不同。很明显,这种实现方式并不太理想。


后来我对这个方案进行了优化,实际上我们不需要计算每一页的分界点,可以计算出多页的分界点,例如10页、20页、50页等。优化后的代码是将step增大,比如设为10000,然后将不能组成一页的尾部内容去掉。优化后,15MB的文本大约需要4秒左右。需要注意的是,step并不是越大越好,太大会导致渲染页面占用时间过长。


这就是我目前用来解决页面渲染大量文本的方法。如果你有更

作者:60岁咯
来源:juejin.cn/post/7261231729523965989
好的方案,欢迎留言。

收起阅读 »

古茗前端到底搞什么飞机

在前几期文章的评论中,我发现不少人有类似“古茗前端到底搞什么飞机”的疑问: 其实在入职古茗前我也有这种观点,不就是做做下单小程序,做做简单的内部管理系统吗,甚至在面试过程中我也问了面试官这个问题,在听完面试官的解答之后,我也同样地忍不住发出了“居然要做这么牛...
继续阅读 »


在前几期文章的评论中,我发现不少人有类似“古茗前端到底搞什么飞机”的疑问:



其实在入职古茗前我也有这种观点,不就是做做下单小程序,做做简单的内部管理系统吗,甚至在面试过程中我也问了面试官这个问题,在听完面试官的解答之后,我也同样地忍不住发出了“居然要做这么牛逼的事情,这还是一个奶茶公司吗”的感慨!


于是我毫不犹豫地选择加入了古茗(有免费奶茶喝!畅饮的那种!)。


到现在已经入职快一年了,是时候跟大家讲讲“古茗前端到底搞什么飞机”了。


做一杯奶茶,总共分几步?


其实古茗的业务真的很多,很多,很多!具体有哪些我不方便透露,只能说光是我们前端团队就服务了4个业务域,18条业务线。那我们是怎么服务这些业务域和业务线的呢?我就拿我所在的“机料”举例吧。


那什么是机料呢?在这里我先卖个关子,相信看完下面的介绍,你就会知道是什么意思了,以后去古茗点奶茶就可以跟别人吹牛了(狗头)


首先问大家一个问题:做一杯奶茶,总共分几步?


就和把大象塞进冰箱一样,第一步:倒上茶,第二步:加上料,第三步:吨吨吨!


是不是很简单?步骤看着是简单,但是衍生出来的问题还是很多的


第一步涉及到的问题:



  1. 泡茶汤时,不同的茶,分别用多少度的水泡?泡多长时间?泡多少量?

  2. 怎么保证全国门店的店员按要求执行了泡茶方法?

  3. 怎么灵活控制不同地区的茶保持相同/不同口感?

  4. ...


再来看看第二步涉及到的问题:



  1. 有些物料是冷冻运输的,什么时候拆封解冻?解冻多久?保质期多久?

  2. 有些物料是原材料,什么时候要制备成半成本?保质期多久?

  3. 怎么保证加料时物料是在最佳赏味期内的?

  4. 怎么保证全国门店的店员遵守食品安全规范?

  5. 怎么提前告知门店高峰期的预估物料种类以及用量?

  6. ...


最后第三步里的问题:



  1. 怎么保证门店能尽可能还原研发室里研发出来的口味?

  2. 怎么保证更快地出杯?

  3. ...


虽然看着很麻烦很多,但是我们稍微捋一下还是能捋明白的:



  • 对于“怎么保证”这类问题,其实是一种功能性问题,我们需要让我们的功能代码在门店运行

  • 对于“多少”、“多久”、“什么时候”这些问题,可以归类为配置性问题,可以通过后台配置并进行下发


那基于这两大类问题,“机料”业务就浮出水面了。


机-机器设备


机,就是机器设备(下文统一叫设备),奶茶店里有各种各样的设备,这些机器都是用来辅助店员去标准化地制作奶茶的,设备更多地在解决一些“功能性”的问题,有了这些功能,我们就可以更好地保证一系列流程的规范性。


解决了什么问题


比如上面提到的问题:泡茶汤时,不同的茶,分别用多少度的水泡?泡多长时间?泡多少量?这些问题都是直接影响茶汤的口感的,茶汤是一杯奶茶的基底,要是口感不佳,那么整杯奶茶就毁了。


虽然古茗有很严格的培训体系,每一个店员都需要来总部进行培训学习和考试,但是哪怕是老虎也有打盹的时候,我们不能完全寄希望于店员时时刻刻严格遵守不同类别的茶的制作流程,人不是机器,对不同茶汤的温度、水量、时长等等因素进行人为控制,这些都是难度极大的。


既然人不是机器,那就直接造!于是乎我们的设备部做了泡茶机、制冷设备等等设备。


有泡茶机前,我们的店员需要记下每一款茶的调制过程,然后人工去泡,这就导致同一个人,同一家店,不同时间,泡出来的茶口感会不一样。


有了泡茶机以后,店员只需要把茶包包装上的茶包码往泡茶机上扫一下,泡茶机就可以检索对应的茶汤配方自动按照标准流程、按照标准参数进行泡茶,这样就解决了人为带来的茶汤口感不稳定的问题。


并且无形中还解决了另一个问题,就是“灵活控制不同地区的茶保持相同/不同口感”,因为刚才有提到,泡茶机会根据对应的茶汤配方进行泡茶流程,那么我们就可以根据不同地区下发不同的配方,来保证泡出预期口感的茶汤。


怎么实现


那这一套我们是怎么做的呢?前端在其中扮演了什么角色呢?考虑到保密的原因,我这里就简单画几个图,详细的技术细节就不透露了。



设备侧的产品经理需求评审之后:



  1. 嵌入式开发:与“网关”开发(一般是前端开发)共同制定数据通信协议,并按这份协议进行嵌入式开发,通过这份协议可以实现硬件设备与“网关”上USB或者蓝牙的数据传输方式

  2. 后端开发:与前端制定接口格式,通过接口可以将设备的数据上传至后端,用做设备信息展示、设备异常告警等用处

  3. 前端开发:根据数据通信协议进行“网关”功能的开发,通过协议解析设备的数据,并按照接口格式上传至后端,在后台大盘上显示数据,或者在“网关”上显示相关设备的异常告警


可以看到前端不光要开发后台的功能,还需要开发“网关”的功能,这对传统前端开发来说是一种新的挑战,因为不光要写前端代码,还要掌握硬件的通信协议,了解客户端的相关开发技能,目前的“网关”其实是运行在搭载了Android系统平板的APP中,协议的实现都需要在这个APP中完成。



用这种“伪网关”的方式存在一个很明显的弊端,就是我们的门店存在多个种类的设备,光是制冷设备就分为平冷、冷冻、冷藏三种类型,每种类型分别会有1-2台设备下店,加上一些研发中的设备,这样一来网关平板的蓝牙连接数量很容易达到上限,就会造成有部分设备无法连接蓝牙的问题,为了解决这个问题,我们出了一个临时方案,一个长久方案。


临时方案就是保证核心设备是保持连接的,但是给一些相对来说不是那么重要的设备做定时断开、轮流连接的处理,这样能一定程度上解决这个问题。


长久方案就是开发一个真正意义上的“边缘网关”,边缘网关能接入更多的设备,且能更聚焦于设备的通信、信息的处理分析等功能。


边缘网关是一种用于连接边缘设备和云平台之间的网络设备。 其主要作用是在边缘设备和云平台之间构建一个灵活、高效和安全的网络,将数据从边缘设备收集并处理后,再传输到云平台上进行存储和分析。 在这个过程中,边缘网关需要具备多种功能,包括数据的采集、处理、分析、存储和传输等。


小结


我经常感叹,我们组不是单纯的前端开发工程师了,因为组里的成员一直在和设备、各种协议打交道,桌上也放着各种各样的电路板,年前甚至把一台要下店的净水器直接放在工位边联调,也写过“220V,勿碰”的牌子放在工位上。


之前和TL聊物料网的应用场景,我们都认为这就是物联网一个很好的落地场景,并且我们的目标远不止于此,业务上还有很多的问题要依赖于设备去解决,很多降本增效的事情也要依赖于设备去实现,我相信在不久的将来,古茗的设备一定会给门店带来更多的收益,古茗的IoT方案会成为茶饮界IoT方案中值得参考的那个。


料-物料


说完“机”,再来看看“料”,就是制作一杯奶茶所需要用到的物料。


餐饮行业最重视的一个问题就是食品安全问题,这首先关系到每个消费者的身体健康,其次涉及门店的经营情况,最后会影响品牌在公众心里的印象,所以食安是每一个餐饮企业的底线。


古茗为了保证食安,搞了自己的种植基地,搭建了自己的供应链系统、智能报货系统,研发室的研发员们针对每一款物料制定了“有效期”...


除了种植基地,其他业务都和我们组有关系,这里我就介绍一下物料“有效期”这件事。


解决了什么问题


在开头的经典三步的第二步,都是物料相关的内容,首先我先科普一下物料有效期相关的几个概念:解冻时间、备料时间、最佳赏味期。


解冻时间:冷冻品需要解冻的时长,比如家里的冻肉,做菜时得先拿出来解冻一下;


备料时间:解冻完成后就需要对原料进行备料加工了,还是拿冻肉举例,冻肉解冻完了就得切成肉片开始炒了;


最佳赏味期:顾名思义,就是在这个期间内食用是最好的,好比你妈妈把肉炒完了,喊你吃饭,结果你一直在玩游戏就错过了最佳赏味期了。



为什么是定这三个时间,而不是其他的四个时间,五个时间呢?


其实也是根据奶茶的制作方式来定的,因为门店要做一杯奶茶是需要用到半成品物料的,半成品物料又需要由原料制备得到,而那些冷冻原料又需要解冻(解冻其实也是制备的一个环节,只不过解冻需要的时间会很长,且不是每个物料都需要解冻,比如茶就不需要解冻),所以结合实际情况,就定了这三个时间。


按我个人理解的话,其实就是为了保证任何一家门店、任何一个店员做出来的奶茶,都是符合食安的、最新鲜的、口感最好的。


怎么实现


那这三个时间需要怎么在门店里直观地展现给店员呢?


聪明的你可能想到了,对,就是利用设备!这台设备的名字就叫“效期机”,我再画个图给大家看看效期机是怎么在门店发挥作用的。



假设店员发现XX冷冻原浆快不够用了,那他就会从冷冻柜中取出XX冷冻原浆,然后去效期机的物料列表上去找XX冷冻原浆这个物料,找到后在使用效期机的打印功能进行打印,这时打印机就会打印XX冷冻原浆的效期贴(记载了物料相关信息的一个小贴纸),店员撕下小贴纸后贴到XX冷冻原浆的容器上,最后放到解冻区进行解冻。



店员做完上面这些步骤之后就可以做自己的事了,等到了解冻时间、备料时间、超过最佳赏味期前2分钟这些节点,效期机就会进行语音播报,提醒店员XX冷冻原浆需要进行XX操作了。


那效期机是怎么知道物料的这三个时间并在对应时间给出语音提醒的呢?


其实这个链路的流程蛮简单的,效期机种记录了每个物料的三种时间,在店员打印效期贴的那一刻,这个物料的生命周期就开始了,物料的生命周期每个阶段的时长是按照物料配置平台配置的时间决定的,同时效期机内部维护了三个有序队列,分别是解冻提醒队列,备料提醒队列,最佳赏味期超期提醒队列,物料的生命周期就在这三个队列之间流转,驱动生命周期的是一个10s一次的轮询,每次轮询都会去判断三个队列的头数据是否达到触发条件,即是否到了提醒时间。


比如XX冷冻原浆的配置是解冻时间30分钟,备料时间10分钟,最佳赏味期60分钟,店员打印效期贴的时间是12:00,那么这个物料的生命周期就是这么流转的:



这里有个小细节:我们会在最佳赏味期前2分钟就进行提醒,这个的目的就是为了更进一步保证食安以及最后奶茶的口感。


小结


其实对于物料的管理,上文提到的内容只是冰山一角,我们已经做的、正在做的、未来计划要做的事还多的很。


在食安问题上,针对私自篡改效期时间的门店,会进行高额的罚款,每天也会定期进行后厨的打扫,在培训时也会针对食安部分进行严格的培训和考试;


在物料提醒的优化上,我们发现目前的提醒交互还是会存在店员理解不到位的情况,这里也在不断地进行优化和迭代;


在保证更快的出杯速度上,我们现在正在做的一件事是基于门店的实际出杯情况去生成预测物料用量,并通过效期机下发给到门店,辅助门店更好准确地进行物料的提前制备,以及提升出杯速度。


感悟


最后聊聊入职古茗后的感悟吧,在这不到一年的时间感觉成长了很多。


首先是技术吧,前面也提到了,我所在的组要和设备打交道,加上我本来是个安卓开发,现在加入古茗前端部门后,也在针对性学习前端的内容、跨端的技术,其实学习一门语言并不是难题,难的是要培养前端的编程思维,这和客户端思维还是有差别的。


其次是业务,古茗应该是我离业务最近的公司了,我给门店打过上百通电话,线下跑过十来家门店,跟产品经理去实地调研......这些经历让我更了解了店员需要什么,我们需要提供什么样的功能给他们。


然后是工程师能力,这是在古茗前端部门经常被提起的一个概念,我们给工程师的定位是能解决一类复杂问题的人,在此基础上我们就应该具备更完善的能力,不仅仅局限于写某一种或几种代码,为了搭建运维平台,我特地去学习了产品知识,按照一个完整的流程进行了原型图的评审,需求文档的评审,并参与到平台的开发中。不仅是我,古茗的前端都在朝着“工程师”而在努力着。


最后的最后,就是人长胖了,古茗真的是无限畅饮,我可真喝不动了......


最后


关注公众号「Goodme前端团队」,获取更多干货

作者:古茗前端团队
来源:juejin.cn/post/7261628991055183930
实践,欢迎交流分享~

收起阅读 »

浅谈软件质量与度量

我正在参加「掘金·启航计划」本文从研发角度探讨下高质量软件应具备哪些特点,以及如何度量软件质量。软件质量的分类软件质量通常可以分为:内部质量和外部质量。内部质量内部质量是指软件的结构和代码质量,以及其是否适合维护、扩展和重构。它关注的是软件本身的特性和属性,包...
继续阅读 »

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

本文从研发角度探讨下高质量软件应具备哪些特点,以及如何度量软件质量。

软件质量的分类

软件质量通常可以分为:内部质量和外部质量。

内部质量

内部质量是指软件的结构和代码质量,以及其是否适合维护、扩展和重构。它关注的是软件本身的特性和属性,包括:

  • 可读性:代码易于阅读和理解;
  • 易维护性:代码易于修改和维护;
  • 可测试性:代码易于编写单元测试并进行自动化测试;
  • 可靠性:代码稳定、不容易崩溃或出现错误;
  • 可扩展性:代码能够方便地进行扩展;
  • 可重用性:代码可被复用于其他项目中。

内部质量直接影响软件的可维护性和开发效率。如果软件的内部质量很差,那么开发人员可能需要花费更多的时间修复问题,而不是开发新功能。

外部质量

外部质量是指软件的用户体验和其符合用户需求的程度。它关注的是软件的功能和表现形式,包括:

  • 功能性:软件是否具有所需的功能,并且这些功能是否能够正常工作;
  • 易用性:软件是否易于使用,是否符合用户的期望;
  • 性能:软件是否运行快速并响应迅速;
  • 兼容性:软件是否能够在不同的操作系统和设备上正常工作。

外部质量如果很差,那么用户在使用软件过程中可能会遇到问题,而这些问题可能会影响用户体验,导致用户流失。

为什么内部质量更重要

内部质量高的核心降低了未来变更的成本

可以参考下图的时间-功能累计关系图。

对于内部质量比较差的软件,虽然初期进展迅速,但是随着时间的流逝,添加新功能变得越来越困难。甚至一个小改动也需要程序员理解大量代码。当开发做代码变更时,还可能产生意想不到的缺陷,因此导致测试时间长,需要更高成本来做缺陷修复和验证。

对于内部质量高的软件,则与其相反,可以参考下图的比较。

内部质量高的软件更容易被实现。

内部质量高的软件特点之一就是易读性。 这样利于开发者更快弄清楚应用程序是如何运行的,这样就可以知道如何添加新功能。如果将软件很好地划分为不同的实现模块,则开发者没必要阅读所有代码,只需要快速找到涉及功能变动模块的代码就行。

如何衡量软件质量

Cyclomatic Complexity(圈复杂度)

Cyclomatic Complexity通过计算代码中不同路径的数量来衡量代码的复杂程度。圈复杂度越高,表示代码的控制流程越复杂,可能存在更多的错误和缺陷。

下面举例说明Cyclomatic Complexity如何计算。

public int calculate(x, y) {
if (x >= 20) {
if (y >= 20) {
return y;
}

return x;
}

return x + y;
}

这段代码的流程图如下:

圈复杂度的公式如下:

E - N + 2

其中 E 表示图中的边数(上图中的所有形状),N 表示节点数(上图中的所有箭头)。因此,在我们的例子中,6 - 5 + 2 = 3,的确这段代码包含三条路径。

Maintainability Index(可维护性指数)

Maintainability Index(可维护性指数)是一种用于评估软件代码可维护性的指标。它通常考虑代码的复杂度、长度和注释等因素,并将这些因素整合成一个分数来衡量代码的可读性、可维护性和可重构性。

通常情况下,可维护性指数的分数范围是 [0,100],分数越高表示代码的可维护性越好。可维护性指数可以帮助开发人员识别哪些代码需要改进,以提高代码的可维护性和可读性,从而减少维护成本、降低缺陷率,提高代码的质量。

Dependencies(依赖)

软件的开发过程势必会依赖外部框架和库,这些框架和库自身也会经常更新(维护者会添加和删除功能、修复错误、改进性能,并修补安全漏洞)。

旧版本库和框架通常会对依赖它的软件质量产生负面影响。例如安全漏洞是明显的风险(例如22年8月份的

Apachelog4j漏洞)。

SQALE评估法

SQALE评估法主要关注四个指标:

  1. 技术债:即未来要花费的时间和资源去修复当前存在的问题
  2. 可维护性:即代码的易读性、可理解性和可扩展性,从代码的模块化程度、命名规范、注释等因素,并对这些因素进行打分。
  3. 可靠性:即软件的稳定性和可靠性,评估代码中存在的错误、漏洞和异常处理情况。如果存在较多的问题,他们就需要考虑重新设计代码或增加更多的测试用例。
  4. 性能:即软件的响应速度和处理能力

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

for和range性能大比拼!

能GET到的知识点什么场景使用for和range1. 从一个遍历开始万能的range遍历遍历array/slice/stringsarraypackage main import "fmt" func main() { var ...
继续阅读 »

能GET到的知识点

  • 什么场景使用for和range

1. 从一个遍历开始

万能的range遍历

  1. 遍历array/slice/strings

array

package main  

import "fmt"

func main() {
var UserIDList = [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

for i, v := range UserIDList {
fmt.Println(i, v)
}
}

0 1
1 2
2 3
3 4
4 5
5 6
6 7
7 8
8 9
9 10

slice

package main  

import "fmt"

func main() {
var UserIDList = [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
var UerSlice = UserIDList[:]

for i, v := range UerSlice {
fmt.Println(i, v)
}
}
0 1
1 2
2 3
3 4
4 5
5 6
6 7
7 8
8 9
9 10

字符串

func main(){
var Username = "斑斑砖abc"
for i, v := range Username {
fmt.Println(i, v)
}
}


0 26001
3 26001
6 30742
9 97
10 98
11 99


range进行对array、slice类型遍历一切都正常,但是到了对字符串进行遍历时这里就出问题了,出问题主要在索引这一块。可以看出索引是每个字节的位置,在go语言中的字符串是UTF-8编码的字节序列。而不是单个的Unicode字符。遇到中文字符时需要使用多个字节表示,英文字符一个字节进行表示,索引0-3表示了一个字符及以此完后。

  1. 遍历map
func ByMap() {  
m := map[string]int{
"one": 1,
"two": 2,
"three": 3,
}
for k, v := range m {
delete(m, "two")
m["four"] = 4
fmt.Printf("%v: %v\n", k, v)
}
}

one: 1
four: 4
three: 3


  • 和切片不同的是,迭代过程中,删除还未迭代到的键值对,则该键值对不会被迭代。
  • 在迭代过程中,如果创建新的键值对,那么新增键值对,可能被迭代,也可能不会被迭代。个人认为应该是hash的无序性问题
  • 针对 nil 字典,迭代次数为 0
  1. 遍历channel
func ByChannel() {  
ch := make(chan string)
go func() {
ch <- "a"
ch <- "b"
ch <- "c"
ch <- "d"
close(ch)
}()
time.Sleep(time.Second)
for n := range ch {
fmt.Println(n)
}
}
  • 针对于range对关闭channel的遍历,会直到把元素都读取完成。
  • 但是在for遍历会造成阻塞,因为for变量读取一个关闭的管道并不会进行退出,而是一直进行等待,但是如果关闭了会返回一个状态值可以根据该状态值判断是否需要操作

2.for和range之间奇怪的问题

2.1 无限遍历现象

for

c := []int{1, 2, 3}  
for i := 0; i < len(c); i++ {
c = append(c, i)
fmt.Println(i)
}

1
2
3
.
.
.
15096
15097
15098
15099
15100
15101
15102
15103
15104

range

c := []int{1, 2, 3}  
for _, v := range c {
c = append(c, v)
fmt.Println(v)
}

1
2
3

可以看出for循环一直在永无止境的进行追加元素。 range循环正常。原因:for循环的i < len(c)-1都会进行重新计算一次,造成了永远都不成立。range循环遍历在开始前只会计算一次,如果在循环进行修改也不会影响正常变量。

2.2 在for和range进行修改操作

for

type UserInfo struct {  
Name string
Age int
}
var UserInfoList = [3]UserInfo{
{Name: "John", Age: 25},
{Name: "Jane", Age: 30},
{Name: "Mike", Age: 28},
}
for i := 0; i < len(UserInfoList); i++ {

UserInfoList[i].Age += i
}
fmt.Println(UserInfoList)

0
1
2
[{John 25} {Jane 31} {Mike 30}]

range

var UserInfoList = [3]UserInfo{  
{Name: "John", Age: 25},
{Name: "Jane", Age: 30},
{Name: "Mike", Age: 28},
}


for i, info := range UserInfoList {
info.Age += i
}
fmt.Println(UserInfoList)

[{John 25} {Jane 30} {Mike 28}]

可以看出for循环进行修改了成功,但是在range循环修改失效,为什么呢?因为range循环返回的是对该值的拷贝,所以修改失效。for循环修相当于进行原地修改了。但如果在for循环里面进行赋值修改操作,那么修改也会进行失效 具体如下

var UserInfoList = [3]UserInfo{  
{Name: "John", Age: 25},
{Name: "Jane", Age: 30},
{Name: "Mike", Age: 28},
}
for i := 0; i < len(UserInfoList); i++ {
fmt.Println(i)
item := UserInfoList[i]
item.Age += i

}


fmt.Println(UserInfoList)
> [{John 25} {Jane 30} {Mike 28}]

3. Benchmark大比拼

主要是针对大类型结构体

type Item struct {  
id int
val [4096]byte
}

for_test.go

func BenchmarkForStruct(b *testing.B) {  
var items [1024]Item
for i := 0; i < b.N; i++ {
length := len(items)
var tmp int
for k := 0; k < length; k++ {
tmp = items[k].id
}
_ = tmp
}
}
func BenchmarkRangeStruct(b *testing.B) {
var items [1024]Item
for i := 0; i < b.N; i++ {
var tmp int
for _, item := range items {
tmp = item.id
}
_ = tmp
}
}
goos: windows
goarch: amd64
pkg: article/02fortest
cpu: AMD Ryzen 5 5600G with Radeon Graphics
BenchmarkForStruct-12 2503378 474.8 ns/op 0 B/op 0 allocs/op
BenchmarkRangeStruct-12 4983 232744 ns/op 0 B/op 0 allocs/op
PASS
ok article/02fortest 3.268s

可以看出 for 的性能大约是 range 的 600 倍。

为什么会产生这么大呢?

上述也说过,range遍历会对迭代的值创建一个拷贝。在占据占用较大的结构时每次都需要进行做一次拷贝,取申请大约4kb的内存,显然是大可不必的。所以在对于占据较大的结构时,应该使用for进行变量操作。

总结

如何选择合适的遍历,在针对与测试场景的情况下,图便捷可以使用range,毕竟for循环需要写一堆的条件,初始值等。但是如果遍历的元素是个占用大个内存的结构的话,避免使用range进行遍历。且如果需要进行修改操作的话只能用for遍历来修改,其实range也可以进行索引遍历的,在本文为写,读者可以去尝试一下。


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

告别StringUtil:使用Java 全新String API优化你的代码

前言  Java 编程语言的每一次重要更新,都引入了许多新功能和改进。 并且在String 类中引入了一些新的方法,能够更好地满足开发的需求,提高编程效率。repeat(int count):返回一个新的字符串,该字符串是由原字符串重复指定次数形成的。isBl...
继续阅读 »

前言

  Java 编程语言的每一次重要更新,都引入了许多新功能和改进。 并且在String 类中引入了一些新的方法,能够更好地满足开发的需求,提高编程效率。

  1. repeat(int count):返回一个新的字符串,该字符串是由原字符串重复指定次数形成的。
  2. isBlank():检查字符串是否为空白字符序列,即长度为 0 或仅包含空格字符的字符串。
  3. lines():返回一个流,该流由字符串按行分隔而成。
  4. strip():返回一个新的字符串,该字符串是原字符串去除前导空格和尾随空格后形成的。
  5. stripLeading():返回一个新的字符串,该字符串是原字符串去除前导空格后形成的。
  6. stripTrailing():返回一个新的字符串,该字符串是原字符串去除尾随空格后形成的。
  7. formatted(Object... args):使用指定的参数格式化字符串,并返回格式化后的字符串。
  8. translateEscapes():将 Java 转义序列转换为相应的字符,并返回转换后的字符串。
  9. transform() 方法:该方法可以将一个函数应用于字符串,并返回函数的结果。

示例

1. repeat(int count)

public class StringRepeatExample {
public static void main(String[] args) {
String str = "abc";
String repeatedStr = str.repeat(3);
System.out.println(repeatedStr);
}
}

输出结果:

abcabcabc

2. isBlank()

public class StringIsBlankExample {
public static void main(String[] args) {
String str1 = "";
String str2 = " ";
String str3 = " \t ";

System.out.println(str1.isBlank());
System.out.println(str2.isBlank());
System.out.println(str3.isBlank());
}
}

输出结果:

true
true
true

3. lines()

import java.util.stream.Stream;

public class StringLinesExample {
public static void main(String[] args) {
String str = "Hello\nWorld\nJava";
Stream<String> lines = str.lines();
lines.forEach(System.out::println);
}
}

输出结果:

Hello
World
Java

4. strip()

public class StringStripExample {
public static void main(String[] args) {
String str1 = " abc ";
String str2 = "\t def \n";
System.out.println(str1.strip());
System.out.println(str2.strip());
}
}

输出结果:

abc
def

5. stripLeading()

public class StringStripLeadingExample {
public static void main(String[] args) {
String str1 = " abc ";
String str2 = "\t def \n";
System.out.println(str1.stripLeading());
System.out.println(str2.stripLeading());
}
}

输出结果:

abc
def

6. stripTrailing()

public class StringStripTrailingExample {
public static void main(String[] args) {
String str1 = " abc ";
String str2 = "\t def \n";
System.out.println(str1.stripTrailing());
System.out.println(str2.stripTrailing());
}
}

输出结果:

abc
def

7. formatted(Object... args)

public class StringFormattedExample {
public static void main(String[] args) {
String str = "My name is %s, I'm %d years old.";
String formattedStr = str.formatted( "John", 25);
System.out.println(formattedStr);
}
}

输出结果:

My name is John, I'm 25 years old.

8. translateEscapes()

public class StringTranslateEscapesExample {
public static void main(String[] args) {
String str = "Hello\\nWorld\\tJava";
String translatedStr = str.translateEscapes();
System.out.println(translatedStr);
}
}

输出结果:

Hello
World Java

9. transform()

public class StringTransformExample {
public static void main(String[] args) {
String str = "hello world";
String result = str.transform(i -> i + "!");
System.out.println(result);
}
}

输出结果:

hello world!

结尾

  如果觉得对你有帮助,可以多多评论,多多点赞哦,也可以到我的主页看看,说不定有你喜欢的文章,也可以随手点个关注哦,谢谢。

  我是不一样的科技宅,每天进步一点点,体验不一样的生活。我们下


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

俺,25岁,踌躇一下?

人比山高大家好,我是寒草,一只工作两年的程序猿。农历七月初五,阳历八月七日是我的生日,小时候总是期盼生日的到来,而且总有一种我过生日的那天我就是山大王的错觉,毕竟可以吃香的,喝辣的。但是我也早就度过了无忧无虑的年纪,而生日这一天的意义也早就有所不同。长大后,每...
继续阅读 »

人比山高

大家好,我是寒草,一只工作两年的程序猿。农历七月初五,阳历八月七日是我的生日,小时候总是期盼生日的到来,而且总有一种我过生日的那天我就是山大王的错觉,毕竟可以吃香的,喝辣的。但是我也早就度过了无忧无虑的年纪,而生日这一天的意义也早就有所不同。

长大后,每当生日临近,总是思绪万千,或是思考过去,或是展望未来,年少时的悠哉不再,而思绪确是一年比一年多,有一位著名工程师曾经说过:

思绪和发量是负相关的。

虽然上面这个定律在我身上还没有印证,但是我不妨借生日这个难得的机会整理一下,收拾行囊,以便整装待发。

脚比路长

写文的当下,我正在思考,去年写文章的时候是什么样的心理:

  • 持续亢奋?
  • 充满希望?
  • 活力四射?

反正在我印象里,去年的我心中是充满希望和活力的,但是现在的我肯定和去年大不相同,可能是因为这一年出现了很多的变化:

  • 停不下来的裁员潮
  • 一座座楼(公司)塌了

听了很多悲伤的事,也经历了很多令人消沉的事,使得我并不会如去年那般纯粹的充满活力,也以更加理性和辩证的去看待问题,世界也不再非黑即白,这大概就是“成长”吧,在此推荐:《少有人走的路

在夜晚一个人走在回家的小路,我会思考很多很多事,但多数像泡沫一样飞散了,但是有一件事是我不会放弃的,也算是我长久以来的梦。

见下文

白山旭日

前一段时间,我开了一个新坑:程序猿之梦!星辰大海的前端建站之路「第一周」,没错,现在的我有一个理想,想创造一个自己的产品,我对她寄托了很多很多情感:

  • 我希望她可以承载更多的美好
  • 我希望她可以创造一股清流在网络社会流淌
  • 我希望她可以为社会提供向上的价值
  • ...

但是这个事情已经停摆了一个月了,毕竟我上个月大概上了 250 个小时的班,工作饱和度也基本来到了 150% ,整个人特别特别疲惫,掘金技术圈的群也是会经常 cue 到我的加班。

不要说我卷,我是很期望刘慈欣摸鱼写《三体》的那种生活的,但是生活所迫。

即使工作比较辛苦让我疲惫至极,我也会想把我的产品搞下去,我总是做不切实际的梦,我总是有一堆幻想,我总希望我从事的事业可以让世界更加美好,我总是焦虑,我总是烦躁,我总是有一种奇奇怪怪的理想主义,还有那么一丝丝的浪漫情怀

所以,无论如何,我欣赏我,我会是我。

黑水金光

前几天农历生日的时候,同事们陪我过了一个生日:

我作为一个东北人,也是第一次吃到铁锅炖大鹅(我怕不是个假东北人),还是很开心的,我从小到大还是第一次这么“正式”的过生日,近几天也陆陆续续的收到一些礼物:

  • 一把工学椅:这样我在家就可以更舒适的创作了
  • 一个木制密码箱:最关键还需要我自己拼(其实我没有什么特别值得放在密码箱里的东西)
  • 特利迦奥特曼:这肯定是很了解我的人才会送的礼物~
  • ...

羁绊越来越多了呢🌟还是要充满阳光的好好生活呀~

旅程

如果大家有耐心读到这里,一定有这样的一个疑问:“你这前四个标题是什么意思?”,其实只是我摘取了我母校校歌中的几句词,同时我也想到很多关于母校的指引我前进的故事:

  • 黄大年教授:振兴中华,乃我辈之责

已故,纪念文:「振兴中华,乃我辈之责」于75周年校庆使用 canvas 纪念黄大年老师

  • 张希校长:群居不倚,独立不惧
  • ...

最后,祝我自己 25 岁生日快乐 🎂

✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨

江山如此多娇,引无数英雄竞折腰。
惜秦皇汉武,略输文采;唐宗宋祖,稍逊风骚。
一代天骄,成吉思汗,只识弯弓射大雕。
俱往矣,数风流人物,还看今朝。

— 《沁园春.雪》

✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨

各位,感谢阅读,一起加油!也欢迎各位加我微信:hancao97 和我交流。

-寒草写于2022.08.06 🔥 To Be Continued-


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

深入理解与运用Android Jetpack ViewModel

在Android开发中,数据与界面的分离一直是一项重要的挑战。为了解决这个问题,Google推出了Android Jetpack组件之一的ViewModel。ViewModel是一种用于管理UI相关数据的架构组件,它能够帮助开发者实现优雅的数据驱动和生命周期管...
继续阅读 »

在Android开发中,数据与界面的分离一直是一项重要的挑战。为了解决这个问题,Google推出了Android Jetpack组件之一的ViewModel。ViewModel是一种用于管理UI相关数据的架构组件,它能够帮助开发者实现优雅的数据驱动和生命周期管理。本文将深入浅出地介绍ViewModel的使用和原理,带你一步步掌握这个强大的组件。

什么是ViewModel

ViewModel是Android Jetpack组件之一,它的主要目的是将UI控制器(如Activity和Fragment)与数据相关的业务逻辑分开,使得UI控制器能够专注于展示数据和响应用户交互,而数据的获取和处理则交由ViewModel来管理。这种分离能够使代码更加清晰、易于测试和维护。

ViewModel的原理

ViewModel的原理其实并不复杂。在设备配置发生变化(如屏幕旋转)导致Activity或Fragment重建时,ViewModel不会被销毁,而是保留在内存中。这样,UI控制器可以在重建后重新获取之前的ViewModel实例,并继续使用其中的数据,从而避免数据丢失和重复加载。

ViewModelStore和ViewModelStoreOwner

ViewModel的原理涉及两个核心概念:ViewModelStore和ViewModelStoreOwner。

ViewModelStore是一个存储ViewModel实例的容器,它的生命周期与UI控制器的生命周期关联。在UI控制器(Activity或Fragment)被销毁时,ViewModelStore会清理其中的ViewModel实例,避免内存泄漏。

ViewModelStoreOwner是拥有ViewModelStore的对象,通常是Activity或Fragment。ViewModelProvider通过ViewModelStoreOwner来获取ViewModelStore,并通过ViewModelStore来管理ViewModel的生命周期。

ViewModelProvider

ViewModelProvider是用于创建和获取ViewModel实例的工具类。它负责将ViewModel与ViewModelStoreOwner关联,并确保ViewModel在合适的时机被销毁。

在Activity中获取ViewModel实例:

viewModel = new ViewModelProvider(this).get(MyViewModel.class);

在Fragment中获取ViewModel实例:

viewModel = new ViewModelProvider(this).get(MyViewModel.class);

使用ViewModel

添加ViewModel依赖

首先,确保你的项目已经使用了AndroidX,并在build.gradle中添加ViewModel依赖:

dependencies {
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1"
}

创建ViewModel

创建ViewModel非常简单,只需继承ViewModel类并在其中定义数据和相关操作。

public class MyViewModel extends ViewModel {
private MutableLiveData<String> data = new MutableLiveData<>();

public LiveData<String> getData() {
return data;
}

public void fetchData() {
// 模拟异步数据获取
new Handler().postDelayed(() -> {
data.setValue("Hello, ViewModel!");
}, 2000);
}
}

在UI控制器中使用ViewModel

在Activity或Fragment中获取ViewModel的实例,并观察数据变化:

viewModel = new ViewModelProvider(this).get(MyViewModel.class);
viewModel.getData().observe(this, data -> {
// 更新UI
textView.setText(data);
});

viewModel.fetchData(); // 触发数据获取操作

ViewModel与跨组件通信

ViewModel不仅仅用于在单个UI控制器内部共享数据,它还可以用于在不同UI控制器之间共享数据,实现跨组件通信。例如,一个Fragment中的数据可以通过ViewModel传递给Activity。

在Activity中共享数据:

sharedViewModel = new ViewModelProvider(this).get(SharedViewModel.class);
sharedViewModel.getData().observe(this, data -> {
// 更新UI
textView.setText(data);
});

在Fragment中共享数据:

sharedViewModel = new ViewModelProvider(requireActivity()).get(SharedViewModel.class);

注意:在跨组件通信时,需要使用同一个ViewModelProvider获取相同类型的ViewModel实例。在Activity中,使用this作为ViewModelProvider的参数,在Fragment中,使用requireActivity()作为参数。

ViewModel与SavedState

有时,我们可能希望在ViewModel中保存一些与UI控制器生命周期无关的数据,以便在重建时恢复状态。ViewModel提供了SavedState功能,它可以让我们在ViewModel中持久化保存数据。

示例代码:

public class MyViewModel extends ViewModel {
private SavedStateHandle savedStateHandle;

public MyViewModel(SavedStateHandle savedStateHandle) {
this.savedStateHandle = savedStateHandle;
}

public LiveData<String> getData() {
return savedStateHandle.getLiveData("data");
}

public void setData(String data) {
savedStateHandle.set("data", data);
}
}

使用SavedStateViewModelFactory创建带有SavedState功能的ViewModel:

public class MyActivity extends AppCompatActivity {
private MyViewModel viewModel;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

ViewModelProvider.Factory factory = new SavedStateViewModelFactory(getApplication(), this);
viewModel = new ViewModelProvider(this, factory).get(MyViewModel.class);

viewModel.getData().observe(this, data -> {
// 更新UI
textView.setText(data);
});

if (savedInstanceState == null) {
// 第一次创建时,触发数据获取操作
viewModel.fetchData();
}
}
}

ViewModel使用过程中的注意点

  • 不要在ViewModel中持有Context的引用,避免引发内存泄漏。
  • ViewModel应该只关注数据和业务逻辑,不应处理UI相关的操作。
  • 不要在ViewModel中保存大量数据,避免占用过多内存。
  • 当数据量较大或需要跨进程共享数据时,应该考虑使用其他解决方案,如Room数据库或SharedPreferences。

结论

通过本文的介绍,你已经了解了Android Jetpack ViewModel的使用与原理。ViewModel的出现极大地简化了Android开发中的数据管理和生命周期处理,使得应用更加健壮和高效。在实际开发中,合理使用ViewModel能够帮助你构建优雅、易维护的Android应用。


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

Android TextView中那些冷门好用的用法

介绍TextView 是 Android 开发中最常用的小部件之一。它用于在屏幕上显示文本。但是,TextView 有几个较少为人知的功能,对开发人员非常有用。在本博客文章中,我们将探讨其中的一些功能。自定义字体默认情况下,TextView 使用系统字体显示文...
继续阅读 »

介绍

TextView 是 Android 开发中最常用的小部件之一。它用于在屏幕上显示文本。但是,TextView 有几个较少为人知的功能,对开发人员非常有用。在本博客文章中,我们将探讨其中的一些功能。

自定义字体

默认情况下,TextView 使用系统字体显示文本。但其实我们也可以导入我们自己的字体文件在 TextView 中使用自定义字体。这可以通过将字体文件添加到资源文件夹(res/font 或者 assets)并在 TextView 上以编程方式设置来实现。

要使用自定义字体,我们需要下载字体文件(或者自己生成)并将其添加到资源文件夹中。然后,我们可以使用setTypeface()方法在TextView上以编程方式设置字体。我们还可以在XML中使用android:fontFamily属性设置字体。需要注意的是,fontFamily方式只能使用系统预设的字体并且仅对英文字符有效,如果TextView的文本内容是中文的话这个属性设置后将不会有任何效果。

以下是 Android TextView 自定义字体的代码示例:

  1. 将字体文件添加到 assets 或 res/font 文件夹中。
  2. 通过以下代码设置字体:
// 字体文件放到 assets 文件夹的情况
Typeface tf = Typeface.createFromAsset(getAssets(), "fonts/myfont.ttf");
TextView tv = findViewById(R.id.tv);
tv.setTypeface(tf);
// 字体文件放到 res/font 文件夹的情况, 需注意的是此方式在部分低于 Android 8.0 的设备上可能会存在兼容性问题
val tv = findViewById<TextView>(R.id.tv)
val typeface = ResourcesCompat.getFont(this, R.font.myfont)
tv.typeface = typeface

在上面的示例中,我们首先从 assets 文件夹中创建了一个新的 Typeface 对象。然后,我们使用 setTypeface() 方法将该对象设置为 TextView 的字体。

在上面的示例中,我们将字体文件命名为 “myfont.ttf”。我们可以将其替换为要使用的任何字体文件的名称。

自定义字体是 TextView 的强大功能之一,它可以帮助我们创建具有独特外观和感觉的应用程序。另外,我们也可以通过这种方法实现自定义图标的绘制。

AutoLink

AutoLink 可以自动检测文本中的模式并将其转换为可点击的链接。例如,如果 TextView 包含电子邮件地址或 URL,则 AutoLink 将识别它并使其可点击。此功能使开发人员无需手动创建文本中的可点击链接。

要在 TextView 上启用 AutoLink,您需要将autoLink属性设置为emailphoneweball。您还可以使用Linkify类设置自定义链接模式。

以下是一个Android TextView AutoLink代码使用示例:

<TextView
android:id="@+id/tv3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:autoLink="web"
android:textColorLink="@android:color/holo_red_dark"
android:text="这是我的个人博客地址: http://www.geektang.cn" />

在上面的示例中,我们将 autoLink 属性设置为 web ,这意味着 TextView 将自动检测文本中的 URL 并将其转换为可点击的链接。我们还将 text 属性将文本设置为 这是我的个人博客地址: http://www.geektang.cn 。当用户单击链接时,它们将被带到 http://www.geektang.cn 网站。另外,我们也可以通过 textColorLink 属性将 Link 颜色为我们喜欢的颜色。

AutoLink是一个非常有用的功能,它可以帮助您更轻松地创建可交互的文本。

对齐模式

对齐模式允许您通过在单词之间添加空格将文本对齐到左右边距,这使得文本更易读且视觉上更具吸引力。您可以将对齐模式属性设置为 inter_word 或 inter_character

要使用对齐模式功能,您需要在 TextView 上设置 justificationMode 属性。但是,此功能仅适用于运行 Android 8.0(API 级别 26)或更高版本的设备。

以下是对齐模式功能的代码示例:

<TextView
android:id="@+id/text_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="This is some sample text that will be justified."
android:justificationMode="inter_word"/>

在上面的示例中,我们将 justificationMode 属性设置为 inter_word 。这意味着 TextView 将在单词之间添加空格,以便将文本对齐到左右边距。

以下是对齐模式功能的显示效果示例:

同样一段文本,上面的设置 justificationMode 为 inter_word ,是不是看起来会比下面的好看一些呢?这个属性一般用于多行英文文本,如果只有一行文本或者文本内容是纯中文字符的话,不会有任何效果。


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

六种常见的排序算法

排序算法数组任意两值交换创建临时变量进行交换private void swap(int[] nums, int idx1, int idx2) { int temp = nums[idx1]; nums[idx1] = nums[idx2]; ...
继续阅读 »

排序算法

数组任意两值交换

创建临时变量进行交换

private void swap(int[] nums, int idx1, int idx2) {
int temp = nums[idx1];
nums[idx1] = nums[idx2];
nums[idx2] = temp;
}

冒泡排序

思路:每次对 [0, j] 进行排序,把该区间中最大的值放到这个区间的最右边

时间复杂度:O(n2)

空间复杂度:O(1)

/**
* 冒泡排序
*
* @param nums 数组
*/
public void bubbleSort(int[] nums) {
for (int i = 0; i < nums.length - 1; i++) {
for (int j = 0; j < nums.length - 1 - i; j++) {
if (nums[j] > nums[j + 1]) {
swap(nums, j, j + 1);
}
}
}
}

选择排序

思路:对于区间 [j, nums.length] (i <= j <= nums.length),每次在这个区间中选择最小的值,插入到 nums[i] 中,即每次选择一个最小的值插入到 nums[i] 中;

时间复杂度:O(n2)

空间复杂度:O(1)

/**
* 插入排序
*
* @param nums 数组
*/
public void insertSort(int[] nums) {
for (int i = 0; i < nums.length; i++) {
int idx = 0;
int min = Integer.MAX_VALUE;
for (int j = i; j < nums.length; j++) {
if (nums[j] < min) {
min = nums[j];
idx = j;
}
}
swap(nums, i, idx);
}
}

插入排序

思路:对于区间 [0, j] ,在 [i, length-1] 的区间中每次使用下标的 i 的数( j <= i ),插入到区间 [0, j] 中,保证 [0, j] 是有序的

时间复杂度:O(n2)

空间复杂度:O(1)

/**
* 插入排序
*
* @param nums 数组
*/
public void insertSort(int[] nums) {
for (int i = 1; i < nums.length; i++) {
int temp = nums[i];
int j = i;
for (; j > 0; j--) {
if (temp < nums[j - 1]) {
nums[j] = nums[j - 1];
} else {
break;
}
}
nums[j] = temp;
}
}

快速排序

思路:

  1. 对于单次的排序 partition() ,定义一个标志 part ,凡是小于该值的都放左边,大于该值的都放右边,最后把该值放到中间,并返回中间的下标 partition ,这里实现的关键是:存在一个指针 j 始终指向左边区间的最靠右的值,若 j + 1,则去到了右区间;
  2. 将数组以 partition 为中点,将数组分成两份,每一份继续进行 partition()

时间复杂度:O(nlogn)

空间复杂度:O(logn)

/**
* 递归函数
*
* @param nums 数组
* @param left 左
* @param right 右
*/
public void quickSort(int[] nums, int left, int right) {

if (left >= right) {
return;
}

int partition = partition(nums, left, right);

quickSort(nums, left, partition - 1);
quickSort(nums, partition + 1, right);
}

/**
* 将小于某个元素的值放到左边,大于某个元素的值放到右边
*
* @param nums 数组
* @param left 左
* @param right 右
* @return 结果
*/
public int partition(int[] nums, int left, int right) {
// 以数组的左边的值作为标记
int part = nums[left];
int i = left + 1;
// j 始终指向左边区间小于或等于 part 的最靠右的值
int j = left;

for (; i < nums.length; i++) {
if (nums[i] < part) {
j++;
swap(nums, i, j);
}
}

swap(nums, j, left);
return j;
}

三向切分快速排序

适用于有重复内容的排序

思路:

  1. 分成三个区间,小于 pivot左区间),等于 pivot中区间),大于 pivot右区间);
  2. 左区间的 lt 指针永远指向该区间的最右的位置,右区间的指针永远指向该区间的最左的位置;
  3. 对于中区间,不断移动游标 i 的位置即可;

时间复杂度:O(nlogn)

空间复杂度:O(logn)

public void threeQuickSort(int[] nums, int left, int right) {
if (left >= right) {
return;
}
int pivot = nums[left];

// [left + 1, lt] 小于 pivot
// [lt + 1, i) 等于 pivot
// [gt, right] 大于 pivot
int lt = left; // 左区间的指针
int gt = right + 1; // 右区间的指针
int i = left + 1;

while (i < gt) {
if (nums[i] < pivot) {
lt++;
swap(nums, i, lt);
i++;
} else if (nums[i] == pivot) {
i++;
} else {
gt++;
swap(nums, gt, i);
}
}
swap(nums, left, lt);
threeQuickSort(nums, left, lt - 1);
threeQuickSort(nums, gt, right);
}

归并排序

思路:

  1. 分隔:先将数组不断分割,直到分割到区间 [left, right] 内只有一个值
  2. 合并:将分隔后的数组不断向上合并,利用临时数组 temp[] 存储 原来 nums 数组 [left,right] 区间的值,然后分别从 temp 数组中 [left, mid] 和 [mid + 1, right] 区间分别取出最小的值,放入 nums 数组对应的位置即可;
  3. 代码的主要难点是 nums 数组 和 temp 数组的下标对应关系
    1. 对应 left,即 [left, mid] 的起点,i 在 temp 数组起始值为 0
    2. 对应 mid + 1,即 [mid + 1, right] 的起点,j 在 temp 数组起始值为 mid - left + 1

时间复杂度:O(nlogn)

空间复杂度:O(n + logn) => O(n):临时的数组和递归时压入栈的数据占用的空间

public void mergeSort(int[] nums, int left, int right) {

if (left >= right) {
return;
}
int mid = left + (right - left) / 2;

mergeSort(nums, left, mid);
mergeSort(nums, mid + 1, right);

merge(nums, left, mid, right);
}

/**
* 合并数组
*
* @param nums 数组
* @param left 左端点
* @param mid 中点
* @param right 右端点
*/
private void merge(int[] nums, int left, int mid, int right) {
int length = right - left + 1;
int[] temp = new int[length];

for (int i = 0; i < length; i++) {
temp[i] = nums[left + i];
}

// i j 为 temp 数组的下标
// 关键是找到 i j 与 原数组 nums 下标的对应关系
int i = 0;
int j = mid - left + 1;
for (int k = 0; k < length; k++) {
if (i == mid - left + 1) {
nums[k + left] = temp[j];
j++;
} else if (j == right - left + 1) {
nums[k + left] = temp[i];
i++;
} else if (temp[i] <= temp[j]) {
nums[k + left] = temp[i];
i++;
} else {
nums[k + left] = temp[j];
j++;
}
}
}

堆排序

思路:

  1. 读者首先搞懂什么是  ,代码示例中介绍的 大顶堆,这里不作过多介绍;
  2. 首先初始化一个大顶堆,每个大顶堆的根节点是最大值;
  3. 不断把根节点的值与数组最后一个值交换,然后长度减 1 再次进行大顶堆的整理操作;

时间复杂度:O(nlogn),每次整理的时间复杂度是 logn,要进行 n 次

空间复杂度:O(1)

/**
* 堆排序
*
* @param nums 数组
*/
public void heapSort(int[] nums) {
initMaxHeap(nums);
int len = nums.length - 1;
while (len > 0) {
swap(nums, 0, len);
len--;
siftDown(nums, 0, len);
}
}

/**
* 初始化为大顶堆
*
* @param nums 数组
*/
public void initMaxHeap(int[] nums) {
int len = nums.length;
for (int i = (len - 1) / 2; i >= 0; i--) {
siftDown(nums, i, len - 1);
}
}

/**
* 向下整理
* @param nums 数组
* @param k 某个节点
* @param len 数组长度
*/
public void siftDown(int[] nums, int k, int len) {

while (k * 2 + 1 <= len) {
int j = k * 2 + 1;
if (j + 1 <= len && nums[j] < nums[j + 1]) {
j++;
}
if (nums[k] > nums[j]) {
break;
}
swap(nums, k, j);
k = j;
}
}

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

Java 理论知识整理

过滤器数据准备DAO 层 UserDao、AccountDao、BookDao、EquipmentDaopublic interface UserDao { public void save(); }@Component("userDao") public ...
继续阅读 »

过滤器

数据准备
  • DAO 层 UserDao、AccountDao、BookDao、EquipmentDao

    public interface UserDao {
    public void save();
    }
    @Component("userDao")
    public class UserDaoImpl implements UserDao {
    public void save() {
    System.out.println("user dao running...");
    }

    }
  • Service 业务层

    public interface UserService {
    public void save();
    }
    @Service("userService")
    public class UserServiceImpl implements UserService {
    @Autowired
    private UserDao userDao;//...........BookDao等

    public void save() {
    System.out.println("user service running...");
    userDao.save();
    }
    }

过滤器

名称:TypeFilter

类型:接口

作用:自定义类型过滤器

示例:

  • config / filter / MyTypeFilter

    public class MyTypeFilter implements TypeFilter {
    @Override
    /**
    * metadataReader:读取到的当前正在扫描的类的信息
    * metadataReaderFactory:可以获取到任何其他类的信息
    */
    //加载的类满足要求,匹配成功
    public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
    //获取当前类注解的信息
    AnnotationMetadata am = metadataReader.getAnnotationMetadata();
    //获取当前正在扫描的类的类信息
    ClassMetadata classMetadata = metadataReader.getClassMetadata();
    //获取当前类资源(类的路径)
    Resource resource = metadataReader.getResource();


    //通过类的元数据获取类的名称
    String className = classMetadata.getClassName();
    //如果加载的类名满足过滤器要求,返回匹配成功
    if(className.equals("service.impl.UserServiceImpl")){
    //返回true表示匹配成功,返回false表示匹配失败。此处仅确认匹配结果,不会确认是排除还是加入,排除/加入由配置项决定,与此处无关
    return true;
    }
    return false;
    }
    }
  • SpringConfig

    @Configuration
    //设置排除bean,排除的规则是自定义规则(FilterType.CUSTOM),具体的规则定义为MyTypeFilter
    @ComponentScan(
    value = {"dao","service"},
    excludeFilters = @ComponentScan.Filter(
    type= FilterType.CUSTOM,
    classes = MyTypeFilter.class
    )
    )
    public class SpringConfig {
    }

导入器

bean 只有通过配置才可以进入 Spring 容器,被 Spring 加载并控制

  • 配置 bean 的方式如下:

    • XML 文件中使用 标签配置
    • 使用 @Component 及衍生注解配置

导入器可以快速高效导入大量 bean,替代 @Import({a.class,b.class}),无需在每个类上添加 @Bean

名称: ImportSelector

类型:接口

作用:自定义bean导入器

  • selector / MyImportSelector

    public class MyImportSelector implements ImportSelector{
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
    // 1.编程形式加载一个类
    // return new String[]{"dao.impl.BookDaoImpl"};

    // 2.加载import.properties文件中的单个类名
    // ResourceBundle bundle = ResourceBundle.getBundle("import");
    // String className = bundle.getString("className");

    // 3.加载import.properties文件中的多个类名
    ResourceBundle bundle = ResourceBundle.getBundle("import");
    String className = bundle.getString("className");
    return className.split(",");
    }
    }
  • import.properties

    #2.加载import.properties文件中的单个类名
    #className=dao.impl.BookDaoImpl

    #3.加载import.properties文件中的多个类名
    #className=dao.impl.BookDaoImpl,dao.impl.AccountDaoImpl

    #4.导入包中的所有类
    path=dao.impl.*
  • SpringConfig

    @Configuration
    @ComponentScan({"dao","service"})
    @Import(MyImportSelector.class)
    public class SpringConfig {
    }

注册器

可以取代 ComponentScan 扫描器

名称:ImportBeanDefinitionRegistrar

类型:接口

作用:自定义 bean 定义注册器

  • registrar / MyImportBeanDefinitionRegistrar

    public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
    /**
    * AnnotationMetadata:当前类的注解信息
    * BeanDefinitionRegistry:BeanDefinition注册类,把所有需要添加到容器中的bean调用registerBeanDefinition手工注册进来
    */
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
    //自定义注册器
    //1.开启类路径bean定义扫描器,需要参数bean定义注册器BeanDefinitionRegistry,需要制定是否使用默认类型过滤器
    ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(registry,false);
    //2.添加包含性加载类型过滤器(可选,也可以设置为排除性加载类型过滤器)
    scanner.addIncludeFilter(new TypeFilter() {
    @Override
    public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
    //所有匹配全部成功,此处应该添加实际的业务判定条件
    return true;
    }
    });
    //设置扫描路径
    scanner.addExcludeFilter(tf);//排除
    scanner.scan("dao","service");
    }
    }
  • SpringConfig

    @Configuration
    @Import(MyImportBeanDefinitionRegistrar.class)
    public class SpringConfig {
    }

处理器

通过创建类继承相应的处理器的接口,重写后置处理的方法,来实现拦截 Bean 的生命周期来实现自己自定义的逻辑

BeanPostProcessor:bean 后置处理器,bean 创建对象初始化前后进行拦截工作的

BeanFactoryPostProcessor:beanFactory 的后置处理器

  •  加载时机:在 BeanFactory 初始化之后调用,来定制和修改 BeanFactory 的内容;所有的 bean 定义已经保存加载到 beanFactory,但是 bean 的实例还未创建
  •   执行流程:
    • ioc 容器创建对象

    • invokeBeanFactoryPostProcessors(beanFactory):执行 BeanFactoryPostProcessor

      • 在 BeanFactory 中找到所有类型是 BeanFactoryPostProcessor 的组件,并执行它们的方法
      • 在初始化创建其他组件前面执行

BeanDefinitionRegistryPostProcessor:

  • 加载时机:在所有 bean 定义信息将要被加载,但是 bean 实例还未创建,优先于 BeanFactoryPostProcessor 执行;利用 BeanDefinitionRegistryPostProcessor 给容器中再额外添加一些组件

  • 执行流程:

    • ioc 容器创建对象

    • refresh() → invokeBeanFactoryPostProcessors(beanFactory)

    • 从容器中获取到所有的 BeanDefinitionRegistryPostProcessor 组件

      • 依次触发所有的 postProcessBeanDefinitionRegistry() 方法
      • 再来触发 postProcessBeanFactory() 方法

监听器

基本概述

ApplicationListener:监听容器中发布的事件,完成事件驱动模型开发

public interface ApplicationListener<E extends ApplicationEvent>

所以监听 ApplicationEvent 及其下面的子事件

应用监听器步骤:

  • 写一个监听器(ApplicationListener实现类)来监听某个事件(ApplicationEvent及其子类)

  • 把监听器加入到容器 @Component

  • 只要容器中有相关事件的发布,就能监听到这个事件;

    •  ContextRefreshedEvent:容器刷新完成(所有 bean 都完全创建)会发布这个事件
    •  ContextClosedEvent:关闭容器会发布这个事件
  • 发布一个事件:applicationContext.publishEvent()

@Component
public class MyApplicationListener implements ApplicationListener<ApplicationEvent> {
//当容器中发布此事件以后,方法触发
@Override
public void onApplicationEvent(ApplicationEvent event) {
System.out.println("收到事件:" + event);
}
}

实现原理

ContextRefreshedEvent 事件:

  • 容器初始化过程中执行 initApplicationEventMulticaster():初始化事件多播器

    • 先去容器中查询 id = applicationEventMulticaster 的组件,有直接返回
    • 没有就执行 this.applicationEventMulticaster = new SimpleApplicationEventMulticaster(beanFactory) 并且加入到容器中
    • 以后在其他组件要派发事件,自动注入这个 applicationEventMulticaster
  • 容器初始化过程执行 registerListeners() 注册监听器

    • 从容器中获取所有监听器:getBeanNamesForType(ApplicationListener.class, true, false)
    • 将 listener 注册到 ApplicationEventMulticaster
  • 容器刷新完成:finishRefresh() → publishEvent(new ContextRefreshedEvent(this))

    发布 ContextRefreshedEvent 事件:

    • 获取事件的多播器(派发器):getApplicationEventMulticaster()

    • multicastEvent 派发事件

      • 获取到所有的 ApplicationListener

      • 遍历 ApplicationListener

        • 如果有 Executor,可以使用 Executor 异步派发 Executor executor = getTaskExecutor()
        • 没有就同步执行 listener 方法 invokeListener(listener, event),拿到 listener 回调 onApplicationEvent

容器关闭会发布 ContextClosedEvent


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

frp内网穿透

frp
Frp是什么简单地说,frp就是一个反向代理软件,它体积轻量但功能很强大,可以使处于内网或防火墙后的设备对外界提供服务,它支持HTTP、TCP、UDP等众多协议。服务端配置SSH连接到VPS之后运行如下命令查看处理器架构,根据架构下载不同版本的frp运行如下命...
继续阅读 »

Frp是什么

简单地说,frp就是一个反向代理软件,它体积轻量但功能很强大,可以使处于内网或防火墙后的设备对外界提供服务,它支持HTTP、TCP、UDP等众多协议。

服务端配置

SSH连接到VPS之后运行如下命令查看处理器架构,根据架构下载不同版本的frp
运行如下命令,根据架构不同,选择相应版本并进行下载

wget https://github.com/fatedier/frp/releases/download/v0.22.0/frp_0.22.0_linux_amd64.tar.gz

然后解压缩

tar -zxvf frp_0.22.0_linux_amd64.tar.gz

服务端的配置我们只需要关注如下几个文件

  • frps
  • frps.ini

这两个文件(s结尾代表server)分别是服务端程序和服务端配置文件
然后修改frps.ini文件

[common]
bind_port = 49273
vhost_http_port = 9001
token = Er3@SGTwHtPl+jMRD0/f3QH/A
  • “bind_port”表示用于客户端和服务端连接的端口,这个端口号我们之后在配置客户端的时候要用到。
  • “vhost_http_port”和“vhost_https_port”用于反向代理HTTP主机时使用。
  • “token”是用于客户端和服务端连接的口令,请自行设置并记录,稍后会用到。

编辑完成后保存(vim保存如果不会请自行搜索)

客户端配置

frp的客户端就是我们想要真正进行访问的那台设备。
同样地,根据客户端设备的情况选择相应的frp程序进行下载,将“frp_0.22.0_windows_amd64.zip”解压
客户端的配置我们只需要关注如下几个文件

  • frpc

  • frpc.ini

    这两个文件(c结尾代表client)分别是客户端程序和客户端配置文件。
    然后修改frpc.ini文件

[common]
server_addr = 52.80.184.170
server_port = 49273
token = Er3@SGTwHtPl+jMRD0/f3QH/A

[sentry]
type = http
local_ip = 10.10.75.137
local_port = 9001
custom_domains = 172.31.20.248

其中common字段下的三项即为服务端的设置。

  • server_addr”为服务端IP地址,填入即可。
  • server_port”为服务器端口,填入你设置的端口号即可。
  • token”是你在服务器上设置的连接口令,原样填入即可。

自定义规则

上面frpc.ini的sentry字段是自己定义的规则,自定义端口对应时格式如下。

  • [xxx]”表示一个规则名称,自己定义,便于查询即可。
  • type”表示转发的协议类型,有TCP和UDP等选项可以选择,如有需要请自行查询frp手册。
  • local_ip”是本地应用的IP地址,按照实际应用工作在本机的IP地址填写即可。
  • local_port”是本地应用的端口号,按照实际应用工作在本机的端口号填写即可。
  • custom_domains”服务端IP地址或域名,可以直接使用服务端ip或者生成一个内网ip。

后台运行脚本

运行服务端

./frpc -c frps.ini

运行客户端

./frpc -c frpc.ini

至此,我们的frp仅运行在前台,如果Ctrl+C停止或者关闭SSH窗口后,frp均会停止运行,因而我们使用 nohup命令将其运行在后台。
服务端创建start.sh脚本文件以及frps.log日志文件 编辑start.sh

nohup ./frps -c frps.ini &> frps.log &

客户端创建start.sh脚本文件以及frpc.log日志文件 编辑start.sh

nohup ./frpc -c frpc.ini &> frpc.log &

客户端和服务端执行start.sh脚本

./stash

查看log日志

tail -f frps.log
tail -f frpc.log

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

一篇文章学会正则表达式(Kotlin举例)

一篇文章学会正则表达式(Kotlin举例)正则表达式是一种用来匹配字符串的工具,它可以在文本中查找特定的模式,从而实现对文本的处理和分析。在很多编程语言中,正则表达式都是非常重要的一部分。了解正则表达式在学习正则表达式之前,我们需要先了解一些基本概念。正则表达...
继续阅读 »

一篇文章学会正则表达式(Kotlin举例)

正则表达式是一种用来匹配字符串的工具,它可以在文本中查找特定的模式,从而实现对文本的处理和分析。在很多编程语言中,正则表达式都是非常重要的一部分。

了解正则表达式

在学习正则表达式之前,我们需要先了解一些基本概念。正则表达式由一系列字符和特殊字符组成,用来匹配字符串中的模式。例如,我们可以使用正则表达式来匹配一个电话号码、一个电子邮件地址或者一个网址。正则表达式中的一些常用特殊字符包括:

  • .:匹配任意一个字符。
  • *:匹配前面的字符零次或多次。
  • +:匹配前面的字符一次或多次。
  • ?:匹配前面的字符零次或一次。
  • |:表示或的关系,匹配两边任意一边的内容。
  • ():表示分组,可以将多个字符组合成一个整体。
  • ^:匹配字符串的开头。
  • $:匹配字符串的结尾。
  • {n}:匹配前面的字符恰好出现 n 次。
  • {n,}:匹配前面的字符

正则表达式的基本语法

正则表达式的基本语法包括两个部分:模式和修饰符。模式是用来匹配字符串的规则,而修饰符则用来控制匹配的方式。

在 Kotlin 中,我们可以使用 Regex 类来表示一个正则表达式。例如,下面的代码定义了一个简单的正则表达式,用来匹配一个由数字组成的字符串:

val pattern = Regex("\\d+")

在这个正则表达式中,\d 表示匹配一个数字,+ 表示匹配前面的字符一次或多次。注意,在 Kotlin 中,我们需要使用 \\ 来表示 \ 字符,因为 \ 在字符串中有特殊的含义。

接下来,我们可以使用 matchEntire 函数来检查一个字符串是否符合这个正则表达式:

val input = "12345"
if (pattern.matchEntire(input) != null) {
println("Match!")
} else {
println("No match.")
}

这个代码会输出 Match!,因为输入的字符串符合正则表达式的规则。

常见的正则表达式的高级用法

除了基本的语法之外,正则表达式还有很多高级用法,可以实现更加复杂的匹配和替换操作。下面是一些常用的高级用法:

1. 捕获组

捕获组是指用 () 包围起来的一部分正则表达式,可以将匹配到的内容单独提取出来。例如,下面的代码定义了一个正则表达式,用来匹配一个由姓和名组成的字符串:

val pattern = Regex("(\\w+)\\s+(\\w+)")
val input = "John Smith"
val matchResult = pattern.matchEntire(input)
if (matchResult != null) {
val firstName = matchResult.groupValues[1]
val lastName = matchResult.groupValues[2]
println("First name: $firstName")
println("Last name: $lastName")
}

在这个代码中,\\w+ 表示匹配一个或多个字母、数字或下划线,\\s+ 表示匹配一个或多个空格。groupValues属性可以返回一个列表,其中包含了所有捕获组的内容。在这个例子中,groupValues[1] 表示第一个捕获组的内容,即姓,groupValues[2] 表示第二个捕获组的内容,即名。

2. 非捕获组

非捕获组是指用 (?:) 包围起来的一部分正则表达式,它和普通的捕获组的区别在于,非捕获组匹配到的内容不会单独提取出来。例如,下面的代码定义了一个正则表达式,用来匹配一个由单词和空格组成的字符串:

val pattern = Regex("(?:\\w+\\s+)+\\w+")
val input = "one two three four"
val matchResult = pattern.matchEntire(input)
if (matchResult != null) {
println("Match!")
} else {
println("No match.")
}

在这个代码中,(?:\\w+\\s+)+ 表示匹配一个或多个单词和空格组成的片段,\\w+ 表示匹配一个或多个字母、数字或下划线,\\s+ 表示匹配一个或多个空格。注意,这个正则表达式并没有使用捕获组,因此 matchResult.groupValues 的结果是一个空列表。

3. 零宽断言

零宽断言是指用 (?=) 或 (?!) 包围起来的一部分正则表达式,它可以在匹配的时候不消耗任何字符。例如,下面的代码定义了一个正则表达式,用来匹配一个以 http 或 https 开头的 URL:

val pattern = Regex("(?=http|https)\\w+")
val input = "https://www.google.com"
val matchResult = pattern.find(input)
if (matchResult != null) {
println("Match: ${matchResult.value}")
} else {
println("No match.")
}

在这个代码中,(?=http|https) 表示匹配一个以 http 或 https 开头的字符串,但是不消耗任何字符。find 函数可以在输入字符串中查找第一个匹配的子串,返回一个 MatchResult? 类型的结果。

总结

本文介绍了正则表达式的基本概念和语法,以及一些常用的高级用法。在实际的编程中,正则表达式是一种非常有用的工具,可以帮助我们快速地处理和分析文本数据。在 Kotlin 中,我们可以使用 Regex 类来表示和操作正则表达式,同时还可以使用一些高级用法来实现更加复杂的匹配和替换操作。


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

ThreadLocal的实现原理,ThreadLocal为什么使用弱引用

前言本文将讲述ThreadLocal的实现原理,还有## ThreadLocal为什么使用弱引用。ThreadLocalThreadLocal 是 Java 中的一个类,用于在多线程环境下为每个线程提供独立的变量副本。它通常用于解决多线程并发访问共享变量时的线...
继续阅读 »

前言

本文将讲述ThreadLocal的实现原理,还有## ThreadLocal为什么使用弱引用。

ThreadLocal

ThreadLocal 是 Java 中的一个类,用于在多线程环境下为每个线程提供独立的变量副本。它通常用于解决多线程并发访问共享变量时的线程安全性问题。

ThreadLocal 的工作原理是每个线程内部维护一个 ThreadLocalMap 对象,该对象用于存储每个线程的变量副本。当通过 ThreadLocal 对象获取变量时,它会首先检查当前线程是否已经创建了该变量的副本,如果有,则直接返回副本;如果没有,则通过初始化方法创建一个新的副本,并将其保存在当前线程的 ThreadLocalMap 中

使用 ThreadLocal 时,每个线程都可以独立地访问和修改自己的变量副本,而不会影响其他线程的副本。这使得在多线程环境中共享变量变得更加安全和可靠。

需要注意的是,使用 ThreadLocal 时要注意及时清理不再使用的变量副本,以避免内存泄漏问题。可以通过调用 remove() 方法来清除当前线程的变量副本。

源码解释

set方法源码

// ThreadLocal的set方法,value是要保存的值
public void set(T value) {
   // 得到当前线程对象
   Thread t = Thread.currentThread();
   // 得到当前线程对象关联的ThreadLocalMap对象
   ThreadLocalMap map = getMap(t);
  // 得到map对象就保存值,键为当前ThreadLocal对象
   // 如果没有map对象就创建一个map对象,保存值
   if (map != null)
       map.set(this, value);
   else
       createMap(t, value);
}
// 得到当前线程关联的ThreadLocalMap对象
ThreadLocalMap getMap(Thread t) {
      return t.threadLocals;
}
// 创建一个ThreadLocalMap对象,赋给当前线程的threadLocals属性,并且存入值
void createMap(Thread t, T firstValue) {
      t.threadLocals = new ThreadLocalMap(this, firstValue);
}
private void set(ThreadLocal<?> key, Object value) {
   Entry[] tab = table;
   int len = tab.length;
   // 通过key计算在tab数组中的槽位i
   int i = key.threadLocalHashCode & (len-1);
// 拿到槽位上的Entry对象,如果不为null,则进入循环,如果为null则表示可以直接加入该槽位
   // e = tab[i = nextIndex(i, len)])取出下一个槽位的Entry实体,如果为null,则表示可以直接添加进该槽位
   for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
       // 拿到与当前Entry有关联的ThreadLocal对象
       ThreadLocal<?> k = e.get();
   // 如果k与当前要保存值的key相等,则替换掉value,相当于修改key的值
       if (k == key) {
           e.value = value;
           return;
      }
// 检查当前节点的ThreadLocal如果为null,表示ThreadLocal已经被gc回收,则调用 replaceStaleEntry() 方法来替换陈旧的 Entry,将新的 ThreadLocal 和值插入到数组中的索引位置 i 处,并返回。
       if (k == null) {
           replaceStaleEntry(key, value, i);
           return;
      }
  }
// 创建一个Entry对象,加入i槽位
   tab[i] = new Entry(key, value);
   // 记录Entry对象个数
   int sz = ++size;
   // cleanSomeSlots清理陈旧的Entry,清理完后如果大于阈值,则调用rehash扩容数组
   if (!cleanSomeSlots(i, sz) && sz >= threshold)
       rehash();
}

get方法源码

public T get() {
   // 获取当前线程对象
   Thread t = Thread.currentThread();
   // 得到当前线程关联的ThreadLocalMap对象
   ThreadLocalMap map = getMap(t);
   if (map != null) {
       // 通过key获取到Entry对象
       ThreadLocalMap.Entry e = map.getEntry(this);
       // Entry不为空,则直接获取值返回结果
       if (e != null) {
           @SuppressWarnings("unchecked")
           T result = (T)e.value;
           return result;
      }
  }
   // 如果map为null,或者Entry为null,则返回一个初始化值
   return setInitialValue();
}
private T setInitialValue() {
  // 如果是在调用构造器初始化的ThreadLocal对象,该方法直接返回null
  // 如果是调用的静态方法withInitial,则返回你指定的一个初始化则
  // 并且还会把该初始化的值保存进ThreadLocalMap
       T value = initialValue();
       Thread t = Thread.currentThread();
       ThreadLocalMap map = getMap(t);
       if (map != null)
           map.set(this, value);
       else
           createMap(t, value);
       return value;
}
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
  // SuppliedThreadLocal是ThreadLocal的子类,重写了initialValue方法,通过传入一个Supplier,指定初始化值
       return new SuppliedThreadLocal<>(supplier);
}
private Entry getEntry(ThreadLocal<?> key) {
   // 计算当前key的落脚点
   int i = key.threadLocalHashCode & (table.length - 1);
   // 取出落脚点的Entry对象
   Entry e = table[i];
   // 如果e不为空,并且跟e关联的ThreadLocal对象等于当前的key,则返回当前e对象
   if (e != null && e.get() == key)
       return e;
   // 否则进入getEntryAfterMiss
   else
       return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
   Entry[] tab = table;
   int len = tab.length;
   // 如果e为null,则直接返回null,表示当前key并没有数据
   while (e != null) {
  // 取出与e关联的ThreadLocal对象
  ThreadLocal<?> k = e.get();
       // 判断k是否等于当前的ThreadLocal对象
       if (k == key)
           return e;
       // 当前k是否等于null,为null表示被gc垃圾回收,就清理旧的Entry对象
       if (k == null)
           expungeStaleEntry(i);
       else
           // 否则k不为null,取出下一个槽位,接着循环
           i = nextIndex(i, len);
       e = tab[i];
  }
   return null;
}

总结

可以看出实际保存线程局部变量的是ThreadLocalMap对象,每个线程都有一个这样的对象,保存的是键值对,键为当前的ThreadLocal对象,ThreadLocal对象一般设置为静态,非静态只会造成对象的冗余,因为ThreadLocalMap的键只能是当前ThreadLocal对象,所以只能保存一个键值对,如果要保存多个键值对,可以定义多个ThreadLocal对象作为不同的键,这样获取到的还是与线程有关联的ThreadLocalMap对象,而ThreadLocalMap的键是当前的ThreadLocal对象,多少个该对象,那就可以保存多少个值

强软弱虚四大引用

在Java中,引用是用于引用对象的一个机制,它允许我们通过引用变量来操作和访问对象。在Java中,主要有以下几种引用类型:

  1. 强引用(Strong Reference):这是最常见的引用类型。当我们使用 new 关键字创建对象时,默认就是使用强引用。如果一个对象具有强引用,即存在一个强引用变量引用它,那么垃圾回收器就不会回收该对象。只有当对象没有任何强引用时,才会被认为是不再需要的,可以被垃圾回收
  2. 软引用(Soft Reference):软引用用于描述还有用但非必需的对象。在内存不足时,垃圾回收器可能会选择回收软引用对象。使用软引用可以实现一些缓存功能,在内存不足时释放缓存中的对象,从而避免 OutOfMemoryError。可以使用 SoftReference 类来创建软引用。
  3. 弱引用(Weak Reference):弱引用的生命周期更短暂,只要垃圾回收器发现一个对象只有弱引用与之关联,就会立即回收该对象。弱引用通常用于实现一些特定的缓存或关联数据结构,当对象的强引用被释放后,关联的弱引用对象也会被自动清除。可以使用 WeakReference 类来创建弱引用。
  4. 虚引用(Phantom Reference):虚引用是最弱的引用类型,几乎没有实际的使用场景。虚引用的主要作用是跟踪对象被垃圾回收的状态。当垃圾回收器决定回收一个对象时,如果该对象有虚引用,将会在对象被回收之前,将虚引用加入到与之关联的引用队列中,供应用程序获取对象回收的状态信息。

在内存管理方面,软引用和弱引用都可以用于解决一些特定的内存问题,例如缓存管理或对象关联。它们对于临时性或可替代性对象的管理非常有用,可以在内存紧张时进行垃圾回收,从而提高系统的性能和可用性。然而,需要注意的是,对于软引用和弱引用对象,程序应该在使用时进行必要的判空和恢复处理,以避免 NullPointerException 和其他相关问题。

ThreadLocal为什么使用弱引用

ThreadLocal 使用弱引用的主要原因是为了避免内存泄漏问题。

当使用强引用持有 ThreadLocal 对象时,只有线程销毁或显式地调用 remove() 方法时,Entry 才会被释放。这可能导致在多线程环境下使用线程池时,即使线程已经使用结束处于空闲状态,对应的 Entry 仍然会存在于 ThreadLocalMap 中,导致无法回收相关资源,从而造成内存泄漏。

使用弱引用作为 ThreadLocal 的键(key),可以解决这个问题。弱引用在垃圾回收时只要发现只有弱引用指向,则会被直接回收。因此,当线程结束且对应的 ThreadLocal 对象只有弱引用存在时,垃圾回收器会自动清理该弱引用,进而清理 ThreadLocalMap 中对应的 Entry。这样可以避免内存泄漏问题。

需要注意的是,尽管 ThreadLocalMap 使用了弱引用来避免内存泄漏问题,但仍然需要在使用 ThreadLocal 后调用 remove() 方法,以确保及时清理 ThreadLocal 对象和对应的值。这是因为弱引用的回收时机不确定,不能完全依赖垃圾回收器的工作。

当我们应该请求进来分配一个线程处理请求,此时ThreadLocal对象就会被创建,并且是一个强引用,当第一次把值存入时ThreadLocal时,就会通过Thread拿到或者创建一个ThreadLocalMap对象,并且存入我们的数据,此时ThreadLocal作为键就会被放入弱引用中,此时就算发送垃圾回收也不会回收ThreadLocal因为有一个强引用指向,但是一旦我们的请求执行完毕返回,线程处于空闲状态时,这个强引用就没了,此时就剩下一个弱引用,这个时候发生垃圾回收就ThreadLocal就会被收回。


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

Kotlin | 高阶函数reduce()、fold()详解

在 Kotlin 中,reduce() 和 fold() 是函数式编程中常用的高阶函数。它们都是对集合中的元素进行聚合操作的函数,将一个集合中的元素缩减成一个单独的值。它们的使用方式非常相似,但是返回值略有不同...
继续阅读 »

在 Kotlin 中,reduce() 和 fold() 是函数式编程中常用的高阶函数。它们都是对集合中的元素进行聚合操作的函数,将一个集合中的元素缩减成一个单独的值。它们的使用方式非常相似,但是返回值略有不同。下面是它们的区别:

  • reduce() 函数是对集合中的所有元素进行聚合处理,并返回最后一个合并处理值。
  • fold() 函数除了合并所有元素之外,还可以接受一个初始值,并将其与聚合结果一起返回。注:如果集合为空的话,只会返回初始值。

reduce示例

1、使用 reduce() 函数计算列表中所有数字的总和:

fun reduceAdd() {
val list = listOf(1, 2, 3, 4, 5)
val sum = list.reduce { acc, i ->
println("acc:$acc, i:$i")
acc + i
}
println("sum is $sum") // 15
}

执行结果:

acc:1, i:2
acc:3, i:3
acc:6, i:4
acc:10, i:5
sum is 15

2、使用 reduce() 函数计算字符串列表中所有字符串的拼接结果:

val strings = listOf("apple", "banana", "orange", "pear")
val result = strings.reduce { acc, s -> "$acc, $s" }
println(result) // apple, banana, orange, pear

执行结果:

apple, banana, orange, pear

fold示例

1、使用 fold() 函数计算列表中所有数字的总和,并在其基础上加上一个初始值:

val numbers = listOf(1, 2, 3, 4, 5)
val sum = numbers.fold(10) { acc, i -> acc + i }
println(sum) // 25

执行结果为:

acc:10, i:1
acc:11, i:2
acc:13, i:3
acc:16, i:4
acc:20, i:5
sum is 25

2、使用 fold() 函数将列表中的所有字符串连接起来,并在其基础上加上一个初始值:

val strings = listOf("apple", "banana", "orange", "pear")
val result = strings.fold("Fruits:") { acc, s -> "$acc $s" }
println(result) // Fruits: apple banana orange pear

执行结果:

Fruits: apple banana orange pear

源码解析

  • reduce() 在Kotlin标准库的实现如下:
public inline fun <S, T : S> Iterable<T>.reduce(operation: (acc: S, T) -> S): S {
val iterator = this.iterator()
if (!iterator.hasNext()) throw UnsupportedOperationException("Empty collection can't be reduced.")
var accumulator: S = iterator.next()
while (iterator.hasNext()) {
accumulator = operation(accumulator, iterator.next())
}
return accumulator
}

从代码中可以看出,reduce函数接收一个operation参数,它是一个lambda表达式,用于聚合计算。reduce函数首先获取集合的迭代器,并判断集合是否为空,若为空则抛出异常。然后通过迭代器对集合中的每个元素进行遍历操作,对元素进行聚合计算,将计算结果作为累加器,传递给下一个元素,直至聚合所有元素。最后返回聚合计算的结果。

  • fold() 在Kotlin标准库的实现如下:
public inline fun <T, R> Iterable<T>.fold(
initial: R,
operation: (acc: R, T) -> R
): R {
var accumulator: R = initial
for (element in this) {
accumulator = operation(accumulator, element)
}
return accumulator
}

从代码中可以看出,fold函数接收两个参数,initial参数是累加器的初始值,operation参数是一个lambda表达式,用于聚合计算。

fold函数首先将初始值赋值给累加器,然后对集合中的每个元素进行遍历操作,对元素进行聚合计算,将计算结果作为累加器,传递给下一个元素,直至聚合所有元素。最后返回聚合计算的结果。

总结

  • reduce()适用于不需要初始值的聚合操作,fold()适用于需要初始值的聚合操作。
  • reduce()操作可以直接返回聚合后的结果,而fold()操作需要通过lambda表达式的返回值来更新累加器的值。

在使用时,需要根据具体场景来选择使用哪个函数。


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

Kotlin 字符串常用的操作符

字符串常用的操作符commonPrefixWith返回两个字符串中最长的相同前缀,如果它们没有共同的前缀,则返回空字符串,可以定义 ignoreCase 为 true忽略大小写val action = "蔡徐坤唱跳rap" val...
继续阅读 »

字符串常用的操作符

commonPrefixWith

返回两个字符串中最长的相同前缀,如果它们没有共同的前缀,则返回空字符串,可以定义 ignoreCase 为 true忽略大小写

val action = "蔡徐坤唱跳rap"
val time = "蔡徐坤两年半"
val introduce = "个人练习生蔡徐坤喜欢唱跳rap"
println(action.commonPrefixWith(time)) // 蔡徐坤
println(action.commonPrefixWith(introduce)) // ""

源码实现

// 通过while获取两个字符串同个索引下的字符是否相等
// 最后通过subSequence切割字符串
public fun CharSequence.commonPrefixWith(other: CharSequence, ignoreCase: Boolean = false): String {
val shortestLength = minOf(this.length, other.length)

var i = 0
while (i < shortestLength && this[i].equals(other[i], ignoreCase = ignoreCase)) {
i++
}
if (this.hasSurrogatePairAt(i - 1) || other.hasSurrogatePairAt(i - 1)) {
i--
}
return subSequence(0, i).toString()
}

commonSuffixWith

返回两个字符串中最长的相同后缀,如果它们没有共同的后缀,则返回空字符串,可以定义 ignoreCase 为 true忽略大小写

val action = "蔡徐坤唱跳rap"
val time = "蔡徐坤两年半"
val introduce = "个人练习生蔡徐坤喜欢唱跳rap"
println(action.commonSuffixWith(time))
println(action.commonSuffixWith(introduce))

源码实现

// 与commonPrefixWith的实现差不多,只是commonSuffixWith是倒序循环
public fun CharSequence.commonSuffixWith(other: CharSequence, ignoreCase: Boolean = false): String {
val thisLength = this.length
val otherLength = other.length
val shortestLength = minOf(thisLength, otherLength)

var i = 0
while (i < shortestLength && this[thisLength - i - 1].equals(other[otherLength - i - 1], ignoreCase = ignoreCase)) {
i++
}
if (this.hasSurrogatePairAt(thisLength - i - 1) || other.hasSurrogatePairAt(otherLength - i - 1)) {
i--
}
return subSequence(thisLength - i, thisLength).toString()
}

contains

判断字符串是否包含某字符或某字符串,可以定义 ignoreCase 为 true 忽略大小写

val introduce = "个人练习生蔡徐坤喜欢唱跳rap"
println(introduce.contains('唱')) // true
println(introduce.contains("蔡徐坤")) // true
println("蔡徐坤" in introduce) // // 同上,contains是重载操作符,可以使用该表达式
println(introduce.contains("Rap", ignoreCase = true)) // true
println("Rap" !in introduce) // !in表示不包含的意思,与!introduce.contains("Rap")是同个意思

源码实现

// 通过indexOf判断字符是否存在
public operator fun CharSequence.contains(char: Char, ignoreCase: Boolean = false): Boolean =
indexOf(char, ignoreCase = ignoreCase) >= 0

// 通过indexOf判断字符串是否存在
public operator fun CharSequence.contains(other: CharSequence, ignoreCase: Boolean = false): Boolean =
if (other is String)
indexOf(other, ignoreCase = ignoreCase) >= 0
else
indexOf(other, 0, length, ignoreCase) >= 0

endsWith

判断字符串是否以某字符或某字符串作为后缀,可以定义 ignoreCase 为 true 忽略大小写

val introduce = "个人练习生蔡徐坤喜欢唱跳rap"
println(introduce.endsWith("蔡徐坤")) // false
println(introduce.endsWith("唱跳rap")) // true

源码实现

// 字符直接判断最末尾的字符
public fun CharSequence.endsWith(char: Char, ignoreCase: Boolean = false): Boolean =
this.length > 0 && this[lastIndex].equals(char, ignoreCase)
// 如果都是String,返回String.endsWith,否则返回regionMatchesImpl
public fun CharSequence.endsWith(suffix: CharSequence, ignoreCase: Boolean = false): Boolean {
if (!ignoreCase && this is String && suffix is String)
return this.endsWith(suffix)
else
return regionMatchesImpl(length - suffix.length, suffix, 0, suffix.length, ignoreCase)
}

// 不忽略大小写,返回java.lang.String.endsWith
public actual fun String.endsWith(suffix: String, ignoreCase: Boolean = false): Boolean {
if (!ignoreCase)
return (this as java.lang.String).endsWith(suffix)
else
return regionMatches(length - suffix.length, suffix, 0, suffix.length, ignoreCase = true)
}

equals

判断两个字符串的值是否相等,可以定义 ignoreCase 为 true 忽略大小写

val introduce = "蔡徐坤rap"
println(introduce.equals("蔡徐坤Rap")) // false
println(introduce == "蔡徐坤Rap") // 同上,因为equals是重载操作符,通常使用 == 表示即可
println(introduce.equals("蔡徐坤Rap", false)) // true

源码实现

// 通过java.lang.String的equals和equalsIgnoreCase判断
public actual fun String?.equals(other: String?, ignoreCase: Boolean = false): Boolean {
if (this === null)
return other === null
return if (!ignoreCase)
(this as java.lang.String).equals(other)
else
(this as java.lang.String).equalsIgnoreCase(other)
}

ifBlank

如果字符串都是空格,将字符串转成默认值。这个操作符非常有用

val whitespace = "    ".ifBlank { "default" }
val introduce = "蔡徐坤rap".ifBlank { "default" }
println(whitespace) // default
println(introduce) // 蔡徐坤rap

源码实现

public inline fun <C, R> C.ifBlank(defaultValue: () -> R): R where C : CharSequence, C : R =
if (isBlank()) defaultValue() else this

ifEmpty

如果字符串都是空字符串,将字符串转成默认值。这个操作符非常有用,省去了你去判断空字符串然后再次赋值的操作

val whitespace = "    ".ifEmpty { "default" }
val empty = "".ifEmpty { "default" }
val introduce = "蔡徐坤rap".ifEmpty { "default" }
println(whitespace) // " "
println(empty) // default
println(introduce) // 蔡徐坤rap

判断空字符串、null 和空格字符串

  • isEmpty 判断空字符串
  • isBlank 判断字符串都是空格
  • isNotBlank 与 isBlank 相反,判断字符串不是空格
  • isNotEmpty 与 isEmpty 相反,判断字符串不是空格
  • isNullOrBlank 判断字符串不是 null 和 空格
  • isNullOrEmpty 判断字符串不是 null 和 空字符串

lines

将字符串以换行符或者回车符进行分割,返回每一个分割的子字符串 List<String>

val article = "大家好我是练习时长两年半的个人练习生\n蔡徐坤\r喜欢唱跳rop"
println(article.lines()) // [大家好我是练习时长两年半的个人练习生, 蔡徐坤, 喜欢唱跳rop]

源码实现

// 大概就是通过Sequence去切割字符串
public fun CharSequence.lines(): List<String> = lineSequence().toList()
public fun CharSequence.lineSequence(): Sequence<String> = splitToSequence("\r\n", "\n", "\r")

public fun <T> Sequence<T>.toList(): List<T> {
return this.toMutableList().optimizeReadOnlyList()
}

lowercase

将字符串都转换成小写

val introduce = "蔡徐坤RaP"
println(introduce.lowercase()) // 蔡徐坤rap

源码实现

// 通过java.lang.String的toLowerCase方法实现,其实很多kotlin的方法都是调用java的啦
public actual inline fun String.lowercase(): String = (this as java.lang.String).toLowerCase(Locale.ROOT)

replace

将字符串内的某一部分替换为新的值,可以定义 ignoreCase 为 true 忽略大小写

val introduce = "蔡徐坤rap"
println(introduce.replace("rap", "RAP"))
println(introduce.replace("raP", "RAP", ignoreCase = true))

源码实现

// 首先通过indexOf判断是否存在要被替换的子字符串
// do while循环添加被替换之后的字符串,因为字符串有可能是有多个地方需要替换,所有通过occurrenceIndex判断是否还有需要被替换的部分
public actual fun String.replace(oldValue: String, newValue: String, ignoreCase: Boolean = false): String {
run {
var occurrenceIndex: Int = indexOf(oldValue, 0, ignoreCase)
// FAST PATH: no match
if (occurrenceIndex < 0) return this

val oldValueLength = oldValue.length
val searchStep = oldValueLength.coerceAtLeast(1)
val newLengthHint = length - oldValueLength + newValue.length
if (newLengthHint < 0) throw OutOfMemoryError()
val stringBuilder = StringBuilder(newLengthHint)

var i = 0
do {
stringBuilder.append(this, i, occurrenceIndex).append(newValue)
i = occurrenceIndex + oldValueLength
if (occurrenceIndex >= length) break
occurrenceIndex = indexOf(oldValue, occurrenceIndex + searchStep, ignoreCase)
} while (occurrenceIndex > 0)
return stringBuilder.append(this, i, length).toString()
}
}

startsWith

判断字符串是否以某字符或某字符串作为前缀,可以定义 ignoreCase 为 true 忽略大小写

val introduce = "rap"
println(introduce.startsWith("Rap"))
println(introduce.startsWith("Rap", ignoreCase = true))

源码实现

// 还是调用的java.lang.String的startsWith
public actual fun String.startsWith(prefix: String, ignoreCase: Boolean = false): Boolean {
if (!ignoreCase)
return (this as java.lang.String).startsWith(prefix)
else
return regionMatches(0, prefix, 0, prefix.length, ignoreCase)
}

substringAfter

获取分割符之后的子字符串,如果不存在该分隔符默认返回原字符串,当然你可以自定义返回

例如在截取 ip:port 格式的时候,分隔符就是 :

val ipAddress = "192.168.1.1:8080"
println(ipAddress.substringAfter(":")) // 8080
println(ipAddress.substringAfter("?")) // 192.168.1.1:8080
println(ipAddress.substringAfter("?", missingDelimiterValue = "没有?这个子字符串")) // 没有?这个子字符串

源码实现

// 还是通过substring来截取字符串的
public fun String.substringAfter(delimiter: String, missingDelimiterValue: String = this): String {
val index = indexOf(delimiter)
return if (index == -1) missingDelimiterValue else substring(index + delimiter.length, length)
}

substringAfterLast

与 substringAfter 是同一个意思,不同的是如果一个字符串中有多个分隔符,substringAfter 是从第一个开始截取字符串,substringAfterLast 是从最后一个分隔符开始截取字符串

val network = "255.255.255.0:192.168.1.1:8080"
println(network.substringAfter(":")) // 192.168.1.1:8080
println(network.substringAfterLast(":")) // 8080

源码实现

// 源码和substringAfter差不多,只是substringAfterLast获取的是最后一个分割符的索引
public fun String.substringAfterLast(delimiter: String, missingDelimiterValue: String = this): String {
val index = lastIndexOf(delimiter)
return if (index == -1) missingDelimiterValue else substring(index + delimiter.length, length)
}

substringBefore

获取分割符之前的子字符串,如果不存在该分隔符默认返回原字符串,当然你可以自定义返回,与 substringAfter 刚好相反

val ipAddress = "192.168.1.1:8080"
println(ipAddress.substringBefore(":")) // 192.168.1.1
println(ipAddress.substringBefore("?")) // 192.168.1.1:8080
println(ipAddress.substringBefore("?", missingDelimiterValue = "没有?这个子字符串")) // 没有?这个子字符串

源码实现

// 还是通过substring来截取字符串的,只是是从索引0开始截取子字符串
public fun String.substringBefore(delimiter: String, missingDelimiterValue: String = this): String {
val index = indexOf(delimiter)
return if (index == -1) missingDelimiterValue else substring(0, index)
}

substringBeforeLast

与 substringBefore 是同一个意思,不同的是如果一个字符串中有多个分隔符,substringBefore 是从第一个开始截取字符串,substringBeforeLast 是从最后一个分隔符开始截取字符串

val network = "255.255.255.0:192.168.1.1:8080"
println(network.substringBefore(":")) // 255.255.255.0
println(network.substringBeforeLast(":")) // 255.255.255.0:192.168.1.1

源码实现

// 源码和substringBefore差不多,只是substringBeforeLast获取的是最后一个分割符的索引
public fun String.substringBeforeLast(delimiter: String, missingDelimiterValue: String = this): String {
val index = lastIndexOf(delimiter)
return if (index == -1) missingDelimiterValue else substring(0, index)
}

trim

去掉字符串首尾的空格符,如果要去掉字符串中间的空格符请用 replace

val introduce = "  个人练习生蔡徐坤  喜欢唱跳rap  "
println(introduce.trim()) // 个人练习生蔡徐坤 喜欢唱跳rap

uppercase

将字符串都转换成大写

源码实现

// java.lang.String.toUpperCase
public actual inline fun String.uppercase(): String = (this as java.lang.String).toUpperCase(Locale.ROOT)

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

简单教你Intent如何传大数据

前言最近想不出什么比较好的内容,但是碰到一个没毕业的小老弟问的问题,那就借机说说这个事。Intent如何传大数据?为什么是简单的说,因为这背后深入的话,有很多底层的细节包括设计思想,我也不敢说完全懂,但我知道当你用Intent传大数据报错的时候应该怎么解决,并...
继续阅读 »

前言

最近想不出什么比较好的内容,但是碰到一个没毕业的小老弟问的问题,那就借机说说这个事。Intent如何传大数据?为什么是简单的说,因为这背后深入的话,有很多底层的细节包括设计思想,我也不敢说完全懂,但我知道当你用Intent传大数据报错的时候应该怎么解决,并且简单聊聊这背后所涉及到的东西。

Intent传大数据

平时可能不会发生这种问题,但比如我之前是做终端设备的,我的设备每秒都会生成一些数据,而长时间的话数据量自然大,这时当我跳到另外一个页面使用intent把数据传过去的时候,就会报错

我们调用

intent.putExtra("key", value) // value超过1M

会报错

android.os.TransactionTooLargeException: data parcel size xxx bytes

这里的xxx就是1M左右,告诉你传输的数据大小不能超过1M,有些话咱也不敢乱说,有点怕误人子弟。我这里是凭印象说的,如果有大佬看到我说错,请狠狠的纠正我。

这个错误描述是这么描述,但真的是限死1M吗,说到这个,就不得不提一样东西,Binder机制,先不要跑,这里不会详细讲Binder,只是提一嘴。

说到Binder那就会联系到mmap内存映射,你可以先简单理解成内存映射是分配一块空间给内核空间和用户空间共用,如果还是不好理解,就简单想成分配一块空间通信用,那在android中mmap分配的空间是多少呢?1M-4K。

那是不是说Intent传输的数据超过1M-4K就会报错,理论上是这样,但实际没到这个值,比如0.8M也可能会报错。所以你不能去走极限操作,比如你的数据到了1M,你觉得只要减少点数据,减到8K,应该就能过了,也许你自己测试是正常的,但是这很危险。

所以能不传大数据就不要传大数据,它的设计初衷也不是为了传大数据用的。如果真要传大数据,也不要走极限操作。

那怎么办,切莫着急,请听我慢慢讲。就这个Binder它是什么玩意,它是Android中独特的进程通信的方式,而Linux中进程通信的方式,在Android中同样也适用。进程间通信有很多方式,Binder、管道、共享内存等。为什么会有这么多种通信方式,因为每种通信方式都有自己的特点,要在不同的场合使用不同的通信方式。

为什么要提这个?因为要看懂这个问题,你需要知道Binder这种通信方式它有什么特点,它适合大量的数据传输吗?那你Binder又与我Intent何干,你抓周树人找我鲁迅干嘛~~所以这时候你就要知道Android四大组件之间是用什么方式通信的。

有点扯远了,现在可以来说说结论了,Binder没办法传大数据,我就1M不到你想怎样?当然它不止1M,只是Android在使用时限制了它只能最多用1M,内核的最大限制是4M。又有点扯远了,你不要想着怎么把限制扩大到4M,不要往这方面想。前面说了,不同的进程通信方式,有自己的特点,适用于某些特定的场景。那Binder不适用于传输大数据,我共享内存行不行?

所以就有了解决办法

bundle.putBinder()

有人可能一看觉得,这有什么不同,这在表面上看差别不大,实则内部大大的不同,bundle.putBinder()用了共享内存,所以能传大数据,那为什么这里会用共享内存,而putExtra不是呢?想搞清楚这个问题,就要看源码了。 这里就不深入去分析了,我怕劝退,不是劝退你们,是劝退我自己。有些东西是这样的,你要自己去看懂,看个大概就差不多,但是你要讲出来,那就要看得细致,而有些细节确实会劝退人。所以想了解为什么的,可以自己去看源码,不想看的,就知道这是怎么一回事就行。

那还有没有其它方式呢?当然有,你不懂共享内存,你写到本地缓存中,再从本地缓存中读取行不行?

办法有很多,如果你不知道这个问题怎么解决,你找不到你觉得可行的解决方案,甚至可以通过逻辑通过流程的方式去绕开这个问题。但是你要知道为什么会出现这样的问题,如果你没接触过进程通信,没接触过Binder,让你看一篇文章就能看懂我觉得不切实际,但是至少得知道是怎么一回事。

比如我只说bundle.putBinder()能解决这个问题,你一试,确实能解决,但是不知道为什么,你又怕会不会有其它问题。虽然这篇文章我一直在打擦边球,没有提任何的原理,但我觉得还是能大概让人知道为什么bundle.putBinder()能解决Intent传大数据,你也就能放心去用了。


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

记一次反编译并重新打包的过程

反编译部分的介绍在文章末尾排查原因根据现象来看,这程序要嘛崩溃了,要嘛该App不适配此款盒子(比如ABI不支持、Target SDK Version等问题)minSdkVersion系统版本不支持?但是同事告诉我这款盒子是Android9,在其他的Androi...
继续阅读 »

反编译部分的介绍在文章末尾

排查原因

根据现象来看,这程序要嘛崩溃了,要嘛该App不适配此款盒子(比如ABI不支持、Target SDK Version等问题)

minSdkVersion系统版本不支持?

但是同事告诉我这款盒子是Android9,在其他的Android9和Android 4.4盒子上都跑过,没问题,排除了minSdkVersion的问题

ABI不支持?

不太可能,先不想这个

ADB才是王道

但凡遇到问题,只要设备能够adb,起码问题就解决了一半,但一问,说这盒子似乎不能Adb,,鹅鹅鹅饿~~ 后来借助adbhelper发现,此款盒子还是能adb,只是常规情况下adb的端口是60001,连上之后执行adb root的话,又会换回默认端口,这个情况我也是活久见。。

说正题,连上adb之后,通过抓日志,发现了如下问题:Permission denial: writing to settings requires:android.permission.WRITE_SECURE_SETTINGS,根据错误堆栈信息,大致是app调用了wifimanager.setWifiEnabled(true)这个方法引起的

解决

权限思路

因为自己对android.permission.WRITE_SECURE_SETTINGS这个权限并不太了解,所以从异常的字面意思理解,我以为是权限不够,所以我尝试让app拥有权限来确保其正常运行。

方法1:adb shell pm grant {包名} {权限内容}

执行命令,赋予该程序权限adb shell pm grant {packagename} android.permission.WRITE_SECURE_SETTINGS,执行命令之后,提示java.lang.SecurityException: Package xxxx has not requested permission android.permission.WRITE_SECURE_SETTINGS意思说该程序不需要这个权限,这个问题的原因是因为app并没有在清单文件中申明这个权限,这就有意思了,这个app操作需要这个权限却没有申请权限,可能主要原因是因为以前的低版本不需要,Android9需要吧,所以我们得先给他增加这个申明,然后再赋予这个权限。

通过apktool反编译,然后修改清单文件添加这个权限,再重新打包(后面再说具体得反编译重打包的步骤),一切妥当之后再次执行上面命令,果然老天是不会让我舒坦的,执行后出现异常:java.lang.SecurityException: Package android does not belong to 10034,触发问题的调用堆栈还是之前那个方法引起的。(⊙﹏⊙),思考半天,看了下它的清单文件,并没有申明targetSdkVersion,这有点怪哦,也是活久见,难道游戏apk就可以这么无视规则?那我要不给他增加上,,,嗯可以一试,还是相同的配方,给清单文件增加如下代码:

<uses-sdk
android:minSdkVersion="17"
android:targetSdkVersion="22" />

然后重新打包,再来,没错,还是同样的味道,同样的问题,我以为修改来低于23,权限能够就自动允许了,现在想起来真是too young to simple。。

东搜搜西搜搜,想尝试下是不是因为这个程序不是系统app,后来将程序放在system/app下作为系统程序,还是同样问题,所以很显然,权限这条路行不通。

所以这个问题的解决办法应该参考如下内容:

这个错误是因为你的应用试图调用setWifiEnabled方法,这个方法在Android 9(API级别28)及以上版本已经被弃用。在这些版本中,只有系统应用才能调用setWifiEnabled方法。  
即使你的应用已经被安装为系统应用,并且已经获得了WRITE_SECURE_SETTINGS权限,它仍然不能调用setWifiEnabled方法。这是因为这个方法现在只能被系统UI调用,其他应用,包括系统应用,都不能调用这个方法。
你可以考虑使用WifiNetworkSuggestion API来提示用户连接到特定的Wi-Fi网络,或者使用Settings.Panel.ACTION_WIFI来引导用户到Wi-Fi设置页面。
以下是如何使用Settings.Panel.ACTION_WIFI的示例:
val intent = Intent(Settings.Panel.ACTION_WIFI)
startActivity(intent)
这段代码会打开Wi-Fi设置页面,让用户自己开启或关闭Wi-Fi。

修改程序源代码

权限的路行不通,那我们只能想办法修复app这段逻辑代码了,但是别人的apk,显然不是那么容易让人想改就改撒,提出是这里的问题,别人也不一定信啊,所以我打算自己改这个apk的编译后的源代码,让他不要触发引起异常的那个逻辑,绕过看程序能不能正常跑起来,这一步就需要懂得起smali,还好这个问题比较简单,定位到代码改了之后,重新打包,这下消停了,程序很好正常的运行。

反编译

反编译这一块涉及很多概念,以前大概接触过,都只是看,没有实际操作,有些时候操作也只是简单的反编译看下源码,但总体工具和涉及的概念主要有ApkTool、dex2jar-2.1、jarsigner、jd-gui,还有些文章提到SignApk.jar、jax-gui等;

说下自己的理解,假如我们需要重新打包一个apk,那么我们肯定要从这个apk得到我们可以编辑的文件进行修改,修改后再重新打包,这是我们需要的核心流程;

反编译APK

ApkTool,具体的作用自己查,大致意思是如果我们想看清单文件内容,资源文件之类的,我们就可以通过这个工具进行反编译,由于前面我需要给源程序在清单文件中新增权限申明,所以我们就需要先得到反编译的工程,然后直接修改清单文件即可(把AndroidManifest拖动到Android Studio或者其他文本编辑工具中直接修改然后保存即可)

  • 反编译apk :先在终端将当前位置定位到ApkTool的目录,然后执行命令apktool d {xxx.apk:你的apk名称},该命令会将apk反编译后保存在apktool所在目录下。也可以使用如下命令指定反编译后工程的存储路径apktool d -o {反编译后的存储目录} {xxx.apk},其实只需要知道反编译apk是使用的apktool d即可,查一下文档了解更详细的用法。

  • 重新编译:修改之后我们需要重新打包成apk,使用命令apk b {反编译后的存储目录},编译成功之后,会保存在指定目录下的dist文件夹中。也可以用-o 指定存储的目录。

重签名

apk反编译修改了,也重新编译成了新的apk,但此时这个apk是没有签名的,直接拿到设备上安装是不行的,所以我们需要签名。 这里就需要用到jarsign,这个应该是jdk内自带的jarsigner -verbose -keystore {签名文件路径:也就是keystore、jks文件} -signedjar {签名后的apk路径} {没有签名的apk路径} {使用的签名文件别名,也就是keyalias}如果没出错,则我们就拥有了已经签名的被修改过的apk了,可以去运行验证了。但是由于我们是用的自己签名文件签名的,是不能覆盖安装原来的apk的,两者签名不一致。

使用系统签名

这块我没尝试过,需要的自行查看搜索查看,附带个链接Android应用程序签名系统的签名(SignApk.jar)_新根的博客-CSDN博客

修改源码

跟直接修改清单文件的原理差不多,都是直接修改,但是由于是smali,就需要做一定的语法了解才能改了。也有工具可以将smali转换成java的,比如使用skylot/jadx: Dex to Java decompiler (github.com)工具,不过这个方法只是将smali转换成java来查阅,如果我们想重新打包,那还是必须修改smali文件才行的。

关于这一节建议参考:Android App 逆向入門之二:修改 smali 程式碼 (cymetrics.io)

额外说的:其实上面整个过程我们没用到dex2jar-2.1、jd-gui之类的,其实作用不同,dex2jar的作用是将apk的后缀改成zip解压出来会得到很多dex文件,我们通过dex2jar可以让这些dex转换成jar文件,jar文件就是常规生成的java class文件了,但是jar文件没法直接打开查看,就需要借助jd-gui之类的工具。。所以的目的是说我们想看看别人程序的代码的时候用的吧。

就到这吧,做个记录。。


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

何时使用Kafka而不是RabbitMQ

Kafka 和 RabbitMQ 都是流行的开源消息系统,它们可以在分布式系统中实现数据的可靠传输和处理。Kafka 和 RabbitMQ 有各自的优势和特点,它们适用于不同的场景和需求。本文将比较 Kafka 和 RabbitMQ 的主要区别,并分析何时使用...
继续阅读 »

Kafka 和 RabbitMQ 都是流行的开源消息系统,它们可以在分布式系统中实现数据的可靠传输和处理。Kafka 和 RabbitMQ 有各自的优势和特点,它们适用于不同的场景和需求。本文将比较 Kafka 和 RabbitMQ 的主要区别,并分析何时使用 Kafka 而不是 RabbitMQ。

推荐博主开源的H5商城项目waynboot-mall,这是一套全部开源的微商城项目,包含一个运营后台、h5商城和后台接口。 实现了一个商城所需的首页展示、商品分类、商品详情、sku详情、商品搜索、加入购物车、结算下单、订单状态流转、商品评论等一系列功能。 技术上基于最新得Springboot3.0、jdk17,整合了Redis、RabbitMQ、ElasticSearch等常用中间件, 贴近生产环境实际经验开发而来不断完善、优化、改进中。

github地址:github.com/wayn111/way…

影响因素

  1. 可扩展性:Kafka 旨在处理大容量、高吞吐量和实时数据流。它每秒能够处理数百万个事件,并且可以处理大量数据。另一方面,RabbitMQ 的设计更加灵活,可以处理广泛的用例,但可能不太适合大容量、实时数据流。
  2. 耐用性:Kafka 通过将所有数据写入磁盘来提供高度的耐用性,这对于任务关键型应用程序非常重要。 RabbitMQ 还提供基于磁盘的持久性,但这可能不如 Kafka 提供的那么强大。
  3. 延迟:RabbitMQ 设计为低延迟,这对于实时数据处理和分析非常重要。Kafka 延迟相比 RabbitMQ 会高一点。
  4. 数据流:Kafka 使用无界的数据流,即数据持续地流入到指定的主题(topic)中,不会被删除或过期,除非达到了预设的保留期限或容量限制。RabbitMQ 使用有界的数据流,即数据被生产者(producer)创建并发送到消费者(consumer),一旦被消费或者达到了过期时间,就会从队列(queue)中删除。
  5. 数据使用:Kafka 支持多个消费者同时订阅同一个主题,并且可以根据自己的进度来消费数据,不会影响其他消费者。这意味着 Kafka 可以支持多种用途和场景,比如实时分析、日志聚合、事件驱动等。RabbitMQ 的消费者从一个队列中消费数据,一旦被消费,就不会再被该队列其他消费者看到。这意味着 RabbitMQ 更适合一对一的通信或任务分发。
  6. 数据顺序:Kafka 保证了同一个分区(partition)内的数据是有序的,即按照生产者发送的顺序来存储和消费。但是不同分区之间的数据是无序的,即不能保证跨分区的数据按照全局顺序来处理。 RabbitMQ 保证了同一个队列内的数据是有序的,即按照先进先出(FIFO)的原则来存储和消费。但是不同队列之间的数据是无序的,即不能保证跨队列的数据按照全局顺序来处理。
  7. 数据可靠性:Kafka 通过副本(replica)机制来保证数据的可靠性,即每个主题可以有多个副本分布在不同的节点(broker)上,如果某个节点发生故障,可以自动切换到其他节点继续提供服务。 RabbitMQ 通过镜像(mirror)机制来保证数据的可靠性,即每个队列可以有多个镜像分布在不同的节点上,如果某个节点发生故障,可以自动切换到其他节点继续提供服务。
  8. 数据持久性:Kafka 将数据持久化到磁盘中,并且支持数据压缩和批量传输,以提高性能和节省空间。Kafka 可以支持TB级别甚至PB级别的数据存储,并且可以快速地重放历史数据。RabbitMQ 将数据缓存在内存中,并且支持消息确认和事务机制,以提高可靠性和一致性。RabbitMQ 也可以将数据持久化到磁盘中,但是会降低性能和吞吐量。RabbitMQ 更适合处理小规模且实时性较高的数据。
  9. 数据扩展性:Kafka 通过分区机制来实现水平扩展,即每个主题可以划分为多个分区,并且可以动态地增加或减少分区数量
  10. 复杂性:与 RabbitMQ 相比,Apache Kafka 具有更复杂的架构,并且可能需要更多的设置和配置,因此它的复杂性也允许更高级的功能和定制。另一方面,RabbitMQ 更容易设置和使用。

应用场景

Kafka 适用场景和需求

  • 跟踪高吞吐量的活动,如网站点击、应用日志、传感器数据等。
  • 事件溯源,Kafka 保存着所有历史消息,可以用于事件回溯和审计。
  • 流式处理,如实时分析、实时推荐、实时报警等。
  • 日志聚合,如收集不同来源的日志并统一存储和分析。

RabbitMQ 适用场景和需求

  • 中小项目,项目消息量小、吞吐量不高、对延时敏感。
  • 遗留应用,如需要与旧系统或第三方系统进行集成或通信。
  • 复杂路由,如需要根据不同的规则或条件来分发或过滤消息。
  • 任务分发,如需要将任务均匀地分配给多个工作进程或消费者。

总结

在公司项目中,一般消息量都不大的情况下,博主推荐大家可以使用 RabbitMQ。消息量起来了可以考虑切换到 Kafka,但是也要根据公司内部对两种 MQ 的熟悉程度来进行选择,避免 MQ 出现问题时无法及时处理。


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

实战:工作中对并发问题的处理

大家好,我是 方圆。最近在接口联调时发生了数据并发修改问题,我想把这个问题讲解一下,并把当时提出的解决方案进行实现,希望它能在大家以后在遇到同样的问题时提供一些借鉴和思考的方向。原文还是收录在我的 Github: enthusiasm&nb...
继续阅读 »

大家好,我是 方圆。最近在接口联调时发生了数据并发修改问题,我想把这个问题讲解一下,并把当时提出的解决方案进行实现,希望它能在大家以后在遇到同样的问题时提供一些借鉴和思考的方向。原文还是收录在我的 Github: enthusiasm 中,欢迎Star和获取原文。

1. 问题背景

问题发生在快递分拣的流程中,我尽可能将业务背景简化,让大家只关注并发问题本身。

分拣业务针对每个快递包裹都会生成一个任务,我们称它为 task。task 中有两个字段需要关注,一个是分拣中发生的 异常(exp_type),另一个是分拣任务的 状态(status)。另外,需要关注 分拣状态上报接口,通过它来记录分拣过程中的异常和状态变更。

一般情况下,分拣机在分拣异常发生时会及时调用接口上报,在分拣完成时调用接口来标记为完成状态,两次接口调用的时间间隔较长,不会发生并发问题。

但是有一种特殊的分拣机,它不会在异常发生时及时上报,而是在分拣完成时将分拣过程中发生的异常和分拣结果一起上报,那么此时分拣状态上报接口在同一时间内就会有两次调用,这时便发生了预期外的并发问题。

我们先看下分拣状态上报接口的执行流程:

  1. 先查询到该分拣任务 task,默认情况下 exp_type 和 status 均为默认值0

  2. 分拣异常修改 task 中的 exp_type,分拣完成修改 status 字段信息

  3. 修改完成将 task 写入

数据库初始值为 1, 0, 0,分拣异常和分拣完成几乎同时上报,它们都读取到该值。分拣异常动作将 exp_type 修改为9,写入数据库,此时数据库值为 1, 9, 0;分拣完成动作将 status 修改为1,写入数据库,使得数据库最终值为 1, 0, 1,它将异常字段的值覆盖掉了。正常情况下,最终值应该为 1, 9, 1,分拣完成动作应该读取到分拣异常完成后的值 1, 9, 0 后再进行修改才对。

2. 解决方案

发生这个问题的原因很容易就能发现:两个事务同时执行 读取-修改-写入 序列,其中一个写操作在没有合并另一个写操作变更的情况下,直接覆盖了另一个写操作的结果,所以导致了数据的丢失。

这种问题是比较典型的 丢失更新 问题,可以通过对数据库读操作加锁或者改变数据库的隔离级别为可串行化使事务串行执行的方式进行避免。下面我会将大家在讨论避免丢失更新问题时提出的方案进行介绍,并尽可能的用代码来表现它们。

2.1 数据库读操作加锁和可串行化隔离级别

我们可以考虑:如果对每条Task数据修改的事务都是在当前事务完成之后才允许后续事务进行修改,使事务串行执行,那么我们就能够避免这种情况。比较直接的实现是通过显式加锁来实现,如下

select exp_type, status
from task
where id = 1
for update;

先查询该行数据的事务会获取到该行数据的 排他锁,后续针对该数据的所有读写请求都会被阻塞,直到先前事务执行完将锁释放。

这样通过加锁的方式实现了事务的串行执行。但是,在为SQL添加加锁语句时,需要确定是不是为该行数据加锁而不是锁住了整个表,如果是后者,那么可能会造成系统性能严重下降,而且还需要关注有哪些业务场景使用到了该SQL,是否存在长时间执行的只读事务使用,如果存在的话可能会出现因加锁导致延迟和系统性能下降,所以需要谨慎的评估。

此外,可串行化的数据库隔离级别也能保证事务的串行执行,不过它针对的是所有事务。一般情况下为了保证性能,我们不会采用这种方案(默认使用MySQL可重复读隔离级别)。

MySQL的InnoDB引擎实现可串行化隔离级别采用的是2PL机制:在第一阶段事务执行时获取锁,第二阶段事务执行完成释放锁。

2.2 针对业务只修改必要字段

如果异常状态请求仅修改 exp_type 字段,分拣完成仅修改 status 字段的话,那么我们可以梳理一下业务逻辑,仅将必要修改的字段写入数据库,这样就不会发生丢失更新的异常,如下代码所示:

// 处理异常状态请求,封装修改数据的对象
Task task = new Task();
tast.setId(id);
task.setExpType(expType);

// 更改数据
taskService.updateById(task);

在执行修改数据前,创建一个新的修改对象,并只为其必要修改字段赋值。但是还需要考虑的是:如果这个业务流程处理已经很复杂了,很可能不清楚该为哪些字段赋值而导致再发生新的异常,所以采用这种方法需要对业务足够熟悉,并且在修改完后进行充分的测试。

2.3 分布式锁

分布式锁的方法与方法一类似,都是通过加锁的方式来保证同时只有一个事务执行,区别是方法一的锁加在了数据库层,而分布式锁是借助Redis来实现。

这种实现方式的好处是锁的粒度小,发生锁争抢仅限于单个包裹,无需像数据库加锁一样去考虑锁的粒度和对相关业务的影响。伪代码如下所示:

// 分布式锁KEY
String distributedKey = String.format(DISTRIBUTED_KEY_PREFIX, packageNo);
try {
// 分布式锁阻塞同一包裹号的修改
lock(distributedKey);
// 处理业务逻辑
handler();
} finally {
// 执行完解锁
redissonDistributedLocker.unlock(distributedKey);
}

需要注意,lock() 加锁方法要保证加锁失败或发生其他异常情况不影响业务逻辑的执行,并设定好锁持有时间和等待锁的阻塞时间,此外解锁方法务必添加到 finally 代码块中保证锁的释放。

2.4 CAS

CAS是乐观的解决方案,它一般通过在数据库中增加时间戳列来记录上次数据更改的时间,当新的事务执行时,需要比对读取时该行数据的时间戳和数据库中保存的时间戳是否一致,以此来判断事务执行期间是否有其他事务修改过该行数据,只有在没有发生改变的情况下才允许更新,否则需要重试这个事务。样例SQL如下所示:

update task 
set exp_type = #{expType}, status = #{status}, ts = #{currentTs}
where id = #{id} and ts = #{readTs}

它的原理不难理解,但是实现起来可能会存在困难,因为需要考虑在执行失败后该如何重试,重试的方式和重试的次数需要根据业务去判断。


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

技术主管是否需要什么段位的技术

今天来跟大家讨论一下技术主管需要什么样段位的技术?首先我要说明的一点,技术主管前提一定是技术出身。对于那些完全不懂技术,但是又身兼技术主管或者总监的同学,我这里就不再赘述,毕竟这个已经超出我目前理解力的范围。比如阿里云的王坚博士,基本上不懂技术细节,但是依然是...
继续阅读 »

今天来跟大家讨论一下技术主管需要什么样段位的技术?

首先我要说明的一点,技术主管前提一定是技术出身。对于那些完全不懂技术,但是又身兼技术主管或者总监的同学,我这里就不再赘述,毕竟这个已经超出我目前理解力的范围。比如阿里云的王坚博士,基本上不懂技术细节,但是依然是阿里云的CTO,一手缔造了阿里云。

那我们这里再详细讨论一下,作为一名技术主管,到底应该有什么样的一个技术的段位?或者换句话来说,你的主管的技术水平需要到达什么样的一个水位?

先说结论,作为一名技术主管,一定是整个团队的技术架构师。像其他的一些大家所讨论的条件我觉得都是次要的,比如说写代码的多少,对于技术深度的钻研多少,带的团队人数多少等等,最核心的是技术主管一定要把控整个团队整个业务技术发展的骨架。

为什么说掌控团队技术架构是最重要的?因为对于一个团队来说无非就两点,第一点就是业务价值,第二点就是技术价值。

对于业务价值来说,有各种各样的同学都可以去负责业务上面的一些导向和推进,比如说产品经理,比如说运营同学。技术主管可以在一定程度上去帮助业务成功,甚至是助力业务成功,但是一定要明白技术同学一定要有自己的主轴,就是你对于整个技术的把握。因为业务上的决策说到底技术主管是只能去影响而非去决策,否则就是你们整体业务同学太过拉胯,无法形成战术合力的目的。

对于一线开发同学来说,你只要完成一个接一个的技术项目即可。但是对于技术主管来说,你就要把握整体的技术发展脉络。要清晰的明白什么样的技术架构是和当前的业务匹配的,同时又具备未来业务发展的可扩展性。

那为什么不能把整个技术架构的设计交给某一个核心的骨干研发同学呢?

所以这里就要明白,对于名技术主管来说,未必一定要深刻的钻研技术本身,一定要把技术在业务上的价值发挥到最大。所以在一定程度上来说,可以让适当的同学参与或者主导整个技术架构的设计,但是作为主管必须要了解到所谓的技术投入的产出比是什么。但是如果不对技术架构有一个彻底的理解,如何能决定ROI?

也就是在技术方案的选型里面一定要有一个平衡,能够用最小的技术投入获取到最大的技术利益,而非深究于技术本身的实习方式。如果一名技术主管不了解技术的框架或者某一些主干流程,那么就根本谈不上怎么样去评估这投入的技术产出比。一旦一名技术主管无法衡量整个技术团队的投入产出比,那就意味着整个团队的管理都是在抓虾和浑水摸鱼的状态,这时候就看你团队同学是否自觉了。

出现了这种情况下的团队,可能换一头猪在主管的位置上,业务依然运行良好。如果在业务发展好的时候,可能一直能够顺利推动,你只要坐享其成就可以了,但是一旦到了要突破困难的时期,或者在业务走下行的时候,这个时候你技术上面的优势就一点就没有了。而且在这种情况下,如果你跳槽到其他公司,作为一名技术主管,对方的公司对你的要求也是非常高的,所以这个时候你如果都说不出来你的技术价值对于业务上面的贡献是什么那想当然,你可能大概率就凉凉了。

那问题又回到了什么样的水平才能到达架构师这个话题,可以出来另一篇文章来描述,但是整体上来说,架构的本质首先一定要明白,为的就是业务的增长。

其次,架构的设计其实就是建造一个软件体系的结构,使得具备清晰度,可维护性和可扩展性。另外要想做好架构,基本的基础知识也必不可少,比如说数据库选型、分布式缓存、分库分表、幂等、分布式锁、消息架构、异步架构等等。所以本身来说做好架构师本身难度就非常大,需要长期的积累,实现厚积而薄发。如何成为一名优秀的架构师可以看我的公众号的其他文章,这里就不再详细的介绍了。

第二点是技术主管需要对于技术细节有敏感度。很多人在问一名主管到底应该具备什么样的综合能力,能不能用一种更加形象的方式来概括,我认为就有一句话就可以概括了。技术主管应该是向战略轰炸机在平常的时候一直遨游在大气的最上层能够掌控整个全局,当到了必须要战斗的时候,可以快速的补充下去,定点打击。

我参加过一次TL培训课程,讲师是阿里云智能交付技术部总经理张瑞,他说他最喜欢的一句管理概括,就是“心有猛虎,细嗅蔷薇”,也就是技术主管在平常的时候会关注于更大的宏观战略或策略,也就是注重思考全局,但是在关键的时候一定要关注和落地实际的细节。

换句更加通俗的话来说,就是管理要像战略轰炸机,平常的时候飞在万丈高空巡视,当发生了战斗的时候,立即能够实现定点轰炸。

所以如果说架构上面的设计就是对于整个团队业务和技术骨架的把握,那么对于细节的敏感度就是对于解决问题的落地能力。

那怎么样能够保证你自己有一个技术细节的敏感度?

我认为必要的代码量是需要的,也就是说对于一个主管来说,不必要写太多低代码,但一定要保证一定的代码量,让自己能够最好的,最快的,最贴近实际的理解实际的业务项目。自己写一些代码,其实好处非常多,一方面能够去巩固和加深自己对技术的理解,另外一方面也能够通过代码去更加理解业务。

当然贴近技术的方式有很多种,不一定要全部靠写代码来完成,比如说做code review的方式来完成,做技术方案的评审来完成,这都是可以的。对我来说,我就会强迫自己在每一个迭代会写上一个需求,需求会涉及到各方各面的业务点。有前端的,有后端的,也有数据库设计的。

自己亲自参与写代码或者code review,会让自己更加贴近同学,能够感知到同学的痛点,而不至于只是在空谈说教。

总结

所以对于一个技术主管来说,我认为首要的就是具备架构设计的能力,其次就是要有代码细节的敏感度,对全局和对细节都要有很强大的把控能力。

当然再总结一下,这一套理论只是适用于基础的管理者,而非高层的CTO等,毕竟不同的层级要求的能力和影响力都是不一样的。


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

前端发展:走进行业迷茫的迷雾中

引言2023年,前端开发作为IT行业中备受关注的领域之一,正在经历着巨大的挑战和变革。然而,在当前行业不景气、失业率居高不下以及裁员潮席卷而来的情况下,许多人开始质疑前端开发的未来前景以及学习它是否依然有意义。本文将探讨这个问题并试图给出一些启示。第一部分:前...
继续阅读 »

引言

2023年,前端开发作为IT行业中备受关注的领域之一,正在经历着巨大的挑战和变革。然而,在当前行业不景气、失业率居高不下以及裁员潮席卷而来的情况下,许多人开始质疑前端开发的未来前景以及学习它是否依然有意义。本文将探讨这个问题并试图给出一些启示。

第一部分:前端的价值

前端开发作为网页和移动应用程序开发的重要组成部分,扮演着连接用户与产品的桥梁。前端技术的发展不仅推动了用户体验的提升,也对整个互联网行业产生了深远的影响。随着移动互联网的普及和技术的进步,前端在用户与产品之间的交互变得越来越重要。

对于企业而言,拥有优秀的前端开发团队意味着能够提供更好的用户体验、增强品牌形象、吸引更多用户和扩大市场份额。因此,前端开发的技能依然是企业争相追求的核心能力之一。

第二部分:行业不景气的背后

然而,正如每个行业都经历高低起伏一样,前端开发也面临着行业不景气带来的挑战。2023年,全球经济增长乏力、市场竞争激烈以及萧条的就业市场等因素,使得许多公司紧缩预算、停止招聘,并导致了失业率的上升和裁员的潮水。

在这种情况下,前端开发者需要重新审视自己的技能和市场需求。他们需要具备综合能力,包括对最新前端技术的深入了解、与其他团队成员的良好沟通合作能力以及持续学习和适应变化的能力。

第三部分:自我调整与进阶

面对市场变化和就业压力,前端开发者需要主动调整自己的发展路径。以下是一些建议:

  1. 多元化技能:学习并精通多种前端框架和库,如React、Vue.js和Angular等。同时,了解后端开发和数据库知识,拥有全栈开发的能力,将会让你在就业市场上更具竞争力。
  2. 学习与实践并重:不仅仅是学习新知识,还要将所学应用于实际项目中。积累项目经验,并在GitHub等平台分享你的作品,以展示自己的能力和潜力。同时,参加行业内的比赛、活动和社区,与他人交流并学习他们的经验。
  3. 持续学习:前端技术发展日新月异,不断学习是必需的。关注行业的最新趋势和技术,参加培训、研讨会或在线课程,保持对新知识的敏感度和学习能力。

第四部分:面对就业市场的挑战

在面对行业不景气和裁员的情况下,重新进入就业市场变得更加具有挑战性。以下是一些建议:

  1. 提升个人竞争力:通过获得认证、实习或自主开发项目等方式,提升自己在简历中的竞争力。扩展自己的专业网络,与其他开发者和雇主建立联系。
  2. 寻找新兴领域:探索新兴的技术领域,如大数据、人工智能和物联网等,这些领域对前端开发者的需求逐渐增加,可能为你提供新的机会。
  3. 转型或深耕细分领域:如果市场需求不断减少,可以考虑转型到与前端相关的领域,如UI设计、交互设计或用户体验设计等。或者在前端领域深耕细分领域,在特定行业或特定技术方向上寻找就业机会。

结论

 虽然当前的行业环境确实严峻,但前端开发作为连接用户与产品的重要纽带,在未来依然有着广阔的发展空间。关键在于前端开发者要不断自我调整与进阶,持续学习并适应市场需求。通过多元化技能、学习实践、提升个人竞争力以及面对市场挑战,前端开发者依然可以在这个变革时代中谋得一席之地。


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

一代枭雄曹操也需要借力,何况我们

前言1、人情世故如果做得好就会说是情商高,做不好会说是世故,这是冯仑老师一段话,然后怎么做不世故呢,也很难评判。借着这个聊聊人情世故,在我看来它也是做事规则的一部分,我们发展很长一段历史,从不同的立场、不同的利益分出了派别,又从血缘关系分出了宗族,这些都是为了...
继续阅读 »

前言


1、人情世故

如果做得好就会说是情商高,做不好会说是世故,这是冯仑老师一段话,然后怎么做不世故呢,也很难评判。

借着这个聊聊人情世故,在我看来它也是做事规则的一部分,我们发展很长一段历史,从不同的立场、不同的利益分出了派别,又从血缘关系分出了宗族,这些都是为了利益最大化的一个产物。

反观博主本人,典型理工男,执着技术研究,所以这块一直是弱项,不太会讲话,但是我人缘一直比较好的。当然有利也有弊,弊端的话比较明显的,当一个人说话很厉害的时候,会给人自信,给人觉得靠谱,当一个人说话不咋样的时候,其实也有好处,就是藏锋,你不说出来个人想法大家是不知道你心里的小九九的,所以保全了你自身。(当一个人份量足的时候,说话会引发很大的影响,所以你可以发现如果一个人在公开场合大发演讲,要么是初出茅庐要么就是有靠山)

2、人生的发展需要平台

王立群老师:人生发展往往需要平台,秦国李斯这么一个故事,他发现仓鼠跟厕鼠待遇很不一样,同样是一个物种,但是一个光明正大的吃着粮食,一个过街老鼠人人喊打,所以他悟到了一个道理,人生好的发展需要借助平台的。

我们今天讲的人物:曹操,我们还是从几个学习角度去看,一个是做事的方法,另一个我们从他的事迹里面看出成事的借力的这么一回事。

曹操


出身

他祖父是一个大太监,伺候皇后还有皇上,古代有三股力量,两股都是因为比较亲近产生的,一个是外戚,另一个太监,还有一股力量是文官,这个是人数最多的。那么他祖父权利很大的,然后收了一个义子也就是曹操的父亲,然后他本身属于夏侯家族,所以他带的资源是曹家还有夏侯家非常有实力。

他并没有说直接躺平,而是想着有所作为,接下来我们再看看他的做事方面

做事手段

1、许劭风评

古代有个一个规则,靠着这些有能力、有品德的人来进行推荐人才,曹操想出来做事,他找到许劭,一开始是不肯的,因为前面讲过三股力量,文官是很鄙视太监的,后面曹操使了点手段最终让许劭给他做了风评,然后他听完大笑而去。

idea:从这件事看做什么事都是有个窍门,这个方式是别人建议曹操这么干,所以做事要恰到好处。另外里面提到曹操使了点手段,哈哈透出了一个狠,有点东西。

2、傍大腿

曹操曾经在袁绍下面干活,然后好几次都把自己的精锐干没了,袁绍作为盟主,慷慨的给予兵马才得以恢复元气。

idea:我们看曹操的出身,这么牛逼的背景,他也需要大腿的支持,更何况普普通通的我们。

3、挟天子以令诸侯

这个是非常著名的历史典故,也是因为这个跟袁绍闹掰了,当汉献帝去了洛阳的时候,他马上去迎接,然后用这个发号施令讨伐别人。

idea:曹操的眼光十分毒辣,他看出潜在的价值,不愧是曹老板。

4、善用人才

像官渡之战,像迎接汉献帝,都是底下这批谋士给的主意,曹操手下文官是人才济济的,另外这个老板是善于听从这些好的计谋,这是非常重要的。

官渡之战,袁绍没有听从谋士的重兵把守粮草,导致给了曹操抓住了机会,乌巢一把火烧光了粮草。

个人看法

a、平台是重要的,借力也是需要的

从曹操的发迹来看,他站在一个大平台上面,不像刘备四处投奔。人并不是说能力很强就能表现出来,需要有平台,有这么伯乐去发现你,然后有这么一股力量在你困难的时候拉你一把,这是重要的。

b、曹操做事狠

这里的狠,不是残暴,而是毒辣,眼光毒辣、做事方式到位,我们从善用人才,许劭风评,挟天子以令诸侯,这些做的都很到位。举个例子,比如说我们要煮开一壶水,需要火柴、木头、可能需要鼓风工具,这都是关键那些点。

这个我们前面也提到了,做事一定要有所研究,事情的关键点是什么,当然有这么一群得力助手也很重要,发现关键突破点。所以古代对英雄标准是:腹有良策,有大气概。

c、驾驭人

司马家起来是在曹操去世后几代的事情,可以说在曹操在的时候,这些有心机的人没有动作的,侧面看出曹操的厉害之处,懂人心。在资治通鉴里面也有一个例子,就是桓温,他也是古代一个权臣,后面几代就不行了压不住这批人。

学历史,学读懂人心


历史里面基本都是那个朝代的精英,他们的事迹,做事方法,当然我们看到很多东西,包括抱负、无奈、遗憾;我们学的不仅仅是做事方法,避开权谋的陷阱,还有就是学习读懂人心、人性。当我们谈到这个,大家第一印象就是坏的人性,其实它是一种自然的表现,就像饿了就要吃饭。

《百家讲坛》里面讲了这么一个故事,曹操的下邳之战生擒了吕布,原本曹操很爱惜人才的,后面刘备的一句话:吕布对以往老板不好,而曹操生性多疑,最终嘎了吕布。王立群老师:人们往往看重结果,以结果说话,而不是问你这么做的原因。

是啊,我们在故事背后,看到整件事情人心的博弈,刘备被人称为仁义之君,但是他在那会落进下石了,因为他之前跟吕布有些矛盾的,吕布把他从原来的根据地赶走了,当然他说的也是事实。所以我们除了学习历史,还需要去洞察人心,往往这些能决定事情的走向。


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

独立开发前100天真正重要的事

我从4月开始离职开始全职做独立开发,算是真正踏进入了这条河流。在过去半年多了我也观察了很多独立开发者。自己目前算是过了新手村(有正常的开发节奏,有3万用户)。看到很多刚起步的独立开发者还是有很多疑问,所以分享一下我在独立开发最初期的一些经验。因为我也不是很成功...
继续阅读 »

我从4月开始离职开始全职做独立开发,算是真正踏进入了这条河流。在过去半年多了我也观察了很多独立开发者。自己目前算是过了新手村(有正常的开发节奏,有3万用户)。看到很多刚起步的独立开发者还是有很多疑问,所以分享一下我在独立开发最初期的一些经验。因为我也不是很成功(没有走的很远),所以只能分享独立开发前100天的经验。

先说一下我认为独立开发起步阶段面临的主要困难:

第一:没有公司的孤独感。如果是一个人全职开发就更寂寞了。即使有一两个合作伙伴,但是大概率也是异地,因此也算是网友性质的社交。人说到底是群居的,所以需要找到一种社交平衡。我想可能这也是很多独立开发白天要在外面地方待着的原因,也许一个人一直在家待着有点闷。

第二:无法建立产品的健康开发节奏。以前在公司的时候自己是流程里的一环,只关心自己分工的完成情况。做了独立开发以后,所有事情都需要自己决策。太多自由的结果就是没了方向。什么都想做,好像什么都可以做,又感觉什么都不好做。

第三:没有收入。产品从开始建造到有足够健康收入中间有一段过程。这还有一个前提要是一个真正有用户价值的产品。如果你起步的时候自己没有一个优势产品方向,又没有个人社区号召力,就算你的产品是好的,也需要一段时间(可长可短)才能获得有效收入。一上线就火的概率太小了。高强度投入一件事情,如果长期没有收入,家人会有很多质疑,可能最后自己也很怀疑自己。

我把这三点结合起来,编一个故事大家可能比较有画面了:

一个人做独立开发已经半年多了,产品设计都是自己做,也没有什么人可以讨论,不知道下一步该做什么。目前每天只有零星的新增。做了这么久,总共只有两三千的收入。看来做产品还得会营销**,打算最近开始学习一下运营**。最近也打算做一下AI产品,感觉这个赛道很火。老婆说如果不行就早点回去上班好了,总不能一直这样。家人们,你们说我应该坚持吗。

家人们会说你的产品很棒,会说你做的比他们强,会说下次一定,会说你未来会成功。但是家人们不会为你掏一分钱。

也许我们不知道如何成功,但是我们可以知道什么是失败。你知道的失败方式越多,你成功的概率就越大。总的来说,产品的成功就两个要点:有用户价值,能赚钱。注意,这两点是或的关系,不是且的关系。一个产品可以能赚钱,但是没有用。一个产品也可以有用,但是不赚钱。失败就是你做的产品:既没用,又不赚钱

基于前面提到的三个困难,我得出的前100天最重要的事是:找到一个可行的产品迭代方向。和团队经过磨合,互相能有有效、信任的协作。找到一百个种子用户。你越早解决这三个困难,你越快走上轨道

确认产品方向

如果你真的做过产品,你就知道最终正确的产品路径不是通过脑中的某刻灵光乍现得到的。所以不是那种大脑飞速运算解题的方式。这里有两件事情需要确认:大的产品方向,产品的路径。

比如阿里巴巴,马云不是一开始就做的淘宝网。他只是觉得互联网普及以后,电子商务会有需求。最开始做的是黄页,并不是淘宝网。但是他没有在第一个项目失败以后,去做门户网站。产品路径的例子是特斯拉。特斯拉很早就确定了先出高性能的跑车,高性能轿车(model s),有了前面的技术积累以后,最后通过推出平价的轿车赢得市场(model 3)。特斯拉在 model 3 大规模量产前都是亏损的。

所以最重要的是确认产品方向。这个方向要结合自身的情况进行设定,就是我在前面帖子里提到的要是你想做的,能做的。也许想达到的产品方向有很多工作量,这个时候就要有同步的产品路径。比如小米手机的创业,他们一开始就想造手机。但是直接启动手机的制造市场、技术都有很大的困难。于是他们先通过做 MIUI 入局。

这里面首先要有个大的方向判断,对于独立开发来说,我觉得张宁在《创作者》里提到的两个维度的方向挺有意思:大众、小众;高频、低频。这里面两两结合各有什么特点我这里就不展开了,大家可以自行体会。

但是可以明确的是,独立开发者做不了又大众又高频的应用。大众又高频,就不可能小而美。大众又高频,最后赢家除了产品能力,要有运营优势,要有资源优势。独立开发者通常没有运营优势和资源优势。另外一点,如果是小众低频,就一定要高忠诚,高付费转化。可以往大众低频或者小众高频的方向多想想

产品方向选择还有一个建议就是要有秘密。成功的业务后面一定有秘密。秘密也回答了一个问题:如果这个需求真的存在,为什么用户选择了你的产品。

最初级的秘密就是信息差,你知道别人不知道,所以你可以,更早做,可以更低的成本,更高效,有更高的获客率。

更高级的秘密就是大家都能看到,但是大家知道了,但是大家不信(脑中想到了拼多多的砍一刀)。

最高级的秘密就是所有人都知道,但是他们做不到。

总结起来,你应该找到一个你有优势的细分方向。信息优势,洞察优势也是优势。

没有一发即中的银弹,最平凡的方式想很多方向,用最低成本进行最快速的验证。在反馈中渐渐明晰产品路径。如果你三个月不管反馈闷头做,只做出了一个产品方向。你失败的概率是很大的。所以我看到很多产品1.0 的时候就做会员,做社区,做跨平台我是很不理解的。其实这些功能在早期性价比很低。

我的方式是脑海中有10个想法,挑出3个想法做初步设计,选出一个或者两个想法做产品验证。可能是原型,数据是模拟的,没有设计,如果产品真的解决了痛点的话,用户会愿意用,然后他会给你反馈他想要更好的体验,他愿意付钱得到这些改进。这里的效率优势是,你能在更短的时间验证产品方向是不是对的。总比走了3个月才发现是一条死胡同要好。

开发者很容易因为想到一个想法很兴奋,觉得这个很有用,就闷头做了一个月。有可能的问题是,这个想法虽然是个痛点,但是这个痛点频次很低,场景很少,所以虽然有用,但是没人会愿意买单。所以尽量跳出自己的思维,从用户的角度来进行验证是很必要的。

团队协作

独立开发的开发方式和传统公司不同。需要建立一个全新的工作流程。在初期大家都是空白,所以需要通过产品迭代中,形成高效的开发默契。大家松散做东西,工作习惯,工作职责都需要有共识才行。

比如我合作的设计师早期喜欢一次做一大板块的整体设计,大概一周的工作量。初期我觉得我们对产品有激情,大家都应该有自由的发挥空间。但是做了一周的设计图和产品脑海中的产品行进方向不一致怎么办。在工作时间上,我合作的设计师因为目前还是兼职,他只能在下班后设计。然而我全职只在6点前工作。这又是一个要协调的地方。

如果你是一个产品,需要协调研发和设计,三个人协调就又更复杂了。要找到一个大家都舒服,高效的协作方式。

100个种子用户

独立开发最核心的一环就是找到一个健康的商业模式。产品方向和团队协作的目标都是为了未来可以达成一个健康的商业模式。我觉得太多独立开发者上来就把目标(野心)定的太高。一口吃不成胖子。独立开发早期的商业目标只有一个:尽快达成团队最低维持标准。一鸟在手胜过二鸟在林。不要在团队只有几个人的时候用几十个人的方式管理。

初期就要估算出产品(团队)能够持续运转的最低收入。这个成本越低,团队就越容易跑起来。当收入足够覆盖团队的成本后,你的心态就会得到极大的自由,可以尝试很多奇奇怪怪有趣的想法。所以早期不要想有多高的天花板,如何建立壁垒,就关心如何达成产品的及格生命线。谁会想做一个注定失败的产品呢。

早期在没有运营优势的情况下,最重要的指标就是用户满意度了。用户满意度,就暗示了这个产品有没有解决切实的用户问题,用户愿不愿意为你宣传。其实很多人都搞错了重点,在产品没有让100个种子用户满意前,新增的流量是没有意义的。因为再多的用户都会流失。竹篮打水一场空。如果你把产品的用户目标定在100个种子用户,你也就没了运营压力,可以关注在如何打造正确的产品上。在产品基本盘没有问题后,再思考后面的才有意义。

总结

总结起来三点就是:做什么(产品方向),怎么做(团队协作),为谁做(验证用户)。以上就是我全职独立开发3个多月以来肤浅的经验分享,希望对你有帮助。


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

《程序员职场工具库》从行动开始 —— MORS 法则

你是否曾经有过类似的疑惑?我尝试过好几次健身,但都坚持不下来,我是不是一个没有耐心,没有毅力的人?领导反馈说我需要提升沟通能力(开发效率、主动性),可是我要怎么提升呢?我知道该怎么做,但就是做不到呀。在我们的个人成长中,出现这些疑惑是很正常的,它们可以归纳为以...
继续阅读 »

你是否曾经有过类似的疑惑?

  • 我尝试过好几次健身,但都坚持不下来,我是不是一个没有耐心,没有毅力的人?
  • 领导反馈说我需要提升沟通能力(开发效率、主动性),可是我要怎么提升呢?
  • 我知道该怎么做,但就是做不到呀。

在我们的个人成长中,出现这些疑惑是很正常的,它们可以归纳为以下两个原因:

  • 不知道方法
  • 知道方法但不知道该怎样坚持

如果你经常被这两个问题困扰,长此以往,就会陷入自我怀疑的状态。这时,MORS 法则可以帮到你。

MORS 法则,又叫做具体性原则。MORS 法则认为,我们的行为只有符合Measured(可测评)、Observable(可观察)、Reliable(可信赖)、Specific(明确化) 这 4 个要素,才是一个具体的、可执行的行为。

针对上面说到的问题,我们不要光喊口号,而需要给出具体的、可执行的方案,然后行动起来!

也就是说,当你碰到一个很复杂的目标时,要尽量拆解目标,制定一系列符合 MORS 法则的行动计划,然后开始行动,这样就能达成目标

让我用一个栗子来解释 MORS 法则,假想一下,你今年给自己立了一个 flag:“今年要跑半马(21公里)”。要是只有这样一个目标,你能做到吗?大概率是不能的,即使你知道要每天坚持跑步,也很难坚持下来。你有可能会被临时的一些事情耽搁了几天就放弃了;你有可能在坚持了一段时间之后发现自己“根本做不到”就放弃了。

想要达成这个大目标,你可以尝试先拆解目标。假设第一次开始跑步,你可以跑 1 公里,希望 10 个月后能跑 21 公里,那就是每个月要多跑 2 公里;然后再拆到每周要多跑 0.5 公里;然后再拆解到每 2 天多跑 0.07 公里左右。这样分拆之后,就可以罗列出每天的跑步行动计划,比如:xx月xx日,跑 1.14 公里;xx月xx+1日,跑 1.21 公里... 这些行动是符合 MORS 法则的:

  • Measured:这些行动是可以被测量和评估的,你可以明确知道今天计划跑多少公里,最终有没有完成这个行动也可以很清楚地衡量和评估。
  • Observable:这些行动是可见的,你要跑起来,可能需要一些跑步装备和资源(比如准备水),这些都是实实在在的行动,不是 YY。
  • Reliable:这些行动是可以完成的,它不会太难,每天多跑 0.07 公里远远比多跑 20 公里要简单太多了,肯定能做到吧?
  • Specific:这些行动是非常明确的,就是跑步,不会有什么歧义。

当然,肯定有更好的、更加符合运动理论的跑步计划,比如是不是每天跑,还是一周休息 2 天、是应该每天突破,还是隔一天突破等等。这里只是举例,就简单一点了。不过不管是什么跑步计划,原理是不变的。

当你制定了符合 MORS 法则的行动计划之后,就能够更加容易地达成目标,也更加容易坚持下来了。因为:

  • 行动计划是具体的,明确的,可行的。相比原来的假大空的目标,它更容易做到!而且,按照这个行动计划来行动,就能达成目标。
  • 能够不断地感受到成果,这会帮助你更容易坚持下来。在这个栗子中,就是每天都多跑了一些,感受到自己每天都在进步,这是强大的正反馈,会给予你力量。

注意不要跟 SMART 原则搞混了哈。SMART 原则是制定目标时使用的,MORS 法则是制定行动计划的时候使用的。它们有一些相同的点,比如 Measured 和 Specific,那是因为每一个行动本身也是带有目的性的,所以 MORS 法则和 SMART 原则会有重叠的地方。

好了,MORS 法则的介绍就到这里了,它的内容非常简单,但是 MORS 法则对于我们的个人成长来说,作用是非常大的,希望你们可以多加实践啦。

除了在个人成长上面有帮助之外,MORS 法则对于管理者指导下属也非常有用,后面有机会我再介绍吧。

加油,让我们从行动开始!


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

我的日常开发收获

passive event listenerspassive event listeners 是一种新兴的web标准,Chrome 51中提供的新功能为滚动性能提供了巨大的潜在提升。Chrome Release Notes.背景:所有的现代浏览器都有一个滚动功...
继续阅读 »
  1. passive event listeners

passive event listeners 是一种新兴的web标准,Chrome 51中提供的新功能为滚动性能提供了巨大的潜在提升。Chrome Release Notes.

背景:所有的现代浏览器都有一个滚动功能的线程,可以保证即使在运行耗时的js代码时滚动也能够平滑进行,但这种优化部分因需要等待任何touchstart 和 touchmove处理程序的结果而失败,因为这些交互可能会通过调用preventDefault() 事件来完全阻止滚动。

于是有了 {passive: true}

通过将touchwheel事件监听标记为 passive,开发人员承诺处理程序不会调用 preventDefault来禁用滚动。这使浏览器可以立即响应滚动,而无需等待js的执行,从而确保为用户提供可靠流畅的滚动体验。

  1. 关于系统设计

    • 关于分层

分层一般是基于模块功能来分层,有时候分层不清晰可能是有哪些模块,各模块间的功能,整个功能流程不是很清楚。

有时候两个模块间的交互复杂度增加,可以考虑构建一个中间层。 这样可以保持两个模块不会杂糅相关度不是很高的处理逻辑,功能逻辑更纯粹,保持边界清晰,降低模块本身的复杂度。

  1. 关于编程思维

仔细想来,虽然从事开发工作很久了,但是编程上还是很没有章法,架构设计能力较弱,多年来都是凭借以前热血和几分小聪明存活。

今日份反思:

拿到一个需求,分析该需求需要支持那些场景,为了支持这些场景它需要具备哪些功能,思考怎样实现这些功能,根据功能做模块划分,对这些模块进行分析,做逻辑抽象(也就是分层),然后整个需求实现的大致框架就心中有数了,开始产出技术方案。 技术方案产出后,按照技术方案的设想去实施,实施过程中可能会遇到没考虑到的场景,或者发现之前的设计不能很好的cover,调整设计,增加分层或者调整已有的分层,然后修改技术方案。 不断的经历上述过程,会慢慢的沉淀出一些业务通用的设计思路,这样下次再做技术方案的时候就不会很迷茫。脑子理清楚,而后出设计,实践后总结。

  1. ShadowRealm API

一个进入 statge3 的新的 JavaScript 提案,用于创建一个独立的JavaScript运行环境,里面有独立的变量作用域。

数据结构:

declare class ShadowRealm {
  constructor();
// 同步执行字符串,类似eval()
  evaluate(sourceText: string): PrimitiveValueOrCallable;
// 返回一个Promise对象,异步执行代码字符串
  importValue(specifier: string, bindingName: string): Promise<PrimitiveValueOrCallable>;
}

使用场景:

  • 在Web IDE 或 Web绘图应用程序中运行插件等第三方代码; 这种方式比iframe的实现更简单、灵活度更高,占用内存更少、代码的安全性更高。
  • 用 ShadowRealms 中创建编程环境,运行用户代码,如codepen,codesandbox;
  • 服务器可以在 ShadowRealms 运行第三方代码,防止第三方代码出错打挂主环境;
  • 网页抓取和网页应用测试可以在ShadowRealms中运行;

补充:Node.jsvm模块与ShadowRealm API类似,但具有更多功能,缓存Javascript 引擎,拦截import() 等等。

  1. 关于状态管理

做状态管理的核心就是监听数据的变化,监听数据的变化有两种方式:

  • 提供api来修改,内部做联动处理(React的setState)
  • 对对象做一层代理,set的时候做联动处理,同时get时收集所有依赖。(vue,mobx的响应式数据)
  1. 需求/调研

关于需求调研,我还是很急躁,急急忙忙开始技术方案评审、开发、排期的话,就会导致整个开发过程很被动。

比较好的方式,前期对于自己做的需求,以及需求的各个功能依赖有比较充分的了解(不过这在很多功能依赖方都没有文档的情况下很难做到),然后写一个符合需求的简版的demo,对可能出现的阻塞点和解决方案心里有一个预期,把图纸画好,照着图纸开发,从而更好的掌控整个开发进度。

  1. 功能/需求拆解

对功能需求的有效拆解,功能模块详细具体,粒度合适,不会太过细化,也不会忽略一些关键点,才能够更好的把控整个开发进度,评估可能出现的风险点。

  1. 工具方法收敛思路

收敛-内部集中分发给各个具体的util处理,保证对外暴露的接口的统一,降低该方法使用的心智负担。

  1. 关于「自顶向下」和「自底向上」

自顶向下:

程序设计时,先考虑整体,后考虑细节。先考虑全局目标,后考虑局部目标。不要一开始就过多追求总多的细节,先从最上层总目标开始设计,逐步使问题具体化。

模块化设计:一个复杂的问题,肯定是有若干稍简单的问题构成。模块化是把程序要解决的总目标拆解为子目标,再进一步细化分解为具体的小目标,把每一个小目标称为一个模块。

自底向上:

自底向上的设计简单来说就是先完成细节功能,每个细节功能抽象成一个运算符,然后将这些完成的细节功能组装到整体的架构中。

自动化的设计是不是就应该采用自底向上的设计思路,把每个需要的细节功能做抽象,使得配置规则的人可以任意组装,对于不支持的功能制造新的抽象?

  1. eval 和 new Function的区别

eval 和 new Function都可以解析执行一段传入的字符串。但有以下不同的地方:

  • eval中的代码是当前作用域,它可以访问当前函数中的局部变量和全局变量。new Function中的代码执行的作用域是全局作用域,不论它在哪个地方被调用,可访问的都是全局变量;
  • eval接收函数作为字符串时需要“(”和“)”作为前缀和后缀,new Function不需要,new Function可以接收N个参数,最后一个参数作为函数体;
  • eval不容易调试,用chromeDev等调试工具无法打断点调试;
  • 性能问题,eval通常比其他替代方法更慢,因为他必须调用js解释器,而其他结构则可被现代js引擎优化;
  • eval存在安全问题,因为可访问局部作用域的变量,其内部逻辑不可预测性很强,可能导致XSS攻击;
  1. 乐观更新与保守更新
  • 乐观更新(Optimistic Update): 乐观更新:如果有编辑等改动,先更新前端页面,再像服务端发送请求,如果请求成功则结束操作,无需额外处理,若请求失败,则页面回滚到先前状态; 这样更新方式的优点是响应及时,缺点就是低概率的请求失败回滚的体验不太好。
  • 保守更新(Perssimistic Update): 保守更新:如果有编辑等改动,向服务端发送请求,等收到回复请求后再响应用户操作,在此之前用户都需要处于等待状态。 这样做的缺点是会使页面有比较大的延时感,优点是最终呈现的结果是可信赖、稳定可靠的。
  1. 正交的概念

编程上的正交,从数学上引进这个词,用于表示相互独立,相互间不可替代,并且可以组合起来实现其他功能。比如if和for语言是正交的,但for和while与句的功能是有重叠的。逻辑运算not、and也是正交的,其他复杂的逻辑运算都可以用这三种基本运算叠加起来。 编程语言经常定义一组正交语法特性,相互间不可替代,组合起来可以实现其他功能。为了更方便使用,在基础特性之上,再添加一些额外特性。这些非基本的额外特性,成为语法糖。语法糖对语言的功能没有太大影响,只是有了,代码写起来更方便些。

  1. 引入外部字体,因等待字体文件的加载而产生文字不可见问题的一些解决方案
  • 临时显示系统字体:添加font-display: swap;到自定义字体的style中,在自定义字体加载好之前显示系统字体;
  • 预加载网页字体:用<link rel="preload" as="font" >更早的获取字体文件。
  1. 总结写技术文章的几个步骤,引导自己学习并践行:
  • 学习优秀的人写的东西,看看他们的理解;
  • 带着自己的疑问,和目前接收到的理解去读源码;
  • 读完之后,按照自己最终的理解绘制相关逻辑的流程图;
  • 针对关键功能模块做拆解和源码解读;
  • 总结相关功能的实现机制,以及给我带来的启发和思考;
  1. 关于团队中被高频讨论的去底座;

去底座不是完全失去对底座(数据)的访问能力,而是设计一个标准化的API来支持按需访问底座(数据)的能力。


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

用小明的故事随便谈谈kotlin中的apply等函数

前言本文仅简单描述一下kotlin中常用到的scope function,如apply,let,run,with,also等函数的常用方法和选取。即使很多情况下选择不同函数,也同样都能达到最终效果,具体选择哪个函数我们不会严格约束,但如果你是对代码规范要求比较...
继续阅读 »

前言

本文仅简单描述一下kotlin中常用到的scope function,如apply,let,run,with,also等函数的常用方法和选取。即使很多情况下选择不同函数,也同样都能达到最终效果,具体选择哪个函数我们不会严格约束,但如果你是对代码规范要求比较高的,最好建立良好的代码习惯。

一般对比

函数一般使用场景函数定义上下文对象可用作返回值
apply在需要对对象进行初始化或配置的时候使用public inline fun <T> T.apply(block: T.() -> Unit): T接收器this返回值是对象本身
also在需要对对象执行额外操作并返回原对象的时候使用public inline fun <T> T.also(block: (T) -> Unit): T变量it返回值是对象本身
let在需要对对象进行非空判断并执行特定操作的时候使用public inline fun <T, R> T.let(block: (T) -> R): R变量it返回值是 lambda 结果
run在需要对对象进行多个操作,并返回一个结果的时候使用,通常是一个新的对象或其他public inline fun <T, R> T.run(block: () -> R): R 接收器this返回值是 lambda 结果
with在不拥有对象的上下文的时候使用public inline fun <T, R> with(receiver: T, block: T.() -> R): R接收器this返回值是 lambda 结果
  1. apply 函数接收一个 lambda 表达式作为参数,并返回被调用对象本身。通过 apply,可以在对象创建后立即对其进行链式操作,设置属性值、调用方法等。适合用于链式初始化或配置一些属性。
val person = Person().apply { 
name = "John"
age = 30
}
  1. also 函数接收一个 lambda 表达式作为参数,lambda 表达式中的 it 引用指向调用 also 的对象。通过 also,可以对对象进行额外的操作,而原对象仍然是函数调用的结果。适合用于在对象操作过程中执行额外的副作用操作。
val modifiedObject = myObject.also {
// 额外操作 it
}
  1. let 函数接收一个 lambda 表达式作为参数,lambda 表达式中的 it 引用指向调用 let 的对象。如果对象不为空,则执行 lambda 表达式内的操作,并返回 lambda 表达式的结果。适合用于安全地操作对象,避免空指针异常。
val result = nullableValue?.let {
// 操作非空对象 it
}
  1. run 函数接收一个 lambda 表达式作为参数,lambda 表达式中的 this 引用指向调用 run 的对象。通过 run,可以便捷地对对象进行多次操作,并返回最后一个表达式的结果。适合用于执行一系列操作并返回最终结果。
val result = myObject.run {
// 对象操作1
// 对象操作2
// ...
// 返回结果
}

  1. with 函数接收一个对象和一个 lambda 表达式作为参数,lambda 表达式中的 this 引用指向传入的对象。通过 with,可以在没有对象接收者的情况下操作对象,并返回最后一个表达式的结果。适合用于对对象进行一系列操作,而无需在乎返回值。
val result = with(myObject) {
// 对象操作1
// 对象操作2
// ...
// 返回结果
}

小明的故事

故事是这样的

  1. 小明今年上一年级
  2. 但是家长跟学校说,小明是个天才,现在可以直接跳级到二年级
  3. 学校给二年级分配的老师是王老师,是个女教师
  4. 半学期后,王老师怀孕了需要休息,于是学校给王老师放假
  5. 学校给二年级分配了新的李老师,小明有了新老师

下面是故事的代码:


data class Student(var name: String = "", var grade: String = "", var teacher: Teacher? = null) {
//插班跳级
fun needSkippingGrade(insertGrade: String) {
this.grade = insertGrade
}
}

data class Teacher(var name: String = "") {
fun relax() {
println("$name 休假了!")
}
}

fun main() {

//1. **小明**今年上一年级
val xiaoming = Student()
.apply {
name = "小明"
grade = "一年级"
println("小明开始前: $this")
}
.also {
//2.现在可以直接跳级到二年级
it.needSkippingGrade("二年级")
println("小明插班后: $it")
}

//3. 学校给二年级分配的老师是**王老师**,是个女教师
val ownTeacher = xiaoming.teacher?.let {
println("小明当前的老师不为NULL,是${it}")
} ?: Teacher("王老师").also {
xiaoming.teacher = it
println("小明有了老师: $xiaoming")
}

fun changeStudentCurrentTeacher(student: Student): Teacher? {
return student.run {
teacher?.relax()
Teacher("李老师")
}
}

//4. 半学期后,王老师怀孕了需要休息,于是学校给王老师放假
//5. 学校给二年级分配了新的**李老师**,小明有了新老师
with(xiaoming) {
println("开学了!")
println("半学期后,王老师怀孕了!...")
val newTeacher = changeStudentCurrentTeacher(this)
println("新老师是$newTeacher")
teacher = newTeacher
println("小明有了新老师$this")
}

}

输出结果:

小明开始前: Student(name=小明, grade=一年级, teacher=null)
小明插班后: Student(name=小明, grade=二年级, teacher=null)
小明有了老师: Student(name=小明, grade=二年级, teacher=Teacher(name=王老师))
开学了!
半学期后,王老师怀孕了!...
王老师 休假了!
新老师是Teacher(name=李老师)
小明有了新老师Student(name=小明, grade=二年级, teacher=Teacher(name=李老师))

最后

实际过程中,需要根据具体的场景和需求来选择适合的函数。前面这些函数在 Kotlin 中提供了更简洁、可读性更高的方式来处理对象,根据不同的使用场景,你可以选择最适合和更易读的函数来操作对象


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

当面试官问你BroadcastReciver的静态注册与动态注册的区别,你又该作何应对?

什么是广播,简单点广播就是安卓系统本身发出的声音,我们可以通过安卓提供给我们的一系列内容来接收和发出广播,以此来简单快捷地实现一些功能。在实际开发中也常常用到,而是否熟悉使用,这成为面试官最常问的问题。当面试官问你:1.请问BroadcastReciver的静...
继续阅读 »

什么是广播,简单点广播就是安卓系统本身发出的声音,我们可以通过安卓提供给我们的一系列内容来接收和发出广播,以此来简单快捷地实现一些功能。

在实际开发中也常常用到,而是否熟悉使用,这成为面试官最常问的问题。

当面试官问你:

1.请问BroadcastReciver的静态注册与动态注册的区别?,你在开发中用过吗?

答:

其实广播分为两种基本类型:

在一个程序中,可以发送广播供当前程序的广播接收器收到。首先我们来看下两种方式的发送广播。 在Android系统中,主要有两种基本的广播类型: - 标准广播(Normal Broadcasts) - 有序广播(Ordered Broadcasts)

标准广播:

是一种完全异步执行的广播,在广播发出之后,所有的广播接收器会在同一时刻接收到这条广播,广播无法被中断。

发送广播的方式十分容易的,只需要实例化一个Intent对象,然后调用context的** sendBroadcast() **方法。这样就完成了广播的发送。

        //intent中的参数为action
       Intent intent=new Intent("com.example.dimple.BROADCAST_TEST");
       sendBroadcast(intent);

有序广播:

是一种同步执行的广播,在广播发出之后,优先级高的广播接收器会先接收到这条广播,并可以在优先级较低的广播接收器之前终止发送这条广播。

        //intent中的参数为action
       Intent intent=new Intent("com.example.dimple.BROADCAST_TEST");
       sendOrderBroadcast(intent,null);//第二个参数是与权限相关的字符串。

到此时,如果你的程序中只有一个广播接收器的话,是体现不出有序广播的特点的, 右击包名——New——Other——BroadcastReceiver多创建几个广播接收器。

此时你还是会发现,所有的广播接收器是同时接收到广播消息的。注意上面介绍的时候说到优先级,这个时候我们需要设置优先级,在AndroidManifest文件中的Receiver标签中设置广播接收器的优先级。

        <receiver
           android:name=".MyReceiver"
           android:enabled="true"
           android:exported="true">
           <!--注意此时有一个Priority属性-->
           <intent-filter android:priority="100">
               <action android:name="android.intent.action.BROADCAST_TEST"></action>
           </intent-filter>
       </receiver>

优先级越高的广播接收器优先收到广播,也可以在收到广播的时候调用abortBroadcast() 方法截断广播。优先级低的广播接收器就无法接收到广播了。

面试官,假设我有一个接收者如下:

在Android的广播接收机制中,如果接收到广播,就需要创建广播接收器。而创建广播接收器的方法就是新建一个类(可以是单独新建类,也可以是内部类(public)) 继承自BroadcastReceiver

   class myBroadcastReceiver extends BroadcastReceiver{

       @Override
       public void onReceive(Context context, Intent intent) {
           //接收到广播的处理,注意不能有耗时操作,当此方法长时间未结束,会报错。
           //同时,广播接收器中不能开线程。
      }
  }

面试官,接下来就是面临注册的问题了,有两种注册方式,一种是动态注册,一种是静态注册

所谓动态注册是指在代码中注册。步骤如下 :

  • 实例化自定义的广播接收器。
  • 创建IntentFilter实例。
  • 调用IntentFilter实例的addAction()方法添加监听的广播类型。
  • 最后调用Context的registerReceiver(BroadcastReceiver,IntentFilter)动态的注册广播。

这个时候,已经为我们自定义的广播接收器关联了广播,当收到和绑定的广播一直的广播的时候,就会调用广播接收器中的onReceiver方法。

        MyBroadcastReceiver myBroadcastReceiver=new MyBroadcastReceiver();
       IntentFilter intentFilter=new IntentFilter();
       intentFilter.addAction("com.example.dimple.MY_BROADCAST");
       registerReceiver(myBroadcastReceiver,intentFilter);

这里需要注意的是,如果需要接收系统的广播(比如电量变化,网络变化等等),别忘记在AndroidManifest配置文件中加上权限。另外,动态注册的广播在活动结束的时候需要取消注册:

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

静态注册:

在创建好的广播接收器中添加一个Toast提示。代码如下:

public class MyReceiver extends BroadcastReceiver {
   @Override
   public void onReceive(Context context, Intent intent) {
       Toast.makeText(context,"开机启动!",Toast.LENGTH_LONG).show();
  }  
}

然后在AndroidManifest文件中添加:

  • 权限 <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"></uses-permission>

  • Intent-filter

            <receiver
               android:name=".MyReceiver"
               android:enabled="true"
               android:exported="true">
               <!--添加以下3行-->
               <intent-filter>
                   <action android:name="android.intent.action.BOOT_COMPLETED"></action>
               </intent-filter>
           </receiver>

    此时重启Android系统就可以收到开机提示了。

总结:

动态注册静态注册的不同:

动态注册的广播接收器可以自由的实现注册和取消,有很大的灵活性。但是只有在程序启动之后才能收到广播,此外,不知道你注意到了没,广播接收器的注销是在onDestroy()方法中的。所以广播接收器的生命周期是和当前Activity的生命周期一样。

静态注册的广播不受程序是否启动的约束,当应用程序关闭之后,还是可以接收到广播。

标准广播和有序广播的接收和发送都是全局性的,这样会使得其他程序有几率接收到广播,会造成一定的安全问题。为了解决这个问题,Android系统中有一套本地广播的机制。这个机制是让所有的广播事件(接收与发送)都在程序内部完成。主要是采用的一个localBroadcastReceiver对广播进行管理。


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

终于搞明白了什么是同步屏障

背景今天突然听到隔壁在讨论同步屏障,听到这个名字,我依稀记得 Handler 里面是有同步屏障机制的,但是具体的原理怎么有点模糊不清呢?就像一个明星,你明明看着面熟,就是想不起来他叫啥,让我这样的强迫症患者无比难受,所以抽时间来扒一扒同步屏...
继续阅读 »

背景

今天突然听到隔壁在讨论同步屏障,听到这个名字,我依稀记得 Handler 里面是有同步屏障机制的,但是具体的原理怎么有点模糊不清呢?就像一个明星,你明明看着面熟,就是想不起来他叫啥,让我这样的强迫症患者无比难受,所以抽时间来扒一扒同步屏障。

同步屏障机制

1. 直奔主题,同步屏障机制这几个字听起来很牛逼,能浅显的解释一下,先让大家明白它的作用是啥不?

同步屏障实际上就是字面意思,可以理解为建立一道屏障,隔离同步消息,优先处理消息队列中的异步消息进行处理,所以才叫同步屏障。

2. 第二个问题,同步消息又是啥呢?异步消息和同步消息有啥不一样呢?

要回答这个问题,我们就得了解一下 MessageMessage 的消息种类分为三种:

  • 普通消息(同步消息)
  • 异步消息
  • 同步屏障消息

我们平时使用 Handler 发送的消息基本都是普通消息,中规中矩的排到消息队列中,轮到它了再乖乖地出来执行。

考虑一个场景,我现在往 UI 线程发送了一个消息,想要绘制一个关键的 View,但是现在 UI 线程的消息队列里面消息已经爆满了,我的这条消息迟迟都没有办法得到处理,导致这个关键 View 绘制不出来,用户使用的时候很恼怒,一气之下给出差评这是什么垃圾 app,卡的要死。

此时,同步屏障就派上用场了。如果消息队列里面存在了同步屏障消息,那么它就会优先寻找我们想要先处理的消息,把它从队列里面取出来,可以理解为加急处理。那同步屏障机制怎么知道我们想优先处理的是哪条消息呢?如果一条消息如果是异步消息,那同步屏障机制就会优先对它处理。

3.那要如何设置异步消息呢?怎样的消息才算一条异步消息呢?

Message 已经提供了现成的标记位 isAsynchronous 用来标志这条消息是不是异步消息。

4.能看看源码了解下官方到底怎么实现的吗?

看看怎么往消息队列 MessageQueue 中插入同步屏障消息吧。

private int postSyncBarrier(long when) {
synchronized (this) {
final int token = mNextBarrierToken++;
final Message msg = Message.obtain();
msg.markInUse();
msg.when = when;
msg.arg1 = token;

Message prev = null;
// 当前消息队列
Message p = mMessages;
if (when != 0) {
// 根据when找到同步屏障消息插入的位置
while (p != null && p.when <= when) {
prev = p;
p = p.next;
}
}
// 插入同步屏障消息
if (prev != null) {
msg.next = p;
prev.next = msg;
} else {
msg.next = p;
// 前面没有消息的话,同步屏障消息变成队首了
mMessages = msg;
}
return token;
}
}

在代码关键位置我都做了注释,简单来说呢,其实就像是遍历一个链表,根据 when 来找到同步屏障消息应该插入的位置。

5.同步屏障消息好像只设置了when,没有target呢?

这个问题发现了华点,熟悉 Handler 的朋友都知道,插入消息到消息队列的时候,系统会判断当前的消息有没有 targettarget 的作用就是标记了这个消息最终要由哪个 Handler 进行处理,没有 target 会抛异常。

boolean enqueueMessage(Message msg, long when) {
// target不能为空
if (msg.target == null) {
throw new IllegalArgumentException("Message must have a target.");
}
...
}

问题 4 的源码分析中,同步屏障消息没有设置过 target,所以它肯定不是通过 enqueueMessage() 添加到消息队列里面的啦。很明显就是通过 postSyncBarrier() 方法,把一个没有 target 的消息插入到消息队列里面的。

6.上面我都明白了,下面该说说同步屏障到底是怎么优先处理异步消息的吧?

OK,插入了同步屏障消息之后,消息队列也还是正常出队的,显然在队列获取下一个消息的时候,可能对同步屏障消息有什么特殊的判断逻辑。看看 MessageQueue 的 next 方法:

Message next() {
...
// msg.target == null,很明显是一个同步屏障消息
if (msg != null && msg.target == null) {
// Stalled by a barrier. Find the next asynchronous message in the queue.
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
...
}

方法代码很长,看源码最主要还是看关键逻辑,也没必要一行一行的啃源码。这个方法中相信你一眼就发现了msg.target == null,前面刚说过同步屏障消息的 target 就是空的,很显然这里就是对同步屏障消息的特殊处理逻辑。用了一个 do...while 循环,消息如果不是异步的,就遍历下一个消息,直到找到异步消息,也就是 msg.isAsynchronous() == true

7.原来如此,那如果消息队列中没有异步消息咋办?

如果队列中没有异步消息,就会休眠等待被唤醒。所以 postSyncBarrier() 和 removeSyncBarrier() 必须成对出现,否则会导致消息队列中的同步消息不会被执行,出现假死情况。

8.系统的 postSyncBarrier() 貌似也没提供给外部访问啊?这我们要怎么使用?

确实我们没办法直接访问 postSyncBarrier() 方法创建同步屏障消息。你可能会想到不让访问我就反射调用呗,也不是不可以。

但我们也可以另辟蹊径,虽然没办法创建同步屏障消息,但是我们可以创建异步消息啊!只要系统创建了同步屏障消息,不就能找到我们自己创建的异步消息啦。

系统提供了两个方法创建异步 Handler

public static Handler createAsync(@NonNull Looper looper) {
if (looper == null) throw new NullPointerException("looper must not be null");
// 这个true就是代表是异步的
return new Handler(looper, null, true);
}

public static Handler createAsync(@NonNull Looper looper, @NonNull Callback callback) {
if (looper == null) throw new NullPointerException("looper must not be null");
if (callback == null) throw new NullPointerException("callback must not be null");
return new Handler(looper, callback, true);
}

异步 Handler 发送的就是异步消息。

9.那系统什么时候会去添加同步屏障呢?

有对 View 的工作流程比较了解的朋友想必已经知道了,在 ViewRootImpl 的 requestLayout 方法中,系统就会添加一个同步屏障。

不了解也没关系,这里我简单说一下。

(1)创建 DecorView

当我们启动了 Activity 后,系统最终会执行到 ActivityThread 的 handleLaunchActivity 方法中:

final Activity a = performLaunchActivity(r, customIntent);

这里我们只截取了重要的一行代码,在 performLaunchActivity 中执行的就是 Activity 的创建逻辑,因此也会进行 DecorView 的创建,此时的 DecorView 只是进行了初始化,添加了布局文件,对用户来说,依然是不可见的。

(2)加载 DecorView 到 Window

onCreate 结束后,我们来看下 onResume 对应的 handleResumeActivity 方法:

@Override
public void handleResumeActivity(ActivityClientRecord r, boolean finalStateRequest,
boolean isForward, String reason) {
...
// 1.performResumeActivity 回调用 Activity 的 onResume
if (!performResumeActivity(r, finalStateRequest, reason)) {
return;
}
...
final Activity a = r.activity;
...
if (r.window == null && !a.mFinished && willBeVisible) {
r.window = r.activity.getWindow();
// 2.获取 decorview
View decor = r.window.getDecorView();
// 3.decor 现在还不可见
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode |= forwardBit;
...
if (a.mVisibleFromClient) {
if (!a.mWindowAdded) {
a.mWindowAdded = true;
// 4.decor 添加到 WindowManger中
wm.addView(decor, l);
} else {
a.onWindowAttributesChanged(l);
}
}
}
...
}

注释 4 处,DecorView 会通过 WindowManager 执行了 addView() 方法后加载到 Window 中,而该方法实际上是会最终调用到 WindowManagerGlobal 的 addView() 中。

(3)创建 ViewRootImpl 对象,调用 setView() 方法

// WindowManagerGlobal.ddView()
root = new ViewRootImpl(view.getContext(), display);
root.setView(view, wparams, panelParentView);

WindowManagerGlobal 的 addView() 会先创建一个 ViewRootImpl 实例,然后将 DecorView 作为参数传给 ViewRootImpl,通过 setView() 方法进行 View 的处理。setView() 的内部主要就是通过 requestLayout 方法来请求开始测量、布局和绘制流程

(4)requestLayout() 和 scheduleTraversals()

@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
// 主要方法
scheduleTraversals();
}
}

void scheduleTraversals() {
if (!mTraversalScheduled) {
// 1.将mTraversalScheduled标记为true,表示View的测量、布局和绘制过程已经被请求。
mTraversalScheduled = true;
// 2.往主线程发送一个同步屏障消息
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
// 3.注册回调,当监听到VSYNC信号到达时,执行该异步消息
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}

看到了吧,注释 2 的代码熟悉的很,系统调用了 postSyncBarrier() 来创建同步屏障了。那注释 3 是啥意思呢?mChoreographer 是一个 Choreographer 对象。

要理解 Choreographer 的话,还要明白 VSYNC

我们的手机屏幕刷新频率是 1s 内屏幕刷新的次数,比如 60Hz、120Hz 等。60Hz表示屏幕在一秒内刷新 60 次,也就是每隔 16.6ms 刷新一次。屏幕会在每次刷新的时候发出一个 VSYNC 信号,通知CPU进行绘制计算,每收到 VSYNC,CPU 就开始处理各帧数据。这时 Choreographer 就上场啦,当有 VSYNC 信号到来时,会唤醒 Choreographer,触发指定的工作。它提供了一个回调功能,让业务知道 VSYNC 信号来了,可以进行下一帧的绘制了,也就是注释 3 使用的 postCallback 方法。

当监听到 VSYNC 信号后,会回调来执行 mTraversalRunnable 这个 Runnable 对象。

final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}

void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
// 移除同步屏障
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
// View的绘制入口方法
performTraversals();

if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}

在这个 Runnable 里面,会移除同步屏障。然后调用 performTraversals 这个View 的工作流程的入口方法完成对 View 的绘制。

这回明白了吧,系统会在调用 requestLayout() 的时候创建同步屏障,等到下一个 VSYNC 信号到来时才会执行相应的绘制任务并移除同步屏障。所以在等待 VSYNC 信号到来的期间,就可以执行我们自己的异步消息了。


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

该怎么放弃你,我的内卷

各位,两个月没写文章了,这两个月发生了很多事,也让我产生了很多不一样的感悟。从上次发完《阅阿里大裁员有感》,我的手机里就推了越来越多的“裁员”、“经济下行”、“焦虑”等信息。我这边现在这家公司,虽然不裁员,但是执行了“SABC”绩效分布考核,什么内容大家应该也...
继续阅读 »

各位,两个月没写文章了,这两个月发生了很多事,也让我产生了很多不一样的感悟。从上次发完《阅阿里大裁员有感》,我的手机里就推了越来越多的“裁员”、“经济下行”、“焦虑”等信息。我这边现在这家公司,虽然不裁员,但是执行了“SABC”绩效分布考核,什么内容大家应该也都清楚,最后给我打了个 B-。呵呵,扣工资 20%,变成所谓的绩效工资,下次考核看情况发放。

很多兄弟看到这可能会替我打抱不平,狗资本家,快去发起劳动仲裁。可是他们马上又下上另一剂猛药,那就是不停的 PUA 你,告诉你现在有家庭,要多努力,要一心扑在工作上,放心,下次一定给你打回来。

我承认,他们这些话术我都看过,基本相当于明牌。可依然我还是被影响到了,情绪十分低落,也没心思去劳动局跟他们 PK。对未来的预期瞬间变得很悲观,人要是一悲观了,真的干什么也提不起兴趣。我一门心思都扑在以后能干什么上,其它的啥也不想管,疯狂的在国内外门户上刷信息,希望能找到一条“赚钱之路”。我研究了 Web3、AI 绘画、搞自媒体、网赚攻略(什么视频搬运、抄书、小说转漫画等等),基本上信息流推给我的,我都研究了一遍。这些玩意越研究越让人焦虑,因为那些标题都起的特别的有煽动性,动不动就日入几万,而我发的那些,浏览量都破不了百。于是我就想研究更多的路子去赚钱,老实说,东南亚那边的情况,我也了解过一些。

后面我对家人的态度也越来越坏,经常不耐烦,看着我小孩我经常叹气,我想这他妈可能就是中年危机提前爆发了,总之那段时间人会越来越焦虑。

后来还是我一兄弟,邀请我一家人去平潭自驾游,我们其实也没玩几天,属于特种兵式旅游,两天两晚(晚上熬夜开车去)。回来之后心情就好多了,也没那么焦虑了。其实本来也没什么,君子不立于危墙之下,这里不行那就走。找不到就先干自己的项目(我有开源项目)。我其实对干这行还是蛮有兴趣的,应该持续坚持的干下去,半途而废干别的是下策。

回想下我那时候焦虑的经历,我以前根本看都不看那种赚钱文章的,因为我知道这些大部分是在卖课,可为什么那时候我着了魔一样呢?其实很大部分与网络有关系,你着急干什么,你就愿意看点什么,你看点什么,网络就给你推什么。这种消极循环人一旦深陷其中,光凭自己是很难走出来的。其实这种时候应该主动去接收一些积极乐观的情绪,有助于自己调整心态,网络给不了你,只有身边人能给你。

更深一步的想,所谓内卷是不是也是通过网络在传播着,深刻的影响到每一个人。所谓的“智能推荐算法”,真的智能吗?大家想看的一定就是适合每个人的吗?你不停的点击去看的信息,真的能帮助到你吗?网络是我们的工具,还是我们是网络的工具?

我想我们真的应该停下来,想想我们到底在多大程度上需要抖音、需要 BiliBili、需要知乎,也许它们真的没这么重要。

人生在世,我们到底应该追逐什么?或者说,追逐什么其实不重要,重要的是我们去追逐的过程。在这个过程中,没有内卷,没有与别人的竞争,只有对自我的审视和成长。

换句话说,我有多久没有好好了解自己了,那些独属于自己的东西,永远不会背叛的资源。我们常说的:能力、人脉、技术、视野。其实除此之外还有很多很多,我刷视频看到的有趣视频点的赞,我 Chrome 里收藏的网页,我百度网盘里躺着的分享资料等等等等,还有最重要的一项,就是我的身体和组成我身体的每一个部分:大脑、心脏、肺...... 有多久没有关注和了解它们了?在这个内卷的时代,每个人都在比拼都在竞争,都怕落于人后,都想快点挣更多的钱,这些,时常让我们忽视了对我们最重要的东西。

有趣的是,每个平台都在疯狂的更新自己的算法,期望能更精准的描述一个人,给人打上各种各样的标签。但在这场竞赛中,没有平台能竞争的过你自己,在这个世界上,只有自己更了解自己。所以我真的感觉它们在做无用功,浪费资源,最好的平台,不是给打各种标签,而是引导每个人发现自己的标签是什么。

这里我想分享给各位几个我思考的点,以供探讨。

原则一:相比与到处去找信息差,更重要的是建立自己的“资源池”

我那时候不停的刷信息,不停的找信息,本质上,我是在幻想着找到一个信息差,从而获利。这也是网上铺天盖地的文章所推崇的,所谓在风口上猪都能飞。但它们总是在掩盖一个逻辑错误,那就是找到信息差和获利之间的因果关系。实际上,找到信息差只是获利的条件之一,你有多大的能力利用这个信息差,这个信息差的时效性,方方面面的因素都会互相交织和影响。

更进一步的想,信息差就像风一样,它存在于冷热空气的交换之时,它存在于各行各业、每时每刻。让我们去追逐风,这现实吗?

我们更应该静下来,好好数数自己手头的东西,整理自己的大脑。找到自己“资源池”有哪些资源,哪些可以为我们所用,哪些可以继续扩充。思路可以打开一点,任何在当前时刻属于你的东西,都是你“资源池”的一部分。

原则二:出卖自己时间和体力的不做

这个不做,不是指不去做,而是指不长期的做。一般入门一个行业或者技术,肯定要付出时间和体力的。但你要说十年如一日的付出相同的东西,那所谓“35 岁”危机就只能找到你了。这点其实各行各业都一样,只是互联网行业处在发声的前沿罢了。

包括所谓网赚、搬运都是一个道理,毫无技术含量的事做几年就好。要时常审视自己现在在干什么,手头有哪些资源,未来的目标是什么。这跟程序运行是一个道理,运行了一段时间,停下来让自己 GC 一下。不然很容易 StackOverflow。

原则三:自己抓住的资源,千万不要轻易放手

如果不经常审视自己的“资源池”,给所有资源估估价值,就很容易被人带坑里。

原先我就做过一个项目,这是个跨部门项目,我那个领导一直告诉我说这个项目没前途、没卵用,绩效也给我打的不好,问我还要不要继续做。我说那就算了吧,做的我都不想做了。

我一放弃,马上就有新人接手,连交接也不用做,代码直接拿走,吃相可见一斑。

也就是从这里我才理解到,我其实没有了解自己,没了解过我手里的项目,被人潜移默化的影响了。影响一个人的思想真的不难,不停的重复就好了。所以还是那句话,多把自己手里的“资源池”拿出来晒一晒,整理一下。

其实 996 也是一样,拿出了你最重要的资源---身体,到底换来了什么,值得好好评估一下。

原则四:做自己喜欢的赛道,更要积累自己的资源

这几个月的经历给我的最大感觉是,这世界上真的有太多太多的行业,也有很多人赚到了钱(至少网络上宣传他们赚了钱)。网络能让这些信息病毒式的传播,导致很多人错觉的以为自己照着做也能挣到钱。但他们忽视的是,网络能把世界各地的人汇聚起来,让信息流通。其实也提供了一个更大的平台,在这个平台里,只有更卷的人才能挣到钱。

有时候真的应该抛开网络。比如,你会写代码,这是你“资源池”里的一项技能,你把这个技能公开到网络售卖。只有两种情况,要么你非常的卷,打拼出一番事业;要么你根本竞争不过别人,这是普遍情况,这世界那么大,比你优秀的人有太多太多了。

但是抛开网络,回到你身边的小小社交圈子,你的技能可能就没那么普遍了。可能你会说,那我做程序员,我身边朋友认识的大部分也是做程序员啊。那么可以这么想,假如你会做菜,你身边的程序员朋友都会做菜吗?假如你会画画,你身边的程序员朋友都会画画吗?人和人总有差异点,你觉得找不到优势,那是因为你尚未建立自己的“资源库”。

先认识自己,再让身边的人认识自己,当他们会给你打标签时,他们就成了你“资源库”中的一员,这就是人脉。这才是是独属于你自己的标签,而不是抖音、B 站为你打的冷冰冷的标签。

总结

以上我感悟的四个原则,我称之为“资源池思维”,一个比较程序员化的名词。

这篇文章发完后,我后续可能就继续更新一下具体的技术文章了,继续深耕技术。

最后,推荐看到最后的各位看一部冷门电影:《神迹》,讲述的是医生维维安托马斯的故事。看完可以来一起交流交流感悟。


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

摸鱼时间打造一款产品

辞职我辞职了拿上水杯,挎起背包,工位被我丢在了身后,一阵清风过后,我便离开了这度过一年半载的地方辞职的原因很简单,公司快没钱了,要么同公司共进退,要么离开,于是我选择了离开公司的待遇不算好也不算差,工资不算满意,但至少双休不加班。平时开发阶段末尾还比较闲,大把...
继续阅读 »

辞职

我辞职了

拿上水杯,挎起背包,工位被我丢在了身后,一阵清风过后,我便离开了这度过一年半载的地方

辞职的原因很简单,公司快没钱了,要么同公司共进退,要么离开,于是我选择了离开

公司的待遇不算好也不算差,工资不算满意,但至少双休不加班。平时开发阶段末尾还比较闲,大把摸鱼时间,逛逛各种论坛,掘金、知乎、github不亦乐乎,现在看来公司倒闭和我不无关系

久而久之,不免有些无聊。论坛里充斥着灌水文章,看多了属实是食之无味。于是为了打发时间,只能写一写自己的项目。一想到老板在为我打工,敲打键盘的双手便愈发轻盈了

未曾设想的道路

大半年前为了记录学习一项技能到底要花多少时间,我开了个新坑,做一款计时软件,记录某个时间段发生的事情

和以往一样,最初只是打算随便写写,写个基础功能完事。但在使用的过程中,越来越多的需求在脑海中构建。编程最有趣的便是创造感,你能感受到自己在创建一个新的世界,在创造的过程中时间飞速流逝,一转眼便度过了无聊的一天

激情是短暂的,生活是漫长的。和往常一样,功能在逐步完成,但我的兴趣在逐渐减少。没有添加柴薪,火便只能渐渐势弱

此时两个选择摆在面前,一个便是不再更新下去,毕竟要做的已经完成了,再去寻找下一个打发时间的事情就好了。另一个则是保持兴趣继续做下去

兴趣?利益?

做一个开源软件,如果能收获社区的掌声想必是件自豪的事情。但如果只有掌声,久而久之开源作者可能会陷入自己到底为了什么才做这件事的思维泥潭。有的人失去了兴趣便离开了,有的人发出了声音希望得到一些回馈

兴趣可以支撑人前行,但又有多少人能不求回报去做一件事?不可否认,曾经幻想过做出爆红的软件,然后不用打工,财富自由这样的白日梦。虽然不能一步登天,但我想借助它向前一步

审视一下目前的状况,如果要供用户使用,一个简单的计时功能加上记录,未免太过单薄。这么简单的功能实在谈不上什么竞争力,实现成本过低,而且我相信人们更愿意使用移动app,而不是在pc上去使用这个功能。我需要一个特定于pc且有实在价值的功能,很快我便找到了,它既满足前面的要求,又契合软件的主题

广告恐怕是最理想的获利方式,不会影响用户使用,也不用去考虑升级版之类的的东西。虽然不知道具体能有多少收入,但希望起码能够抵消掉域名的费用

有了继续前进的目标,这艘小船便能扬帆远航

但眼下的问题很严重,我在技术选型上摔了个大跟头

重头再来

好的开始是成功的一半,但没有人能预料到未来会发生什么

使用vue3为前端,我直接选择了webview方向的跨端框架

在以go为后端的wails和rust为后端tauri中,我选择了go。之前学习过一段时间的rust,深知学习的难度。而且在最初的预想中,我只是打算做个简单的计时软件,使用go也只是做一下数据库操作。不久后就完成了最初的一版,但在后续的尝试中,发现wails的生态还是太小了,很多基础的功能都需要自己实现。这时再看看tauri就显得很香了,各种插件和前端的绑定,再加上go并没有用得多么称手,于是只能长痛不如短痛了

ui框架的选择上我也犯了同样的问题。开始是偏向于material design这种风格,选择了vuetify,这个框架当时我看了很久,做的时候已经要到v3正式版本了。本来以为没问题,但后续使用时过于难受,此时文档基本没怎么更新,issue也被各种bug塞满了。只能快刀斩乱麻,换了习惯的ant-design-vue,风格区别很大,但改改样式也能用。quasar同样在我的考虑范围内,但更加小众,目前是不打算换了,在tauri v2移动端正式版后,再做尝试

为什么最开始没有选择做移动端?功能契合,使用起来也更方便。一方面是我的主要技能栈是js,另一方面重新学移动端过于不切实际,为一个八字没一撇的项目去学实在没有必要。flutter我之前也学过,试着写了一点,但还是不如js来得舒服

回过头来,发现走了很多弯路,但不去尝试只站在远处观望,永远也不会有结果。颠颠撞撞重头再来

编程之外

我一直把时间花在了代码之上,但想要做一款产品还远远不够,它迫使我不得不将视角转向那些我不曾关注的角落

UI可谓是产品的脸面,用户的第一印象便停留在了logo和界面上,虽然使用了风格统一的组件库,但将他们组合在一起的时候未必能将它们严丝合缝。目前只能说是勉强能看,日后再做修改

说明文档带领用户快速理解程序的运作,由于用户没有设计者的前提条件,很多理所当然也就需要一一记录

想要完善功能,bug和feature的反馈也要做指引,方便接收用户意见,确定前进路线

说明文档

参考vite的官网,使用vitepress,写markdown就可以了,还可以配上vue组件,还算方便

部署上选择了netlify,可以换自己的域名,还可以自动更新ssl证书

本来以为部署很麻烦的,结果一个小时左右就全部搞定,包括在namesilo上买域名,然后在netlify部署、配置

拥抱AI

在完成这些工作的过程中,有不少地方借助了AI,可以说很大程度加快了进程

编码上,由于我完全不懂windows编程和勉强会点rust语法,想要完成监听系统上的应用状态这项功能,根本就无从谈起。要花大量时间去学习的话,反而和我利用碎片时间进行编程相冲突了。况且在new bing的帮助下,我完成一个简单的函数就要花费数个小时的尝试。new bing根据我的需求返回了相关的api参考,但很多时候返回的代码并不能直接运行,有着这样那样的问题,需要去修正。很难想象仅凭我一人去翻找资料何时才能完成这冰山一角

在这个过程中,new bing最大的帮助就是提供了关键词。很多时候,你知道一个事物,想用自己的语言需要一长串词语去描述,但过去的搜索引擎并不能理解这些,而且就算把描述输入进去,也会因为过多的关键字导致答案被淹没在茫茫的网页之中。这就造成了一个困境,我不知道它叫什么,所以我要去搜索,但搜索的时候要知道它叫什么

在netlify配置域名,我输入了如何去配置,new bing给出了关键的name servers,省去了花时间去到处去找教程

为应用绘制一个logo,很显然我并没有这个能力,使用Bing Image Creator,一段描述就能生成

这些都是一些无关紧要,琐碎的事情,我只想获取结果,把精力留在我擅长的事情上。试想一下,我一个人去实现要花掉多少时间?最终能实现吗?部分功能交给其他人,又要用什么去换取?

计划

讲到这里如果有兴趣了解一下的话,可以移步仓库地址,但目前的功能我只能说很少,而且还可能出现问题,我提前声明一下。说明文档见此处,需要看清警告提示

为什么这个时候来写这篇文章来介绍呢,主要是辞职了也没事干,已经做了大半年就整理了一下。原本是半年后再辞职的,但计划赶不上变化,只能提前放出来看看情况

还有一项没能赶上的便是广告,使用的是google adsense,但提交申请后便石沉大海。尽管提前做了申请,但已经过去了几个星期。可能是开始建站时随意申请被驳回的缘故,久久没有反应

最后

辞职其实还有一个原因,就是累了,光是待在公司什么都不干,也能感觉到劳累。工作小憩之余,在过道眺望远方时,我一直想问自己究竟在干些什么。我想做出改变,想去尝试新的东西,体验另外一种生活

想过很多次,以后也许不会从事编程的工作,但又有什么选择呢。我希望是创作,而不是枯燥的重复劳作,但我很清楚这不是换一个职业就能改变的问题,终究是实力的问题。编程很有趣,但在公司并不是如此

现在我已经度过了一周的悠闲时光了,白天在家看看书,傍晚下楼走走,看着面向我驶过的匆忙下班的人流,感叹这也是自己前不久的模样。我背朝着喧闹,走上凉爽的林荫道,晚风吹过,天边挂着一轮淡淡的月牙


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

工作三年后的胡思乱想

一眨眼工作已经三年了,前两年的总结 工作第一年、工作第二年 基本上把在公司做的事情都介绍了,今年站在「前端已死」、互联网大裁员的环境下,想想未来的路可能更为应景。经常说这是最好的时代,也是最坏的时代,互联网便是如此。通过互联网将人与人之间的...
继续阅读 »

一眨眼工作已经三年了,前两年的总结 工作第一年工作第二年 基本上把在公司做的事情都介绍了,今年站在「前端已死」、互联网大裁员的环境下,想想未来的路可能更为应景。

经常说这是最好的时代,也是最坏的时代,互联网便是如此。通过互联网将人与人之间的各种链接都成为了可能,在互联网诞生之前,人与人之间的交流就是现实生活中的圈子,而现在本来这一辈子都不会在现实中产生交集的人在互联网却会相遇。

各种写书的大佬、开源的大佬,以往可能只是从文字、代码中了解他们,但现在通过社交媒体、微信竟然就产生了互动。当然不好一面就是也会遇到和自己不相投的人,也许会影响自己的心情。

通过互联网极大的扩宽了我们的视野,看到了别人在怎么生活,也放大了自己的焦虑和欲望。我们需要认清自己的边界,知道自己想要什么,自己能做什么,不需要对本来不可能发生在自己身上的事情而焦虑。

当迷茫焦虑时,看看宇宙的纪录片,从宇宙的视角去看自己,无论从空间大小还是时间维度,其实自己什么都不是,想那么多干啥。

再想想其他动物,吃饭睡觉喵喵叫,也挺好的。

前端已死

互联网已经结束了快速扩张的时期,这是个客观事实,因此招聘的人数相对于之前减少了很多,但远没到一个已死的状态,相对于其他行业,选择互联网依旧是一个不错的选择。

前端会不会死不知道,互联网肯定会一直存在下去,现在整个社会都是基于互联网,已经变成了像电、水一样的基础设施,没有人可以离开它。因此互联网的相关的岗位一定会一直一直存在。

至于互联网中具体的职业划分,前端、后端、算法、数据库等,它们各自使用的语言、技术一定会发生变化的,当选择互联网技术行业的时候,就应该抱有持续学习的态度。

塞班操作系统被安卓、iOS 取代、.Net 岗位的减少、客户端大量岗位转前端,这些也就发生在近十几二十年。当某一个岗位减少的时候,一定又会出现新的岗位,保持开放的心态去学就可以,变化再多肯定也有不变的东西。当掌握一门技术再学习另一门技术的时候,肯定会比小白学习一门新技术快很多很多,很多经验也会迁移过去。

去年 12 月出来的 chatGPT 为代表的大模型,到现在也就半年多的时间,很多以前完全不敢想的事情就这样发生了。可以预见的是一部分岗位数量肯定也会减少,目前影响最大的应该是 UI 岗,其次一定程度上可以提高程序员的开发以及学习效率,但还没有到取代的程度,但未来会再怎么发展就不得而知了。

相对于其他行业,虽然互联网相关技术迭代确实很快,但如果是因为热爱而选择这个行业,我觉得去做一辈子是没问题的。

技术

底层技术服务于上层技术,上层技术服务于应用,真正赚钱的是应用,它可能提升了用户的效率、也可能提升了用户的生活体验,这样用户才愿意付费。上层技术的人收到了钱,进一步也愿意为底层技术的人付费。

但对于一个应用,技术并不是最重要的,更多需要的是产品和运营,一个应用在 chatGPT 和各种框架、云服务的加持下做出来变得太简单了,更多的是我们需要思考如何设计产品和如何推广运营产品,和用户产生更亲密的连接,用户才愿意付费。

极端一点,即使现在所有的应用都停止更新了,其实也并不会产生多大的影响。

在公司中亦是如此,对于技术开发,没有谁是不可取代的,公司更期望的是那些可以发现问题、分析问题、定义问题的人,至于怎么解决,问题定义清楚以后,解决方案自然可以出来,谁去解决并不重要了。

但也不用太过悲观,虽然技术不是最重要的,但一定是不可或缺的,在解决问题的过程中也会区分出能力强和能力差的:方案的设定、代码编写的好坏、线上的 bug 数、代码的扩展性等。

赚钱

赚钱很大程度又是需要运气的,比如同一个人十年前进入互联网和现在进入互联网差别就会很大,再比如开发一个应用突然爆火,例如「羊了个羊」,这些我们是很难控制的,我们只能「尽人事,听天命」。

最近几年,除了在公司工作,对于有技术的同学赚钱有下边的方式:

  • 付费课程、出书

    最近几年越来越多的人在极客时间、掘金小册写课程或者直接出书。

    对于写课的人赚到了钱,对于买课的人只要跟着看完了,多多少少都会有很多收获。付费课程会比较系统, 如果没有这些课程,去学东西肯定也是可以学的,但需要花很多时间去网上搜一些零碎的资料,由于没有经验甚至可能走很多弯路。

  • 付费社群

    市面上也会有一些付费训练的社群或者知识星球

    对于组织付费社群的人会花费很大的精力,需要持续运营并且照顾到每一个人,不然就等着挨骂吧。因此这类收益也会很高,一些人会辞去工作专职来搞。

  • 开源

    大部分开源基本上是用爱发电,更多是收获一些朋友、流量、提升技术。

    比如 core-js 作者的经历,一个 22.6k star 的项目,几乎各个网站都在用的一个项目,作者却因为钱的问题被很多人谩骂。因此如果是个人专职开源一个项目靠 GitHub Sponsor 会很难很难。

    当然,开源也是能赚到钱的,比如 Vue 开源就赚到了很多钱,但毕竟是很少很少数了。

    依赖纯开源项目赚到钱,还是需要背靠公司。比如阿里云谦的 Umi、通过开源加入 NuxtLab 的 Anthony Fu、在 AFFiNE 的雪碧等等。

  • 应用

    身为一个程序员,尤其是前端程序员,当然可以自己维护一个应用来赚钱。

    做得很成功的比如 Livid 的 V2ex 社区,Abner Lee 的 Typora(后来知道作者竟然是国内开发者)。

    也有一些没有那么出名的,比如大鹏的 mdnice,秋风的 木及简历

    当然如果要做一个很大的项目,背靠公司也是一个很好的选择,比如之前阿里玉伯的语雀、之前极客邦池建强的极客时间。

    还有一些小的创业公司会做的,冯大辉的「抽奖助手」、吴鲁加的「知识星球」等。

    做出这些应用不需要很多时间,需要我们善于发现生活中的痛点以及强大的执行力,当然想成功的话需要再加一点运气,在成功前需要不断尝试不同的东西。

  • 流量变现

    有流量就会赚钱,不管是接广告、还是带货。互联网上也会有部分人专注于怎么搞流量,知乎怎么获得更多曝光、视频号怎么获得更多流量、怎么批量注册号,各个平台规则可能是什么,怎么对抗规则,这类有技术加持也会更加顺利,很多人也在专职做。

赚钱的方式有很多,对于我来说,我会尽量选择复利的事情,这样才能产生更大的价值。比如一对一咨询,一份时间换一份收入。但如果把东西写成课程,只需要花一份的时间就能获得 N 份的收入。

另外就是需要保持分享,分享除了能帮助其他人,对自己也会有很大的帮助,写文章的过程中也会不断的有新的认知得到。虽然当下可能没有金钱方面的收入,但时间放宽到几十年,相信一定会有很大的回报。

人的欲望是无穷的,也不能陷入赚钱的极端,目标应该是关注此刻,体验生活,享受生活,而不是不停的赚钱。之前听播客,有一个恰当的比喻,钱就好比汽油,不停的赚钱相当于不停的加油,但如果汽车停着一直不动,再多的汽油也是无意义的。

健康

最近几年总是爆出程序员突然离世的新闻,前段时间耗子叔突然离世的消息听到之后真的很震惊。twitter 经常刷到耗子叔的动态,然后突然一天竟然就戛然而止了,毫无征兆。

意外是无法避免的,只能尽可能的从饮食、作息、锻炼三方面降低生病的风险。

饮食

我是工作第一年体检的时候检查出了中度脂肪肝、尿酸高,当时因为是刚毕业,体重是我的巅峰,140 多斤,脂肪都堆在了肚子上。那段时间就开始跑步加吃沙拉,少吃米饭、面条。降的也快,几个月就回到了 130 斤以下,甚至到 120 多点。

第二年体检的时候,脂肪肝基本没有了,尿酸也降了许多。

image-20230702141922024

后来就保持少吃米饭,多吃蛋白质、蔬菜的饮食了。

作息

有一次得了带状疱疹,那种非常痛的类似于痘痘的东西,后来了解了一下是因为免疫力低导致病毒入侵的。猜测因为晚上坐在电脑前,气温降低了没注意,从而导致了生病。

病好之后就决心养成早睡早起的习惯。

之前作息基本上是 1 点到 2 点睡觉,9 点前后起床。现在基本上保持在 11 点前后睡觉,6 点到 7 点间起床了。

早起的好处就是早上会有大把的时间,而且这段时间是专属于自己的,并且因为大脑刚苏醒,效率也会很高。但如果是工作一天,晚上回家再做自己的事情,此时大脑已经很疲惫了,效率会比较低。

运动

最开始是跑步,但确实很难坚持下去,跑步需要换衣服、出门,还依赖于外边的天气,成本很高。后来陆续尝试过 keep、一些付费课程,都做了但没有完全养成习惯。

后来知道了 switch 的健身环大冒险,然后就一路坚持到了现在,前段时间已经通关了。

目前也一直在坚持,基本上一周会运动三到四次,一次大概花费 50 分钟左右。

投资

大学的时候开始接触到理财,知道了基金的概念,看了银行螺丝钉的「指数基金定投指南」,也看了「穷爸爸富爸爸」、「小狗钱钱」这类理财入门的书。当时赚到的一些钱,就跟着银行螺丝钉投了,主要是一些宽基和中概、医疗。

一直到工作的第一年,基金收入确实不错,甚至赚了百分之四五十。当时想着原来股市这么简单,这咋还能亏钱了。

接着疫情不断发展,还有外部经济的变化,中概、医疗都大跌,当时发了年终奖还不停的补仓中概,到现在亏损也有百分之三四十了。

但我心态是可以的,一切都是浮亏和浮盈,只要不卖一切都是浮云。

经历了大起大落后吸取了一些教训,那就是一定要严格执行计划,现金流多不一定要立刻全部投入,而是按计划定投,因为没人知道会跌多久,只有有充足的现金流,才能够把亏损逐步拉平。

现在国家规定互联网基金这些必须走「投顾」,也就是主理人帮我们买入、卖出,我们只需要交一定的投顾费即可。目前我都是在雪球上投,跟投的有孟岩的「长钱账户」、alex 的「全球精选」、螺丝钉的指数增强和主动优选。

能设置自动跟投的就自动跟投了,我相信专业的事交给专业的人肯定是没问题的。

投资肯定是财富自由不了的,但一定比把钱放余额宝强一些,只要耐心持有,尤其是目前这样的熊市投入,相信到下一个牛市会有不错的回报。

(以上仅个人看法,股市有风险,入市需谨慎)

保险

如果开始接触理财,除了投资,一个绕不过去的点就是保险。

对于保险是什么的比喻,之前听薛兆丰的课时候印象深刻。

我现在还年轻力壮,将来年纪大了可能会生病,为了防止以后生病要花一大笔医药费,今天就开始存钱,每个月拿出 10% 的收入存起来,未雨绸缪。这是一种做法。

另外一种做法,是我每个月也拿出 10% 的收入去买保险。

这两种做法有什么区别呢?

区别在于,如果我是用储蓄来未雨绸缪,那么未来可能就会发生两种不同的情形。

如果我将来年纪大了也没生病,我存的钱就还是我的钱,我不需要花出去,这时候我还是很幸运的,能够保有我原来的收入,这份储蓄没有被花掉,我赚了。

但是如果我运气不好,生病了,这份储蓄就会被用掉,甚至需要借很多钱去治病,生活会发生巨大的变化。

所以通过储蓄来未雨绸缪,它的特点是未来的结局是可变的,是变动的、是带有风险的。要么高、要么低,要么能够保有原来的这份储蓄,要么这份储蓄就被用掉了甚至借更多的钱。

而对于保险来说,如果你没病,那你的生活该怎么样还是怎么样。如果你病了,那会有保险公司给你支付一大笔钱,你也不用和别人借钱,病好后继续该干啥干啥。

因此存钱去防止生病就有赌的成分了,如果没病就白赚了很多钱,如果病了生活质量可能会发生很大的变化。

而保险就可以降低风险,未来即使生病了,由于看病不需要花钱了,病好后生活质量也尽可能的维持在原来轨道 。

我期望未来肯定是尽量稳定的,所以在不影响当前生活质量的条件下我愿意拿出一部分钱来买保险。原计划我可能会 30 岁以后开始买重疾险,之前女朋友的朋友有推荐保险的,然后就跟女朋友一起配置了重疾险。

选保险一定要慎重,一些看起来很划算的保险, 到理赔的时候可能会推三阻四,甚至理赔前公司破产了,尽量要选择大公司。

当然生活没有标准答案,每个人看到世界也都是不同的,我也一直在成长,一直在认识新的东西,上边的所想的也不能保证说未来不会再变。

未来能做的就是多看看书,不限制自己,看看经济学的、哲学的、心理学的、人文的,多出去走走看看,尽可能多的增加人生体验,去认识世界,认识自己,做自己想做的事,爱自己所爱的人,走下去就好了。


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

谈软件项目中的外行管理内行

掘友的一个评论,让我又想跟大家唠会儿嗑了。我之前写过一篇文章叫《该写好代码吗?我也迷茫了》。于是,这位掘友评论说,他在上家公司的时候,他们组代码质量要求高,前期设计考虑全面,所以上线后问题少。但是在领导眼里就像没事做一样。其他组经常出事故,反而很忙,像是一直在...
继续阅读 »

掘友的一个评论,让我又想跟大家唠会儿嗑了。

我之前写过一篇文章叫《该写好代码吗?我也迷茫了》。于是,这位掘友评论说,他在上家公司的时候,他们组代码质量要求高,前期设计考虑全面,所以上线后问题少。但是在领导眼里就像没事做一样。其他组经常出事故,反而很忙,像是一直在攻克难题。

我回复说:那可能是领导不够专业,没有深入到项目里

回复完了,我仍然意犹未尽。于是,才有了这篇文章。

正常情况下,对于一个软件项目或者一次版本迭代,负责人应该是心中有数的。这个项目的难度如何?手里的人员能力如何?大约做多长时间?最终会达到什么效果?这些应该是负责人的基本功。我不自夸,反正我带项目时,我们几个负责人都是能做到的。

据此,你才能更好地把握一个项目的始终。有时,项目中间出现难题,要干预。有时,大家超额完成,要嘉奖。有时,很简单的功能,他开发出了很多bug,要扣奖金。甚至有时候,一个月的工期,老板硬要半个月完成,这时对结果不能要求太高,你不能又要做完又要做好,那是找打的节奏,最终会两败俱伤。

但是,存在一种叫“外行管理内行”的情况。这类领导呢,他不知道你干了啥,也不知道对你来说这活好不好干,甚至你干没干完他也分辨不出,全靠读你的周报和计划。但他却是你的分管领导

我原本以为,在技术领域不会存在这类情况。后来发现,不但有,居然还很普遍。而且在国内,这还是一道别致的景观。

听过一个报道,说国内有一位教授,他从国外买来高端芯片,先把标志磨掉,再印上自己的Logo。然后,他对外宣传是自主研发的。出人意料的是,他还开发布会,还拿到了上亿的科研资金。随后几年,他甚至还推出了第二代、第三代、第四代,都是通过自己印标签的手段完成的。

整个过程,但凡有内行审一下图纸,看一下生产车间,都会很容易识破骗局

当然,咱们普通人一般到不了研究芯片,那么高精尖的层次。但是,我觉得作为一个普通企业的小领导,参与到项目中,这总是可以的吧。

从实际情况来看,其实很少有领导能躬身入局。哪怕这个领导只管六、七个人,他也会再分成三四个组。遇到一项任务,领导会安排给员工,让它们去协商完成。而他,要去规划团队未来的发展。

我有种很明显的感觉,但现在还找不到一个合适的名词来描述它。只能从一些例子上去体会其中的差异。

我有一个任领导,开周会是他做总结。他说这一周,后端完成了什么,前端没有完成什么。前端还反驳他,说自己完成了。领导说,别糊弄我,你只是把接口地址敲上了,根本没调用过,你跑跑试试,数据结构对不上。他是最了解整个项目开发进度的人

还有些领导与他形成鲜明的对比。他们了解事情靠开会听汇报,就算只有三个下属,也要他们说说这周干了啥。他做一下汇总,然后再上报给他的领导。功能交付后,项目出现了问题,他会说是下属欺骗他,居然谎报工期,没测说测了,没做说做了。

至于形成这种区别的原因,可能跟文化和制度有关。不能说哪种绝对好或者不好。

前面那种“严管型”领导所在的公司,企业文化中,负责人制,不讲理由。出了问题不要推脱是张三、李四没做好,他们担不起这个责任,就是你负责人的问题。所以,这才导致了领导会落实每一项流程。

后者那类“汇报型”的企业,从上到下都注重文书的格式、措辞,讲究高瞻远瞩,着眼未来,允许试错,探索创新。因此,他们便将更多的精力投入到了汇报上。

作为一名老程序员,我感觉,软件开发是一种很工程化的工作,一层层拆分好,安排到基层人员手里,告诉他们如何执行就可以了。但是新一代领导说,管理的最高境界是让团队具备自驱力,硅谷就是这样的。自驱力就是你不用安排,他们自己就能主动克服困难去完成。即便领导不在,团队也能照常运转,这才是健康的团队。

抱歉,有点儿跑题了。

本文的初衷是“外行管理内行”与“躬身入局”。

我觉得“外行管理内行”在国内是必然现象,尤其在技术领域

在国内的中小企业,作为团队的领导,“行政类”的事情要比“专业类”多。你看看你的领导,他经常开会和写材料。这不是它自己要干的,也不是老板要求的,是受行业和环境的影响。你想要申报科技企业,申请政策扶持,参加高新评级,你就得去准备。

除此之外,团队的日常管理,年计划、月计划,人员流动、评优、优化等等,都会耗费日常的精力。而这些活,专业的程序员并不想干。

另外,就算一个内行被推上管理岗位,程序员工种复杂,知识更新也快,那他干上几年,也会慢慢被磨成“外行”。

因此,内行外行只是相对的,并不重要。我则更强调“躬身入局”

躬身入局,就是参与到项目中。就算不写代码,起码也了解下大家遇到的问题。在这个过程中,作为管理者,可以制定一些规则和标准,来保证良性运作。好的流程制度,是可以实现员工自驱动的。

举一个身边的例子。现在夏天了,天气很热,办公室都会开一天的空调,温度调到23度。

这天下过大暴雨,空气清爽,气温很低。这时,23度的空调就很冷了。正对着空调口的瘦同事,就想去关掉空调。结果被一个静止都会喘粗气的大胖子给制止了。

胖子问:为啥关空调?

瘦子说:外面下雨,不热了!我冷。

胖子说:你冷你穿衣服。你关了我热。

瘦子说:你热,你打开小风扇。

说完,瘦子关了空调。 随后,胖子又打开了空调。

这类事情就是公说公有理,婆说婆有理。搞不好还需要领导出面调停,或者搞一个群体性的投票。如果没人理你,那就看谁的底线更低,或者说谁更狠。

但是,如果说团队制定这么一个小规则,就是室温超过26度可以开空调制冷,制冷底线是23度。那么,即便没有领导在场,大家也能很好处理此类事情。甚至,这类情况根本都不会发生。

这只是为了体现规则重要性的例子。到软件开发中,可能就是比如这类情形:一个接口如果多个端都调用,那么就由后端组织特定的数据格式。

如果你躬身入局,你就会经历类似的事情。否则的话,你可能会说,吵什么吵,大家要以大局为重(只是要求停止争吵,没有决定开不开空调)。

可能说这么多,有人朝我笑了:你以为我傻?我这么做,老板给我涨工资,多拿钱!

抱歉,抱歉!我只想着如何提高项目质量了。关于挣钱方面,我外行了!

唉,大家一定要重视环境的重要性。


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

假如互联网人都很懂冒犯

大家好,我是老三,最近沉迷于听脱口秀,并且疯狂安利同事。脱口秀演员常常说的一句话是:“脱口秀是冒犯的艺术”。最近我发现,同事们好像有点不一样了。阳光灿烂的早上,趿拉着我的宝马拖鞋,跨上包浆的小黄车,屁股感受着阳光积累的炙热,往公司飞驰而去。一步跨进电梯间,我擦...
继续阅读 »

大家好,我是老三,最近沉迷于听脱口秀,并且疯狂安利同事。

脱口秀演员常常说的一句话是:“脱口秀是冒犯的艺术”。最近我发现,同事们好像有点不一样了。


阳光灿烂的早上,趿拉着我的宝马拖鞋,跨上包浆的小黄车,屁股感受着阳光积累的炙热,往公司飞驰而去。

一步跨进电梯间,我擦汗的动作凝固住了,挂上了矜持的微笑:“老板,早上好。”

老板:“早,你还在呢?又来带薪划水了?”

我:“嗨,我这再努力,最后不也就让你给我们多换几个嫂子嘛。”

老板:“没有哈哈,我开玩笑。”

我:“我也是,哈哈哈。”

今天的电梯似乎比往常慢了很多。

我:“老板最近在忙什么?”

老板:“昨天参加了一个峰会,马xx知道吧?他就坐我前边。”

我:“卧槽,真能装。没有,哈哈。”

老板:“哈哈哈”。

电梯到了,我俩都步履匆匆地进了公司。

小组内每天早上都有一个晨会,汇报工作进度和计划。

开了一会,转着椅子,划着朋友圈的我停了下来——到我了。

我:“昨天主要……今天计划……”

Leader:“你这不能说没有一点产出,也可以说一点产出都没有。其实,我对你是有一些失望的,原本今年绩效考评给你一个……”

我:“影响你合周报了是吗?不是哈哈。”

Leader、小组同事:“哈哈哈“。

Leader:“好了,我们这次顺便来对齐一下双月OKR,你们OKR都写的太保守了,一看就是能完成的,往大里吹啊。开玩笑哈哈。”。

我:”我以前就耕一亩田,现在把整个河北平原都给犁了。不是,哈哈。”

同事:“我要带公司打上月球,把你踢下来,我来当话事人。唉,哈哈”

Leader、同事、我:“哈哈哈“。

晨会开完,开始工作,产品经理拉我和和前端对需求。

产品经理:“你们程序员懂Java语言、Python语言、Go语言,就是不懂汉语言,真不想跟你们对需求。开个玩笑,哈哈。”

我:“没啥,你吹牛皮像狼,催进度像狗,做需求像羊,就这需求文档,还没擦屁股纸字多,没啥好对的。不是哈哈。”

产品经理、前端、我:“哈哈哈”。

产品经理:“那我们就对到这了,你们接着聊技术实现。”

前端:“没啥好聊的,后端大哥看着写吧,反正你们那破接口,套的比裹脚布还厚,没事还老出BUG。没有哈哈。”

我:“还不是为了兼容你们,一点动脑子的逻辑都不写,天天切图当然不出错。不是哈哈。”

前端、我:“哈哈哈”。

经过一番拉扯之后,我终于开始写代码了。

看到一段代码,我皱起了眉头,同事写的,我顺手写下了这样一段注释:

/**
* 写这段代码的人,建议在脑袋开个口,把水倒掉。不是哈哈,开个玩笑。
**/

代码写完了,准备上线,找同事给我Review,同事看了一会,给出了评论。

又在背着我们偷偷写烂代码了,建议改行。没有哈哈。

同事、我:“哈哈哈”。

终于下班了,路过门口,HR小姐姐还在加班。

我:“小姐姐怎么还没下班?别装了,老板都走了。开玩笑哈哈。”

HR小姐姐:“这不是看看怎么优化你们嘛,任务比较重。不是,哈哈。”

HR小姐姐、我:“哈哈哈”。

我感觉到一种不一样的氛围在公司慢慢弥散开来,我不知道怎么形容,但我想到了一句话——

“既分高下,也决生死”。


写这篇的时候,想到两年前,有个叫码农小说家的作者横空出世,写了一些生动活泼、灵气十足的段子,我也跟风写了两篇,这就是“荒腔走板”系列的来源。

后来,他结婚了。

看(抄)不到的我只能自己想,想破头也写不不来像样的段子,这个系列就不了了之,今天又偶尔来了灵感,写下一篇,也顺带缅怀一下光哥带来的快乐。


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

人性的弱点之如何让别人信服你

前言 早些年花时间拜读了《人性的弱点》一书,不可否认其中的一些观点和方法确实很有用,在后来的工作与生活中我有意无意的使用其中的一些语言上的技巧,确实化解了一些无意义的争端,在生活中与人相处也顺利、平和了许多。 当然了,里面的一些观点与方法并不适合所有人,也...
继续阅读 »

前言


早些年花时间拜读了《人性的弱点》一书,不可否认其中的一些观点和方法确实很有用,在后来的工作与生活中我有意无意的使用其中的一些语言上的技巧,确实化解了一些无意义的争端,在生活中与人相处也顺利、平和了许多。


当然了,里面的一些观点与方法并不适合所有人,也许有人会觉得这样很累,也会有人觉得大可不必,因人而异吧, 如果有些朋友明显感觉到在生活、工作中因为说话频频损失了一些机会,那么我建议可以阅读一下这本书。


本文是我对其中一部分章节的提炼与总结,也是后来时长翻阅警醒自己的摘要。


一、避免陷入争论

  1. 丢掉先入为主的观念
  2. 控制好自己的情绪
  3. 耐心听对方说完
  4. 努力找认同点
  5. (语言上)能让步就让步
  6. 真心感谢别人对你的重视

二、永远别说:“你错了。”


永远不要对别人说:“让我来告诉你,你哪里错了!”


这是一种挑衅,会造成对方的反感,会让聆听者想要和你争论,即使你是好心提醒,并没有想要引起争端。


如果你直接了当的告诉他:你错了。人们的第一反应是会反击你,但不会让他们改变主意。


因为你的直接否定了对方的判断力、智商、打击了他们的自尊和骄傲。


三、学会认错

  • 靠争夺,你永远都不能让自己满足;
  • 懂得谦让,你才会收获出乎意料的受益。

四、温柔友爱比狂躁和武力更强大

  1. 一滴蜂蜜比一加仑胆汁更吸引苍蝇
  2. 如果你是充满物理倾向的来谈论问题,别人也会气势汹汹的和你争论
  3. 你用温和友好的态度,别人也会不由自主的认同你的观点
  4. 善良友好的交谈方式更容易让别人接受

五、让对方说“是”

  1. 再开始与别人交流的时候,先认可对方,在刚开始得到“是”也多,越容易被接受
  2. 不要着急把自己的见解说出来,先强调你认同对方的那些观点
  3. 如果一开始就让对方说“是”,那么就会忘记争议,听从我的建议
  4. 温和提出问题,一个可以让对方说“是”的问题
  5. 轻履者行远

六、对待抱怨的安全方式

  1. 人们通常会不停的谈论自己的观点,借此获得他人的信服
  2. 不要打断别人的讲话,即便是你不认同的观点
  3. 耐心听完对方的话,一定要诚恳,还要鼓励他们说完想法和意见
  4. 让对方成为你们交谈的主要人物
  5. 如果想交友,就让你的朋友胜过你

七、让对方觉得自己聪明

  1. 和别人的叙述相比,人们更愿意相信自己努力的出来的结论
  2. 让他人觉得这是依照他的想法所为
  3. 我们不喜欢别人强行把东西卖给我们,也不喜欢别人逼迫我们做某事
  4. 我们希望可以购买自己喜欢的东西,可以做自己喜欢的事
  5. 我们希望别人关心我们的想法和愿望

八、学会换位思考

  1. 一定要站在对方的立场想问题
  2. 一个人即使真的错了,也不会认为自己有错
  3. 人们的一切想法和行为都是有依据的
  4. 想要他人和你倾心交谈,请像重视你自己的感受一向重视他人
  5. 出于自愿的改过,不会有不满的情绪
  6. 想让对方认可你钱,先问问自己:“他为什么想做这种事?”

九、与他人充分共情

  1. 你的观点我一点都不否认,如果换做是我,我也会和你有相同的感受
  2. 你所遇到的人中,百分之九十的人都渴望得到怜悯
  3. 对于那些不愉快的情绪,同情能起到关键性的调节作用
  4. 所有人都希望得到同情

十、激发他人高尚的情操

  1. 人们做一件事的原因有两个,一个是高尚的借口,另一个才是真正的理由
  2. 人们总是把自己理想化,更愿意去相信那些高尚的借口
  3. 当你想让他们是发生改变的时候,就需要把他的高尚动机激发出法
  4. 大部分人都是诚实的, 他们一旦认为自己行为是正确的,就会非常想维护这种正确性。
  5. 我相信即是一个村新欺骗的人,当你嘉定他是真诚正直的时候,他也不想辜负你的信任。

十一、戏剧化表达你的想法

  1. 单凭陈述事实不能解决问题,需要以生动有趣的方式展现出来
  2. 在任何常场所,都可以把你想的想法戏剧化的展现出来
  3. 戏剧化表达方式就是利用某件东西,环境来衬托下面要说的事

十二、发起挑战的激励性

  1. 竞争可以产生效率,所以要激发他的竞争意识
  2. 所有人的心中都有恐惧,只有勇士会忘记恐惧,勇往直前。他们可能一败涂地,但通常都会取得胜利
  3. 战胜恐惧是人世间最大的挑战
  4. 每个成功人士都热爱竞争
  5. 激发人对胜利的渴望,对“被重视的感觉”的渴望


作者:子洋
来源:mdnice.com/writing/2b351a9007a249e19c9d3757876c19e5
收起阅读 »

图解项目管理抓手,如何轻松管理项目?

项目管理抓手.png 一、项目进度控制 1.  制定周报、月报、季报等进度报告,实时监控项目进度,及时跟进。 2. 设置里程碑,定期审核各项任务的完成情况和项目整体进度。 3. 使用项目管理软件或工具,制作项目进度计划,动态分配资源和任...
继续阅读 »
项目管理抓手.png

项目管理抓手.png


一、项目进度控制


1.  制定周报、月报、季报等进度报告,实时监控项目进度,及时跟进。


2. 设置里程碑,定期审核各项任务的完成情况和项目整体进度。


3. 使用项目管理软件或工具,制作项目进度计划,动态分配资源和任务。


4. 加强对关键任务的跟踪和监督,确保按时完成。


5. 积极处理进度偏差,采取调整计划、增加资源等措施弥补延误。


二、项目成本控制


1. 制定具体的项目预算,并下达到每个任务和人员。


2. 实施成本监控,分析阶段成本与实际情况间的差异。


3. 评估项目影响要素,制定成本风险预案。


4. 优化资源配置,最大程度提高投入产出比。


5. 严格审核支出,防范不必要的支出。


三、项目变更管理


1. 根据明确的变更标准和流程管理项目变更事项。


2. 通过项目变更委员会或会议,审议和批准变更。


3. 评估变更对其他事项的影响,并做出相应调整。


4. 详细记录变更内容及其历史,方便查询和参考。


5. 定期分析项目变更趋势,降低未来变更的可能性。


四、质量控制


1. 对项目产出物在功能、性能、兼容性和可靠性等方面进行测试和复核。


2. 设置质量标准和控制措施,并对团队成员进行培训和监督。


3. 针对测试或使用过程中发现的问题进行跟踪处理和改进。


4. 详细记录质量数据和信息,分析质量事件的影响因素。


5. 不断完善质量管理流程和标准 ,以提高整体质量水平。


六、风险管理


1. 开展项目风险识别,穷尽可能出现的风险因素。


2. 对风险因素进行定量分析,评估影响程度和可能性。


3. 制定风险应对计划并统筹资源,积极化解风险。


4. 持续监测项目进展中的不确定性,及时更新风险分析与应对。


5. 分析风险事件的教训与启示。为今后提供借鉴。


七、项目团队管理


1.  明确团队各成员的工作任务和职责。


2. 建立沟通机制和交流渠道。


3. 通过奖励机制来激励团队工作积极性。


4. 建立团队协同工作能力,提高效率和效果。


5. 不断完善团队组建和管理流程与机制。


四、其他方面


1. 统筹资源配置,有效考核绩效,激励执行。


2. 保存项目相关文件,创建项目知识库。


3. 分析项目经验教训,为今后提供借鉴。


4. 加强项目状态信息和数据的共享。


5. 不断优化项目管理流程,以提高效率。


作者:极客技术之路
来源:mdnice.com/writing/a3b44581c0f5475a9def01b2c762b3fa
收起阅读 »

8个方面快速提高项目交付速度

项目完成和项目成功地完成是两个不同的概念。作为一个专业的项目管理人员,需要确保项目尽可能地输出好的成果,这个成果要满足以下两个基本要求,才能算是真正成功的项目交付,其中 达到内外部客户满意的要求 是最重要的。 达到基本的质量要求 ...
继续阅读 »

项目完成和项目成功地完成是两个不同的概念。作为一个专业的项目管理人员,需要确保项目尽可能地输出好的成果,这个成果要满足以下两个基本要求,才能算是真正成功的项目交付,其中 达到内外部客户满意的要求 是最重要的。





  • 达到基本的质量要求



  • 达到内外部客户满意的要求


以下是整理的8个方面提高项目交付速度,供大家参考。





  • 提前开始



  • 停止多任务



  • 设定预期



  • 并行增加工作量



  • 减少需求



  • 优化依赖关系



  • 尝试增加风险



  • 优先排序


一、提前开始



如果在项目还没有正式开始之前,项目经理就已经知道项目的时间限制,那么可以提前做好项目的准备工作。特别是对于项目需要交付的内容做好提前规划,对于确定性的工作形成合适的规划,对于不确定的部分提前做好变化应对。一旦不确定性的工作确定下来,可以节省项目前期规划的时间,加快整个项目的交付进度。






  1. 提前组建项目团队,尽早进入工作状态





  2. 与客户进行需求交流,尽早确定需求范围和优先级





  3. 对确定的需求提前进行设计和规划





  4. 将确定的设计转换为项目任务和工作包,形成初步的项目计划。





  5. 对不确定需求,提前制定假设和方案,待确定后快速推进





  6. 提前准备技术方案,评估不同方案的可行性。





  7. 预先确定供应商,签订合同,准备材料





  8. 制定项目进度表、资源计划,确定项目范围。





  9. 提前测试相关工具、环境,确保可用性。





  10. 制定风险管理策略,识别可能的风险因素。




二、停止多任务



许多报告和研究表明,在项目中不断从一个任务转向另一个任务是一种不切实际的工作方式,而且这种方式反而会让工作效率变得更低。项目经理需要确保项目团队成员每天留出专用的时间专注于某一个任务,这样他们才能更好地完成工作,更少地分心,更有可能在截止期限前完成任务。






  1. 制定项目工作规范,建立不间断工作时间段,减少随机任务切换





  2. 对团队成员的工作时间进行合理规划,留出至少2-3小时的专注工作区间。





  3. 在专注区间内,关闭通讯工具,减少外部干扰。





  4. 制定个人工作计划表,按优先级专注于一项任务,避免跳跃作业。





  5. 优化工作环境,减少外界干扰源。





  6. 建立时间管理意识,记录每个工作区间的产出。





  7. 跟踪工作进度,衡量多专注工作的效果提升。





  8. 培养团队自律习惯,鼓励单项任务深度完成。





  9. 合理安排休息,保证工作效率。





  10. 不断优化多任务问题,提升团队效率。




三、设定预期


> 不要总是以为项目的发起人对项目的进度、成本和质量都关注,他们可能只会关注其中的某一个方面。项目经理要管理项目关键利益相关者的期望,一定要找出对方最为关注的点。这是非常重要的一件事,这样一旦项目中出现冲突的时候,项目经理可以做出一定的取舍。如果重点关注质量,并想要提前交付完成,那就可以在保证质量的前提下要求增加资源。





  1. 与项目发起人和关键利益相关方进行充分沟通,理解各方的需求和期望。





  2. 制定项目三角管理框架,评估客户对成本、进度、质量、范围各方面的关注点。





  3. 对客户进行需求调研和访谈,确认关键的期望指标。





  4. 构建项目目标体系,确定客户的主要关注点,如质量或交付时间。





  5. 根据主要关注点设定项目进度和质量目标,形成项目章程。





  6. 进行风险分析,识别可能影响关键关注点的风险因素。





  7. 制定风险缓解策略,保证达成客户关键预期。





  8. 与客户持续沟通,明确范围变更对预期的影响。





  9. 在项目执行中优先保证客户关键关注点,做出必要权衡和取舍。





  10. 不断确认客户的关注点,及时调整项目目标。




四、并行增加工作量



项目经理可以对项目中的各项任务进行审核和分析,看看哪些工作可以早点启动,哪些工作需要和其他任务一并执行。项目经理需要同时注意资源管理,因为你无法安排同一个人同时执行两项任务,如果确实增加了工作量,那就需要引进更多的人来帮助你进行任务管理。






  1. 分析任务依赖关系,识别可以并行执行的任务





  2. 对独立任务进行资源评估,确定可以增加的并行工作量。





  3. 根据关键路径,优先使关键任务尽早开始





  4. 对冗余资源较充裕的任务适当提前启动。





  5. 重新制定资源计划,扩充项目团队,提供并行任务所需资源。





  6. 制定多团队协作机制,加强沟通和协调。





  7. 监控各并行任务的进展,必要时进行资源调配。





  8. 合理安排任务优先级,平衡资源使用。





  9. 加强项目整体计划和风险管理。





  10. 避免过度并行增加项目管理复杂度。




五、减少需求



项目经理可以与项目发起人一起商量这个项目究竟能砍掉些什么,哪些可选的要求能够取消,将范围缩小到实际可以实现的交付工作量。通过这种方式可以去掉不能为实现项目主要目标增值的任务,也就是意味着缩小项目的范围,从而减少交付工时,加快项目完成进度。






  1. 与客户沟通确定核心需求和次要需求。





  2. 对次要需求进行价值分析,评估其实施的优先级。





  3. 制定不同的交付方案,包含次要需求的全量和简化版本。





  4. 估算各方案的交付进度,与客户讨论不同方案的业务影响。





  5. 根据客户的意见,确定移除或简化的次要需求。





  6. 更新需求文档,移除或标记出简化处理的需求。





  7. 重新评估工作量和交付时间表,更新项目计划。





  8. 调整资源分配,将精力集中在核心需求的交付上。





  9. 加强对范围变更的管理,避免需求不断膨胀





  10. 提高客户沟通频率,及时获取客户反馈




六、优化依赖关系



项目中各项任务之间的依赖关系在调度中非常重要,而且往往是项目计划中的关键组成部分,也意味着项目任务必须按照顺序进行。但是,实际的项目交付并非一成不变地按照项目经理制订的计划来进行。这个时候,项目经理要检查所有的依赖关系,看看哪些是必需的,哪些是需要去除的。大多数情况下都能发现一些可以改变和优化的地方。






  1. 清理任务之间不必要的依赖关系,识别可以同时进行的任务。





  2. 对依赖关系进行质疑和验证,确定必须依赖的关键任务链。





  3. 采用方法设计、敏捷开发等,使任务模块更加独立。





  4. 明确依赖关系中的关键路径任务,缩短其时长。





  5. 对冗余依赖任务进行重构、优化或外包。





  6. 增加资源投入,使依赖任务可以并行推进。





  7. 引入新的技术或方法,简化任务流程,减少依赖。





  8. 加强沟通协作,使信息流通,及时消除依赖。





  9. 监控依赖关系变化,及时更新项目计划。





  10. 继续寻找简化依赖的新思路。




七、尝试增加风险


很多时候,过于保守地执行计划会导致项目进度缓慢。如果出现这种情况,为了更快地交付,项目经理可以尝试做出一些改变而不是墨守成规。项目本身的存在即是一种风险,项目经理不应该简单地避免风险,而是要学会管理风险,需要查看项目规划阶段时的假设,看看能否通过一些假设来推动项目进度。





  1. 识别项目进展缓慢的领域,寻找可采取的风险。





  2. 对增加进行成本效益分析,评估收益和代价。





  3. 优先选择对项目进度影响显著、风险较可控的方案。





  4. 制定风险缓解和应急策略,做好风险管理。





  5. 留出时间和资源增加风险缓冲。





  6. 与团队和利益相关方沟通,获取支持。





  7. 监控风险状况,根据需要及时调整策略。





  8. 加强数据统计分析,衡量风险的成效。





  9. 记录风险管理过程,总结经验教训。





  10. 不断提高风险意识和管理能力。




八、优先排序


项目经理可以对工作量和要求进行优先级排序,将低优先级的工作或项目延后,提前完成项目中的高优先级部分。





  1. 明确项目关键路径和里程碑节点





  2. 根据客户需求与项目目标,确定各项任务的优先级。





  3. 制定优先级评估矩阵,评分确定任务顺序。





  4. 优先安排关键路径和高优先级任务所需资源。





  5. 优化资源计划,将非关键任务适当推迟。





  6. 对低优先任务简化要求,减少工作量。





  7. 提前完成高优先级任务,为后续任务创造缓冲。





  8. 与客户沟通,获取对优先级的确认。





  9. 监控进展情况,必要时调整任务优先级。





  10. 持续总结和改进优先排序的决策机制。




作者:极客技术之路
来源:mdnice.com/writing/42e1d4e8e00843e7a5dcdfa6d5db9781
收起阅读 »

看完这篇,SpringBoot再也不用写try/catch了

前言 使用 SpringBoot 开发 Web 应用时,异常处理是必不可少的一部分。在应用中,异常可能会出现在任何地方,例如在控制器、服务层、数据访问层等等。如果不对异常进行处理,可能会导致应用崩溃或者出现未知的错误。因此,对于异常的处理是非常重要的。 ...
继续阅读 »

前言


使用 SpringBoot 开发 Web 应用时,异常处理是必不可少的一部分。在应用中,异常可能会出现在任何地方,例如在控制器、服务层、数据访问层等等。如果不对异常进行处理,可能会导致应用崩溃或者出现未知的错误。因此,对于异常的处理是非常重要的。


本篇主要讲述在SpringBoot 中,如何用全局异常处理优雅的处理异常。


为什么要优雅的处理异常


如果我们不统一的处理异常,开发人员经常会在代码中东一块的西一块的写上 try catch代码块,长久以往容易堆积成屎山。


@Slf4j
@Api(value = "User Interfaces", tags = "User Interfaces")
@RestController
@RequestMapping("/user")
public class UserController {
    /**
     * @param userParam user param
     * @return user
     */

    @ApiOperation("Add User")
    @ApiImplicitParam(name = "userParam", type = "body", dataTypeClass = UserParam.classrequired true)
    @PostMapping("add")
    public ResponseEntity add(@Valid @RequestBody UserParam userParam) {
        // 每个接口都需要手动try catch
        try {
            // do something
        } catch(Exception e) {
            return ResponseEntity.fail("error");
        }
        return ResponseEntity.ok("success");
    }
}

那我们应该如何实现统一的异常处理呢?


使用 @ControllerAdvice + @ExceptionHandler注解



@ControllerAdvice 定义该类为全局异常处理类


@ExceptionHandler 定义该方法为异常处理方法。value 的值为需要处理的异常类的 class 文件。



首先自定义异常类 BusinessException :


/**
 * 业务异常类
 * @author rango
 */

@Data
public class BusinessException extends RuntimeException {
    private String code;
    private String msg;
 
    public BusinessException(String code, String msg) {
        this.code = code;
        this.msg = msg;
    }
}

然后编写全局异常类,用 @ControllerAdvice 注解:


/**
 * 全局异常处理器
 * @author rango
 */

@ControllerAdvice
public class GlobalExceptionHandler {
    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
 
    /**
     * 处理 Exception 异常
     * @param httpServletRequest httpServletRequest
     * @param e 捕获异常
     * @return
     */

    @ResponseBody
    @ExceptionHandler(value = Exception.class)
    public ResponseEntity exceptionHandler(HttpServletRequest httpServletRequestException e
{
        logger.error("服务错误:", e);
        return new ResponseEntity("******""服务出错");
    }
 
    /**
     * 处理 BusinessException 异常
     * @param httpServletRequest httpServletRequest
     * @param e 捕获异常
     * @return
     */

    @ResponseBody
    @ExceptionHandler(value = BusinessException.class)
    public ResponseEntity businessExceptionHandler(HttpServletRequest httpServletRequestBusinessException e
{
        logger.info("业务异常报错!code:" + e.getCode() + "msg:" + e.getMsg());
        return new ResponseEntity(e.getCode(), e.getMsg());
    }
}


定义了全局异常处理器,项目就可以对不同的异常进行统一处理了。通常,为了使 controller 中不再使用任何 try/catch,会在 GlobalExceptionHandler 中对 Exception 做统一的拦截处理。这样其他没有用 @ExceptionHandler 配置的异常就都会统一被处理。


遇到异常时主动抛出异常


在业务中,遇到业务异常的地方,我们直接 throw 抛出对应的业务异常即可。如下所示


throw new BusinessException(ERROR_CODE, "用户账号/密码有误");

在 Controller 中的写法


Controller 中,不需要再写 try/catch,除非特殊场景。


@RequestMapping(value = "/test")
public ResponseEntity test() {
    ResponseEntity re = new ResponseEntity();
    // 业务处理
    return re;
}

结果展示


异常抛出后,返回如下结果。


{
    "code""E0014",
    "msg""用户账号/密码有误",
    "data"null
}

注意!!!



  • 抛出的异常如果被代码内的 try/catch 捕获了,就不会被 GlobalExceptionHandler 处理



  • 异步方法中的异常不会被全局异常处理(多线程)



  • 不是 controller 层抛出的异常才能被 GlobalExceptionHandler 处理,只要异常最后是从 contoller 层抛出去的都可以被捕获并处理

总结


本文介绍了使用 SpringBoot 时,如何通过配置全局异常处理器统一处理项目中的一些通用的异常,避免程序员不断的写try/catch导致的代码冗余,有利于代码的维护。


作者:程序员典籍
来源:mdnice.com/writing/103055f00ba04cf4b06f0195f839a449
收起阅读 »

pnpm 是凭什么对 npm 和 yarn 降维打击的

web
大家最近是不是经常听到 pnpm,我也一样。今天研究了一下它的机制,确实厉害,对 yarn 和 npm 可以说是降维打击。 那具体好在哪里呢? 我们一起来看一下。 我们按照包管理工具的发展历史,从 npm2 开始讲起: npm2 用 node 版本管理工具把...
继续阅读 »

大家最近是不是经常听到 pnpm,我也一样。今天研究了一下它的机制,确实厉害,对 yarn 和 npm 可以说是降维打击。
那具体好在哪里呢? 我们一起来看一下。



我们按照包管理工具的发展历史,从 npm2 开始讲起:


npm2


用 node 版本管理工具把 node 版本降到 4,那 npm 版本就是 2.x 了。


768C1B00093D82D19D2CC333F3221670.jpg


然后找个目录,执行下 npm init -y,快速创建个 package.json。


然后执行 npm install express,那么 express 包和它的依赖都会被下载下来:


FB54F396F6A73093CE052A6881AF7C50.jpg
展开 express,它也有 node_modules:


E652FB00C06BA36FA8861E3B785981BF.jpg
再展开几层,每个依赖都有自己的 node_modules:


75AC9B15A99383C9EA9E5E4EF8302588.jpg
也就是说 npm2 的 node_modules 是嵌套的。


这很正常呀?有什么不对么?


这样其实是有问题的,多个包之间难免会有公共的依赖,这样嵌套的话,同样的依赖会复制很多次,会占据比较大的磁盘空间。


这个还不是最大的问题,致命问题是 windows 的文件路径最长是 260 多个字符,这样嵌套是会超过 windows 路径的长度限制的。


当时 npm 还没解决,社区就出来新的解决方案了,就是 yarn:


yarn


yarn 是怎么解决依赖重复很多次,嵌套路径过长的问题的呢?


铺平。所有的依赖不再一层层嵌套了,而是全部在同一层,这样也就没有依赖重复多次的问题了,也就没有路径过长的问题了。


我们把 node_modules 删了,用 yarn 再重新安装下,执行 yarn add express:


这时候 node_modules 就是这样了:


7AF387F155588612B92C329F91D30BFA.jpg


全部铺平在了一层,展开下面的包大部分是没有二层 node_modules 的:


BBBA3B5F68AB691541FD51569E5B1316.jpg


当然也有的包还是有 node_modules 的,比如这样:


B5E0DBF3C7E6FBEEDC2CC90D350A278C.jpg
为什么还有嵌套呢?


因为一个包是可能有多个版本的,提升只能提升一个,所以后面再遇到相同包的不同版本,依然还是用嵌套的方式。


npm 后来升级到 3 之后,也是采用这种铺平的方案了,和 yarn 很类似:


67B0E1280BAD542944AB08A35CCE88C3.jpg
当然,yarn 还实现了 yarn.lock 来锁定依赖版本的功能,不过这个 npm 也实现了。


yarn 和 npm 都采用了铺平的方案,这种方案就没有问题了么?


并不是,扁平化的方案也有相应的问题。


最主要的一个问题是幽灵依赖,也就是你明明没有声明在 dependencies 里的依赖,但在代码里却可以 require 进来。


这个也很容易理解,因为都铺平了嘛,那依赖的依赖也是可以找到的。


但是这样是有隐患的,因为没有显式依赖,万一有一天别的包不依赖这个包了,那你的代码也就不能跑了,因为你依赖这个包,但是现在不会被安装了。


这就是幽灵依赖的问题。


而且还有一个问题,就是上面提到的依赖包有多个版本的时候,只会提升一个,那其余版本的包不还是复制了很多次么,依然有浪费磁盘空间的问题。


那社区有没有解决这俩问题的思路呢?


当然有,这不是 pnpm 就出来了嘛。


那 pnpm 是怎么解决这俩问题的呢?


pnpm


回想下 npm3 和 yarn 为什么要做 node_modules 扁平化?不就是因为同样的依赖会复制多次,并且路径过长在 windows 下有问题么?


那如果不复制呢,比如通过 link。


首先介绍下 link,也就是软硬连接,这是操作系统提供的机制,硬连接就是同一个文件的不同引用,而软链接是新建一个文件,文件内容指向另一个路径。当然,这俩链接使用起来是差不多的。


如果不复制文件,只在全局仓库保存一份 npm 包的内容,其余的地方都 link 过去呢?


这样不会有复制多次的磁盘空间浪费,而且也不会有路径过长的问题。因为路径过长的限制本质上是不能有太深的目录层级,现在都是各个位置的目录的 link,并不是同一个目录,所以也不会有长度限制。


没错,pnpm 就是通过这种思路来实现的。


再把 node_modules 删掉,然后用 pnpm 重新装一遍,执行 pnpm install。


你会发现它打印了这样一句话:


FA450CB6BE37F7AEDADDD7AF8CB5EBF9.jpg


包是从全局 store 硬连接到虚拟 store 的,这里的虚拟 store 就是 node_modules/.pnpm。


我们打开 node_modules 看一下:


DD21BA4ABF8516795C6BC205C18793E3.jpg
确实不是扁平化的了,依赖了 express,那 node_modules 下就只有 express,没有幽灵依赖。


展开 .pnpm 看一下:


25BF2AA593655F0A20232371A43AB81A.jpg
所有的依赖都在这里铺平了,都是从全局 store 硬连接过来的,然后包和包之间的依赖关系是通过软链接组织的。


比如 .pnpm 下的 expresss,这些都是软链接:


6F84C353D1CFE72E2F820B62C9A3B96E.jpg
也就是说,所有的依赖都是从全局 store 硬连接到了 node_modules/.pnpm 下,然后之间通过软链接来相互依赖。


官方给了一张原理图,配合着看一下就明白了:


0E694CA43CC1E52ED6AF8BCD50882004.jpg
这就是 pnpm 的实现原理。


那么回过头来看一下,pnpm 为什么优秀呢?


首先,最大的优点是节省磁盘空间呀,一个包全局只保存一份,剩下的都是软硬连接,这得节省多少磁盘空间呀。


其次就是快,因为通过链接的方式而不是复制,自然会快。


这也是它所标榜的优点:


image.png


相比 npm2 的优点就是不会进行同样依赖的多次复制。


相比 yarn 和 npm3+ 呢,那就是没有幽灵依赖,也不会有没有被提升的依赖依然复制多份的问题。


这就已经足够优秀了,对 yarn 和 npm 可以说是降维打击。


总结


pnpm 最近经常会听到,可以说是爆火。本文我们梳理了下它爆火的原因:


npm2 是通过嵌套的方式管理 node_modules 的,会有同样的依赖复制多次的问题。


npm3+ 和 yarn 是通过铺平的扁平化的方式来管理 node_modules,解决了嵌套方式的部分问题,但是引入了幽灵依赖的问题,并且同名的包只会提升一个版本的,其余的版本依然会复制多次。


pnpm 则是用了另一种方式,不再是复制了,而是都从全局 store 硬连接到 node_modules/.pnpm,然后之间通过软链接来组织依赖关系。


这样不但节省磁盘空间,也没有幽灵依赖问题,安装速度还快,从机制上来说完胜 npm 和 yarn。


pnpm 就是凭借这个对 npm 和 yarn 降维打击的。


作者:JEECG官方
来源:juejin.cn/post/7260283292754919484
收起阅读 »

前端发展:走进行业迷茫的迷雾中

web
引言 2023年,前端开发作为IT行业中备受关注的领域之一,正在经历着巨大的挑战和变革。然而,在当前行业不景气、失业率居高不下以及裁员潮席卷而来的情况下,许多人开始质疑前端开发的未来前景以及学习它是否依然有意义。本文将探讨这个问题并试图给出一些启示。 第一部...
继续阅读 »

引言


image.png
2023年,前端开发作为IT行业中备受关注的领域之一,正在经历着巨大的挑战和变革。然而,在当前行业不景气、失业率居高不下以及裁员潮席卷而来的情况下,许多人开始质疑前端开发的未来前景以及学习它是否依然有意义。本文将探讨这个问题并试图给出一些启示。


第一部分:前端的价值


image.png
前端开发作为网页和移动应用程序开发的重要组成部分,扮演着连接用户与产品的桥梁。前端技术的发展不仅推动了用户体验的提升,也对整个互联网行业产生了深远的影响。随着移动互联网的普及和技术的进步,前端在用户与产品之间的交互变得越来越重要。


对于企业而言,拥有优秀的前端开发团队意味着能够提供更好的用户体验、增强品牌形象、吸引更多用户和扩大市场份额。因此,前端开发的技能依然是企业争相追求的核心能力之一。


第二部分:行业不景气的背后


image.png
然而,正如每个行业都经历高低起伏一样,前端开发也面临着行业不景气带来的挑战。2023年,全球经济增长乏力、市场竞争激烈以及萧条的就业市场等因素,使得许多公司紧缩预算、停止招聘,并导致了失业率的上升和裁员的潮水。


在这种情况下,前端开发者需要重新审视自己的技能和市场需求。他们需要具备综合能力,包括对最新前端技术的深入了解、与其他团队成员的良好沟通合作能力以及持续学习和适应变化的能力。


第三部分:自我调整与进阶


image.png
面对市场变化和就业压力,前端开发者需要主动调整自己的发展路径。以下是一些建议:



  1. 多元化技能:学习并精通多种前端框架和库,如React、Vue.js和Angular等。同时,了解后端开发和数据库知识,拥有全栈开发的能力,将会让你在就业市场上更具竞争力。

  2. 学习与实践并重:不仅仅是学习新知识,还要将所学应用于实际项目中。积累项目经验,并在GitHub等平台分享你的作品,以展示自己的能力和潜力。同时,参加行业内的比赛、活动和社区,与他人交流并学习他们的经验。

  3. 持续学习:前端技术发展日新月异,不断学习是必需的。关注行业的最新趋势和技术,参加培训、研讨会或在线课程,保持对新知识的敏感度和学习能力。


第四部分:面对就业市场的挑战


image.png
在面对行业不景气和裁员的情况下,重新进入就业市场变得更加具有挑战性。以下是一些建议:



  1. 提升个人竞争力:通过获得认证、实习或自主开发项目等方式,提升自己在简历中的竞争力。扩展自己的专业网络,与其他开发者和雇主建立联系。

  2. 寻找新兴领域:探索新兴的技术领域,如大数据、人工智能和物联网等,这些领域对前端开发者的需求逐渐增加,可能为你提供新的机会。

  3. 转型或深耕细分领域:如果市场需求不断减少,可以考虑转型到与前端相关的领域,如UI设计、交互设计或用户体验设计等。或者在前端领域深耕细分领域,在特定行业或特定技术方向上寻找就业机会。


结论


image.png
虽然当前的行业环境确实严峻,但前端开发作为连接用户与产品的重要纽带,在未来依然有着广阔的发展空间。关键在于前端开发者要不断自我调整与进阶,持续学习并适应市场需求。通过多元化技能、学习实践、提升个人竞争力以及面对市场挑战,前端开发者依然可以在这个变革

作者:Jony_men
来源:juejin.cn/post/7260330862289371173
时代中谋得一席之地。

收起阅读 »

树结构的数据扁平化

web
function flattenTree(data) { data = JSON.parse(JSON.stringify(data)); var res = []; while(data.length) { var n...
继续阅读 »

function flattenTree(data) {
data = JSON.parse(JSON.stringify(data));
var res = [];
while(data.length) {
var node = data.shift();
if (node.children && node.children.length) {
data = data.concat(node.children);
}
delete node.children;
res.push(node);
}
return res;
}


我们用一个数据来测试:



var tree = [{
id: 1,
name: '1',
children: [{
id: 2,
name: '2',
children: [{
id: 3,
name: '3',
children: [{
id: 4,
name: '4'
}]
}, {
id: 6,
name: '6'
}]
}]
}, {
id: 5,
name: '5'
}]


使用:



console.log(flattenTree(tree));


打印结果:


image.png


作者:tntxia
来源:juejin.cn/post/7260500913848090661
收起阅读 »

千万级高可用分布式对账系统设计实践

背景         目前线上业务量与日俱增,每日的订单量超过千万,资金流动大,资金安全成为了重点关注的问题。为了确保每一笔交易的正确性,提高资金的正确性和保障业务的利益,除了RD代码逻辑严格以外,还需要对每日甚至每小时订单的流水进行核对,对异常情况能及时处理...
继续阅读 »

背景


        目前线上业务量与日俱增,每日的订单量超过千万,资金流动大,资金安全成为了重点关注的问题。为了确保每一笔交易的正确性,提高资金的正确性和保障业务的利益,除了RD代码逻辑严格以外,还需要对每日甚至每小时订单的流水进行核对,对异常情况能及时处理。面对千万级的订单量,人工对账肯定是不可行的,所以,实现一套对账系统成为了必然的事,不仅为资金安全提供依据,也节省公司运维人力,数据更加可视化。目前这套系统已覆盖聚合渠道网关与外部渠道100%的对账业务,完成春晚期间支付宝亿级订单量对账,完成日常AC项目千万级订单量对账,对账准确率实现6个9,为公司节省2~3个人力。


介绍


        对账模块是支付系统的核心功能之一,不同业务设计的对账模型不同,但是都会遇到以下几个问题:



  • 海量的数据,就目前聚合支付的订单量来看,设计的对账系统需要应对千万级的数据量;

  • 面对日切、多账、少账等异常差异订单应该如何处理;

  • 账单格式、下载账单时间、下载方式等不一致问题。


        针对以上问题,并结合财经聚合支付系统的特点,本文将设计一套可以应对千万级数据量、分布式和高可用的对账系统,利用消息队列Kafka的解耦性解决对账系统各模块之间的强依赖性。文章从三个方面介绍对账系统,第一方面,总体介绍对账系统的设计,依次介绍各个模块的实现及其过程中使用到的设计模式;第二方面,介绍对账系统版本迭代的过程,为什么需要进行版本迭代,以及版本迭代过程中踩过的“坑”;第三方面,总结现有版本的特点并提出下一步的优化思路。


系统设计


系统结构图


        图1为对账系统总结构图,分为六个模块,分别是文件下载模块、文件解析并推送模块、平台数据获取并推送模块、执行对账模块、对账结果统计模块和中间态模块,每个模块负责自己的职能。
对账系统总结构图


图1 对账系统总结构图


        图2为对账系统利用Kafka实现的状态转换图。每个模块独立存在,彼此之间通过消息中间件Kafka实现系统状态转换,通过中间态UpdateReconStatus类实现状态更新和message发送。这种设计不仅实现流水线对账,也利用消息中间件的特点,实现重试和模块之间的解耦。

对账系统状态转换图.png


图2 对账系统状态转换图


        为了更好的了解每个模块的实现过程,下面将依次对各个模块进行说明。

文件下载模块


设计

        文件下载模块主要完成各个外部渠道账单的下载功能。众所周知,聚合支付是聚众家三方机构能力为一体的支付方式,其中三方机构包括支付宝、微信等支付界的领头羊,多样性的支付渠道导致账单下载存在多样性,如何实现多模式、可拔插的文件下载能力成为该模块设计的重点。分析Java设计模式的特点,本模块采用接口模式,符合面向对象的设计理念,可实现快速接入。具体实现类图如图3所示(只展示部分类图)。


图3 文件下载实现类图


        下面就以支付宝对账文件下载方式为例,具体阐述一下实现过程。


实现

        分析支付宝接口文档,目前采用的下载方式为HTTPS,文件格式为.csv的压缩包。根据这些条件,本系统的实现方式如下(只摘取了部分代码)。由于消息中间件Kafka和中间态模块的机制,已经从系统层面考虑了重试的能力,因此不需要考虑重试机制,后续模块也如此。


public interface BillFetcher {
// ReconTaskMessage 为kafka消息,
// FetcherConsumer为自定义账单下载后的处理方式
String[] fetch(ReconTaskMessage message,FetcherConsumer consumer) throws IOException;
}

@Component
public class AlipayFetcher implements BillFetcher {

public AlipayFetcher(@Autowired BillDownloadService billDownloadService) {
Security.addProvider(new BouncyCastleProvider());
billDownloadService.register(BillFetchWay.ALIPAY, this);
}
...
@Override
public String[] fetch(ReconTaskMessage message, FetcherConsumer consumer) throws IOException {
String appId = map.getString("appId");
String privateKey = getConvertedPrivateKey(map.getString("privateKey"));
String alipayPublicKey = getPublicKey(map.getString("publicKey"), appId);
String signType = map.getString("signType");
String url = "https://openapi.alipay.com/gateway.do";
String format = "json";
String charset = "utf-8";
String billDate = DateFormatUtils.format(message.getBillDate(), DateTimeConstants.BILL_DATE_PATTERN);
String notExists = "isp.bill_not_exist";
String fileContentType = "application/oct-stream";
String contentTypeAttr = "Content-Type";
//实例化客户端
AlipayClient alipayClient = new DefaultAlipayClient(url, appId, privateKey, format, charset, alipayPublicKey, signType);
//实例化具体API对应的request类,类名称和接口名称对应,当前调用接口名称
AlipayDataDataserviceBillDownloadurlQueryRequest request = new AlipayDataDataserviceBillDownloadurlQueryRequest();
// trade指商户基于支付宝交易收单的业务账单
// signcustomer是指基于商户支付宝余额收入及支出等资金变动的帐务账单
request.setBizContent("{" +
""bill_type":"trade"," +
""bill_date":"" + billDate + """ +
" }");
AlipayDataDataserviceBillDownloadurlQueryResponse response = alipayClient.execute(request);
if(response.isSuccess()){
//do 根据下载地址获取对账文件,通过流式方式将文件放到指定的目录下
...
System.out.println("调用成功");
} else {
System.out.println("调用失败");
}
}
}

具体步骤:



  1. 重写构造方法,将实现类注入到一个map中,根据对应的下载方式获取实现类;

  2. 实现fetch接口,包括构造请求参数、请求支付宝、解析响应结果、采用流式将文件放入对应的目录下,以及这个过程中的异常处理。


文件解析并推送模块


设计

        前面提到,聚合支付是面对不同的外部渠道,对账文件的多样性不言而喻。比如微信是采用txt格式,支付宝采用csv格式等等,而且各个渠道的账单内容也是不一致的。如何解决渠道之间账单的差异性成为该模板需要重点考虑的问题。通过调研和现有对账系统的分析,本系统采用接口模式+RDF(结构化文本文件)的实现方式,其中接口模式解决账单多模式的问题,同时也实现可拔插的机制,RDF工具组件实现账单的快速标准化,操作简单易会。具体实现类图如图4所示(只展示部分类图)。


图4 文件标准化实现类图


        下面就以支付宝对账文件解析为例,具体阐述一下实现过程。
实现

        根据支付宝的账单格式,提前定义RDF标准模板,后续账单解析将根据模板将每一行对账文件解析为对应的一个实体类,其中需要注意标准模板的字段必须要和账单数据一一对应,实体类的字段可以多于账单字段,但必须包括所有的账单字段。接口定义如下:


public interface BillConverter<T> {
//账单是否可以使用匹配器
boolean match(String channelType, String name);
//转换原始对账文件到Hive
void convertBill(InputStream sourceFile, ConverterConsumer<T> consumer) throws IOException;
//转换原始对账文件到Hive
void convertBill(String localPath, ConverterConsumer<T> consumer) throws IOException;
}

具体实现步骤如图5所示:


流程图.png


图5 文件解析流程图



  1. 定义RDF标准模板,如下为支付宝业务流水明细模板,其中body结构内字段名必须和实体类名保持一致。


{
"head": [
"title|支付宝业务明细查询|Required",
"merchantId|账号|Required",
"billDate|起始日期|Required",
"line|业务明细列表|Required",
"header|header|Required"
],
"body": [
"channelNo|支付宝交易号",
"merchantNo|商户订单号",
"businessType|业务类型",
"production|商品名称",
"createTime|创建时间|Date:yyyy-MM-dd HH:mm:ss",
"finishTime|完成时间|Date:yyyy-MM-dd HH:mm:ss",
"storeNo|门店编号",
"storeName|门店名称",
"operator|操作员",
"terminalNo|终端号",
"account|对方账户",
"orderAmount|订单金额|BigDecimal",
"actualReceipt|商家实收|BigDecimal",
"alipayRedPacket|支付宝红包|BigDecimal",
"jiFenBao|集分宝|BigDecimal",
"alipayPreferential|支付宝优惠|BigDecimal",
"merchantPreferential|商家优惠|BigDecimal",
"cancelAfterVerificationAmount|券核销金额|BigDecimal",
"ticketName|券名称",
"merchantRedPacket|商家红包消费金额|BigDecimal",
"cardAmount|卡消费金额|BigDecimal",
"refundOrRequestNo|退款批次号/请求号",
"fee|服务费|BigDecimal",
"feeSplitting|分润|BigDecimal",
"remark|备注",
"merchantIdNo|商户识别号"
],
"tail": [
"line|业务明细列表结束|Required",
"tradeSummary|交易合计|Required",
"refundSummary|退款合计|Required",
"exportTime|导出时间|Required"
],
"protocol": "alib",
"columnSplit":","
}


  1. 实现接口的getChannelType、match方法,这两个方法用于匹配具体使用哪一个Convert类。如匹配支付宝账单,实现方式为:


@Override
public String getChannelType() {
return ChannelType.ALI.name();
}
@Override
public boolean match(String channelType, String name) {
return name.endsWith(".csv.zip");
}


  1. 实现接口的convertBill方法,完成账单标准化;


@Override
public void convertBill(String path, ConverterConsumer<ChannelBillPojo> consumer) throws IOException
{
FileConfig config = new FileConfig(path, "rdf/alipay-business.json", new StorageConfig("nas"));
config.setFileEncoding("UTF-8");
FileReader fileReader = FileFactory.createReader(config);
AlipayBusinessConverter.AlipayBusinessPojo row;
try {
while (null != (row = fileReader.readRow(AlipayBusinessConverter.AlipayBusinessPojo.class))) {
convert(row, consumer);
}
...
}


  1. 将标准化账单推送至Hive


平台数据获取并推送模块


        平台数据获取一般都是从数据库中获取,数据量小的时候,查询时数据库的压力不会很大,但是数据量很大时,如电商交易,每天成交量在100万以上,通过数据库查询是不可取的,不仅效率低,而且容易导致数据库崩溃,影响线上交易,这点会在后续的版本迭代中体现。因此,平台数据的抽取是从Hive上获取,只需要提前将交易数据同步到Hive表中即可,这样做不仅效率高,而且更加安全。考虑到抽取的Hive表不同、数据的表结构,数据收集器Collector类也采用了接口模式。Collector接口定义如下:


public interface DataCollector {
void collect(OutputStream os) throws IOException;
}

        根据目前平台数据收集器实现情况,可以得到类图如图6所示。


图6 平台数据收集器实现类图


执行对账模块


        该模块主要完成Hive命令的执行,在平台账单和渠道账单已全部推送至Hive的前提下,利用Hive处理大数据效率高的特点,执行全连接sql,并将结果存入指定的Hive表中,用于对账结果统计。执行对账sql可以根据业务需求而定,如需要了解本系统的全连接sql,欢迎与我交流。


对账结果统计模块


        对账任务执行成功之后,需要统计全连接后的数据,重点统计金额不一致、状态不一致、日切、少账(平台无账,渠道有账)和多账(平台有账,渠道无账)等差异。针对不同的情况,本系统分别采用如下的解决方案:



  1. 金额不一致:前端页面展示差异原因,人工进行核对;

  2. 状态不一致:针对退款订单,查询平台退款表,存在且金额一致认为已对平,不展示差异,其他情况,需要在前端页面展示差异原因,人工进行核对;

  3. 日切:当平台订单为成功,渠道无单时,根据平台订单创建时间判断是否可能存在日切,如果判断是日切订单,会将这笔订单存入buffer文件中,待统计结束后,将buffer文件上传至Hive日切表中,等第二天重新加载这部分数据实现跨日对账。对于平台无订单,渠道有单的情况,通过查询平台数据库判断是否存在差异,如果存在差异,需要在前端页面展示差异,人工进行核对。

  4. 少账:目前主要通过查询平台数据库判断是否存在差异,确认确实存在差异时,需要在前端页面展示差异,人工进行核对。

  5. 多账:目前这种有可能是日切,会先考虑日切,如果不在日切范围内,需要在前端页面展示差异,人工进行核对。


中间态模块


        中间态模块是用于各模块之间状态转换的模块,利用Kafka和状态是否更新的机制,实现消息的重发和对账状态的更新。从一个状态到下一个状态,必须满足当前状态为成功,对账流程才会往下一步执行。中间态的设计不仅解决了重试问题,而且将数据库的操作进行了收敛,更符合模块化的设计,各个模块各司其职。重试次数也不是无限的,目前设置的重试次数为3次,如果3次重试后依然没有成功,会发lark通知,人工介入解决。


        总之,对账工作,既复杂也不复杂,需要我们细心,对业务要有深入的了解,并选择合适的处理方式,针对不同的业务,不断迭代优化系统。


版本迭代


        系统的设计很大程度受业务规模的影响,对于财经聚合支付而言,订单量发生了几个数量级的变化,这个过程中不断暴露出对账系统存在的问题,优化改进对账系统是必然的事。从系统设计到目前大致可以分为三个阶段:初始阶段、过渡阶段和当前阶段。


初始版(v1.0)

        初始版上线后实现了聚合渠道对账的自动化,尤其在2018年的春节活动中,资金安全提供了重要的保障,实现了聚合和老合众、支付宝、微信等渠道的对账。随着财经业务的发展,抖音电商的快速崛起,对账系统逐渐暴露出不足,比如对账任务失败增多,尤其是数据量大的对账、非正常差异结果展示、对账效率低等问题。通过不断分析,发现存在以下几个问题:



  1. 系统的文件都是放在临时目录tmp下的,TCE平台会对这个目录下的文件定时清理,导致推送文件到Hive时会报找不到文件的情况,尤其是大数据量的对账任务;

  2. Kafka消息积累多,导致对账流程中断,主要是新增渠道,对账任务增加,同时Hive执行队列是共享队列,大部分的对账流程因为没有资源而卡住;

  3. 非正常差异结果展示,主要是查单没有增加重试机制,当查询过程中出现超时等异常,会出现非正常差异结果,还有部分原因是日切跨度小而导致的非正常差异结果。


过渡版(v2.0)

        考虑到初始版对账系统存在的不足和对账功能的急迫性,对初始版进行过渡性的优化,初步实现大数据量的对账功能,同时也提高了差异结果的准确率。相比初始版,该版本主要进行了以下几点优化:



  1. 文件存放目录由临时目前改为服务下的某一个目录,防止大文件被回收,文件上传到Hive后删除文件;

  2. 重新申请独占的执行队列,解决资源不足导致对账流程卡住的问题;

  3. 查单新增重试机制,日切跨度增大,解决非正常差异结果展示,提供差异结果的准确率。


        过渡版集中解决初始版明显存在的问题,对于一些潜在的问题并没有彻底解决,如代码容错率低、对账任务异常后人工响应慢、对账效率低、数据库安全性低等问题。


当前版(v3.0)

        当前版优化的宗旨是实现对账系统的"三高",分别为高效率、高准确率(6个9)和高稳定性。


        对于高效率,主要体现在平台数据获取慢,而且存在数据库安全问题,针对这块逻辑进行了优化,改变数据获取途径,由原来的数据库获取改为从高效率的Hive中获取,只需要提前将数据同步到Hive表中即可。


        对于高准确率,主要优化对账差异处理逻辑,进一步细化差异处理方式,新增差异结果报警,细化前端页面差异原因。


        对于高稳定性,主要优化RDF处理对账文件发生异常时新增兜底逻辑,提高系统的容错性;对账任务失败或超过指定重试阈值时增加报警,加快人工响应速率;对查单等操作数据库逻辑增加限流,防止数据库崩溃。


        版本迭代过程可以总结如下,希望读者别重复入坑,尤其是大文件处理方面。


业务情况优点存在的问题目标
初始版(v1.0)财经部门初期,订单量少,业务结构简单实现少量交易量对账;支持分布式效率低;对账任务容易卡住;非异常case普遍;大数据基本不能完成对账保障资金安全问题,实现聚合渠道网关与外部渠道的对账功能
过渡版(v2.0)电商业务崛起,订单量增加,业务种类增多实现海量数据对账;查单新增重试机制;降低非异常case数量影响数据库安全性;代码容错率低;对账效率低;对账任务异常时人工响应慢支持千万级订单量对账
当前版(v3.0)优化过渡版遗漏问题,改变数据获取路径效率大大提升;实现千万级数据量对账;实现高稳定性,高准确率,高效率全连接效率低;不支持订单状态推进实现对账系统的高效率,准确率实现6个9;功能全面

总结


        对账系统模型与业务息息相关,业务不同,对账系统模型也会不同,但是大部分对账系统的整体架构变化不大,主要区别是各个模块的实现方式不同。希望本文介绍的对账系统能为各位读者提供设计思路,避免重复入坑。对对账系统感兴趣的同学可以找财经支付团队同学详聊,一起深入探讨,提出优化建议,比如优化全连接策略,也欢迎各种简历推荐。


参考文章


信息流对账与平台化实现-曾佳


混合编程在财经对账中的应用-王亚宁


内推链接


image.png

收起阅读 »

【镜·映】《烂》:没有反转的生活

增村保造的这部《烂》(Tadare,1962,tt0310199),改编自日本自然主义大家德田秋声的同名小说。 益子(Masuko,若尾文子)在东京与汽车销售员浅井(Asai,田宫二郎)同居一段时间之后,才发现这个男人原有妻子。益子不情愿破坏浅井的婚姻,提议...
继续阅读 »


增村保造的这部《烂》(Tadare,1962,tt0310199),改编自日本自然主义大家德田秋声的同名小说。


益子(Masuko,若尾文子)在东京与汽车销售员浅井(Asai,田宫二郎)同居一段时间之后,才发现这个男人原有妻子。益子不情愿破坏浅井的婚姻,提议分手,但浅井早已经不能忍受神经质的妻子了,只是因为早年接受过妻家的资助,一直未能下决心离婚。被益子发现后,浅井终于离了婚,而前妻也因承受不住打击而精神崩溃。此时益子的侄女英子(Eiko,水谷八重子)不愿与家里安排的对象相亲,从乡下跑到东京,寄宿在姑姑家中。英子向往大城市的生活,却又看不起给人做情妇的姑姑,结果却是自己与浅井勾搭在一起,被益子捉奸在床,怒不可遏的益子将英子逐出家门。而后则强行安排英子与她原本的相亲对象——一个西装革履的农民结了婚。


小三上位,然后又被自己的侄女绿了,好在她“奋起反击”,终于捍卫了所谓“爱情”——看起来,这是一段有些夸张却俗套的故事,然而,故事的结尾,却拍得令人震撼。


一切依传统进行。姑姑拉扯着身着新娘盛装的侄女来到一众亲友面前,“看,她是个好新娘!”





浅井站起来,走到英子面前,面对着这个前两天还和自己享受着最后疯狂的性爱的女子,他挤出一些不多的笑容:“你真美,你太棒了!”,然后转头离开。





在众人面带微笑的审视中,一脸漠然的新人被送上花车,她的姑姑益子就坐在她身边,紧盯着她,像极了押送犯人走向牢笼。





镜头切换,开往名古屋的列车就要出发了。





已经换了便装的英子坐在车厢中,带着幽怨凝视着窗外的浅井——他和益子站在窗外,眉头微蹙,益子则面无表情。





英子的丈夫在模糊的前景中微笑着和他人告别。车站的广播,正一遍遍播放着:“请站在白线以外,请站在白线以外......”





列车徐徐开动,浅井缓缓抬起右手,犹犹豫豫地做了一个告别的手势,似乎还未完成,就缓缓放下了。





益子抬眼凝视着他,轻叹一口气,默然转头,独自离开,送行人群纷纷挥动手臂,背景渐渐模糊,益子面色苍白疲惫,仿佛仍旧难以释怀,而明明一切已然结束,除了当事人,没有人知道姑侄两人曾是情敌,为了争夺一个男人,到了以死相拼的地步。





本来,电影到这里其实可以结束了。然而,接下来的3分多钟,才是见证一位大导演真实功力的时刻。


送走英子,益子和浅井 “像往常一样” 回到“家”,开始了 “像往常一样”“日常时刻”


益子问:“要吃点东西吗?”“要我给你准备洗澡水吗?”“要睡觉了吗?”“我给你泡点热茶吧?”浅井一概说不。





然而他回到卧室,看着曾经和两个女人翻滚过的床,却又觉得空虚。





回到客厅,益子已经泡好了茶。两人开始 “像往常一样”“日常闲谈”





益子说,“她会是一位好妻子”,浅井说,“也许吧”;益子说,“举办婚礼真好”“我们要不要也举办一场婚礼?”,浅井笑笑说,“那也挺好,我们准备一下吧”。然后,就独自回房了。





益子漠然坐着,低下头将茶杯顶在额头。自己的提议并未遭到拒绝,然而,浅井那种怎么都无所谓的回答,却比拒绝还令人难受。





尽管如此,当听到浅井那一句“你怎么还不来?”,她还是反射般地应道 “哈依” 。不想表达内心的苦楚,因为表达了也无意义。她开始一件一件脱去衣服,像往常一样搭在椅子背上,只留下半透的薄纱内衣。





然后,益子慢慢踱向内室,带上了门,屏幕转暗,左下角显示出一个 “终” 字。





还记得《毕业生》(The Graduate,1967)最后的反转吗?曾被女友母亲诱惑的男主最后的时刻鼓起勇气冲入婚礼现场,劫走了即将成为别人新娘的女友,宣告一切错误终结与新生活的开始。


然而,《烂》的结尾,没有反转


一切如常。


一切都过去了,一切都被无形的力量压制在生活的 “日常和谐” 之下,再无声息,只有三个当事人吞咽下那些无法言说的苦楚,不出意外的话,他们将会把这些意难平带进坟墓。而那些不明缘由的关系人与看客,只会知道这是一场完美的婚礼,并为此或真情或假意地抚掌相庆。错误已被终结,但新的生活并未开始。


村上春树说:“我不想找一个搭伙过日子的人,我要找一个一见我就笑,我一见就笑,喝了酒满眼光给我讲浪漫和爱的人。”也许,这是个讽刺。


也许,浅井和益子也曾期待过那样的生活。但经过这小小的插曲,他们的生活似乎又回到了常态,并且很可能那就是他们可能期待的、唯一的生活。


益子看似是这场“宫斗”戏中的胜利者,她挤走了浅井的原配,又逼退了自家的侄女,然而,除了肉体,她不知道还有什么可以留住这个男人——现在连这一点,她都不那么确定了。更为悲哀的是,她似乎只能接受这种生活的“安排”,她可以战胜情敌,却无力摆脱一个社会系统将她锁定的位置。某种程度上,她甚至不如英子这个“失败者”,至少英子曾经痛快地享受过Stolen Pleasure(这是影片的另一个名字)。


如果仅从女性主义的角度解读这部电影,就会忽略一个事实:浅井也不是胜利者。这个男人因为接受过前妻家的资助,娶了他并不爱的女人,觉得自己处处受制于神经质的妻子,好不容易摆脱了,却发现益子的善妒与疯狂,比前妻更甚。在这场荒唐的情爱纠缠中,的确女性受到的损害更甚,但浅井也没办法为所欲为,当益子和英子两位女性疯狂地撕打在一起时,他的惶恐无措说明了一切。他依靠益子摆脱了前妻,现在他必须接受这个可以为了保住自己的位置而试图掐死自己侄女的疯狂的女人。这里没有胜利者,也没有自由人。


并非是男人操纵女人,或者女人操纵男人那么简单,所有人都在被一只看不见的手操弄着。


人一般很难超越对于自己身处其中的社会的既定秩序的理解。 一般人所能做的,就是无意识地压抑,然后再无意识地合理化这种压抑:事实如此,历来如此,所有人都如此。


故事中所有的当事人,都不是脸谱化的坏人,浅井可以为情妇的兄弟慷慨解囊(当时英子还没出现);益子曾经不愿拆散浅井的婚姻,在愤怒地将英子逐出家门之后,又不忍她流落街头。


普通人的普通,也许正在于此,无法摆脱甚至完全意识不到自己就生活在社会话语的规训中,不能知行合一地依照本心行事,却又无法让良知彻底沉默。


这当然不是说人就应当违背公序良俗、像野蛮人一样生活,而是说每个人都应该意识到这些话语权力与生存困境,这种问题意识的觉醒也许会带来痛苦,但却是通向自由意志选择的必经之路。像《烂》中的男男女女一样,本能的情欲化反抗,彼此扯着头发的撕打,保卫虚假爱情的算计,始终都不可能在没有反转的生活里掀起一点点波澜。


作者:wingsay
来源:mdnice.com/writing/df16952233da49c1816ddf3746d1fa84
收起阅读 »