注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

深入学习 Kotlin 特色之 Sealed Class 和 Interface

前言sealed class 以及 1.5 里新增的 sealed interface 可谓是 Kotlin 语言的一大特色,其在类型判断、扩展和实现的限制场景里非常好用。本文将从特点、场景和原理等角度综合分析 sealed 语法。Sealed ClassSe...
继续阅读 »

前言

sealed class 以及 1.5 里新增的 sealed interface 可谓是 Kotlin 语言的一大特色,其在类型判断、扩展和实现的限制场景里非常好用。

本文将从特点、场景和原理等角度综合分析 sealed 语法。

  • Sealed Class
  • Sealed Interface
  • Sealed Class & Interface VS Enum
  • Sealed Class VS Interface

🏁 Sealed Class

sealed class,密封类。具备最重要的一个特点:

  • 其子类可以出现在定义 sealed class 的不同文件中,但不允许出现在与不同的 module 中,且需要保证 package 一致

这样既可以避免 sealed class 文件过于庞大,又可以确保第三方库无法扩展你定义的 sealed class,达到限制类的扩展目的。事实上在早期版本中,只允许在 sealed class 内部或定义的同文件内扩展子类,这些限制在 Kotlin 1.5 中被逐步放开。

如果在不同 module 或 package 中扩展子类的话,IDE 会显示如下的提示和编译错误:

Inheritor of sealed class or interface declared in package xxx but it must be in package xxx where base class is declared

sealed class 还具有如下特点或限制:

  1. sealed class 是抽象类,可以拥有抽象方法,无法直接实例化。否则,编译器将提示如下:

    Sealed types cannot be instantiated

  2. sealed class 的构造函数只能拥有两种可见性:默认情况下是 protected,还可以指定成 private,public 是不被允许的。

    Constructor must be private or protected in sealed class

  3. sealed class 子类可扩展局部以及匿名类以外的任意类型子类,包括普通 class、data classobject、sealed class 等,子类信息在编译期可知。

    假使匿名类扩展自 sealed class 的话,会弹出错误提示:

    This type is sealed, so it can be inherited by only its own nested classes or objects

  4. sealed class 的实例,可配合 when 表达式进行判断,当所有类型覆盖后可以省略 else 分支

    如果没有覆盖所有类型,也没有 else 统筹则会发生编译警告或错误

    1.7 以前:

    Non-exhaustive 'when' statements on sealed class/interface will be prohibited in 1.7.

    1.7 及以后:

    'when' expression must be exhaustive, add ...

当 sealed class 没有指定构造方法或定义任意属性的时候,建议子类定义成单例,因为即便实例化成多个实例,互相之间没有状态的区别:

'sealed' subclass has no state and no overridden 'equals()'

下面结合代码看下 sealed class 的使用和原理:

示例代码:

 // TestSealed.kt
 sealed class GameAction(times: Int) {
     // Inner of Sealed Class
     object Start : GameAction(1)
     data class AutoTick(val time: Int) : GameAction(2)
     class Exit : GameAction(3)
 }

除了在 sealed class 内嵌套子类外,还可以在外部扩展子类:

 // TestSealed.kt
 sealed class GameAction(times: Int) {
    ...
 }
 
 // Outer of Sealed Class
 object Restart : GameAction(4)

除了可以在同文件下 sealed class 外扩展子类外,还可以在同包名不同文件下扩展。

 // TestExtendedSealedClass.kt
 // Outer of Sealed Class file
 class TestExtendedSealedClass: GameAction(5)

对于不同类型的扩展子类,when 表达式的判断亦不同:

  • 判断 sealed class 内部子类类型自然需要指定父类前缀
  • object class 的话可以直接进行实例判断,也可以用 is 关键字判断类型匹配
  • 普通 class 类型的话则必须加上 is 关键字
  • 判断 sealed class 外部子类类型自然无需指定前缀
 class TestSealed {
     fun test(gameAction: GameAction) {
         when (gameAction) {
             GameAction.Start -> {}
             // is GameAction.Start -> {}
             is GameAction.AutoTick -> {}
             is GameAction.Exit -> {}
 
             Restart -> {}
             is TestExtendedSealedClass -> {}
        }
    }
 }

如下反编译的 Kotlin 代码可以看到 sealed class 本身被编译为 abstract class。

扩展自其的内部子类按类型有所不同:

  • object class 在 class 内部集成了静态的 INSTANCE 实例
  • 普通 class 仍是普通 class
  • data Class 则是在 class 内部集成了属性的 gettoString 以及 hashCode 函数
 public abstract class GameAction {
    private GameAction(int times) { }
 
    public GameAction(int times, DefaultConstructorMarker $constructor_marker) {
       this(times);
    }
     
    // subclass:object
    public static final class Start extends GameAction {
       @NotNull
       public static final GameAction.Start INSTANCE;
 
       private Start() {
          super(1, (DefaultConstructorMarker)null);
      }
 
       static {
          GameAction.Start var0 = new GameAction.Start();
          INSTANCE = var0;
      }
    }
 
    // subclass:class
    public static final class Exit extends GameAction {
       public Exit() {
          super(3, (DefaultConstructorMarker)null);
      }
    }
 
    // subclass:data class
    public static final class AutoTick extends GameAction {
       private final int time;
 
       public final int getTime() {
          return this.time;
      }
 
       public AutoTick(int time) {
          super(2, (DefaultConstructorMarker)null);
          this.time = time;
      }
      ...
       @NotNull
       public String toString() {
          return "AutoTick(time=" + this.time + ")";
      }
 
       public int hashCode() { ... }
 
       public boolean equals(@Nullable Object var1) { ... }
    }
 }

而外部子类则自然是定义在 GameAction 抽象类外部。

 public abstract class GameAction {
    ...
 }
 
 public final class Restart extends GameAction {
    @NotNull
    public static final Restart INSTANCE;
 
    private Restart() {
       super(4, (DefaultConstructorMarker)null);
    }
 
    static {
       Restart var0 = new Restart();
       INSTANCE = var0;
    }
 }

文件外扩展子类可想而知。

 public final class TestExtendedSealedClass extends GameAction {
    public TestExtendedSealedClass() {
       super(5, (DefaultConstructorMarker)null);
    }
 }

🏴 Sealed Interface

sealed interface 即密封接口,和 sealed class 有几乎一样的特点。比如:

  • 限制接口的实现:一旦含有包含 sealed interface 的 module 经过了编译,就无法再有扩展的实现类了,即对其他 module 隐藏了接口

还有些额外的优势:

  • 帮助密封类、枚举类等类实现多继承和扩展性,比如搭配枚举,以处理更复杂的分类逻辑

    Additionally, sealed interfaces enable more flexible restricted class hierarchies because a class can directly inherit more than one sealed interface.

    比如 Flappy Bird 游戏的过程中会产生很多 Action 来触发数据的计算以推动 UI 刷新以及游戏的进程,Action 可以用 enum class 来管理。

    其中有些 Action 是关联的,有些则没有关联、不是同一层级。但是 enum class 默认扩展自 Enum 类,无法再嵌套 enum。

    Enum class cannot inherit from classes

    这将导致层级混乱、阅读性不佳,甚至有的时候功能相近的时候还得特意取个不同的名称。

     enum class Action {
         Tick,
         // GameAction
         Start, Exit, Restart,
         // BirdAction
         Up, Down, HitGround, HitPipe, CrossedPipe,
         // PipeAction
         Move, Reset,
         // RoadAction
         // 防止和 Pipe 的 Action 重名导致编译出错,
         // 将功能差不多的 Road 移动和重置 Action 定义加上了前缀
         RoadMove, RoadReset
     }
     
     fun dispatch(action: Action) {
         when (action) {
             Action.Tick -> TODO()
     
             Action.Start -> TODO()
             Action.Exit -> TODO()
             Action.Restart -> TODO()
     
             Action.Up -> TODO()
             Action.Down -> TODO()
             Action.HitGround -> TODO()
             Action.HitPipe -> TODO()
             Action.CrossedPipe -> TODO()
     
             Action.Move -> TODO()
             Action.Reset -> TODO()
     
             Action.RoadMove -> TODO()
             Action.RoadReset -> TODO()
        }
     }

    借助 sealed interface 我们可以给抽出 interface,并将 enum 进行层级拆分。更加清晰、亦不用担心重名。

     sealed interface Action
     
     enum class GameAction : Action {
         Start, Exit, Restart
     }
     
     enum class BirdAction : Action {
         Up, Down, HitGround, HitPipe, CrossedPipe
     }
     
     enum class PipeAction : Action {
         Move, Reset
     }
     
     enum class RoadAction : Action {
         Move, Reset
     }
     
     object Tick: Action

    使用的时候就可以对抽成的 Action 进行嵌套判断:

     fun dispatch(action: Action) {
         when (action) {
             Tick -> TODO()
             
             is GameAction -> {
                 when (action) {
                     GameAction.Start -> TODO()
                     GameAction.Exit -> TODO()
                     GameAction.Restart -> TODO()
                }
            }
             is BirdAction -> {
                 when (action) {
                     BirdAction.Up -> TODO()
                     BirdAction.Down -> TODO()
                     else -> TODO()
                }
            }
             is PipeAction -> {
                 when (action) {
                     PipeAction.Move -> TODO()
                     PipeAction.Reset -> TODO()
                }
            }
             is RoadAction -> {
                 when (action) {
                     RoadAction.Move -> TODO()
                     RoadAction.Reset -> TODO()
                }
            }
        }
     }

🤔 总结

1. Sealed Class & Interface VS Enum

总体来说 sealed class 和 interface 和 enum 有相近的地方,也有明显区别,需要留意:

  • 每个 enum 常量只能以单例的形式存在
  • sealed class 子类可以拥有多个实例,不受限制,每个均可以拥有自己的状态
  • enum class 不能扩展自 sealed class 以及其他任何 Class,但他们可以实现 sealed 等 interface

2. Sealed Class VS Interface

Sealed classes and interfaces represent restricted class hierarchies that provide more control over inheritance.

sealed class 和 interface 都意味着受限的类层级结构,便于在继承和实现上进行更多控制。具备如下的共同特性:

  • 其 sub class 需要定义在同一 Module 以及同一 package,不局限于 sealed 内部或同文件内

看下对比:

Sealed适用/优势原理
Class限制类的扩展abstract class
Interface限制接口的实现 帮助类实现多继承和复杂的扩展性interface


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

带你深入理解Flutter及Dart单线程模型

前言 大家好,我是未央歌,一个默默无闻的移动开发搬砖者~ 众所周知,Java 是一种多线程语言,适量并合适地使用多线程,会极大提高资源利用率和运行效率,但缺点也明显,比如开启过多的线程会导致资源和性能的消耗过大以及多线程共享内存容易死锁。 而 Dart 则是一...
继续阅读 »

前言


大家好,我是未央歌,一个默默无闻的移动开发搬砖者~


众所周知,Java 是一种多线程语言,适量并合适地使用多线程,会极大提高资源利用率和运行效率,但缺点也明显,比如开启过多的线程会导致资源和性能的消耗过大以及多线程共享内存容易死锁


而 Dart 则是一种单线程语言,单线程语言就意味着代码执行顺序是有序的,下面结合一个demo带大家深入了解单线程模型。


demo 示例


点击 APP 右下角的刷新按钮,会调用如下方法,读取一个约 2M 大小的 json 文件。


void loadAssetsJson() async {
var jsonStr = await DefaultAssetBundle.of(context).loadString("assets/list.json");

VideoListModel.fromJson(json.decode(jsonStr));
VideoListModel.fromJson(json.decode(jsonStr));
VideoListModel.fromJson(json.decode(jsonStr));
VideoListModel.fromJson(json.decode(jsonStr));
VideoListModel.fromJson(json.decode(jsonStr));
VideoListModel.fromJson(json.decode(jsonStr));
}

如下图所示,点击刷新按钮之后,中间的 loading 会卡一下。很多同学一看这个代码就知道,肯定会卡,解析一个 2M 的文件,而且是同步解析,主页面肯定是会卡的。

那如果我换成异步解析呢?还卡不卡?大家可以脑海中思考下这个问题。


异步解析


void loadAssetsJson() async {
var jsonStr = await DefaultAssetBundle.of(context).loadString("assets/list.json");

// 异步解析
Future(() {
VideoListModel.fromJson(json.decode(jsonStr));
VideoListModel.fromJson(json.decode(jsonStr));
VideoListModel.fromJson(json.decode(jsonStr));
VideoListModel.fromJson(json.decode(jsonStr));
VideoListModel.fromJson(json.decode(jsonStr));
VideoListModel.fromJson(json.decode(jsonStr));
}).then((value) {});
}


大家可以看到,我已经放在异步里解析了,为什么还是会卡呢?大家可以先思考下这个问题。


前面已经提到了 Dart 是一种单线程语言,单线程语言就意味着代码执行顺序是有序的。当然 Dart 也是支持异步的。这两点其实并不冲突。


Dart 线程解析



我们来看看 Dart 的线程,当我们 main() 方法启动之后,Dart已经开启了一个线程,这个线程的名字就叫 Isolate。每一个 Isolate 线程都包含了图示的两个队列,一个 Microtask queue,一个 Event queue


如图,Isolate 线程会优先执行 Microtask queue 里的事件,当 Microtask queue 里的事件变成空了,才会去执行 Event queue 里的事件。如果正在执行 Microtask queue 里的事件,那么 Event queue 里的事件就会被阻塞,就会导致渲染、手势响应等都得不到响应(绘制图形,处理鼠标点击,处理文件IO等都是在 Event Queue 里完成)。


所以为了保证功能正常使用不卡顿,尽量少在 Microtask queue 做事情,可以放在 Event queue 做


为什么单线程可以做一个异步操作呢?



  • 因为 APP 只有在你滑动或者点击操作的时候才会响应事件。没有操作的时候进入等待时间,两个队列里都是空的。这个时间正是可以进行异步操作的,所以基于这个特点,单线程模型可以在等待过程中做一些异步操作,因为等待的过程并不是阻塞的,所以给我们的感觉就像同时在做多件事情,但自始至终只有一个线程在处理事情。


Future


当方法加上 async 关键字,就代表这个方法开启了一个异步操作,如果这个方法有返回值,就必须要返回一个 Future。


void loadAssetsJson() async {
var jsonStr = await DefaultAssetBundle.of(context).loadString("assets/list.json");

// 异步解析
Future(() {
...
}).then((value) {});
}

一个 Future 异步任务的执行,相对简单。在我们声明一个 Future 之后,Dart 会将异步里的代码函数体放在 Event queue 里执行然后返回。这里注意下,Future 和 then 是放在同一个 Event queue 里的。


假设,我执行 Future 代码之后没有立即执行 then 方法,而是等 Future 执行之后5秒,才调用 then 方法,这时候还是放在同一个 Event queue 里吗?显然是不可能的,我们看一下源码是怎么实现的。


Future<R> then<R>(FutureOr<R> f(T value), {Function? onError}) {
...
_addListener(new _FutureListener<T, R>.then(result, f, onError));
return result;
}

bool get _mayAddListener => _state <= (_statePendingComplete | _stateIgnoreError);

void _addListener(_FutureListener listener) {
assert(listener._nextListener == null);
if (_mayAddListener) {
// 待完成
listener._nextListener = _resultOrListeners;
_resultOrListeners = listener;
} else {
// 已完成
...
_zone.scheduleMicrotask(() {
_propagateToListeners(this, listener);
});
}
}

可以看到 then 方法里有一个监听,Future 执行之后5秒才调用,很明显是已完成状态,走 else 那里的 scheduleMicrotask() 方法,就是说把 then 里面的方法放到 Microtask queue 里。


Future 为何卡顿


再来说一下刚刚的问题,我已经放在异步里解析了,为什么还是会卡呢?


其实很简单,Future 里的代码可能需要执行10s,也就是 Event queue 需要10s才能执行完。那这个10s内其他代码肯定就无法执行了。所以 Future 里的代码执行时间过长,还是会卡 UI 的。


以 Android 为例,Android的刷新频率是60帧/秒,Android系统中每隔16.6ms会发送一次 VSYNC(同步)信号,触发UI的渲染。所以我们就要考虑下,一旦代码执行时间超过16.6ms,到底应不应该放在 Future 里执行?


这时候是不是有同学有疑问,我网络请求也是用 Future 写的,为什么就不卡呢?


这个大家就需要注意一下,网络请求不是放在 Dart 层面执行的,它是由操作系统提供的异步线程去执行的,当这个异步执行完系统又返回给 Dart。所以即使 http 请求需要耗时十几秒,也不会感到卡顿。


compute


既然 Future 执行也会卡顿,那要怎么去优化呢?这时候我们可以开一个线程操作,Flutter 为我们封装好了一个 compute()方法,这个方法可以为我们开一个线程。我们用这个方法来优化一下代码,然后再看下执行效果。


void loadAssetsJson() async {
var jsonStr = await DefaultAssetBundle.of(context).loadString("assets/list.json");

var result = compute(parse,jsonStr);
}

static VideoListModel parse(String jsonStr){
VideoListModel.fromJson(json.decode(jsonStr));
VideoListModel.fromJson(json.decode(jsonStr));
VideoListModel.fromJson(json.decode(jsonStr));
VideoListModel.fromJson(json.decode(jsonStr));
VideoListModel.fromJson(json.decode(jsonStr));
return VideoListModel.fromJson(json.decode(jsonStr));
}


可以看到此时点击刷新按钮,已经不再卡顿了。遇到一些耗时的操作,这确实是一种比较好的解决方式。


我们再看看 DefaultAssetBundle.of(context).loadString("assets/list.json") 方法里面是怎么执行的。


Future<String> loadString(String key, { bool cache = true }) async {
final ByteData data = await load(key);
if (data == null)
throw FlutterError('Unable to load asset: $key');
// 50 KB of data should take 2-3 ms to parse on a Moto G4, and about 400 μs
// on a Pixel 4.
if (data.lengthInBytes < 50 * 1024) {
return utf8.decode(data.buffer.asUint8List());
}
// For strings larger than 50 KB, run the computation in an isolate to
// avoid causing main thread jank.
return compute(_utf8decode, data, debugLabel: 'UTF8 decode for "$key"');
}

从官方源码可以看到,当文件的大小超过 50kb 时,也是采用 compute() 方法开一个线程去操作的。


多线程机制


Dart 作为一个单线程语言,虽然提供了多线程的机制,但是在多线程的资源是隔离的,两个线程之间资源是不互通的


Dart 的多线程数据交互需要从 A 线程传给 B 线程,再由 B 线程返回给 A 线程。而像 Android 在主线程开一个子线程,子线程可以直接拿主线程的数据,而不用让主线程传给子线程。


总结



  • Future 适合耗时小于 16ms 的操作

  • 可以通过 compute() 进行耗时操作

  • Dart 是单线程原因,但也支持多线程,但是线程间数据不互通

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

Kotlin Sequence 是时候派上用场了

前言 在进入Flow世界之前,先来分析Sequence,进而自然延伸到Flow。 通过本篇文章,你将了解到: Java与Kotlin 对集合的处理 Java Stream 的简单使用 Sequence 的简单使用 Sequence 的原理 Sequence...
继续阅读 »

前言


在进入Flow世界之前,先来分析Sequence,进而自然延伸到Flow。

通过本篇文章,你将了解到:




  1. Java与Kotlin 对集合的处理

  2. Java Stream 的简单使用

  3. Sequence 的简单使用

  4. Sequence 的原理

  5. Sequence 的优劣势



1. Java与Kotlin 对集合的处理


场景分析


客户有个场景想考验一下Java和Kotlin:

从一堆数据里(0--10000000)找到大于1000的偶数的个数。


Java和Kotlin 均表示so easy,跃跃欲试。

秉着尊老爱幼的优良传统,老大哥Java先出场。


Java 出场


    public List<Integer> dealCollection() {
List<Integer> evenList = new ArrayList<>();
for (Integer integer : list) {
//筛选出偶数
if (integer % 2 == 0) {
evenList.add(integer);
}
}

List<Integer> bigList = new ArrayList<>();
for (Integer integer : evenList) {
//从偶数中筛选出大于1000的数
if (integer > 1000) {
bigList.add(integer);
}
}
//返回筛选结果列表
return bigList;
}

Java解释说:“先将偶数的结果保存到列表里,再从偶数列表里筛选出大于1000的数。”


Kotlin 出场


Kotlin 看到Java的解决方案,表示写法有点冗余,不够灵活,于是拿出自己的方案:


    fun testCollection() {
var time = measureTimeMillis {
var list = (0..10000000).filter {
it % 2 == 0
}.filter {
it > 1000
}
println("kotlin collection list size:${list.size}")
}
println("kotlin collection use time:$time")
}

Kotlin 说:“老大哥,看看我这个写法,只需要几行代码,简洁如斯。”

Java 淡定到:“确实够简洁,但是表面的简洁掩盖了背后的许多冗余,能一层一层剥开你的心吗?”

Kotlin道:“你我赤诚相对,士为知己者死,刀来!”

Java赶紧递上自己随身携带的水果刀...


Kotlin 反编译


遇事不决反编译:


    public final void testCollection() {
//构造迭代器
Iterable $this$filter$iv = (Iterable)(new IntRange(var8, 10000000));
//构造链表用来存储偶数
Collection destination$iv$iv = (Collection)(new ArrayList());
//取出迭代器
Iterator var13 = $this$filter$iv.iterator();

//遍历取出偶数
while(var13.hasNext()) {
element$iv$iv = var13.next();
it = ((Number)element$iv$iv).intValue();
var16 = false;
if (it % 2 == 0) {
destination$iv$iv.add(element$iv$iv);
}
}

$this$filter$iv = (Iterable)((List)destination$iv$iv);
$i$f$filter = false;
//构造链表用来存储>1000的偶数
destination$iv$iv = (Collection)(new ArrayList());
$i$f$filterTo = false;
//取出迭代器
var13 = $this$filter$iv.iterator();
//遍历链表
while(var13.hasNext()) {
element$iv$iv = var13.next();
it = ((Number)element$iv$iv).intValue();
var16 = false;
if (it > 1000) {
destination$iv$iv.add(element$iv$iv);
}
}

//最终的结果
List list = (List)destination$iv$iv;
}

看到这,Java恍然大悟到:“原来如此,你也是分步存储结果,我俩想到一起了,真机智啊。”

Kotlin:“彼此彼此。”


客户说:“你俩就不要商业互吹了,我就想要一个结果而已,你们就给我弄了两个循环,若是此时我再加一两个条件,你们是不是得要再加几个循环遍历?那不是白白增加耗时吗?”

Java 神秘的笑道:“非也非也,我好歹也是沉浸代码界几十年的存在,早有预案。”

客户说:“那就开始你的表演吧...”


2. Java Stream 的简单使用


什么是流


Java说:“我从Java8开始就支持Stream(流) API了,可以满足你的需求。”

客户不解道:“什么是流?”

Java:“流就是一个过程,比如说你之前的需求就可以当做一个流,可以在中途对流做一系列的处理,而后在流的末尾取出处理后的结果,这个结果就是最终的结果。”

Kotlin补充道:“老大哥,你说的比较抽象,我举个例子吧。”



image.png


在一个管道的入口处放入了各种鱼,如草鱼、鲤鱼、鲢鱼、金鱼等,管道允许接入不同的小管道用以筛选不同组合的鱼类。

比如有个客户只想要金鱼,于是它分别接了4个小管道,第一个管道用来将草鱼分流,第二个管道用来分流鲤鱼,第三个管道用来分流鲢鱼,最后剩下的就是金鱼。

当然,他也可以只分流草鱼,剩下的鲤鱼、鲢鱼、金鱼他都需要,这就增加了操作的灵活性。

客户说:“talk is cheap, show me the code。”


Java Stream


Java 撸起袖子,几个呼吸之间就写好了如下代码:


    public long dealCollectionWithStream() {
Stream<Integer> stream = list.stream();
return stream.filter(value -> value % 2 == 0)
.filter(value -> value > 1000)
.count();
}

客户不解地问:“这确实很简洁了,但是和Kotlin写法一样的嘛?”

Java道:“No No No,别被简洁的外表迷惑了,我们直接来看看处理的耗时即可。”


    public static void main(String args[]) {
Java8Stream java8Stream = new Java8Stream();
//普通集合耗时
long startTime = System.currentTimeMillis();
List<Integer> list = java8Stream.dealCollection();
System.out.println("java7 list size:" + list.size() + " use time:" + (System.currentTimeMillis() - startTime) + "ms");

//Stream API 的耗时
long startTime2 = System.currentTimeMillis();
long count = java8Stream.dealCollectionWithStream();
System.out.println("java8 stream list size:" + count + " use time:" + (System.currentTimeMillis() - startTime2) + "ms");
}

打印结果如下:



image.png


Java 继续解释:“既然只关心最后的结果,那么对于流来说,可以在各个位置指定条件对流的内容进行筛选,对于同一个内容来说只有上一个条件满足了,才会继续处理下一个条件,否则将会处理流里的其它内容。如此一来,再也不用反复存取中间结果了,对于大批量的数据来说,大大减少了耗时。”

客户赞赏:“不错,能解决我的痛点。”

Java 说:“不仅如此,我还可以并行操作流,最后将结果汇总,又可以减少一些耗时了。”

客户:“优秀,那我就选...”

Kotlin 急道:“住口...不,等等,我有话说。”

客户:“你快说,说不出子丑寅卯,我就...”


3. Sequence 的简单使用


Sequence 引入


Kotlin:“和Java老大哥一样,我也可以对流进行操作,主要是用sequence实现”


    fun testSequence() {
var time = measureTimeMillis {
val count = (0..10000000)
.asSequence()//转换为sequence
.filter {
it % 2 == 0//过滤偶数
}.filter {
it > 1000//过滤>1000
}.count() //统计个数
println("kotlin sequence list size:${count}")
}
println("kotlin sequence use time:$time")
}

和未使用sequence 对比耗时:


    public static void main(String args[]) {
SequenceDemo sequenceDemo = new SequenceDemo();
//使用集合操作
sequenceDemo.testCollection();
//使用sequence操作
sequenceDemo.testSequence();
}


image.png


可以看出,使用了sequence后,可以大大减少耗时。


Kotlin 反编译Sequence



image.png


由此可见,并没有对中间结果进行存储遍历,而是通过嵌套调用进而操作流的。


4. Sequence 的原理


集合转Sequence


(0..10000000)

这表示的是0到10000000的集合,它的实现类是:



image.png


IntRange 里定义了集合的开始值和结束值,重点在其父类:IntProgression。

IntProgression 实现了Iterable接口,并实现了该接口里的唯一方法:iterator()

具体实现类为:


internal class IntProgressionIterator(first: Int, last: Int, val step: Int) : IntIterator() {
private val finalElement: Int = last
private var hasNext: Boolean = if (step > 0) first <= last else first >= last
private var next: Int = if (hasNext) first else finalElement

override fun hasNext(): Boolean = hasNext

override fun nextInt(): Int {
val value = next
if (value == finalElement) {
if (!hasNext) throw kotlin.NoSuchElementException()
hasNext = false
}
else {
next += step
}
return value
}
}

通常来说,迭代器有三个重要元素:




  1. 起始值

  2. 步长

  3. 结束值



对应的两个核心方法:




  1. 检测是否还有下个元素

  2. 取出下个元素



对于当前的Int迭代器来说:它的起始值为0,步长是1,结束值是10000000,当我们调用迭代器时就可以取出里面的每个数。


迭代器有了,接下来看看如何构造为一个Sequence。


public fun <T> Iterable<T>.asSequence(): Sequence<T> {
//取当前的迭代器,也就是IntProgressionIterator
return Sequence { this.iterator() }
}
//构造一个Sequence
public inline fun <T> Sequence(crossinline iterator: () -> Iterator<T>): Sequence<T> = object : Sequence<T> {
override fun iterator(): Iterator<T> = iterator()
}

Sequence 是个接口,它的唯一接口是:


public interface Sequence<out T> {
public operator fun iterator(): Iterator<T>
}

结合两者分析可知:



asSequence() 构造了Sequence匿名内部类对象,而其实现的方法就是iterator(),该方法最终返回IntProgressionIterator 对象
也就是说Sequence初始迭代器即为Collection的迭代器



Sequence中间操作符


以filter为例:


public fun <T> Sequence<T>.filter(predicate: (T) -> Boolean): Sequence<T> {
//构造Sequence 子类,该子类用来过滤流
return FilteringSequence(this, true, predicate)
}

override fun iterator(): Iterator<T> = object : Iterator<T> {
//上一个Sequence的迭代器
val iterator = sequence.iterator()
var nextState: Int = -1 // -1 for unknown, 0 for done, 1 for continue
var nextItem: T? = null

private fun calcNext() {
//先判断上一个Sequence的迭代器
while (iterator.hasNext()) {
val item = iterator.next()
//拿到值后判断本Sequence的逻辑
//是否符合过滤条件,符合就取出值,交个下一个条件,不符合则找下一个元素
if (predicate(item) == sendWhen) {
nextItem = item
nextState = 1
return
}
}
nextState = 0
}

//重写next()与hasNext(),里边调用了calcNext
override fun next(): T {
if (nextState == -1)
calcNext()
if (nextState == 0)
throw NoSuchElementException()
val result = nextItem
nextItem = null
nextState = -1
@Suppress("UNCHECKED_CAST")
return result as T
}

override fun hasNext(): Boolean {
if (nextState == -1)
calcNext()
return nextState == 1
}
}

我们调用了两次filter操作符,最终形成的结构如下:



image.png


此处用到了设计模式里的装饰模式:




  1. Sequence 只有普通的迭代功能,现在需要为它增强过滤偶数的功能,因此新建了FilteringSequence 对象A,并持有Sequence对象,当需要调用过滤偶数的功能时,先借助Sequence获取基本数据,再使用FilteringSequenceA过滤偶数

  2. 同样的,还需要在1的基础上继续增强FilteringSequence的过滤功能,再新建FilteringSequence B持有FilteringSequence对象A
    A,当需要调用过滤>1000的数时,先借助FilteringSequence 对象A获取偶数,再使用FilteringSequenceB过滤>1000的数



如此一来,通过嵌套调用就实现了众多操作过程。


Sequence终端操作符


你可能已经发现了:中间操作符仅仅只是建立了装饰(引用)关系,并没有触发迭代啊,那什么时候触发迭代呢?

这个时候就需要用到终端操作符(也叫末端操作符)。

比如count方法:


    public fun <T> Sequence<T>.count(): Int {
var count = 0
//触发遍历,统计个数
for (element in this) checkCountOverflow(++count)
return count
}

当调用了count()方法后,将会触发遍历,最终调用栈如下:



image.png


只有调用了终端操作符,流才会动起来,这也就是为啥说Sequence、Java Stream 中间操作符是惰性操作符的原因。


Sequence与普通集合链式调用区别


还是之前的Demo

普通集合链式调用



image.png


每次操作(如filter)都需要遍历集合找到符合条件的条目加入到新的集合,然后再在新的集合基础上再次进行操作。

如上图,先执行紫色区块,再执行蓝色区块。


Sequence 调用



image.png


每次先对某个条目进行所有的操作(比如filter),先判断每一步该条目是否符合,不符合则再找下一个条目进行所有的操作。

如上图:从左到右按顺序执行紫色区块。


5. Sequence 的优劣势


与普通集合链式调用对比,Sequence也有链式调用。

前者链式调用每次都需要完整遍历集合并将中间结果缓存,下一次调用依赖上一次调用缓存的结果。

而后者链式调用先是将每个操作关联起来,然后当触发终端操作符时针对每一个条目(元素)先执行所有的操作(这些操作在上一步已经关联)。

由此可见,如果集合里元素很多,Sequence可以大大节约时间(没有多次遍历,没有暂存结果)


除此之外,Sequence 只做最少的操作,尽可能地节约时间。

怎么理解呢?还是上面的例子,我们只想取前10个偶数,代码如下:


    fun testSequence1() {
var time = measureTimeMillis {
val count = (0..10000000)
.asSequence()//转换为sequence
.filter {
it % 2 == 0//过滤偶数
}.take(10).count()
println("kotlin sequence1 list size:${count}")
}
println("kotlin sequence1 use time:$time")
}

该序列只会执行到集合里的条目=18就终止了,因为到了0~18已经有10个偶数了,而剩下的一堆条目都无需执行,大大节省了时间。


因此,Sequence 优势:




  1. 不暂存数据,不进行多次遍历

  2. 只做最少的操作

  3. 可以产生无限的序列



以上以filter/take/count 操作符阐述了Sequence的原理及其优势,当然还有更多的具体的使用场景/方式待挖掘。


此时,Kotlin 迫不及待跳出来说:“怎么样,我这个Sequence 6吧?”

客户说:“看你字多的份上,我选择信你,那我就选...”

Java急忙道:“我有问题,我的Stream支持并行,你支持吗?”

Kotlin:“...”

想了一会儿,Kotlin继续道:“Sequence 虽然不支持切换线程,但是它的兄弟支持,它就是Flow。”

Java补充说:“你有Flow,我有LiveData,那我俩继续PK?”

没等Kotlin回话,客户急忙道:“哎哎,行了,时间不够了,下次再继续吧,散会...”

Java:“...”

Kotlin:“...”


第100篇博客,不忘初心,砥砺前行,继续输出高质量、成体系的博客。

下次将会进入Flow的世界。


本文基于Kotlin 1.5.3,文中完整Demo请点击


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

uni-app跨端开发之疑难杂症

今年,公司决定解决各个团队移动端开发的混战局面,由架构部出一套移动端框架,规范化开发标准。经过一段时间的调研,考虑到跨端以及公司主要技术栈为vue,最终选择了uni-app作为移动端框架,在大家都“很忙”的情况下,我成为了移动端框架的主要开发。以前就总听同事说...
继续阅读 »

前言

今年,公司决定解决各个团队移动端开发的混战局面,由架构部出一套移动端框架,规范化开发标准。经过一段时间的调研,考虑到跨端以及公司主要技术栈为vue,最终选择了uni-app作为移动端框架,在大家都“很忙”的情况下,我成为了移动端框架的主要开发。以前就总听同事说,uni-app有很多坑,我对其也只是有些许了解,这回的全身心投入,才知道一入深坑愁似海

这段时间也做了一些成效,头大如斗的路由拦截、必不可少的http请求封装、提高成效的组件库、仿照微信的oAuth 2.0登录、复杂逻辑的离线存储、用户需要的增量更新包

有成效也踩了一些坑,百思不得解的console.log、烦到吐血的网络调试、爬坑许久的APP与h5通讯、性能极差的微信小程序端uni.canvasToTempFilePath

今天就要聊聊一些疑难杂症,有些忘记了,有些还没碰到,后续持续更新吧!

百思不得解的console.log

移动端框架是采用npm包的方式提供给业务部门使用,其中包含oAuth2.0登录方式,这其中涉及到了h5通过scheme协议唤醒app并且带回code等参数,相应的参数会存放在plus.runtime.arguments,其他情况下,plus.runtime.arguments的值为空。在给同事排查问题时我就简单操作,在node_modules对应的npm包里面写了不是很严谨的如下代码:

const args = plus.runtime.arguments;
// 这个是业务部门出错时,我添加的调试代码
console.log('>>>>>>'args)
if (args) {
 const isLogout = args.includes('logout');
 if (isLogout) {
   await this.handleSession();
else {
   await this.handleAuthorization(args);
}
}

我测试是正常的,args是空值,所以是不会执行if内的逻辑的,但是他这边会执行if内的逻辑的,初步判断args由于某个原因导致存在值了,为了简单明了的查看输出内容,然后我就写了毁一生的console.log('>>>>>>', args),这行调试代码的输出内容如下,我一直以为args是空值,但是判断依旧为true,有点颠覆了我的人生观,后来灵机一动,删掉了第一个修饰参数,发现args原来是有值的,经过排查,是因为添加了微信小程序打开指定页面,导致记录当前页面数据。


烦到吐血的网络调试

网络调试对于我们的日常开发是很重要的,有助于快速判断资源请求问题,但uni-app在这方面有很大的缺陷,在讨论这个问题时,先来看一下uni-app的真机调试方式。

终端调试工具

当项目运行时,点击终端上的调试按钮,会弹出一个调试界面。


从调试面板中,可以看到仅有ConsoleElementsSources三个选项,期待许久的Network并没有出现,这种调试方式没办法实现网络请求调试。


webview调试控制台

点击工具栏的 运行 -> 运行到手机或模拟器 -> 显示webview调试控制台 会出现一个跟谷歌浏览器一样的调试界面,虽然这里有Network,但是很可惜,这个功能存在问题,没办法监听到网络请求。


Fiddler 抓取网络请求

在走投无路之下,只能另辟蹊径,借助工具,抓取真机的网络请求,接下来阐述一下怎么使用Fiddler抓取真机的网络请求,配置完需要重启才生效。

下载Fiddler

这是一个免费工具,自行在网络上下载即可。

Fiddler 基础配置

点击工具栏的tools,选择options就会弹出一个配置界面



HTTPS 配置

选择HTTPS选项,勾选选矿中的Capture HTTPS CONNECTsDecrypt HTTPs trfficIgnore server certificate errors


Connections 配置

这边配置的端口号后面配置代理的时候需要使用到。


手机配置代理

注意需要和电脑连接同一网络,点击进入手机WIFI详情界面,有个代理,选择手动模式,输入电脑的IP地址和Fiddler的监听端口,即可拦截到真机的所有网络请求,包含我们app对应的网络请求。


过滤

这边可以选择过滤对应的ip或域名,多个的话通过分号隔开即可。


爬坑许久的APP与h5通讯

谈论这个问题时,先描述一下uni-app实现的app怎么和h5通讯

app端

对于app端的通讯,.vue.nvue有两点区别,1. 获取webView实例不一致,2. 监听方法不一致。app向h5传递数据时,需要借助webview.evalJS执行h5的全局方法,而h5向app传递参数时,类似于h5发送postMessage,可以在webview的message/onPostMessage监听函数获取数据。

vue

获取webView示例

webView实例的获取,对于vue文件不是特别友好,需要借助于this.$scope.$getAppWebview(),如果是在组件中需要使用this.$parent.$scope.$getAppWebview(),添加延时的原因是,h5页面可能未加载完成,无法获取到对应的全局函数,会提示xxx函数undefined;

<template>
   <web-view src="http://www.juejin.com"></web-view>
</template>
<script>
   export default {
       onReady() {
           const currentWebview = this.$scope.$getAppWebview();
           const account = '清欢bx'
           setTimeout(() => {
               const webView = currentWebview.children()[0];
               webView.evalJS(`setAccountInfo(${account})`);
          }, 1000);
      }
  }
</script>

监听方法

vue文件采用@message触发监听函数

<template>
   <web-view @message="handleMessage" src="http://www.juejin.com"></web-view>
</template>
<script>
   export default {
       methods: {
           handleMessage(data) {
               console.log(data)
          }
      }
  }
</script>

nvue

获取webView示例

在nvue获取webView实例就很流畅了,直接通过this.$refs.webview就能获取到。

<template>
   <web-view ref="webview" src="http://www.juejin.com"></web-view>
</template>
<script>
   export default {
       onReady() {
           const account = '清欢bx'
           this.$refs.webview.evalJs(`setAccountInfo(${account})`);
      }
  }
</script>

监听方法

nvue文件采用@onPostMessage触发监听函数

<template>
   <web-view @onPostMessage="handleMessage" src="http://www.juejin.com"></web-view>
</template>
<script>
   export default {
       methods: {
           handleMessage(data) {
               console.log(data)
          }
      }
  }
</script>

h5 端

发送数据

需要引入一个uni-app的sdk,uni.webview.1.5.4.js,最低版本需要1.5.4,可以在index.html引入,也可以在main.js引入,注意点是传递的参数必须写在data里面,也就是维持这样的数据结构。

uni.postMessage({
   data: {
     xxxxxx,
     xxxxxx
  }
});

如果是页面加载完成时就需要发送数据,需要等待UniAppJSBridgeReady钩子结束后触发postMessage;

<script>
   export default {
       mounted() {
           document.addEventListener('UniAppJSBridgeReady'function() {
               uni.webView.getEnv(function(res) {
                   console.log('当前环境:' + JSON.stringify(res));
              });
               uni.postMessage({
                   data: {
                     action'message'
                  }
              });
          });
      }
  }
</script>

如果是通过事件点击发送数据,因为这时候页面已经加载完成,不需要再去监听UniAppJSBridgeReady钩子,直接触发uni.postMessage即可。

<template>
   <view>
       <button @click="handlePostMessage">发送数据</button>
   </view>
</template>
<script>
   export default {
       methods: {
           handlePostMessage() {
               uni.postMessage({
                   data: {
                     action'message'
                  }
              });
          }
      }
  }
</script>

获取数据

获取数据的函数,需要挂载到window上,可以直接写在main.js里面,数据需要共享到具体页面内,可以使用本地村存储localStorage、事件总线eventBusvuex,根据自己的需求选择。

window.setAccountInfo = function(data) {
   console.log(data)
}

踩坑点

uni is not defined

app需要涉及到离线或者内网,索引uni.webview.js下载到本地进行引入,因为uni.webview.js已经被编译成了umd格式,在vue项目中在进行一次打包后,导致this指向不是window,所以没有把uni挂在到全局上,将this指向改为window即可。

未改造之前的代码


改造后


或者


app向h5传递参数时,无法传递对象,并且传递的参数需要字符串序列化

在传递参数时,对象传递过去没办法识别,同时传递的参数需要执行JSON.stringify(),多个参数时,可以多个参数传递,也可以把多个参数进行字符串拼接,然后再h5端进行拆分处理。

const { accountpassword } = accountInfo;
const _account = JSON.stringify(account);
const _password = JSON.stringify(password);
setTimeout(() => {
   const webView = currentWebview.children()[0];
   webView.evalJS(`setAccountInfo(${_account}, ${_password})`);
}, 1000);

四、性能极差的canvas转图片

自定义组件库里包含手写签名组件,需要用到uni.canvasToTempFilePathcanvas转成图片,这个方法的生成基础图片大小是根据当前屏幕分辨率,在模拟器上运行感觉性能还可以,但是在真机上的性能不高,如果笔画多的话,有时需要十几秒时间,这是没办法接受的,不过也有解决方式,可以通过设置destWidthdestHeight来自定义图片生成的大小,牺牲一些图片清晰度,来提高性能。

uni.canvasToTempFilePath(
  {
     canvasIdthis.canvaId,
     destWidththis.imgWidth,
     destHeightthis.imgHeight,
     success: (res) => {
       console.log('success')
    },
     fail(e) {
       console.error(e);
    },
  },
   this,
);

小结

我目前主要负责公司uni-app移动端框架的开发,包含组件库相应的生态工具多端适配离线存储hybrid,如果你也正在做相同的事,或者在使用uni-app开发,或者在学习uni-app都可以相互探讨,在这踩坑的过程中,我会持续完善此系类文章,帮助大家和自己更好的使用uni-app开发项目,fighting~

作者:清欢bx
来源:juejin.cn/post/7156017191169556511

收起阅读 »

三个多月,被现实雪藏了的锐气

距离 7.15 已经过去了三个多月。体内的热情不能说熄灭,但不足以点燃残留的激情。三个多月不算长,没能把梦想耗尽,遗憾变少;也不算短,没能让想法付出实践,自由变得可贵。回看自己的脚印,有延续,也有分叉,但只有少数几步能够留下青草的芳香,大部分脚印风干后让人难以...
继续阅读 »

距离 7.15 已经过去了三个多月。体内的热情不能说熄灭,但不足以点燃残留的激情。三个多月不算长,没能把梦想耗尽,遗憾变少;也不算短,没能让想法付出实践,自由变得可贵。回看自己的脚印,有延续,也有分叉,但只有少数几步能够留下青草的芳香,大部分脚印风干后让人难以辨认,甚至不相信是从前的自己。像溅到了油渍的白衬衫,但不同的是,白衬衫还有机会回到出厂时的样子。

二十多岁也不是一个少不经事的年纪了,随着自我意识变强,属于自己的世界观正在构建。慢慢的对生活、工作、理想有了自己的看法。当发现课本上所教授的和社会需要你所掌握的相差甚远,自己也就有了想逃避的情绪。不愿随波逐流,像片枯黄的叶子飘落在水中,没有方向。路人不会因为一片叶子而驻足,或许有少部分人会感叹,在寒冬里积极汲取的养分敌不过春去秋来的自然规律和经典力学中的万有引力。


手的一个作用就是捂住耳朵,隔绝噪音

三个多月的时间,锻炼了写作,结识了新朋友,捡起了读书时最爱的篮球,经历疫情,重温友情,放空自己。

惊喜

写作是带来惊喜最大的尝试,分享技术的同时加深了对于某项知识点的理解,并且还能提升自己的文笔,让自己在面试中多一个加分项。在这个过程中,自己还加了一位远在厦门独立开发者的微信,大家分享想法,交流行业的运作和进程,交换资源,挺好。虽然写作没能带来实际的收益,但也让我在点滴生活中找到一点满足感。在这也想问下,大家最近是都成了 优秀毕业生吗 ?两个多月前写的一篇 一位 98 年程序员离职后 的阅读量和点赞量最近又多了起来,是不是当看到 离职 这个字眼,都想点进去看两眼。当然我也希望各位能从中找到了一些方向和归属感,让自己疲惫的心能得到片刻的缓解。毕竟大家在两微一抖上看到了很多的精英人士,觉得全世界都在挣着自己的钱。

友情

联系了初中同学、大学同学。先说说初中同学,距今认识已超过 10 年。初中时,在同一屋檐下共同生活了三年,也有幸 “同床异梦” 过。到现在也还记得初中时那啼笑皆非的日子,一起罚站过,像展品一样立在操场被路过的人打量;一起挑灯夜聊,讲着班里的女生和男生,调侃授课老师,一遍遍模仿着他们略带喜感的动作和回荡在耳边的经典语录;期末前在宿舍厕所嚼着白加黑,背着让人痛不欲生的课文;上午的最后一声下课铃响后,等在对方的桌前或课室外,一起走向校园中承载了不少话题量的饭堂。

毕业后的初中同学也还是会有联系。前些日子和他们吃了几顿饭。我想想啊,有东南亚菜、江浙菜、新疆菜、顺德菜、粤菜。对这几个菜系排序,粤菜 = 顺德菜 > 东南亚菜 > 新疆菜 > 江浙菜。


西湖龙井虾,姿色不错,口感上个人认为没什么很特别的


顺德醉鹅,肉多,对得起这价


新疆烤包子,牛肉馅的,皮薄馅多,不错


罗布泊烤鱼,唯一一道让我们产生分歧的菜,只因我只吃了我面前的那面,他吃另一面


咕噜肉,酸甜的口感,不腻

一个进入了广告业,刚找到了工作,结束了家里蹲的日子;另一个成了编导,不加班,目前积累作品中;而我正试图通过写作和运动摆脱焦虑和迷茫带来的副作用。

大学同学当了两年兵,九月份退伍回来。外表变化不大,但性格变了挺多。大学做事时会想的比较多,属于那种给了机会都要思前想后的,现在呢?没有机会,创造机会都要上,胆子大了不少。在兵营里不仅身体得到了锻炼,心态更是被蹂躏到需要推倒重建。跟他交流时,能感觉到和社会脱离的有点久,有些事情想的过于简单。但对朋友,他还像从前那样。

篮球

女篮世界杯夺得了第二名的好成绩,国庆在家看了世界杯的几场比赛。比男篮强的不是一星半点,不管是队内的配合、队员的基本功、防守和进攻的态度都让我感觉女篮未来可期。这次世界杯也让我回忆起以前那块让我无比留恋的场地,不管是水泥地还是塑胶地,篮架是崭新还是磨损,边界是清晰还是模糊,这些元素加起来都足以让那时的我顶着烈日,不厌其烦的追逐着那颗用青春编织起的篮球,一上一下也像极了年轻人那有力的心跳。


我追过的人不多,但追过的球,不少

上班后打球的时间呈指数级下降。原以为对篮球的热爱就到这了,但把手从键盘放到篮球上时,体内有关它的一切都被唤醒了。


灯光一点都不耀眼

疫情后踏上球场,竟然有种疫情从未发生过的错觉。不用戴口罩,每个人分享着球权,对抗时肌肉之间的碰撞让人忘记了在这两年里不停被提起的一米安全线。如果有读者也在广州,也爱打球,可以私信我约场球,让自己痛快一场,酣畅淋漓。

放空自己

脑子空着的时候,大部分想的是创业的东西。创业这个想法从大学时期就有了,但不具备所需的条件,于是一门心思的想横向扩展。结果出来后又开始纠正大学时的想法,横向扩展行不通,个人认为一万小时定律忽略了实际环境对结果的影响,于是决定踹开这扇门。鉴于本人对于创业还是萌新一个,就不花篇幅了。有兴趣的,私下交流。

放空自己的时候,除了想创业,也想过其他东西。比如,自己的优势是啥,如何能把优势更好的发挥出来,如何说服自己目前是个平凡人的事实,为啥 boss 直聘上一堆已读未回等等。

作者:对方正在输入
来源:juejin.cn/post/7158817364467777550

收起阅读 »

离职交接,心态要好

话说今年经历了几次项目交接?主动和被动的都算!01实在是没想到,都到年底快收尾的时候,还要突然接手离职人员的项目;不断拉扯和管理内心情绪,避免原地裂开;年度中再次经历突发的交接事宜,并且团队要在极短的时间内完成所有事项的交接流程;毫无征兆的变动必然会引起一系列...
继续阅读 »

话说今年经历了几次项目交接?主动和被动的都算!

01

实在是没想到,都到年底快收尾的时候,还要突然接手离职人员的项目;

不断拉扯和管理内心情绪,避免原地裂开;

年度中再次经历突发的交接事宜,并且团队要在极短的时间内完成所有事项的交接流程;

毫无征兆的变动必然会引起一系列问题,最直接的就是影响团队现有节奏进度,需要重新调整和规划;

人员的小规模变动,对部门甚至公司产生的影响是显而易见的,道理都懂;

但是从理性上思考,这个问题并非是无解的,是可以在各个团队中,进行内部消化的;

而人力减少带来的成本降低,以及确保公司的可持续,这是极具确定性的,也是核心目的;

所以感性上说,这个梦幻的职场,可能真的是"爱了";

02

如果是常规情况下的离职流程,交接并不是一件复杂的事情,因为有时间有心情来处理这事,好聚好散;

然而最骚的是,奇袭一般的裁员手段,几分钟谈话结束直接走人;

丝毫不顾及由此带来的影响,认定留下的人应该兜底相应的责任,实现无缝接坑;

当然并不是什么公司都有底气这么做的,大部分还是在裁员通知后,留有一定的时间处理交接事项;

对于交的过程是否有质量,完全看接的一方是否聪明;

从感性上分析,都已经被裁了自然要牢牢把握摸鱼的机会,根本不会在意交出的事项谁来维护,不反越防线就不错了;

而压力会直接传送后闪现到接的人正上方;

03

面对被动离职的交接,确实很难妥善处理,情绪化容易导致事情变质,能真正理性对待的并不多;

交接涉及到三方的核心利益:公司、交出人、接手人,不同角度对待这件事件,态度完全不同;

公司,并不关心交接的质量,只要项目有人兜底即可;

交出方,感性上说直接敷衍交接单上的流程即可,并不在意后续的影响;

接手方,项目交接完成后的第一责任人,可能会关心项目的质量状况;

至于说接手的人能否有时间,有能力,有心情接下这种天降大任,可能除了自己以外,不到出问题的时候关注的很少;

因为项目交接过程没有处理好,从而导致后续的事故与甩锅,情绪化的现象并不少见;

如果是在内部矛盾突出的团队中,由此引发的离职效应也并不少见;

04

人的情绪真的是很奇怪,能让复杂的事情变的简单,也能让简单的事情变的离谱;

情绪上头的时候,事情本身是否真的复杂就已经不太重要了;

接手方最大的问题在于吃力不讨好,如果接了一个质量奇差的项目,意味之后很长一段时间内,工作状态都会陷入混乱的节奏中;

对于大部分研发团队来说,都是存在排期规划的,如果被交接的项目横插一脚,重新调规划影响面又偏大;

向上反馈,多半是回答一句:自行消化;

何谓自行消化,就是占用空闲时间处理,比如下班后,比如周末,比如摸鱼,这些都是对工作情绪的持续伤害;

最终兜底的个人或者团队,可能需要带着夜宵去公司搬砖;

05

吐槽归吐槽,裂开归裂开,成熟的搬砖人不该表现出明显的情绪化;

先捋一捋在面对离职交接时的注意事项,虽然说离职后有一个过渡期,但是真正涉及交接的时间通常一周左右;

作为接手一方,自然期待的是各种文档齐全,对于坑坑洼洼的描述足够清楚;

然而对于被离职的交出方,会带着若隐若现的情绪化状态,很难用心处理交接事项,能不挖坑就已经是良心队友了;

接手方作为后续的兜底人员,兜不住就是一地鸡毛;

如果兜住了呢?那是职责所在、理所应当、不要多想、安心搬砖;

06

面对项目交接,这种隔三差五个月就会突发的事,完全可以用一套固定的模式和节奏去执行;

强烈建议:不排斥、不积极、不情绪化;

但是在处理的过程中要理性且严谨,这样可以规避掉许多可能出现的麻烦,毕竟签了交接单,从此该项目问题根本甩不开;

职场几年,在多次"交"与"接"的角色转换过程中,总结以下几点是研发需要注意的;

P1:文档,信息的核心载体;

不管项目涉及多少文档,照单全收;

如果文档严重缺失甚至没有,直接在交接单上写明情况,并且得加粗划重点展示;

文档和项目的维护极有可能是线性不相关,但是手有文档心里不慌,因为方便后续再把项目交接给其他人;

所以,敷衍一时爽,出事火葬场;

07

P2:代码工程,坑与不坑全看此间;

接到手里的项目,是否会导致情绪崩塌,全看项目代码工程的质量,遇上一堆烂摊子,心情会持续的跌跌跌,然后裂开;

直接把人打包送走的情况也并不少见;

如果代码工程质量极高,架构设计稳定,组件集成比较常规,分包井然有序,悬着的情绪可以适当下落;

P3:库表设计,就怕没注释;

对于数据库层面的设计,与代码工程和业务文档三者相辅相成,把握其中的主线逻辑即可;

但前提是表的设计得有清晰的注释,如果是纯中式英文混搭拼音,且缺乏注释,必然会成为解决问题的最佳卡点;

P4:核心接口,应当关注细节;

从项目的核心业务中选出2-3个复杂的接口读一读;需要将注意点放在细节逻辑上,给内心积蓄一丢丢解决问题的底气;

熟悉接口的基本思路:请求从客户端发出,业务服务的处理逻辑,对数据层面的影响,最终响应的主体;

08

P5:遗留问题,考验职场关系的时候到了;

公司一片祥和的时候,员工之间还可以做做样子;

但是已经走到了一别两宽的地步,从感性上来说只要不藏着掖着就行,还想窥探别人安稳摸鱼的秘密,确实想的不错;

老练的开发常干的事,为了解决某个问题临时上线一段代码,处理好后关闭触发的入口,但是会保留代码主体;

这还算常规操作,最骚的是在本地写一段脚本工具解决线上的问题;

这些隐藏的接口和脚本只有开发的人自己清楚,如果不给个说明文档,这不单是挖坑,还顺手倒了一定比例的水进行混合;

P6:结尾事项,寒暄几句还是要的;

安全意识好的公司,会对员工的账号权限做好备份,以便离职时快速处理,不会留下风险隐患;

在所有权限关闭之后,接手人就可以在交接单上完成签字仪式;

交接完成后还是得适当的寒暄几句,万一接了个坑,转头就得再联系也不稀奇,所以职场留一线方便语音再连线;

09

年度收到的离职交接,已经累计好几份,对这种事情彻底麻了;

事来了先兜着,等兜不住的时候自然会有解决办法;

抗拒与烦躁都不会影响流程的持续推进,这种心态需要自己用清醒的意识不断的说服自己;

最后想探讨一个话题,跟项目前负责人联系,用什么话术请教问题,才能显得不卑不亢?

作者:知了一笑
来源:juejin.cn/post/7157651258046677029

收起阅读 »

Flutter之事件节流、防抖封装

在应用开发过程中经常会遇到因用户短时间内连续多次重复触发某个事件,导致对应事件的业务逻辑重复执行而出现业务异常,此时就需要对事件进行节流或者防抖处理避免出现业务异常。本文将介绍在 Flutter 开发中如何实现节流和防抖的统一封装。 前言 首先我们来了解一下节...
继续阅读 »

在应用开发过程中经常会遇到因用户短时间内连续多次重复触发某个事件,导致对应事件的业务逻辑重复执行而出现业务异常,此时就需要对事件进行节流或者防抖处理避免出现业务异常。本文将介绍在 Flutter 开发中如何实现节流和防抖的统一封装。


前言


首先我们来了解一下节流和防抖的定义,以及在什么场景下需要用到节流和防抖。


节流


节流是在事件触发时,立即执行事件的目标操作逻辑,在当前事件未执行完成时,该事件再次触发时会被忽略,直到当前事件执行完成后下一次事件触发才会被执行。


throttle.png


按指定时间节流


按指定时间节流是在事件触发时,立即执行事件的目标操作逻辑,但在指定时间内再次触发事件会被忽略,直到指定时间后再次触发事件才会被执行。


throttle-timeout.png


防抖


防抖是在事件触发时,不立即执行事件的目标操作逻辑,而是延迟指定时间再执行,如果该时间内事件再次触发,则取消上一次事件的执行并重新计算延迟时间,直到指定时间内事件没有再次触发时才执行事件的目标操作。


debounce.png


使用场景


节流多用于按钮点击事件的限制,如数据提交等,可有效防止数据的重复提交。防抖则多用于事件频繁触发的场景,如滚动监听、输入框输入监听等,可实现滚动停止间隔多久后触发事件的操作或输入框输入变化停止多久后触发事件的操作。


效果


先看一下最终封装完成后的使用示例及效果,实现计数器功能,对点击分别进行节流、指定时间节流、防抖限制。


/// 事件目标操作
void increase() {
count += 1;
}

/// 节流
() async{
await Future.delayed(Duration(seconds: 1));
increase();
}.throttle()

/// 指定时间节流
increase.throttleWithTimeout(2000)

///防抖
increase.debounce(timeout: 1000)


increase 是事件目标操作,即这里的数字加一,分别进行节流、指定时间节流、防抖限制,调用封装的 throttlethrottleWithTimeoutdebounce 扩展方法实现。其中节流为了模拟事件耗时操作增加了一秒延迟。


实现效果:


flutter-throttle-demo.gif


实现


接下来将通过从单事件的节流/防抖限制到封装抽取一步一步实现对节流和防抖的通用封装。


简单节流实现


首先来看一下节流的简单实现,前面讲了节流的原理,就是在事件未执行完成时忽略事件的再次触发,根据这个原理添加一个变量标识事件是否可执行,默认为 true 可执行,当事件执行时设置为 false,执行完成后重新设置为 true,当标识为 false 时忽略事件,这样就实现了对事件的节流,代码实现如下:


Future increase() async{
count += 1;
await Future.delayed(const Duration(seconds: 1));
}

/// 事件是否可执行
bool enable = true;

void throttleIncrease() async{
if(enable){
enable = false;
await increase();
enable = true;
}
}

添加一个 enable 变量标识事件是否可执行。这里为了模拟事件的耗时操作在 increase 方法里添加了一秒的延时。这样就简单实现了事件的节流,运行看一下效果:


flutter-throttle-simple.gif


节流封装


通过上面的简单代码实现了对事件的节流,但是只对某一个确定的事件有效,如果还有其他事件也需要实现节流效果那就得重新写一遍上面的代码,这样很明显是不科学的。那么我们就需要对上面的代码进行封装,使其能应用到多个事件上。


上面的代码事件调用是直接写在节流的实现里的,那么将事件进行抽象,把事件的具体执行方法抽取为一个参数,这样就能满足多个事件的节流控制了,实现如下:


bool enable = true;
void throttle(Function func) async{
if(enable){
enable = false;
await func();
enable = true;
}
}

/// 使用
throttle(increase);

throttle(decrease);

进一步封装


经过前面的封装后,确实可以对多个事件进行节流限制,但在实际开发过程中发现有两个问题:


**问题一:**所有事件的节流控制使用的是一个 enable 变量控制,这样就会导致在事件 1 执行过程中事件 2 会被忽略,这显然不是我们想要的效果。


举一个典型的场景,在 Flutter 中跳转新页面并获取页面的返回值,此时实现如下:


Future toNewPage() async{
var result = await Navigator.pushNamed(context, "/newPage");
/// do something
}

此时如果对 toNewPage 进行节流控制,并且跳转的页面里的按钮事件也做了同样的节流控制,就会导致新界面的按钮事件无法执行,因为我们节流用的是同一个变量进行控制,而 toNewPage 需要接收页面返回值,事件未执行完一直在等待页面返回值导致 enable 变量一直为 false 所以新界面的点击事件就会被忽略。


**问题二:**当事件的执行报错,会导致后续所有使用该方式节流的事件都不会被触发。原理跟上面的一样,当事件执行报错时不会继续向下执行,此时 enable 无法赋值为 true,一直为 false 从而导致后续事件都不会被执行。


怎么解决上面两个问题呢?首先解决简单的问题二,问题二很好解决,加一个 try-catch-finally 即可:


void throttle(Function func) async{
if(enable){
enable = false;
try {
await func();
} catch (e) {
rethrow;
} finally {
enable = true;
}
}
}

在方法调用上增加 try-catch-finally ,在 finally 中将 enable 设置为 true,在 catch 中不对异常做任何处理,使用 rethrow 将异常重新抛出去即可,这样就解决了问题二。


再来看问题一,既然使用同一个 enable 会有问题,那就使用多个变量来控制,每个事件用一个 enable 变量来控制,实现如下:


Map _funcThrottle = {};

void throttle(Function func) async{
String key = func.hashCode.toString();
bool enable = _funcThrottle[key] ?? true;
if(enable){
_funcThrottle[key] = false;
try {
await func();
} catch (e) {
rethrow;
} finally {
_funcThrottle.remove(key);
}
}
}

使用一个 Map 来存放事件的 enable 变量,使用事件方法的 hashCode 作为事件的 key,这样就解决了问题一。


但实际开发过程中发现还是有问题,封装后的 throttle 方法在使用时有下面两种方式:


/// 1
Future increase() async{
count += 1;
await Future.delayed(const Duration(seconds: 1));
}

throttle(increase);

/// 2
throttle(() async{
count += 1;
await Future.delayed(const Duration(seconds: 1));
});

使用第一种方式时是没有问题,但是第二种发现就有问题,节流不起作用了,为什么呢?是因为第二种使用的是匿名函数或者叫 lambda 函数,这种方式每次触发事件相当于都会重新创建一个函数参数传入 throttle 就会导致 func.hashCode.toString() 获取的值每次都不一样,所以导致节流无效。


那这种情况又该怎么解决呢?首先想到的是给 throttle 增加一个参数 key ,不同的事件传入不同的 key 值。这样确实能解决问题,但是增加了使用成本,每个事件都得传入一个 key,对于已有代码改造也相对来说不方便。于是想到了另外一种解决办法,也是本方案最终实现的方法,用一个对象来代理执行事件,具体实现如下:


class FunctionProxy {
static final Map _funcThrottle = {};
final Function? target;

FunctionProxy(this.target);

void throttle() async {
String key = hashCode.toString();
bool enable = _funcThrottle[key] ?? true;
if (enable) {
_funcThrottle[key] = false;
try {
await target?.call();
} catch (e) {
rethrow;
} finally {
_funcThrottle.remove(key);
}
}
}
}

创建一个方法的代理类,在该类里实现 throttle ,此时使用的 key 为该代理类的 hashCode , 使用如下:


onPressed: FunctionProxy(increase).throttle
/// or
onPressed: FunctionProxy(() async{
count += 1;
await Future.delayed(const Duration(seconds: 1));
}).throttle

这样最终返回给 onPressed 的是 FunctionProxythrottle 函数,而 throttle 是一个确定的函数,这就最终解决了上述问题。


但是使用时每次都创建 FunctionProxy 类,看着不太友好,给 Function 增加一个 throttle 方法,让使用更加简单:


extension FunctionExt on Function{
VoidCallback throttle(){
return FunctionProxy(this).throttle;
}
}

/// 使用
Future increase() async{
count += 1;
await Future.delayed(const Duration(seconds: 1));
}

onPressed: increase.throttle()

onPressed: () async{
count += 1;
await Future.delayed(const Duration(seconds: 1));
}.throttle()

指定时间节流


节流是事件执行完后才允许下次事件执行,指定时间节流是事件开始执行指定时间后允许下次事件执行,使用延迟指定时间后将 enable 设置为 true 来实现,代码如下:


class FunctionProxy {
static final Map _funcThrottle = {};
final Function? target;

final int timeout;

FunctionProxy(this.target, {int? timeout}) : timeout = timeout ?? 500;

void throttleWithTimeout() {
String key = hashCode.toString();
bool enable = _funcThrottle[key] ?? true;
if (enable) {
_funcThrottle[key] = false;
Timer(Duration(milliseconds: timeout), () {
_funcThrottle.remove(key);
});
target?.call();
}
}
}

增加了 timeout 参数,即指定的节流时间,使用 Timer 延迟指定时间后将 key 从 _funcThrottle 中移除,这里没有加 try-catch ,因为事件异常并不会影响 Timer 的执行,同样的为 Function 增加一个 throttleWithTimeout 扩展:


extension FunctionExt on Function{
VoidCallback throttleWithTimeout({int? timeout}){
return FunctionProxy(this, timeout: timeout).throttleWithTimeout;
}
}

使用:


Future increase() async{
count += 1;
await Future.delayed(const Duration(seconds: 1));
}
onPressed: increase.throttleWithTimeout(timeout: 1000)

onPressed: () async{
count += 1;
await Future.delayed(const Duration(seconds: 1));
}.throttleWithTimeout(timeout: 1000)

防抖


防抖是在事件触发指定时间内该事件未再次触发时再执行,同样可以使用 Timer 来实现:


class FunctionProxy {
static final Map _funcDebounce = {};
final Function? target;

final int timeout;

FunctionProxy(this.target, {int? timeout}) : timeout = timeout ?? 500;

void debounce() {
String key = hashCode.toString();
Timer? timer = _funcDebounce[key];
timer?.cancel();
timer = Timer(Duration(milliseconds: timeout), () {
Timer? t = _funcDebounce.remove(key);
t?.cancel();
target?.call();
});
_funcDebounce[key] = timer;
}
}

同样增加 timeout 参数用于指定防抖的时间间隔,与节流不同的是防抖的 Map 的 value 不是 bool 类型而是 Timer 类型,当事件触发时创建一个 Timer 设置延迟 timeout 后执行,并将 Timer 添加到 Map 中,如果在 timeout 时间内事件再次触发则将 Map 中的 Timer 取消再重新创建新的 Timer,从而实现防抖效果。


同样为 Function 添加 debounce 防抖扩展方法:


extension FunctionExt on Function{
VoidCallback debounce({int? timeout}){
return FunctionProxy(this, timeout: timeout).debounce;
}
}

使用:


Future increase() async{
count += 1;
await Future.delayed(const Duration(seconds: 1));
}
onPressed: increase.debounce(timeout: 1000)

onPressed: () async{
count += 1;
await Future.delayed(const Duration(seconds: 1));
}.debounce(timeout: 1000)

完整代码


至此节流、防抖的封装就完成了,完整代码如下:


extension FunctionExt on Function{
VoidCallback throttle(){
return FunctionProxy(this).throttle;
}

VoidCallback throttleWithTimeout({int? timeout}){
return FunctionProxy(this, timeout: timeout).throttleWithTimeout;
}

VoidCallback debounce({int? timeout}){
return FunctionProxy(this, timeout: timeout).debounce;
}
}

class FunctionProxy {
static final Map _funcThrottle = {};
static final Map _funcDebounce = {};
final Function? target;

final int timeout;

FunctionProxy(this.target, {int? timeout}) : timeout = timeout ?? 500;

void throttle() async {
String key = hashCode.toString();
bool enable = _funcThrottle[key] ?? true;
if (enable) {
_funcThrottle[key] = false;
try {
await target?.call();
} catch (e) {
rethrow;
} finally {
_funcThrottle.remove(key);
}
}
}

void throttleWithTimeout() {
String key = hashCode.toString();
bool enable = _funcThrottle[key] ?? true;
if (enable) {
_funcThrottle[key] = false;
Timer(Duration(milliseconds: timeout), () {
_funcThrottle.remove(key);
});
target?.call();
}
}

void debounce() {
String key = hashCode.toString();
Timer? timer = _funcDebounce[key];
timer?.cancel();
timer = Timer(Duration(milliseconds: timeout), () {
Timer? t = _funcDebounce.remove(key);
t?.cancel();
target?.call();
});
_funcDebounce[key] = timer;
}
}

点击组件封装


完成对节流、防抖的封装后,我们还可以对点击组件进行封装,这样不管是对现有 Flutter 代码还是新代码增加节流、防抖功能都会更加的简单。比如对 GestureDetector 组件可做如下封装:


enum ClickType {
none,
throttle,
throttleWithTimeout,
debounce
}

class ClickWidget extends StatelessWidget {
final Widget child;
final Function? onTap;
final ClickType type;
final int? timeout;

const ClickWidget(
{Key? key,
required this.child,
this.onTap,
this.type = ClickType.throttle,
this.timeout})
: super(key: key);

@override
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
child: child,
onTap: _getOnTap(),
);
}

VoidCallback? _getOnTap() {
if (type == ClickType.throttle) {
return onTap?.throttle();
} else if (type == ClickType.throttleWithTimeout) {
return onTap?.throttleWithTimeout(timeout: timeout);
}else if (type == ClickType.debounce) {
return onTap?.debounce(timeout: timeout);
}
return () => onTap?.call();
}
}

增加 type,用于指定节流、指定时间节流、防抖或者不限制,分别调用对应的方法。默认为节流,可根据项目实际需求设置默认方式或对项目中使用到的其他点击组件进行封装,经过封装后,修改已有代码增加默认限制功能就可以直接替换组件名字而无需改动其他代码实现事件限制的功能。


使用:


/// before
GestureDetector(
child: Text("xxx"),
onTap: increase,
)
/// after
ClickWidget(
child: Text("xxx"),
onTap: increase,
)

ClickWidget(
child: Text("指定时间节流"),
type: ClickType.throttleWithTimeout,
timeout: 1000,
onTap: increase,
)

ClickWidget(
child: Text("防抖"),
type: ClickType.debounce,
timeout: 1000,
onTap: increase,
)

总结


开发过程中,大部分的事件处理都需要进行节流或者防抖限制,防止事件的重复处理导致业务的异常,经过封装后不管是对老项目的优化改造还是新项目的开发,节流和防抖的处理都将变得更简单快捷。


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

常见的Android编译优化问题

编译常见问题 在开发过程中,有碰到过一些由于编译优化导致的代码修改并不符合我们预期的情况。这也就是之前为什么我经常说编译产物其实是不太可以被信任的。 方法签名变更,底层仓库的方法变更但是上层模块并没有跟随一起重新编译导致的这个问题。 常量优化,将一些常量的调...
继续阅读 »

编译常见问题


在开发过程中,有碰到过一些由于编译优化导致的代码修改并不符合我们预期的情况。这也就是之前为什么我经常说编译产物其实是不太可以被信任的。



  1. 方法签名变更,底层仓库的方法变更但是上层模块并没有跟随一起重新编译导致的这个问题。

  2. 常量优化,将一些常量的调用点直接替换成常量的值。

  3. 删除空导包, 没有用的一些导包就会做一次剔除。


最近倒霉了


我们最近碰到一个pipeline相关而且很妖怪的问题。我们一个pipeline会检查apk产物中是否存在异常的方法调用,就是之前介绍的在R8的基础上开发出来的A8。但是最近有一个类被删除了之后呢,但是代码中还有一处调用点。但是这个检测竟然被通过了,然后这部分代码就被合入了master。


image.png


这个引用的文件就如上图所示,是一个debug buildType中的,所以并不是所有的apk中都会存在这部分代码。


然后呢,这个MergeRequest就被合入了master分支,因为当天是我们出下一个版本包的时间,然后交付给测试的就是全量编译的debugrelease包。别的开发同学rebase完master之后就发现piepline都跑不过了,就导致了他们当天的代码无法被合入。


这个就是事情大概的起因和经过,但是各位有没有想过为什么会发生这个问题吗。这个是不是我们的pipeline出现了bug,导致了这种问题无法被识别出来了呢。


以前有说过,如果简单的说我们的快编系统就是把模块替换成对应的aar,从而达到编译提速。所以因为我们使用的是这个模块对应的aar产物,所以大概率就是因为这个模块的编译产物和源代码有差异导致了这个问题。


其实这个问题一出现我就已经知道大概率是由空导包优化导致的这个问题,因为在pipeline检查的时候,检测的apk产物中确实不存在这个导包。因为我们使用的是一个历史版本的aar,其中无效导包的部分已经被编译器做了删除空导包的优化了。接下来我们看下我写的一个demo中的无效导包。


image.png


image.png


图一呢是源代码java文件,图二呢则是jar包中的代码。可以简单的看出来行号呢是可以对应的上的,但是这个AppCompatActivity的无效导包在产物中已经被优化掉了。这里也就回答了在编译过程中会保留行号,但是也会优化掉一部分不需要的代码,让我们编译出来的产物更小。


所以也就导致了我们的产物和我们的源代码之间的差异,另外一个角度就是说从apk中我们确实是不存在这个类的导包。但是呢在我们把这部分代码重新编译成aar的时候,就会出现source缺失,导致的语法树无法生成,之后导致的编译失败问题。


这也就是所以我一直和大家说编译产物是不可以被信任的呢。


以前倒霉过


这个是之前的一个故事了,我们之前呢在模块中定义了一些静态常量吧,然后用来标识当前SDK的版本,然后这个值在别的模块中被引用到了。


有一次因为需求变更,我们更改了这个静态变量的值,然后呢我就把这个需求提测了。之后测试反馈给我为什么这边的这个值没有变化啊。


image.png


我的天,当时我就是这样,发生了什么情况。然后呢我全量打了个包好了,我当时也就以为只是编译时的一个bug而已。然后后来呢,我查了下资料发现这个就是一个java编译时的常量优化问题。过了一阵子吧,我面试了下字节跳动,然后我和面试官也聊了下这个话题,然后呢在这个方法签名变更的问题上,当时我略输一筹,哈哈哈哈。接下来我们就看下一个demo。


image.png


image.png


图1呢也是java代码,图2呢则是aar中的编译产物。其中我们可以看到,这个静态常量在编译成产物之后就会被编译成这样。


所以这个就解释了我一开始碰到的这个问题,他就是由于我们的编译器已经把aar中的这部分静态常量编译成了直接的值,然后呢我们的源变化之后如果没有重新编译对应的模块,就会导致这个值一直无法被更新到最新的值。


结论


如果大家对安卓编译相关有兴趣的话,这些问题很可能都会在面试的时候被问到。希望这不仅仅只是一篇我对于这些问题的思考,也能对各位有所帮助吧。


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

KeyValueX:消除样板代码,让 Android 项目不再 KV 爆炸

背景源于深夜一段独白:Key Value 定义几十上百个是常见事,目前有更简便方式么,此为项目中为数不多不受控制之地,指数膨胀,且易埋下一致性隐患,每新增一 value,需兼顾 key、get、put、init,5 处 …public class Config...
继续阅读 »

背景

源于深夜一段独白:

Key Value 定义几十上百个是常见事,目前有更简便方式么,

此为项目中为数不多不受控制之地,指数膨胀,且易埋下一致性隐患,

每新增一 value,需兼顾 key、get、put、init,5 处 …

public class Configs {
 
...
   
 private static int TEST_ID;
 
 public final static String KEY_TEST_ID = "KEY_TEST_ID";
 
 public static void setTestId(int id) {
   TEST_ID = id;
   SPUtils.getInstance().put(KEY_TEST_ID, id);
}
 
 public static int getTestId() {
   return TEST_ID;
}
 
 public static void initConfigs() {
   TEST_ID = SPUtils.getInstance().getInt(KEY_TEST_ID, 0);
}
}

随后陆续收到改善建议,有小伙伴提到 “属性代理”,并推荐了群友 DylanCai 开源库 MMKV-KTX

与此同时,受 “属性代理” 启发,本人萌生 Java 下 key、value、get、put、init 缩减为一设计。

Github:KeyValueX

V1.0

V1.0 使用 3 步曲

1.如读写 POJO,需实现 Serializable 接口

public class User implements Serializable {
 public String title;
 public String content;
}

2.像往常一样,创建项目配置管理类,如 Configs

//Configs 中不再定义一堆 KEY、VALUE 常量和 get、put、init 静态方法,
//只需一条条 KeyValue 静态变量:

public class Configs {
 public final static KeyValueString accountId = new KeyValueString("accountId");
 public final static KeyValueSerializable<User> user = new KeyValueSerializable<>("user");
}

3.在页面等处通过 get( ) set( ) 方法读写 KeyValue

public class MainActivity extends AppCompatActivity {
...
         
 //测试持久化写入
 Configs.user.set(u);

 //测试读取
 Log.d("---title", Configs.user.get().title);
 Log.d("---content", Configs.user.get().content);
}

V1.0 复盘

KeyValueX v1.0 一出,很快在群里引发热议,群友 DylanCai 提到多模块或存在 KeyName 重复问题,群友彭旭锐提议通过 “注解” 消除重复等问题。

与此同时我也发现,KeyValueX 虽然已消除 key、value、get、put、init 样板代码,但还是有两处一致性问题:

final 修饰符和 KeyName 一致性,

—— final 在 Java 下必写,以免开发者误给 KeyValue 直接赋值:

public class MainActivity extends AppCompatActivity {
...
     
 //正常使用
 Configs.user.set(u);
 
 //误用
 Configs.user = u;
}

那么有开发者可能说,我每次新增 KeyValue,通过 ctrl + D 复制行不就行了

public class Configs {
 public final static KeyValueString accountId = new KeyValueString("accountId");
 public final static KeyValueBoolean isAdult = new KeyValueBoolean("accountId");
}

确实这样可解决 final 一致性,但同时也会滋生 KeyName 一致性问题,也即记得改 KeyValue 变量名,忘改 KeyName,且这种疏忽编译器无法发现,唯有线上出事故专门排查时才可发现。

故综合多方面因素考虑,v2.0 采取注解设计:

V2.0

V2.0 使用 3 步曲

1.创建 KeyValueGroup 接口

@KeyValueGroup
public interface KeyValues {
 @KeyValue KeyValueInteger days();
 @KeyValue KeyValueString accountId();
 @KeyValue KeyValueSerializable<User> user();
}

2.像往常一样,创建项目配置管理类,如 Configs

//Configs 中不再定义一堆 KEY、VALUE 常量和 get、put、init 静态方法,
//只需一条 KeyValueGroup 静态变量:

public class Configs {
 public final static KeyValues keyValues = KeyValueCreator.create(KeyValues.class);
}

3.在页面等处通过 get( ) set( ) 方法读写 KeyValue

public class MainActivity extends AppCompatActivity {
...
         
 //测试持久化写入
 Configs.keyValues.user().set(u);

 //测试读取
 Log.d("---title", Configs.keyValues.user().get().title);
 Log.d("---content", Configs.keyValues.user().get().content);
}

V2.0 复盘

V2.0 通过接口 + 注解设计,一举消除 final 和 KeyName 一致性问题,

且通过无参反射实现 KeyValues 的实例化,使写代码过程中无需特意 build 生成 Impl 类,对 build 一次便需数分钟的巨型项目较友好。

根据上图可见,无参反射加载类,耗时仅次于 new,故 V2.0 设计本人还算满意,已在 Java 项目中全面使用,欢迎测试反馈。

Github:KeyValueX

KeyValue-Dispatcher

期间群友 DylanCai 提出可改用动态代理实现,也即效仿 Retrofit,根据接口定义运行时动态生成方法,

如此无需声明注解,使接口定义更简洁,看起来就像:

public interface KeyValues {
 KeyValueInteger days();
 KeyValueString accountId();
 KeyValueSerializable<User> user();
}

与此同时,可根据适配器模式实现个转换器,例如转换为 UnPeek-LiveData,如此即可顺带完成高频操作 —— 更新配置后通知部分页面刷新 UI。

public interface KeyValues {
Result<Integer> days();
Result<String> accountId();
Result<User> user();
}

Configs.keyValues.days().observer(this, result -> {
...
});

不过动态代理有个硬伤,即类名方法名不可混淆,不然运行时难调到对应方法,

故动态代理方式最终未考虑,不过转换器设计我甚是喜欢,加之 Java 后端 Properties 启发,故萌生 Dispatcher 设计 ——

基于 MVI-Dispatcher 实现 KeyValue-Dispatcher。具体思路即通过 HashMap 内聚 KeyValue,如此只需声明 Key,而无需考虑 value、getter、setter、init:

KV-D 使用 3 步曲

1.定义 Key 列表

public class Key {
 public final static String TEST_STRING = "test_string";
 public final static String TEST_BOOLEAN = "test_boolean";
}

2.读写

//读
boolean b = GlobalConfigs.getBoolean(Key.TEST_BOOLEAN);
//写
GlobalConfigs.put(Key.TEST_STRING, value);

3.顺带可通知 UI 刷新

GlobalConfigs.output(this, keyValueEvent -> {
 switch (keyValueEvent.currentKey) {
   case Key.TEST_STRING: ... break;
   case Key.TEST_BOOLEAN: ... break;
}
});

依托 MVI-Dispatcher 消息聚合设计,任何由配置变更引发的 UI 刷新,皆从这唯一出口响应。

目前已更新至 MVI-Dispatcher 项目,感兴趣可自行查阅。

KV-D 复盘

KV-D 旨在消除学习成本,让开发者像 SPUtils 一样使用,与此同时自动达成内存快读、消除样板代码、规避不可预期错误。

不过 KV-D 只适合 Java 项目用。如欲于 Kotlin 实现属性代理,还需基于 KeyValueX 那类设计。

于是 KeyValueX 再做升级:

1.简化注解:只需接口处声明此为 KeyValueX 接口,

2.自动分组:以 KeyValueX 接口为单位生成路径 MD5,KeyName 根据 MD5 自动分组,

3.全局内存快读:如 ViewModelProvider 使用,并提供全局内存快读。

V3.0

V3.0 使用 2 步曲

1.创建 KeyValueGroup 接口,例如

@KeyValueX
public interface Configs {
 KeyValueInteger days();
 KeyValueString accountId();
 KeyValueSerializable<User> user();
}

2.在页面等处通过 get( ) set( ) 方法读写 KeyValue

public class MainActivity extends AppCompatActivity {
 private final Configs configs = KeyValueProvider.get(Configs.class);
 
...

 //写
 configs.user().set(user);

 //读
 configs.user().get().title;
 configs.user().get().content;
}

已更新至 KeyValueX 项目,感兴趣可自行查阅。


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

Flutter Clip 用来实现文本标签的效果

Clip 是Material Design的一个 Widget,用来实现标签效果,Clip中通过属性可配置一个文本、完整的 Widget、一个动作(比如按钮点击)。 1 基本使用效果如下 class ClipHomeState extends State {...
继续阅读 »

Clip 是Material Design的一个 Widget,用来实现标签效果,Clip中通过属性可配置一个文本、完整的 Widget、一个动作(比如按钮点击)。


1 基本使用效果如下


在这里插入图片描述


class ClipHomeState extends State {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Clip")),
body: const Center(
///-----------------------------------------------------
///核心代码
child: Chip(
//左侧的小组件
avatar: CircleAvatar(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
child: Text('早'),
),
//右侧的文本
label: Text('Flutter 基础 '),
),

///-----------------------------------------------------
),
);
}
}

Chip 的 avatar 属性配置的是一个Widget,所这里可以组合足够复杂的 Widget合集,在本实例中只是使用了 CircleAvatar,CircleAvatar常用来展示圆形的小头像。


2 结合 Wrap 组件实现多标签效果


class ClipWrapHomeState extends State {
final List<String> _tagList = [
"早起", "晚睡", "测试", "努力",
"不想睡觉", "清晨的太阳", "这是什么", "哈哈"
];

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Clip")),
body: Padding(
///-----------------------------------------------------
///核心代码
padding: EdgeInsets.all(12),
child: Wrap(
spacing: 10,
children: _tagList.map((e) => Chip(label: Text(e))).toList(),
),
///-----------------------------------------------------
),
);
}
}

在这里插入图片描述


3 Clip 的属性概述


3.1 label 文本相关配置

label 是用来配置文本的,labelStyle是用来设置这个文本的样式,如颜色、大小 、粗细等等,labelPadding是用来设置文本四周的边距的。


  Chip buildChip() {
return const Chip(
//左侧的小组件
avatar: CircleAvatar(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
child: Text('早'),
),

///右侧的文本
label: Text('Flutter 基础 '),
labelStyle: TextStyle(color: Colors.red),
labelPadding: EdgeInsets.only(left: 12, right: 12),
);
}
}

在这里插入图片描述


3.2 右侧的删除按钮配置 deleteIcon

avatar 是用来配置左侧的显示的小 Widget,deleteIcon是用来配置右侧的删除按钮的。


  Chip buildChip() {
return Chip(
//左侧的小组件
avatar: const CircleAvatar(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
child: Text('早'),
),

///右侧的文本
label: Text('Flutter 基础 '),
labelStyle: TextStyle(color: Colors.red),
labelPadding: EdgeInsets.only(left: 12, right: 12),

///
deleteIcon: Icon(Icons.close),
//删除按钮颜色
deleteIconColor: Colors.red,
//长按的提示
deleteButtonTooltipMessage: "删除",
onDeleted: () {
print("--点击了删除");
},
);
}

在这里插入图片描述


3.3 阴影设置


属性 elevation 用来设置阴影高度,shadowColor属性用来设置阴影颜色


Chip buildChip() {
return Chip(
//左侧的小组件
avatar: const CircleAvatar(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
child: Text('早'),
),

///右侧的文本
label: Text('Flutter 基础 '),
labelStyle: TextStyle(color: Colors.red),
labelPadding: EdgeInsets.only(left: 12, right: 12),

///
deleteIcon: Icon(Icons.close),
//删除按钮颜色
deleteIconColor: Colors.red,
//长按的提示
deleteButtonTooltipMessage: "删除",
onDeleted: () {
print("--点击了删除");
},

///
elevation: 10,//阴影 高度
shadowColor: Colors.blue,//阴影颜色
backgroundColor: Colors.grey,//背景色

);
}



完毕


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

Flutter状态管理-Bloc的使用

前言 目前Flutter三大主流状态管理框架分别是provider、flutter_bloc、getx,三大状态管理框架各有优劣,本篇文章将介绍其中的flutter_bloc框架的使用,他是bloc设计思想模式在flutter上的实现,bloc全程全称 bus...
继续阅读 »

前言


目前Flutter三大主流状态管理框架分别是providerflutter_blocgetx,三大状态管理框架各有优劣,本篇文章将介绍其中的flutter_bloc框架的使用,他是bloc设计思想模式在flutter上的实现,bloc全程全称 business logic ,业务逻辑的意思,核心思想就是最大程度的将页面ui与数据逻辑的解耦,使我们的项目可读性、可维护性、健壮性增强。


两种使用模式


首先第一步引入插件:


flutter_bloc: ^8.1.1

引入之后,flutter_bloc使用支持以下两种模式管理。


Bloc模式,分别有ui层(view)、数据层(state)、事件层(event)、逻辑处理层(bloc),适合大型复杂页面使用。这四层结构bloc在源码处就进行了封装处理,所以我们使用的时候是必须要分离出来的,比如eventstate是要强制分开去写的。这也导致了简单页面使用此模式复杂化的问题,所以这种模式对于简单页面是非常没有必要的,但是如果是复杂页面的话,非常建议使用此模式,相信你在处理页面数据逻辑的时候会非常的清晰。

下面我们以计数器为例写个demo,记住bloc模式有四层,且必须分开,我们建四个文件分别代表这四层,

image.png

数据层: 用来存放数据,这个很简单。


/// 数据层
class DemoState {
// 自增数字
late int num;

DemoState init() {
// 初始化为0
return DemoState()..num = 0;
}

DemoState clone() {
// 获取最新对象
return DemoState()..num = num;
}
}

事件层: 用来存放页面所有事件的地方,比如计数器页面只有初始化事件和自增事件。


/// 页面中所有的交互事件
abstract class DemoEvent {}
/// 初始化事件
class InitEvent extends DemoEvent {}
/// 自增事件
class IncrementEvent extends DemoEvent {}

Bloc逻辑处理层: 处理上方数据和事件逻辑的地方,通过源码就能发现作者的意图,泛型里必须分开传入事件和数据,也足以说明这个模式的特点,就是为复杂页面准备的。所以如果写计数器的话,你就会感觉非常没有必要,因为计数器页面很简单,但是当你的state层里的数据非常多且复杂的时候,你就能体会出分开的好处了。
image.png
代码:


/// 逻辑处理层 继承Bloc
class DemoBloc extends Bloc<DemoEvent, DemoState> {
///构造方法
DemoBloc() : super(DemoState().init()) {
/// on 注册所有事件 on固定写法
on<InitEvent>(_init);
on<IncrementEvent>(_add);
}

/// 私有化逻辑方法 暴露Event事件即可
void _init(InitEvent event, Emitter<DemoState> emit) {
// emit方法,通知更新状态 类似于 ChangeNotifier的notifyListeners方法。
emit(state.clone());
}

_add(IncrementEvent event, Emitter<DemoState> emit) {
state.num++;
// 调用emit方法更新状态
emit(state.clone());
}
}

UI层: UI层只负责页面的编写,而无需关心数据的生成,根节点返回BlocProvider,并实现create方法,返回我们的bloc实体类。child实现我们的 UI页面。


/// UI层
class BlocNumPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 以下固定写法
return BlocProvider(
create: (BuildContext context) => DemoBloc()..add(InitEvent()),
child: Builder(builder: (context) => _buildPage(context)),
);
}

Widget _buildPage(BuildContext context) {
// 获取bloc实例
final bloc = BlocProvider.of<DemoBloc>(context);
return Stack(
children: [
Center(
// 对于需要更新状态的组件 外层包裹一个BlocBuilder,传入bloc、state
child: BlocBuilder<DemoBloc, DemoState>(
builder: (context, state) {
// 返回具体ui组件
return Text("点击了${bloc.state.num}次");
},
),
),
Positioned(
bottom: 20,
right: 20,
child: FloatingActionButton(
onPressed: () {
// 调用add方法触发自增事件,
bloc.add(IncrementEvent());
},
child: Icon(Icons.add),
),
)
],
);
}
}

效果:

Oct-30-2022 18-33-01.gif


Cubit模式


Cubit模式,分别有ui层(view)、数据层(state)、逻辑处理层(cubit),相较于bloc模式去掉了event层,适合比较简单的页面。跟bloc模式只是逻辑处理哪里发生了改变,数据层、页面ui层代码一模一样。


可以看到Cubit模式的逻辑就少了很多代码,而且是直接处理数据即可。通过源码,作者意图也很明显,只是传递了数据层。
image.png


/// 写逻辑
class CubitCubit extends Cubit<CubitState> {
CubitCubit() : super(CubitState().init());

///自增
void increment() => emit(state.clone()..num = state.num + 1);
}

其他写法跟Bloc是一样的,就不粘贴了,那看到这有的小伙伴可能就要问了,一个页面要创建3、4个文件,这也太麻烦了吧,高端的程序员往往不会去写一些重复性较高的代码,其实上面的四个文件都是可以通过插件自动生成的,这里下面两个插件,一个是官方的,官方的不会自动生成ui层的文件,一个是小呆呆写的,可以自动生成ui层重复性的代码文件,两者区别不大,推荐小呆呆的,因为可以多生成一个文件。
image.png


最后


Bloc本质上是一种数据逻辑和UI解耦思想,上面的演示只是非常非常简单的用法,就可以看出作者在源码层给我们强制性的设定了非常明确的各个模型,每个模型只专心负责一个事情,这样看起来一个页面会非常的清晰,可以说Flutter_Bloc是一个非常适合大型项目使用的状态管理框架。


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

公司没钱了,工资发不出来,作为员工怎么办?

现在大环境不好,很多公司都会遇到一些困难。如果公司真的有现金流还好,如果没有,还等着客户回款。那么就有点难办了。很多公司会采取延期发工资,先发 80%、50% 工资这样的操作。 员工遇到这种情况,无非以下几种选择。1认同公司的决策,愿意跟公司共同进退。2不认同...
继续阅读 »

现在大环境不好,很多公司都会遇到一些困难。如果公司真的有现金流还好,如果没有,还等着客户回款。那么就有点难办了。很多公司会采取延期发工资,先发 80%、50% 工资这样的操作。


员工遇到这种情况,无非以下几种选择。

1认同公司的决策,愿意跟公司共同进退。

2不认同公司的决策,我要离职。

3不认同公司的决策,但感觉自己反对也没用。所以嘴上先答应,事后会准备去找新的工作机会。

4不认同公司的决策,我也不主动离职。准备跟公司 battle,” 你们这么做是不合法滴 “


你可以代入这个场景看看自己是哪一类。首先由于每个人遇到的真实情况不一样,所以所有的选择只有适合自己的,并没有对错之分。


我自己的应对思路是,抛开存量,看增量。存量就是我在公司多少多少年了,公司开除我要给我 N+1 的补偿。公司之前对我特别特别好,老板对我有知遇之恩等等。你就当做自己已经不在公司了,现在公司给你发了 offer,现在的你是否愿意接受公司开给你的条件?如果愿意,那么你可以选择方案一。如果不愿意,那么你就可以选择方案三。现在这环境,骑驴找马,否则离职后还要自己交社保。还不如先苟着。


为什么不选择方案四?因为不值得,如果公司属于违法操作,你先苟着,后面离职后还是可以找劳动局仲裁的。这样既不耽误你换工作,也不耽误你要赔偿。如果公司是正规操作,那么闹腾也没用,白浪费自己的时间。


离职赔偿还是比较清晰明确的,如果是散伙那可能会牵扯到更多利益。我自己的经验是,不能什么都想着要。当最优解挺难获得的时候,拿个次优解也可以。当然,不管你选择的哪个,我都有一个建议。那就是当一天和尚,敲一天钟。在职期间,还是要把事情干好的,用心并不全是为了公司,更多是为了自己。人生最大的投资是投资自己的工作和事业,浪费时间就是浪费生命。哪怕公司没有事情安排给你做,也要学会自己找事情做。


如果公司后面没钱了,欠的工资还拿得到吗?



我们作为员工是很难知道公司财务状况的,所以出了这样的事就直接去仲裁,最好是跟同事凑齐十个人去,据说会优先处理。公司如果还要做生意,一般会在仲裁前选择和解,大概是分几个月归还欠款。如果公司不管,那么仲裁后,会冻结公司公账。但有没有钱就看情况了。


如果公司账上没钱且股东已经实缴了股本金,那么公司是可以直接破产清算的。公司破产得话,基本上欠员工的钱就没有了。如果没有实缴,那么股东还需要按照股份比例偿还债务。


链接:https://juejin.cn/post/7156242740034928671

收起阅读 »

阿里面试官:请设计一个不能操作DOM和调接口的环境

web
前言四面的时候被问到了这个问题,当时第一时间没有反应过来,觉得这个需求好奇特面试官给了一些提示,我才明白这道题目的意思,最后回答的也是磕磕绊绊后来花了一些时间整理了下思路,那么如何设计这样的环境呢?最终实现实现思路:1)利用 iframe 创建沙箱,取出其中的...
继续阅读 »

前言

四面的时候被问到了这个问题,当时第一时间没有反应过来,觉得这个需求好奇特

面试官给了一些提示,我才明白这道题目的意思,最后回答的也是磕磕绊绊

后来花了一些时间整理了下思路,那么如何设计这样的环境呢?

最终实现

实现思路:

1)利用 iframe 创建沙箱,取出其中的原生浏览器全局对象作为沙箱的全局对象

2)设置一个黑名单,若访问黑名单中的变量,则直接报错,实现阻止\隔离的效果

3)在黑名单中添加 document 字段,来实现禁止开发者操作 DOM

4)在黑名单中添加 XMLHttpRequest、fetch、WebSocket 字段,实现禁用原生的方式调用接口

5)若访问当前全局对象中不存在的变量,则直接报错,实现禁用三方库调接口

6)最后还要拦截对 window 对象的访问,防止通过 window.document 来操作 DOM,避免沙箱逃逸

下面聊一聊,为何这样设计,以及中间会遇到什么问题

如何禁止开发者操作 DOM ?

在页面中,可以通过 document 对象来获取 HTML 元素,进行增删改查的 DOM 操作

如何禁止开发者操作 DOM,转化为如何阻止开发者获取 document 对象

1)传统思路

简单粗暴点,直接修改 window.document 的值,让开发者无法获取 document

// 将document设置为null
window.document = null;

// 设置无效,打印结果还是document
console.log(window.document);

// 删除document
delete window.document

// 删除无效,打印结果还是document
console.log(window.document);

好吧,document 修改不了也删除不了🤔

使用 Object.getOwnPropertyDescriptor 查看,会发现 window.document 的 configurable 属性为 false(不可配置的)

Object.getOwnPropertyDescriptor(window, 'document');
// {get: ƒ, set: undefined, enumerable: true, configurable: false}

configurable 决定了是否可以修改属性描述对象,也就是说,configurable为false时,value、writable、enumerable和configurable 都不能被修改,以及无法被删除

此路不通,推倒重来

2)有点高大上的思路

既然 document 对象修改不了,那如果环境中原本就没有 document 对象,是不是就可以实现该需求?

说到环境中没有 document 对象,Web Worker 直呼内行,我曾在《一文彻底了解Web Worker,十万、百万条数据都是弟弟🔥》中聊过如何使用 Web Worker,和对应的特性

并且 Web Worker 更狠,不但没有 document 对象,连 window 对象也没有😂

在worker线程中打印window

onmessage = function (e) {
console.log(window);
postMessage();
};

浏览器直接报错


在 Web Worker 线程的运行环境中无法访问 document 对象,这一条符合当前的需求,但是该环境中能获取 XMLHttpRequest 对象,可以发送 ajax 请求,不符合不能调接口的要求

此路还是不通……😓

如何禁止开发者调接口 ?

常规调接口方式有:

1)原生方式:XMLHttpRequest、fetch、WebSocket、jsonp、form表单

2)三方实现:axios、jquery、request等众多开源库

禁用原生方式调接口的思路:

1)XMLHttpRequest、fetch、WebSocket 这几种情况,可以禁止用户访问这些对象

2)jsonp、form 这两种方式,需要创建script或form标签,依然可以通过禁止开发者操作DOM的方式解决,不需要单独处理

如何禁用三方库调接口呢?

三方库很多,没办法全部列出来,来进行逐一排除

禁止调接口的路好像也被封死了……😰

最终方案:沙箱(Sandbox)

通过上面的分析,传统的思路确实解决不了当前的需求

阻止开发者操作DOM和调接口,沙箱说:这个我熟啊,拦截隔离这类的活,我最拿手了😀

沙箱(Sandbox) 是一种安全机制,为运行中的程序提供隔离环境,通常用于执行未经测试或不受信任的程序或代码,它会为待执行的程序创建一个独立的执行环境,内部程序的执行不会影响到外部程序的运行

前端沙箱的使用场景:

1)Chrome 浏览器打开的每个页面就是一个沙箱,保证彼此独立互不影响

2)执行 jsonp 请求回来的字符串时或引入不知名第三方 JS 库时,可能需要创造一个沙箱来执行这些代码

3)Vue 模板表达式的计算是运行在一个沙箱中,模板字符串中的表达式只能获取部分全局对象,详情见源码

4)微前端框架 qiankun ,为了实现js隔离,在多种场景下均使用了沙箱

沙箱的多种实现方式

先聊下 with 这个关键字:作用在于改变作用域,可以将某个对象添加到作用域链的顶部

with对于沙箱的意义:可以实现所有变量均来自可靠或自主实现的上下文环境,而不会从全局的执行环境中取值,相当于做了一层拦截,实现隔离的效果

简陋的沙箱

题目要求: 实现这样一个沙箱,要求程序中访问的所有变量,均来自可靠或自主实现的上下文环境,而不会从全局的执行环境中取值

举个🌰: ctx作为执行上下文对象,待执行程序code可以访问到的变量,必须都来自ctx对象

// ctx 执行上下文对象
const ctx = {
func: variable => {
  console.log(variable);
},
foo: "f1"
};

// 待执行程序
const code = `func(foo)`;

沙箱示例:

// 定义全局变量foo
var foo = "foo1";

// 执行上下文对象
const ctx = {
 func: variable => {
   console.log(variable);
},
 foo: "f1"
};

// 非常简陋的沙箱
function veryPoorSandbox(code, ctx) {
 // 使用with,将eval函数执行时的执行上下文指定为ctx
 with (ctx) {
   // eval可以将字符串按js代码执行,如eval('1+2')
   eval(code);
}
}

// 待执行程序
const code = `func(foo)`;

veryPoorSandbox(code, ctx);
// 打印结果:"f1",不是最外层的全局变量"foo1"

这个沙箱有一个明显的问题,若提供的ctx上下文对象中,没有找到某个变量时,代码仍会沿着作用域链一层层向上查找

假如上文示例中的 ctx 对象没有设置 foo属性,打印的结果还是外层作用域的foo1

With + Proxy 实现沙箱

题目要求: 希望沙箱中的代码只在手动提供的上下文对象中查找变量,如果上下文对象中不存在该变量,则提示对应的错误

举个🌰: ctx作为执行上下文对象,待执行程序code可以访问到的变量,必须都来自ctx对象,如果ctx对象中不存在该变量,直接报错,不再通过作用域链向上查找

实现步骤:

1)使用 Proxy.has() 来拦截 with 代码块中的任意变量的访问

2)设置一个白名单,在白名单内的变量可以正常走作用域链的访问方式,不在白名单内的变量,会继续判断是否存 ctx 对象中,存在则正常访问,不存在则直接报错

3)使用new Function替代eval,使用 new Function() 运行代码比eval更为好一些,函数的参数提供了清晰的接口来运行代码

new Function与eval的区别

沙箱示例:

var foo = "foo1";

// 执行上下文对象
const ctx = {
 func: variable => {
   console.log(variable);
}
};

// 构造一个 with 来包裹需要执行的代码,返回 with 代码块的一个函数实例
function withedYourCode(code) {
 code = "with(shadow) {" + code + "}";
 return new Function("shadow", code);
}

// 可访问全局作用域的白名单列表
const access_white_list = ["func"];

// 待执行程序
const code = `func(foo)`;

// 执行上下文对象的代理对象
const ctxProxy = new Proxy(ctx, {
 has: (target, prop) => {
   // has 可以拦截 with 代码块中任意属性的访问
   if (access_white_list.includes(prop)) {
     // 在可访问的白名单内,可继续向上查找
     return target.hasOwnProperty(prop);
  }
   if (!target.hasOwnProperty(prop)) {
     throw new Error(`Not found - ${prop}!`);
  }
   return true;
}
});

// 没那么简陋的沙箱
function littlePoorSandbox(code, ctx) {
 // 将 this 指向手动构造的全局代理对象
 withedYourCode(code).call(ctx, ctx);
}
littlePoorSandbox(code, ctxProxy);

// 执行func(foo),报错: Uncaught Error: Not found - foo!

执行结果:


天然的优质沙箱(iframe)

iframe 标签可以创造一个独立的浏览器原生级别的运行环境,这个环境由浏览器实现了与主环境的隔离

利用 iframe 来实现一个沙箱是目前最方便、简单、安全的方法,可以把 iframe.contentWindow 作为沙箱执行的全局 window 对象

沙箱示例:

// 沙箱全局代理对象类
class SandboxGlobalProxy {
 constructor(sharedState) {
   // 创建一个 iframe 标签,取出其中的原生浏览器全局对象作为沙箱的全局对象
   const iframe = document.createElement("iframe", { url: "about:blank" });
   iframe.style.display = "none";
   document.body.appendChild(iframe);
   
   // sandboxGlobal作为沙箱运行时的全局对象
   const sandboxGlobal = iframe.contentWindow;

   return new Proxy(sandboxGlobal, {
     has: (target, prop) => {
       // has 可以拦截 with 代码块中任意属性的访问
       if (sharedState.includes(prop)) {
         // 如果属性存在于共享的全局状态中,则让其沿着原型链在外层查找
         return false;
      }
       
       // 如果没有该属性,直接报错
       if (!target.hasOwnProperty(prop)) {
         throw new Error(`Not find: ${prop}!`);
      }
       
       // 属性存在,返回sandboxGlobal中的值
       return true;
    }
  });
}
}

// 构造一个 with 来包裹需要执行的代码,返回 with 代码块的一个函数实例
function withedYourCode(code) {
 code = "with(sandbox) {" + code + "}";
 return new Function("sandbox", code);
}
function maybeAvailableSandbox(code, ctx) {
 withedYourCode(code).call(ctx, ctx);
}

// 要执行的代码
const code = `
 console.log(history == window.history) // false
 window.abc = 'sandbox'
 Object.prototype.toString = () => {
     console.log('Traped!')
 }
 console.log(window.abc) // sandbox
`;

// sharedGlobal作为与外部执行环境共享的全局对象
// code中获取的history为最外层作用域的history
const sharedGlobal = ["history"];

const globalProxy = new SandboxGlobalProxy(sharedGlobal);

maybeAvailableSandbox(code, globalProxy);

// 对外层的window对象没有影响
console.log(window.abc); // undefined
Object.prototype.toString(); // 并没有打印 Traped

可以看到,沙箱中对window的所有操作,都没有影响到外层的window,实现了隔离的效果😘

需求实现

继续使用上述的 iframe 标签来创建沙箱,代码主要修改点

1)设置 blacklist 黑名单,添加 document、XMLHttpRequest、fetch、WebSocket 来禁止开发者操作DOM和调接口

2)判断要访问的变量,是否在当前环境的 window 对象中,不在的直接报错,实现禁止通过三方库调接口

// 设置黑名单
const blacklist = ['document', 'XMLHttpRequest', 'fetch', 'WebSocket'];

// 黑名单中的变量禁止访问
if (blacklist.includes(prop)) {
 throw new Error(`Can't use: ${prop}!`);
}

但有个很严重的漏洞,如果开发者通过 window.document 来获取 document 对象,依然是可以操作 DOM 的😱

需要在黑名单中加入 window 字段,来解决这个沙箱逃逸的漏洞,虽然把 window 加入了黑名单,但 window 上的方法,如 open、close 等,依然是可以正常获取使用的

最终代码:

// 沙箱全局代理对象类
class SandboxGlobalProxy {
constructor(blacklist) {
// 创建一个 iframe 标签,取出其中的原生浏览器全局对象作为沙箱的全局对象
const iframe = document.createElement("iframe", { url: "about:blank" });
iframe.style.display = "none";
document.body.appendChild(iframe);

// 获取当前HTMLIFrameElement的Window对象
const sandboxGlobal = iframe.contentWindow;

return new Proxy(sandboxGlobal, {
// has 可以拦截 with 代码块中任意属性的访问
has: (target, prop) => {

// 黑名单中的变量禁止访问
if (blacklist.includes(prop)) {
throw new Error(`Can't use: ${prop}!`);
}
// sandboxGlobal对象上不存在的属性,直接报错,实现禁用三方库调接口
if (!target.hasOwnProperty(prop)) {
throw new Error(`Not find: ${prop}!`);
}

// 返回true,获取当前提供上下文对象中的变量;如果返回false,会继续向上层作用域链中查找
return true;
}
});
}
}

// 使用with关键字,来改变作用域
function withedYourCode(code) {
code = "with(sandbox) {" + code + "}";
return new Function("sandbox", code);
}

// 将指定的上下文对象,添加到待执行代码作用域的顶部
function makeSandbox(code, ctx) {
withedYourCode(code).call(ctx, ctx);
}

// 待执行的代码code,获取document对象
const code = `console.log(document)`;

// 设置黑名单
// 经过小伙伴的指导,新添加Image字段,禁止使用new Image来调接口
const blacklist = ['window', 'document', 'XMLHttpRequest', 'fetch', 'WebSocket', 'Image'];

// 将globalProxy对象,添加到新环境作用域链的顶部
const globalProxy = new SandboxGlobalProxy(blacklist);

makeSandbox(code, globalProxy);

打印结果:


持续优化

经过与评论区小伙伴的交流,可以通过 new Image() 调接口,确实是个漏洞

// 不需要创建DOM 发送图片请求
let img = new Image();
img.src= "http://www.test.com/img.gif";

黑名单中添加'Image'字段,堵上这个漏洞。如果还有其他漏洞,欢迎交流讨论💕

总结

通过解决面试官提出的问题,介绍了沙箱的基本概念、应用场景,以及如何去实现符合要求的沙箱,发现防止沙箱逃逸是一件挺有趣的事情,就像双方在下棋一样,你来我往,有攻有守😄

关于这个问题,小伙伴们如果有其他可行的方案,或者有要补充、指正的,欢迎交流讨论


参考资料:
浅析 JavaScript 沙箱机制

作者:海阔_天空
来源:juejin.cn/post/7157570429928865828

收起阅读 »

数据结构:7种哈希散列算法,你知道几个?

一、前言 哈希表的历史 哈希散列的想法在不同的地方独立出现。1953 年 1 月,汉斯·彼得·卢恩 ( Hans Peter Luhn ) 编写了一份IBM内部备忘录,其中使用了散列和链接。开放寻址后来由 AD Linh 在 Luhn 的论文上提出。大约在同一...
继续阅读 »

一、前言


哈希表的历史


哈希散列的想法在不同的地方独立出现。1953 年 1 月,汉斯·彼得·卢恩 ( Hans Peter Luhn ) 编写了一份IBM内部备忘录,其中使用了散列和链接。开放寻址后来由 AD Linh 在 Luhn 的论文上提出。大约在同一时间,IBM Research的Gene Amdahl、Elaine M. McGraw、Nathaniel Rochester和Arthur Samuel为IBM 701汇编器实现了散列。 线性探测的开放寻址归功于 Amdahl,尽管Ershov独立地有相同的想法。“开放寻址”一词是由W. Wesley Peterson在他的文章中创造的,该文章讨论了大文件中的搜索问题。


二、哈希数据结构


哈希表的存在是为了解决能通过O(1)时间复杂度直接索引到指定元素。


这是什么意思呢?通过我们使用数组存放元素,都是按照顺序存放的,当需要获取某个元素的时候,则需要对数组进行遍历,获取到指定的值。如图所示;





而这样通过循环遍历比对获取指定元素的操作,时间复杂度是O(n),也就是说如果你的业务逻辑实现中存在这样的代码是非常拉胯的。那怎么办呢?这就引入了哈希散列表的设计。




在计算机科学中,一个哈希表(hash table、hash map)是一种实现关联数组的抽象数据结构,该结构将键通过哈希计算映射到值。


也就是说我们通过对一个 Key 值计算它的哈希并与长度为2的n次幂的数组减一做与运算,计算出槽位对应的索引,将数据存放到索引下。那么这样就解决了当获取指定数据时,只需要根据存放时计算索引ID的方式再计算一次,就可以把槽位上对应的数据获取处理,以此达到时间复杂度为O(1)的情况。如图所示;





哈希散列虽然解决了获取元素的时间复杂度问题,但大多数时候这只是理想情况。因为随着元素的增多,很可能发生哈希冲突,或者哈希值波动不大导致索引计算相同,也就是一个索引位置出现多个元素情况。如图所示;





李二狗拎瓢冲都有槽位的下标索引03的 叮裆猫 发生冲突时,情况就变得糟糕了,因为这样就不能满足O(1)时间复杂度获取元素的诉求了。


那么此时就出现了一系列解决方案,包括;HashMap 中的拉链寻址 + 红黑树、扰动函数、负载因子ThreadLocal 的开放寻址、合并散列、杜鹃散列、跳房子哈希、罗宾汉哈希等各类数据结构设计。让元素在发生哈希冲突时,也可以存放到新的槽位,并尽可能保证索引的时间复杂度小于O(n)


三、实现哈希散列


哈希散列是一个非常常见的数据结构,无论是我们使用的 HashMap、ThreaLocal 还是你在刷题中位了提升索引效率,都会用到哈希散列。


只要哈希桶的长度由负载因子控制的合理,每次查找元素的平均时间复杂度与桶中存储的元素数量无关。另外许多哈希表设计还允许对键值对的任意插入和删除,每次操作的摊销固定平均成本。


好,那么介绍了这么多,小傅哥带着大家做几个关于哈希散列的数据结构,通过实践来了解会更加容易搞懂。



1. 哈希碰撞


说明:通过模拟简单 HashMap 实现,去掉拉链寻址等设计,验证元素哈新索引位置碰撞。


public class HashMap01<K, V> implements Map<K, V> {

private final Object[] tab = new Object[8];

@Override
public void put(K key, V value) {
int idx = key.hashCode() & (tab.length - 1);
tab[idx] = value;
}

@Override
public V get(K key) {
int idx = key.hashCode() & (tab.length - 1);
return (V) tab[idx];
}

}





  • HashMap01 的实现只是通过哈希计算出的下标,散列存放到固定的数组内。那么这样当发生元素下标碰撞时,原有的元素就会被新的元素替换掉。


测试


@Test
public void test_hashMap01() {
Map<String, String> map = new HashMap01<>();
map.put("01", "花花");
map.put("02", "豆豆");
logger.info("碰撞前 key:{} value:{}", "01", map.get("01"));

// 下标碰撞
map.put("09", "蛋蛋");
map.put("12", "苗苗");
logger.info("碰撞前 key:{} value:{}", "01", map.get("01"));
}




06:58:41.691 [main] INFO cn.bugstack.algorithms.test.AlgorithmsTest - 碰撞前 key:01 value:花花
06:58:41.696 [main] INFO cn.bugstack.algorithms.test.AlgorithmsTest - 碰撞前 key:01 value:苗苗

Process finished with exit code 0


  • 通过测试结果可以看到,碰撞前 map.get("01") 的值是花花,两次下标索引碰撞后存放的值则是苗苗

  • 这也就是使用哈希散列必须解决的一个问题,无论是在已知元素数量的情况下,通过扩容数组长度解决,还是把碰撞的元素通过链表存放,都是可以的。


2. 拉链寻址


说明:既然我们没法控制元素不碰撞,但我们可以对碰撞后的元素进行管理。比如像 HashMap 中拉链法一样,把碰撞的元素存放到链表上。这里我们就来简化实现一下。


public class HashMap02BySeparateChaining<K, V> implements Map<K, V> {

private final LinkedList<Node<K, V>>[] tab = new LinkedList[8];

@Override
public void put(K key, V value) {
int idx = key.hashCode() & (tab.length - 1);
if (tab[idx] == null) {
tab[idx] = new LinkedList<>();
tab[idx].add(new Node<>(key, value));
} else {
tab[idx].add(new Node<>(key, value));
}
}

@Override
public V get(K key) {
int idx = key.hashCode() & (tab.length - 1);
for (Node<K, V> kvNode : tab[idx]) {
if (key.equals(kvNode.getKey())) {
return kvNode.value;
}
}
return null;
}

static class Node<K, V> {
final K key;
V value;

public Node(K key, V value) {
this.key = key;
this.value = value;
}

public K getKey() {
return key;
}

public V getValue() {
return value;
}

}

}



  • 因为元素在存放到哈希桶上时,可能发生下标索引膨胀,所以这里我们把每一个元素都设定成一个 Node 节点,这些节点通过 LinkedList 链表关联,当然你也可以通过 Node 节点构建出链表 next 元素即可。

  • 那么这时候在发生元素碰撞,相同位置的元素就都被存放到链表上了,获取的时候需要对存放多个元素的链表进行遍历获取。


测试


@Test
public void test_hashMap02() {
Map<String, String> map = new HashMap02BySeparateChaining<>();
map.put("01", "花花");
map.put("05", "豆豆");
logger.info("碰撞前 key:{} value:{}", "01", map.get("01"));

// 下标碰撞
map.put("09", "蛋蛋");
map.put("12", "苗苗");
logger.info("碰撞前 key:{} value:{}", "01", map.get("01"));
}


07:21:16.654 [main] INFO cn.bugstack.algorithms.test.AlgorithmsTest - 碰撞前 key:01 value:花花
07:22:44.651 [main] INFO cn.bugstack.algorithms.test.AlgorithmsTest - 碰撞前 key:01 value:花花

Process finished with exit code 0


  • 此时第一次和第二次获取01位置的元素就都是花花了,元素没有被替代。因为此时的元素是被存放到链表上了。


3. 开放寻址


说明:除了对哈希桶上碰撞的索引元素进行拉链存放,还有不引入新的额外的数据结构,只是在哈希桶上存放碰撞元素的方式。它叫开放寻址,也就是 ThreaLocal 中运用斐波那契散列+开放寻址的处理方式。


public class HashMap03ByOpenAddressing<K, V> implements Map<K, V> {

private final Node<K, V>[] tab = new Node[8];

@Override
public void put(K key, V value) {
int idx = key.hashCode() & (tab.length - 1);
if (tab[idx] == null) {
tab[idx] = new Node<>(key, value);
} else {
for (int i = idx; i < tab.length; i++) {
if (tab[i] == null) {
tab[i] = new Node<>(key, value);
break;
}
}
}
}

@Override
public V get(K key) {
int idx = key.hashCode() & (tab.length - 1);
for (int i = idx; i < tab.length; i ++){
if (tab[idx] != null && tab[idx].key == key) {
return tab[idx].value;
}
}
return null;
}

static class Node<K, V> {
final K key;
V value;

public Node(K key, V value) {
this.key = key;
this.value = value;
}

}

}



  • 开放寻址的设计会对碰撞的元素,寻找哈希桶上新的位置,这个位置从当前碰撞位置开始向后寻找,直到找到空的位置存放。

  • 在 ThreadLocal 的实现中会使用斐波那契散列、索引计算累加、启发式清理、探测式清理等操作,以保证尽可能少的碰撞。


测试


@Test
public void test_hashMap03() {
Map<String, String> map = new HashMap03ByOpenAddressing<>();
map.put("01", "花花");
map.put("05", "豆豆");
logger.info("碰撞前 key:{} value:{}", "01", map.get("01"));
// 下标碰撞
map.put("09", "蛋蛋");
map.put("12", "苗苗");
logger.info("碰撞前 key:{} value:{}", "01", map.get("01"));
}


07:20:22.382 [main] INFO cn.bugstack.algorithms.test.AlgorithmsTest - 碰撞前 key:01 value:花花
07:20:22.387 [main] INFO cn.bugstack.algorithms.test.AlgorithmsTest - 碰撞前 key:01 value:花花
07:20:22.387 [main] INFO cn.bugstack.algorithms.test.AlgorithmsTest - 数据结构:HashMap{tab=[null,{"key":"01","value":"花花"},{"key":"09","value":"蛋蛋"},{"key":"12","value":"苗苗"},null,{"key":"05","value":"豆豆"},null,null]}

Process finished with exit code 0


  • 通过测试结果可以看到,开放寻址对碰撞元素的寻址存放,也是可用解决哈希索引冲突问题的。


4. 合并散列


说明:合并散列是开放寻址和单独链接的混合,碰撞的节点在哈希表中链接。此算法适合固定分配内存的哈希桶,通过存放元素时识别哈希桶上的最大空槽位来解决合并哈希中的冲突。


public class HashMap04ByCoalescedHashing<K, V> implements Map<K, V> {

private final Node<K, V>[] tab = new Node[8];

@Override
public void put(K key, V value) {
int idx = key.hashCode() & (tab.length - 1);
if (tab[idx] == null) {
tab[idx] = new Node<>(key, value);
return;
}

int cursor = tab.length - 1;
while (tab[cursor] != null && tab[cursor].key != key) {
--cursor;
}
tab[cursor] = new Node<>(key, value);

// 将碰撞节点指向这个新节点
while (tab[idx].idxOfNext != 0){
idx = tab[idx].idxOfNext;
}

tab[idx].idxOfNext = cursor;
}

@Override
public V get(K key) {
int idx = key.hashCode() & (tab.length - 1);
while (tab[idx].key != key) {
idx = tab[idx].idxOfNext;
}
return tab[idx].value;
}

static class Node<K, V> {
final K key;
V value;
int idxOfNext;

public Node(K key, V value) {
this.key = key;
this.value = value;
}

}

}



  • 合并散列的最大目的在于将碰撞元素链接起来,避免因为需要寻找碰撞元素所发生的循环遍历。也就是A、B元素存放时发生碰撞,那么在找到A元素的时候可以很快的索引到B元素所在的位置。


测试


07:18:43.613 [main] INFO cn.bugstack.algorithms.test.AlgorithmsTest - 碰撞前 key:01 value:花花
07:18:43.618 [main] INFO cn.bugstack.algorithms.test.AlgorithmsTest - 碰撞前 key:01 value:苗苗
07:18:43.619 [main] INFO cn.bugstack.algorithms.test.AlgorithmsTest - 数据结构:HashMap{tab=[null,{"idxOfNext":7,"key":"01","value":"花花"},null,null,null,{"idxOfNext":0,"key":"05","value":"豆豆"},{"idxOfNext":0,"key":"12","value":"苗苗"},{"idxOfNext":6,"key":"09","value":"蛋蛋"}]}

Process finished with exit code 0


  • 相对于直接使用开放寻址,这样的挂在链路指向的方式,可以提升索引的性能。因为在实际的数据存储上,元素的下一个位置不一定空元素,可能已经被其他元素占据,这样就增加了索引的次数。所以使用直接指向地址的方式,会更好的提高索引性能。


5. 杜鹃散列


说明:这个名字起的比较有意思,也代表着它的数据结构。杜鹃鸟在孵化🐣的时候,雏鸟会将其他蛋或幼崽推出巢穴;类似的这个数据结构会使用2组key哈希表,将冲突元素推到另外一个key哈希表中。


private V put(K key, V value, boolean isRehash) {
Object k = maskNull(key);
if (containsKey(k)) {
return null;
}
if (insertEntry(new Entry<K, V>((K) k, value))) {
if (!isRehash) {
size++;
}
return null;
}
rehash(2 * table.length);
return put((K) k, value);
}

private boolean insertEntry(Entry<K, V> e) {
int count = 0;
Entry<K, V> current = e;
int index = hash(hash1, current.key);
while (current != e || count < table.length) {
Entry<K, V> temp = table[index];
if (temp == null) {
table[index] = current;
return true;
}
table[index] = current;
current = temp;
if (index == hash(hash1, current.key)) {
index = hash(hash2, current.key);
} else {
index = hash(hash1, current.key);
}
++count;
}
return false;
}



  • 当多个键映射到同一个单元格时会发生这种情况。杜鹃散列的基本思想是通过使用两个散列函数而不是仅一个散列函数来解决冲突。

  • 这为每个键在哈希表中提供了两个可能的位置。在该算法的一种常用变体中,哈希表被分成两个大小相等的较小的表,每个哈希函数都为这两个表之一提供索引。两个散列函数也可以为单个表提供索引。

  • 在实践中,杜鹃哈希比线性探测慢约 20-30%,线性探测是常用方法中最快的。然而,由于它对搜索时间的最坏情况保证,当需要实时响应率时,杜鹃散列仍然很有价值。杜鹃散列的一个优点是它的无链接列表属性,非常适合 GPU 处理。


测试



07:52:04.010 [main] INFO cn.bugstack.algorithms.test.AlgorithmsTest - 碰撞前 key:01 value:花花
07:52:04.016 [main] INFO cn.bugstack.algorithms.test.AlgorithmsTest - 碰撞前 key:01 value:苗苗
07:52:04.016 [main] INFO cn.bugstack.algorithms.test.AlgorithmsTest - 数据结构:{01=花花, 12=苗苗, 05=豆豆, 09=蛋蛋}

Process finished with exit code 0


  • 从测试结果可以看到,杜鹃散列可以通过两个散列函数解决索引冲突问题。不过这个探测的过程比较耗时。


6. 跳房子散列


说明:跳房子散列是一种基于开放寻址的算法,它结合了杜鹃散列、线性探测和链接的元素,通过桶邻域的概念——任何给定占用桶周围的后续桶,也称为“虚拟”桶。 该算法旨在在哈希表的负载因子增长超过 90% 时提供更好的性能;它还在并发设置中提供了高吞吐量,因此非常适合实现可调整大小的并发哈希表。


public boolean insert(AnyType x) {
if (!isEmpty()) {
return false;
}
int currentPos = findPos(x);
if (currentPos == -1) {
return false;
}
if (array[currentPos] != null) {
x = array[currentPos].element;
array[currentPos].isActive = true;
}
String hope;
if (array[currentPos] != null) {
hope = array[currentPos].hope;
x = array[currentPos].element;
} else {
hope = "10000000";
}
array[currentPos] = new HashEntry<>(x, hope, true);
theSize++;
return true;
}



  • 该算法使用一个包含n 个桶的数组。对于每个桶,它的邻域是H个连续桶的小集合(即索引接近原始散列桶的那些)。邻域的期望属性是在邻域的桶中找到一个项目的成本接近于在桶本身中找到它的成本(例如,通过使邻域中的桶落在同一缓存行中)。在最坏的情况下,邻域的大小必须足以容纳对数个项目(即它必须容纳 log( n ) 个项目),但平均只能是一个常数。如果某个桶的邻域被填满,则调整表的大小。


测试


@Test
public void test_hashMap06() {
HashMap06ByHopscotchHashing<Integer> map = new HashMap06ByHopscotchHashing<>();
map.insert(1);
map.insert(5);
map.insert(9);
map.insert(12);
logger.info("数据结构:{}", map);
}


17:10:10.363 [main] INFO cn.bugstack.algorithms.test.AlgorithmsTest - 数据结构:HashMap{tab=[null,{"element":1,"hope":"11000000","isActive":true},{"element":9,"hope":"00000000","isActive":true},null,{"element":12,"hope":"10000000","isActive":true},{"element":5,"hope":"10000000","isActive":true},null,null]}

Process finished with exit code 0


  • 通过测试可以看到,跳房子散列会在其原始散列数组条目中找到,或者在接下来的H-1个相邻条目之一找到对应的冲突元素。


7. 罗宾汉哈希


说明:罗宾汉哈希是一种基于开放寻址的冲突解决算法;冲突是通过偏向从其“原始位置”(即项目被散列到的存储桶)最远或最长探测序列长度(PSL)的元素的位移来解决的。


public void put(K key, V value) {
Entry entry = new Entry(key, value);
int idx = hash(key);
// 元素碰撞检测
while (table[idx] != null) {
if (entry.offset > table[idx].offset) {
// 当前偏移量不止一个,则查看条目交换位置,entry 是正在查看的条目,增加现在搜索的事物的偏移量和 idx
Entry garbage = table[idx];
table[idx] = entry;
entry = garbage;
idx = increment(idx);
entry.offset++;
} else if (entry.offset == table[idx].offset) {
// 当前偏移量与正在查看的检查键是否相同,如果是则它们交换值,如果不是,则增加 idx 和偏移量并继续
if (table[idx].key.equals(key)) {
// 发现相同值
V oldVal = table[idx].value;
table[idx].value = value;
} else {
idx = increment(idx);
entry.offset++;
}
} else {
// 当前偏移量小于我们正在查看的我们增加 idx 和偏移量并继续
idx = increment(idx);
entry.offset++;
}
}
// 已经到达了 null 所在的 idx,将新/移动的放在这里
table[idx] = entry;
size++;
// 超过负载因子扩容
if (size >= loadFactor * table.length) {
rehash(table.length * 2);
}
}



  • 09、12 和 01 发生哈希索引碰撞,进行偏移量计算调整。通过最长位置探测碰撞元素位移来处理。


测试


public void test_hashMap07() {
Map<String, String> map = new HashMap07ByRobinHoodHashing<>();
map.put("01", "花花");
map.put("05", "豆豆");
logger.info("碰撞前 key:{} value:{}", "01", map.get("01"));
// 下标碰撞
map.put("09", "蛋蛋");
map.put("12", "苗苗");
logger.info("碰撞前 key:{} value:{}", "01", map.get("12"));
logger.info("数据结构:{}", map);
}


07:34:32.593 [main] INFO cn.bugstack.algorithms.test.AlgorithmsTest - 碰撞前 key:01 value:花花
09 1
12 1
01 1
09 9
12 1
05 5
07:35:07.419 [main] INFO cn.bugstack.algorithms.test.AlgorithmsTest - 碰撞前 key:01 value:苗苗
07:35:07.420 [main] INFO cn.bugstack.algorithms.test.AlgorithmsTest - 数据结构:HashMap{tab=[null,{"key":"01","offset":0,"value":"花花"},{"key":"12","offset":1,"value":"苗苗"},null,null,{"key":"05","offset":0,"value":"豆豆"},null,null,null,{"key":"09","offset":0,"value":"蛋蛋"},null,null,null,null,null,null]}

Process finished with exit code 0


  • 通过测试结果和调试的时候可以看到,哈希索引冲突是通过偏向从其“原始位置”(即项目被散列到的存储桶)最远或最长探测序列长度(PSL)的元素的位移来解决。这块可以添加断点调试验证。


四、常见面试问题



  • 介绍一下散列表

  • 为什么使用散列表

  • 拉链寻址和开放寻址的区别

  • 还有其他什么方式可以解决散列哈希索引冲突

  • 对应的Java源码中,对于哈希索引冲突提供了什么样的解决方案

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

高并发技巧-redis和本地缓存使用技巧

在这篇文章中,我主要介绍的是分布式缓存和本地缓存的使用技巧,包括缓存种类介绍,各种的使用场景,以及如何使用,最后再给出实战案例。 众所周知,缓存最主要的目的就是加速访问,缓解数据库压力。最常用的缓存就是分布式缓存,比如redis,在面对大部分并发场景或者一些...
继续阅读 »

在这篇文章中,我主要介绍的是分布式缓存和本地缓存的使用技巧,包括缓存种类介绍,各种的使用场景,以及如何使用,最后再给出实战案例。



众所周知,缓存最主要的目的就是加速访问,缓解数据库压力。最常用的缓存就是分布式缓存,比如redis,在面对大部分并发场景或者一些中小型公司流量没有那么高的情况,使用redis基本都能解决了。但是在流量较高的情况下可能得使用到本地缓存了,比如guava的LoadingCache和快手开源的ReloadableCache。


三种缓存的使用场景


这部分会介绍redis,比如guava的LoadingCache和快手开源的ReloadableCache的使用场景和局限,通过这一部分的介绍就能知道在怎样的业务场景下应该使用哪种缓存,以及为什么。


Redis的使用场景和局限性


如果宽泛的说redis何时使用,那么自然就是用户访问量过高的地方使用,从而加速访问,并且缓解数据库压力。如果细分的话,还得分为单节点问题和非单节点问题。


如果一个页面用户访问量比较高,但是访问的不是同一个资源。比如用户详情页,访问量比较高,但是每个用户的数据都是不一样的,这种情况显然只能用分布式缓存了,如果使用redis,key为用户唯一键,value则是用户信息。


redis导致的缓存击穿


但是需要注意一点,一定要设置过期时间,而且不能设置到同一时间点过期。举个例子,比如用户又个活动页,活动页能看到用户活动期间获奖数据,粗心的人可能会设置用户数据的过期时间点为活动结束,这样会


单(热)点问题


单节点问题说的是redis的单个节点的并发问题,因为对于相同的key会落到redis集群的同一个节点上,那么如果对这个key的访问量过高,那么这个redis节点就存在并发隐患,这个key就称为热key。


如果所有用户访问的都是同一个资源,比如小爱同学app首页对所有用户展示的内容都一样(初期),服务端给h5返回的是同一个大json,显然得使用到缓存。首先我们考虑下用redis是否可行,由于redis存在单点问题,如果流量过大的话,那么所有用户的请求到达redis的同一个节点,需要评估该节点能否抗住这么大流量。我们的规则是,如果单节点qps达到了千级别就要解决单点问题了(即使redis号称能抗住十万级别的qps),最常见的做法就是使用本地缓存。显然小爱app首页流量不过百,使用redis是没问题的。


LoadingCache的使用场景和局限性


对于这上面说的热key问题,我们最直接的做法就是使用本地缓存,比如你最熟悉的guava的LoadingCache,但是使用本地缓存要求能够接受一定的脏数据,因为如果你更新了首页,本地缓存是不会更新的,它只会根据一定的过期策略来重新加载缓存,不过在我们这个场景是完全没问题的,因为一旦在后台推送了首页后就不会再去改变了。即使改变了也没问题,可以设置写过期为半小时,超过半小时重新加载缓存,这种短时间内的脏数据我们是可以接受的。


LoadingCache导致的缓存击穿


虽然说本地缓存和机器上强相关的,虽然代码层面写的是半小时过期,但由于每台机器的启动时间不同,导致缓存的加载时间不同,过期时间也就不同,也就不会所有机器上的请求在同一时间缓存失效后都去请求数据库。但是对于单一一台机器也是会导致缓存穿透的,假如有10台机器,每台1000的qps,只要有一台缓存过期就可能导致这1000个请求同时打到了数据库。这种问题其实比较好解决,但是容易被忽略,也就是在设置LoadingCache的时候使用LoadingCache的load-miss方法,而不是直接判断cache.getIfPresent()== null然后去请求db;前者会加虚拟机层面的锁,保证只有一个请求打到数据库去,从而完美的解决了这个问题。


但是,如果对于实时性要求较高的情况,比如有段时间要经常做活动,我要保证活动页面能近实时更新,也就是运营在后台配置好了活动信息后,需要在C端近实时展示这次配置的活动信息,此时使用LoadingCache肯定就不能满足了。


ReloadableCache的使用场景和局限性


对于上面说的LoadingCache不能解决的实时问题,可以考虑使用ReloadableCache,这是快手开源的一个本地缓存框架,最大的特点是支持多机器同时更新缓存,假设我们修改了首页信息,然后请求打到的是A机器,这个时候重新加载ReloadableCache,然后它会发出通知,监听了同一zk节点的其他机器收到通知后重新更新缓存。使用这个缓存一般的要求是将全量数据加载到本地缓存,所以如果数据量过大肯定会对gc造成压力,这种情况就不能使用了。由于小爱同学首页这个首页是带有状态的,一般online状态的就那么两个,所以完全可以使用ReloadableCache来只装载online状态的首页。


小结


到这里三种缓存基本都介绍完了,做个小结:



  1. 对于非热点的数据访问,比如用户维度的数据,直接使用redis即可;

  2. 对于热点数据的访问,如果流量不是很高,无脑使用redis即可;

  3. 对于热点数据,如果允许一定时间内的脏数据,使用LoadingCache即可;

  4. 对于热点数据,如果一致性要求较高,同时数据量不大的情况,使用ReloadableCache即可;


小技巧


不管哪种本地缓存虽然都带有虚拟机层面的加锁来解决击穿问题,但是意外总有可能以你意想不到的方式发生,保险起见你可以使用两级缓存的方式即本地缓存+redis+db。


缓存使用的简单介绍


这里redis的使用就不再多说了,相信很多人对api的使用比我还熟悉


LoadingCache的使用


这个是guava提供的网上一抓一大把,但是给两点注意事项



  1. 要使用load-miss的话, 要么使用V get(K key, Callable<? extends V> loader);要么使用build的时候使用的是build(CacheLoader<? super K1, V1> loader)这个时候可以直接使用get()了。此外建议使用load-miss,而不是getIfPresent==null的时候再去查数据库,这可能导致缓存击穿;

  2. 使用load-miss是因为这是线程安全的,如果缓存失效的话,多个线程调用get的时候只会有一个线程去db查询,其他线程需要等待,也就是说这是线程安全的。


LoadingCache<String, String> cache = CacheBuilder.newBuilder()
.maximumSize(1000L)
.expireAfterAccess(Duration.ofHours(1L)) // 多久不访问就过期
.expireAfterWrite(Duration.ofHours(1L)) // 多久这个key没修改就过期
.build(new CacheLoader<String, String>() {
@Override
public String load(String key) throws Exception {
// 数据装载方式,一般就是loadDB
return key + " world";
}
});
String value = cache.get("hello"); // 返回hello world

reloadableCache的使用


导入三方依赖


<dependency>
<groupId>com.github.phantomthief</groupId>
<artifactId>zknotify-cache</artifactId>
<version>0.1.22</version>
</dependency>

需要看文档,不然无法使用,有兴趣自己写一个也行的。


public interface ReloadableCache<T> extends Supplier<T> {

/**
* 获取缓存数据
*/
@Override
T get();

/**
* 通知全局缓存更新
* 注意:如果本地缓存没有初始化,本方法并不会初始化本地缓存并重新加载
*
* 如果需要初始化本地缓存,请先调用 {@link ReloadableCache#get()}
*/
void reload();

/**
* 更新本地缓存的本地副本
* 注意:如果本地缓存没有初始化,本方法并不会初始化并刷新本地的缓存
*
* 如果需要初始化本地缓存,请先调用 {@link ReloadableCache#get()}
*/
void reloadLocal();
}

老生常谈的缓存击穿/穿透/雪崩问题


这三个真的是亘古不变的问题,如果流量大确实需要考虑。


缓存击穿


简单说就是缓存失效,导致大量请求同一时间打到了数据库。对于缓存击穿问题上面已经给出了很多解决方案了。



  1. 比如使用本地缓存

  2. 本地缓存使用load-miss方法

  3. 使用第三方服务来加载缓存


1.2和都说过,主要来看3。假如业务愿意只能使用redis而无法使用本地缓存,比如数据量过大,实时性要求比较高。那么当缓存失效的时候就得想办法保证只有少量的请求打到数据库。很自然的就想到了使用分布式锁,理论上说是可行的,但实际上存在隐患。我们的分布式锁相信很多人都是使用redis+lua的方式实现的,并且在while中进行了轮训,这样请求量大,数据多的话会导致无形中让redis成了隐患,并且占了太多业务线程,其实仅仅是引入了分布式锁就加大了复杂度,我们的原则就是能不用就不用。


那么我们是不是可以设计一个类似分布式锁,但是更可靠的rpc服务呢?当调用get方法的时候这个rpc服务保证相同的key打到同一个节点,并且使用synchronized来进行加锁,之后完成数据的加载。在快手提供了一个叫cacheSetter的框架。下面提供一个简易版,自己写也很容易实现。


import com.google.common.collect.Lists;
import org.apache.commons.collections4.CollectionUtils;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CountDownLatch;

/**
* @Description 分布式加载缓存的rpc服务,如果部署了多台机器那么调用端最好使用id做一致性hash保证相同id的请求打到同一台机器。
**/
public abstract class AbstractCacheSetterService implements CacheSetterService {

private final ConcurrentMap<String, CountDownLatch> loadCache = new ConcurrentHashMap<>();

private final Object lock = new Object();

@Override
public void load(Collection<String> needLoadIds) {
if (CollectionUtils.isEmpty(needLoadIds)) {
return;
}
CountDownLatch latch;
Collection<CountDownLatch> loadingLatchList;
synchronized (lock) {
loadingLatchList = excludeLoadingIds(needLoadIds);

needLoadIds = Collections.unmodifiableCollection(needLoadIds);

latch = saveLatch(needLoadIds);
}
System.out.println("needLoadIds:" + needLoadIds);
try {
if (CollectionUtils.isNotEmpty(needLoadIds)) {
loadCache(needLoadIds);
}
} finally {
release(needLoadIds, latch);
block(loadingLatchList);
}

}

/**
* 加锁
* @param loadingLatchList 需要加锁的id对应的CountDownLatch
*/
protected void block(Collection<CountDownLatch> loadingLatchList) {
if (CollectionUtils.isEmpty(loadingLatchList)) {
return;
}
System.out.println("block:" + loadingLatchList);
loadingLatchList.forEach(l -> {
try {
l.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}

/**
* 释放锁
* @param needLoadIds 需要释放锁的id集合
* @param latch 通过该CountDownLatch来释放锁
*/
private void release(Collection<String> needLoadIds, CountDownLatch latch) {
if (CollectionUtils.isEmpty(needLoadIds)) {
return;
}
synchronized (lock) {
needLoadIds.forEach(id -> loadCache.remove(id));
}
if (latch != null) {
latch.countDown();
}
}

/**
* 加载缓存,比如根据id从db查询数据,然后设置到redis中
* @param needLoadIds 加载缓存的id集合
*/
protected abstract void loadCache(Collection<String> needLoadIds);

/**
* 对需要加载缓存的id绑定CountDownLatch,后续相同的id请求来了从map中找到CountDownLatch,并且await,直到该线程加载完了缓存
* @param needLoadIds 能够正在去加载缓存的id集合
* @return 公用的CountDownLatch
*/
protected CountDownLatch saveLatch(Collection<String> needLoadIds) {
if (CollectionUtils.isEmpty(needLoadIds)) {
return null;
}
CountDownLatch latch = new CountDownLatch(1);
needLoadIds.forEach(loadId -> loadCache.put(loadId, latch));
System.out.println("loadCache:" + loadCache);
return latch;
}

/**
* 哪些id正在加载数据,此时持有相同id的线程需要等待
* @param ids 需要加载缓存的id集合
* @return 正在加载的id所对应的CountDownLatch集合
*/
private Collection<CountDownLatch> excludeLoadingIds(Collection<String> ids) {
List<CountDownLatch> loadingLatchList = Lists.newArrayList();
Iterator<String> iterator = ids.iterator();
while (iterator.hasNext()) {
String id = iterator.next();
CountDownLatch latch = loadCache.get(id);
if (latch != null) {
loadingLatchList.add(latch);
iterator.remove();
}
}
System.out.println("loadingLatchList:" + loadingLatchList);
return loadingLatchList;
}
}

业务实现


import java.util.Collection;
public class BizCacheSetterRpcService extends AbstractCacheSetterService {
@Override
protected void loadCache(Collection<String> needLoadIds) {
// 读取db进行处理
// 设置缓存
}
}

缓存穿透


简单来说就是请求的数据在数据库不存在,导致无效请求打穿数据库。


解法也很简单,从db获取数据的方法(getByKey(K key))一定要给个默认值。


比如我有个奖池,金额上限是1W,用户完成任务的时候给他发笔钱,并且使用redis记录下来,并且落表,用户在任务页面能实时看到奖池剩余金额,在任务开始的时候显然奖池金额是不变的,redis和db里面都没有发放金额的记录,这就导致每次必然都去查db,对于这种情况,从db没查出来数据应该缓存个值0到缓存。


缓存雪崩


就是大量缓存集中失效打到了db,当然肯定都是一类的业务缓存,归根到底是代码写的有问题。可以将缓存失效的过期时间打散,别让其集中失效就可以了。


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

摆脱USB线,使用无线连接去开发安卓

前言 工作了大半年,之前一直都需要USB线连接手机才能用Android Studio去调试和安装安卓APP,然后上个礼拜,我突然发现前辈没连无线就可以调试,这让我好奇心一下上来,但又不好意思问,于是搜索了一下关于无线调试的内容,就看到谷歌早就给安卓用上了无线调...
继续阅读 »

前言


工作了大半年,之前一直都需要USB线连接手机才能用Android Studio去调试和安装安卓APP,然后上个礼拜,我突然发现前辈没连无线就可以调试,这让我好奇心一下上来,但又不好意思问,于是搜索了一下关于无线调试的内容,就看到谷歌早就给安卓用上了无线调试,只不过我一直不知道。


image.png
经过我探究了一番,踩了许多坑,最终于今天总算是知道如何稳定的进行无线连接了。


正篇


先感叹一下,不得不说,无线调试真的好用,总算不需要担心线绕来绕去。


一波三折


第一波风雨


为什么我对无线调试这么喜悦,原因就在于我手上的这个手机实在太坑,我几乎半年都被它所折磨,其实我一报型号你们就会明白了,它叫XiaoMi11,将它与电脑连接,结果说是在充电但电量一直在减少,而且那条线也在我一直连接高强度使用后露出了内部结构,所以又换了一根。


第二波浪潮


无线调试功能知晓后,我也是仔细阅读官方文档后,先把adb找到,配置到环境


image.png
我的路径在C:\Users\86152\AppData\Local\Android\Sdk\platform-tools,其实目的就是将这个包含adb.exe的platform-tools路径添加到系统环境变量Path:


image.png
这步完成后,直接win+r,再输入cmd回车之后输入adb devices即可看到自己连接的设备,这时也代表adb已经配好了环境。(本人是在Windows环境下的操作)

接着看看自己手机无线网是不是和电脑在一个局域下,直接ping自己手机ip,如果正常收发包即代表可以无线调试,作者是电脑连着经过路由器的线,而无线网也是该路由器发出的,不一定要电脑也连无线网:


image.png


adb devices

ping 你的手机IP 可以在手机无线调试的地方找到,文中下面的图可以看到

再查看自己手机已经是Android12,是大于官方给出的Android11,于是选择第一种连接方式:


image.png
就是如下图,直接在Android Studio中选择这个功能进行无线连接
image.png
如下图,它提供了两种方式,扫二维码,或者在手机开发者选项的无线调试使用验证码:


image.png
image.png

我又按照文档操作,打开了手机的无线调试:


image.png
结果无论是扫描二维码还是使用验证码都如石沉大海,了无音讯,一直就没成功过,就搁这一直转圈。


60b1098f1ac18e3273cb7dce45ee990.jpg
bb2dbd11f03c0b899bd38a4dc4a0f61.jpg

第三波呼啸


迫不得已,我又看了一些博客的老方法与官网对Android11以下的处理终于连上了,不过我每次都得打开手机无线调试看到IP地址和端口全部输进去才能连,而我就这么傻乎乎的连了一个星期:


6c7581671af66ac12318d52be2c7ed8.jpg

解决方法


所以作者在此不介绍这个不大好用的方法了,直接聊聊最好用的吧:
配置完adb环境后可以重启Android Studio,然后我们打开下图的AS的命令编辑器,同样可以输入adb命令了,当然同样可以ping命令去看看是否可以接通。


image.png
首先我们先输入命令:


image.png
会发现第一次给出failed,再输入一次报already connect即可连接成功


adb connect 手机ip地址(即无线调试中IP地址和端口中":xxxx"前面部分)

下次只要你手机仍连接此WIFI,只需要AS每次打开时输入过上面命令即可,不过在第一次连接时要同意手机上的弹窗哦,不然后面可能无线调试在连WIFI时不会自动开启,需要手动去点。


总结


快快掌握这个方法,让我们开启无线调试的安卓开发生活吧!


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

以感知生命周期的方式观察 Flow 数据

问题 Android 是有生命周期的,在 UI 展示的时候可以接受一些数据更新 UI,在 App 进入后台的时候应该停止接受数据以便释放资源,并且避免一些意想不到的异常; 协程和 Flow 是和 Android 平台无关的 API,正常情况下无法感知 ...
继续阅读 »

问题



  • Android 是有生命周期的,在 UI 展示的时候可以接受一些数据更新 UI,在 App 进入后台的时候应该停止接受数据以便释放资源,并且避免一些意想不到的异常;




  • 协程和 Flow 是和 Android 平台无关的 API,正常情况下无法感知 Android 生命周期方法;


解决方案


我们现在当然是有一些方案来解决上述问题,可能会有以下几种方式。


Flow Lifecycle-aware.gif


方式一:手动取消 Job


Activity 方式:



Fragment 方式:



以上方式显然是比较麻烦的,有一些模板代码。


方式二:使用 repeatOnLifecycle 解决模板代码


Activity 方式:



Fragment 方式:




注:需要添加依赖 androidx.lifecycle:lifecycle-runtime-ktx



这种方式虽然解决了一些模板代码的问题,但是仍然有多层嵌套的问题。


方式三:使用 flowWithLifecycle 解决多层嵌套


Activity 方式:




注:此 API 是在 2.6.0-alpha01 版本中添加。



Fragment 方式类似,使用 flowWithLifecycle 即可。flowWithLifecycle 是新版中新增的一个扩展函数,实现方式如下:



看上去是解决了嵌套的问题,但是并彻底,观察数据的操作还是需要在协程体中进行。


方式四:使用 collectWithLifecycle 彻底解决多层嵌套



自定义扩展函数 collectWithLifecycle 实现大致如下:



这个是我自己的一个扩展,官方库中并没有添加,不过我已经反馈给官方了。这种方式仅仅是解决了嵌套的问题,但是却隐藏了 Flow ,从而导致一系列的操作符将无法使用,不过这个在 UI Element/Compose 中并不是什么大问题。。


扩展阅读


处理 Android UI 的方式之外,Compose 本身也有同样的问题,官方也通过自定义扩展函数的形式添加了支持,大致如下:



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

Flutter3.3对Material3设计风格的支持

在Flutter3.3版本以上,支持Material3,使用Material3样式首先是要配置启用Material3。 Material3 主要体现在 圆角风格、颜色、文本样式等方面。 1 配置启用 Material3 查看当前 Flutter的版本 在程序...
继续阅读 »

在Flutter3.3版本以上,支持Material3,使用Material3样式首先是要配置启用Material3。


Material3 主要体现在 圆角风格、颜色、文本样式等方面。


1 配置启用 Material3


查看当前 Flutter的版本


image.png
在程序的入口 MaterialApp中设置主题ThemeData中的useMaterial3属性为true.


///flutter应用程序中的入口函数
void main() => runApp(TextApp());

///应用的根布局
class TextApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
///构建Materia Desin 风格的应用程序
return MaterialApp(
title: "Material3",
///在主题中启用
theme: ThemeData(
brightness: Brightness.light,
colorSchemeSeed: Colors.blue,
//启用
useMaterial3: true,
),
///默认的首页面
home: Material3Home(),
);
}
}

2 按钮样式的改变


按钮默认的圆角大小改变


2.1 ElevatedButton

ElevatedButton(
onPressed: (){},
child: const Text("Elevated"),
),


image.png


2.2 ElevatedButton 自定义样式

  ElevatedButton(
style: ElevatedButton.styleFrom(
// 前景色
// ignore: deprecated_member_use
onPrimary: Theme.of(context).colorScheme.onPrimary,
// 背景色
// ignore: deprecated_member_use
primary: Theme.of(context).colorScheme.primary,
).copyWith(elevation: ButtonStyleButton.allOrNull(0.0)),
onPressed: (){},
child: const Text('Filled'),
),

image.png


2.3 OutlinedButton

OutlinedButton(
onPressed: (){},
child: const Text("Outlined"),
),

image.png


2.4 FloatingActionButton.small

 FloatingActionButton.small(
onPressed: () {},
child: const Icon(Icons.add),
),

image.png


2.5 FloatingActionButton

FloatingActionButton(
onPressed: () {},
child: const Icon(Icons.add),
),

image.png


2.6 FloatingActionButton.extended

   FloatingActionButton.extended(
onPressed: () {},
icon: const Icon(Icons.add),
label: const Text("Create"),
),

image.png


2.7 FloatingActionButton.large

  FloatingActionButton.large(
onPressed: () {},
child: const Icon(Icons.add),
),

image.png


3 AlertDialog 的边框圆角改变


  void openDialog(BuildContext context) {
showDialog<void>(
context: context,
builder: (context) => AlertDialog(
title: const Text("Basic Dialog Title"),
content: const Text(
"A dialog is a type of modal window that appears in front of app content to provide critical information, or prompt for a decision to be made."),
actions: <Widget>[
TextButton(
child: const Text('Dismiss'),
onPressed: () => Navigator.of(context).pop(),
),
TextButton(
child: const Text('Action'),
onPressed: () => Navigator.of(context).pop(),
),
],
),
);
}

image.png


4 主题文本样式的默认大小改变


final textTheme = Theme.of(context)
.textTheme
.apply(displayColor: Theme.of(context).colorScheme.onSurface);


TextStyle large =textTheme.displayLarge;
TextStyle displayMedium =textTheme.displayMedium;
TextStyle displaySmall =textTheme.displaySmall;


image.png


5 ColorScheme 的变更


Widget buildMaterial() {
///当前颜色主题
ColorScheme colorScheme = Theme.of(context).colorScheme;
//背景色
final Color color = colorScheme.surface;
//阴影色
Color shadowColor = colorScheme.shadow;
Color surfaceTint = colorScheme.primary;
return Material(
borderRadius: BorderRadius.all(Radius.circular(4.0)),
elevation: 4,//阴影
color: color,//背景色
shadowColor: shadowColor,//阴影色
surfaceTintColor: surfaceTint,//
type: MaterialType.card,
child: Padding(
padding: const EdgeInsets.all(38.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
'测试1',
style: Theme.of(context).textTheme.labelMedium,
),
Text(
'测试2',
style: Theme.of(context).textTheme.labelMedium,
),
],
),
),
);
}

image.png


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

环信文档提升计划|提建议找bug,领京东卡,环信文档“捉虫“活动进行中!

文档是帮助开发者更好地使用产品的重要一环。为更好的提升集成环信产品的便捷性和易用性,环信文档进行了重大升级,此次改版主要提升内容: 1.导航点击转跳丝滑顺畅 ;2.使用体验更好的搜索插件;3.开发者可以直接对文档进行反馈,提交github pul...
继续阅读 »

文档是帮助开发者更好地使用产品的重要一环。为更好的提升集成环信产品的便捷性和易用性,环信文档进行了重大升级,此次改版主要提升内容:

1.导航点击转跳丝滑顺畅 ;2.使用体验更好的搜索插件;3.开发者可以直接对文档进行反馈,提交github pull request


此次版本的重大更新离不开环友们的日常反馈和建议需求,目前环信正式发起文档提升计划,邀您一起打造更加丝滑的文档体验。无论你是第一次接触环信的新朋友还是深度用户,都欢迎参与此次计划。我们将有专人跟进您的反馈和建议。在参与过程中,你可以感受到文档一点点被打磨被完善的过程,感受到所提的建议被采纳的乐趣,还能累计积分兑换环信周边礼品。


活动时间:

即日至2024年12月31日


参与方式:

此次活动文档包含环信即时通讯云文档、超级社区文档。

反馈在文档中发现或遇到的问题,或针对文档提出的建议意见,发送邮件至:market@easemob.com

3个工作日内平台进行反馈。

经评估确认后,可获得一定积分奖励,累计的积分可兑换环信周边或京东卡。


积分规则:

错误类型

具体描述

积分

不规范或低错类

错别字

1分

拼写错误

表述不通顺,病句等

文档格式错乱

内容结构不合理、语言表达不清晰

图形、表格、文字等晦涩难懂

2分

描述存在歧义或有误

缺少必要的条件,说明,注意事项等

不合理的文档结构

内容错误或缺失

界面/功能和文档描述不一致

3分

代码有误,无法指导操作

链接错误

关键步骤错误或缺失

响应结果与文档描述不符

文档接口与SDK不符

内容未更新

内容过时,文档接口过时等,无法指导集成步骤

3分


活动规则:

1、请将详细的文档问题描述发送邮件至 market@easemob.com,邮件内容经官方确认属实,邮件反馈您提交的问题及获得的积分;

2、除以上表格罗列的内容以外,您发现的任何问题都可以邮件反馈给我们,我们都会酌情归类计分。

3、本次活动积分有效期至2024年6月30日,您可在活动期间联系官方兑换礼品。


积分兑换

达到以下积分,即可扣除相应积分进行礼品兑换。

积分

奖励

3分

优秀环友定制徽章

5分

环信定制钥匙扣/小风扇2选1

10分

50元京东卡/环信定制保温杯 2选1

15分

100元京东卡


加入官方活动群


扫码备注“捉虫”,添加冬冬好友拉你进群。


相关地址及开发文档获取

即时通讯云文档:https://docs-im-beta.easemob.com/document/android/quickstart.html

环信超级社区文档:https://docs-im.easemob.com/ccim/circle/overview


*本活动解释权归环信所有

收起阅读 »

Sourcery 的 Swift Package 命令行插件

什么是Sourcery?Sourcery 是当下最流行的 Swift 代码生成工具之一。其背后使用了 SwiftSyntax[1],旨在通过自动生成样板代码来节省开发人员的时间。Sourcery 通过扫描一组输入文件,然后借助模板的帮助,自动生成模板中定义的 ...
继续阅读 »

什么是Sourcery?

Sourcery 是当下最流行的 Swift 代码生成工具之一。其背后使用了 SwiftSyntax[1],旨在通过自动生成样板代码来节省开发人员的时间。Sourcery 通过扫描一组输入文件,然后借助模板的帮助,自动生成模板中定义的 Swift 代码。

示例

考虑一个为摄像机会话服务提供公共 API 的协议:

protocol Camera {
func start()
func stop()
func capture(_ completion: @escaping (UIImage?) -> Void)
func rotate()
}

当使用此新的 Camera service 进行单元测试时,我们希望确保 AVCaptureSession 没有被真的创建。我们仅仅希望确认 camera service 被测试系统(SUT)正确的调用了,而不是去测试 camera service 本身。
因此,创建一个协议的 mock 实现,使用空方法和一组变量来帮助我们进行单元测试,并断言(asset)进行了正确的调用是有意义的。这是软件开发中非常常见的一个场景,如果你曾维护过一个包含大量单元测试的大型代码库,这么做也可能有点乏味。
好吧~不用担心!Sourcery 会帮助你!⭐️ 它有一个叫做 AutoMockable[2] 的模板,此模板会为任意输入文件中遵守 AutoMockable 协议的协议生成 mock 实现。

注意:在本文中,我扩展地使用了术语 Mock,因为它与 Sourcery 模板使用的术语一致。Mock 是一个相当重载的术语,但通常,如果我要创建一个 双重测试[3],我会根据它的用途进一步指定类型的名称(可能是 Spy 、 Fake 、 Stub 等)。如果您有兴趣了解更多关于双重测试的信息,马丁·福勒(Martin Fowler)有一篇非常好的文章,可以解释这些差异。

现在,我们让 Camera 遵守 AutoMockable。该接口的唯一目的是充当 Sourcery 的目标,从中查找并生成代码。

import UIKit

// Protocol to be matched
protocol AutoMockable {}

public protocol Camera: AutoMockable {
func start()
func stop()
func capture(_ completion: @escaping (UIImage?) -> Void)
func rotate()
}

此时,可以在上面的输入文件上运行 Sourcery 命令,指定 AutoMockable 模板的路径:

sourcery --sources Camera.swift --templates AutoMockable.stencil --output .

💡 本文通过提供一个 .sourcery.yml 文件来配置 Sourcery 插件。如果提供了配置文件或 Sourcery 可以找到配置文件,则将忽略与其值冲突的所有命令行参数。如果您想了解有关配置文件的更多信息,Sourcery的 repo 中有一节[4]介绍了该主题。
命令执行完毕后,在输出目录下会生成一个 模板名 加 .generated.swift 为后缀的文件。在此例是 ./AutoMockable.generated.swift:

// Generated using Sourcery 1.8.2 — https://github.com/krzysztofzablocki/Sourcery
// DO NOT EDIT
// swiftlint:disable line_length
// swiftlint:disable variable_name

import Foundation
#if os(iOS) || os(tvOS) || os(watchOS)
import UIKit
#elseif os(OSX)
import AppKit
#endif

class CameraMock: Camera {

//MARK: - start

var startCallsCount = 0
var startCalled: Bool {
return startCallsCount > 0
}
var startClosure: (() -> Void)?

func start() {
startCallsCount += 1
startClosure?()
}

//MARK: - stop

var stopCallsCount = 0
var stopCalled: Bool {
return stopCallsCount > 0
}
var stopClosure: (() -> Void)?

func stop() {
stopCallsCount += 1
stopClosure?()
}

//MARK: - capture

var captureCallsCount = 0
var captureCalled: Bool {
return captureCallsCount > 0
}
var captureReceivedCompletion: ((UIImage?) -> Void)?
var captureReceivedInvocations: [((UIImage?) -> Void)] = []
var captureClosure: ((@escaping (UIImage?) -> Void) -> Void)?

func capture(_ completion: @escaping (UIImage?) -> Void) {
captureCallsCount += 1
captureReceivedCompletion = completion
captureReceivedInvocations.append(completion)
captureClosure?(completion)
}

//MARK: - rotate

var rotateCallsCount = 0
var rotateCalled: Bool {
return rotateCallsCount > 0
}
var rotateClosure: (() -> Void)?

func rotate() {
rotateCallsCount += 1
rotateClosure?()
}

}

上面的文件(AutoMockable.generated.swift)包含了你对mock的期望:使用空方法实现与目标协议的一致性,以及检查是否调用了这些协议方法的一组变量。最棒的是… Sourcery 为您编写了这一切!🎉

怎么运行 Sourcery?

怎么使用 Swift package 运行 Sourcery?
至此你可能在想如何以及怎样在 Swift package 中运行 Sourcery。你可以手动执行,然后讲文件拖到包中,或者从包目录中的命令运行脚本。但是对于 Swift Package 有两种内置方式运行可执行文件:
通过命令行插件,可根据用户输入任意运行
通过构建工具插件,该插件作为构建过程的一部分运行。
在本文中,我将介绍 Sourcery 命令行插件,但我已经在编写第二部分,其中我将创建构建工具插件,这带来了许多有趣的挑战。

创建插件包

让我们首先创建一个空包,并去掉测试和其他我们现在不需要的文件夹。然后我们可以创建一个新的插件 Target 并添加 Sourcery 的二进制文件作为其依赖项。
为了让消费者使用这个插件,它还需要被定义为一个产品:

// swift-tools-version: 5.6
import PackageDescription

let package = Package(
name: "SourceryPlugins",
products: [
.plugin(name: "SourceryCommand", targets: ["SourceryCommand"])
],
targets: [
// 1
.plugin(
name: "SourceryCommand",
// 2
capability: .command(
intent: .custom(verb: "sourcery-code-generation", description: "Generates Swift files from a given set of inputs"),
// 3
permissions: [.writeToPackageDirectory(reason: "Need access to the package directory to generate files")]
),
dependencies: ["Sourcery"]
),
// 4
.binaryTarget(
name: "Sourcery",
path: "Sourcery.artifactbundle"
)
]
)

让我们一步一步地仔细查看上面的代码:

1.定义插件目标。
2.以 custom 为意图,定义了 .command 功能,因为没有任何默认功能( documentationGeneration 和 sourceCodeFormatting)与该命令的用例匹配。给动词一个合理的名称很重要,因为这是从命令行调用插件的方式。
3.插件需要向用户请求写入包目录的权限,因为生成的文件将被转储到该目录。
4.为插件定义了一个二进制目标文件。这将允许插件通过其上下文访问可执行文件。
💡 我知道我并没有详细介绍上面的一些概念,但如果您想了解更多关于命令插件的信息,这里有一篇由 Tibor Bödecs 写的超级棒的文章⭐。如果你还想了解更多关于 Swift Packages 中二级制的目标(文件),我同样有一篇现今 Swift 包中的二进制目标。

编写插件

现在已经创建了包,是时候编写一些代码了!我们首先在 Plugins/SourceryCommand 下创建一个名为 SourceryCommand.swift 的文件,然后添加一个 CommandPlugin 协议的结构体,这将作为该插件的入口:

import PackagePlugin
import Foundation

@main
struct SourceryCommand: CommandPlugin {
func performCommand(context: PluginContext, arguments: [String]) async throws {

}
}

然后我们为命令编写实现:

func performCommand(context: PluginContext, arguments: [String]) async throws {
// 1
let configFilePath = context.package.directory.appending(subpath: ".sourcery.yml").string
guard FileManager.default.fileExists(atPath: configFilePath) else {
Diagnostics.error("Could not find config at: \(configFilePath)")
return
}

//2
let sourceryExecutable = try context.tool(named: "sourcery")
let sourceryURL = URL(fileURLWithPath: sourceryExecutable.path.string)

// 3
let process = Process()
process.executableURL = sourceryURL

// 4
process.arguments = [
"--disableCache"
]

// 5
try process.run()
process.waitUntilExit()

// 6
let gracefulExit = process.terminationReason == .exit && process.terminationStatus == 0
if !gracefulExit {
Diagnostics.error("🛑 The plugin execution failed")
}
}

让我们仔细看看上面的代码:

1.首先 .sourcery.yml 文件必须在包的根目录,否则将报错。这将使 Sourcery 神奇的工作,并使包可配置。
2.可执行文件路径的 URL 是从命令的上下文中检索的。
3.创建一个进程,并将 Sourcery 的可执行文件的 URL 设置为其可执行文件路径。
4.这一步有点麻烦。Sourcery 使用缓存来减少后续运行的代码生成时间,但问题是这些缓存是在包文件夹之外读取和写入的文件。插件的沙箱规则不允许这样做,因此 --disableCache 标志用于禁用此行为并允许命令运行。
5.进程同步运行并等待。
6.最后,检查进程终止状态和代码,以确保进程已正常退出。在任何其他情况下,通过 Diagnostics API 向用户告知错误。
就这样!现在让我们使用它

使用(插件)包

考虑一个用户正在使用插件,该插件将依赖项引入了他们的 Package.swift 文件:

// swift-tools-version: 5.6
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "SourceryPluginSample",
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "SourceryPluginSample",
targets: ["SourceryPluginSample"]),
],
dependencies: [
.package(url: "https://github.com/pol-piella/sourcery-plugins.git", branch: "main")
],
targets: [
.target(
name: "SourceryPluginSample",
dependencies: [],
exclude: ["SourceryTemplates"]
),
]
)

💡 注意,与构建工具插件不同,命令插件不需要应用于任何目标,因为它们需要手动运行。
用户只使用了上面的 AutoMockable 模板(可以在 Sources/SourceryPluginSample/SourceryTemplates 下找到),与本文前面显示的示例相匹配:

protocol AutoMockable {}

protocol Camera: AutoMockable {
func start()
func stop()
func capture(_ completion: @escaping (UIImage?) -> Void)
func rotate()
}

根据插件的要求,用户还提供了一个位于 SourceryPluginSample 目录下的 .sourcery.yml 配置文件:

sources:
- Sources/SourceryPluginSample
templates:
- Sources/SourceryPluginSample/SourceryTemplates
output: Sources/SourceryPluginSample

运行命令

用户已经设置好了,但是他们现在如何运行包?🤔 有两种方法:

命令行

运行插件的一种方法是用命令行。可以通过从包目录中运行 swift package plugin --list 来检索特定包的可用插件列表。然后可以从列表中选择一个包,并通过运行 swift package <command's verb> 来执行,在这个特殊的例子中,运行: swift package sourcery-code-generation。
注意,由于此包需要特殊权限,因此 --allow-writing-to-package-directory 必须与命令一起使用。
此时,你可能会想,为什么我要费心编写一个插件,仍然必须从命令行运行,而我可以用一个简单的脚本在几行 bash 中完成相同的工作?好吧,让我们来看看 Xcode 14 中会出现什么,你会明白为什么我会提倡编写插件📦。

Xcode

这是运行命令插件最令人兴奋的方式,但不幸的是,它仅在 Xcode 14 中可用。因此,如果您需要运行命令,但尚未使用 Xcode 14,请参阅命令行部分。
如果你正好在使用 Xcode 14,你可以通过在文件资源管理器中右键单击包,从列表中找到要执行的插件,然后单击它来执行包的任何命令。

下一步

这是插件的初始实现。我将研究如何改进它,使它更加健壮。和往常一样,我非常致力于公开构建,并使我的文章中的所有内容都开源,这样任何人都可以提交问题或创建任何具有改进或修复的 PRs。这没有什么不同😀, 这是 公共仓库的链接。
此外,如果您喜欢这篇文章,请关注即将到来的第二部分,其中我将制作一个 Sourcery 构建工具插件。我知道这听起来不多,但这不是一项容易的任务!

收起阅读 »

后端一次给你10万条数据,如何优雅展示,到底考察我什么

web
前言大家好,我是林三心,用最通俗的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心,今天跟大家来唠唠嗑,如果后端真的返回给前端10万条数据,咱们前端要怎么优雅地展示出来呢?(哈哈假设后端真的能传10万条数据到前端)前置工作先把前置工作给做好,后面才能进...
继续阅读 »

前言

大家好,我是林三心,用最通俗的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心,今天跟大家来唠唠嗑,如果后端真的返回给前端10万条数据,咱们前端要怎么优雅地展示出来呢?(哈哈假设后端真的能传10万条数据到前端)


前置工作

先把前置工作给做好,后面才能进行测试

后端搭建

新建一个server.js文件,简单起个服务,并返回给前端10w条数据,并通过nodemon server.js开启服务

没有安装nodemon的同学可以先全局安装npm i nodemon -g

// server.js

const http = require('http')
const port = 8000;

http.createServer(function (req, res) {
 // 开启Cors
 res.writeHead(200, {
   //设置允许跨域的域名,也可设置*允许所有域名
   'Access-Control-Allow-Origin': '*',
   //跨域允许的请求方法,也可设置*允许所有方法
   "Access-Control-Allow-Methods": "DELETE,PUT,POST,GET,OPTIONS",
   //允许的header类型
   'Access-Control-Allow-Headers': 'Content-Type'
})
 let list = []
 let num = 0

 // 生成10万条数据的list
 for (let i = 0; i < 100000; i++) {
   num++
   list.push({
     src: 'https://p3-passport.byteacctimg.com/img/user-avatar/d71c38d1682c543b33f8d716b3b734ca~300x300.image',
     text: `我是${num}号嘉宾林三心`,
     tid: num
  })
}
 res.end(JSON.stringify(list));
}).listen(port, function () {
 console.log('server is listening on port ' + port);
})

前端页面

先新建一个index.html

// index.html

// 样式
<style>
   * {
     padding: 0;
     margin: 0;
  }
   #container {
     height: 100vh;
     overflow: auto;
  }
  .sunshine {
     display: flex;
     padding: 10px;
  }
   img {
     width: 150px;
     height: 150px;
  }
 </style>

// html部分
<body>
 <div id="container">
 </div>
 <script src="./index.js"></script>
</body>

然后新建一个index.js文件,封装一个AJAX函数,用来请求这10w条数据

// index.js

// 请求函数
const getList = () => {
   return new Promise((resolve, reject) => {
       //步骤一:创建异步对象
       var ajax = new XMLHttpRequest();
       //步骤二:设置请求的url参数,参数一是请求的类型,参数二是请求的url,可以带参数
       ajax.open('get', 'http://127.0.0.1:8000');
       //步骤三:发送请求
       ajax.send();
       //步骤四:注册事件 onreadystatechange 状态改变就会调用
       ajax.onreadystatechange = function () {
           if (ajax.readyState == 4 && ajax.status == 200) {
               //步骤五 如果能够进到这个判断 说明 数据 完美的回来了,并且请求的页面是存在的
               resolve(JSON.parse(ajax.responseText))
          }
      }
  })
}

// 获取container对象
const container = document.getElementById('container')

直接渲染

最直接的方式就是直接渲染出来,但是这样的做法肯定是不可取的,因为一次性渲染出10w个节点,是非常耗时间的,咱们可以来看一下耗时,差不多要消耗12秒,非常消耗时间


const renderList = async () => {
   console.time('列表时间')
   const list = await getList()
   list.forEach(item => {
       const div = document.createElement('div')
       div.className = 'sunshine'
       div.innerHTML = `<img src="${item.src}" /><span>${item.text}</span>`
       container.appendChild(div)
  })
   console.timeEnd('列表时间')
}
renderList()

setTimeout分页渲染

这个方法就是,把10w按照每页数量limit分成总共Math.ceil(total / limit)页,然后利用setTimeout,每次渲染1页数据,这样的话,渲染出首页数据的时间大大缩减了


const renderList = async () => {
   console.time('列表时间')
   const list = await getList()
   console.log(list)
   const total = list.length
   const page = 0
   const limit = 200
   const totalPage = Math.ceil(total / limit)

   const render = (page) => {
       if (page >= totalPage) return
       setTimeout(() => {
           for (let i = page * limit; i < page * limit + limit; i++) {
               const item = list[i]
               const div = document.createElement('div')
               div.className = 'sunshine'
               div.innerHTML = `<img src="${item.src}" /><span>${item.text}</span>`
               container.appendChild(div)
          }
           render(page + 1)
      }, 0)
  }
   render(page)
   console.timeEnd('列表时间')
}

requestAnimationFrame

使用requestAnimationFrame代替setTimeout,减少了重排的次数,极大提高了性能,建议大家在渲染方面多使用requestAnimationFrame

const renderList = async () => {
   console.time('列表时间')
   const list = await getList()
   console.log(list)
   const total = list.length
   const page = 0
   const limit = 200
   const totalPage = Math.ceil(total / limit)

   const render = (page) => {
       if (page >= totalPage) return
       // 使用requestAnimationFrame代替setTimeout
       requestAnimationFrame(() => {
           for (let i = page * limit; i < page * limit + limit; i++) {
               const item = list[i]
               const div = document.createElement('div')
               div.className = 'sunshine'
               div.innerHTML = `<img src="${item.src}" /><span>${item.text}</span>`
               container.appendChild(div)
          }
           render(page + 1)
      })
  }
   render(page)
   console.timeEnd('列表时间')
}

文档碎片 + requestAnimationFrame

文档碎片的好处

  • 1、之前都是每次创建一个div标签就appendChild一次,但是有了文档碎片可以先把1页的div标签先放进文档碎片中,然后一次性appendChildcontainer中,这样减少了appendChild的次数,极大提高了性能

  • 2、页面只会渲染文档碎片包裹着的元素,而不会渲染文档碎片

const renderList = async () => {
   console.time('列表时间')
   const list = await getList()
   console.log(list)
   const total = list.length
   const page = 0
   const limit = 200
   const totalPage = Math.ceil(total / limit)

   const render = (page) => {
       if (page >= totalPage) return
       requestAnimationFrame(() => {
           // 创建一个文档碎片
           const fragment = document.createDocumentFragment()
           for (let i = page * limit; i < page * limit + limit; i++) {
               const item = list[i]
               const div = document.createElement('div')
               div.className = 'sunshine'
               div.innerHTML = `<img src="${item.src}" /><span>${item.text}</span>`
               // 先塞进文档碎片
               fragment.appendChild(div)
          }
           // 一次性appendChild
           container.appendChild(fragment)
           render(page + 1)
      })
  }
   render(page)
   console.timeEnd('列表时间')
}

懒加载

为了比较通俗的讲解,咱们启动一个vue前端项目,后端服务还是开着

其实实现原理很简单,咱们通过一张图来展示,就是在列表尾部放一个空节点blank,然后先渲染第1页数据,向上滚动,等到blank出现在视图中,就说明到底了,这时候再加载第二页,往后以此类推。

至于怎么判断blank出现在视图上,可以使用getBoundingClientRect方法获取top属性

IntersectionObserver 性能更好,但是我这里就拿getBoundingClientRect来举例


<script setup lang="ts">
import { onMounted, ref, computed } from 'vue'
const getList = () => {
 // 跟上面一样的代码
}

const container = ref<HTMLElement>() // container节点
const blank = ref<HTMLElement>() // blank节点
const list = ref<any>([]) // 列表
const page = ref(1) // 当前页数
const limit = 200 // 一页展示
// 最大页数
const maxPage = computed(() => Math.ceil(list.value.length / limit))
// 真实展示的列表
const showList = computed(() => list.value.slice(0, page.value * limit))
const handleScroll = () => {
 // 当前页数与最大页数的比较
 if (page.value > maxPage.value) return
 const clientHeight = container.value?.clientHeight
 const blankTop = blank.value?.getBoundingClientRect().top
 if (clientHeight === blankTop) {
   // blank出现在视图,则当前页数加1
   page.value++
}
}

onMounted(async () => {
 const res = await getList()
 list.value = res
})
</script>

<template>
 <div id="container" @scroll="handleScroll" ref="container">
   <div class="sunshine" v-for="(item) in showList" :key="item.tid">
     <img :src="item.src" />
     <span>{{ item.text }}</span>
   </div>
   <div ref="blank"></div>
 </div>
</template>

结语

如果你觉得此文对你有一丁点帮助,点个赞,鼓励一下林三心哈哈。

作者:Sunshine_Lin
来源:juejin.cn/post/7031923575044964389

收起阅读 »

超好用的官方core-ktx库,了解一下(终)~

ktx
Handler.postDelayed()简化lambda传入 不知道大家在使用Handler下的postDelayed()方法是不是感觉很不简洁,我们看下这个函数源码: public final boolean postDelayed(@NonNull Ru...
继续阅读 »

Handler.postDelayed()简化lambda传入


不知道大家在使用Handler下的postDelayed()方法是不是感觉很不简洁,我们看下这个函数源码:


public final boolean postDelayed(@NonNull Runnable r, long delayMillis) {
return sendMessageDelayed(getPostMessage(r), delayMillis);
}

可以看到Runnable类型的参数r放在第一位,在Kotlin中我们就无法利用其提供的简洁的语法糖,只能这样使用:


private fun test11(handler: Handler) {
handler.postDelayed({
//编写代码逻辑
}, 100)
}

有没有感觉很别扭,估计官方也发现了这个问题,就提供了这样一个扩展方法:


public inline fun Handler.postDelayed(
delayInMillis: Long,
token: Any? = null,
crossinline action: () -> Unit
): Runnable {
val runnable = Runnable { action() }
if (token == null) {
postDelayed(runnable, delayInMillis)
} else {
HandlerCompat.postDelayed(this, runnable, token, delayInMillis)
}
return runnable
}

可以看到将函数类型(相当于上面的Runnable中的代码执行逻辑)放到了方法参数的最后一位,这样利用kotlin的语法糖就可以这样使用:


private fun test11(handler: Handler) {
handler.postDelayed(200) {

}
}

可以看到这个函数类型使用了crossinline修饰,这个是用来加强内联的,因为其另一个Runnable的函数类型中进行了调用,这样我们就无法在这个函数类型action中使用return关键字了(return@标签除外),避免使用return关键字带来返回上的歧义不稳定性


除此之外,官方core-ktx还提供了类似的扩展方法postAtTime()方法,使用和上面一样!!


Context.getSystemService()泛型实化获取系统服务


看下以往我们怎么获取ClipboardManager:


private fun test11() {
val cm: ClipboardManager = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
}

看下官方提供的方法:


public inline fun <reified T : Any> Context.getSystemService(): T? =
ContextCompat.getSystemService(this, T::class.java)

借助于内联泛型实化简化了获取系统服务的代码逻辑:


private fun test11() {
val cm: ClipboardManager? = getSystemService()
}

泛型实化的用处有很多应用场景,大家感兴趣可以参考我另一篇文章:Gson序列化的TypeToken写起来太麻烦?优化它


Context.withStyledAttributes简化操作自定义属性


这个扩展一般只有在自定义View中较常使用,比如读取xml中设置的属性值,先看下我们平常是如何使用的:


private fun test11(
@NonNull context: Context,
@Nullable attrs: AttributeSet,
defStyleAttr: Int
) {
val ta = context.obtainStyledAttributes(
attrs, androidx.cardview.R.styleable.CardView, defStyleAttr,
androidx.cardview.R.style.CardView
)
//获取属性执行对应的操作逻辑
val tmp = ta.getColorStateList(androidx.cardview.R.styleable.CardView_cardBackgroundColor)

ta.recycle()
}

在获取完属性值后,还需要调用recycle()方法回收TypeArray,这个一旦忘记写就不好了,能让程序保证的写法那就尽量避免人为处理,所以官方提供了下面的扩展方法:


public inline fun Context.withStyledAttributes(
@StyleRes resourceId: Int,
attrs: IntArray,
block: TypedArray.() -> Unit
) {
obtainStyledAttributes(resourceId, attrs).apply(block).recycle()
}

使用如下:


private fun test11(
@NonNull context: Context,
@Nullable attrs: AttributeSet,
defStyleAttr: Int
) {
context.withStyledAttributes(
attrs, androidx.cardview.R.styleable.CardView, defStyleAttr,
androidx.cardview.R.style.CardView
) {
val tmp = getColorStateList(androidx.cardview.R.styleable.CardView_cardBackgroundColor)
}
}

上面的写法就保证了recycle()不会漏写,并且带接收者的函数类型block: TypedArray.() -> Unit也能让我们省略this直接调用TypeArray中的公共方法。


SQLiteDatabase.transaction()自动开启事务读写数据库


平常对SQLite进行写操作时为了效率及安全保证需要开启事务,一般我们都会手动进行开启和关闭,还是那句老话,能程序自动保证的事情就尽量避免手动实现,所以一般我们都会封装一个事务开启和关闭的方法,如下:


private fun writeSQL(sql: String) {
SQLiteDatabase.beginTransaction()
//执行sql写入语句
SQLiteDatabase.endTransaction()
}

官方core-ktx也提供了相似的扩展方法:


public inline fun <T> SQLiteDatabase.transaction(
exclusive: Boolean = true,
body: SQLiteDatabase.() -> T
): T {
if (exclusive) {
beginTransaction()
} else {
beginTransactionNonExclusive()
}
try {
val result = body()
setTransactionSuccessful()
return result
} finally {
endTransaction()
}
}

大家可以自行选择使用!


<K : Any, V : Any> lruCache()简化创建LruCache


LruCache一般用作数据缓存,里面使用了LRU算法来优先淘汰那些近期最少使用的数据。在Android开发中,我们可以使用其设计一个Bitmap缓存池,感兴趣的可以参考Glide内存缓存这块的源码,就利用了LruCache实现。


相比较于原有创建LruCache的方式,官方库提供了下面的扩展方法简化其创建流程:


inline fun <K : Any, V : Any> lruCache(
maxSize: Int,
crossinline sizeOf: (key: K, value: V) -> Int = { _, _ -> 1 },
@Suppress("USELESS_CAST")
crossinline create: (key: K) -> V? = { null as V? },
crossinline onEntryRemoved: (evicted: Boolean, key: K, oldValue: V, newValue: V?) -> Unit =
{ _, _, _, _ -> }
): LruCache<K, V> {
return object : LruCache<K, V>(maxSize) {
override fun sizeOf(key: K, value: V) = sizeOf(key, value)
override fun create(key: K) = create(key)
override fun entryRemoved(evicted: Boolean, key: K, oldValue: V, newValue: V?) {
onEntryRemoved(evicted, key, oldValue, newValue)
}
}
}

看下使用:


private fun createLRU() {
lruCache<String, Bitmap>(3072, sizeOf = { _, value ->
value.byteCount
}, onEntryRemoved = { evicted: Boolean, key: String, oldValue: Bitmap, newValue: Bitmap? ->
//缓存对象被移除的回调方法
})
}

可以看到,比之手动创建LruCache要稍微简单些,能稍微节省下使用成本。


bundleOf()快捷写入并创建Bundle对象


image.png


bundleOf()方法的参数被vararg声明,代表一个可变的参数类型,参数具体的类型为Pair,这个对象我们之前的文章有讲过,可以借助中缀表达式函数to完成Pair的创建:


private fun test12() {
val bundle = bundleOf("a" to "a", "b" to 10)
}

这种通过传入可变参数实现的Bundle如果大家不太喜欢,还可以考虑自行封装通用扩展函数,在函数类型即lambda中实现更加灵活的Bundle创建及写入:


1.自定义运算符重载方法set实现Bundle写入:


operator fun Bundle.set(key: String, value: Any?) {
when (value) {
null -> putString(key, null)

is Boolean -> putBoolean(key, value)
is Byte -> putByte(key, value)
is Char -> putChar(key, value)
is Double -> putDouble(key, value)
is Float -> putFloat(key, value)
is Int -> putInt(key, value)
is Long -> putLong(key, value)
is Short -> putShort(key, value)

is Serializable -> putSerializable(key, value)
//其实数据类型自定参考bundleOf源码实现
}
}

2.自定义BundleBuild支持向Bundle写入多个值


class BundleBuild(private val bundle: Bundle) {

infix fun String.to(that: Any?) {
bundle[this] = that
}
}

其中to()方法使用了中缀表达式的写法


3.暴漏扩展方法实现在lambda中完成Bundle的写入和创建


private fun bundleOf(block: BundleBuild.() -> Unit): Bundle {
return Bundle().apply {
BundleBuild(this).apply(block)
}
}

然后就可以这样使用:


private fun test12() {
val bundle = bundleOf {
"a" to "haha"
//经过一些逻辑操作获取结果后在写入Bundle
val t1 = 10 * 5
val t2 = ""
t2 to t1
}
}

相比较于官方库提供的bundleOf()提供的创建方式,通过函数类型也就是lambda创建并写入Bundle的方式更加灵活,并内部支持执行操作逻辑获取结果后再进行写入。


总结


关于官方core-ktx的研究基本上已经七七八八了,总共输出了五篇相关文章,对该库了解能够节省我们编写模板代码的时间,提高开发效率,大家如果感觉写的不错,可以点个赞支持下哈,感谢!!


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

【Android爬坑周记】用SplashScreen做一个会动的开屏!

Android 12以上加入了SplashScreen,并且支持开屏动画了!因此我在【小鹅事务所】项目中加入了一个开屏动画,如下(为方便动图展示,我故意延长了几秒钟): SplashScreen 简单介绍一下SplashScreen,仅在冷启动或者温启动的时...
继续阅读 »

Android 12以上加入了SplashScreen,并且支持开屏动画了!因此我在【小鹅事务所】项目中加入了一个开屏动画,如下(为方便动图展示,我故意延长了几秒钟):


开屏.gif


SplashScreen


简单介绍一下SplashScreen,仅在冷启动或者温启动的时候会展示SplashScreen,支持VAD动画、帧动画。我就先使用帧动画实现这个开屏动画,后面会考虑换成VAD动画。关于SplashScreen具体就不细讲啦,我讲这些讲不明白,没有官方文档讲得好,直接进入实战!!


注意裁切


image_bbWXUd5R2b.png


ICON在设计的时候只能够占用三分之二大小的圆,超出这部分的会被裁切掉,所以这点需要注意!


设计


首先打开UI设计软件,我此处用Figma,新建一个方形的框框,方形的框框里面整一个三分二大小的圆圈,像这样。


image_zN3wn3Qj0_.png


然后呢,就把设计好的Icon放进去


image_nPXCuEjgOv.png


这个时候一张静态图就做好啦,但是帧动画需要让图片动起来的话,就需要多张静态图。怎么设计它动起来呢?我的思路是让它扭头!像这样。


image_bKPGvPkiso.png


然后再把框框的颜色隐藏掉,我们只需要透明背景的Icon


image_9vcZawLYyF.png


注意,为了展示外边需要留空间,我给它们的框框加上描边,实际不需要!这个时候就可以导出图片啦,我这边选择导出矢量图,也就是SVG格式。


image_tg-XgUBqeW.png


导入动画


打开Android Studio,右键点击res → new → Vector Asset,再导入图片,将静态图都导进去就可以做动画啦。


image_7DgCf5ExTe.png


新建anim_little_goose.xml,根标签是animation-list,并在里面放4个item。


<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="false">
<item
android:drawable="@drawable/ic_little_goose"
android:duration="50" />
<item
android:drawable="@drawable/ic_little_goose_back"
android:duration="150" />
<item
android:drawable="@drawable/ic_little_goose"
android:duration="50" />
<item
android:drawable="@drawable/ic_little_goose_fore"
android:duration="150" />
</animation-list>

根据命名可以看出




  • 第一帧为正常的小鹅,展示50毫秒




  • 第二帧为向后扭头的小鹅,展示150毫秒




  • 第三帧为正常的小鹅,展示50毫秒




  • 第四帧为向前扭头的小鹅,展示150毫秒




一次循环就是400毫秒,点开Split界面,就能在右边预览动画了,这个时候,动画就简简单单做好了。


As.gif


SplashScreen


引入依赖


由于SplashScreen是Android12以上才有的,而Android12以下需要适配,但是!Jetpack提供了同名适配库,去gradle引用就好了。


//SplashScreen
implementation 'androidx.core:core-splashscreen:1.0.0'

设置开屏主题


然后在res/values/themes中新建一个style标签,并将其父标签设为Theme.SplashScreen,需要注意的是,如果适配了黑夜模式的话,也可以在values-night/themes文件下单独配置。


<style name="Theme.AppSplashScreen" parent="Theme.SplashScreen">
<item name="windowSplashScreenBackground">@color/primary_color</item>
<item name="windowSplashScreenAnimatedIcon">@drawable/anim_little_goose</item>
<item name="windowSplashScreenAnimationDuration">3000</item>
<item name="postSplashScreenTheme">@style/Theme.Account</item>
</style>

我这边配置一个无ICON背景的动画,因此不用windowSplashScreenIconBackgroundColor标签设置ICON背景。


简单介绍一下我设置的4个标签




  • windowSplashScreenBackground设置整个开屏动画的背景颜色。




  • windowSplashScreenAnimatedIcon设置的是开屏动画播放的动画文件,也就是上面写的动画文件。




  • windowSplashScreenAnimationDuration设置的是动画的播放时长,也就是说小鹅抖三秒钟头就会停止播放。




  • postSplashScreenTheme这个设置的是开屏动画播放完需要回到的主题,此处设置了我的主题。


    <style name="Theme.Account" parent="Theme.MaterialComponents.DayNight.NoActionBar">
    ...
    </style>



在Manifest注册


    <application
android:label="@string/app_name"
...
android:theme="@style/Theme.Account">

...
<activity
android:name=".ui.MainActivity"
android:theme="@style/Theme.AppSplashScreen"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

</application>

可以看到在打开应用打开的第一个Activity,即MainActivity中设置了开屏主题,而在Application中设置了自己的主题。在Application设置主题的话,这个Application中的除了特殊设置Theme的Activity,其它都默认使用Application主题。


去MainActivity吧!


class MainActivity : BaseActivity() {

private val binding by viewBinding(ActivityMainBinding::inflate)

override fun onCreate(savedInstanceState: Bundle?) {
val splashScreen = installSplashScreen()
splashScreen.setKeepOnScreenCondition { !isAppInit }
super.onCreate(savedInstanceState)
initView()
}
...

}

重写onCreate函数,并在调用super.onCreate之前加载SplashScreen,即调用installSplashScreen,获得一个splashScreen实例,理论上来说调用installSplashScreen函数已经可以实现开屏动画了,可是我想等到一部分数据加载完再进入APP怎么办?


可以看到我调用了setKeepOnScreenCondition 函数,传入一个接口,这个接口返回一个Boolean值,如果返回true则继续展示开屏,如果返回false则进入APP。而此函数在每次绘制之前都会调用,是主线程调用的,因此不能在这里处理太多东西阻塞主线程!


我这边就设置了一个顶层变量,每次都去看看这个顶层变量的值,不会阻塞主线程。


class AccountApplication : Application() {

val supervisorScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)

//初始化数据 防止第一次打开还要加载
private fun initData() {
supervisorScope.launch {
val initIconDataDeferred = async { TransactionIconHelper.initIconData() }
val initTransactionDeferred = async { TransactionHelper.initTransaction() }
val initScheduleDeferred = async { ScheduleHelper.initSchedule() }
val initNoteDeferred = async { NoteHelper.initNote() }
val initMemorialsDeferred = async { MemorialHelper.initMemorials() }
val initTopMemorialDeferred = async { MemorialHelper.initTopMemorial() }
val initDataStoreDeferred = async { DataStoreHelper.INSTANCE.initDataStore() }
initIconDataDeferred.await()
initTransactionDeferred.await()
initScheduleDeferred.await()
initNoteDeferred.await()
initMemorialsDeferred.await()
initTopMemorialDeferred.await()
initDataStoreDeferred.await()
isAppInit = true
}
}
}

var isAppInit = false

我在Application中对所有需要初始化的东西先初始化一遍,初始化完之后再将isAppInit设置为true,此时在闪屏那边获取的为false,也就是说就会进入APP了。


到这里就结束了,去运行一下吧!


开屏.gif


总结


说实话,在我看来,SplashScreen其实用处不大,因为我们的闪屏一般是用来放advertisement的,而不是放有趣的动画的!


参考


SplashScreen: developer.android.google.cn/develop/ui/…


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

Android APT实战学习技巧

apt
简介 APT(Annotation Processing Tool)即注解处理器,在编译的时候可以处理注解然后搞一些事情,也可以在编译时生成一些文件之类的。ButterKnife和EventBus都使用了APT技术,如果不会APT技术就很难看懂这两个框架的源码...
继续阅读 »

简介


APT(Annotation Processing Tool)即注解处理器,在编译的时候可以处理注解然后搞一些事情,也可以在编译时生成一些文件之类的。ButterKnife和EventBus都使用了APT技术,如果不会APT技术就很难看懂这两个框架的源码。


作用


使用APT可以在编译时来处理编译时注解,生成额外的Java文件,有如下效果:



  • 可以达到减少重复代码手工编写的效果。



如ButterKnife,我们可以直接使用注解来减少findviewbyid这些代码,只需要通过注解表示是哪个id就够了。




  • 功能封装。将主要的功能逻辑封装起来,只保留注解调用。

  • 相对于使用Java反射来处理运行时注解,使用APT有着更加良好的性能。


Android基本编译流程


Android中的代码编译时需要经过:Java——>class ——> dex 流程,代码最终生成dex文件打入到APK包里面。


编译流程如图所示:




  • APT是在编译开始时就介入的,用来处理编译时注解。

  • AOP(Aspect Oridnted Programming)是在编译完成后生成dex文件之前,通过直接修改.class文件的方式,来对代码进行修改或添加逻辑。常用在在代码监控,代码修改,代码分析这些场景。


基本使用


基本使用流程主要包括如下几个步骤:



  1. 创建自定义注解

  2. 创建注解处理器,处理Java文件生成逻辑

  3. 封装一个供外部调用的API

  4. 项目中调用


整理思路



  1. 首先我们需要创建两个JavaLibrary

  2. 一个用来定义注解,一个用来扫描注解

  3. 获取到添加注解的成员变量名

  4. 动态生成类和方法用IO生成文件


实战


创建一个空项目



创建两个JavaLibrary



  • 注解的Lib: apt-annotation

  • 扫描注解的Lib: apt-processor




创建完之后



app模块依赖两个Library


implementation project(path: ':apt-annotation')
annotationProcessor project(path: ':apt-processor')


注解Lib中创建一个注解类


如果还不会自定义注解的同学,可以先去看我之前写的一篇Java自定义注解入门到实战


@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface Print {

}


扫描注解的Lib添加依赖


dependencies {
//自动注册,动态生成 META-INF/...文件
implementation 'com.google.auto.service:auto-service:1.0-rc6'
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'
//依赖apt-annotation
implementation project(path: ':apt-annotation')
}


创建扫描注解的类



重写init方法,输出Hello,APT


注意: 这里是JavaLib,所以不能使用Log打印,这里可以使用Java的println()或注解处理器给我们提供的方法,建议使用注解处理器给我们提供的



现在我们已经完成了APT的基本配置,现在我们可以build一下项目了,成败在此一举



如果你已经成功输出了文本,说明APT已经配置好,可以继续下一步了


继续完成功能


现在我们可以继续完成上面要实现的功能了,我们需要先来实现几个方法


/**
* 要扫描扫描的注解,可以添加多个
*/
@Override
public Set<String> getSupportedAnnotationTypes() {
HashSet<String> hashSet = new HashSet<>();
hashSet.add(Print.class.getCanonicalName());
return hashSet;
}

/**
* 编译版本,固定写法就可以
*/
@Override
public SourceVersion getSupportedSourceVersion() {
return processingEnv.getSourceVersion();
}


定义注解


我们先在MianActivity中添加两个成员变量并使用我们定义的注解



定义注解


真正解析注解的地方是在process方法,我们先试试能不能拿到被注解的变量名


/**
* 扫描注解回调
*/
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
//拿到所有添加Print注解的成员变量
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Print.class);
for (Element element : elements) {
//拿到成员变量名
Name simpleName = element.getSimpleName();
//输出成员变量名
processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE,simpleName);
}
return false;
}


编译试一下



生成类


既然能拿到被注解的变量名,后面就简单了,我们只需要用字符串拼出来一个工具类,然后用IO流写到本地就ok了



查看效果


现在点击一下编译,然后我们可以看到app模块下的build文件已经有我们生成的类了



调用方法


现在我们回到MainActivity,就可以直接调用这个动态生成的类了



总结


优点:


它可以做任何你不想做的繁杂的工作,它可以帮你写任何你不想重复代码,将重复代码抽取出来,用AOP思想去编写。 它可以生成任何java代码供你在任何地方使用。


难点:


在于设计模式和解耦思想的灵活应用。在于代理类代码生成的繁琐:你可以手动进行字符串拼接,也可以用squareup公司的javapoet库来构建出任何你想要的java代码。


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

安卓之如何优雅的处理Activity回收突发事件

情景与原因 前面的文章说过,我的一个业务要从页面A进入页面B,也就意味着我的应用出现了在ActivityA的基础上启动了ActivityB的情景,这个时候ActivityA就进入了停止状态,但这个时候如果出现系统内存不足的情况,就会把ActivityA回收掉,...
继续阅读 »

情景与原因


前面的文章说过,我的一个业务要从页面A进入页面B,也就意味着我的应用出现了在ActivityA的基础上启动了ActivityB的情景,这个时候ActivityA就进入了停止状态,但这个时候如果出现系统内存不足的情况,就会把ActivityA回收掉,此时用户按下Back键返回A,仍然会正常显示A,但此时的A是执行onCreate()方法加载的,而不是执行onRestart()方法,也就是说我们的ActivityA页面是被重新创建出来的。


那有什么区别呢,这就是我们平时网上填表之类的最讨厌的情景会浮现了,比如你好不容易把信息填好,点击下一步,然后发现想修改上个页面信息,哎,这时候你会发现由于系统内存不足回收掉了A,于是你辛辛苦苦填好的信息都没了,这太痛了。这一情景的出现就说明回收了A再重新创建会导致我们在A当中存的一些临时数据与状态都可能被丢失。


这就是我们今天要解决的问题。


解决方法


虽然出现这个问题是是否影响用户体验的,但解决方法是非常简单的,因为我们的Activity中还提供了一个onSaveInstanceState()回调方法,它就可以保证在Activity被回收之前一定被调用,而我们就可以利用这一特性去解决上述问题。


方法介绍


onSaveInstanceState()方法中会携带一个Bundle类型的参数,而Bundle提供了一系列方法用于保存我们想要的数据,其中如putString()就是用于保存字符串的,而putInt()方法显而易见也是保存整型数据的,而每个保存方法都需要传入两个参数,其中第一个参数我们称之为键,是用于在Bundle中取值的特定标记,第二个参数是我们要保存的内容,属于键值对类型,学过数据库的一般都知道。


写法


如下,我们可以这么去写:


override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
val tempData = "your temp data"
outState.putString("data_key", tempData)
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (savedInstanceState != null) {
val tempData = savedInstanceState.getString("data_key")
}
...
}

在onSaveInstanceState()中保存你想要的数据,在onCreate()中给取出来,这样就不用害怕再创建的时候数据丢失了,不过如果是横竖屏切换造成数据丢失还是依照前文ViewModel的写法更好。


结语


其实基础往往是最容易忽视的,我们之前光在那说高楼大厦如何建成,但地基并不牢靠,所以还是需要落实到基础上最好。


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

聊聊ART虚拟机_对象的分配问题

ART
前置知识 有Android开发基础 了解 Java 语法和 JVM 前言 ART 虚拟机(下图 Runtime 层),相信各位搞 Android 开发的同学都有知道,总体的印象呢就是:ART 与 JVM 不同,其不符合 JVM 规范不属于 JVM ,且为 ...
继续阅读 »

前置知识



  • 有Android开发基础

  • 了解 Java 语法和 JVM


前言


ART 虚拟机(下图 Runtime 层),相信各位搞 Android 开发的同学都有知道,总体的印象呢就是:ART 与 JVM 不同,其不符合 JVM 规范不属于 JVM ,且为 Dalvik 的进阶版。


但是,我们有必要对 ART 进行更加深入的了解,其有助于我们对 Android 的更深层次的理解。所以,本文将和聊一聊 ART 虚拟机,以及 ART 中一个对象是如何分配的。



何为ART虚拟机


在开始阶段,我们还是需要来聊一下什么是 ART 虚拟机,其不同在何处。



探析Android中的四类性能优化一文中,我们有提到 ART 虚拟机是 Google 在 Android4.4 的时候引入的,其用于替代 Dalvik 虚拟机。而在替代 Dalvik 虚拟机的同时,他也是兼容之前的 dex 格式的。ART 与 Dalvik 的不同点如下所示。



ART特性

1. 预编译

Dalvik 中的应用每次运行时,字节码都需要通过即时编译器 JIT 转换为机器码,这会使得应用的运行效率降低。在 ART 中,系统在安装应用时会进行一次预编译(AOT,Ahead-Of-Time),将字节码预先编译成机器码并存储在本地,这样应用就不用在每次运行时执行编译了,运行效率也大大提高。


2. 垃圾回收算法

在 Dalvik 采用的垃圾回收算法是标记-清除算法,启动垃圾回收机制会造成两次暂停(一次在遍历阶段,另一次在标记阶段)。而在 ART 下,GC 速度比 Dalvik 要快,这是因为应用本身做了垃圾回收的一些工作,启动 GC 后,不再是两次暂停,而是一次暂停,而且 ART 使用了一种新技术(packard pre-cleaning),在暂停前做了许多事情,减轻了暂停时的工作量。


3. 64 位

Dalvik 是为 32 位 CPU 设计的,而 ART 支持 64 位并兼容 32 位 CPU,这也是 Dalvik 被淘汰的主要原因。



由此可知,ART 让 Android 的性能有了很大的提升,从 2015 直到现在,我们使用的都还是 ART 虚拟机。


下图为 ART 的整体架构,我们可以看出,上层是执行层,负责直接对书写的代码进行处理,而下层则为运行时刻对 Java 语法的支持。


ART架构


对象的分配


对于对象的分配问题,实际上是 ART 对于类的管理问题。而类中则是描述了一个对象的内存布局(类成员的大小、类型和排布)和其函数信息。


例如 Object 类,包含以下的信息:


一个保存的是类型定义,一个保存的是锁的信息。


Object 类


类加载


一个类分配的对象的大小,是由继承链所决定的。当 Java 中的类首次使用的时候,就会进行类加载。例如首次使用到一个子类的时候,会自动将继承链上面的所有父类都进行加载,而整个继承链上面的类的总和就是该子类的大小。


例如下文中的子类的大小就是 AWork + BaseWork 两者合起来的大小。


puvlic class AWork extends BaseWork{

public AWork(WorkBean workBean){
super(work);
}

@Override
public process(Processbean processbean){
workBean.getA().actionA(processbean.getProcessA);
}
}

内存布局


如下图所示,当有 A->B->Object 这个继承关系的时候,其内存布局是父类在上,子类在下的方式进行排布的。而在每一个类里面,则是将引用类型置于最上方,而其他的类型则按字母顺序进行排序。


内存布局


双亲继承(双亲委派)


何为双亲继承呢?


双亲委派的意思是如果一个类加载器需要加载类,那么首先它会把这个类请求委派给父类加载器去完成,每一层都是如此。一直递归到顶层,当父加载器无法完成这个请求时,子类才会尝试去加载。


这么做的好处有一点,那就是不会出现假的委派父类,我们在委派的时候按照既定的逻辑寻找、只有在继承链上面的才是正确的,使得不会有虚假的父类出现。


这类底层的逻辑,反映出合理的继承链是有利于设计和执行的。其实由此我们也可以看到,其实很多设计原则的道理和这些底层逻辑设计也是相同的,例如迪米特原则和接口隔离原则,都是反映出继承链要合理,不要贪多的思维。


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

我爸53岁了,居然还能找到年薪25万的管理岗位,突然很羡慕传统行业!

在35岁焦虑席卷许多打工人的时候,一位53岁的老父亲竟然找到了年薪25万的管理岗位,他的儿子不禁感叹“突然有点羡慕传统行业”!有人问楼主父亲是什么行业?楼主回答:造船。楼主说,父亲之前在央企做项目经理,年薪也有四十几万,后来得罪人被降职,辞职后失业两年,尝试过...
继续阅读 »

在35岁焦虑席卷许多打工人的时候,一位53岁的老父亲竟然找到了年薪25万的管理岗位,他的儿子不禁感叹“突然有点羡慕传统行业”!


有人问楼主父亲是什么行业?

楼主回答:造船。


楼主说,父亲之前在央企做项目经理,年薪也有四十几万,后来得罪人被降职,辞职后失业两年,尝试过很多职业,经历了这么大落差,心态还这么好,真的很佩服他。


许多网友纷纷出来爆料自己的家人也有类似经历,大多都是五六十岁还能找到不错的工作,或者挣的钱比自己还多。

网友感叹:治好了自己的精神内耗。


有人说,这才是正常的,在一个行业耕耘多年,有经验的人不该失业,在传统行业里,三四十岁正是挑大梁的时候,年龄越大挣得越多。

有人说,深耕一个领域的人不缺offer ,因为有不可替代性。打铁还需自身硬,只要是人才,走到哪里都是人才。

有人建议应届生选一个能在一个赛道做久的行业,不要因为一点涨幅就频繁换行业。


但也有人说,就算年薪25万,应届生依然不愿意去一些行业,因为传统行业真的很苦。


不是每一个行业都是吃青春饭,也不是每一个行业都有35岁红线,相反,许多行业是越老越值钱,比如医生、教师、律师、会计、制造等。在这些行业里,年龄大意味着更丰富的经验和阅历,可以担当更重要的责任,承担更重要的工作,自然也能拿到更高的薪资。

可能是互联网行业的声音更容易被听到,时间久了,人们觉得高薪和大龄被裁是所有行业的现状。其实在我们不注意的地方,在许多低调的传统行业里,那些大龄打工人也生活得很好,甚至比互联网行业从业者还好。

所以,在选择行业和赛道时,别总盯着眼前的一亩三分地,多去了解了解那些不起眼的行业,说不定会有意外之喜。

作者:行者

来源:devabc

收起阅读 »

安卓之如何优雅的处理Activity回收突发事件

情景与原因前面的文章说过,我的一个业务要从页面A进入页面B,也就意味着我的应用出现了在ActivityA的基础上启动了ActivityB的情景,这个时候ActivityA就进入了停止状态,但这个时候如果出现系统内存不足的情况,就会把ActivityA回收掉,此...
继续阅读 »

情景与原因

前面的文章说过,我的一个业务要从页面A进入页面B,也就意味着我的应用出现了在ActivityA的基础上启动了ActivityB的情景,这个时候ActivityA就进入了停止状态,但这个时候如果出现系统内存不足的情况,就会把ActivityA回收掉,此时用户按下Back键返回A,仍然会正常显示A,但此时的A是执行onCreate()方法加载的,而不是执行onRestart()方法,也就是说我们的ActivityA页面是被重新创建出来的。

那有什么区别呢,这就是我们平时网上填表之类的最讨厌的情景会浮现了,比如你好不容易把信息填好,点击下一步,然后发现想修改上个页面信息,哎,这时候你会发现由于系统内存不足回收掉了A,于是你辛辛苦苦填好的信息都没了,这太痛了。这一情景的出现就说明回收了A再重新创建会导致我们在A当中存的一些临时数据与状态都可能被丢失。

这就是我们今天要解决的问题。

解决方法

虽然出现这个问题是是否影响用户体验的,但解决方法是非常简单的,因为我们的Activity中还提供了一个onSaveInstanceState()回调方法,它就可以保证在Activity被回收之前一定被调用,而我们就可以利用这一特性去解决上述问题。

方法介绍

onSaveInstanceState()方法中会携带一个Bundle类型的参数,而Bundle提供了一系列方法用于保存我们想要的数据,其中如putString()就是用于保存字符串的,而putInt()方法显而易见也是保存整型数据的,而每个保存方法都需要传入两个参数,其中第一个参数我们称之为键,是用于在Bundle中取值的特定标记,第二个参数是我们要保存的内容,属于键值对类型,学过数据库的一般都知道。

写法

如下,我们可以这么去写:

override fun onSaveInstanceState(outState: Bundle) {
   super.onSaveInstanceState(outState)
   val tempData = "your temp data"
   outState.putString("data_key", tempData)
}
override fun onCreate(savedInstanceState: Bundle?) {
  super.onCreate(savedInstanceState)
  setContentView(R.layout.activity_main)
  if (savedInstanceState != null) {
      val tempData = savedInstanceState.getString("data_key")
  }
  ...
}

在onSaveInstanceState()中保存你想要的数据,在onCreate()中给取出来,这样就不用害怕再创建的时候数据丢失了,不过如果是横竖屏切换造成数据丢失还是依照前文ViewModel的写法更好。

结语

其实基础往往是最容易忽视的,我们之前光在那说高楼大厦如何建成,但地基并不牢靠,所以还是需要落实到基础上最好。

作者:ObliviateOnline
来源:juejin.cn/post/7158096746583687205

收起阅读 »

整体学历较高,硕士占比达 40%,周星驰也开始招募Web3人才!

编辑:Datawhale近期,周星驰发布人才招募令。人才要求:熟悉Web3,有项目管理经验,有头脑还又宅心仁厚;工作范围:助我建造创意未来;还提醒对此职位感兴趣的候选人投简历时请贴出个人简介影片或Web3作品并tag,他本人会亲自拣人。随着数字经济的发展,We...
继续阅读 »

编辑:Datawhale

近期,周星驰发布人才招募令。

人才要求:熟悉Web3,有项目管理经验,有头脑还又宅心仁厚;工作范围:助我建造创意未来;还提醒对此职位感兴趣的候选人投简历时请贴出个人简介影片或Web3作品并tag,他本人会亲自拣人。


随着数字经济的发展,Web3 成为了新的风口,各大厂商和投资人纷纷将目光聚集其上。在部分人眼中,Web3 能够重塑数字金融交易体系,改变全球竞争格局,在未来互联网上实现弯道超车。其中,不乏顶级投资机构红杉和 A16Z,前者甚至一度将简介更改为 DAO。


不过,时不时出现的裁员新闻又让行业之外的人对 Web3 望而生畏。如 Coinbase 此前宣布裁员 18%,规模高达上千人。当然,Web3 也存在正不断开放招聘岗位的企业。

传说中可以跨国分布式从业的 Web3 是精英遍布还是草根丛生,什么样的人才是目前 Web3 企业所需要的成为了大部分用户想了解的信息呢?

Web3企业需要的是什么人才?

中国人才增速较低,但需求强劲

从宏观上来看,截至 2022 年 6 月,区块链人才总量同比增加了 76%,其中,印度、新加坡和美国增速最高,分别为 122%、92%、62%,中国相对较低,仅仅只有 12%。

在人才总量增加的同时,人才需求量呈现出远超供应的增长。根据领英人才大数据洞察获得的数据来看,2021 年相较 2020 年人才需求呈倍数级增长,其中,加拿大增速最高,达到了 560%。印度、新加坡、美国、中国的增速分别为 145%、180%、82%、78%。


虽然供需缺口,但事实上,除了科技和金融公司以外,大部分区块链的人才主要又以内部流动为主。领英人才大数据洞察显示,2021 年至今,人才主要在 Coinbase、Crypto.com、Gemeni、Rippl e 等区块链企业间交叉流动。而外部流入的人才主要来自华尔街和硅谷等地知名巨头如高盛、JPMorgan、HSBS、谷歌、微软、Facebook 等。

从人才的需求端和供应端来看,区块链的人才受到地区限制少,在全球各地都呈现需求量增长的趋势。但是从绝对数量上来看,美国、法国、英国等发达国家依旧占据着优势。对于金融业和 IT 业较为繁荣的国家而言,切入区块链和 Web3 存在着不小的产业优势。

具体到中国的区块链产业上,根据 IDC 研究预测,中国 2020-2025 年区块链市场规模年复合增长率将达 54.6%,增速位居全球第一。而全球区块链市场规模年复合增长率将达 48%。

换言之,中国目前的区块链产业对人才的需求量大,但进入该行业的人才少,同时,中国区块链产业后续增长强劲。从报告来看,换方向从事 Web3 行业的工作对于个人的发展而言或许是一个不错的选择。

核心人才需求主要以金融和研发为主

相较于 Web1 和 Web2 而言,Web3 的定义更宽泛。目前业内对于 Web3 并没有严格的定义,不过其有几个较为明显的特征,比如数据的确权与授权、隐私保护、去中心化运行等等。而这些明显的特征决定了行业主要人才的构成。

从全球区块链领域人才构成上分析,金融、研发、业务开发、信息技术、销售人才为全球区块链前五大人才类型。


全球区块链领域前五大人才类型中,最热门细分职业分别为加密货币交易员、软件工程师、分析师、支持分析师及客户经理。


从人才增速来看,测试工程师、密码逻辑技术专家、合规分析师、设计师和支持分析师分别位列前五,其中,测试工程师增速高达 713%。

从人才构成和需求来看,可以发现,区块链行业发展依旧处于早期阶段,大量的基础设施正在搭建。区块链人才的构成成分最主要还是取决于行业的发展。在行业发展的初期阶段往往需要大量基础性的工作职位,如研发、开发、产品构建。如想要等区块链行业发展更加成熟之后再参与这个行业,或许可以锻炼自己运营、营销、市场等方面的能力。

同时,需要注意的是,不同的国家和地区对于人才的需求也有较为明显的差异,人才容易在地域上产生集聚效应,如大量工程师聚集于硅谷。在考虑城市和职业方向的同时,或许还得思考城市和职业的契合度。如在区块链领域,新加坡侧重于产品经理、软件工程师的招聘,而中国香港更侧重于产品设计师、用户体验作者等。

人才竞争初始,硕士从业人数占比 40%

由于市场对区块链人才的需求远远超过供应,区块链从业者的平均薪资已经超过了大部分行业。

据 Glassdoor 报告,美国区块链开发人员的平均年基本工资为 9.1 万美元。而 2020 年美国社会安全署数据显示,美国民众平均年薪 5.3 万美元,中位数为 3.4 万美元。同样,北京人社局于 2021 年 11 月发布的《2021 年北京市人力资源市场薪酬大数据报告》,在 30 个新职业薪酬排行榜中,区块链工程技术人员最高,年度薪酬中位值达 48.7 万人民币。北、上、广、深等重点城市区块链产业人才平均年薪水平大幅领先城市整体产业人才平均年薪水平。

高薪促使着优秀的人才向区块链行业聚拢,目前全球区块链领域中学士群体占 59%,硕士占比达 40%,整体学历较高。同时,数据显示,全球区块链人才排名前十的学校均为世界知名院校,其中包括加州大学伯克利分校、斯坦福大学、哈佛大学等顶级大学。

与此同时,中国包括中央财经大学、同济大学、浙江大学等在内的多所双一流大学也开设了区块链课程。

综合来看,目前区块链行业依旧处于起始阶段,各国的政策扶持力度正在不断加大,人才流动频繁,需求量巨大。作为从业者,除了学历等硬性指标外,还需要持续拓展延伸自己的能力,从而持续构建核心竞争力。

来源:Datawhale


收起阅读 »

我的灿烂前端人生

本人是 95 前端菜鸟一枚,目前在广州打工混口饭吃。刚好换了工作,感觉生活节奏变得慢了下来,打了这么多年工总觉得想纪录些什么,怕以后自己老了忘记自己还有这么一些风流往事。书接上回公司太子 北京时间 18 点 50 分,离下班时间还有十分钟,本该是令人愉悦的时刻...
继续阅读 »

本人是 95 前端菜鸟一枚,目前在广州打工混口饭吃。刚好换了工作,感觉生活节奏变得慢了下来,打了这么多年工总觉得想纪录些什么,怕以后自己老了忘记自己还有这么一些风流往事。书接上回

公司太子


北京时间 18 点 50 分,离下班时间还有十分钟,本该是令人愉悦的时刻,我心里的雾霾又浓郁了一分。因为我在公司当太子当了大半年了。



能力出众



遥想今年年初,领着上家公司大礼包四处求职碰壁,踏破铁鞋寻寻觅觅,靠着投机取巧的八股文背诵,终于求得广州一家高大上小企业公司的岗位。入职不到一周立刻加入新的项目团队,做一个抽奖小程序,技术栈是 typescript+taro,我之前没有深入开发过,十分的开心,又可以边工作边学习了。花费三个多月,与团队之间不断擦枪走火,这个项目也是勉强完成,开发完成之余,我有空也加入了测试大军,生怕自己第一个项目上线后因为自己的 bug 造成毁灭的影响毕竟以前经常发生。万万没想到,这个项目最终没有落地,老板总结就是我们做的打不过别人竞品,没啥创新,让团队去搞商城小程序去了。我万分失落惊喜,心里想着这样岂不是等于我做了三个月的项目稳定在线上运行,没有bug,不会被用户投诉,也不会被影响绩效,安稳白嫖三个月薪资?美滋滋!。


度过三个月的试用期,因为项目线上无 bug,能力出众,我也如愿以偿拿下转正。



虚空需求



完成了上一任务,接下来 leader 给我分配了一个大 project,重构以前管理后台的权限。这波重构任务,是 leader 直接文字需求下达指令了,我有点头皮发麻,好几年没遇到这种需求了,真的是梦回 S1 赛季,本来和我合作的小伙伴说他要做个原型出来,结果因为分配任务我负责管理后台前端,他负责管理后台 nodejs 的代码,他也就没有做出来,让原型图随风而去,跟我说了句一把梭。我也想一把梭,但我发现 leader 的需求十分灵性,加之我对之前的业务也不熟悉,想着还是花点时间加班把原型图做一下吧。


我战战兢兢的把原型图发到群里,leader 已读并回了没啥问题了,可以开工。我悬着的心放了下来,撸起袖子大胆干。说实话,我心里其实很慌的,首先对 React+Typescript 不熟悉,且这套管理后台十分深奥,用的是自研的核心框架,各种 typescript abstract 抽象类,复杂的类型泛型,对我这个半吊子前端还是比较吃力的。但好在我是拷贝忍者,写业务代码先找下之前代码是怎么写的,CCCV,改个英文单词,就是我的杰作



TX leader 真的很严格



我的 leader 是腾讯大厂出来的,我也是打心底里对他有一丝敬畏,毕竟大厂大佬恐怖如斯,技术水平肯定不是我这种切图仔比拟的。


任务花费了三周多一点,包含联调自测,自测完后就提个 MQ 上去了,信心十足。万万没想到,leaderCode Review 对着我的杰作一顿输出,大概有二十几个修改建议,我都有仔细去看,发现很多都是代码规范,代码优化,leader 都给了一定的建议。说实话,一开始我的心里多多少少有些芥蒂,但是谁让别人是领导呢?开个玩笑。但是 leader 指出来的问题的确是不容忽视的,程序员就是要有更好的追求,其实有人把问题指出来,才是对我最大的帮助,我也是花了不少时间去更改这些问题。下面就放一些 bad code 出来献丑。




之前一直想不明白,传进来的组件是在 children 里面,我如何去改变组件的点击函数,想来想去想不懂,脑门一热直接在组件上加一层蒙层,通过蒙层阻碍组件点击,当时设计完出来我还挺高兴,leader 也直呼天才,送了我两个字 ———— 重做


因为我技术能力确实平庸,只能请教我的良师百度,不断去寻找 children 是否有什么方法或钩子处理事件,功夫不负有心人,果真被我找到了。下面就是修改后的方法

// after
return permission ? children : React.Children.map(children, child => React.cloneElement(child as React.ReactElement, { onClick: () => { message.error('无权限'); } }));

ps:leader 也勉为其难的接受这个方法,可能他不知道有什么更好的方法。如果观众大佬们知道,可以提下意见,不胜感激。

设计组织架构图

07rebuild.png

先让大伙看看原来的功能图吧,之后我们开了一个会议,这里要重做。


我心想我发原型图出来的时候,大佬您可是没有半个不字,怎么 codereview 直接改了一个方向了啊?

不过,毕竟他是我的 leader,我的生死全由他掌控,我也不敢多言,上网找了一个 npm 库 react-organizational-chart。react 的社区就是强~下面是更改后的视图

不得不说,的确是更饱满更清晰直观了一些,leader 还是很有远见的怕他也上掘金,吹了再说


这个项目陆陆续续做了三个月了,因为 leader 平时也很忙,两个城市飞,导致这个项目的进度也进展缓慢,而我就在空闲时间上上掘金学习技术,刷刷 leetcode。


来了大半年,我深刻明白我对公司的建设为 0,所做项目为公司带来 0 收入,就是我的价值完全没有体现,公司把我当太子养了大半年,我非常感谢公司。然后每天都会浏览 boss 直聘,深怕下午就被拉进小黑屋,在这个大环境下,我也时刻准备着,毕竟也有前车之鉴,我明白我只是个平庸的程序员,只能尽力做好自己的本分,随时做好最坏的打算,当真正的打击来临之时,我也不会手忙脚乱。

灿烂?摆烂!


最近 IT 的 HRBP 要我一个新入职的去做一场技术分享,我在这里呆了大半年,没有等来其他前端大佬的分享,竟然是要我亲自上阵,小丑竟是我自己



空虚寂寞冷



回想了一下这六个月,其实自己的水平真的没有半点进步,我想不到有什么可以拿来分享的。而且从入职以来,我在这个公司说的话可能没有超过 100句,其实有时我也纳闷,我印象中自己不是一个这么闷的一个人,在上家公司我吹 * 技术游走于天地之间,能很好的融入团队,并能展开身心为其奋斗前期战神,后期老油条。但是来了新公司之后,我只会干完手头上的活,也没有跟其他同事聊聊天,不过我附近的同事也极少聊天,感觉稍微有点死气沉沉。


以前年轻的时候,看到一些新入职的同事,闷葫芦一个,找他搭话或者说骚话,他都没啥兴趣,现在的我,好像成为了自己以前眼中的怪人。我苦思久已,只能得出几个结论,第一点可能是我以前投入太多,经历过分离,不想再投入更多的感情,投入的越深,离开时就越痛 1000-7=? 痛,太痛了。第二点是因为现在的大环境,让我精神焦虑,我深怕我和某位同事今天刚去饭堂吃个饭,明天人就没了。想看我之前为啥被裁,可以看我往期文章


不过,我觉得出来工作,重点是挣钱,以这个为核心,其他一切都是空谈。而且,解决我的聊天需求还有一大神器,不是陌陌,而是网易狼人杀APP。自从入职新公司以来,每天下班回到家根本不想学习,不想运动,只想躺着,然后冲进大师场厮杀,里面个个都是人才,说话又好听,我喜欢这个游戏,因为它能锻炼提高我的骗人能力当然是表达能力啦!而且它还夹杂着些许人性的味道,人性的魅力也让我欲罢不能。网易打钱。所以要我分享,我真不知道分享什么,难道分享如何悍跳吃警徽,狼查杀狼打板子做高狼同伴身份?



保持平常心



最终 leader 让我去分享一下这个重构项目,我想了一下也可以,其实它不是一次分享,可以把它当做一次项目复盘,把自己的问题抛出来给到大家欣赏,虽然有点丢人,但是赚钱嘛,不寒碜。而且自己的技术也拉胯,可以让自己加深这些问题的印象,对自己成长的路也是有极大帮助的。


不止是大环境,最近社会也出现了许多光怪陆离的事情,心态也有些许变化,我不再绞尽脑汁去想着如何跳槽获得高薪,我只想取悦自己,做自己认为让自己开心而正确的事情,心累了就去外面走走,馋了就去吃点美食,觉得知识匮乏了就化身小厂做题家刷刷 leetcode,看看别人的源码见解虽然多数都看不懂。偶尔什么都想学,什么都学不进去的时候,也会焦虑,解决焦虑的办法,我常常是...... 奖励自己


当下所面临的的困难、焦虑,都会被时间而抚平,我作为一个平庸程序员,面对每天新开始的人生,我只能对自己说一句,啊,又是新的一天

链接:https://juejin.cn/post/7122401595966357518
来源:稀土掘金
收起阅读 »

组员大眼瞪小眼,forEach 处理异步任务遇到的坑

一位组员遇到一个问题,几个同事都没能帮忙解决,我在这边就开门见山直接描述当时他遇到的问题。他在 forEach 处理了异步,但是始终不能顺序执行,至此想要的数据怎么都拿不到,组员绞尽脑汁,不知道问题发生在哪里。此篇文章我们就来探究下 forEach 循环下处理...
继续阅读 »

一位组员遇到一个问题,几个同事都没能帮忙解决,我在这边就开门见山直接描述当时他遇到的问题。他在 forEach 处理了异步,但是始终不能顺序执行,至此想要的数据怎么都拿不到,组员绞尽脑汁,不知道问题发生在哪里。此篇文章我们就来探究下 forEach 循环下处理异步会发生什么样的情况。

探索

我们先看一段简单的 forEach 处理异步的代码

//forEach 处理
let promiseTasek = (num) => {
return new Promise((resolve, rejece) => {
setTimeout(() => {
console.log(num)
resolve(true)
}, 4000)
})
}
function toTaskByForEach() {
const arr = [1, 2, 3, 4, 5, 6]
arr.forEach(async (item) => {
await promiseTasek(item)
})
}

toTaskByForEach()

执行结果 注意执行输出的变化,他会直接打印出 1,2,3,4,5,6 本来想录制一个 gif 的,确实没找到一个好的工具录制浏览器的控制台


我们尝试换一种循环 for of 看一下效果对比一下

let promiseTasek = (num) => {
return new Promise((resolve, rejece) => {
setTimeout(() => {
console.log(num)
resolve(true)
}, 4000)
})
}

async function toTaskByForOf(){
const arr = [1,2,3,4,5,6]
for (let i of arr) {
await promiseTasek(i)
}
}
toTaskByForOf()

来看下执行结果 他会按顺序执行依次打印出 1,2,3,4,5,6

所以这是为啥呢

后来我们研究了一下 map

//forEach 处理
let promiseTasek = (num) => {
return new Promise((resolve, rejece) => {
setTimeout(() => {
console.log(num)
resolve(true)
}, 4000)
})
}
function toTaskByMap() {
const arr = [1, 2, 3, 4, 5, 6]
arr.map(async (item) => {
await promiseTasek(item)
})
}

toTaskByMap()

输出结果和 forEach 一样
后来我们发现 Array.prototype.forEach 不是一个 async 函数,即使 Array.prototype.forEach 的参数 callback 是 async 函数,也暂停不了 Array.prototype.forEach 函数,map 也是同理


Array.forEach 会并发启动所有方法但是丢弃结果,如果 forEach 需要 await 结果的时候可以用这个方法 await Promise.all(arr.map(async (item) => { /** ... */ }))
链接:https://juejin.cn/post/7154650750038048781


收起阅读 »

安卓关于Bitmap.isRecycled()空指针报错的解决方案

前言 起因是我在开发功能需要使用Bitmap的方法: BitmapFactory.decodeResource(my.main.getResources(),R.drawable.vector_my_need); 结果就倒大霉,运行时直接报错: java.la...
继续阅读 »

前言


起因是我在开发功能需要使用Bitmap的方法:


BitmapFactory.decodeResource(my.main.getResources(),R.drawable.vector_my_need);

结果就倒大霉,运行时直接报错:


java.lang.NullPointerException: Attempt to invoke virtual method 'boolean android.graphics.Bitmap.isRecycled()' on a null object reference

从日志分析,我们知道是出现了空指针,当时我先是想自己找原因,结果定位到view的源代码里,是在draw方法中,但没找到,我还没放弃,于是又定位了updateDisplayListIfDirty() 方法以及其他报错对应点,于是终于发现了是我bitmap的使用出了问题:


image.png
找到问题固然是好事,可是如何解决呢?这就要靠搜索了,接下来让我们看看解决方法。


正篇


正确的搜索方法


其实我在搜索上吃了许多亏,一开始在国内搜索上一直给我推C站的结果虽然有点相似但其实都相差甚远,最后我在StackOverflow上找到了答案,这也是曾经让别人困惑的一个问题:


image.png
可以看到有31K人浏览过此问题,所以该问题早有认可的答案:


image.png
意思说,我们用的vector矢量可绘制对象需要创建位图,而不是对其进行解码,且方法在下一个帖子中。


完美的解决方案


其实说到这,我已经明白,是我用SVG图资源放到安卓项目中转成Vector的xml文件,这种文件解码无法获得正确的bitmap,于是我恍然大悟的点开了下个帖子:


image.png
我为这个标准答案标记了中文解释,给出的是将我们的vector资源实例成Drawable对象,然后通过Bitmap的创建方法去创建成一个新的bitmap,代码如下:


Drawable d = getResources().getDrawable(R.drawable.your_drawable, your_app_theme);

这一步就是变成Drawable对象,接下来:


public static Bitmap drawableToBitmap (Drawable drawable) {

if (drawable instanceof BitmapDrawable) {
return ((BitmapDrawable)drawable).getBitmap();
}

Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
drawable.draw(canvas);

return bitmap;
}

这段代码是将Drawable对象转换成Bitmap封装成了工具方法,便于直接应用于主代码。

而方法内部,也分成两层:

首先第一层,我们用instanceof测试它左边的对象是否是它右边的类的实例,如果是真命题则直接返回强制转成BitmapDrawable,并直接调用它的getBitmap()方法即可。


image.png
而如果第一层没有成功,则由第二层处理,我们先实例化Bitmap对象,利用Bitmap的createBitmap()方法输入drawable对象的固有宽高和BItmap通道配置获取bitmap


image.png


image.png
然后调用canvas绘制bitmap,最后先用drawable的setBounds()方法为Drawable对象指定一个边界矩形,这是为了调用 draw() 方法前可以确定绘制对象将绘制的位置,接着用draw()方法完成绘制,返回最终的bitmap即完成。


image.png


结语


这还是我第一次这么容易就获取到了明确的正确解决方案,所以特地记录下来,当然,如果你出现了这个空指针就不需要去看英文结果了,让我们更方便的解决这个问题吧。


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

Android动态更换应用图标

一、背景 近日,微博官方发布了一项新功能,即可以在App设置中动态更换微博的显示图标样式。根据微博官方的说法,除了最原始的图标外,微博还推出了另外10种不同的样式,既有3D微博、炫彩微博等保留了眼睛造型的新样式,也有奶酪甜馨、巧克力等以食物命名的“新口味”,还...
继续阅读 »

一、背景


近日,微博官方发布了一项新功能,即可以在App设置中动态更换微博的显示图标样式。根据微博官方的说法,除了最原始的图标外,微博还推出了另外10种不同的样式,既有3D微博、炫彩微博等保留了眼睛造型的新样式,也有奶酪甜馨、巧克力等以食物命名的“新口味”,还有梦幻紫、幻想星空等抽象派新造型,给了微博用户多种选择的自由。


不过需要注意的是,这一功能并不是面对所有人开放的,只有微博年费会员才能享受。此外,iOS 10.3及以上和Android 10及以上系统版本支持该功能,但是iPad与一加8Pro手机无法使用该功能。因部分手机存在系统差异,会导致该功能不可用,微博方面后续还会对该功能进行进一步优化。


image.png


二、技术实现


其实,说到底,上述功能用到的是动态更换桌面图标的技术。如果说多年以前,实现图标的切换还是一种时髦的技术,那么,我们可以直接使用PackageManager就可以实现动态更换桌面图标。


实现的细节是,在Manifest文件中使用标签准备多个Activity入口,没个activity都指向入口Activity,并且为每个拥有标签的activity设置单独的icon和应用名,最后调用SystemService 服务kill掉launcher,并执行launcher的重启操作。


首先,我们在AndroidManifest.xml文件中添加如下代码:


<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.xzh.demo">

<!-- 权限-->
<uses-permission android:name="android.permission.KILL_BACKGROUND_PROCESSES"/>

<application
android:allowBackup="true"
android:icon="@mipmap/wb_default_logo"
android:label="@string/app_name"
android:roundIcon="@mipmap/wb_default_logo"
android:supportsRtl="true"
android:theme="@style/Theme.AndroidDemo">

...//省略其他代码

<!-- 默认微博-->
<activity-alias
android:name="com.xzh.demo.default"
android:targetActivity=".MainActivity"
android:label="@string/app_name"
android:enabled="false"
android:icon="@mipmap/wb_default_logo"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>

<!-- 3D微博-->
<activity-alias
android:name=".threedweibo"
android:targetActivity=".MainActivity"
android:label="@string/wb_3d"
android:enabled="false"
android:icon="@mipmap/wb_3dweibo"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>

... //省略其他

</application>
</manifest>

上面配置中涉及到的属性如下:



  • android:name:注册的组件名字,启动组件的名称。

  • android:enabled:是否启用这个组件,也就是是否显示这个入口。

  • android:icon:图标

  • android:label:名称

  • android:targetActivity:默认的activity没有这个属性,指定目标activity,与默认的activity中的name属性是一样的,需要有相应的java类文件。


接着,我们在MainActivity触发Logo图标更换逻辑,代码如下:


class MainActivity : AppCompatActivity() {
var list: List<LogoBean> = ArrayList()
var recyclerView: RecyclerView? = null
var adapter: LogoAdapter? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initView()
initData()
initRecycle()
}
private fun initView() {
recyclerView = findViewById(R.id.recycle_view)
}
private fun initData() {
list = Arrays.asList(
LogoBean(R.mipmap.wb_default_logo, "默认图标", true),
LogoBean(R.mipmap.wb_3dweibo, "3D微博", false),
LogoBean(R.mipmap.wb_cheese_sweetheart, "奶酪甜心", false),
LogoBean(R.mipmap.wb_chocolate_sweetheart, "巧克力", false),
LogoBean(R.mipmap.wb_clear_colorful, "清透七彩", false),
LogoBean(R.mipmap.wb_colorful_sunset, "多彩日落", false),
LogoBean(R.mipmap.wb_colorful_weibo, "炫彩微博", false),
LogoBean(R.mipmap.wb_cool_pool, "清凉泳池", false),
LogoBean(R.mipmap.wb_fantasy_purple, "梦幻紫", false),
LogoBean(R.mipmap.wb_fantasy_starry_sky, "幻想星空", false),
LogoBean(R.mipmap.wb_hot_weibo, "热感微博", false),
)
}
private fun initRecycle() {
adapter =LogoAdapter(this,list);
val layoutManager = GridLayoutManager(this, 3)
recyclerView?.layoutManager = layoutManager
recyclerView?.adapter = adapter
adapter?.setOnItemClickListener(object : OnItemClickListener {
override fun onItemClick(view: View?, position: Int) {
if(position==1){
changeLogo("com.xzh.demo.threedweibo")
}else if (position==2){
changeLogo("com.xzh.demo.cheese")
}else if (position==3){
changeLogo("com.xzh.demo.chocolate")
}else {
changeLogo("com.xzh.demo.default")
}
}
})
}

fun changeLogo(name: String) {
val pm = packageManager
pm.setComponentEnabledSetting(
componentName,
PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP
)
pm.setComponentEnabledSetting(
ComponentName(this, name),
PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP
)
reStartApp(pm)
}
fun reStartApp(pm: PackageManager) {
val am = getSystemService(ACTIVITY_SERVICE) as ActivityManager
val intent = Intent(Intent.ACTION_MAIN)
intent.addCategory(Intent.CATEGORY_HOME)
intent.addCategory(Intent.CATEGORY_DEFAULT)
val resolveInfos = pm.queryIntentActivities(intent, 0)
for (resolveInfo in resolveInfos) {
if (resolveInfo.activityInfo != null) {
am.killBackgroundProcesses(resolveInfo.activityInfo.packageName)
}
}
}
}

注意上面的changeLogo()方法中的字符串需要和AndroidManifest.xml文件中的<activity-alias>的name相对应。运行上面的代码,然后点击应用中的某个图标,就可以更换应用的桌面图标,如下图所示。


image.png


不过,测试的时候也遇到一些适配问题:



  • 小米9:版本升级时,新版本在AndroidManifest中删除A3,老版本切换图标到A3,为卸载直接覆盖安装新版本,手机桌面图标消失。

  • magic 4:版本升级时,新版本在AndroidManifest中删除A3,老版本切换图标到A3,为卸载直接覆盖安装新版本,手机桌面图标切换到默认图标,但点击之后未能打开APP。

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

Android gradle迁移至kts

背景 在kotlin语言已经渗透至各领域的环境下,比如服务端,android,跨平台Kmm,native for kotlin,几乎所有的领域都可以用kotlin去编写了,当然还有不成熟的地方,但是JB的目标是很一致的!我们最常用的gradle构建工具,也支持...
继续阅读 »

背景


在kotlin语言已经渗透至各领域的环境下,比如服务端,android,跨平台Kmm,native for kotlin,几乎所有的领域都可以用kotlin去编写了,当然还有不成熟的地方,但是JB的目标是很一致的!我们最常用的gradle构建工具,也支持kotlin好久了,但是由于编译速度或者转换成本的原因,真正实现kts转换的项目很少。在笔者的mac m1 中使用最新版的AS去编译build.gradle.kts,速度已经是和用groovy写的gradle脚本不相上下了,所以就准备写了这篇文章,希望做一个记录与分享。

groovykotlin
好处:构建速度较快,运用广泛,动态灵活好处:编译时完成所有,语法简洁,android项目中可用一套语言开发构建脚本与app编写
坏处:语法糖的作用下,很难理解gradle运行的全貌,作用单一,维护成本较高坏处:编译略慢于groovy,学习资料较少

虽然主流的gradle脚本编写依旧是groovy,但是android开发者官网也在推荐迁移到kotlin


编译前准备


这里推荐看看这篇文章,里面也涵盖了很多干货,


全局替换‘’为“”


在kotlin中,表示一个字符串用“”,不同于groovy的‘ ’,所以我们需要全局替换。可以通过快捷方式command/control+shift+R 全局替换,选中匹配正则表达式并设定file mask 为 *.gradle:


正则表达式
'(.*?[^\\])'
作用范围为
"$1"

image.png


全局替换方法调用


在groovy中,方法是可以隐藏(),举个例子


apply plugin: "com.android.application"

这里实际上是调用apply方法,然后命名参数是plugin,内容围为"com.android.application",然而在kotlin语法中,我们需要以()或者invoke的方式才能调用一个方法,所以我们要给所有的groovy函数调用添加()


正则表达式
(\w+) (([^=\{\s]+)(.*))
作用范围为
$1($2)

image.png
很遗憾的是,这个对于多行来说还是存在不足的,所以我们全局替换后还需要手动去修正部分内容即可,这里我们只要记得一个原则即可,想要调用一个kotlin函数,把参数包裹在()内即可,比如调用一个task函数,那么参数即为


task(sourcesJar(type: Jar) {
from(android.sourceSets.main.java.srcDirs)
classifier = "sources"
})

gradle kt化


接下来我们只需要把build.gradle 更改为文件名称为build.gradle.kts 即可,由于我们修改文件为了build.gradle.kts,所以当前就以kts脚本进行编译,所以很多的参数都是处于找不到状态的,即使sync也会报错,所以我们需要把报错的地方先注释掉,然后再进行sync操作,如果成功的话,AS就会帮我们进行一次编译,此时就可以有代码提示了。


开始前准备


以kotlin的方式编译,此时函数就处于可点击查看状态,区别于groovy,因为groovy是动态类型语言,所以很多做了很多语法糖,但是也给我们在debug阶段带来了很多困难,比如没有提示等等,因为groovy只需要保证在运行时找到函数即可,而kotlin却不一样,所以很多动态生成的函数就无法在编译时直接使用了,比如


image.png
对于这种动态函数,kotlin for gradle 其实也给我们内置了很多参数来对应着groovy的动态函数,下面我们来从以下方面去实践吧,tip:以下是gradle脚本编写常用


ext


我们在groovy脚本中,可以定义额外的变量在ext{}中,那么这个在kotlin中可以使用吗?嘿嘿,能用我就不会提到对吧!对的,不可以,因为ext也是一个动态函数,我们kotlin可没法用呀!那怎么办!别怕,kts中给我们定义了一个类似的变量,即extra,我们可以通过by extra去定义,然后就可以自由用我们的myNewProperty变量啦!


val myNewProperty by extra("initial value")

但是,如果我们在其他的gradle.kts脚本中用myNewProperty这个变量,那么也会找不到,因为myNewProperty这个的作用域其实只在当前文件中,确切来说是我们的build.gradle 最后会被编译生成一个Build_Init的类,这个类里面的东西能用的前提是,被先编译过!如果当前编译中的module引用了未被编译的module的变量,这当然不可行啦!当然,还是有对策的,我们可以在BuildScr这个module中定义自定义的函数,因为BuildScr这个module被定义在第一个先执行的module,所以我们后面的module就可以引用到这个“第一个module”的变量的方式去引用自定义的变量!


task



  • 新建task


groovy版本

task clean(type: Delete) {
delete rootProject.buildDir
}

比如clean就是一个我们自定义的task,转换为kotlin后其实也很简单,task是一个函数名,Delete是task的类型,clean是自定义名称


task("clean",{
delete(rootProject.buildDir)
})

当然,我们的task类型可能在编写的由于泛型推断,隐藏了具体的类型,这个时候我们可以通过


 ./gradlew help --task task名

去查看相应的类型



  • 已有task修改
    对于有些是已经在gradle编译时存在的函数任务,比如


groovy版本

wrapper{
gradleVersion = "7.1.1"
distributionType = Wrapper.DistributionType.BIN
}

这个我们kotlin版本的build.gradle能不能识别呢?其实是不可以的,因为编译器也不知道从哪里去找wrapper的定义,因为这个函数在groovy中隐藏了作用域,其实它存在于TaskContainerScope这个作用域中,所以对于所有的的task,其实都是执行在这里面的,我们可以通过tasks去找到


tasks {
named("wrapper") {
gradleVersion = "7.1.1"
distributionType = Wrapper.DistributionType.BIN

}
}

这种方式,去找到一个我们想要的task,并配置其内容



  • 生命周期函数
    我们可以通过函数调用的方式去配置相应的生命周期函数,比如doLast


tasks.create("greeting") {
doLast { println("Hello, World!") }
}

再比如dependOn


task("javadocJar", {
dependsOn(tasks.findByName("javadoc"))
})

动态函数


sourceSets就是一个典型的动态函数,为什么这么说,因为很多plugin都有自己的设置,比如Groovy的sourceSets,再比如Android的SourceSets,它其实是一个接口,正在实现其实是在plugin中。如果我们需要自定义配置一些东西,比如配置jniLibs的libs目录,直接迁移到kts就会出现main找不到的情况,这里是因为main不是一个内置的函数,但是存在相应的成员,这个时候我们可以通过by getting方式去获取,只要我们的变量在作用域内是存在的(编译阶段会添加),就可以获取到。如果我们想要生成其他成员,也可以通过by creating{}方式去生成一个没有的成员


sourceSets{
val main by getting{
jniLibs.srcDirs("src/main/libs")
jni.srcDirs()
}

}

也可以通过getByName方式去获取


sourceSets.getByName("main")

plugins


在比较旧的版本中,我们AS默认创建引入一个plugin的方式是


apply plugin: 'com.android.application'

其实这也是依赖了groovy的动态编译机制,这里针对的是,比如android{}作用域,如果我们转换成了build.gradle.kts,我们会惊讶的发现,android{}这个作用域居然爆红找不到了!这个时候我们需要改写成


plugins {
id("com.android.application")
}

就能够找到了,那么这背后的原理是什么呢?我们有必要去探究一下gradle的内部实现。


说了这么多的应用层写法,了解我的小伙伴肯定知道,原理解析肯定是放在最后啦!但是gradle是一个庞大的工程,单单靠着干唠是写不完的,所以我选出了最重要的一个例子,即plugins的解析,希望能够抛砖引玉,一起学习下去吧!


Plugins解析


我们可以通过在gradle文件中设置断点,然后debug运行gradle调试来学习gradle,最终在编译时,我们会走到DefaultScriptPluginFactory中进行相应的任务生成,我们来看看


DefaultScriptPluginFactory


            final ScriptTarget initialPassScriptTarget = initialPassTarget(target);

ScriptCompiler compiler = scriptCompilerFactory.createCompiler(scriptSource);

// 第一个阶段Pass 1, extract plugin requests and plugin repositories and execute buildscript {}, ignoring (i.e. not even compiling) anything else
CompileOperation initialOperation = compileOperationFactory.getPluginsBlockCompileOperation(initialPassScriptTarget);
Class scriptType = initialPassScriptTarget.getScriptClass();
ScriptRunner initialRunner = compiler.compile(scriptType, initialOperation, baseScope, Actions.doNothing());
initialRunner.run(target, services);

PluginRequests initialPluginRequests = getInitialPluginRequests(initialRunner);
PluginRequests mergedPluginRequests = autoAppliedPluginHandler.mergeWithAutoAppliedPlugins(initialPluginRequests, target);

PluginManagerInternal pluginManager = topLevelScript ? initialPassScriptTarget.getPluginManager() : null;
pluginRequestApplicator.applyPlugins(mergedPluginRequests, scriptHandler, pluginManager, targetScope);

// 第二个阶段Pass 2, compile everything except buildscript {}, pluginManagement{}, and plugin requests, then run
final ScriptTarget scriptTarget = secondPassTarget(target);
scriptType = scriptTarget.getScriptClass();

CompileOperation operation = compileOperationFactory.getScriptCompileOperation(scriptSource, scriptTarget);

final ScriptRunner runner = compiler.compile(scriptType, operation, targetScope, ClosureCreationInterceptingVerifier.INSTANCE);
if (scriptTarget.getSupportsMethodInheritance() && runner.getHasMethods()) {
scriptTarget.attachScript(runner.getScript());
}
if (!runner.getRunDoesSomething()) {
return;
}

Runnable buildScriptRunner = () -> runner.run(target, services);

boolean hasImperativeStatements = runner.getData().getHasImperativeStatements();
scriptTarget.addConfiguration(buildScriptRunner, !hasImperativeStatements);
}



可以看到,源码中特别注释了,编译时的两个阶段,我们可以看到,所有的script(指函数调用),都是分别经过了阶段1和阶段2之后才真正生效的。


image.png


那么为什么android作用域在apply plugin的方式不行,plugins方式却可以呢?其实就是两个运行阶段不一致的问题。groovy可以在运行时动态找到android 这个函数,即使两者都在阶段2运行,因为groovy语法本身的特性,即使android这个函数没有定义我们也可以引用,也是在运行时阶段报错。而kotlin不一样,kotlin需要在编译的时候需要找到我们要引用的函数,即android,所以同一个阶段即plugin都没有生效(需要执行完阶段才生效),我们当然也找不到android函数,那为什么plugins又可以呢?其实很容易想到,因为plugins是在第一阶段中执行并生效的,而android引用在第二个阶段,我们接着看源码


重点关注一下compileOperationFactory.getPluginsBlockCompileOperation方法,这个方法的实现类是DefaultCompileOperationFactory,在这里我们可以看到里面定义了两个阶段


public class DefaultCompileOperationFactory implements CompileOperationFactory {
private static final StringInterner INTERNER = new StringInterner();
private static final String CLASSPATH_COMPILE_STAGE = "CLASSPATH";
private static final String BODY_COMPILE_STAGE = "BODY";

private final BuildScriptDataSerializer buildScriptDataSerializer = new BuildScriptDataSerializer();
private final DocumentationRegistry documentationRegistry;

public DefaultCompileOperationFactory(DocumentationRegistry documentationRegistry) {
this.documentationRegistry = documentationRegistry;
}

public CompileOperation getPluginsBlockCompileOperation(ScriptTarget initialPassScriptTarget) {
InitialPassStatementTransformer initialPassStatementTransformer = new InitialPassStatementTransformer(initialPassScriptTarget, documentationRegistry);
SubsetScriptTransformer initialTransformer = new SubsetScriptTransformer(initialPassStatementTransformer);
String id = INTERNER.intern("cp_" + initialPassScriptTarget.getId());
return new NoDataCompileOperation(id, CLASSPATH_COMPILE_STAGE, initialTransformer);
}

public CompileOperation getScriptCompileOperation(ScriptSource scriptSource, ScriptTarget scriptTarget) {
BuildScriptTransformer buildScriptTransformer = new BuildScriptTransformer(scriptSource, scriptTarget);
String operationId = scriptTarget.getId();
return new FactoryBackedCompileOperation<>(operationId, BODY_COMPILE_STAGE, buildScriptTransformer, buildScriptTransformer, buildScriptDataSerializer);
}
}

getPluginsBlockCompileOperation中创建了一个InitialPassStatementTransformer类对象,我们关注transform方法的内容,即如果找到了plugins,我们就进行接下来的transform操作transformPluginsBlock,这就验证了,plugins的确在第一个阶段即classpath阶段运行



@Override
public Statement transform(SourceUnit sourceUnit, Statement statement) {
...

if (scriptBlock.getName().equals(PLUGINS)) {
return transformPluginsBlock(scriptBlock, sourceUnit, statement);
}
...


总结


文章列出来了几个关键的迁移了,相信大部分的问题都可以解决了,的确在迁移到kotlin之后,还是存在一定的迁移成本的,大部分就只能生啃官网介绍,希望看完都有收获吧!


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

使用 Flutter 轻松搞定短视频上滑翻页效果

前言 我们在短视频应用中经常会看到不停上滑浏览下一条视频的沉浸式交互效果,这种交互能够让用户不停地翻页,直到找到喜欢的视频内容,从而营造一种不断“搜寻目标”的感觉,让用户欲罢不能。这种交互形式在 Flutter 中可以轻松使用 PageView 组件实现。 ...
继续阅读 »

前言


我们在短视频应用中经常会看到不停上滑浏览下一条视频的沉浸式交互效果,这种交互能够让用户不停地翻页,直到找到喜欢的视频内容,从而营造一种不断“搜寻目标”的感觉,让用户欲罢不能。这种交互形式在 Flutter 中可以轻松使用 PageView 组件实现。
上滑交互.gif


PageView 组件介绍


PageView 组件专门设计用来实现翻页效果,类定义如下:


PageView({
Key? key,
this.scrollDirection = Axis.horizontal,
this.reverse = false,
PageController? controller,
this.physics,
this.pageSnapping = true,
this.onPageChanged,
List<Widget> children = const <Widget>[],
this.dragStartBehavior = DragStartBehavior.start,
this.allowImplicitScrolling = false,
this.restorationId,
this.clipBehavior = Clip.hardEdge,
this.scrollBehavior,
this.padEnds = true,
})

其中常用的属性说明如下:



  • scrollDirection:滑动方向,可以支持纵向翻页或横向翻页,默认是横向翻页。

  • controller:翻页控制器,可以通过控制器来制定初始页,以及跳转到具体的页面。

  • onPageChanged:翻页后的回调函数,会告知翻页后的页码。

  • reverse:是否反向翻页,默认是 false。如果横向滑动翻页的话,如果开启反向翻页,则是从右到左翻页。如果是纵向翻页的话,就是从顶部到底部翻页。

  • children:在翻页中的组件列表,每一页都以自定义组件内容,因此这个组件也可以用于做引导页,或是类似滑动查看详情的效果。


使用示例


PageView 使用起来非常简单,我们先定义一个PageView 翻页的内容组件,简单地将接收的图片文件满屏显示。代码如下,实际应用的时候可以根据需要换成其他自定义组件。


 class ImagePageView extends StatelessWidget {
final String imageName;
const ImagePageView({Key? key, required this.imageName}) : super(key: key);

@override
Widget build(BuildContext context) {
return Scaffold(
body: Image.asset(
imageName,
fit: BoxFit.fitHeight,
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
),
);
}
}

之后是定义一个 PageViewDemo 来应用 PageView 翻页应用示例,代码如下:


class PageViewDemo extends StatefulWidget {
const PageViewDemo({Key? key}) : super(key: key);

@override
State<PageViewDemo> createState() => _PageViewDemoState();
}

class _PageViewDemoState extends State<PageViewDemo> {
late PageController _pageController;
int _pageIndex = 1;

@override
void initState() {
_pageController = PageController(
initialPage: _pageIndex,
viewportFraction: 1.0,
);
super.initState();
}

@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: PageView(
scrollDirection: Axis.vertical,
onPageChanged: (index) {
_pageIndex = index;
},
controller: _pageController,
allowImplicitScrolling: false,
padEnds: true,
reverse: false,
children: const [
ImagePageView(imageName: 'images/earth.jpeg'),
ImagePageView(imageName: 'images/island-coder.png'),
ImagePageView(imageName: 'images/mb.jpeg'),
],
),
);
}
}

这个示例里,我们的 pageController 只是演示了设置初始页码。我们看到的 viewportFraction 可以理解为一页内容占据屏幕的比例,比如我们可以设置该数值为1/3,支持一个屏幕分段显示3个页面内容。


分段显示.gif


PageController 应用


PageController 可以控制滑动到指定位置,比如我们可以调用 animateToPage方法实现一个快速滑动到顶部的悬浮按钮。


floatingActionButton: FloatingActionButton(
onPressed: () {
_pageController.animateToPage(
0,
duration: const Duration(
milliseconds: 1000,
),
curve: Curves.easeOut,
);
},
backgroundColor: Colors.black.withAlpha(180),
child: const Icon(
Icons.arrow_upward,
color: Colors.white,
),
),

实现效果如下。


滑动到顶部.gif
PageController 还有如下控制翻页的方法:



  • jumpToPage:跳转到指定页面,但是没有动画。注意这里不会校验页码是否会超出范围。

  • nextPage:滑动到下一页,实际上调用的是 animateToPage 方法。

  • previousPage:滑动到上一页,实际上调用的是 animateToPage 方法。


总结


本篇介绍了 Flutter 的翻页组件 PageView 的使用,通过 PageView 可以轻松实现类似短视频的纵向上滑翻页的效果,也可以实现横向翻页效果(如阅读类软件)。在接下来的系列文章中,本专栏将会介绍更多 Flutter 实用的组件。


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

❤️Android 快别用Toast了,来试试Snackbar❤️

🔥 应用场景 Toast提示默认显示在界面底部,使用Toast.setGravity()将提示显示在中间,如下: Toast toast = Toast.makeText(this, str, Toast.LENGTH_SHORT); ...
继续阅读 »

🔥 应用场景


Toast提示默认显示在界面底部,使用Toast.setGravity()将提示显示在中间,如下:


        Toast toast = Toast.makeText(this, str, Toast.LENGTH_SHORT);
toast.setGravity(Gravity.CENTER, 0, 0);
toast.show();

运行在在Android 12上无法显示,查看Logcat提示如下:


Toast: setGravity() shouldn't be called on text toasts, the values won't be used

意思就是:你不能使用toast调用setGravity,调用无效。哎呀,看给牛气的,咱看看源码找找原因


🔥 源码


💥 Toast.setGravity()


    /**
* 设置Toast出现在屏幕上的位置。
*
* 警告:从 Android R 开始,对于面向 API 级别 R 或更高级别的应用程序,此方法在文本 toast 上调用时无效。
*/
public void setGravity(int gravity, int xOffset, int yOffset) {
if (isSystemRenderedTextToast()) {
Log.e(TAG, "setGravity() shouldn't be called on text toasts, the values won't be used");
}
mTN.mGravity = gravity;
mTN.mX = xOffset;
mTN.mY = yOffset;
}

妥了,人家就告诉你了 版本>=Android R(30),调用该方法无效。无效就无效呗,还不给显示了,过分。


Logcat的提示居然是在这里提示的,来都来了,咱们看看isSystemRenderedTextToast()方法。


💥 Toast.isSystemRenderedTextToast()


    /**
*Text Toast 将由 SystemUI 呈现,而不是在应用程序内呈现,因此应用程序无法绕过后台自定义 Toast 限制。
*/
@ChangeId
@EnabledAfter(targetSdkVersion = Build.VERSION_CODES.Q)
private static final long CHANGE_TEXT_TOASTS_IN_THE_SYSTEM = 147798919L;

private boolean isSystemRenderedTextToast() {
return Compatibility.isChangeEnabled(CHANGE_TEXT_TOASTS_IN_THE_SYSTEM) && mNextView == null;
}

重点了。Text Toast 将由 SystemUI 呈现,而不是在应用程序内呈现。


清晰明了,可以这样玩,但是你级别不够,不给你玩。


事情整明白了,再想想解决解决方案。他说了Text Toast 将由 SystemUI 呈现,那我不用 Text 不就行了。


🔥 Toast 提供的方法


先看看Tast提供的方法:



有这几个方法。咱们实践一下。保险起见看看源码


💥 Toast.setView() 源码


    /**
* 设置显示的View
* @deprecated 自定义 Toast 视图已弃用。 应用程序可以使用 makeText 方法创建标准文本 toast,
* 或使用 Snackbar
*/
@Deprecated
public void setView(View view) {
mNextView = view;
}

这个更狠,直接弃用。




  • 要么老老实实的用默认的Toast。




  • 要么使用 Snackbar。




🔥 Snackbar


Snackbar 就是一个类似Toast的快速弹出消息提示的控件(我是刚知道,哈哈)。


与Toast相比:




  • 一次只能显示一个




  • 与用户交互



    • 在右侧设置按钮来添加事件,根据 Material Design 的设计原则,只显示 1 个按钮 (添加多个,以最后的为准)




  • 提供Snackbar显示和关闭的监听事件



    • BaseTransientBottomBar.addCallback(BaseCallback)




💥 代码实现


    showMessage(findViewById(android.R.id.content), str, Snackbar.LENGTH_INDEFINITE);

public static void showMessage(View view, String str, int length) {
Snackbar snackbar = Snackbar.make(view, str, length);

View snackbarView = snackbar.getView();
//设置布局居中
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(snackbarView.getLayoutParams().width, snackbarView.getLayoutParams().height);
params.gravity = Gravity.CENTER;
snackbarView.setLayoutParams(params);
//文字居中
TextView message = (TextView) snackbarView.findViewById(R.id.snackbar_text);
//View.setTextAlignment需要SDK>=17
message.setTextAlignment(View.TEXT_ALIGNMENT_GRAVITY);
message.setGravity(Gravity.CENTER);
message.setMaxLines(1);
snackbar.addCallback(new BaseTransientBottomBar.BaseCallback<Snackbar>() {
@Override
public void onDismissed(Snackbar transientBottomBar, int event) {
super.onDismissed(transientBottomBar, event);
//Snackbar关闭
}

@Override
public void onShown(Snackbar transientBottomBar) {
super.onShown(transientBottomBar);
//Snackbar显示
}
});
snackbar.setAction("取消", new View.OnClickListener() {
@Override
public void onClick(View v) {
//显示一个默认的Snackbar。
Snackbar.make(view, "我先走", BaseTransientBottomBar.LENGTH_LONG).show();
}
});
snackbar.show();
}

Snackbar.make的三个参数:



  • View:从View中找出当前窗口最外层视图,然后在其底部显示。

  • 第二个参数(text)

    • CharSequence

    • StringRes



  • duration(显示时长)

    • Snackbar.LENGTH_INDEFINITE 从 show()开始显示,直到它被关闭或显示另一个 Snackbar

    • Snackbar.LENGTH_SHORT 短时间

    • Snackbar.LENGTH_LONG 长时间

    • 自定义持续时间 以毫秒为单位




💥 效果


Android 12



Android 5.1



💥 工具类


如果觉得设置麻烦可以看看下面这边文章,然后整合一套适合自己的。


一行代码搞定Snackbar


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

Vue.js 3 开源组件推荐:代码差异查看器插件

web
一个Vue.js差异查看器插件,可以用来比较两个代码片断之间的差异。Github地址:github.com/hoiheart/vu…支持语言:cssxml: xml, html, xhtml, rss, atom, xjb, xsd, xsl, plist, ...
继续阅读 »

一个Vue.js差异查看器插件,可以用来比较两个代码片断之间的差异。


Github地址:github.com/hoiheart/vu…

支持语言:

  • css

  • xml: xml, html, xhtml, rss, atom, xjb, xsd, xsl, plist, svg

  • markdown: markdown, md, mkdown, mkd

  • javascript: javascript, js, jsx

  • json

  • plaintext: plaintext, txt, text

  • typescript: typescript, ts

如何使用:

  1. 导入并注册diff查看器。

import VueDiff from 'vue-diff'
import 'vue-diff/dist/index.css'
app.use(VueDiff);

2.向模板中添加组件。

<Diff />

3.可用的组件props。

mode: {
 type: String as PropType<Mode>,
 default: 'split' // or unified
},
theme: {
 type: String as PropType<Theme>,
 default: 'dark' // or light
},
language: {
 type: String,
 default: 'plaintext'
},
prev: {
 type: String,
 default: ''
},
current: {
 type: String,
 default: ''
},
inputDelay: {
 type: Number,
 default: 0
},
virtualScroll: {
 type: [Boolean, Object] as PropType<boolean|VirtualScroll>,
 default: false
}

4.使用 highlight.js 扩展插件。

// 注册一门新语言
import yaml from 'highlight.js/lib/languages/yaml'
VueDiff.hljs.registerLanguage('yaml', yaml)

作者:杭州程序员张张
来源:juejin.cn/post/7156839676677423112

收起阅读 »

随机裁员?Meta用算法随机裁掉60名“劳务派遣”员工

Facebook 母公司 Meta 最近使用算法“随机”解雇了 60 名来自埃森哲的劳务派遣人员。此前 Meta 与埃森哲签订了近 5 亿美元的合同,由隶属于后者的劳务派遣人员到 Meta 位于奥斯汀的办公室工作,主要开展内容审核和商业诚信等业务。Meta 通...
继续阅读 »

Facebook 母公司 Meta 最近使用算法“随机”解雇了 60 名来自埃森哲的劳务派遣人员。

此前 Meta 与埃森哲签订了近 5 亿美元的合同,由隶属于后者的劳务派遣人员到 Meta 位于奥斯汀的办公室工作,主要开展内容审核和商业诚信等业务。


Meta 通过视频电话会议告知被裁的 60 名员工,裁员将于 9 月 2 日正式生效,10 月 3 日结束工资发放。除了明确是“随机”选择之外,Meta 没有给出裁员的具体原因。

埃森哲没有立即向这些劳务派遣人员提供其他工作机会,但这些员工被告知可以在未来两周内重新申请新职位。

在今年 6 月 30 日公司举行的一次全体员工大会上,Meta 首席执行官马克・扎克伯格(Mark Zuckerberg)警告员工,最近的市场低迷“可能是我们近年来看到的最严峻的挑战之一”,因此需要通过“积极的业绩评估”来淘汰表现不佳的员工。从扎克伯格的话来看,这次裁员也许并不令人意外。

扎克伯格说:“实际上,公司里可能有很多人不该留在这里。”

扎克伯格补充道:“通过提高期望值,制定更有进取心的目标,并稍微加大压力,我想这可能会让你们中的一些人觉得这个地方不适合自己。我觉得这种自我选择没问题。”

在举行这次全员大会之时,Meta 已经采取了冻结招聘和其他削减成本措施,主要是因为公司股票今年以来已经下跌过半。

就在 Meta 裁减劳务派遣人员几天前,苹果解雇了 100 名负责招聘新员工的人事专员。苹果此前确实警告称,公司将控制支出并放缓招聘。

去年 8 月份,游戏行业支付处理公司 Xsolla 也使用算法裁掉了 150 名员工,所以让机器人解雇员工可能是未来的一种趋势。

来源:IT之家

收起阅读 »

1024程序员节,别人家的公司真香!羡慕ing~

今天是传说中属于程序猿的节日,各大互联网公司已经开整,小编已经在朋友圈里感受到了不同氛围的节日氛围,为大家整合了以下几类:一、掏心窝子型有哪个程序员能对漂亮小姐姐说不?!天天在办公室撸代码的码农而言,在黑白的代码间,小姐姐就是天使一样的存在~没看错,是真人女仆...
继续阅读 »

今天是传说中属于程序猿的节日,各大互联网公司已经开整,小编已经在朋友圈里感受到了不同氛围的节日氛围,为大家整合了以下几类:


一、掏心窝子型


有哪个程序员能对漂亮小姐姐说不?!天天在办公室撸代码的码农而言,在黑白的代码间,小姐姐就是天使一样的存在~


没看错,是真人女仆出现了。


我见过好事成双, 却没想过能站在女团中央~



还有献舞的小姐姐,一起蹦虾咔啦咔


同时还有男人的终极梦想,你相信光吗?


二、驱魔保命型


程序员的梦想是什么 No Code No Bug,此符居家旅行,建议常备。


虽说是防bug,可这猫仔何意?防BUG灵兽?


三、紧张兮兮型


不是所有的符都有用,比如这块氛围感糖饼的出现,让舒缓神经再次紧绷起来,瞬间觉得手里的符咒不香了。


如果有比这个还让人紧张的,那就是抠破了~


这个拔河游戏,看得D哥虎躯一震,往前一步是孤独,退后一步是幸福


四、扎得不行型


开开心心过节不行吗?这波操作,扎疼了码农的心。


比如:这个平平无奇的小黑盒竟读懂了我的内心,不过这个应该送给老板吧


泪崩,你以为我不想有个对象吗?


谁能拒绝一个奔三的秃头小宝贝?爱护头顶,从防脱开始,所以接下来是防脱专场:


单瓶装:防脱就防脱,旁边的青春永驻,是何意?


礼盒装:防脱产品都是成双成对,你呢?


套装:我宣布,今年这篇头顶被我承包了


嗯嗯,终于明白,霸王才是真真的程序员之友。


五、“特殊”服务型


肩颈不适是程序员们的通病,一顿贴心的按摩服务,也能让程序员朋友短暂放松,看这架势,专业~


不过,有些公司的定制化服务,简直服务到工位,反手就是一个赞~


其实,舒不舒服不重要,就是想体验差别化服务。


六、斗智斗勇型


不少公司开启游园会项目,打卡所有项目,就能兑换礼品,游戏项目包括但不限于:


穿越火线(这游戏搁夜市必火)


赌场风云(赌啥,KPI吗?)


数字coding(呵呵,怕这个就枉为程序猿)


也可窥见,很多人事绞尽脑汁,只为大家欢愉一刻,这个必须加鸡腿儿。


七、吃饱喝足型


不少公司准备了精致下午茶,慰藉代码兄弟们,昨天已经被朋友圈投喂饱了,独乐乐不如众乐乐,上图(菜):


精致可口的甜品,琳琅满目的零食,啧啧啧···



零食就算了,大闸蟹就过分了!



八、彰显身份型


一些公司虽然准备的是日常用品,但是····我们一定要透过现象看本质,体味公司的一番深意,比如:


公司送衬衣,称(衬)心如意(衣)。好兆头,这么正式的衣服,恨不能现在就穿上,感受节日氛围。


公司送双肩包,寓意:双减(双肩)别想了,但保(包)你有饭吃。


公司送键盘,沉吟片刻,我悟到了:见(键)一个,盘一个,淦!



礼物或大或小,心意或深或浅,1024,希望大家都能1G棒~


欢迎评论区留言,说出你的程序员礼物~


注:文章素材来源于网络。如侵,请联系删除


收起阅读 »

过几年你不看,就不用胡椒盐

法规及法规vbnmbnm,bn鼓风机发个人fghjghffg不会难看美女吧

法规及法规vbnmbnm,bn鼓风机发个人fghjghffg不会难看美女吧

fghjgf8ytuj复工后的非官方的个

和对方过后就VB你吧VNfghjghffg好看吗帮你们

和对方过后就VB你吧VNfghjghffg好看吗帮你们

非过户结果符合复工后很过分

法国的红酒地方各个很舒服fghjghffg搞好看皇冠

法国的红酒地方各个很舒服fghjghffg搞好看皇冠

小客户更健康分工会经费

发个机会规范fghjghffg发个机会功夫就能发个和

发个机会规范fghjghffg发个机会功夫就能发个和

Flutter 小技巧之优化你的代码性能

又到了小技巧系列更新时间,今天我们分享一个比较轻松的内容:Flutter 里的代码优化,优化的目的主要是为了提高性能和可维护性,放心,本篇我们不讲深入的源码分析,就是分享最最最基础的布局代码优化。我们先从一个简单的例子开始,相信大家对于 Flutter 的 U...
继续阅读 »

又到了小技巧系列更新时间,今天我们分享一个比较轻松的内容:Flutter 里的代码优化,优化的目的主要是为了提高性能和可维护性,放心,本篇我们不讲深入的源码分析,就是分享最最最基础的布局代码优化。

我们先从一个简单的例子开始,相信大家对于 Flutter 的 UI 构建不会陌生,那么如下代码所示,日常开发过程中 A 和 B 这两种代码组织方式,你更常用的是哪一种?

A (函数方式)B (Component Class 方式)

如果是从代码运行之后的 UI 效果来看,这两个方式运行之后的布局效果并不会有什么差异,而通常因为可以写更少代码和参数调用更方便等原因,我们可能在编写页面的内部控件时,会更经常使用 A (函数方式) 这种写法,也有称之为 Helper Method 的叫法。

那使用函数方式构建 UI 有没有问题?答案肯定是没问题,但是某些场景下,对比使用 B (Component Class 方式) ,可能性能表现上相对没那么优秀

举个例子,如下代码所示,在 renderA 函数里我们通过点击按键修改 count,在修改之后触发 UI 渲染时就需要用到 setState ,也就是我们每点一下,当前整个页面就是触发一次 rebuild ,但是我们只是想要改变当前 renderA 里的 count 文本而已。

这就是使用函数构建内部控件最常见的问题之一,因为子控件更新时是通过父容器的 setState ,所以每次子控件比如 renderA 发生变化时,就会触发整个 Widget 都出现 rebuild ,这其实并不是特别符合我们的预期。

科普一个众所周知的知识点, setState 其实就是调用 StatefulWidget 对应的 StatefulElement 里的 markNeedsBuild 方法,也就是对 Element (BuildContext) 里的 _dirty 标识为设置为 true ,仅此而已, 然后等待下次渲染更新

当然,你说像 renderA 这种写法会引起很严重的性能问题吗?事实上并不会,因为众所周知 Flutter 里的 UI 构建是通过多个不同的树来完成的,而 Widget 并不是真实的控件,所以一般情况下 renderA 这种写法导致的 rebuild 是不会产生严重的性能缺陷。

但是,如果同级下你的 renderB 是如下所示这样的情况呢?虽然这段代码毫无意义,但是我们在 renderA 点击改变 count 的时候,其实并没有改变 renderB 的用到的 status 参数,但是因为 renderA 里调用了 setState ,导致 renderB 每次都会进行重复进行浮点计算。

当然你可以说我写个变量进行缓存提前判断也可以解决,但这并不是这个例子的关键,那如果把上面这个例子变成 Component Class 的方式会有什么好处:

  • A 在点击更新 count 时不会影响其他控件
  • B 控件通过 didUpdateWidget 可以用更优雅的方式决定更新条件

这样看起来是不是更合理一些?另外 Component Class 的实现方式,也能在一定层度解决代码层级嵌套的问题,有时候实现一些 Component Class 的模版也可以成为 Flutter 里提高效率的工具,这个后面我们会聊到。

当然使用 Component Class 在无形之中会需要你写更多的代码,同时控件之间的状态联动成本也会有所提高,例如你需要在 B 控件关联 A 的 count 变化去改变高度,这时候可能就需要加入 InheritedWidget 或者 ValueNotifier 等方式来实现。

例如 Flutter 里 DefaultTabController 配合 TabBar 和 TabBarView 的实现就是一个很好的参考。

 Widget build(BuildContext context) {
return DefaultTabController(
length: myTabs.length,
child: Scaffold(
appBar: AppBar(
bottom: TabBar(
tabs: myTabs,
),
),
body: TabBarView(
children: myTabs.map((Tab tab) {
final String label = tab.text.toLowerCase();
return Center(
child: Text(
'This is the $label tab',
style: const TextStyle(fontSize: 36),
),
);
}).toList(),
),
),
);
}

所以到这里我们理解一个小技巧:在不偷懒的情况下,使用 Component Class 的方式实现子控件会比使用函数方式可能得到更好的性能和代码结构

当然,使用 Component Class 实现的方式,在调试时也会比函数方式更方便,如下图所示,当使用函数方式布局时,你在 Flutter Inspector 里看到的 Widget Tree 和 Details Tree 是完全铺平的情况,也没办法定制调试参数。

但是当你 Component Class 组织布局的时候,你就可以通过 override debugFillProperties 方法来可视化一些参数状态,例如 ItemA 里可以把 count 添加到 debugFillProperties 里,这样在 Details Tree 里也可以直观看到目前的 count 状态信息。

所以这里又有一个小技巧:通过 override debugFillProperties ,可以定制一些 Debug 时的可视化参数来帮助我们更好调试布局

既然讲到利用 Component Class 组织布局,那就不得不聊一个典型的控件:AnimatedBuilder 。

AnimatedBuilder 可以是最常说到的一个性能优化的例子, 一般情况下在页面的子控件里使用动画,特别是循环动画的话,我们都会建议使用前面介绍的 Component Class 方式,不然动画导致当前页面不停 rebuild 肯定会导致性能影响。

但是有时候我就不想用 Component Class 该怎么办?我就是想写在当前 Page 里,那就可以使用 AnimatedBuilder ,你只要把需要执行动画的部分放到 builder 方法里就好了。

因为 AnimatedBuilder 的内部会有一个 _AnimatedState 用于独立触发 setState,从而执行外部 builder 方法执行动画效果

类似 AnimatedBuilder 的模版实现,可以在一定程度上解决使用 Component Class 的痛点,当然,在使用 AnimatedBuilder 还是有一些需要注意, 比如 child 如果不需要跟随动画进行其他变化,一般是要放到 AnimatedBuilder 的 child 配置里,因为如果直接放在 builder 方法里,那就会出现 child 也跟随动画重新 rebuild 的情况,但是如果是放到 child 配置项里,那就是调用了 child 的对象缓存。

不正确使用正确使用
image-20221020175113790

如果对于这个缓存概念不理解,可以参考 《MediaQuery 和 build 优化你不知道的秘密》 里的“缓存区域不随帧变化,以便得到最小化的构建”。

当然类似 AnimatedBuilder 的构建方式还要注意 context 问题,不要拿错 context ,这也是很多时候会犯的潜在错误,特别是在调用 of(context) 的时候。

那有的人可能到这里会觉得,那你之前一直说 Widget 很轻,Widget 不是真正的控件,那 rebuild 多几次有什么问题?

一般情况下确实不会有太大问题,但是当你的控件有 Opacity 、ColorFilter 、 ShaderMash 或者 ClipRectClip.antiAliasWithSaveLayer)时,就可能会有较大的性能影响,因为他们都是可能会触发 saveLayer 的操作。

为什么 saveLayer 对性能影响很大?因为需要在 GPU 绘制是需要增加额外的缓冲区域,粗俗点说就是需要做图层的保存和合成,这就会对 GPU 渲染时产生较大影响的耗时。

而这里面最常遇到的应该就是 Opacity 带来的性能问题,因为它看起来是那么的轻便,但是从官方的介绍里,除非真的有必要,不然可以使用效果类似的实现去做场景替代,例如:

你需要对图片做透明度相关的动画是,那么使用 AnimatedOpacity 或 FadeInImage 代替 Opacity 会对性能更有帮助

AnimatedOpacity 和 Opacity 不一样吗?某种程度上还真不大一样, Opacity 的内部是 pushOpacity 的操作,而 AnimatedOpacity 里虽然有 OpacityLayer ,但是变动时是 updateCompositedLayer ;而 FadeInImage 会使用 GPU 的 fragment shader 去处理透明度的问题,所以性能也会更好一些。

或者在类似有颜色透明度的场景时,可以通过 Color.fromRGBO 来替代 Opacity ,除非你需要将不透明度应用到一大组较为复杂的 child 里,你才会需要使用 Opacity 。

/// no
Opacity(opacity: 0.5, child: Container(color: Colors.red))

/// yes
Container(color: Color.fromRGBO(255, 0, 0, 0.5))

另外还有 IntrinsicHeight / IntrinsicWidth 的场景,因为它们是可以通过 child 的内部宽高来调整 child 的大小,但是这个推算布局的过程会比较费时,可能会到 O(N²),虽然 Flutter 里针对这部分计算结果做了缓存,但是不妨碍它的耗时。

这么说可能有点抽象,举一个官方介绍过的例子,如下代码所示,当你在 ListView 里对 Row 的 children 进行 Align 排列时,你可能会发现它没有效果,因为此时通过 Border 可以看到,绿色和蓝色方框的父容器大小一致。

但是在加上 IntrinsicHeight 之后, 因为通过 IntrinsicHeight 的测算之后再返回 size,Row 里的三个 Item 现在高度一致,,这时候 Align 就可以生效了,但是正如前面所说,这个操作性对性能来说相对昂贵,虽然系统有缓存参数,但是如果出现动画 rebuild ,也会对性能造成影响。

对这部分感兴趣的可以看 : 《带你了解不一样的 Flutter》

到这里我们就理解了 (函数方式) 和 (Component Class 方式)组织布局的不同之处,同时也知道了 Component Class 方式可以帮助我们更好地调试布局代码,也举例了一些 UI 布局里常见的耗时场景

那本篇的小技巧到这里就结束了,如果你还有什么感兴趣或者有疑惑的,欢迎留言评论~


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

聊一聊Kotlin之data class

Kotlin是由JetBrains开发的针对JVM、Android和浏览器的静态编程语言,是Android的官方语言。Kotlin拥有较多高级而又简洁的语法特性,提升了我们的开发效率,减少了代码量。在使用 java 的时候,我们在用class定义一个entit...
继续阅读 »

Kotlin是由JetBrains开发的针对JVM、Android和浏览器的静态编程语言,是Android的官方语言。Kotlin拥有较多高级而又简洁的语法特性,提升了我们的开发效率,减少了代码量。

在使用 java 的时候,我们在用class定义一个entity时除了写get、set方法(使用Kotlin后省了这部分工作),经常还会重写类的 equalshashCodetoString方法,这些方法往往都是模板化的。在 kotlin 中提供了data class搞定这些模版化代码。

data class与class的区别:

实现方式

  • class类
    class ClassUser(val name: String, var age: Int)
  • data class类
    data class DataClassUser(val name: String, var age: Int)

自动重写toString方法

  • data类的toString方法会打印出属性的值
  • 非data类的toString方法则打印出内存地址
    val classUser = ClassUser("classuser", 18)
    val dataClassUser = DataClassUser("dataclassuser", 20)
    println("ClassUser -> ${classUser.toString()}")
    // ClassUser -> com.imock.vicjava.keyuse.ClassUser@11026067

    println("DataClassUser -> ${dataClassUser.toString()}")
    // DataClassUser -> DataClassUser(name=dataclassuser, age=20)

新增componentN方法

  • data类新增属性的componentN方法,component1代表第一个属性,component2代表第二个属性。(常用于解构声明)
    val dataClassUser = DataClassUser("dataclassuser", 20)
    println("DataClassUser component1() -> ${dataClassUser.component1()}")
    // DataClassUser component1() -> dataclassuser

    println("DataClassUser component2() -> ${dataClassUser.component2()}")
    // DataClassUser component2() -> 20

新增copy方法

  • data类新增copy方法,可以用来修改部分属性,但是保持其他不变。
    val dataClassUser = DataClassUser("dataclassuser", 20)
    println("ClassUser toString() -> ${classUser.toString()}")
    // DataClassUser -> DataClassUser(name=dataclassuser, age=20)

    val newDataClassUser = dataClassUser.copy(age = 22)
    println("DataClassUser copy -> ${newDataClassUser.toString()}")
    // DataClassUser copy -> DataClassUser(name=dataclassuser, age=22)

重写hashCode和 equals方法

  • data类重写hashCode方法,equals方法可以稍后看下源码,先判断两个是否是同一个对象,如果不是则进行类型判断,是相同类型则逐个比较属性的值。 
    val classUserLisa1 = ClassUser("lisa", 20)
    val classUserLisa2 = ClassUser("lisa", 20)
    println("ClassUser equals -> ${classUserLisa1.equals(classUserLisa2)}")
    // ClassUser equals -> false
    println("classUserLisa1 hashCode -> ${classUserLisa1.hashCode()}")
    // classUserLisa1 hashCode -> 2081652693
    println("classUserLisa2 hashCode -> ${classUserLisa2.hashCode()}")
    // classUserLisa2 hashCode -> 406765571

    val dataClassUserLisa1 = DataClassUser("lisa", 20)
    val dataClassUserLisa2 = DataClassUser("lisa", 20)
    println("DataClassUser equals -> ${dataClassUserLisa1.equals(dataClassUserLisa2)}")
    // DataClassUser equals -> true
    println("dataClassUserLisa1 hashCode -> ${dataClassUserLisa1.hashCode()}")
    // dataClassUserLisa1 hashCode -> 102981865
    println("dataClassUserLisa2 hashCode -> ${dataClassUserLisa2.hashCode()}")
    // dataClassUserLisa2 hashCode -> 102981865

data class为何如此神奇

data class DataClassUser(val name: String, var age: Int)

class ClassUser(var name: String, var age: Int)

单独看实现上两者没有太大的区别,一个使用data class,一个使用class,为何data class却多出那么多能力?得益于Kotlin高级的语法特性。我们都知道kotlin最终还是要编译成 java class 在 JVM 上运行的,为了更好的理解Kotlin高级而又简洁的语法特性,有时我们需要看看用kotlin写完的代码编译后是什么样子。Talk is cheap, show me the code.

class类编译后的java代码

Kotlin写法如下:

class ClassUser(var name: String, var age: Int)

查看编译后的java代码如下,可以看到帮我们自动生成了get、set和构造方法:

public final class ClassUser {
@NotNull
private final String name;
private int age;
@NotNull
public final String getName() {
return this.name;
}
public final int getAge() {
return this.age;
}
public final void setAge(int var1) {
this.age = var1;
}
public ClassUser(@NotNull String name, int age) {
Intrinsics.checkNotNullParameter(name, "name");
super();
this.name = name;
this.age = age;
}
}

data class类编译后的java代码

Kotlin写法如下:

data class DataClassUser(val name: String, var age: Int)

查看其编译后的java代码如下,会发现比class类编译后的代码多了部分方法,新增了componentscopy方法,重写了equalshashCodetoString方法。

public final class DataClassUser {
@NotNull
private final String name;
private int age;
@NotNull
public final String getName() {
return this.name;
}
public final int getAge() {
return this.age;
}
public final void setAge(int var1) {
this.age = var1;
}
public DataClassUser(@NotNull String name, int age) {
Intrinsics.checkNotNullParameter(name, "name");
super();
this.name = name;
this.age = age;
}
// 新增方法
@NotNull
public final String component1() {
return this.name;
}
// 新增方法
public final int component2() {
return this.age;
}
// 新增方法
@NotNull
public final DataClassUser copy(@NotNull String name, int age) {
Intrinsics.checkNotNullParameter(name, "name");
return new DataClassUser(name, age);
}
// 新增方法
// $FF: synthetic method
public static DataClassUser copy$default(DataClassUser var0, String var1, int var2, int var3, Object var4) {
if ((var3 & 1) != 0) {
var1 = var0.name;
}
if ((var3 & 2) != 0) {
var2 = var0.age;
}
return var0.copy(var1, var2);
}
// 重写该方法
@NotNull
public String toString() {
return "DataClassUser(name=" + this.name + ", age=" + this.age + ")";
}
// 重写该方法
public int hashCode() {
String var10000 = this.name;
return (var10000 != null ? var10000.hashCode() : 0) * 31 + Integer.hashCode(this.age);
}
// 重写该方法
public boolean equals(@Nullable Object var1) {
if (this != var1) {
if (var1 instanceof DataClassUser) {
DataClassUser var2 = (DataClassUser)var1;
if (Intrinsics.areEqual(this.name, var2.name) && this.age == var2.age) {
return true;
}
}
return false;
} else {
return true;
}
}
}

总结

知道data class干了啥

  • 重写toString方法。
  • 新增componentN方法(component1()、component2()、...、componentN()),其对应属性的声明顺序(常用于解构声明)。
  • 新增copy方法,可以用来修改部分属性,但是保持其他不变。
    特别提下copy方法,可能有些同学疑问很少见到这个方法使用场景,慢慢地等你用上了MVI框架就知道State必须使用 Kotlin data class,copy方法的应用自然少不了。
  • 重写equalshasCode方法,equals()方法不再单一比较对象引用,而是先判断两个是否是同一个对象,如果不是则进行类型判断,是相同类型则逐个比较属性的值。  

使用data class需要注意啥

  • 主构造函数必须要至少有一个参数。
  • 主构造函数中的所有参数必须被标记为val或者var。
  • 数据类不能有以下修饰符:abstract、inner、open、sealed。  


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

一看就懂!图解 Kotlin SharedFlow 缓存系统

前言Kotlin 为我们提供了两种创建“热流”的工具:StateFlow 和 SharedFlow。StateFlow 经常被用来替代 LiveData 充当架构组件使用,所以大家相对熟悉。其实 StateFlow 只是 SharedFlo...
继续阅读 »

前言

Kotlin 为我们提供了两种创建“热流”的工具:StateFlow 和 SharedFlow。StateFlow 经常被用来替代 LiveData 充当架构组件使用,所以大家相对熟悉。其实 StateFlow 只是 SharedFlow 的一种特化形式,SharedFlow 的功能更强大、使用场景更多,这得益于其自带的缓存系统,本文用图解的方式,带大家更形象地理解 SharedFlow 的缓存系统。

创建 SharedFlow 需要使用到 MutableSharedFlow() 方法,我们通过方法的三个参数配置缓存:

fun <T> MutableSharedFlow(
replay: Int = 0,
extraBufferCapacity: Int = 0,
onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND
): MutableSharedFlow<T>

接下来,我们通过时序图的形式介绍这三个关键参数对缓存的影响。正文之前让我们先统一一下用语:

  • Emitter:Flow 数据的生产者,从上游发射数据
  • Subcriber:Flow 数据的消费者,在下游接收数据

replay

当 Subscriber 订阅 SharedFlow 时,有机会接收到之前已发送过的数据,replay 指定了可以收到 subscribe 之前数据的数量。replay 不能为负数,默认值为 0 表示 Subscriber 只能接收到 subscribe 之后 emit 的数据:

上图展示的是 replay = 0 的情况,Subscriber 无法收到 subscribe 之前 emit 的 ❶,只能接收到 ❷ 和 ❸。

当 replay = n ( n > 0)时,SharedFlow 会启用缓存,此时 BufferSize 为 n,意味着可以缓存发射过的最近 n 个数据,并发送给新增的 Subscriber。

上图以 n = 1 为例 :

  1. Emitter 发送 ❶ ,并被 Buffer 缓存
  2. Subscriber 订阅 SharedFlow 后,接收到缓存的 ❶
  3. Emitter 相继发送 ❷ ❸ ,Buffer 缓存的数据相继依次被更新

在生产者消费者模型中,有时消费的速度赶不及生产,此时要加以控制,要么停止生产,要么丢弃数据。SharedFlow 也同样如此。有时 Subscriber 的处理速度较慢,Buffer 缓存的数据得不到及时处理,当 Buffer 为空时,emit 默认将会被挂起 ( onBufferOverflow = SUSPEND)

上面的图展示了 replay = 1 时 emit 发生 suspend 场景:

  1. Emitter 发送 ❶ 并被缓存
  2. Subscriber 订阅 SharedFlow ,接收 replay 的 ❶ 开始处理
  3. Emitter 发送 ❷ ,缓存数据更新为 ❷ ,由于 Subscriber 对 ❶ 的处理尚未结束,❷ 在缓存中没有及时被消费
  4. Emitter 发送 ❸,由于缓存的 ❷ 尚未被 Subscriber 消费,emit 发生挂起
  5. Subscriber 开始消费 ❷ ,Buffer 缓存 ❸ , Emitter 可以继续 emit 新数据

注意 SharedFlow 作为一个多播可以有多个 Subscriber,所以上面例子中,❷ 被消费的时间点,取决于最后一个开始处理的 Subscriber。

extraBufferCapacity

extraBufferCapacity 中的 extra 表示 replay-cache 之外为 Buffer 还可以额外追加的缓存。

若 replay = n, extraBufferCapacity = m,则 BufferSize = m + n

extraBufferCapacity 默认为 0,设置 extraBufferCapacity 有助于提升 Emitter 的吞吐量

在上图的基础之上,我们再设置 extraBufferCapacity = 1,效果如下图:

上图中 BufferSize = 1 + 1 = 2 :

  1. Emitter 发送 ❶ 并得到 Subscriber1 的处理 ,❶ 作为 replay 的一个数据被缓存,
  2. Emitter 发送 ❷,Buffer 中 replay-cache 的数据更新为 ❷
  3. Emitter 发送 ❸,Buffer 在存储了 replay 数据 ❷ 之上,作为 extra 又存储了 ❸
  4. Emitter 发送 ❹,此时 Buffer 已没有空余位置,emit 挂起
  5. Subscriber2 订阅 SharedFlow。虽然此时 Buffer 中存有 ❷ ❸ 两个数据,但是由于 replay = 1,所以 Subscriber2 只能收到最近的一个数据 ❸
  6. Subscriber1 处理完 ❶ 后,依次处理 Buffer 中的下一个数据,开始消费 ❷
  7. 对于 SharedFlow 来说,已经不存在没有消费 ❷ 的 Subscriber,❷ 移除缓存,❹ 的 emit 继续,并进入缓存,此时 Buffer 又有两个数据 ❸ ❹ ,
  8. Subscriber1 处理完 ❷ ,开始消费 ❸
  9. 不存在没有消费 ❸ 的 Subscriber, ❸ 移除缓存。

onBufferOverflow

前面的例子中,当 Buffer 被填满时,emit 会被挂起,这都是建立在 onBufferOverflow 为 SUSPEND 的前提下的。onBufferOverflow 用来指定缓存移除时的策略,除了默认的 SUSPEND,还有两个数据丢弃策略:

  • DROP_LATEST:丢弃最新的数据
  • DROP_OLDEST:丢弃最老的数据

需要特别注意的是,当 BufferSize = 0 时,extraBufferCapacity 只支持 SUSPEND,其他丢弃策略是无效的。这很好理解,因为 Buffer 中没有数据,所以丢弃无从下手,所以启动丢弃策略的前提是 Buffer 至少有一个缓冲区,且数据被填满

上图展示 DROP_LATEST 的效果。假设 replay = 2,extra = 0

  1. Emitter 发送 ❸ 时,由于 ❶ 已经被消费,所以 Buffer 数据从 ❶❷ 变为 ❷❸
  2. Emitter 发送 ❹ 时,由于 ❷ 还未被消费,Buffer 处于填满状态, ❹ 直接被丢弃
  3. Emitter 发送 ❺ 时,由于 ❷ 已经被费,可以移除缓存,Buffer 数据变为 ❸❺

上图展示了 DROP_OLDEST 的效果,与 DROP_LATEST 比较后非常明显,缓存中永远会储存最新的两个数据,但是较老的数据不管有没有被消费,都可能会从 Buffer 移除,所以 Subscriber 可以消费当前最新的数据,但是有可能漏掉中间的数据,比如图中漏掉了 ❷

注意:当 extraBufferCapacity 设为 SUSPEND 可以保证 Subscriber 一个不漏的消费掉所有数据,但是会影响 Emitter 的速度;当设置为 DROP_XXX 时,可以保证 emit 调用后立即返回,但是 Subscriber 可能会漏掉部分数据。

如果我们不想让 emit 发生挂起,除了设置 DROP_XXX 之外,还有一个方法就是调用 tryEmit,这是一个非 suspend 版本的 emit

abstract suspend override fun emit(value: T)

abstract fun tryEmit(value: T): Boolean

tryEmit 返回一个 boolean 值,你可以这样判断返回值,当使用 emit 会挂起时,使用 tryEmit 会返回 false,其余情况都是 true。这意味着 tryEmit 返回 false 的前提是 extraBufferCapacity 必须设为 SUSPEND,且 Buffer 中空余位置为 0 。此时使用 tryEmit 的效果等同于 DROP_LATEST。

SharedFlow Buffer

前面介绍的 MutableSharedFlow 的三个参数,其本质都是围绕 SharedFlow 的 Buffer 进行工作的。那么这个 Buffer 具体结构是怎样的呢?

上面这个图是 SharedFlow 源码中关于 Buffer 的注释,这个图形象地告诉了我们 Buffer 是一个线性数据结构(就是一个普通的数组 Array<Any?>),但是这个图不能直观反应 Buffer 运行机制。下面通过一个例子,看一下 Buffer 在运行时的具体更新过程:

val sharedFlow = MutableSharedFlow<Int>(
replay = 2,
extraBufferCapacity = 2,
onBufferOverflow = BufferOverflow.SUSPEND
)
var emitValue = 1

fun main() {
runBlocking {
launch {
sharedFlow.onEach {
delay(200) // simulate the consume of data
}.collect()
}

repeat(12) {
sharedFlow.emit(emitValue)
emitValue++
delay(50)
}
}
}

上面的代码很简单,SharedFlow 的 BufferSize = 2+2 = 4,Emitter 生产的速度大于 Subscriber 消费的速度,所以过程中会出现 Buffer 的填充和更新,下面依旧用图的方式展示 Buffer 的变化

先看一下代码对应的时序图:

有前面的介绍,相信这个时序图很容易理解,这里就不再赘述了,下面重点图解一下 Buffer 的内存变化。SharedFlow 的 Buffer 本质上是一个基于 Array 实现的 queue,通过指针移动从往队列增删元素,避免了元素在实际数组中的移动。这里关键的指针有三个:

  • head:队列的 head 指向 Buffer 的第一个有效数据,这是时间上最早进入缓存的数据,在数据被所有的 Subscriber 消费之前不会移除缓存。因此 head 也代表了最慢的 Subscriber 的处理进度
  • replay:Buffer 为 replay-cache 预留空间的其实位置,当有新的 Subscriber 订阅发生时,从此位置开始处理数据。
  • end:新数据进入缓存时的位置,end 这也代表了最快的 Subscriber 的处理进度。

如果 bufferSize 表示当前 Buffer 中存储数据的个数,则我们可知三指针 index 符合如下关系:

  • replay <= head + bufferSize
  • end = head + bufferSize

了解了三指针的含义后,我们再来看上图中的 Buffer 是如何工作的:

最后,总结一下 Buffer 的特点:

  • 基于数组实现,当数组空间不够时进行 2n 的扩容
  • 元素进入数组后的位置保持不变,通过移动指针,决定数据的消费起点
  • 指针移动到数组尾部后,会重新指向头部,数组空间可循环使用


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

Android完美处理输入框被挡问题

前言 前段时间出现了webview的输入框被软键盘挡住的问题,处理之后顺便对一些列的输入框被挡住的情况进行一个总结。 正常情况下的输入框被挡 正常情况下,输入框被输入法挡住,一般给window设softInputMode就能解决。 window.getAttr...
继续阅读 »

前言


前段时间出现了webview的输入框被软键盘挡住的问题,处理之后顺便对一些列的输入框被挡住的情况进行一个总结。


正常情况下的输入框被挡


正常情况下,输入框被输入法挡住,一般给window设softInputMode就能解决。

window.getAttributes().softInputMode = WindowManager.LayoutParams.XXX


有3种情况:

(1)SOFT_INPUT_ADJUST_RESIZE: 布局会被软键盘顶上去
(2)SOFT_INPUT_ADJUST_PAN:只会把输入框给顶上去(就是只顶一部分距离)
(3)SOFT_INPUT_ADJUST_NOTHING:不做任何操作(就是不顶)


SOFT_INPUT_ADJUST_PAN和SOFT_INPUT_ADJUST_RESIZE的不同在于SOFT_INPUT_ADJUST_PAN只是把输入框,而SOFT_INPUT_ADJUST_RESIZE会把整个布局顶上去,这就会有种布局高度在输入框展示和隐藏时高度动态变化的视觉效果。


如果你是出现了输入框被挡的情况,一般设置SOFT_INPUT_ADJUST_PAN就能解决。如果你是输入框没被挡,但是软键盘弹出的时候会把布局往上顶,如果你不希望往上顶,可以设置SOFT_INPUT_ADJUST_NOTHING。


softInputMode是window的属性,你给在Mainifest给Activity设置,也是设给window,你如果是Dialog或者popupwindow这种,就直接getWindow()来设置就行。正常情况下设置这个属性就能解决问题。


Webview的输入框被挡


但是Webview的输入框被挡的情况下,设这个属性有可能会失效。


Webview的情况下,SOFT_INPUT_ADJUST_PAN会没效果,然后,如果是Webview并且你还开沉浸模式的情况的话,SOFT_INPUT_ADJUST_RESIZE和SOFT_INPUT_ADJUST_PAN都会不起作用。

我去查看资料,发现这就是经典的issue 5497, 网上很多的解决方案就是通过AndroidBug5497Workaround,这个方案很容易能查到,我就不贴出来了,原理就是监听View树的变化,然后再计算高度,再去动态设置。这个方案的确能解决问题,但是我觉得这个操作不可控的因素比较多,说白了就是会不会某种机型或者情况下使用会出现其它的BUG,导致你需要写一些判断逻辑来处理特殊的情况。


解法就是不用沉浸模式然后使用SOFT_INPUT_ADJUST_RESIZE就能解决。但是有时候这个window显示的时候就需要沉浸模式,特别是一些适配刘海屏、水滴屏这些场景。


setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)

那我的第一反应就是改变布局


window. setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);

这样是能正常把弹框顶上去,但是控件内部用的也是WRAP_CONTENT导致SOFT_INPUT_ADJUST_RESIZE改变布局之后就恢复不了原样,也就是会变形。而不用WRAP_CONTENT用固定高度的话,SOFT_INPUT_ADJUST_RESIZE也是失效的。


没事,还要办法,在MATCH_PARENT的情况下我们去设置fitSystemWindows为true,但是这个属性会让出一个顶部的安全距离,效果就是向下偏移了一个状态栏的高度。


这种情况下你可以去设置margin来解决这个顶部偏移的问题。


params.topMargin = statusHeight == 0 ? -120 : -statusHeight;
view.setLayoutParams(params);

这样的操作是能解除顶部偏移的问题,但是布局有可能被纵向压缩,这个我没完全测试过,我觉得如果你布局高度是固定的,可能不会受到影响,但我的webview是自适应的,webview里面的内容也是自适应的,所以我这出现了布局纵向压缩的情况。举个例子,你的view的高度是800,状态栏高度是100,那设fitSystemWindows之后的效果就是view显示700,paddingTop 100,这样的效果,设置params.topMargin =-100,之后,view显示700,paddingTop 100。大概是这个意思:能从视觉上消除顶部偏移,但是布局纵向被压缩的问题没得到处理


所以最终的解决方法是改WindowInsets的Rect (这个我等下会再解释是什么意思)


具体的操作就是在你的自定义view中加入下面两个方法


@Override
public void setFitsSystemWindows(boolean fitSystemWindows) {
fitSystemWindows = true;
super.setFitsSystemWindows(fitSystemWindows);
}


@Override
protected boolean fitSystemWindows(Rect insets) {
Log.v("mmp", "测试顶部偏移量: "+insets.top);
insets.top = 0;
return super.fitSystemWindows(insets);
}

小结


解决WebView+沉浸模式下输入框被软键盘挡住的步骤:



  1. window.getAttributes().softInputMode设置成SOFT_INPUT_ADJUST_RESIZE

  2. 设置view的fitSystemWindows为true,我这里是webview里面的输入框被挡住,设的就是webview而不是父View

  3. 重写fitSystemWindows方法,把insets的top设为0


WindowInsets


根据上面的3步操作,你就能处理webview输入框被挡的问题,但是如果你想知道为什么,这是什么原理。你就需要去了解WindowInsets。我们的沉浸模式的操作setSystemUiVisibility和设置fitSystemWindows属性,还有重写fitSystemWindows方法,都和WindowInsets有关。


WindowInsets是应用于窗口的系统视图的插入。例如状态栏STATUS_BAR和导航栏NAVIGATION_BAR。它会被view引用,所以我们要做具体的操作,是对view进行操作。


还有一个比较重要的问题,WindowInsets的不同版本都是有一定的差别,Android28、Android29、Android30都有一定的差别,例如29中有个android.graphics.Insets类,这是28里面没有的,我们可以在29中拿到它然后查看top、left等4个属性,但是只能查看,它是final的,不能直接拿出来修改。


但是WindowInsets这块其实能讲的内容比较多,以后可以拿出来单独做一篇文章,这里就简单介绍下,你只需要指定我们解决上面那些问题的原理,就是这个东西。


源码解析


大概对WindowInsets有个了解之后,我再带大家简单过一遍setFitsSystemWindows的源码,相信大家会印象更深。


public void setFitsSystemWindows(boolean fitSystemWindows) {
setFlags(fitSystemWindows ? FITS_SYSTEM_WINDOWS : 0, FITS_SYSTEM_WINDOWS);
}

它这里只是设置一个flag而已,如果你看它的注释(我这里就不帖出来了),他会把你引导到protected boolean fitSystemWindows(Rect insets)这个方法(我之后会说为什么会到这个方法)


@Deprecated
protected boolean fitSystemWindows(Rect insets) {
if ((mPrivateFlags3 & PFLAG3_APPLYING_INSETS) == 0) {
if (insets == null) {
// Null insets by definition have already been consumed.
// This call cannot apply insets since there are none to apply,
// so return false.
return false;
}
// If we're not in the process of dispatching the newer apply insets call,
// that means we're not in the compatibility path. Dispatch into the newer
// apply insets path and take things from there.
try {
mPrivateFlags3 |= PFLAG3_FITTING_SYSTEM_WINDOWS;
return dispatchApplyWindowInsets(new WindowInsets(insets)).isConsumed();
} finally {
mPrivateFlags3 &= ~PFLAG3_FITTING_SYSTEM_WINDOWS;
}
} else {
// We're being called from the newer apply insets path.
// Perform the standard fallback behavior.
return fitSystemWindowsInt(insets);
}
}

(mPrivateFlags3 & PFLAG3_APPLYING_INSETS) == 0 这个判断后面会简单讲,你只需要知道正常情况是执行fitSystemWindowsInt(insets)


而fitSystemWindows又是哪里调用的?往前跳,能看到是onApplyWindowInsets调用的,而onApplyWindowInsets又是由dispatchApplyWindowInsets调用的。其实到这里已经没必要往前找了,能看出这个就是个分发机制,没错,这里就是WindowInsets的分发机制,和View的事件分发机制类似,再往前找就是viewgroup调用的。前面说了WindowInsets在这里不会详细说,所以WindowInsets分发机制这里也不会去展开,你只需要先知道有那么一回事就行。


public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) {
try {
mPrivateFlags3 |= PFLAG3_APPLYING_INSETS;
if (mListenerInfo != null && mListenerInfo.mOnApplyWindowInsetsListener != null) {
return mListenerInfo.mOnApplyWindowInsetsListener.onApplyWindowInsets(this, insets);
} else {
return onApplyWindowInsets(insets);
}
} finally {
mPrivateFlags3 &= ~PFLAG3_APPLYING_INSETS;
}
}

假设mPrivateFlags3是0,PFLAG3_APPLYING_INSETS是20,0和20做或运算,就是20。然后判断是否有mOnApplyWindowInsetsListener,这个Listener就是我们有没有在外面做


setOnApplyWindowInsetsListener(new OnApplyWindowInsetsListener() {
@Override
public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
......
return insets;
}
});

假设没有,调用onApplyWindowInsets


public WindowInsets onApplyWindowInsets(WindowInsets insets) {
if ((mPrivateFlags3 & PFLAG3_FITTING_SYSTEM_WINDOWS) == 0) {
// We weren't called from within a direct call to fitSystemWindows,
// call into it as a fallback in case we're in a class that overrides it
// and has logic to perform.
if (fitSystemWindows(insets.getSystemWindowInsetsAsRect())) {
return insets.consumeSystemWindowInsets();
}
} else {
// We were called from within a direct call to fitSystemWindows.
if (fitSystemWindowsInt(insets.getSystemWindowInsetsAsRect())) {
return insets.consumeSystemWindowInsets();
}
}
return insets;
}

mPrivateFlags3 & PFLAG3_FITTING_SYSTEM_WINDOWS就是20和40做与运算,那就是0,所以调用fitSystemWindows。


而fitSystemWindows的(mPrivateFlags3 & PFLAG3_APPLYING_INSETS) == 0)就是20和20做与运算,不为0,所以调用fitSystemWindowsInt。


分析到这里,就需要结合我们上面解决BUG的思路了,我们其实是要拿到Rect insets这个参数,并且修改它的top。


setOnApplyWindowInsetsListener(new OnApplyWindowInsetsListener() {
@Override
public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
......
return insets;
}
});

setOnApplyWindowInsetsListener回调中的insets可以拿到android.graphics.Insets这个类,但是你只能看到top是多少,没办法修改。当然你可以看到top是多少,然后按我上面的做法Margin设置一下


params.topMargin = -top;


如果你的布局不发生纵向变形,那倒没有多大关系,如果有变形,那就不能用这个做法。从源码看,这个过程主要涉及3个方法。我们能看出最好下手的地方就是fitSystemWindows。因为onApplyWindowInsets和dispatchApplyWindowInsets是分发机制的方法,你要在这里下手的话可能会出现流程混乱等问题。


所以我们这样做来解决fitSystemWindows = true出现的顶部偏移。


@Override
public void setFitsSystemWindows(boolean fitSystemWindows) {
fitSystemWindows = true;
super.setFitsSystemWindows(fitSystemWindows);
}


@Override
protected boolean fitSystemWindows(Rect insets) {
Log.v("mmp", "测试顶部偏移量: "+insets.top);
insets.top = 0;
return super.fitSystemWindows(insets);
}

扩展


上面已经解决问题了,这里是为了扩展一下思路。

fitSystemWindows方法是protected,导致你能重写它,但是如果这个过程我们没办法用继承来实现呢?


其实这就是一个解决问题的思路,我们要知道为什么会出现这种情况,原理是什么,比如这里我们知道这个fitSystemWindows导致的顶部偏移是insets的top导致的。你得先知道这一点,不然你不知道怎么去解决这个问题,你只能去网上找别人的方法一个一个试。那我怎么知道是insets的top导致的呢?这就需要有一定的源码阅读能力,还要知道这个东西设计的思想是怎样的。当你知道有这么一个东西之后,再想办法去拿到它然后改变数据。


这里我我们是利用继承protected方法这个特性去获取到insets,那如果这个过程没办法通过继承实现怎么办?比如这里是因为fitSystemWindows是view的方法,而我们自定义view正好继承view。如果它是内部自己写的一个类去实现这个操作呢?


这种情况下一般两种操作比较万金油:



  1. 你写一个类去继承它那个类,然后在你写的类里面去改insets,然后通过反射的方式把它注入给View

  2. 动态代理


我其实一开始改这个的想法就是用动态代理,所以马上把代码撸出来。


public class WebViewProxy implements InvocationHandler {

private Object relObj;

public Object newProxyInstance(Object object){
this.relObj = object;
return Proxy.newProxyInstance(relObj.getClass().getClassLoader(), relObj.getClass().getInterfaces(), this);
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if ("fitSystemWindows".equals(method.getName()) && args != null && args.length == 1){
Log.v("mmp", "测试代理效果 "+args);
}
}catch (Exception e){
e.printStackTrace();
}
return proxy;
}

}

WebViewProxy proxy = new WebViewProxy();
View viewproxy = (View) proxy.newProxyInstance(mWebView);

然后才发现fitSystemWindows不是接口方法,白忙活一场,但是如果fitSystemWindows是接口方法的话,我这里就可以用通过动态代理加反射的操作去修改这个insets,虽然用不上,但也是个思路。最后发现可以直接重写这个方法就行,我反倒还把问题想复杂了。


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

Android 搜索框架使用

App中搜索功能是必不可少的,搜索功能可以帮助用户快速获取想要的信息。对此,Android提供了一个搜索框架,本文介绍如何通过搜索框架实现搜索功能。 搜索框架简介 Android 搜索框架提供了搜索弹窗和搜索控件两种使用方式。 搜索弹窗:系统控制的弹窗,激...
继续阅读 »

App中搜索功能是必不可少的,搜索功能可以帮助用户快速获取想要的信息。对此,Android提供了一个搜索框架,本文介绍如何通过搜索框架实现搜索功能。


搜索框架简介


Android 搜索框架提供了搜索弹窗和搜索控件两种使用方式。




  • 搜索弹窗:系统控制的弹窗,激活后显示在页面顶部,输入的内容提交后会通过Intent传递到指定的搜索Activity中处理,可以添加搜索建议。




  • 搜索控件(SearchView):系统实现的搜索控件,可以放在任意位置(可以与Toolbar结合使用),默认情况下与EditText类似,需要自己添加监听处理用户输入的数据,通过配置可以达到与搜索弹窗一致的行为。




使用搜索框架实现搜索功能


可搜索配置


在res/xml目录下创建searchable.xml(必须用此命名),如下:


<?xml version="1.0" encoding="utf-8"?>
<searchable xmlns:android="http://schemas.android.com/apk/res/android"
android:hint="@string/search_hint"
android:label="@string/app_name" />

android:label是此配置文件必须配置的属性,通常配置为App的名字,android:hint配置用户未输入内容时的提示文案,官方建议格式为“搜索${content or product}”


更多可搜索配置包含的语法和用法可以看官方文档


搜索页面


配置一个单独的Activity用于显示搜索内容,用户可能会在搜索完一个内容后立刻搜索下一个内容,所以建议把搜索页面设置为SingleTop,避免重复创建搜索页面。


AndroidManifest中配置搜索页面,如下:


<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<application
...
>

<activity
android:name=".search.SearchActivity"
android:exported="false"
android:launchMode="singleTop">

<meta-data
android:name="android.app.searchable"
android:resource="@xml/searchable" />

<intent-filter>
<action android:name="android.intent.action.SEARCH" />
</intent-filter>
</activity>
</application>
</manifest>

Activity中处理搜索数据,代码如下:


class SearchActivity : AppCompatActivity() {

private lateinit var binding: LayoutSearchActivityBinding

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.layout_search_activity)
// 当搜索页面第一次打开时,获取搜索内容
getQueryKey(intent)
}

override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
// 更新Intent数据
setIntent(intent)
// 当搜索页面多次打开,并仍在栈顶时,获取搜索内容
getQueryKey(intent)
}

private fun getQueryKey(intent: Intent?) {
intent?.run {
if (Intent.ACTION_SEARCH == action) {
// 用户输入的内容
val queryKey = getStringExtra(SearchManager.QUERY) ?: ""
if (queryKey.isNotEmpty()) {
doSearch(queryKey)
}
}
}
}

private fun doSearch(queryKey: String) {
// 根据用户输入内容执行搜索操作
}
}

使用SearchView


SearchView可以放在页面的任意位置,本文与Toolbar结合使用,如何在Toolbar中创建菜单项在上一篇文章中介绍过,此处省略。要使SearchView与搜索弹窗保持一致的行为需要在代码中进行配置,如下:


class SearchActivity : AppCompatActivity() {

private lateinit var binding: LayoutSearchActivityBinding

override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.example_seach_menu, menu)
menu?.run {
val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager
val searchView = findItem(R.id.action_search).actionView as SearchView
//设置搜索配置
searchView.setSearchableInfo(searchManager.getSearchableInfo(componentName))
}
return true
}

...
}

使用搜索弹窗


Activity中使用搜索弹窗,如果Activity已经配置为搜索页面则无需额外配置,否则需要在AndroidManifest中添加配置,如下:


<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<application
...
>

<activity
android:name=".search.SearchExampleActivity">

<!--为当前页面指定搜索页面-->
<!--如果所有页面都使用搜索弹窗,则将此meta-data移到applicaion标签下-->
<meta-data
android:name="android.app.default_searchable"
android:value=".search.SearchActivity" />
</activity>
</application>
</manifest>

Activity中通过onSearchRequested方法来调用搜索弹窗,如下:


class SearchExampleActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding: LayoutSearchExampleActivityBinding = DataBindingUtil.setContentView(this, R.layout.layout_search_example_activity)
binding.btnSearchDialog.setOnClickListener { onSearchRequested() }
}
}

搜索弹窗对Activity生命周期的影响


搜索弹窗的显示隐藏,不会像其他弹窗一样触发ActivityonPauseonResume方法。如果在搜索弹窗显示隐藏的同时需要对其他功能进行处理,可以通过onSearchRequestedOnDismissListener来实现,代码如下:


class SearchExampleActivity : AppCompatActivity() {

override fun onSearchRequested(): Boolean {
// 搜索弹窗显示,可以在此处理其他功能
return super.onSearchRequested()
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding: LayoutSearchExampleActivityBinding = DataBindingUtil.setContentView(this, R.layout.layout_search_example_activity)
binding.btnSearchDialog.setOnClickListener { onSearchRequested() }
val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager
searchManager.setOnDismissListener {
// 搜索弹窗隐藏,可以在此处理其他功能
}
}
}

附加额外的参数


使用搜索弹窗时,如果需要附加额外的参数用于优化搜索查询的过程,例如用户的性别、年龄等,可以通过如下代码实现:


// 配置额外参数
class SearchExampleActivity : AppCompatActivity() {

override fun onSearchRequested(): Boolean {
val appData = Bundle()
appData.putString("gender", "male")
appData.putInt("age", 24)
startSearch(null, false, appData, false)
// 返回true表示已经发起了查询
return true
}

...
}

// 在搜素页面中获取额外参数
class SearchActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
intent?.run {
if (Intent.ACTION_SEARCH == action) {
// 用户输入的内容
val queryKey = getStringExtra(SearchManager.QUERY) ?: ""
// 额外参数
val appData = getBundleExtra(SearchManager.APP_DATA)
appData?.run {
val gender = getString("gender") ?: ""
val age = getInt("age")
}
}
}
}
}

语音搜索


语音搜索让用户无需输入内容就可进行搜索,要开启语音搜索,需要在searchable.xml增加配置,如下:


<?xml version="1.0" encoding="utf-8"?>
<searchable xmlns:android="http://schemas.android.com/apk/res/android"
android:hint="@string/search_hint"
android:label="@string/app_name"
android:voiceSearchMode="showVoiceSearchButton|launchRecognizer" />

语音搜索必须配置showVoiceSearchButton用于显示语音搜索按钮,配置launchRecognizer指定语音搜索按钮启动一个语音识别程序用于识别语音转录为文本并发送至搜索页面。


更多语音搜索配置包含的语法和用法可以看官方文档


注意,语音识别后的文本会直接发送至搜索页面,无法更改,需要进行完备的测试确保语音搜索功能适合你的App。


搜索记录


用户执行过搜索后,可以将搜索的内容保存下来,下次要搜索相同的内容时,输入部分文字后就会显示匹配的搜索记录。


要实现此功能,需要完成下列步骤:


创建SearchRecentSuggestionsProvider


自定义RecentSearchProvider继承SearchRecentSuggestionsProvider,代码如下:


class RecentSearchProvider : SearchRecentSuggestionsProvider() {

companion object {
// 授权方的名称(建议设置为文件提供者的完整名称)
const val AUTHORITY = "com.chenyihong.exampledemo.search.RecentSearchProvider"
// 数据库模式
// 必须配置 DATABASE_MODE_QUERIES
// 可选配置 DATABASE_MODE_2LINES,为搜索记录提供第二行文本,可用于作为详情补充
const val MODE: Int = DATABASE_MODE_QUERIES or DATABASE_MODE_2LINES
}

init {
// 设置搜索授权方的名称与数据库模式
setupSuggestions(AUTHORITY, MODE)
}
}

AndroidManifest中配置Provider,如下:


<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<application
...
>

<!--android:authorities的值与RecentSearchProvider中的AUTHORITY一致-->
<provider
android:name=".search.RecentSearchProvider"
android:authorities="com.chenyihong.exampledemo.search.RecentSearchProvider"
android:exported="false" />
</application>
</manifest>

修改可搜索配置


searchable.xml增加配置,如下:


<?xml version="1.0" encoding="utf-8"?>
<searchable xmlns:android="http://schemas.android.com/apk/res/android"
android:hint="@string/search_hint"
android:label="@string/app_name"
android:voiceSearchMode="showVoiceSearchButton|launchRecognizer"
android:searchSuggestAuthority="com.chenyihong.exampledemo.search.RecentSearchProvider"
android:searchSuggestSelection=" ?"/>

android:searchSuggestAuthority 的值与RecentSearchProvider中的AUTHORITY保持一致。android:searchSuggestSelection的值必须为" ?",该值为数据库选择参数的占位符,自动由用户输入的内容替换。


在搜索页面中保存查询


获取到用户输入的数据时保存,代码如下:


class SearchActivity : BaseGestureDetectorActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
intent?.run {
if (Intent.ACTION_SEARCH == action) {
val queryKey = getStringExtra(SearchManager.QUERY) ?: ""
if (queryKey.isNotEmpty()) {
// 第一个参数为用户输入的内容
// 第二个参数为第二行文本,可为null,仅当RecentSearchProvider.MODE为DATABASE_MODE_QUERIES or DATABASE_MODE_2LINES时有效。
SearchRecentSuggestions(this@SearchActivity, RecentSearchProvider.AUTHORITY, RecentSearchProvider.MODE)
.saveRecentQuery(queryKey, "history $queryKey")
}
}
}
}
}

清除搜索历史


为了保护用户的隐私,官方的建议是App必须提供清除搜索记录的功能。请求搜索记录可以通过如下代码实现:


class SearchActivity : BaseGestureDetectorActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
SearchRecentSuggestions(this, RecentSearchProvider.AUTHORITY, RecentSearchProvider.MODE)
.clearHistory()
}
}

示例


整合之后做了个示例Demo,代码如下:


// 可搜索配置
<?xml version="1.0" encoding="utf-8"?>
<searchable xmlns:android="http://schemas.android.com/apk/res/android"
android:hint="@string/search_hint"
android:label="@string/app_name"
android:searchSuggestAuthority="com.chenyihong.exampledemo.search.RecentSearchProvider"
android:searchSuggestSelection=" ?"
android:voiceSearchMode="showVoiceSearchButton|launchRecognizer" />

// 清单文件
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<application
...
>

<activity
android:name=".search.SearchExampleActivity"
android:screenOrientation="portrait">

<!--为当前页面指定搜索页面-->
<meta-data
android:name="android.app.default_searchable"
android:value=".search.SearchActivity" />
</activity>

<activity
android:name=".search.SearchActivity"
android:exported="false"
android:launchMode="singleTop"
android:parentActivityName="com.chenyihong.exampledemo.search.SearchExampleActivity"
android:screenOrientation="portrait">

<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="com.chenyihong.exampledemo.search.SearchExampleActivity" />

<meta-data
android:name="android.app.searchable"
android:resource="@xml/searchable" />

<intent-filter>
<action android:name="android.intent.action.SEARCH" />
</intent-filter>
</activity>

<provider
android:name=".search.RecentSearchProvider"
android:authorities="com.chenyihong.exampledemo.search.RecentSearchProvider"
android:exported="false" />
</application>
</manifest>

// 示例Activity
class SearchExampleActivity : BaseGestureDetectorActivity() {

override fun onSearchRequested(): Boolean {
val appData = Bundle()
appData.putString("gender", "male")
appData.putInt("age", 24)
startSearch(null, false, appData, false)
return true
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding: LayoutSearchExampleActivityBinding = DataBindingUtil.setContentView(this, R.layout.layout_search_example_activity)
binding.btnSearchView.setOnClickListener { startActivity(Intent(this, SearchActivity::class.java)) }
binding.btnSearchDialog.setOnClickListener { onSearchRequested() }
val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager
searchManager.setOnDismissListener {
runOnUiThread { Toast.makeText(this, "Search Dialog dismiss", Toast.LENGTH_SHORT).show() }
}
}
}

class SearchActivity : BaseGestureDetectorActivity() {

private lateinit var binding: LayoutSearchActivityBinding

private val textDataAdapter = TextDataAdapter()

private val originData = ArrayList<String>()

private var lastQueryValue = ""

override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.example_seach_menu, menu)
menu?.run {
val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager
val searchView = findItem(R.id.action_search).actionView as SearchView
searchView.setOnCloseListener {
textDataAdapter.setNewData(originData)
false
}
searchView.setSearchableInfo(searchManager.getSearchableInfo(componentName))
if (lastQueryValue.isNotEmpty()) {
searchView.setQuery(lastQueryValue, false)
}
}
return true
}

override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == R.id.action_clear_search_histor) {
SearchRecentSuggestions(this, RecentSearchProvider.AUTHORITY, RecentSearchProvider.MODE)
.clearHistory()
}
return super.onOptionsItemSelected(item)
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.layout_search_activity)
setSupportActionBar(binding.toolbar)
supportActionBar?.run {
title = "SearchExample"
setHomeAsUpIndicator(R.drawable.icon_back)
setDisplayHomeAsUpEnabled(true)
}
binding.rvContent.adapter = textDataAdapter
originData.add("test data qwertyuiop")
originData.add("test data asdfghjkl")
originData.add("test data zxcvbnm")
originData.add("test data 123456789")
originData.add("test data /.,?-+")
textDataAdapter.setNewData(originData)
// 获取搜索内容
getQueryKey(intent, false)
}

override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
// 更新Intent数据
setIntent(intent)
// 获取搜索内容
getQueryKey(intent, true)
}

private fun getQueryKey(intent: Intent?, newIntent: Boolean) {
intent?.run {
if (Intent.ACTION_SEARCH == action) {
val queryKey = getStringExtra(SearchManager.QUERY) ?: ""
if (queryKey.isNotEmpty()) {
SearchRecentSuggestions(this@SearchActivity, RecentSearchProvider.AUTHORITY, RecentSearchProvider.MODE)
.saveRecentQuery(queryKey, "history $queryKey")
if (!newIntent) {
lastQueryValue = queryKey
}
val appData = getBundleExtra(SearchManager.APP_DATA)
doSearch(queryKey, appData)
}
}
}
}

private fun doSearch(queryKey: String, appData: Bundle?) {
appData?.run {
val gender = getString("gender") ?: ""
val age = getInt("age")
}
val filterData = originData.filter { it.contains(queryKey) } as ArrayList<String>
textDataAdapter.setNewData(filterData)
}
}

ExampleDemo github


ExampleDemo gitee


效果如图:


device-2022-10-07-18 -original-original.gif

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

死磕操作系统!谷歌重磅发布开源KataOS,网友:「谷歌坟场」喜+1

谷歌又发布新系统了!等等,我为什么要说「又」?出走半生,谷歌的操作系统之心始终不死。对于全新推出的KataOS,谷歌计划让它成为一个「可证明的安全平台」,并针对运行机器学习应用的嵌入式设备进行充分的优化。有趣的是,文章发布之后,虽然陆续有了不少报道,但并没有激...
继续阅读 »

谷歌又发布新系统了!

等等,我为什么要说「又」?


出走半生,谷歌的操作系统之心始终不死。对于全新推出的KataOS,谷歌计划让它成为一个「可证明的安全平台」,并针对运行机器学习应用的嵌入式设备进行充分的优化。


有趣的是,文章发布之后,虽然陆续有了不少报道,但并没有激起什么水花。

没想到,就在这两天,竟然同时登上了知乎和Reddit的热榜。


不过,网友们的观点都出奇的一致——早晚得黄……

KataOS:用Rust写的「安全操作系统」

在博客中,谷歌解释了开发这个系统的理由。

当我们被越来越多收集和处理环境信息的智能设备所包围时,我们比任何时候都更需要一个简单的解决方案,来为嵌入式硬件构建可验证的安全系统。

如果我们的设备不能证明自己能保证数据的安全,那么它们收集的个人身份识别数据——如人的图像和声音的记录——就可能被恶意软件获取。

不幸的是,系统安全通常被视为添加到现有系统,或通过额外的ASIC硬件解决的软件功能——这远远不够。

针对这个问题,谷歌希望建立一个可证明的安全平台,为运行ML应用程序的嵌入式设备进行优化。

现在,谷歌已经在GitHub上开放了KataOS的几个组件,并且已经与Antmicro合作开发了Renode模拟器和相关框架。


这个新操作系统以seL4作为微内核。谷歌给出的理由是:「因为它把安全放在第一位;它在数学上被证明是安全的,具有保证保密性、完整性和可用性。」

为什么KataOS的安全性这么高呢?

谷歌解释说,因为从逻辑上讲,应用程序不可能破坏内核的硬件安全保护,并且系统组件是可验证安全的。

同时,KataOS也几乎完全由Rust实现,这更是加了一层buff,因为Rust消除了整类错误,比如逐一错误和缓冲区溢出。


目前的GitHub版本,已经涵盖了大部分KataOS的核心部分,包括用于Rust的框架(如sel4-sys crate,用于让seL4系统调用API),一个用Rust编写的备用根服务器(用于全系统的动态内存管理),以及对seL4的内核修改,用于回收根服务器使用的内存。

在内部,KataOS也能够动态地加载和运行CAmkES框架之外的第三方应用程序。

目前,Github上的代码不包括运行这些应用程序所需的组件,这些功能可能会在不久后发布。

同时,谷歌还为KataOS建立一个名为Sparrow的参考实现,它让KataOS与安全的硬件平台结合起来。

除了逻辑安全的操作系统内核外,Sparrow还包括一个在RISC-V架构上用OpenTitan构建的逻辑安全的信任根。对于最初的版本,谷歌的目标是建立一个用QEMU模拟运行的更标准的64位ARM平台。

谷歌希望在以后将Sparrow的全部内容开源,包括所有的硬件和软件设计。

而现在,谷歌发出号召,希望大家能共建「智能环境ML系统值得信赖的未来。」

KataOS的横空出世,又会掀起怎样的波澜?


国外网友:坐等被弃

对此,Reddit网友表示:Abandon是早晚的事儿!


另有扎心回复:「不懂就问,是已经宣布关闭日期了吗?」讽刺值瞬间拉满。


可以说,抛弃现有项目,转而支持那些还没成熟的半成品新项目,是谷歌20多年来的「传统艺能」了。

他们会支付数十亿美元,招揽全球的顶尖人才,花费数年打磨一个项目,制造出昂贵的东西,然后再丢掉。


于是,在外界看来,谷歌的方向完全可以用俩字来形容——「混乱」。

对于游戏领域,他们是三心二意,在大量的项目中手忙脚乱。前脚大举进军,后脚就狠心抛弃。

在硬件方面,前几年收购Fitibt之后,直到现在都没有把它很好地集成到Google Fit里。


对于谷歌一言不合就砍项目的操作,有网友调侃道:

「我们决定关掉『Google Existential」。我们仍然会坚信这个概念,但我们觉得它从未达到期望的高度。」

「那个服务是做什么的?」

「我们还没决定呢。」


至于这次推出的KataOS,知乎答主「星辰」表示:


知乎答主「亚东」也表示,谷歌做出来操作系统还能保它不挂,主要就是太有钱了。无数的古早系统,都死在了沙滩上。


取代安卓没下文,任职10年高管走人

说到谷歌的操作系统,除了大名鼎鼎的「Android」之外,还有一个相当神秘的「Fuchsia OS」。

而Fuchasia OS的命运,或许可以给KataOS做个参考。

要知道,曾经一度,Fuchasia OS可是被宣传为能取代Android的操作系统。


2016年8月,GitHub上的一组神秘源码,指向了谷歌正在开发的一个名为「Fuchsia OS」的全新操作系统。

代码显示,Fuchsia OS能够跨平台运行,包括「汽车的娱乐媒体系统和嵌入式设备,如手表、手机、平板以及电脑等等」。

2018年1月,谷歌允许开发者以Google Pixelbook为目标设备,下载Fuchsia OS进行开发与测试。

2019年6月,Fuchsia OS的开发者网站Fuchsia.dev上线。

2020年12月,首度在Google Open Source 博亮相,吁开发者来做贡献。

2021年5月,谷歌员工证实,Fuchsia OS首次实现了消费市场的部署。在对预览版设备进行第一波更新后,Fuchsia OS于2021年8月被推送至所有Nest Hub设备。

来源:雲爸

最初大家还在猜测,谷歌开发Fuchsia OS的目的是希望以单一平台统一移动操作系统生态系。

然而,谷歌至今都未曾说明Fuchsia OS的产品定位。

除了应用在了新款的Nest Hub上之外,并未像先前说明的那样,应用在手机、平板、电脑,甚至众多物联网设备上。

时间来到2022年3月,Fuchsia OS团队的负责人Chris McKillop,宣布自己已经离开任职10年的谷歌,加入到了微软Xbox团队。


不过比较起来,Fuchsia和KataOS还是有区别的。

KataOS/Sparrow似乎在一开始就明确了自己的计划——低功耗嵌入式设备。

从Github项目里可以看到,Sparrow最初的目标总内存为4MiB。

谷歌坟场:那些年被「杀死」的项目们

那么,为啥网友们清一色的表示谷歌早晚要「Abandon」呢?

看看那些被腰斩的项目就知道了。

据统计,这个数量至今已经达到了275个。2023年还没到,就已经预定了4个。

在这片触目惊心的「谷歌坟场」,你可以按年份搜索它「死」去的项目——2022年,23个;2021年,31个;2020年,25个……

项目地址:https://killedbygoogle.com/

这不,就在上个月,谷歌便官宣了云游戏服务平台Stadia正式下线的消息。

时间回到3年前,谷歌在推出Stadia时声称,只要一台普通电脑,装个Chrome,就能畅玩游戏大作。

然而,这几年以来,用户反馈并不好,甚至可以用糟糕来形容。


用户不买账,游戏阵容迟迟起不来,这业务又挺烧钱的,那就砍了吧。

不过,今年早些时候,谷歌还专门针对Stadia要黄的传言发推特澄清过:「Stadia没有关闭。请放心,我们一直在努力为平台和Stadia Pro带来更多优秀的游戏」。


结果过了两个月就官宣了Stadia下线的消息......


目前来看,反正谷歌财大气粗闲钱多,所以试一试KataOS和Rust也不是什么大事。

大不了,进展不顺利了再砍掉,就像以前无数被拍死在沙滩上的谷歌项目一样。

参考资料:

https://opensource.googleblog.com/2022/10/announcing-kataos-and-sparrow.html

https://www.reddit.com/r/programming/comments/y7noit/google_announces_a_new_os_written_in_rust/

https://www.zhihu.com/question/560937437

来源:好困 Aeneas | 新智元

收起阅读 »

前端的焦虑,你想过30岁以后的前端路怎么走吗?

曾几何时,我总会很庆幸自己进了前端这个行业。因为在这个职业范畴里面,我如鱼得水,成长很快,成就感满满。然而,随着年龄和工龄的增长,渐渐发现自己的瓶颈越来越明显了,我感觉自己似乎碰到了前端的天花板。原因何在1.从客观原因来看,前端相对于后端的入门门槛确实低了不少...
继续阅读 »

曾几何时,我总会很庆幸自己进了前端这个行业。因为在这个职业范畴里面,我如鱼得水,成长很快,成就感满满。然而,随着年龄和工龄的增长,渐渐发现自己的瓶颈越来越明显了,我感觉自己似乎碰到了前端的天花板。

原因何在

1.从客观原因来看,前端相对于后端的入门门槛确实低了不少。公司对前端的需求量虽然很旺盛,但是对前端的技术能力要求却不是很高,特别是一些小公司或者不是技术驱动的公司。这给人一种错觉,好像只需要懂一些js,会一般的html+css就能完成前端的工作。也由于这种原因,前端总是处于技术鄙视链的最底层。
2.从主观原因来说,前端平时基本都是和页面和看得到的UI打交道居多,对于后端的服务,数据存储,运维,部署等等懂得的不多,也导致了领导我们的往往都是后端。在大多数的情况下,你基本很难看到前端去统筹大局,统领前后端。
3.从个人原因来总结,前端经验上去了,工作年限上去了,但是职级却没有上去。归根结底,主要是因为自己的后端知识薄弱,前端深度不够。还有前端管理的职位僧多粥少导致的。

居于上述的原因,前端的天花板来得比别的技术栈更早。这也是导致我们焦虑的主要原因。既然有原因,那就可以找相应的解决方法。

解决方法

1.对症下药,哪里缺乏补哪里。前端的进阶,总离不开对后端的认知。我们不能把自己限死在前端这个范畴里面。业务驱动技术,而不是技术引导业务。不懂数据库,补数据库。不懂服务端,补服务端。幸好现在有nodeJs这个利器。 我们完全可以借用nodejs,去切入后端的世界,了解和学习后端的知识。做到不受语言的限制,学习应用,也就能突破自己的瓶颈。 除了node,php也是一个不错的选择。
2.主动创造条件。很多时候,选择比努力更重要。如果你发现你在一个地方再怎么努力也改变不了现状,这个时候你就应该出去别的地方看看,或者想想怎样改变现状。如果你无法升管理,那你可以尝试去别的地方当管理;如果你总是厌倦天天的无止境的切图和coding,但是又有很多想法,转岗去尝试当产品也是一个选择。
3.大前端和全栈是以后前端的一个趋势,懂后端的前端,懂各端的前端更加具有竞争力,以后可以往这个方向靠拢。

现在脑补一下前端知识体系的脑图。


注:脑图来自 ouvens/frontend-system-map

接下来再总结一下前端以后的路怎么走。

选择一:前端——高级前端——全栈——前端架构师(前端专家)

选择这条路的童鞋,最好就是技术迷,热爱前端,对技术有说不出的热情。喜欢专研,不管现在,还是将来,都乐于接受新事物新知识。

这条路的优点:一直都能呆在自己喜欢的领域,踏踏实实的敲代码,薪水也能不断提高。

这条路的缺点:30多岁还要各种敲代码,难免要被其他人管着,疲于各种公司的需求。

选择二:前端——高级前端——前端主管——前端经理

这条路,可能是大部分前端,都渴望走的路,都会理所当然的以为自己以后会走上的路。这个时候问题来了?哪里来这么多的前端主管和前端经理给你啊?

这条路的优点:一步一脚印,人生不断往上爬。成为高富帅,赢取白富美,登上事业的高峰。

这条路的缺点:就拿广州来说,不要说前端经理,就是前端主管这个职位,估计也没有多少公司是存在的。很多人上到前端经理也算到顶了。这里是想说明一点,路是有的,但是选择很少。万一有一天你要跳槽了,你真的不一定能找到下一间公司,又能当会前端主管的。 我所在的公司,当得上主管或者组长这个职位的人,真的两只手就可以数完。

ps:本人其实也想走这条路,但是我很唠叨的再强调一遍,30几岁之后,你未必能找到喜欢的公司的这个职位。僧多粥少啊。最后的结果会沦为,继续当码农。

选择三:前端——高级前端——转后台——高级后台——后台经理

这也是不少有实力的前端走的一条路。毕竟,在大多的公司,在大多的时候,都是后台统领着前台。说一句不好听的话,前端是一个习惯被领导的职位。 后台引导统筹项目的开发,估计大家都看得多了。前端统领后台,统筹项目开发你听过没有(除了张云龙)? 很少。至少我是没接触过的。

这条路的优点:华丽转岗,前后通杀,也能走出一辈子码农的死循环,当上经理,做管理层。

这条路的缺点:前端转后台,这明显不是一条好走的路,需要熬很多苦,学很多后台的东西,再慢慢成长起来。简单概括就是成本高,前期很辛苦。熬过了,上路了,就有机会走上更高的台阶;熬不过了,浪费了青春,继续当个二流的后台开发,继续码农。

选择四:前端——高级前端——转产品——产品经理——高级产品经理

这条路本人觉得也是一条不错的出路。在这个最好又最坏的年代,人人都是产品经理。在前端界打滚了这么多年,自然有不少产品的基础和思想。所以前端转产品,也是一条相对不会很吃力的路。

这条路的优点:有一定的基础,产品经理需求量大,以后的选择很多。

这条路的缺点:半路出家,前期也会很吃力地转型,转产品需要自身很有想法。懒于思考的人儿不适合。

选择五:前端——高级前端——其他行业,创业等等

这条路就是现在的我,总是憧憬着以后有一份不错的生意,然后有白富美,有车有楼,财务自由的一条路。

这条路的优点:未知性很大,不用再整天敲代码,可能还真的很赚钱。

这条路的缺点:正因为未知性太大,所以前途未卜。选择走这条路的童鞋,要早早地想好要干什么,干的事情需要具备什么技能,趁早学。

总结:学无止境,祝大家都能突破自己的瓶颈。可能还有其他的路,欢迎补充。 ps:以上所说带有强烈的个人主观意愿,可能有失客观事实,望体谅。

作者:Alone381
来源:juejin.cn/post/6844903615681806344

收起阅读 »